agentic-browser 1.5.1 → 1.6.1
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/README.md +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.mjs +177 -49
- package/dist/{runtime-OsYo7Rh2.mjs → runtime-DUSoQl5x.mjs} +271 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -152,7 +152,7 @@ Returns a JSON array of elements with CSS selectors and fallback selectors usabl
|
|
|
152
152
|
}
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
MCP responses are compact — `visible`, `actions`, and `tagName` are omitted to reduce token usage. The full element shape is available via the programmatic API.
|
|
155
|
+
MCP responses are compact — `visible`, `actions`, and `tagName` are omitted to reduce token usage, and `enabled` is omitted when it is `true`. Responses also include a `summary` block with `countsByRole` and `primaryActions` so an LLM can identify the main controls faster. The full element shape is available via the programmatic API.
|
|
156
156
|
|
|
157
157
|
````
|
|
158
158
|
|
package/dist/cli/index.mjs
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-
|
|
2
|
+
import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-DUSoQl5x.mjs";
|
|
3
3
|
|
|
4
4
|
export { AgenticBrowserCore, createAgenticBrowserCore, createMockAgenticBrowserCore };
|
package/dist/mcp/index.mjs
CHANGED
|
@@ -1,9 +1,163 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as createAgenticBrowserCore } from "../runtime-
|
|
2
|
+
import { n as createAgenticBrowserCore } from "../runtime-DUSoQl5x.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
|
|
7
|
+
//#region src/mcp/response-helpers.ts
|
|
8
|
+
function uniqueLines(content, maxLineLength = 160) {
|
|
9
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10
|
+
const lines = [];
|
|
11
|
+
for (const raw of content.split("\n")) {
|
|
12
|
+
const line = raw.replace(/\s+/g, " ").trim();
|
|
13
|
+
if (!line || seen.has(line)) continue;
|
|
14
|
+
seen.add(line);
|
|
15
|
+
lines.push(line.slice(0, maxLineLength));
|
|
16
|
+
}
|
|
17
|
+
return lines;
|
|
18
|
+
}
|
|
19
|
+
function truncateContent(content, limit) {
|
|
20
|
+
if (!limit || content.length <= limit) return {
|
|
21
|
+
content,
|
|
22
|
+
truncated: false
|
|
23
|
+
};
|
|
24
|
+
const originalLength = content.length;
|
|
25
|
+
return {
|
|
26
|
+
content: content.slice(0, limit) + `\n\n[Truncated — showing first ${limit} of ${originalLength} characters. Use a CSS selector to scope the content.]`,
|
|
27
|
+
truncated: true
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function summarizeTextContent(content) {
|
|
31
|
+
const lines = uniqueLines(content);
|
|
32
|
+
return {
|
|
33
|
+
summaryLines: lines.slice(0, 8),
|
|
34
|
+
previewLines: lines.slice(0, 20)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function summarizeA11yContent(content) {
|
|
38
|
+
const lines = uniqueLines(content);
|
|
39
|
+
const headings = [];
|
|
40
|
+
const landmarks = [];
|
|
41
|
+
const actions = [];
|
|
42
|
+
const inputs = [];
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
const lower = line.toLowerCase();
|
|
45
|
+
if ((lower.startsWith("heading ") || lower.includes(" heading ")) && headings.length < 8) {
|
|
46
|
+
headings.push(line);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if ((lower.startsWith("main") || lower.startsWith("navigation") || lower.startsWith("banner") || lower.startsWith("complementary") || lower.startsWith("contentinfo") || lower.startsWith("search")) && landmarks.length < 8) {
|
|
50
|
+
landmarks.push(line);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if ((lower.includes("button") || lower.includes("link")) && actions.length < 12) {
|
|
54
|
+
actions.push(line);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if ((lower.includes("textbox") || lower.includes("combobox") || lower.includes("searchbox") || lower.includes("checkbox") || lower.includes("radio")) && inputs.length < 12) inputs.push(line);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
headings,
|
|
61
|
+
landmarks,
|
|
62
|
+
actions,
|
|
63
|
+
inputs,
|
|
64
|
+
previewLines: lines.slice(0, 30)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function compactPageContent(result, maxChars) {
|
|
68
|
+
const limit = maxChars ?? {
|
|
69
|
+
text: 8e3,
|
|
70
|
+
a11y: 1e4,
|
|
71
|
+
html: 4e3,
|
|
72
|
+
title: void 0
|
|
73
|
+
}[result.mode];
|
|
74
|
+
const truncated = truncateContent(result.content, limit);
|
|
75
|
+
if (result.mode === "title") return {
|
|
76
|
+
...result,
|
|
77
|
+
truncated: false
|
|
78
|
+
};
|
|
79
|
+
if (result.mode === "text") {
|
|
80
|
+
const summary = summarizeTextContent(truncated.content);
|
|
81
|
+
return {
|
|
82
|
+
mode: result.mode,
|
|
83
|
+
content: truncated.content,
|
|
84
|
+
truncated: truncated.truncated,
|
|
85
|
+
summaryLines: summary.summaryLines,
|
|
86
|
+
previewLines: summary.previewLines
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (result.mode === "a11y") {
|
|
90
|
+
const summary = summarizeA11yContent(truncated.content);
|
|
91
|
+
return {
|
|
92
|
+
mode: result.mode,
|
|
93
|
+
content: truncated.content,
|
|
94
|
+
truncated: truncated.truncated,
|
|
95
|
+
headings: summary.headings,
|
|
96
|
+
landmarks: summary.landmarks,
|
|
97
|
+
actions: summary.actions,
|
|
98
|
+
inputs: summary.inputs,
|
|
99
|
+
previewLines: summary.previewLines
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
mode: result.mode,
|
|
104
|
+
content: truncated.content,
|
|
105
|
+
truncated: truncated.truncated
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function summarizeElements(elements) {
|
|
109
|
+
const byRole = /* @__PURE__ */ new Map();
|
|
110
|
+
const primaryActions = [];
|
|
111
|
+
for (const element of elements) {
|
|
112
|
+
const role = String(element.role ?? "unknown");
|
|
113
|
+
byRole.set(role, (byRole.get(role) ?? 0) + 1);
|
|
114
|
+
if (primaryActions.length < 12 && typeof element.selector === "string" && typeof element.text === "string" && element.text) primaryActions.push({
|
|
115
|
+
role,
|
|
116
|
+
text: element.text,
|
|
117
|
+
selector: element.selector,
|
|
118
|
+
fallbackSelectors: element.fallbackSelectors
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
countsByRole: Object.fromEntries([...byRole.entries()].sort((a, b) => b[1] - a[1])),
|
|
123
|
+
primaryActions
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function compactElement(element, visibleOnly) {
|
|
127
|
+
const compact = { ...element };
|
|
128
|
+
if (visibleOnly) delete compact.visible;
|
|
129
|
+
delete compact.actions;
|
|
130
|
+
delete compact.tagName;
|
|
131
|
+
if (compact.enabled === true) delete compact.enabled;
|
|
132
|
+
if (!compact.text) delete compact.text;
|
|
133
|
+
if (compact.ariaLabel && compact.ariaLabel === compact.text) delete compact.ariaLabel;
|
|
134
|
+
if (compact.placeholder && compact.placeholder === compact.text) delete compact.placeholder;
|
|
135
|
+
return compact;
|
|
136
|
+
}
|
|
137
|
+
function compactInteractiveElementsResult(result, visibleOnly) {
|
|
138
|
+
const elements = result.elements.map((el) => compactElement(el, visibleOnly));
|
|
139
|
+
return {
|
|
140
|
+
elements,
|
|
141
|
+
totalFound: result.totalFound,
|
|
142
|
+
truncated: result.truncated,
|
|
143
|
+
summary: summarizeElements(elements)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function compactMemoryResults(results) {
|
|
147
|
+
return { results: results.map((r) => {
|
|
148
|
+
const compact = { ...r };
|
|
149
|
+
delete compact.score;
|
|
150
|
+
delete compact.lastVerifiedAt;
|
|
151
|
+
if (Array.isArray(compact.selectorAliases)) compact.selectorAliases = compact.selectorAliases.map((alias) => {
|
|
152
|
+
const compactAlias = { ...alias };
|
|
153
|
+
if (Array.isArray(compactAlias.fallbackSelectors) && compactAlias.fallbackSelectors.length === 0) delete compactAlias.fallbackSelectors;
|
|
154
|
+
return compactAlias;
|
|
155
|
+
});
|
|
156
|
+
return compact;
|
|
157
|
+
}) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
7
161
|
//#region src/mcp/index.ts
|
|
8
162
|
let core;
|
|
9
163
|
let activeSessionId;
|
|
@@ -48,9 +202,15 @@ server.tool("browser_navigate", "Navigate the browser to a URL. A session is aut
|
|
|
48
202
|
type: "navigate",
|
|
49
203
|
payload: { url }
|
|
50
204
|
});
|
|
205
|
+
let cookieBanner;
|
|
206
|
+
if (result.resultStatus === "success") try {
|
|
207
|
+
const dismissed = await getCore().dismissCookieBanner(sid);
|
|
208
|
+
if (dismissed.dismissed) cookieBanner = dismissed;
|
|
209
|
+
} catch {}
|
|
51
210
|
const compact = {
|
|
52
211
|
resultStatus: result.resultStatus,
|
|
53
|
-
resultMessage: result.resultMessage
|
|
212
|
+
resultMessage: result.resultMessage,
|
|
213
|
+
cookieBanner
|
|
54
214
|
};
|
|
55
215
|
return { content: [{
|
|
56
216
|
type: "text",
|
|
@@ -124,30 +284,9 @@ server.tool("browser_get_content", "Get the current page content. Modes: \"text\
|
|
|
124
284
|
mode,
|
|
125
285
|
selector
|
|
126
286
|
});
|
|
127
|
-
const limit = maxChars ?? {
|
|
128
|
-
text: 12e3,
|
|
129
|
-
a11y: 12e3,
|
|
130
|
-
html: 6e3,
|
|
131
|
-
title: void 0
|
|
132
|
-
}[mode];
|
|
133
|
-
if (limit && typeof result.content === "string" && result.content.length > limit) {
|
|
134
|
-
const originalLength = result.content.length;
|
|
135
|
-
const truncatedContent = result.content.slice(0, limit) + `\n\n[Truncated — showing first ${limit} of ${originalLength} characters. Use a CSS selector to scope the content.]`;
|
|
136
|
-
return { content: [{
|
|
137
|
-
type: "text",
|
|
138
|
-
text: JSON.stringify({
|
|
139
|
-
...result,
|
|
140
|
-
content: truncatedContent,
|
|
141
|
-
truncated: true
|
|
142
|
-
})
|
|
143
|
-
}] };
|
|
144
|
-
}
|
|
145
287
|
return { content: [{
|
|
146
288
|
type: "text",
|
|
147
|
-
text: JSON.stringify(
|
|
148
|
-
...result,
|
|
149
|
-
truncated: false
|
|
150
|
-
})
|
|
289
|
+
text: JSON.stringify(compactPageContent(result, maxChars))
|
|
151
290
|
}] };
|
|
152
291
|
});
|
|
153
292
|
server.tool("browser_get_elements", "Discover all interactive elements on the current page (buttons, links, inputs, etc.). Returns CSS selectors and fallbackSelectors you can use with browser_interact. Pass fallbackSelectors to browser_interact for automatic retry when the primary selector breaks. Actions are derived from role: link/button/custom→click, input/textarea/contenteditable→click+type+press, select→click+select, checkbox/radio→toggle. A session is auto-started if needed.", {
|
|
@@ -175,21 +314,9 @@ server.tool("browser_get_elements", "Discover all interactive elements on the cu
|
|
|
175
314
|
limit,
|
|
176
315
|
selector
|
|
177
316
|
});
|
|
178
|
-
const compactElements = result.elements.map((el) => {
|
|
179
|
-
const compact = { ...el };
|
|
180
|
-
if (visibleOnly) delete compact.visible;
|
|
181
|
-
delete compact.actions;
|
|
182
|
-
delete compact.tagName;
|
|
183
|
-
if (compact.ariaLabel && compact.ariaLabel === compact.text) delete compact.ariaLabel;
|
|
184
|
-
return compact;
|
|
185
|
-
});
|
|
186
317
|
return { content: [{
|
|
187
318
|
type: "text",
|
|
188
|
-
text: JSON.stringify(
|
|
189
|
-
elements: compactElements,
|
|
190
|
-
totalFound: result.totalFound,
|
|
191
|
-
truncated: result.truncated
|
|
192
|
-
})
|
|
319
|
+
text: JSON.stringify(compactInteractiveElementsResult(result, visibleOnly))
|
|
193
320
|
}] };
|
|
194
321
|
});
|
|
195
322
|
server.tool("browser_search_memory", "Search task memory for previously learned selectors, selector aliases, and interaction patterns. Results include selectorHints (proven selectors) and selectorAliases (human-readable names mapped to selectors with fallbacks). Use this before interacting with a known site to reuse proven selectors instead of rediscovering them.", {
|
|
@@ -197,24 +324,22 @@ server.tool("browser_search_memory", "Search task memory for previously learned
|
|
|
197
324
|
siteDomain: z.string().optional().describe("Domain to scope the search"),
|
|
198
325
|
limit: z.number().default(5).describe("Maximum number of results")
|
|
199
326
|
}, async ({ taskIntent, siteDomain, limit }) => {
|
|
200
|
-
const
|
|
327
|
+
const result = getCore().searchMemory({
|
|
201
328
|
taskIntent,
|
|
202
329
|
siteDomain,
|
|
203
330
|
limit
|
|
204
|
-
}).results.map((r) => {
|
|
205
|
-
const compact = { ...r };
|
|
206
|
-
delete compact.score;
|
|
207
|
-
delete compact.lastVerifiedAt;
|
|
208
|
-
if (Array.isArray(compact.selectorAliases)) compact.selectorAliases = compact.selectorAliases.map((alias) => {
|
|
209
|
-
const compactAlias = { ...alias };
|
|
210
|
-
if (Array.isArray(compactAlias.fallbackSelectors) && compactAlias.fallbackSelectors.length === 0) delete compactAlias.fallbackSelectors;
|
|
211
|
-
return compactAlias;
|
|
212
|
-
});
|
|
213
|
-
return compact;
|
|
214
331
|
});
|
|
215
332
|
return { content: [{
|
|
216
333
|
type: "text",
|
|
217
|
-
text: JSON.stringify(
|
|
334
|
+
text: JSON.stringify(compactMemoryResults(result.results))
|
|
335
|
+
}] };
|
|
336
|
+
});
|
|
337
|
+
server.tool("browser_dismiss_cookies", "Dismiss cookie consent banners on the current page. Uses the accessibility tree first (most robust), then falls back to known CSS selectors and text-based button matching. Supports banners in English, German, French, Spanish, Italian, Dutch, and Portuguese. Call this after navigating to a new page. A session is auto-started if needed.", { sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)") }, async ({ sessionId }) => {
|
|
338
|
+
const sid = await resolveSession(sessionId);
|
|
339
|
+
const result = await getCore().dismissCookieBanner(sid);
|
|
340
|
+
return { content: [{
|
|
341
|
+
type: "text",
|
|
342
|
+
text: JSON.stringify(result)
|
|
218
343
|
}] };
|
|
219
344
|
});
|
|
220
345
|
server.tool("browser_stop_session", "Stop the browser session and terminate Chrome. The next browser tool call will auto-start a fresh session.", { sessionId: z.string().optional().describe("Session ID (uses active session if omitted)") }, async ({ sessionId }) => {
|
|
@@ -245,6 +370,9 @@ async function main() {
|
|
|
245
370
|
} catch {}
|
|
246
371
|
activeSessionId = void 0;
|
|
247
372
|
}
|
|
373
|
+
try {
|
|
374
|
+
getCore().context.memoryService.flushSync();
|
|
375
|
+
} catch {}
|
|
248
376
|
try {
|
|
249
377
|
getCore().sessions.cleanupSessions({ maxAgeDays: 0 });
|
|
250
378
|
} catch {}
|
|
@@ -50,6 +50,7 @@ var CdpConnection = class CdpConnection {
|
|
|
50
50
|
return new CdpConnection(ws);
|
|
51
51
|
}
|
|
52
52
|
async send(method, params, timeoutMs = 15e3) {
|
|
53
|
+
if (this.ws.readyState !== WebSocket.OPEN) throw new Error(`WebSocket not open (state=${this.ws.readyState}), cannot send '${method}'`);
|
|
53
54
|
const id = ++this.nextId;
|
|
54
55
|
const payload = {
|
|
55
56
|
id,
|
|
@@ -106,8 +107,8 @@ var CdpConnection = class CdpConnection {
|
|
|
106
107
|
this.ws.close();
|
|
107
108
|
}
|
|
108
109
|
};
|
|
109
|
-
async function getJson(url) {
|
|
110
|
-
const response = await fetch(url);
|
|
110
|
+
async function getJson(url, timeoutMs = 5e3) {
|
|
111
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
111
112
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);
|
|
112
113
|
return await response.json();
|
|
113
114
|
}
|
|
@@ -202,9 +203,15 @@ function resolveDefaultProfileDir() {
|
|
|
202
203
|
}
|
|
203
204
|
var ChromeCdpBrowserController = class {
|
|
204
205
|
connections = /* @__PURE__ */ new Map();
|
|
206
|
+
cleanupInterval;
|
|
205
207
|
constructor(baseDir, connectionFactory = CdpConnection.connect) {
|
|
206
208
|
this.baseDir = baseDir;
|
|
207
209
|
this.connectionFactory = connectionFactory;
|
|
210
|
+
this.cleanupInterval = setInterval(() => {
|
|
211
|
+
const cutoff = Date.now() - 300 * 1e3;
|
|
212
|
+
for (const [url, entry] of this.connections) if (entry.lastUsedAt < cutoff) this.dropConnection(url);
|
|
213
|
+
}, 6e4);
|
|
214
|
+
this.cleanupInterval.unref();
|
|
208
215
|
}
|
|
209
216
|
async getConnection(targetWsUrl) {
|
|
210
217
|
const cached = this.connections.get(targetWsUrl);
|
|
@@ -354,7 +361,8 @@ var ChromeCdpBrowserController = class {
|
|
|
354
361
|
await this.ensureEnabled(targetWsUrl);
|
|
355
362
|
const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 6e3).catch(() => void 0);
|
|
356
363
|
const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
|
|
357
|
-
await conn.send("Page.navigate", { url });
|
|
364
|
+
const navResult = await conn.send("Page.navigate", { url });
|
|
365
|
+
if (navResult.errorText) throw new Error(`Navigation failed: ${navResult.errorText}`);
|
|
358
366
|
const navigatedEvent = await navigatedPromise;
|
|
359
367
|
try {
|
|
360
368
|
await loadPromise;
|
|
@@ -913,6 +921,164 @@ var ChromeCdpBrowserController = class {
|
|
|
913
921
|
}));
|
|
914
922
|
});
|
|
915
923
|
}
|
|
924
|
+
/**
|
|
925
|
+
* Best-effort cookie/consent banner dismissal.
|
|
926
|
+
*
|
|
927
|
+
* Strategy (in order):
|
|
928
|
+
* 1. A11y tree scan — find buttons with consent-like accessible names
|
|
929
|
+
* 2. CSS selector scan — check common consent framework IDs/classes
|
|
930
|
+
* 3. Text-based scan — find visible buttons matching accept/agree patterns
|
|
931
|
+
*
|
|
932
|
+
* Returns silently on failure — never blocks navigation.
|
|
933
|
+
*/
|
|
934
|
+
async dismissCookieBanner(targetWsUrl) {
|
|
935
|
+
try {
|
|
936
|
+
const domResult = await this.dismissViaDom(targetWsUrl);
|
|
937
|
+
if (domResult.dismissed) return domResult;
|
|
938
|
+
} catch {}
|
|
939
|
+
try {
|
|
940
|
+
const a11yResult = await this.dismissViaA11y(targetWsUrl);
|
|
941
|
+
if (a11yResult.dismissed) return a11yResult;
|
|
942
|
+
} catch {}
|
|
943
|
+
return { dismissed: false };
|
|
944
|
+
}
|
|
945
|
+
async dismissViaDom(targetWsUrl) {
|
|
946
|
+
const expression = `(() => {
|
|
947
|
+
// Common cookie-consent button selectors (ordered by specificity)
|
|
948
|
+
const selectors = [
|
|
949
|
+
'#onetrust-accept-btn-handler',
|
|
950
|
+
'#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll',
|
|
951
|
+
'#CybotCookiebotDialogBodyButtonAccept',
|
|
952
|
+
'#accept-cookies', '#acceptCookies',
|
|
953
|
+
'#cookie-accept', '#cookieAccept',
|
|
954
|
+
'#gdpr-accept', '#consent-accept',
|
|
955
|
+
'#uc-btn-accept-banner',
|
|
956
|
+
'.cc-accept', '.cc-btn.cc-dismiss',
|
|
957
|
+
'.cookie-consent-accept', '.cookie-accept-btn',
|
|
958
|
+
'.js-cookie-accept', '.js-accept-cookies',
|
|
959
|
+
'.consent-accept', '.consent-btn-accept',
|
|
960
|
+
'.gdpr-accept',
|
|
961
|
+
'[data-testid="cookie-accept"]',
|
|
962
|
+
'[data-action="accept-cookies"]',
|
|
963
|
+
'[data-cookiefirst-action="accept"]',
|
|
964
|
+
];
|
|
965
|
+
|
|
966
|
+
for (const sel of selectors) {
|
|
967
|
+
const el = document.querySelector(sel);
|
|
968
|
+
if (el && el.offsetParent !== null) {
|
|
969
|
+
el.click();
|
|
970
|
+
return JSON.stringify({ dismissed: true, method: 'selector', detail: sel });
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Text-based fallback: find visible buttons with consent text
|
|
975
|
+
const patterns = [
|
|
976
|
+
/^accept\\s*(all)?\\s*(cookies)?$/i,
|
|
977
|
+
/^(alle\\s+)?akzeptieren$/i,
|
|
978
|
+
/^(i\\s+)?agree$/i,
|
|
979
|
+
/^allow\\s*(all)?\\s*(cookies)?$/i,
|
|
980
|
+
/^(alle\\s+)?zustimmen$/i,
|
|
981
|
+
/^got\\s*it$/i,
|
|
982
|
+
/^ok$/i,
|
|
983
|
+
/^consent$/i,
|
|
984
|
+
/^(j')?accepte(r)?$/i,
|
|
985
|
+
/^(tout\\s+)?accepter$/i,
|
|
986
|
+
];
|
|
987
|
+
const candidates = [
|
|
988
|
+
...document.querySelectorAll('button'),
|
|
989
|
+
...document.querySelectorAll('a[role="button"]'),
|
|
990
|
+
...document.querySelectorAll('[role="button"]'),
|
|
991
|
+
];
|
|
992
|
+
for (const el of candidates) {
|
|
993
|
+
if (!el.offsetParent) continue;
|
|
994
|
+
const text = (el.textContent || '').trim();
|
|
995
|
+
if (text.length > 40) continue;
|
|
996
|
+
for (const pat of patterns) {
|
|
997
|
+
if (pat.test(text)) {
|
|
998
|
+
el.click();
|
|
999
|
+
return JSON.stringify({ dismissed: true, method: 'text', detail: text });
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return JSON.stringify({ dismissed: false });
|
|
1005
|
+
})()`;
|
|
1006
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1007
|
+
await this.ensureEnabled(targetWsUrl);
|
|
1008
|
+
const raw = (await conn.send("Runtime.evaluate", {
|
|
1009
|
+
expression,
|
|
1010
|
+
returnByValue: true
|
|
1011
|
+
})).result.value ?? "{\"dismissed\":false}";
|
|
1012
|
+
try {
|
|
1013
|
+
return JSON.parse(raw);
|
|
1014
|
+
} catch {
|
|
1015
|
+
return { dismissed: false };
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Use the CDP Accessibility tree to find and click consent buttons.
|
|
1021
|
+
* This works even when consent banners use shadow DOM, iframes, or obfuscated class names.
|
|
1022
|
+
*/
|
|
1023
|
+
async dismissViaA11y(targetWsUrl) {
|
|
1024
|
+
return await this.withRetry(targetWsUrl, async (conn) => {
|
|
1025
|
+
await conn.send("Accessibility.enable");
|
|
1026
|
+
const { nodes } = await conn.send("Accessibility.getFullAXTree");
|
|
1027
|
+
const consentPatterns = [
|
|
1028
|
+
/^accept\s*(all)?\s*(cookies)?$/i,
|
|
1029
|
+
/^(i\s+)?agree(\s+to\s+all)?$/i,
|
|
1030
|
+
/^allow\s*(all)?\s*(cookies)?$/i,
|
|
1031
|
+
/^got\s*it$/i,
|
|
1032
|
+
/^ok$/i,
|
|
1033
|
+
/^consent$/i,
|
|
1034
|
+
/^(alle\s+)?akzeptieren$/i,
|
|
1035
|
+
/^(alle\s+)?zustimmen$/i,
|
|
1036
|
+
/^einverstanden$/i,
|
|
1037
|
+
/^(j')?accepte(r)?(\s+tout)?$/i,
|
|
1038
|
+
/^(tout\s+)?accepter$/i,
|
|
1039
|
+
/^aceptar(\s+todo)?$/i,
|
|
1040
|
+
/^accetta(\s+tutto)?$/i,
|
|
1041
|
+
/^accepteren$/i,
|
|
1042
|
+
/^alle\s+accepteren$/i,
|
|
1043
|
+
/^aceitar(\s+tudo)?$/i
|
|
1044
|
+
];
|
|
1045
|
+
const candidates = [];
|
|
1046
|
+
for (const node of nodes) {
|
|
1047
|
+
if (node.ignored) continue;
|
|
1048
|
+
const role = node.role?.value;
|
|
1049
|
+
if (role !== "button" && role !== "link") continue;
|
|
1050
|
+
const name = node.name?.value?.trim();
|
|
1051
|
+
if (!name || name.length > 50) continue;
|
|
1052
|
+
if (node.properties?.some((p) => p.name === "disabled" && p.value.value === true)) continue;
|
|
1053
|
+
for (const pat of consentPatterns) if (pat.test(name) && node.backendDOMNodeId) {
|
|
1054
|
+
candidates.push({
|
|
1055
|
+
nodeId: node.nodeId,
|
|
1056
|
+
backendDOMNodeId: node.backendDOMNodeId,
|
|
1057
|
+
name
|
|
1058
|
+
});
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (candidates.length === 0) return { dismissed: false };
|
|
1063
|
+
const target = candidates[0];
|
|
1064
|
+
try {
|
|
1065
|
+
const { object } = await conn.send("DOM.resolveNode", { backendNodeId: target.backendDOMNodeId });
|
|
1066
|
+
if (object?.objectId) {
|
|
1067
|
+
await conn.send("Runtime.callFunctionOn", {
|
|
1068
|
+
objectId: object.objectId,
|
|
1069
|
+
functionDeclaration: "function() { this.click(); }",
|
|
1070
|
+
returnByValue: true
|
|
1071
|
+
});
|
|
1072
|
+
return {
|
|
1073
|
+
dismissed: true,
|
|
1074
|
+
method: "a11y",
|
|
1075
|
+
detail: target.name
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
} catch {}
|
|
1079
|
+
return { dismissed: false };
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
916
1082
|
terminate(pid) {
|
|
917
1083
|
if (pid === 0) return;
|
|
918
1084
|
try {
|
|
@@ -1006,6 +1172,9 @@ var MockBrowserController = class {
|
|
|
1006
1172
|
truncated: false
|
|
1007
1173
|
};
|
|
1008
1174
|
}
|
|
1175
|
+
async dismissCookieBanner(_targetWsUrl) {
|
|
1176
|
+
return { dismissed: false };
|
|
1177
|
+
}
|
|
1009
1178
|
terminate(_pid) {}
|
|
1010
1179
|
closeConnection(_targetWsUrl) {}
|
|
1011
1180
|
};
|
|
@@ -1014,6 +1183,7 @@ var MockBrowserController = class {
|
|
|
1014
1183
|
//#region src/session/session-store.ts
|
|
1015
1184
|
var SessionStore = class {
|
|
1016
1185
|
filePath;
|
|
1186
|
+
cache = null;
|
|
1017
1187
|
constructor(baseDir) {
|
|
1018
1188
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
1019
1189
|
this.filePath = path.join(baseDir, "sessions.json");
|
|
@@ -1081,9 +1251,19 @@ var SessionStore = class {
|
|
|
1081
1251
|
this.write(state);
|
|
1082
1252
|
}
|
|
1083
1253
|
read() {
|
|
1084
|
-
|
|
1254
|
+
if (this.cache) return this.cache;
|
|
1255
|
+
try {
|
|
1256
|
+
const parsed = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
|
|
1257
|
+
this.cache = parsed;
|
|
1258
|
+
return parsed;
|
|
1259
|
+
} catch {
|
|
1260
|
+
const empty = { sessions: {} };
|
|
1261
|
+
this.write(empty);
|
|
1262
|
+
return empty;
|
|
1263
|
+
}
|
|
1085
1264
|
}
|
|
1086
1265
|
write(state) {
|
|
1266
|
+
this.cache = state;
|
|
1087
1267
|
fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), "utf8");
|
|
1088
1268
|
}
|
|
1089
1269
|
};
|
|
@@ -1222,6 +1402,10 @@ var SessionManager = class {
|
|
|
1222
1402
|
const record = await this.ensureSession(sessionId);
|
|
1223
1403
|
return await this.browser.getInteractiveElements(record.targetWsUrl, options);
|
|
1224
1404
|
}
|
|
1405
|
+
async dismissCookieBanner(sessionId) {
|
|
1406
|
+
const record = await this.ensureSession(sessionId);
|
|
1407
|
+
return await this.browser.dismissCookieBanner(record.targetWsUrl);
|
|
1408
|
+
}
|
|
1225
1409
|
setStatus(status, reason) {
|
|
1226
1410
|
const active = this.store.getActive();
|
|
1227
1411
|
if (!active) throw new Error("No active session");
|
|
@@ -1488,6 +1672,9 @@ var ControlApi = class {
|
|
|
1488
1672
|
async getInteractiveElements(sessionId, options) {
|
|
1489
1673
|
return await this.sessions.getInteractiveElements(sessionId, options);
|
|
1490
1674
|
}
|
|
1675
|
+
async dismissCookieBanner(sessionId) {
|
|
1676
|
+
return await this.sessions.dismissCookieBanner(sessionId);
|
|
1677
|
+
}
|
|
1491
1678
|
listEvents(sessionId, limit = 100) {
|
|
1492
1679
|
return { events: this.eventStore.list(sessionId, limit) };
|
|
1493
1680
|
}
|
|
@@ -1526,7 +1713,7 @@ function loadConfig(env = process.env) {
|
|
|
1526
1713
|
wsPort,
|
|
1527
1714
|
commandTimeoutMs,
|
|
1528
1715
|
logDir: env.AGENTIC_BROWSER_LOG_DIR ?? path.resolve(process.cwd(), ".agentic-browser"),
|
|
1529
|
-
browserExecutablePath: env.AGENTIC_BROWSER_CHROME_PATH,
|
|
1716
|
+
browserExecutablePath: env.AGENTIC_BROWSER_CHROME_EXECUTABLE_PATH ?? env.AGENTIC_BROWSER_CHROME_PATH,
|
|
1530
1717
|
cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
|
|
1531
1718
|
userProfileDir,
|
|
1532
1719
|
headless: env.AGENTIC_BROWSER_HEADLESS === "true" || env.AGENTIC_BROWSER_HEADLESS === "1",
|
|
@@ -1568,54 +1755,55 @@ var Logger = class {
|
|
|
1568
1755
|
|
|
1569
1756
|
//#endregion
|
|
1570
1757
|
//#region src/observability/event-store.ts
|
|
1758
|
+
const MAX_EVENTS_PER_SESSION = 500;
|
|
1571
1759
|
var EventStore = class {
|
|
1572
1760
|
events = /* @__PURE__ */ new Map();
|
|
1573
1761
|
filePath;
|
|
1574
1762
|
constructor(baseDir) {
|
|
1575
|
-
this.baseDir = baseDir;
|
|
1576
1763
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
1577
1764
|
this.filePath = path.join(baseDir, "session-events.log");
|
|
1578
1765
|
}
|
|
1579
1766
|
append(event) {
|
|
1580
|
-
|
|
1767
|
+
let existing = this.events.get(event.sessionId) ?? [];
|
|
1581
1768
|
existing.push(event);
|
|
1769
|
+
if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-MAX_EVENTS_PER_SESSION);
|
|
1582
1770
|
this.events.set(event.sessionId, existing);
|
|
1583
|
-
fs.
|
|
1771
|
+
fs.appendFileSync(this.filePath, `${JSON.stringify(event)}\n`, "utf8");
|
|
1584
1772
|
}
|
|
1585
1773
|
list(sessionId, limit = 100) {
|
|
1586
1774
|
const entries = this.events.get(sessionId) ?? [];
|
|
1587
1775
|
return entries.slice(Math.max(0, entries.length - limit));
|
|
1588
1776
|
}
|
|
1777
|
+
clear(sessionId) {
|
|
1778
|
+
this.events.delete(sessionId);
|
|
1779
|
+
}
|
|
1589
1780
|
};
|
|
1590
1781
|
|
|
1591
1782
|
//#endregion
|
|
1592
1783
|
//#region src/auth/session-token.ts
|
|
1593
1784
|
var SessionTokenService = class {
|
|
1594
|
-
|
|
1785
|
+
tokens = /* @__PURE__ */ new Map();
|
|
1595
1786
|
issue(sessionId) {
|
|
1596
1787
|
const token = crypto.randomBytes(24).toString("hex");
|
|
1597
|
-
this.
|
|
1788
|
+
this.tokens.set(sessionId, token);
|
|
1598
1789
|
return token;
|
|
1599
1790
|
}
|
|
1600
1791
|
validate(sessionId, token) {
|
|
1601
|
-
const
|
|
1602
|
-
if (!
|
|
1603
|
-
const
|
|
1604
|
-
const
|
|
1605
|
-
if (
|
|
1606
|
-
return crypto.timingSafeEqual(
|
|
1792
|
+
const expected = this.tokens.get(sessionId);
|
|
1793
|
+
if (!expected) return false;
|
|
1794
|
+
const expectedBuf = Buffer.from(expected);
|
|
1795
|
+
const providedBuf = Buffer.from(token);
|
|
1796
|
+
if (expectedBuf.length !== providedBuf.length) return false;
|
|
1797
|
+
return crypto.timingSafeEqual(expectedBuf, providedBuf);
|
|
1607
1798
|
}
|
|
1608
1799
|
revoke(sessionId) {
|
|
1609
|
-
|
|
1610
|
-
if (!record) return;
|
|
1611
|
-
record.revokedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1612
|
-
this.bySession.set(sessionId, record);
|
|
1800
|
+
this.tokens.delete(sessionId);
|
|
1613
1801
|
}
|
|
1614
1802
|
get(sessionId) {
|
|
1615
|
-
return this.
|
|
1803
|
+
return this.tokens.get(sessionId);
|
|
1616
1804
|
}
|
|
1617
1805
|
seed(sessionId, token) {
|
|
1618
|
-
this.
|
|
1806
|
+
this.tokens.set(sessionId, token);
|
|
1619
1807
|
}
|
|
1620
1808
|
};
|
|
1621
1809
|
|
|
@@ -1853,6 +2041,8 @@ const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
|
|
|
1853
2041
|
//#region src/memory/task-insight-store.ts
|
|
1854
2042
|
const EMPTY_STATE = { insights: [] };
|
|
1855
2043
|
const FLUSH_DELAY_MS = 500;
|
|
2044
|
+
const REGISTERED_EXIT_HANDLERS = Symbol.for("agentic-browser.task-insight-store.exit-handlers");
|
|
2045
|
+
const REGISTERED_STORES = Symbol.for("agentic-browser.task-insight-store.instances");
|
|
1856
2046
|
var TaskInsightStore = class {
|
|
1857
2047
|
filePath;
|
|
1858
2048
|
/** In-memory cache – authoritative after first load. */
|
|
@@ -1868,16 +2058,7 @@ var TaskInsightStore = class {
|
|
|
1868
2058
|
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
1869
2059
|
} catch {}
|
|
1870
2060
|
if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE);
|
|
1871
|
-
|
|
1872
|
-
process.on("exit", onExit);
|
|
1873
|
-
process.on("SIGINT", () => {
|
|
1874
|
-
this.flushSync();
|
|
1875
|
-
process.exit(0);
|
|
1876
|
-
});
|
|
1877
|
-
process.on("SIGTERM", () => {
|
|
1878
|
-
this.flushSync();
|
|
1879
|
-
process.exit(0);
|
|
1880
|
-
});
|
|
2061
|
+
this.registerProcessHandlers();
|
|
1881
2062
|
}
|
|
1882
2063
|
list() {
|
|
1883
2064
|
return this.getCache();
|
|
@@ -1917,13 +2098,10 @@ var TaskInsightStore = class {
|
|
|
1917
2098
|
}
|
|
1918
2099
|
markDirty() {
|
|
1919
2100
|
this.dirty = true;
|
|
1920
|
-
if (!this.flushTimer) {
|
|
1921
|
-
this.flushTimer =
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
}, FLUSH_DELAY_MS);
|
|
1925
|
-
if (this.flushTimer && typeof this.flushTimer === "object" && "unref" in this.flushTimer) this.flushTimer.unref();
|
|
1926
|
-
}
|
|
2101
|
+
if (!this.flushTimer) this.flushTimer = setTimeout(() => {
|
|
2102
|
+
this.flushTimer = null;
|
|
2103
|
+
this.flushSync();
|
|
2104
|
+
}, FLUSH_DELAY_MS);
|
|
1927
2105
|
}
|
|
1928
2106
|
/** Read and validate from disk (only on first load or corruption recovery). */
|
|
1929
2107
|
readDisk() {
|
|
@@ -1962,6 +2140,25 @@ var TaskInsightStore = class {
|
|
|
1962
2140
|
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf8");
|
|
1963
2141
|
fs.renameSync(tempPath, this.filePath);
|
|
1964
2142
|
}
|
|
2143
|
+
registerProcessHandlers() {
|
|
2144
|
+
const proc = process;
|
|
2145
|
+
const stores = proc[REGISTERED_STORES] ??= /* @__PURE__ */ new Set();
|
|
2146
|
+
stores.add(this);
|
|
2147
|
+
if (proc[REGISTERED_EXIT_HANDLERS]) return;
|
|
2148
|
+
proc[REGISTERED_EXIT_HANDLERS] = true;
|
|
2149
|
+
const flushAll = () => {
|
|
2150
|
+
for (const store of stores) store.flushSync();
|
|
2151
|
+
};
|
|
2152
|
+
process.on("exit", flushAll);
|
|
2153
|
+
process.on("SIGINT", () => {
|
|
2154
|
+
flushAll();
|
|
2155
|
+
process.exit(0);
|
|
2156
|
+
});
|
|
2157
|
+
process.on("SIGTERM", () => {
|
|
2158
|
+
flushAll();
|
|
2159
|
+
process.exit(0);
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
1965
2162
|
};
|
|
1966
2163
|
|
|
1967
2164
|
//#endregion
|
|
@@ -1976,6 +2173,10 @@ var MemoryService = class {
|
|
|
1976
2173
|
this.store = new TaskInsightStore(baseDir);
|
|
1977
2174
|
this.index = new MemoryIndex();
|
|
1978
2175
|
}
|
|
2176
|
+
/** Force an immediate synchronous flush of pending writes. */
|
|
2177
|
+
flushSync() {
|
|
2178
|
+
this.store.flushSync();
|
|
2179
|
+
}
|
|
1979
2180
|
search(input) {
|
|
1980
2181
|
const cacheKey = `${input.taskIntent}\0${input.siteDomain ?? ""}\0${input.limit ?? 10}`;
|
|
1981
2182
|
const now = Date.now();
|
|
@@ -2026,7 +2227,7 @@ var MemoryService = class {
|
|
|
2026
2227
|
}
|
|
2027
2228
|
recordSuccess(input) {
|
|
2028
2229
|
const insights = this.store.list();
|
|
2029
|
-
const matched = this.findBestExactMatch(insights, input
|
|
2230
|
+
const matched = this.findBestExactMatch(insights, input);
|
|
2030
2231
|
const evidence = this.createEvidence(input, "success");
|
|
2031
2232
|
if (!matched) {
|
|
2032
2233
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2083,7 +2284,7 @@ var MemoryService = class {
|
|
|
2083
2284
|
}
|
|
2084
2285
|
recordFailure(input, errorMessage) {
|
|
2085
2286
|
const insights = this.store.list();
|
|
2086
|
-
const matched = this.findBestExactMatch(insights, input
|
|
2287
|
+
const matched = this.findBestExactMatch(insights, input);
|
|
2087
2288
|
if (!matched) return;
|
|
2088
2289
|
const signal = detectStalenessSignal(errorMessage, input.selector);
|
|
2089
2290
|
const evidence = this.createEvidence(input, "failure", errorMessage);
|
|
@@ -2096,15 +2297,37 @@ var MemoryService = class {
|
|
|
2096
2297
|
this.invalidateSearchCache();
|
|
2097
2298
|
return failed;
|
|
2098
2299
|
}
|
|
2099
|
-
findBestExactMatch(insights,
|
|
2100
|
-
const intentLower = taskIntent.toLowerCase();
|
|
2101
|
-
const domainLower = siteDomain.toLowerCase();
|
|
2300
|
+
findBestExactMatch(insights, input) {
|
|
2301
|
+
const intentLower = input.taskIntent.toLowerCase();
|
|
2302
|
+
const domainLower = input.siteDomain.toLowerCase();
|
|
2102
2303
|
let best;
|
|
2103
2304
|
for (const insight of insights) if (insight.taskIntent.toLowerCase() === intentLower && insight.siteDomain.toLowerCase() === domainLower) {
|
|
2104
|
-
if (!best || insight.updatedAt > best.updatedAt) best = insight;
|
|
2305
|
+
if (!best || this.scoreExactMatch(insight, input) > this.scoreExactMatch(best, input) || this.scoreExactMatch(insight, input) === this.scoreExactMatch(best, input) && insight.updatedAt > best.updatedAt) best = insight;
|
|
2105
2306
|
}
|
|
2106
2307
|
return best;
|
|
2107
2308
|
}
|
|
2309
|
+
scoreExactMatch(insight, input) {
|
|
2310
|
+
let score = 0;
|
|
2311
|
+
if (insight.sitePathPattern === input.sitePathPattern) score += 4;
|
|
2312
|
+
else if (insight.sitePathPattern === "/" || input.sitePathPattern === "/" || this.sameFirstPathSegment(insight.sitePathPattern, input.sitePathPattern)) score += 2;
|
|
2313
|
+
if (input.selector) {
|
|
2314
|
+
if (insight.actionRecipe.some((step) => step.selector === input.selector)) score += 3;
|
|
2315
|
+
if (insight.evidence.some((ev) => ev.selector === input.selector && ev.result === "success")) score += 2;
|
|
2316
|
+
if ((insight.selectorAliases ?? []).some((alias) => alias.selector === input.selector)) score += 1;
|
|
2317
|
+
}
|
|
2318
|
+
score += Math.min(insight.successCount, 5) * .1;
|
|
2319
|
+
score -= Math.min(insight.failureCount, 5) * .1;
|
|
2320
|
+
return score;
|
|
2321
|
+
}
|
|
2322
|
+
sameFirstPathSegment(left, right) {
|
|
2323
|
+
const normalize = (value) => {
|
|
2324
|
+
const [firstSegment = ""] = value.replace(/\/\*$/, "").split("/").filter(Boolean);
|
|
2325
|
+
return firstSegment.toLowerCase();
|
|
2326
|
+
};
|
|
2327
|
+
const leftSegment = normalize(left);
|
|
2328
|
+
const rightSegment = normalize(right);
|
|
2329
|
+
return Boolean(leftSegment) && leftSegment === rightSegment;
|
|
2330
|
+
}
|
|
2108
2331
|
createEvidence(input, result, reason) {
|
|
2109
2332
|
return {
|
|
2110
2333
|
evidenceId: crypto.randomUUID(),
|
|
@@ -2217,6 +2440,9 @@ var AgenticBrowserCore = class {
|
|
|
2217
2440
|
selector: input.selector
|
|
2218
2441
|
});
|
|
2219
2442
|
}
|
|
2443
|
+
async dismissCookieBanner(sessionId) {
|
|
2444
|
+
return await this.api.dismissCookieBanner(sessionId);
|
|
2445
|
+
}
|
|
2220
2446
|
async restartSession(sessionId) {
|
|
2221
2447
|
return await this.api.restartSession(sessionId);
|
|
2222
2448
|
}
|