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.
- package/dist/bridge/local-server.js +6 -10
- package/dist/bridge/local-server.js.map +1 -1
- package/dist/cli/commands/research.js +15 -0
- package/dist/cli/commands/research.js.map +1 -1
- package/dist/lib/browser/batch.d.ts +47 -2
- package/dist/lib/browser/batch.js +48 -5
- package/dist/lib/browser/batch.js.map +1 -1
- package/dist/lib/browser/browse-cdp.js +5 -1
- package/dist/lib/browser/browse-cdp.js.map +1 -1
- package/dist/lib/browser/browse-editor.js +8 -0
- package/dist/lib/browser/browse-editor.js.map +1 -1
- package/dist/lib/browser/cdp.d.ts +2 -0
- package/dist/lib/browser/cdp.js +8 -0
- package/dist/lib/browser/cdp.js.map +1 -1
- package/dist/lib/browser/index.d.ts +3 -1
- package/dist/lib/browser/index.js +12 -1
- package/dist/lib/browser/index.js.map +1 -1
- package/dist/lib/browser/login.d.ts +67 -25
- package/dist/lib/browser/login.js +681 -174
- package/dist/lib/browser/login.js.map +1 -1
- package/dist/lib/browser/manager.d.ts +26 -0
- package/dist/lib/browser/manager.js +48 -0
- package/dist/lib/browser/manager.js.map +1 -1
- package/dist/lib/browser/network-log.d.ts +9 -0
- package/dist/lib/browser/network-log.js +21 -0
- package/dist/lib/browser/network-log.js.map +1 -1
- package/dist/lib/browser/page-context.d.ts +176 -0
- package/dist/lib/browser/page-context.js +637 -0
- package/dist/lib/browser/page-context.js.map +1 -0
- package/dist/lib/browser/page-events.d.ts +39 -0
- package/dist/lib/browser/page-events.js +128 -0
- package/dist/lib/browser/page-events.js.map +1 -0
- package/dist/lib/browser/screen-change.js +3 -0
- package/dist/lib/browser/screen-change.js.map +1 -1
- package/dist/lib/browser/session-pool.d.ts +44 -0
- package/dist/lib/browser/session-pool.js +128 -0
- package/dist/lib/browser/session-pool.js.map +1 -0
- package/dist/lib/browser/session.d.ts +115 -1
- package/dist/lib/browser/session.js +355 -2
- package/dist/lib/browser/session.js.map +1 -1
- package/dist/lib/config.js +25 -4
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/orchestrator/handlers/browser.js +283 -40
- package/dist/lib/orchestrator/handlers/browser.js.map +1 -1
- package/dist/lib/tools/browser-tool.d.ts +2 -2
- package/dist/lib/tools/browser-tool.js +58 -19
- package/dist/lib/tools/browser-tool.js.map +1 -1
- package/package.json +1 -1
- 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,
|
|
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":"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"}
|
|
@@ -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
|
|
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
|
*
|