teamshare-bridge 0.26.11 → 0.26.13

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.
Files changed (49) hide show
  1. package/dist/bridge/local-server.js +6 -10
  2. package/dist/bridge/local-server.js.map +1 -1
  3. package/dist/cli/commands/research.js +15 -0
  4. package/dist/cli/commands/research.js.map +1 -1
  5. package/dist/lib/browser/batch.d.ts +47 -2
  6. package/dist/lib/browser/batch.js +48 -5
  7. package/dist/lib/browser/batch.js.map +1 -1
  8. package/dist/lib/browser/browse-cdp.js +5 -1
  9. package/dist/lib/browser/browse-cdp.js.map +1 -1
  10. package/dist/lib/browser/browse-editor.js +8 -0
  11. package/dist/lib/browser/browse-editor.js.map +1 -1
  12. package/dist/lib/browser/cdp.d.ts +2 -0
  13. package/dist/lib/browser/cdp.js +8 -0
  14. package/dist/lib/browser/cdp.js.map +1 -1
  15. package/dist/lib/browser/index.d.ts +3 -1
  16. package/dist/lib/browser/index.js +12 -1
  17. package/dist/lib/browser/index.js.map +1 -1
  18. package/dist/lib/browser/login.d.ts +67 -25
  19. package/dist/lib/browser/login.js +681 -174
  20. package/dist/lib/browser/login.js.map +1 -1
  21. package/dist/lib/browser/manager.d.ts +26 -0
  22. package/dist/lib/browser/manager.js +48 -0
  23. package/dist/lib/browser/manager.js.map +1 -1
  24. package/dist/lib/browser/network-log.d.ts +9 -0
  25. package/dist/lib/browser/network-log.js +21 -0
  26. package/dist/lib/browser/network-log.js.map +1 -1
  27. package/dist/lib/browser/page-context.d.ts +176 -0
  28. package/dist/lib/browser/page-context.js +637 -0
  29. package/dist/lib/browser/page-context.js.map +1 -0
  30. package/dist/lib/browser/page-events.d.ts +39 -0
  31. package/dist/lib/browser/page-events.js +128 -0
  32. package/dist/lib/browser/page-events.js.map +1 -0
  33. package/dist/lib/browser/screen-change.js +3 -0
  34. package/dist/lib/browser/screen-change.js.map +1 -1
  35. package/dist/lib/browser/session-pool.d.ts +44 -0
  36. package/dist/lib/browser/session-pool.js +128 -0
  37. package/dist/lib/browser/session-pool.js.map +1 -0
  38. package/dist/lib/browser/session.d.ts +115 -1
  39. package/dist/lib/browser/session.js +355 -2
  40. package/dist/lib/browser/session.js.map +1 -1
  41. package/dist/lib/config.js +25 -4
  42. package/dist/lib/config.js.map +1 -1
  43. package/dist/lib/orchestrator/handlers/browser.js +283 -40
  44. package/dist/lib/orchestrator/handlers/browser.js.map +1 -1
  45. package/dist/lib/tools/browser-tool.d.ts +2 -2
  46. package/dist/lib/tools/browser-tool.js +58 -19
  47. package/dist/lib/tools/browser-tool.js.map +1 -1
  48. package/package.json +1 -1
  49. package/templates/common/AGENTS.md.tpl +19 -19
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Persistent page-event buffer for the debug browser (2026-10).
3
+ *
4
+ * WHY THIS EXISTS. The auto-answer browser scrapes toasts/errors at capture
5
+ * time, and the debug browser had NOTHING: a toast that appeared after a form
6
+ * submit and vanished two seconds later was invisible to an agent that only
7
+ * looked on its next tool call. In the live Soludesks QA run the agent spent
8
+ * minutes re-reading the DOM for a dialog/error that had already been shown.
9
+ *
10
+ * This module injects a tiny, idempotent MutationObserver into the page that
11
+ * records toast / alert / status / dialog events in a bounded ring buffer
12
+ * (`window.__tsPageEvents`). It is installed:
13
+ * - immediately when a session connects,
14
+ * - on every new document via `Page.addScriptToEvaluateOnNewDocument`,
15
+ * - again after a main-frame navigation (belt and braces).
16
+ *
17
+ * The script is dependency-free and returns a string so it can be injected
18
+ * with `Runtime.evaluate` or registered as a new-document script. It never
19
+ * reads field values and never logs anything.
20
+ */
21
+ /** Maximum events retained in the page ring buffer (oldest dropped). */
22
+ export declare const PAGE_EVENT_LIMIT = 80;
23
+ export interface PageEvent {
24
+ /** toast | alert | status | error | dialog | dialog-closed */
25
+ type: string;
26
+ text: string;
27
+ /** epoch ms, page clock */
28
+ at: number;
29
+ role?: string;
30
+ count?: number;
31
+ }
32
+ /**
33
+ * Build the injected page-event script.
34
+ *
35
+ * IMPORTANT: regex escapes must be DOUBLE-escaped inside this template literal
36
+ * (`\\s`), otherwise the injected JS receives `s` (the exact bug the bridge lint
37
+ * gate found in another injected script).
38
+ */
39
+ export declare function buildPageEventScript(): string;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * Persistent page-event buffer for the debug browser (2026-10).
4
+ *
5
+ * WHY THIS EXISTS. The auto-answer browser scrapes toasts/errors at capture
6
+ * time, and the debug browser had NOTHING: a toast that appeared after a form
7
+ * submit and vanished two seconds later was invisible to an agent that only
8
+ * looked on its next tool call. In the live Soludesks QA run the agent spent
9
+ * minutes re-reading the DOM for a dialog/error that had already been shown.
10
+ *
11
+ * This module injects a tiny, idempotent MutationObserver into the page that
12
+ * records toast / alert / status / dialog events in a bounded ring buffer
13
+ * (`window.__tsPageEvents`). It is installed:
14
+ * - immediately when a session connects,
15
+ * - on every new document via `Page.addScriptToEvaluateOnNewDocument`,
16
+ * - again after a main-frame navigation (belt and braces).
17
+ *
18
+ * The script is dependency-free and returns a string so it can be injected
19
+ * with `Runtime.evaluate` or registered as a new-document script. It never
20
+ * reads field values and never logs anything.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.PAGE_EVENT_LIMIT = void 0;
24
+ exports.buildPageEventScript = buildPageEventScript;
25
+ /** Maximum events retained in the page ring buffer (oldest dropped). */
26
+ exports.PAGE_EVENT_LIMIT = 80;
27
+ /**
28
+ * Build the injected page-event script.
29
+ *
30
+ * IMPORTANT: regex escapes must be DOUBLE-escaped inside this template literal
31
+ * (`\\s`), otherwise the injected JS receives `s` (the exact bug the bridge lint
32
+ * gate found in another injected script).
33
+ */
34
+ function buildPageEventScript() {
35
+ return `
36
+ (() => {
37
+ if (window.__tsEventInstalled) return 'already';
38
+ window.__tsEventInstalled = true;
39
+ const MAX = ${exports.PAGE_EVENT_LIMIT};
40
+ const events = [];
41
+ window.__tsPageEvents = events;
42
+ const clean = (v) => String(v == null ? '' : v).replace(/\\s+/g, ' ').trim().slice(0, 500);
43
+ const now = () => Date.now();
44
+ const vis = (el) => {
45
+ if (!el || el.nodeType !== 1) return false;
46
+ const r = el.getBoundingClientRect();
47
+ if (r.width === 0 || r.height === 0) return false;
48
+ const cs = getComputedStyle(el);
49
+ return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
50
+ };
51
+ const push = (type, text, extra) => {
52
+ const t = clean(text);
53
+ if (!t || t.length < 2) return;
54
+ const last = events[events.length - 1];
55
+ if (last && last.type === type && last.text === t && now() - last.at < 1200) return;
56
+ const row = { type: type, text: t, at: now() };
57
+ if (extra) { if (extra.role) row.role = extra.role; if (extra.count != null) row.count = extra.count; }
58
+ events.push(row);
59
+ if (events.length > MAX) events.splice(0, events.length - MAX);
60
+ };
61
+ const TOAST_SEL = '[role="alert"],[role="status"],[aria-live],[data-sonner-toast],' +
62
+ '[class*="toast"],[class*="Toast"],[class*="snackbar"],[class*="Snackbar"],' +
63
+ '[class*="notification"],[class*="Notification"],[class*="alert"],[class*="Alert"],' +
64
+ '[class*="banner"],[class*="Banner"]';
65
+ const kindOf = (el) => {
66
+ const role = String((el.getAttribute && el.getAttribute('role')) || '').toLowerCase();
67
+ const cls = String((el.className && typeof el.className === 'string') ? el.className : '') + ' ' + String(el.id || '');
68
+ const live = String((el.getAttribute && el.getAttribute('aria-live')) || '').toLowerCase();
69
+ if (role === 'alert' || /error|danger|invalid/i.test(cls)) return 'error';
70
+ if (/toast|snackbar|notification/i.test(cls)) return 'toast';
71
+ if (role === 'status' || live === 'polite' || live === 'assertive') return 'status';
72
+ if (/alert|banner/i.test(cls)) return 'alert';
73
+ return null;
74
+ };
75
+ const record = (el) => {
76
+ if (!el || el.nodeType !== 1 || !vis(el)) return;
77
+ // A container that holds a nested toast should not be recorded as a
78
+ // second, duplicate event; use the deepest matching node.
79
+ const nested = el.querySelector && el.querySelector(TOAST_SEL);
80
+ if (nested && nested !== el) return;
81
+ const kind = kindOf(el);
82
+ if (!kind) return;
83
+ const text = el.innerText || el.textContent || el.getAttribute('aria-label') || '';
84
+ push(kind, text, { role: String((el.getAttribute && el.getAttribute('role')) || '') });
85
+ };
86
+ const scan = (root) => {
87
+ if (!root || root.nodeType !== 1) return;
88
+ if (root.matches && root.matches(TOAST_SEL)) record(root);
89
+ if (root.querySelectorAll) {
90
+ const nodes = root.querySelectorAll(TOAST_SEL);
91
+ for (let i = 0; i < nodes.length && i < 12; i++) record(nodes[i]);
92
+ }
93
+ };
94
+ const dialogs = () => [...document.querySelectorAll('dialog[open],[role="dialog"],[aria-modal="true"]')].filter(vis);
95
+ let dialogCount = dialogs().length;
96
+ const topDialogText = () => {
97
+ const list = dialogs();
98
+ const el = list[list.length - 1];
99
+ return el ? clean(el.innerText || el.textContent || '') : '';
100
+ };
101
+ const obs = new MutationObserver((mutations) => {
102
+ for (const m of mutations) {
103
+ if (m.type === 'childList') {
104
+ for (const n of m.addedNodes) if (n && n.nodeType === 1) scan(n);
105
+ } else if (m.type === 'characterData') {
106
+ const p = m.target && m.target.parentElement;
107
+ if (p) scan(p);
108
+ }
109
+ }
110
+ const count = dialogs().length;
111
+ if (count !== dialogCount) {
112
+ if (count > 0) push('dialog', topDialogText(), { count: count });
113
+ else push('dialog-closed', 'dialog closed', { count: 0 });
114
+ dialogCount = count;
115
+ }
116
+ });
117
+ const root = document.documentElement || document.body;
118
+ if (root) {
119
+ obs.observe(root, { childList: true, subtree: true, characterData: true });
120
+ // Record anything already on screen once (a login modal that opened
121
+ // before this script ran).
122
+ scan(document.body || root);
123
+ }
124
+ return 'ok';
125
+ })()
126
+ `;
127
+ }
128
+ //# sourceMappingURL=page-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-events.js","sourceRoot":"","sources":["../../../src/lib/browser/page-events.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;;AAsBH,oDA6FC;AAjHD,wEAAwE;AAC3D,QAAA,gBAAgB,GAAG,EAAE,CAAC;AAYnC;;;;;;GAMG;AACH,SAAgB,oBAAoB;IAClC,OAAO;;;;oBAIW,wBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuFjC,CAAC;AACJ,CAAC"}
@@ -117,6 +117,9 @@ function buildObserverScript() {
117
117
  });
118
118
  lastText = getText();
119
119
  window[KEY] = { observer: obs, changed: 0, mutated: 0 };
120
+ // String-addressable alias for OTHER injected scripts (the debug
121
+ // browser's waitFor reads it; a Symbol is not reachable by name).
122
+ window.__tsScreenObs = window[KEY];
120
123
  obs.observe(document.body || document.documentElement, {
121
124
  childList: true, subtree: true, characterData: true
122
125
  });
@@ -1 +1 @@
1
- {"version":3,"file":"screen-change.js","sourceRoot":"","sources":["../../../src/lib/browser/screen-change.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;;AAcH,kDAkBC;AAOD,sDAIC;AAQD,kDA8DC;AA/GD,0EAA0E;AAC7D,QAAA,uBAAuB,GAAG,EAAE,CAAC;AAE1C;;;;;;;;GAQG;AACH,SAAgB,mBAAmB,CACjC,QAAgB,EAChB,QAAgB;IAEhB,MAAM,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACxE,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACpC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACpB,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,MAAM;gBAAE,OAAO,EAAE,CAAC;;gBACpD,OAAO,EAAE,CAAC;QACjB,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC/D,OAAO,KAAK,GAAG,EAAE,GAAG,OAAO,GAAG,EAAE,GAAG,OAAO,GAAG,CAAC,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,SAAgB,qBAAqB,CACnC,MAA4D;IAE5D,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC;AACnD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,mBAAmB;IACjC,OAAO;;;;;sCAK6B,mBAAmB,CAAC,QAAQ,EAAE;;;;;;;;;;;;;;;;qDAgBf,+BAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAuCvE,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"screen-change.js","sourceRoot":"","sources":["../../../src/lib/browser/screen-change.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;;AAcH,kDAkBC;AAOD,sDAIC;AAQD,kDAiEC;AAlHD,0EAA0E;AAC7D,QAAA,uBAAuB,GAAG,EAAE,CAAC;AAE1C;;;;;;;;GAQG;AACH,SAAgB,mBAAmB,CACjC,QAAgB,EAChB,QAAgB;IAEhB,MAAM,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACxE,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACpC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACpB,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,MAAM;gBAAE,OAAO,EAAE,CAAC;;gBACpD,OAAO,EAAE,CAAC;QACjB,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC/D,OAAO,KAAK,GAAG,EAAE,GAAG,OAAO,GAAG,EAAE,GAAG,OAAO,GAAG,CAAC,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,SAAgB,qBAAqB,CACnC,MAA4D;IAE5D,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC;AACnD,CAAC;AAED;;;;;GAKG;AACH,SAAgB,mBAAmB;IACjC,OAAO;;;;;sCAK6B,mBAAmB,CAAC,QAAQ,EAAE;;;;;;;;;;;;;;;;qDAgBf,+BAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0CvE,CAAC;AACN,CAAC"}
@@ -0,0 +1,44 @@
1
+ /** 9222 belongs to the auto-answer browser; the debug pool never uses it. */
2
+ export declare const AUTO_ANSWER_PORT = 9222;
3
+ /** Normalize an explicit session name, or null when it carries nothing. */
4
+ export declare function normalizeSessionKey(raw: unknown): string | null;
5
+ /**
6
+ * The session key a command runs against.
7
+ *
8
+ * Explicit name > the caller's identity (`agent:<userId>`), so two agents on one
9
+ * PC never collide and a later action without a sessionId still lands on the
10
+ * browser its own `browser_open` created.
11
+ */
12
+ export declare function deriveSessionKey(explicit: unknown, agentUserId: string | null | undefined): string;
13
+ /** Filesystem-safe, collision-resistant slug for a session's profile dir. */
14
+ export declare function sessionSlug(key: string): string;
15
+ /**
16
+ * The first free debug port at or above `preferred`.
17
+ *
18
+ * Returns null when the whole span is taken (a machine running that many
19
+ * browsers needs a human, not a guess).
20
+ */
21
+ export declare function pickDebugPort(preferred: number, used: Iterable<number>, span?: number): number | null;
22
+ export interface ProfileChoice {
23
+ dir: string;
24
+ /** True when this session got the shared debug profile. */
25
+ shared: boolean;
26
+ /** Set when a NAMED profile is already owned by another live session. */
27
+ conflict?: string;
28
+ }
29
+ /**
30
+ * Pick a user-data-dir for a session.
31
+ *
32
+ * `baseDir` is the shared debug profile (`~/.teamshare/chrome-profile-debug`).
33
+ * The FIRST live session keeps it (so logins persist exactly as before); any
34
+ * additional concurrent session gets `…-sessions/<slug>`, because Chrome cannot
35
+ * run two processes on one user-data-dir. A caller-supplied named profile is
36
+ * exclusive: a second session asking for the same name is refused rather than
37
+ * silently given different cookies.
38
+ */
39
+ export declare function pickDebugProfileDir(opts: {
40
+ baseDir: string;
41
+ sessionKey: string;
42
+ usedDirs: Iterable<string>;
43
+ namedDir?: string;
44
+ }): ProfileChoice;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AUTO_ANSWER_PORT = void 0;
4
+ exports.normalizeSessionKey = normalizeSessionKey;
5
+ exports.deriveSessionKey = deriveSessionKey;
6
+ exports.sessionSlug = sessionSlug;
7
+ exports.pickDebugPort = pickDebugPort;
8
+ exports.pickDebugProfileDir = pickDebugProfileDir;
9
+ /**
10
+ * Browser session identity + port/profile allocation (2026-10).
11
+ *
12
+ * WHY THIS EXISTS. Every browser command used to default to ONE session named
13
+ * `'default'`, so two QA runs on the same PC shared a single Chrome: the second
14
+ * `browser_open` silently attached to the first run's browser, and a
15
+ * `browser_close` could release the other run's session. The backend cannot fix
16
+ * this alone - the bridge owns the Chrome processes and the CDP ports - so the
17
+ * bridge derives a session per caller identity and allocates a free debug port
18
+ * + a separate profile when a second browser is genuinely needed.
19
+ *
20
+ * The rules:
21
+ * - A session key is a NAME, never a path. Anything outside a small charset is
22
+ * replaced, and the value is bounded.
23
+ * - The caller's explicit `sessionId` wins; otherwise the agent/user identity
24
+ * is the key. Same identity ⇒ same browser (a re-open reuses it).
25
+ * - Ports: the requested debug port is used when no OTHER session owns it;
26
+ * otherwise the next free port is allocated. 9222 (auto-answer) is never
27
+ * chosen.
28
+ * - Profiles: two Chromes cannot share a user-data-dir (Chrome's
29
+ * SingletonLock forwards to the running instance), so the first session gets
30
+ * the shared debug profile and each extra concurrent session gets its own
31
+ * persistent profile directory. Named profiles stay exclusive.
32
+ *
33
+ * Everything here is pure so it can be unit-tested without Chrome.
34
+ */
35
+ const node_crypto_1 = require("node:crypto");
36
+ const node_path_1 = require("node:path");
37
+ /** 9222 belongs to the auto-answer browser; the debug pool never uses it. */
38
+ exports.AUTO_ANSWER_PORT = 9222;
39
+ const MAX_SESSION_KEY = 80;
40
+ const PORT_SPAN = 20;
41
+ /** Normalize an explicit session name, or null when it carries nothing. */
42
+ function normalizeSessionKey(raw) {
43
+ if (typeof raw !== 'string')
44
+ return null;
45
+ const trimmed = raw.trim();
46
+ if (!trimmed)
47
+ return null;
48
+ const clean = trimmed
49
+ .replace(/[^a-zA-Z0-9:_.-]+/g, '-')
50
+ .replace(/-{2,}/g, '-')
51
+ .replace(/^[-.]+|[-.]+$/g, '')
52
+ .slice(0, MAX_SESSION_KEY);
53
+ return clean || null;
54
+ }
55
+ /**
56
+ * The session key a command runs against.
57
+ *
58
+ * Explicit name > the caller's identity (`agent:<userId>`), so two agents on one
59
+ * PC never collide and a later action without a sessionId still lands on the
60
+ * browser its own `browser_open` created.
61
+ */
62
+ function deriveSessionKey(explicit, agentUserId) {
63
+ const named = normalizeSessionKey(explicit);
64
+ if (named)
65
+ return named;
66
+ const user = normalizeSessionKey(agentUserId);
67
+ return user ? `agent:${user}` : 'default';
68
+ }
69
+ /** Filesystem-safe, collision-resistant slug for a session's profile dir. */
70
+ function sessionSlug(key) {
71
+ const safe = key
72
+ .toLowerCase()
73
+ .replace(/[^a-z0-9]+/g, '-')
74
+ .replace(/^-+|-+$/g, '')
75
+ .slice(0, 40);
76
+ const hash = (0, node_crypto_1.createHash)('sha1').update(key).digest('hex').slice(0, 8);
77
+ return `${safe || 'session'}-${hash}`;
78
+ }
79
+ /**
80
+ * The first free debug port at or above `preferred`.
81
+ *
82
+ * Returns null when the whole span is taken (a machine running that many
83
+ * browsers needs a human, not a guess).
84
+ */
85
+ function pickDebugPort(preferred, used, span = PORT_SPAN) {
86
+ const taken = new Set(used);
87
+ if (preferred !== exports.AUTO_ANSWER_PORT && !taken.has(preferred))
88
+ return preferred;
89
+ for (let i = 1; i <= span; i++) {
90
+ const candidate = preferred + i;
91
+ if (candidate > 65535)
92
+ break;
93
+ if (candidate === exports.AUTO_ANSWER_PORT)
94
+ continue;
95
+ if (!taken.has(candidate))
96
+ return candidate;
97
+ }
98
+ return null;
99
+ }
100
+ /**
101
+ * Pick a user-data-dir for a session.
102
+ *
103
+ * `baseDir` is the shared debug profile (`~/.teamshare/chrome-profile-debug`).
104
+ * The FIRST live session keeps it (so logins persist exactly as before); any
105
+ * additional concurrent session gets `…-sessions/<slug>`, because Chrome cannot
106
+ * run two processes on one user-data-dir. A caller-supplied named profile is
107
+ * exclusive: a second session asking for the same name is refused rather than
108
+ * silently given different cookies.
109
+ */
110
+ function pickDebugProfileDir(opts) {
111
+ const key = (p) => process.platform === 'win32' ? (0, node_path_1.resolve)(p).toLowerCase() : (0, node_path_1.resolve)(p);
112
+ const used = new Set([...opts.usedDirs].map(key));
113
+ if (opts.namedDir) {
114
+ if (used.has(key(opts.namedDir))) {
115
+ return { dir: opts.namedDir, shared: false, conflict: opts.namedDir };
116
+ }
117
+ return { dir: opts.namedDir, shared: false };
118
+ }
119
+ if (!used.has(key(opts.baseDir))) {
120
+ return { dir: opts.baseDir, shared: true };
121
+ }
122
+ const dir = (0, node_path_1.join)((0, node_path_1.dirname)(opts.baseDir), `${(0, node_path_1.basename)(opts.baseDir)}-sessions`, sessionSlug(opts.sessionKey));
123
+ if (used.has(key(dir))) {
124
+ return { dir, shared: false, conflict: dir };
125
+ }
126
+ return { dir, shared: false };
127
+ }
128
+ //# sourceMappingURL=session-pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-pool.js","sourceRoot":"","sources":["../../../src/lib/browser/session-pool.ts"],"names":[],"mappings":";;;AAoCA,kDAUC;AASD,4CAQC;AAGD,kCAQC;AAQD,sCAcC;AAoBD,kDA8BC;AAlJD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,6CAAyC;AACzC,yCAA6D;AAE7D,6EAA6E;AAChE,QAAA,gBAAgB,GAAG,IAAI,CAAC;AAErC,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,MAAM,SAAS,GAAG,EAAE,CAAC;AAErB,2EAA2E;AAC3E,SAAgB,mBAAmB,CAAC,GAAY;IAC9C,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,MAAM,KAAK,GAAG,OAAO;SAClB,OAAO,CAAC,oBAAoB,EAAE,GAAG,CAAC;SAClC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC;SAC7B,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC;IAC7B,OAAO,KAAK,IAAI,IAAI,CAAC;AACvB,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAC9B,QAAiB,EACjB,WAAsC;IAEtC,MAAM,KAAK,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAC5C,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC;IACxB,MAAM,IAAI,GAAG,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC9C,OAAO,IAAI,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5C,CAAC;AAED,6EAA6E;AAC7E,SAAgB,WAAW,CAAC,GAAW;IACrC,MAAM,IAAI,GAAG,GAAG;SACb,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAChB,MAAM,IAAI,GAAG,IAAA,wBAAU,EAAC,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtE,OAAO,GAAG,IAAI,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;AACxC,CAAC;AAED;;;;;GAKG;AACH,SAAgB,aAAa,CAC3B,SAAiB,EACjB,IAAsB,EACtB,IAAI,GAAG,SAAS;IAEhB,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS,IAAI,CAAC,CAAC;IACpC,IAAI,SAAS,KAAK,wBAAgB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9E,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,MAAM,SAAS,GAAG,SAAS,GAAG,CAAC,CAAC;QAChC,IAAI,SAAS,GAAG,KAAK;YAAE,MAAM;QAC7B,IAAI,SAAS,KAAK,wBAAgB;YAAE,SAAS;QAC7C,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC9C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAUD;;;;;;;;;GASG;AACH,SAAgB,mBAAmB,CAAC,IAKnC;IACC,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CACxB,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,IAAA,mBAAO,EAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAA,mBAAO,EAAC,CAAC,CAAC,CAAC;IACvE,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAElD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;YACjC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACxE,CAAC;QACD,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC/C,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;QACjC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC7C,CAAC;IAED,MAAM,GAAG,GAAG,IAAA,gBAAI,EACd,IAAA,mBAAO,EAAC,IAAI,CAAC,OAAO,CAAC,EACrB,GAAG,IAAA,oBAAQ,EAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EACpC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAC7B,CAAC;IACF,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;IAC/C,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAChC,CAAC"}
@@ -2,6 +2,8 @@ import type { CdpPage, ElementInfo, PageInfo, PageText, PageLink } from './cdp';
2
2
  import { type NetworkEntry, type ConsoleEntry } from './network-log';
3
3
  import { type ReadStagedResult, type FillExcelResult } from './xlsx-template';
4
4
  import { type LoginResult, type PerformLoginOptions } from './login';
5
+ import { type NativeDialogState, type PageSnapshot, type WaitForKind } from './page-context';
6
+ import { type PageEvent } from './page-events';
5
7
  /**
6
8
  * Persistent Chrome profile for the AUTO-ANSWER browser (research reads,
7
9
  * `browse --auto-answer`, the web-auto-answer tool).
@@ -78,13 +80,36 @@ export declare class BrowserSession {
78
80
  * { action: "networkLog" }` when it needs to prove an API mismatch.
79
81
  */
80
82
  private network;
83
+ /**
84
+ * The native alert/confirm/prompt/beforeunload currently blocking the page
85
+ * (2026-10). Captured from `Page.javascriptDialogOpening`; other actions are
86
+ * refused while it is set, and `browser_action({action:"dialog"})` resolves
87
+ * it. A native dialog blocks the renderer, so a page snapshot would hang.
88
+ */
89
+ private _nativeDialog;
81
90
  constructor(id: string, opts?: BrowserSessionOptions);
91
+ /** The stable session name the browser manager keys this session by. */
92
+ get sessionKey(): string;
93
+ /** The user-data-dir this session launches Chrome with. */
94
+ get profileDir(): string;
95
+ /**
96
+ * Reconfigure a DISCONNECTED session before reconnecting it (2026-10).
97
+ *
98
+ * The manager keeps one session per identity; a re-open after the browser died
99
+ * may need a different port (another agent took the old one) or a different
100
+ * profile (the shared debug profile is now owned by a concurrent session).
101
+ * Only launch-time options are touched - an open connection is never
102
+ * reconfigured underneath its page.
103
+ */
104
+ configure(opts: BrowserSessionOptions): void;
82
105
  /**
83
106
  * Toggle credential entry for this session. Persists across actions - a
84
107
  * `browser_open` sets it once and later `type`/`login` calls honour it.
85
108
  */
86
109
  setAllowCredentials(allow: boolean): void;
87
110
  get allowCredentials(): boolean;
111
+ /** The native JS dialog currently blocking the page, if any. */
112
+ get nativeDialog(): NativeDialogState | null;
88
113
  /** Last time a command actually used this session (drives idle close). */
89
114
  private _lastUsedAt;
90
115
  /** When this session was last used — the idle-close timer reads this. */
@@ -119,6 +144,37 @@ export declare class BrowserSession {
119
144
  * Connect to a specific tab by targetId.
120
145
  */
121
146
  connectToTab(targetId: string): Promise<PageInfo>;
147
+ /**
148
+ * Install the debug browser's page instrumentation (2026-10):
149
+ *
150
+ * - the persistent toast/alert/dialog ring buffer (`page-events.ts`),
151
+ * registered for FUTURE documents via
152
+ * `Page.addScriptToEvaluateOnNewDocument` and injected into the current
153
+ * one immediately;
154
+ * - the screen-change observer (`__tsScreenObs.mutated`) so `waitFor
155
+ * {for:"change"}` knows a click had an effect without a blind sleep;
156
+ * - the native-dialog listener (`Page.javascriptDialogOpening`), because an
157
+ * alert/confirm/prompt blocks the renderer and must be surfaced and
158
+ * resolved explicitly.
159
+ *
160
+ * All pieces are idempotent in the page. The CDP listeners are bound once per
161
+ * page object (a reconnect creates a new one).
162
+ */
163
+ private instrumentPage;
164
+ /**
165
+ * Resolve a native JS dialog (alert/confirm/prompt/beforeunload).
166
+ * `accept:false` dismisses it; `promptText` is used by a prompt.
167
+ */
168
+ dialogAction(opts: {
169
+ accept?: boolean;
170
+ promptText?: string;
171
+ }): Promise<{
172
+ ok: boolean;
173
+ accepted?: boolean;
174
+ type?: string;
175
+ message?: string;
176
+ error?: string;
177
+ }>;
122
178
  /**
123
179
  * Navigate to a URL. Returns page info after navigation.
124
180
  */
@@ -153,7 +209,9 @@ export declare class BrowserSession {
153
209
  * credential for the run - the debug browser is the QA surface, the
154
210
  * auto-answer loop is a separate path that still never touches credentials.
155
211
  */
156
- type(text: string): Promise<void>;
212
+ type(text: string, opts?: {
213
+ clearFirst?: boolean;
214
+ }): Promise<void>;
157
215
  /**
158
216
  * Deterministically fill a login form (username + password) and submit.
159
217
  * Handles the common two-step email -> Continue -> password flow. Requires
@@ -297,6 +355,62 @@ export declare class BrowserSession {
297
355
  * Get current page info.
298
356
  */
299
357
  pageInfo(): Promise<PageInfo>;
358
+ /**
359
+ * Full page state (2026-10): dialogs (topmost with buttons/fields), toasts
360
+ * (persistent buffer), visible errors, form fields (lengths only), focus,
361
+ * busy, headings, optional page text, pending requests and console errors.
362
+ *
363
+ * This is the debug browser's equivalent of the auto-answer loop's rich
364
+ * `extractPageContext` - and it is what lets an agent see a login modal
365
+ * instead of guessing with raw `evaluate`.
366
+ */
367
+ snapshot(opts?: {
368
+ includeText?: boolean;
369
+ maxTextChars?: number;
370
+ maxFields?: number;
371
+ }): Promise<PageSnapshot>;
372
+ /**
373
+ * Bounded state attached to every mutating action result. Never include the
374
+ * page text (that is what `snapshot` is for); a failure returns null so an
375
+ * action result is never failed by its own observation.
376
+ */
377
+ compactSnapshot(): Promise<Record<string, unknown> | null>;
378
+ /**
379
+ * Recent toast / alert / dialog events from the persistent page buffer.
380
+ * This is how a toast that disappeared before the next tool call is still
381
+ * visible.
382
+ */
383
+ events(opts?: {
384
+ limit?: number;
385
+ clear?: boolean;
386
+ since?: number;
387
+ }): Promise<{
388
+ events: PageEvent[];
389
+ count: number;
390
+ }>;
391
+ /**
392
+ * Wait until a page condition is true, then return the current screen.
393
+ *
394
+ * This replaces the manual `evaluate` polling loop the QA agent used to run
395
+ * (and which broke on "Inspected target navigated"). On timeout it returns
396
+ * `matched:false` WITH the current screen, so the caller can see why.
397
+ */
398
+ waitFor(opts: {
399
+ for: WaitForKind;
400
+ selector?: string;
401
+ text?: string;
402
+ urlIncludes?: string;
403
+ state?: 'open' | 'closed';
404
+ loginPhase?: 'username' | 'password' | 'otp' | 'done';
405
+ timeoutMs?: number;
406
+ pollMs?: number;
407
+ }): Promise<{
408
+ matched: boolean;
409
+ elapsedMs: number;
410
+ for: WaitForKind;
411
+ screen: Record<string, unknown> | null;
412
+ nativeDialog: NativeDialogState | null;
413
+ }>;
300
414
  /**
301
415
  * Is the page actually usable right now? (2026-10)
302
416
  *