agentic-browser 1.6.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/cli/index.mjs +166 -16
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.mjs +169 -49
- package/dist/{runtime-C7B024bN.mjs → runtime-DUSoQl5x.mjs} +119 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -111,8 +111,8 @@ agentic-browser agent start --user-profile default
|
|
|
111
111
|
agentic-browser agent start --headless
|
|
112
112
|
agentic-browser agent start --user-agent "MyBot/1.0"
|
|
113
113
|
agentic-browser agent status
|
|
114
|
-
agentic-browser agent
|
|
115
|
-
agentic-browser agent
|
|
114
|
+
agentic-browser agent navigate https://example.com
|
|
115
|
+
agentic-browser agent click "#login"
|
|
116
116
|
agentic-browser agent content --mode text
|
|
117
117
|
agentic-browser agent content --mode html --selector main
|
|
118
118
|
agentic-browser agent elements
|
|
@@ -132,7 +132,7 @@ agentic-browser agent elements --roles button,link,input --visible-only --limit
|
|
|
132
132
|
agentic-browser agent elements --selector "#main-content"
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
-
Returns a JSON array of elements with CSS selectors and fallback selectors usable in `agent
|
|
135
|
+
Returns a JSON array of elements with CSS selectors and fallback selectors usable in the typed agent commands like `agent click`, `agent type`, and `agent select`:
|
|
136
136
|
|
|
137
137
|
```json
|
|
138
138
|
{
|
|
@@ -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
|
|
|
@@ -255,7 +255,7 @@ agentic-browser memory:stats
|
|
|
255
255
|
|
|
256
256
|
1. `agent start` — launch Chrome and persist session.
|
|
257
257
|
2. `agent elements` — discover what's on the page.
|
|
258
|
-
3. `agent
|
|
258
|
+
3. `agent navigate`, `agent click`, `agent type`, `agent select`, and related typed commands — execute actions using discovered selectors.
|
|
259
259
|
4. `agent content` — read page content after actions.
|
|
260
260
|
5. `agent memory-search` — reuse known selectors for repeated tasks.
|
|
261
261
|
6. `agent stop` — terminate when done.
|
|
@@ -265,7 +265,7 @@ agentic-browser memory:stats
|
|
|
265
265
|
- Exactly **one** managed session is supported at a time.
|
|
266
266
|
- Session state is persisted in `.agentic-browser/`.
|
|
267
267
|
- All commands print exactly one JSON line to `stdout`.
|
|
268
|
-
- `
|
|
268
|
+
- The typed `agent` commands are the preferred interface for LLMs. Use low-level `command:run` only when you need raw JSON payload control.
|
|
269
269
|
- Parse only `stdout` as result object and use exit code for success/failure.
|
|
270
270
|
|
|
271
271
|
## Programmatic API
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { r as createCliRuntime } from "../runtime-
|
|
2
|
+
import { r as createCliRuntime } from "../runtime-DUSoQl5x.mjs";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import crypto from "node:crypto";
|
|
@@ -64,7 +64,7 @@ async function agentStop(runtime) {
|
|
|
64
64
|
sessionId
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
async function
|
|
67
|
+
async function executeAgentCommand(runtime, input) {
|
|
68
68
|
const sessionId = requireSessionId(runtime);
|
|
69
69
|
await ensureReady(runtime, sessionId);
|
|
70
70
|
const attempt = () => runtime.api.executeCommand(sessionId, {
|
|
@@ -75,22 +75,75 @@ async function agentRun(runtime, input) {
|
|
|
75
75
|
try {
|
|
76
76
|
const first = await attempt();
|
|
77
77
|
if (first.resultStatus === "failed") throw new Error(first.resultMessage ?? "Command failed");
|
|
78
|
-
return
|
|
79
|
-
ok: true,
|
|
80
|
-
action: "run",
|
|
81
|
-
...first
|
|
82
|
-
};
|
|
78
|
+
return first;
|
|
83
79
|
} catch {
|
|
84
80
|
await runtime.api.restartSession(sessionId);
|
|
85
81
|
const second = await attempt();
|
|
86
82
|
if (second.resultStatus === "failed") throw new Error(second.resultMessage ?? "Command failed after retry");
|
|
87
|
-
return
|
|
88
|
-
ok: true,
|
|
89
|
-
action: "run",
|
|
90
|
-
...second
|
|
91
|
-
};
|
|
83
|
+
return second;
|
|
92
84
|
}
|
|
93
85
|
}
|
|
86
|
+
async function agentNavigate(runtime, input) {
|
|
87
|
+
const result = await executeAgentCommand(runtime, {
|
|
88
|
+
type: "navigate",
|
|
89
|
+
payload: { url: input.url }
|
|
90
|
+
});
|
|
91
|
+
let cookieBanner;
|
|
92
|
+
if (result.resultStatus === "success") try {
|
|
93
|
+
const sessionId = requireSessionId(runtime);
|
|
94
|
+
const dismissed = await runtime.api.dismissCookieBanner(sessionId);
|
|
95
|
+
if (dismissed.dismissed) cookieBanner = dismissed;
|
|
96
|
+
} catch {}
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
action: "navigate",
|
|
100
|
+
...result,
|
|
101
|
+
cookieBanner
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function agentInteract(runtime, input) {
|
|
105
|
+
const result = await executeAgentCommand(runtime, {
|
|
106
|
+
type: "interact",
|
|
107
|
+
payload: input
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
action: input.action,
|
|
112
|
+
...result
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function agentRestart(runtime) {
|
|
116
|
+
return {
|
|
117
|
+
ok: true,
|
|
118
|
+
action: "restart",
|
|
119
|
+
...await executeAgentCommand(runtime, {
|
|
120
|
+
type: "restart",
|
|
121
|
+
payload: {}
|
|
122
|
+
})
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function agentTerminate(runtime) {
|
|
126
|
+
const result = await executeAgentCommand(runtime, {
|
|
127
|
+
type: "terminate",
|
|
128
|
+
payload: {}
|
|
129
|
+
});
|
|
130
|
+
saveState(runtime, { sessionId: null });
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
action: "terminate",
|
|
134
|
+
...result
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function agentDismissCookies(runtime) {
|
|
138
|
+
const sessionId = requireSessionId(runtime);
|
|
139
|
+
await ensureReady(runtime, sessionId);
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
action: "cookies",
|
|
143
|
+
sessionId,
|
|
144
|
+
...await runtime.api.dismissCookieBanner(sessionId)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
94
147
|
async function agentContent(runtime, input) {
|
|
95
148
|
const sessionId = requireSessionId(runtime);
|
|
96
149
|
await ensureReady(runtime, sessionId);
|
|
@@ -243,6 +296,7 @@ async function main() {
|
|
|
243
296
|
const runtime = createCliRuntime();
|
|
244
297
|
const program = new Command();
|
|
245
298
|
program.name("agentic-browser").description("Agentic browser CLI");
|
|
299
|
+
const collect = (value, previous = []) => [...previous, value];
|
|
246
300
|
program.command("session:start").option("--cdp-url <url>", "connect to existing Chrome via CDP endpoint URL").option("--user-profile <path>", "use 'default' for system Chrome profile or an absolute path").option("--headless", "run Chrome in headless mode (no visible window)").option("--user-agent <string>", "override the browser user-agent string").action(async (options) => {
|
|
247
301
|
if (options.cdpUrl) runtime.context.config.cdpUrl = options.cdpUrl;
|
|
248
302
|
if (options.userProfile) runtime.context.config.userProfileDir = options.userProfile === "true" || options.userProfile === "default" ? "default" : options.userProfile;
|
|
@@ -328,13 +382,109 @@ async function main() {
|
|
|
328
382
|
const result = await agentStop(runtime);
|
|
329
383
|
console.log(JSON.stringify(result));
|
|
330
384
|
});
|
|
331
|
-
agent.command("
|
|
332
|
-
const result = await
|
|
333
|
-
|
|
334
|
-
|
|
385
|
+
agent.command("navigate").argument("<url>").action(async (url) => {
|
|
386
|
+
const result = await agentNavigate(runtime, { url });
|
|
387
|
+
console.log(JSON.stringify(result));
|
|
388
|
+
});
|
|
389
|
+
agent.command("click").argument("<selector>").option("--fallback <selector>", "backup CSS selector", collect, []).action(async (selector, options) => {
|
|
390
|
+
const result = await agentInteract(runtime, {
|
|
391
|
+
action: "click",
|
|
392
|
+
selector,
|
|
393
|
+
fallbackSelectors: options.fallback
|
|
394
|
+
});
|
|
395
|
+
console.log(JSON.stringify(result));
|
|
396
|
+
});
|
|
397
|
+
agent.command("type").argument("<selector>").argument("<text>").option("--fallback <selector>", "backup CSS selector", collect, []).action(async (selector, text, options) => {
|
|
398
|
+
const result = await agentInteract(runtime, {
|
|
399
|
+
action: "type",
|
|
400
|
+
selector,
|
|
401
|
+
text,
|
|
402
|
+
fallbackSelectors: options.fallback
|
|
335
403
|
});
|
|
336
404
|
console.log(JSON.stringify(result));
|
|
337
405
|
});
|
|
406
|
+
agent.command("press").argument("<key>").action(async (key) => {
|
|
407
|
+
const result = await agentInteract(runtime, {
|
|
408
|
+
action: "press",
|
|
409
|
+
key
|
|
410
|
+
});
|
|
411
|
+
console.log(JSON.stringify(result));
|
|
412
|
+
});
|
|
413
|
+
agent.command("wait").argument("<selector>").option("--timeout <ms>", "timeout in milliseconds", "2000").action(async (selector, options) => {
|
|
414
|
+
const result = await agentInteract(runtime, {
|
|
415
|
+
action: "waitFor",
|
|
416
|
+
selector,
|
|
417
|
+
timeoutMs: Number.parseInt(options.timeout, 10)
|
|
418
|
+
});
|
|
419
|
+
console.log(JSON.stringify(result));
|
|
420
|
+
});
|
|
421
|
+
agent.command("hover").argument("<selector>").option("--fallback <selector>", "backup CSS selector", collect, []).action(async (selector, options) => {
|
|
422
|
+
const result = await agentInteract(runtime, {
|
|
423
|
+
action: "hover",
|
|
424
|
+
selector,
|
|
425
|
+
fallbackSelectors: options.fallback
|
|
426
|
+
});
|
|
427
|
+
console.log(JSON.stringify(result));
|
|
428
|
+
});
|
|
429
|
+
agent.command("select").argument("<selector>").argument("<value>").option("--fallback <selector>", "backup CSS selector", collect, []).action(async (selector, value, options) => {
|
|
430
|
+
const result = await agentInteract(runtime, {
|
|
431
|
+
action: "select",
|
|
432
|
+
selector,
|
|
433
|
+
value,
|
|
434
|
+
fallbackSelectors: options.fallback
|
|
435
|
+
});
|
|
436
|
+
console.log(JSON.stringify(result));
|
|
437
|
+
});
|
|
438
|
+
agent.command("toggle").argument("<selector>").option("--fallback <selector>", "backup CSS selector", collect, []).action(async (selector, options) => {
|
|
439
|
+
const result = await agentInteract(runtime, {
|
|
440
|
+
action: "toggle",
|
|
441
|
+
selector,
|
|
442
|
+
fallbackSelectors: options.fallback
|
|
443
|
+
});
|
|
444
|
+
console.log(JSON.stringify(result));
|
|
445
|
+
});
|
|
446
|
+
agent.command("scroll").option("--x <pixels>", "horizontal scroll delta", "0").option("--y <pixels>", "vertical scroll delta", "0").option("--selector <selector>", "scroll a specific element instead of the page").option("--fallback <selector>", "backup CSS selector", collect, []).action(async (options) => {
|
|
447
|
+
const result = await agentInteract(runtime, {
|
|
448
|
+
action: "scroll",
|
|
449
|
+
selector: options.selector,
|
|
450
|
+
fallbackSelectors: options.fallback,
|
|
451
|
+
scrollX: Number.parseInt(options.x, 10),
|
|
452
|
+
scrollY: Number.parseInt(options.y, 10)
|
|
453
|
+
});
|
|
454
|
+
console.log(JSON.stringify(result));
|
|
455
|
+
});
|
|
456
|
+
agent.command("back").action(async () => {
|
|
457
|
+
const result = await agentInteract(runtime, { action: "goBack" });
|
|
458
|
+
console.log(JSON.stringify(result));
|
|
459
|
+
});
|
|
460
|
+
agent.command("forward").action(async () => {
|
|
461
|
+
const result = await agentInteract(runtime, { action: "goForward" });
|
|
462
|
+
console.log(JSON.stringify(result));
|
|
463
|
+
});
|
|
464
|
+
agent.command("refresh").action(async () => {
|
|
465
|
+
const result = await agentInteract(runtime, { action: "refresh" });
|
|
466
|
+
console.log(JSON.stringify(result));
|
|
467
|
+
});
|
|
468
|
+
agent.command("dialog").option("--dismiss", "dismiss the current dialog").option("--value <text>", "prompt response text").action(async (options) => {
|
|
469
|
+
const result = await agentInteract(runtime, {
|
|
470
|
+
action: "dialog",
|
|
471
|
+
text: options.dismiss ? "dismiss" : void 0,
|
|
472
|
+
value: options.value
|
|
473
|
+
});
|
|
474
|
+
console.log(JSON.stringify(result));
|
|
475
|
+
});
|
|
476
|
+
agent.command("cookies").action(async () => {
|
|
477
|
+
const result = await agentDismissCookies(runtime);
|
|
478
|
+
console.log(JSON.stringify(result));
|
|
479
|
+
});
|
|
480
|
+
agent.command("restart").action(async () => {
|
|
481
|
+
const result = await agentRestart(runtime);
|
|
482
|
+
console.log(JSON.stringify(result));
|
|
483
|
+
});
|
|
484
|
+
agent.command("terminate").action(async () => {
|
|
485
|
+
const result = await agentTerminate(runtime);
|
|
486
|
+
console.log(JSON.stringify(result));
|
|
487
|
+
});
|
|
338
488
|
agent.command("content").option("--mode <mode>", "title|text|html|a11y", "text").option("--selector <selector>", "optional CSS selector").action(async (options) => {
|
|
339
489
|
const result = await agentContent(runtime, options);
|
|
340
490
|
console.log(JSON.stringify(result));
|
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,14 @@ 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))
|
|
218
335
|
}] };
|
|
219
336
|
});
|
|
220
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 }) => {
|
|
@@ -253,6 +370,9 @@ async function main() {
|
|
|
253
370
|
} catch {}
|
|
254
371
|
activeSessionId = void 0;
|
|
255
372
|
}
|
|
373
|
+
try {
|
|
374
|
+
getCore().context.memoryService.flushSync();
|
|
375
|
+
} catch {}
|
|
256
376
|
try {
|
|
257
377
|
getCore().sessions.cleanupSessions({ maxAgeDays: 0 });
|
|
258
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;
|
|
@@ -924,10 +932,17 @@ var ChromeCdpBrowserController = class {
|
|
|
924
932
|
* Returns silently on failure — never blocks navigation.
|
|
925
933
|
*/
|
|
926
934
|
async dismissCookieBanner(targetWsUrl) {
|
|
935
|
+
try {
|
|
936
|
+
const domResult = await this.dismissViaDom(targetWsUrl);
|
|
937
|
+
if (domResult.dismissed) return domResult;
|
|
938
|
+
} catch {}
|
|
927
939
|
try {
|
|
928
940
|
const a11yResult = await this.dismissViaA11y(targetWsUrl);
|
|
929
941
|
if (a11yResult.dismissed) return a11yResult;
|
|
930
942
|
} catch {}
|
|
943
|
+
return { dismissed: false };
|
|
944
|
+
}
|
|
945
|
+
async dismissViaDom(targetWsUrl) {
|
|
931
946
|
const expression = `(() => {
|
|
932
947
|
// Common cookie-consent button selectors (ordered by specificity)
|
|
933
948
|
const selectors = [
|
|
@@ -988,22 +1003,18 @@ var ChromeCdpBrowserController = class {
|
|
|
988
1003
|
|
|
989
1004
|
return JSON.stringify({ dismissed: false });
|
|
990
1005
|
})()`;
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
});
|
|
1004
|
-
} catch {
|
|
1005
|
-
return { dismissed: false };
|
|
1006
|
-
}
|
|
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
|
+
});
|
|
1007
1018
|
}
|
|
1008
1019
|
/**
|
|
1009
1020
|
* Use the CDP Accessibility tree to find and click consent buttons.
|
|
@@ -1172,6 +1183,7 @@ var MockBrowserController = class {
|
|
|
1172
1183
|
//#region src/session/session-store.ts
|
|
1173
1184
|
var SessionStore = class {
|
|
1174
1185
|
filePath;
|
|
1186
|
+
cache = null;
|
|
1175
1187
|
constructor(baseDir) {
|
|
1176
1188
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
1177
1189
|
this.filePath = path.join(baseDir, "sessions.json");
|
|
@@ -1239,9 +1251,19 @@ var SessionStore = class {
|
|
|
1239
1251
|
this.write(state);
|
|
1240
1252
|
}
|
|
1241
1253
|
read() {
|
|
1242
|
-
|
|
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
|
+
}
|
|
1243
1264
|
}
|
|
1244
1265
|
write(state) {
|
|
1266
|
+
this.cache = state;
|
|
1245
1267
|
fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), "utf8");
|
|
1246
1268
|
}
|
|
1247
1269
|
};
|
|
@@ -1691,7 +1713,7 @@ function loadConfig(env = process.env) {
|
|
|
1691
1713
|
wsPort,
|
|
1692
1714
|
commandTimeoutMs,
|
|
1693
1715
|
logDir: env.AGENTIC_BROWSER_LOG_DIR ?? path.resolve(process.cwd(), ".agentic-browser"),
|
|
1694
|
-
browserExecutablePath: env.AGENTIC_BROWSER_CHROME_PATH,
|
|
1716
|
+
browserExecutablePath: env.AGENTIC_BROWSER_CHROME_EXECUTABLE_PATH ?? env.AGENTIC_BROWSER_CHROME_PATH,
|
|
1695
1717
|
cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
|
|
1696
1718
|
userProfileDir,
|
|
1697
1719
|
headless: env.AGENTIC_BROWSER_HEADLESS === "true" || env.AGENTIC_BROWSER_HEADLESS === "1",
|
|
@@ -1733,54 +1755,55 @@ var Logger = class {
|
|
|
1733
1755
|
|
|
1734
1756
|
//#endregion
|
|
1735
1757
|
//#region src/observability/event-store.ts
|
|
1758
|
+
const MAX_EVENTS_PER_SESSION = 500;
|
|
1736
1759
|
var EventStore = class {
|
|
1737
1760
|
events = /* @__PURE__ */ new Map();
|
|
1738
1761
|
filePath;
|
|
1739
1762
|
constructor(baseDir) {
|
|
1740
|
-
this.baseDir = baseDir;
|
|
1741
1763
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
1742
1764
|
this.filePath = path.join(baseDir, "session-events.log");
|
|
1743
1765
|
}
|
|
1744
1766
|
append(event) {
|
|
1745
|
-
|
|
1767
|
+
let existing = this.events.get(event.sessionId) ?? [];
|
|
1746
1768
|
existing.push(event);
|
|
1769
|
+
if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-MAX_EVENTS_PER_SESSION);
|
|
1747
1770
|
this.events.set(event.sessionId, existing);
|
|
1748
|
-
fs.
|
|
1771
|
+
fs.appendFileSync(this.filePath, `${JSON.stringify(event)}\n`, "utf8");
|
|
1749
1772
|
}
|
|
1750
1773
|
list(sessionId, limit = 100) {
|
|
1751
1774
|
const entries = this.events.get(sessionId) ?? [];
|
|
1752
1775
|
return entries.slice(Math.max(0, entries.length - limit));
|
|
1753
1776
|
}
|
|
1777
|
+
clear(sessionId) {
|
|
1778
|
+
this.events.delete(sessionId);
|
|
1779
|
+
}
|
|
1754
1780
|
};
|
|
1755
1781
|
|
|
1756
1782
|
//#endregion
|
|
1757
1783
|
//#region src/auth/session-token.ts
|
|
1758
1784
|
var SessionTokenService = class {
|
|
1759
|
-
|
|
1785
|
+
tokens = /* @__PURE__ */ new Map();
|
|
1760
1786
|
issue(sessionId) {
|
|
1761
1787
|
const token = crypto.randomBytes(24).toString("hex");
|
|
1762
|
-
this.
|
|
1788
|
+
this.tokens.set(sessionId, token);
|
|
1763
1789
|
return token;
|
|
1764
1790
|
}
|
|
1765
1791
|
validate(sessionId, token) {
|
|
1766
|
-
const
|
|
1767
|
-
if (!
|
|
1768
|
-
const
|
|
1769
|
-
const
|
|
1770
|
-
if (
|
|
1771
|
-
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);
|
|
1772
1798
|
}
|
|
1773
1799
|
revoke(sessionId) {
|
|
1774
|
-
|
|
1775
|
-
if (!record) return;
|
|
1776
|
-
record.revokedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1777
|
-
this.bySession.set(sessionId, record);
|
|
1800
|
+
this.tokens.delete(sessionId);
|
|
1778
1801
|
}
|
|
1779
1802
|
get(sessionId) {
|
|
1780
|
-
return this.
|
|
1803
|
+
return this.tokens.get(sessionId);
|
|
1781
1804
|
}
|
|
1782
1805
|
seed(sessionId, token) {
|
|
1783
|
-
this.
|
|
1806
|
+
this.tokens.set(sessionId, token);
|
|
1784
1807
|
}
|
|
1785
1808
|
};
|
|
1786
1809
|
|
|
@@ -2018,6 +2041,8 @@ const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
|
|
|
2018
2041
|
//#region src/memory/task-insight-store.ts
|
|
2019
2042
|
const EMPTY_STATE = { insights: [] };
|
|
2020
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");
|
|
2021
2046
|
var TaskInsightStore = class {
|
|
2022
2047
|
filePath;
|
|
2023
2048
|
/** In-memory cache – authoritative after first load. */
|
|
@@ -2033,16 +2058,7 @@ var TaskInsightStore = class {
|
|
|
2033
2058
|
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
2034
2059
|
} catch {}
|
|
2035
2060
|
if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE);
|
|
2036
|
-
|
|
2037
|
-
process.on("exit", onExit);
|
|
2038
|
-
process.on("SIGINT", () => {
|
|
2039
|
-
this.flushSync();
|
|
2040
|
-
process.exit(0);
|
|
2041
|
-
});
|
|
2042
|
-
process.on("SIGTERM", () => {
|
|
2043
|
-
this.flushSync();
|
|
2044
|
-
process.exit(0);
|
|
2045
|
-
});
|
|
2061
|
+
this.registerProcessHandlers();
|
|
2046
2062
|
}
|
|
2047
2063
|
list() {
|
|
2048
2064
|
return this.getCache();
|
|
@@ -2082,13 +2098,10 @@ var TaskInsightStore = class {
|
|
|
2082
2098
|
}
|
|
2083
2099
|
markDirty() {
|
|
2084
2100
|
this.dirty = true;
|
|
2085
|
-
if (!this.flushTimer) {
|
|
2086
|
-
this.flushTimer =
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
}, FLUSH_DELAY_MS);
|
|
2090
|
-
if (this.flushTimer && typeof this.flushTimer === "object" && "unref" in this.flushTimer) this.flushTimer.unref();
|
|
2091
|
-
}
|
|
2101
|
+
if (!this.flushTimer) this.flushTimer = setTimeout(() => {
|
|
2102
|
+
this.flushTimer = null;
|
|
2103
|
+
this.flushSync();
|
|
2104
|
+
}, FLUSH_DELAY_MS);
|
|
2092
2105
|
}
|
|
2093
2106
|
/** Read and validate from disk (only on first load or corruption recovery). */
|
|
2094
2107
|
readDisk() {
|
|
@@ -2127,6 +2140,25 @@ var TaskInsightStore = class {
|
|
|
2127
2140
|
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf8");
|
|
2128
2141
|
fs.renameSync(tempPath, this.filePath);
|
|
2129
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
|
+
}
|
|
2130
2162
|
};
|
|
2131
2163
|
|
|
2132
2164
|
//#endregion
|
|
@@ -2141,6 +2173,10 @@ var MemoryService = class {
|
|
|
2141
2173
|
this.store = new TaskInsightStore(baseDir);
|
|
2142
2174
|
this.index = new MemoryIndex();
|
|
2143
2175
|
}
|
|
2176
|
+
/** Force an immediate synchronous flush of pending writes. */
|
|
2177
|
+
flushSync() {
|
|
2178
|
+
this.store.flushSync();
|
|
2179
|
+
}
|
|
2144
2180
|
search(input) {
|
|
2145
2181
|
const cacheKey = `${input.taskIntent}\0${input.siteDomain ?? ""}\0${input.limit ?? 10}`;
|
|
2146
2182
|
const now = Date.now();
|
|
@@ -2191,7 +2227,7 @@ var MemoryService = class {
|
|
|
2191
2227
|
}
|
|
2192
2228
|
recordSuccess(input) {
|
|
2193
2229
|
const insights = this.store.list();
|
|
2194
|
-
const matched = this.findBestExactMatch(insights, input
|
|
2230
|
+
const matched = this.findBestExactMatch(insights, input);
|
|
2195
2231
|
const evidence = this.createEvidence(input, "success");
|
|
2196
2232
|
if (!matched) {
|
|
2197
2233
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2248,7 +2284,7 @@ var MemoryService = class {
|
|
|
2248
2284
|
}
|
|
2249
2285
|
recordFailure(input, errorMessage) {
|
|
2250
2286
|
const insights = this.store.list();
|
|
2251
|
-
const matched = this.findBestExactMatch(insights, input
|
|
2287
|
+
const matched = this.findBestExactMatch(insights, input);
|
|
2252
2288
|
if (!matched) return;
|
|
2253
2289
|
const signal = detectStalenessSignal(errorMessage, input.selector);
|
|
2254
2290
|
const evidence = this.createEvidence(input, "failure", errorMessage);
|
|
@@ -2261,15 +2297,37 @@ var MemoryService = class {
|
|
|
2261
2297
|
this.invalidateSearchCache();
|
|
2262
2298
|
return failed;
|
|
2263
2299
|
}
|
|
2264
|
-
findBestExactMatch(insights,
|
|
2265
|
-
const intentLower = taskIntent.toLowerCase();
|
|
2266
|
-
const domainLower = siteDomain.toLowerCase();
|
|
2300
|
+
findBestExactMatch(insights, input) {
|
|
2301
|
+
const intentLower = input.taskIntent.toLowerCase();
|
|
2302
|
+
const domainLower = input.siteDomain.toLowerCase();
|
|
2267
2303
|
let best;
|
|
2268
2304
|
for (const insight of insights) if (insight.taskIntent.toLowerCase() === intentLower && insight.siteDomain.toLowerCase() === domainLower) {
|
|
2269
|
-
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;
|
|
2270
2306
|
}
|
|
2271
2307
|
return best;
|
|
2272
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
|
+
}
|
|
2273
2331
|
createEvidence(input, result, reason) {
|
|
2274
2332
|
return {
|
|
2275
2333
|
evidenceId: crypto.randomUUID(),
|