cbrowser 18.73.2 → 18.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +113 -8
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
- package/dist/analysis/agent-ready-audit.js +66 -5
- package/dist/analysis/agent-ready-audit.js.map +1 -1
- package/dist/analysis/natural-language.d.ts.map +1 -1
- package/dist/analysis/natural-language.js +9 -8
- package/dist/analysis/natural-language.js.map +1 -1
- package/dist/analysis/page-understanding.d.ts.map +1 -1
- package/dist/analysis/page-understanding.js +115 -14
- package/dist/analysis/page-understanding.js.map +1 -1
- package/dist/analysis/webmcp-readiness.d.ts.map +1 -1
- package/dist/analysis/webmcp-readiness.js +18 -7
- package/dist/analysis/webmcp-readiness.js.map +1 -1
- package/dist/artifact-store.d.ts +55 -0
- package/dist/artifact-store.d.ts.map +1 -0
- package/dist/artifact-store.js +63 -0
- package/dist/artifact-store.js.map +1 -0
- package/dist/browser.d.ts +8 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +157 -41
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +28 -1
- package/dist/config.js.map +1 -1
- package/dist/mcp-server-remote.d.ts +17 -1
- package/dist/mcp-server-remote.d.ts.map +1 -1
- package/dist/mcp-server-remote.js +22 -3
- package/dist/mcp-server-remote.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +20 -0
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools/base/analysis-tools.d.ts +1 -1
- package/dist/mcp-tools/base/analysis-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/analysis-tools.js +83 -10
- package/dist/mcp-tools/base/analysis-tools.js.map +1 -1
- package/dist/mcp-tools/base/assertion-tools.d.ts +1 -1
- package/dist/mcp-tools/base/assertion-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/assertion-tools.js +26 -3
- package/dist/mcp-tools/base/assertion-tools.js.map +1 -1
- package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/audit-tools.js +189 -28
- package/dist/mcp-tools/base/audit-tools.js.map +1 -1
- package/dist/mcp-tools/base/browser-management-tools.d.ts +1 -1
- package/dist/mcp-tools/base/browser-management-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/browser-management-tools.js +66 -15
- package/dist/mcp-tools/base/browser-management-tools.js.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.js +6 -1
- package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.js +103 -30
- package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
- package/dist/mcp-tools/base/extraction-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/extraction-tools.js +6 -0
- package/dist/mcp-tools/base/extraction-tools.js.map +1 -1
- package/dist/mcp-tools/base/gif-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/gif-tools.js +7 -7
- package/dist/mcp-tools/base/gif-tools.js.map +1 -1
- package/dist/mcp-tools/base/interaction-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/interaction-tools.js +76 -11
- package/dist/mcp-tools/base/interaction-tools.js.map +1 -1
- package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/persona-comparison-tools.js +78 -10
- package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
- package/dist/mcp-tools/base/session-tools.d.ts +1 -1
- package/dist/mcp-tools/base/session-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/session-tools.js +64 -7
- package/dist/mcp-tools/base/session-tools.js.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.js +47 -1
- package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
- package/dist/mcp-tools/base/testing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/testing-tools.js +20 -0
- package/dist/mcp-tools/base/testing-tools.js.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.js +71 -46
- package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
- package/dist/mcp-tools/persona-creation-tools.d.ts.map +1 -1
- package/dist/mcp-tools/persona-creation-tools.js +40 -10
- package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
- package/dist/mcp-tools/session-policy.d.ts +43 -0
- package/dist/mcp-tools/session-policy.d.ts.map +1 -0
- package/dist/mcp-tools/session-policy.js +54 -0
- package/dist/mcp-tools/session-policy.js.map +1 -0
- package/dist/mcp-ui-resources.d.ts +83 -0
- package/dist/mcp-ui-resources.d.ts.map +1 -0
- package/dist/mcp-ui-resources.js +128 -0
- package/dist/mcp-ui-resources.js.map +1 -0
- package/dist/performance/metrics.d.ts +0 -3
- package/dist/performance/metrics.d.ts.map +1 -1
- package/dist/performance/metrics.js +45 -1
- package/dist/performance/metrics.js.map +1 -1
- package/dist/personas.d.ts.map +1 -1
- package/dist/personas.js +38 -4
- package/dist/personas.js.map +1 -1
- package/dist/remediation/patch-generator.d.ts.map +1 -1
- package/dist/remediation/patch-generator.js +36 -2
- package/dist/remediation/patch-generator.js.map +1 -1
- package/dist/selector-escape.d.ts +37 -0
- package/dist/selector-escape.d.ts.map +1 -0
- package/dist/selector-escape.js +83 -0
- package/dist/selector-escape.js.map +1 -0
- package/dist/testing/coverage.d.ts +8 -0
- package/dist/testing/coverage.d.ts.map +1 -1
- package/dist/testing/coverage.js +12 -0
- package/dist/testing/coverage.js.map +1 -1
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/visual/attention-quality.d.ts +0 -9
- package/dist/visual/attention-quality.d.ts.map +1 -1
- package/dist/visual/attention-quality.js +28 -1
- package/dist/visual/attention-quality.js.map +1 -1
- package/dist/visual/barrier-overlay-html.d.ts +88 -0
- package/dist/visual/barrier-overlay-html.d.ts.map +1 -0
- package/dist/visual/barrier-overlay-html.js +142 -0
- package/dist/visual/barrier-overlay-html.js.map +1 -0
- package/dist/visual/cognitive-transport.d.ts +15 -8
- package/dist/visual/cognitive-transport.d.ts.map +1 -1
- package/dist/visual/cognitive-transport.js +113 -34
- package/dist/visual/cognitive-transport.js.map +1 -1
- package/dist/visual/cross-browser.d.ts +13 -0
- package/dist/visual/cross-browser.d.ts.map +1 -1
- package/dist/visual/cross-browser.js +29 -3
- package/dist/visual/cross-browser.js.map +1 -1
- package/dist/visual/perceptual-transport.d.ts +8 -0
- package/dist/visual/perceptual-transport.d.ts.map +1 -1
- package/dist/visual/perceptual-transport.js +19 -0
- package/dist/visual/perceptual-transport.js.map +1 -1
- package/dist/visual/regression.d.ts.map +1 -1
- package/dist/visual/regression.js +76 -3
- package/dist/visual/regression.js.map +1 -1
- package/package.json +2 -1
|
@@ -5,15 +5,21 @@
|
|
|
5
5
|
* @license MIT
|
|
6
6
|
*/
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { refuseUnboundSession } from "../session-policy.js";
|
|
8
9
|
import { findElementByIntent, runAIReadinessBenchmark } from "../../analysis/index.js";
|
|
9
10
|
/**
|
|
10
11
|
* Register analysis tools (4 tools: analyze_page, generate_tests, find_element_by_intent, ai_benchmark)
|
|
11
12
|
*/
|
|
12
|
-
export function registerAnalysisTools(server, { getBrowser }) {
|
|
13
|
+
export function registerAnalysisTools(server, { getBrowser, getBrowserByToken }) {
|
|
13
14
|
server.registerTool("analyze_page", {
|
|
14
15
|
title: "Analyze Page Structure",
|
|
15
|
-
description: "Analyze page structure for forms, buttons, links",
|
|
16
|
-
inputSchema: {
|
|
16
|
+
description: "Analyze page structure for forms, buttons, links. MUST pass _browserToken from a previous tool call to analyze the same page — without it a blank page is analyzed and every count comes back zero.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
// Took no arguments at all, so on the HTTP transport it always analysed a
|
|
19
|
+
// fresh blank page: 0 forms, 0 buttons, 0 links, hasLogin false. Reads as a
|
|
20
|
+
// real analysis of a sparse page rather than as an error. (2026-07-28)
|
|
21
|
+
_browserToken: z.string().optional().describe("Browser session token from a previous tool call"),
|
|
22
|
+
},
|
|
17
23
|
annotations: {
|
|
18
24
|
title: "Analyze Page Structure",
|
|
19
25
|
readOnlyHint: true,
|
|
@@ -21,14 +27,29 @@ export function registerAnalysisTools(server, { getBrowser }) {
|
|
|
21
27
|
idempotentHint: true,
|
|
22
28
|
openWorldHint: false,
|
|
23
29
|
},
|
|
24
|
-
}, async () => {
|
|
25
|
-
|
|
30
|
+
}, async ({ _browserToken }) => {
|
|
31
|
+
// Reports on page structure, so a blank-page answer (0 forms, 0 buttons,
|
|
32
|
+
// 0 links) reads as a real finding rather than an error.
|
|
33
|
+
const unbound = refuseUnboundSession("analyze_page", { getBrowserByToken }, _browserToken);
|
|
34
|
+
if (unbound)
|
|
35
|
+
return unbound;
|
|
36
|
+
let b;
|
|
37
|
+
let token;
|
|
38
|
+
if (getBrowserByToken) {
|
|
39
|
+
const resolved = await getBrowserByToken(_browserToken);
|
|
40
|
+
b = resolved.browser;
|
|
41
|
+
token = resolved.token;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
b = await getBrowser();
|
|
45
|
+
}
|
|
26
46
|
const analysis = await b.analyzePage();
|
|
27
47
|
return {
|
|
28
48
|
content: [
|
|
29
49
|
{
|
|
30
50
|
type: "text",
|
|
31
51
|
text: JSON.stringify({
|
|
52
|
+
...(token ? { _browserToken: token } : {}),
|
|
32
53
|
title: analysis.title,
|
|
33
54
|
forms: analysis.forms.length,
|
|
34
55
|
buttons: analysis.buttons.length,
|
|
@@ -46,6 +67,10 @@ export function registerAnalysisTools(server, { getBrowser }) {
|
|
|
46
67
|
description: "Generate test scenarios for a page",
|
|
47
68
|
inputSchema: {
|
|
48
69
|
url: z.string().url().optional().describe("URL to analyze (uses current page if not provided)"),
|
|
70
|
+
// "uses current page if not provided" was unreachable on the HTTP transport:
|
|
71
|
+
// omitting url fell back to a blank page, so the no-url branch generated
|
|
72
|
+
// tests for about:blank. (2026-07-28)
|
|
73
|
+
_browserToken: z.string().optional().describe("Browser session token from a previous tool call — required when url is omitted"),
|
|
49
74
|
},
|
|
50
75
|
annotations: {
|
|
51
76
|
title: "Generate Test Cases",
|
|
@@ -54,20 +79,52 @@ export function registerAnalysisTools(server, { getBrowser }) {
|
|
|
54
79
|
idempotentHint: true,
|
|
55
80
|
openWorldHint: false,
|
|
56
81
|
},
|
|
57
|
-
}, async ({ url }) => {
|
|
58
|
-
|
|
82
|
+
}, async ({ url, _browserToken }) => {
|
|
83
|
+
// Only when it is being asked about "the current page". With an explicit
|
|
84
|
+
// url it navigates itself and needs no session.
|
|
85
|
+
if (!url) {
|
|
86
|
+
const unbound = refuseUnboundSession("generate_tests", { getBrowserByToken }, _browserToken);
|
|
87
|
+
if (unbound)
|
|
88
|
+
return unbound;
|
|
89
|
+
}
|
|
90
|
+
let b;
|
|
91
|
+
let token;
|
|
92
|
+
if (getBrowserByToken) {
|
|
93
|
+
const resolved = await getBrowserByToken(_browserToken);
|
|
94
|
+
b = resolved.browser;
|
|
95
|
+
token = resolved.token;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
b = await getBrowser();
|
|
99
|
+
}
|
|
59
100
|
const result = await b.generateTests(url);
|
|
60
101
|
return {
|
|
61
102
|
content: [
|
|
62
103
|
{
|
|
63
104
|
type: "text",
|
|
64
105
|
text: JSON.stringify({
|
|
106
|
+
...(token ? { _browserToken: token } : {}),
|
|
65
107
|
testsGenerated: result.tests.length,
|
|
108
|
+
// `steps` used to be t.steps.length — a bare count. A caller got
|
|
109
|
+
// "3 steps" with no way to see or run them, so there was no handoff
|
|
110
|
+
// into nl_test_inline / nl_test_file and the tool could only be
|
|
111
|
+
// read, not used. The step text and a runnable script were both
|
|
112
|
+
// already computed and then discarded. (2026-07-29)
|
|
66
113
|
tests: result.tests.map(t => ({
|
|
67
114
|
name: t.name,
|
|
68
115
|
description: t.description,
|
|
69
|
-
|
|
116
|
+
stepCount: t.steps.length,
|
|
117
|
+
steps: t.steps.map(st => ({
|
|
118
|
+
action: st.action,
|
|
119
|
+
...(st.target ? { target: st.target } : {}),
|
|
120
|
+
...(st.value !== undefined ? { value: st.value } : {}),
|
|
121
|
+
description: st.description,
|
|
122
|
+
})),
|
|
123
|
+
assertions: t.assertions,
|
|
70
124
|
})),
|
|
125
|
+
// Ready to paste straight into nl_test_inline.
|
|
126
|
+
cbrowserScript: result.cbrowserScript,
|
|
127
|
+
usage: "Pass cbrowserScript to nl_test_inline as `content`, or write it to a file and use nl_test_file.",
|
|
71
128
|
}, null, 2),
|
|
72
129
|
},
|
|
73
130
|
],
|
|
@@ -79,6 +136,11 @@ export function registerAnalysisTools(server, { getBrowser }) {
|
|
|
79
136
|
inputSchema: {
|
|
80
137
|
intent: z.string().describe("Natural language description like 'the cheapest product' or 'login form'"),
|
|
81
138
|
verbose: z.boolean().optional().describe("Include alternative matches with confidence scores and AI suggestions"),
|
|
139
|
+
// Without this, the tool searched a blank browser on the HTTP transport and
|
|
140
|
+
// returned `confidence: 0, "No match found"` with empty alternatives — a
|
|
141
|
+
// plausible "nothing here" that is indistinguishable from a real miss.
|
|
142
|
+
// (2026-07-28)
|
|
143
|
+
_browserToken: z.string().optional().describe("Browser session token from a previous tool call"),
|
|
82
144
|
},
|
|
83
145
|
annotations: {
|
|
84
146
|
title: "Find Element by Intent",
|
|
@@ -87,8 +149,19 @@ export function registerAnalysisTools(server, { getBrowser }) {
|
|
|
87
149
|
idempotentHint: true,
|
|
88
150
|
openWorldHint: false,
|
|
89
151
|
},
|
|
90
|
-
}, async ({ intent, verbose }) => {
|
|
91
|
-
|
|
152
|
+
}, async ({ intent, verbose, _browserToken }) => {
|
|
153
|
+
// Searches the page, so a blank-browser search returns "no match found"
|
|
154
|
+
// with confidence 0 — indistinguishable from a genuine miss.
|
|
155
|
+
const unbound = refuseUnboundSession("find_element_by_intent", { getBrowserByToken }, _browserToken);
|
|
156
|
+
if (unbound)
|
|
157
|
+
return unbound;
|
|
158
|
+
let b;
|
|
159
|
+
if (getBrowserByToken) {
|
|
160
|
+
b = (await getBrowserByToken(_browserToken)).browser;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
b = await getBrowser();
|
|
164
|
+
}
|
|
92
165
|
const result = await findElementByIntent(b, intent, { verbose });
|
|
93
166
|
if (result && result.confidence > 0) {
|
|
94
167
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analysis-tools.js","sourceRoot":"","sources":["../../../src/mcp-tools/base/analysis-tools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAEvF;;GAEG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAAiB,EACjB,EAAE,UAAU,EAA2B;
|
|
1
|
+
{"version":3,"file":"analysis-tools.js","sourceRoot":"","sources":["../../../src/mcp-tools/base/analysis-tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,5 +8,5 @@ import type { McpServer, ToolRegistrationContext } from "../types.js";
|
|
|
8
8
|
/**
|
|
9
9
|
* Register assertion tools (1 tool: assert)
|
|
10
10
|
*/
|
|
11
|
-
export declare function registerAssertionTools(server: McpServer, { getBrowser }: ToolRegistrationContext): void;
|
|
11
|
+
export declare function registerAssertionTools(server: McpServer, { getBrowser, getBrowserByToken }: ToolRegistrationContext): void;
|
|
12
12
|
//# sourceMappingURL=assertion-tools.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertion-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp-tools/base/assertion-tools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"assertion-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp-tools/base/assertion-tools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAGtE;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,SAAS,EACjB,EAAE,UAAU,EAAE,iBAAiB,EAAE,EAAE,uBAAuB,GACzD,IAAI,CAqDN"}
|
|
@@ -5,15 +5,22 @@
|
|
|
5
5
|
* @license MIT
|
|
6
6
|
*/
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { refuseUnboundSession } from "../session-policy.js";
|
|
8
9
|
/**
|
|
9
10
|
* Register assertion tools (1 tool: assert)
|
|
10
11
|
*/
|
|
11
|
-
export function registerAssertionTools(server, { getBrowser }) {
|
|
12
|
+
export function registerAssertionTools(server, { getBrowser, getBrowserByToken }) {
|
|
12
13
|
server.registerTool("assert", {
|
|
13
14
|
title: "Assert Page State",
|
|
14
15
|
description: "Assert a condition using natural language",
|
|
15
16
|
inputSchema: {
|
|
16
17
|
assertion: z.string().describe("Natural language assertion like \"page contains 'Welcome'\" or \"title is 'Home'\""),
|
|
18
|
+
// Without this, assert could not bind to the session navigate created on the
|
|
19
|
+
// HTTP transport: it asserted against a blank page and returned
|
|
20
|
+
// `passed:false, actual:""`. Worse than a crash — a NEGATIVE assertion
|
|
21
|
+
// ("page does not contain 'error'") then PASSED against nothing, so a whole
|
|
22
|
+
// suite could report green while testing an empty browser. (2026-07-28)
|
|
23
|
+
_browserToken: z.string().optional().describe("Browser session token from a previous tool call"),
|
|
17
24
|
},
|
|
18
25
|
annotations: {
|
|
19
26
|
title: "Assert Page State",
|
|
@@ -22,8 +29,23 @@ export function registerAssertionTools(server, { getBrowser }) {
|
|
|
22
29
|
idempotentHint: true,
|
|
23
30
|
openWorldHint: false,
|
|
24
31
|
},
|
|
25
|
-
}, async ({ assertion }) => {
|
|
26
|
-
|
|
32
|
+
}, async ({ assertion, _browserToken }) => {
|
|
33
|
+
// Adding the token was only half the fix: omitting it entirely still
|
|
34
|
+
// asserted against a blank page and returned a confident verdict, with a
|
|
35
|
+
// fresh _browserToken attached that made it look session-backed.
|
|
36
|
+
const unbound = refuseUnboundSession("assert", { getBrowserByToken }, _browserToken);
|
|
37
|
+
if (unbound)
|
|
38
|
+
return unbound;
|
|
39
|
+
let b;
|
|
40
|
+
let token;
|
|
41
|
+
if (getBrowserByToken) {
|
|
42
|
+
const resolved = await getBrowserByToken(_browserToken);
|
|
43
|
+
b = resolved.browser;
|
|
44
|
+
token = resolved.token;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
b = await getBrowser();
|
|
48
|
+
}
|
|
27
49
|
const result = await b.assert(assertion);
|
|
28
50
|
return {
|
|
29
51
|
content: [
|
|
@@ -34,6 +56,7 @@ export function registerAssertionTools(server, { getBrowser }) {
|
|
|
34
56
|
message: result.message,
|
|
35
57
|
actual: result.actual,
|
|
36
58
|
expected: result.expected,
|
|
59
|
+
...(token ? { _browserToken: token } : {}),
|
|
37
60
|
}, null, 2),
|
|
38
61
|
},
|
|
39
62
|
],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertion-tools.js","sourceRoot":"","sources":["../../../src/mcp-tools/base/assertion-tools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"assertion-tools.js","sourceRoot":"","sources":["../../../src/mcp-tools/base/assertion-tools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D;;GAEG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAAiB,EACjB,EAAE,UAAU,EAAE,iBAAiB,EAA2B;IAE1D,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE;QAC5B,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EAAE,2CAA2C;QACxD,WAAW,EAAE;YACX,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,oFAAoF,CAAC;YACpH,6EAA6E;YAC7E,gEAAgE;YAChE,uEAAuE;YACvE,4EAA4E;YAC5E,wEAAwE;YACxE,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iDAAiD,CAAC;SACjG;QACD,WAAW,EAAE;YACX,KAAK,EAAE,mBAAmB;YAC1B,YAAY,EAAE,IAAI;YAClB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,IAAI;YACpB,aAAa,EAAE,KAAK;SACrB;KACF,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,aAAa,EAAE,EAAE,EAAE;QACtC,qEAAqE;QACrE,yEAAyE;QACzE,iEAAiE;QACjE,MAAM,OAAO,GAAG,oBAAoB,CAAC,QAAQ,EAAE,EAAE,iBAAiB,EAAE,EAAE,aAAa,CAAC,CAAC;QACrF,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;QAE5B,IAAI,CAAC,CAAC;QACN,IAAI,KAAyB,CAAC;QAC9B,IAAI,iBAAiB,EAAE,CAAC;YACtB,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,aAAa,CAAC,CAAC;YACxD,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC;YACrB,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,CAAC,GAAG,MAAM,UAAU,EAAE,CAAC;QACzB,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACzC,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACnB,MAAM,EAAE,MAAM,CAAC,MAAM;wBACrB,OAAO,EAAE,MAAM,CAAC,OAAO;wBACvB,MAAM,EAAE,MAAM,CAAC,MAAM;wBACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;wBACzB,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;qBAC3C,EAAE,IAAI,EAAE,CAAC,CAAC;iBACZ;aACF;SACF,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp-tools/base/audit-tools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"audit-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp-tools/base/audit-tools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAoDtE;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,IAAI,CA4rC7F"}
|
|
@@ -5,8 +5,52 @@
|
|
|
5
5
|
* @license MIT
|
|
6
6
|
*/
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { randomBytes } from "crypto";
|
|
9
|
+
import { htmlUiResource, attachUiResource, uiResourcesEnabled } from "../../mcp-ui-resources.js";
|
|
10
|
+
import { writeArtifact } from "../../artifact-store.js";
|
|
8
11
|
import { runAgentReadyAudit, runCompetitiveBenchmark, runEmpathyAudit, runWebMCPReadyAudit, } from "../../analysis/index.js";
|
|
9
12
|
import { listAccessibilityPersonas } from "../../personas.js";
|
|
13
|
+
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
/**
|
|
17
|
+
* Persist an audit screenshot and return its path.
|
|
18
|
+
*
|
|
19
|
+
* empathy_audit inlined base64 page screenshots, which routinely overflowed an
|
|
20
|
+
* MCP client's context and made the tool untestable through a normal client.
|
|
21
|
+
* Writing them out keeps the overlay data usable without carrying megabytes
|
|
22
|
+
* through the transport. Returns undefined rather than throwing if the write
|
|
23
|
+
* fails — a screenshot is not worth failing an audit over. (2026-07-29)
|
|
24
|
+
*/
|
|
25
|
+
function writeAuditScreenshot(base64, persona) {
|
|
26
|
+
if (!base64)
|
|
27
|
+
return {};
|
|
28
|
+
const safe = String(persona || "persona").toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
29
|
+
const out = {};
|
|
30
|
+
// Local copy, as before — the CLI and anything on this box reads this.
|
|
31
|
+
try {
|
|
32
|
+
const dir = join(process.env.CBROWSER_DATA_DIR || join(homedir(), ".cbrowser"), "audit-screenshots");
|
|
33
|
+
if (!existsSync(dir))
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
const path = join(dir, `empathy-${safe}-${Date.now()}.png`);
|
|
36
|
+
writeFileSync(path, Buffer.from(base64, "base64"));
|
|
37
|
+
out.path = path;
|
|
38
|
+
}
|
|
39
|
+
catch { /* a screenshot is not worth failing an audit over */ }
|
|
40
|
+
// Served copy, so the barrier overlay has an image an MCP host can fetch.
|
|
41
|
+
// The name carries 12 random hex chars rather than only a timestamp: these
|
|
42
|
+
// are captures of a CUSTOMER'S page served from a public path, and a
|
|
43
|
+
// timestamped name is guessable by anyone who knows roughly when an audit
|
|
44
|
+
// ran. Unguessability is the only access control this path has. (2026-07-29)
|
|
45
|
+
try {
|
|
46
|
+
const nonce = randomBytes(6).toString("hex");
|
|
47
|
+
const written = writeArtifact(Buffer.from(base64, "base64"), `empathy-${safe}-${nonce}.png`);
|
|
48
|
+
if (written)
|
|
49
|
+
out.url = written.url;
|
|
50
|
+
}
|
|
51
|
+
catch { /* overlay is optional; the audit is not */ }
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
10
54
|
/**
|
|
11
55
|
* Register audit tools (4 tools + site_cognitive_assessment + visual_cognitive_story)
|
|
12
56
|
*/
|
|
@@ -92,8 +136,14 @@ export function registerAuditTools(server, context) {
|
|
|
92
136
|
wcagLevel: z.enum(["A", "AA", "AAA"]).optional().default("AA").describe("WCAG conformance level"),
|
|
93
137
|
maxSteps: z.number().optional().default(5).describe("Max cognitive journey steps (keep low for MCP)"),
|
|
94
138
|
maxTime: z.number().optional().default(20).describe("Max time per persona in seconds"),
|
|
139
|
+
// Base64 page screenshots are large enough to blow an MCP client's context
|
|
140
|
+
// on a single call — every empathy_audit in the 18.73.3 test pass had to be
|
|
141
|
+
// read off disk instead. Off by default; images go to disk and their paths
|
|
142
|
+
// are returned. (2026-07-29)
|
|
143
|
+
includeScreenshots: z.boolean().optional().default(false).describe("Inline base64 page screenshots in the response. Off by default because they are large enough to overflow an MCP client's context; when off, screenshots are written to disk and screenshotPath is returned instead."),
|
|
95
144
|
scope: z.enum(["viewport", "full_page"]).optional().default("viewport").describe("What to score: 'viewport' (first impression, above-the-fold only — default) or 'full_page' (scroll through entire page, all barriers). Use 'viewport' for landing page optimization; 'full_page' for WCAG compliance audits."),
|
|
96
145
|
device: z.string().optional().describe("Device emulation: 'mobile', 'tablet', 'desktop', or specific device like 'iPhone 15', 'Pixel 7'. Essential for mobile WCAG audits — touch targets, viewport sizing, and responsive barriers differ significantly on mobile."),
|
|
146
|
+
uiResource: z.boolean().optional().default(true).describe("Return an interactive HTML report as an MCP UI resource alongside the JSON. Hosts that support MCP Apps render it inline; others ignore it. Set false for scripted callers that diff whole responses."),
|
|
97
147
|
},
|
|
98
148
|
annotations: {
|
|
99
149
|
title: "Empathy Accessibility Audit",
|
|
@@ -102,7 +152,7 @@ export function registerAuditTools(server, context) {
|
|
|
102
152
|
idempotentHint: false,
|
|
103
153
|
openWorldHint: true,
|
|
104
154
|
},
|
|
105
|
-
}, async ({ url, goal, disabilities, wcagLevel, maxSteps, maxTime, scope, device }) => {
|
|
155
|
+
}, async ({ url, goal, disabilities, wcagLevel, maxSteps, maxTime, scope, device, includeScreenshots, uiResource }) => {
|
|
106
156
|
try {
|
|
107
157
|
// Auto-limit to 1 persona to avoid MCP client timeout on Claude.ai (~60s limit)
|
|
108
158
|
const allPersonas = listAccessibilityPersonas();
|
|
@@ -140,11 +190,23 @@ export function registerAuditTools(server, context) {
|
|
|
140
190
|
goalAchieved: r.goalAchieved,
|
|
141
191
|
empathyScore: r.empathyScore,
|
|
142
192
|
// v18.22.0: Added score context for transparency
|
|
193
|
+
// Whitelist projection: anything added to scoreContext upstream is
|
|
194
|
+
// invisible to the caller unless named here. The blend breakdown
|
|
195
|
+
// below is what lets the deductions reconcile to a score the
|
|
196
|
+
// caller can actually see. (2026-07-28)
|
|
143
197
|
scoreContext: r.scoreContext ? {
|
|
144
198
|
explanation: r.scoreContext.explanation,
|
|
145
199
|
deductionsByType: r.scoreContext.deductionsByType,
|
|
200
|
+
totalBarrierDeduction: r.scoreContext.totalBarrierDeduction,
|
|
146
201
|
frictionDeduction: r.scoreContext.frictionDeduction,
|
|
147
202
|
goalDeduction: r.scoreContext.goalDeduction,
|
|
203
|
+
cognitiveOverloadPenalty: r.scoreContext.cognitiveOverloadPenalty,
|
|
204
|
+
appliedBarrierWeights: r.scoreContext.appliedBarrierWeights,
|
|
205
|
+
explainsScore: r.scoreContext.explainsScore,
|
|
206
|
+
barrierOnlyScore: r.scoreContext.barrierOnlyScore,
|
|
207
|
+
perceptualScore: r.scoreContext.perceptualScore,
|
|
208
|
+
blendWeights: r.scoreContext.blendWeights,
|
|
209
|
+
finalScore: r.scoreContext.finalScore,
|
|
148
210
|
} : undefined,
|
|
149
211
|
barrierTypeCount: uniqueTypes.size,
|
|
150
212
|
barrierTypes: Array.from(uniqueTypes),
|
|
@@ -155,6 +217,10 @@ export function registerAuditTools(server, context) {
|
|
|
155
217
|
empathyScoreBarrierOnly: r.empathyScoreBarrierOnly || undefined,
|
|
156
218
|
// v18.27.0: Cognitive load estimation (optimal transport)
|
|
157
219
|
cognitiveLoad: r.cognitiveLoad || undefined,
|
|
220
|
+
// Names the three different "cognitive load" numbers in this
|
|
221
|
+
// payload and what each measures. Same whitelist caveat as
|
|
222
|
+
// scoreContext above: set upstream, invisible unless named here.
|
|
223
|
+
cognitiveLoadReadings: r.cognitiveLoadReadings || undefined,
|
|
158
224
|
// v18.28.0: Attention transport analysis (W₂ saliency)
|
|
159
225
|
attentionAnalysis: r.attentionAnalysis || undefined,
|
|
160
226
|
// v18.29.0: Journey validation — evidence, path, forensics
|
|
@@ -166,34 +232,123 @@ export function registerAuditTools(server, context) {
|
|
|
166
232
|
}),
|
|
167
233
|
allWcagViolations: result.allWcagViolations,
|
|
168
234
|
topBarriers: result.topBarriers.slice(0, 5),
|
|
235
|
+
// A barrier's affectedPersonas names the personas it hits HARDEST. It
|
|
236
|
+
// is not an exclusion list, which is why a persona absent from it can
|
|
237
|
+
// still take a deduction — susceptibility is applied as a weight, not
|
|
238
|
+
// a gate (see scoreContext.appliedBarrierWeights). Stating it here
|
|
239
|
+
// because the pairing otherwise reads as a contradiction: a
|
|
240
|
+
// touch_target barrier listing motor and elderly personas, scored
|
|
241
|
+
// against cognitive-adhd. (2026-07-29)
|
|
242
|
+
barrierFieldNotes: {
|
|
243
|
+
affectedPersonas: "Exemplars — the personas this barrier affects most. NOT an exclusion list; every tested persona is scored against every barrier, weighted by susceptibility.",
|
|
244
|
+
severity: "On topBarriers entries this is the group maximum across all elements of that type (severityIsGroupMax), not one element's severity.",
|
|
245
|
+
},
|
|
169
246
|
topRemediation: result.combinedRemediation.slice(0, 5),
|
|
170
247
|
duration: result.duration,
|
|
171
248
|
// v18.60.0: Include per-result screenshots and element rects for WCAG overlay
|
|
172
249
|
pageScreenshots: result.results?.map((r) => ({
|
|
173
250
|
persona: r.persona,
|
|
174
|
-
|
|
251
|
+
...(includeScreenshots
|
|
252
|
+
? { screenshot: r.pageScreenshot }
|
|
253
|
+
: (() => {
|
|
254
|
+
const shot = writeAuditScreenshot(r.pageScreenshot, r.persona);
|
|
255
|
+
return {
|
|
256
|
+
screenshotPath: shot.path,
|
|
257
|
+
screenshotUrl: shot.url,
|
|
258
|
+
screenshotNote: "Pass includeScreenshots:true to inline the base64 instead.",
|
|
259
|
+
};
|
|
260
|
+
})()),
|
|
175
261
|
viewportSize: r.viewportSize,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
262
|
+
// Rects come from getBoundingClientRect with no scroll compensation,
|
|
263
|
+
// so they are VIEWPORT coordinates — which is correct here, because
|
|
264
|
+
// the screenshot above is captured with fullPage: false and these are
|
|
265
|
+
// meant to overlay it. Converting them to document coordinates would
|
|
266
|
+
// break that alignment.
|
|
267
|
+
//
|
|
268
|
+
// The real hazard is scope: "full_page", where the audit scrolls and
|
|
269
|
+
// rects captured at different offsets land in one list against a
|
|
270
|
+
// single viewport image; anything above the captured viewport then
|
|
271
|
+
// carries a negative y and would render off-canvas. Say which space
|
|
272
|
+
// these are in and flag the ones that fall outside the image rather
|
|
273
|
+
// than emitting silent negatives. (2026-07-28)
|
|
274
|
+
// Rects are now DOCUMENT coordinates (scroll offset added at capture),
|
|
275
|
+
// so they are stable across the scrolling that scope:"full_page" does.
|
|
276
|
+
// They were previously labelled "viewport" while holding values from
|
|
277
|
+
// several scroll positions at once — y:-274 and y:3951 in one list.
|
|
278
|
+
coordinateSpace: "document",
|
|
279
|
+
// Height of the captured image in document space. A viewport capture
|
|
280
|
+
// covers only the scroll position it was taken at; a full_page capture
|
|
281
|
+
// covers the document. Without this the bounds test compared document
|
|
282
|
+
// coordinates against a viewport height and flagged EVERY rect,
|
|
283
|
+
// including ones plainly inside the image. (2026-07-29)
|
|
284
|
+
captureHeight: (scope === "full_page" ? (r.documentHeight ?? undefined) : r.viewportSize?.height),
|
|
285
|
+
barrierRects: r.barriers?.filter((b) => b.rect).map((b) => {
|
|
286
|
+
const vp = r.viewportSize;
|
|
287
|
+
const bound = scope === "full_page" ? (r.documentHeight ?? Infinity) : (vp?.height ?? Infinity);
|
|
288
|
+
const outside = !!b.rect && (b.rect.y + b.rect.height < 0 || b.rect.x + b.rect.width < 0 ||
|
|
289
|
+
b.rect.y > bound || (!!vp && b.rect.x > vp.width));
|
|
290
|
+
return {
|
|
291
|
+
type: b.type, severity: b.severity, element: b.element,
|
|
292
|
+
description: b.description, rect: b.rect,
|
|
293
|
+
wcag: b.wcagCriteria,
|
|
294
|
+
...(outside ? { outsideScreenshot: true } : {}),
|
|
295
|
+
};
|
|
296
|
+
}),
|
|
297
|
+
// Keep entries that carry EITHER the inline image or a path to it.
|
|
298
|
+
// Filtering on `screenshot` alone silently dropped every entry once
|
|
299
|
+
// the default stopped inlining base64, taking the barrierRects with
|
|
300
|
+
// them. (2026-07-29)
|
|
301
|
+
})).filter((s) => s.screenshot || s.screenshotPath),
|
|
182
302
|
};
|
|
183
303
|
// Add guidance if we limited the request
|
|
184
304
|
if (wasLimited) {
|
|
185
305
|
response.note = `Limited to 1 persona to avoid timeout. For full coverage, call again with: ${remainingPersonas.slice(0, 3).join(", ")}${remainingPersonas.length > 3 ? `, and ${remainingPersonas.length - 3} more` : ""}`;
|
|
186
306
|
response.remainingPersonas = remainingPersonas;
|
|
187
307
|
}
|
|
188
|
-
// Auto-save handled by tier-gate wrapper
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
308
|
+
// Auto-save handled by tier-gate wrapper.
|
|
309
|
+
//
|
|
310
|
+
// The UI resource is APPENDED to the text block, never substituted for
|
|
311
|
+
// it: content[0] stays the same JSON every CLI and CI caller already
|
|
312
|
+
// parses. `generateEmpathyAuditHtmlReport` is the report the --html flag
|
|
313
|
+
// has always produced — self-contained, no remote refs — so this is a
|
|
314
|
+
// transport seam rather than new UI. If it cannot be produced safely,
|
|
315
|
+
// htmlUiResource returns null and the response is exactly as before.
|
|
316
|
+
// (2026-07-29)
|
|
317
|
+
const content = [
|
|
318
|
+
{ type: "text", text: JSON.stringify(response, null, 2) },
|
|
319
|
+
];
|
|
320
|
+
if (uiResourcesEnabled(uiResource)) {
|
|
321
|
+
// The overlay goes FIRST. It is the thing that makes a score
|
|
322
|
+
// falsifiable by eye — barriers drawn on the page at their real
|
|
323
|
+
// positions — and the text report is the detail behind it.
|
|
324
|
+
try {
|
|
325
|
+
const { buildBarrierOverlayHtml } = await import("../../visual/barrier-overlay-html.js");
|
|
326
|
+
const shots = (response.pageScreenshots ?? []);
|
|
327
|
+
for (const shot of shots) {
|
|
328
|
+
const overlayHtml = buildBarrierOverlayHtml({
|
|
329
|
+
imageUrl: shot.screenshotUrl,
|
|
330
|
+
imageWidth: shot.viewportSize?.width,
|
|
331
|
+
captureHeight: shot.captureHeight ?? shot.viewportSize?.height,
|
|
332
|
+
barrierRects: shot.barrierRects ?? [],
|
|
333
|
+
persona: String(shot.persona ?? testedPersona),
|
|
334
|
+
pageUrl: String(result.url),
|
|
335
|
+
score: result.overallScore,
|
|
336
|
+
});
|
|
337
|
+
attachUiResource(content, htmlUiResource(`ui://cbrowser/barrier-overlay/${encodeURIComponent(String(shot.persona ?? testedPersona))}`, overlayHtml ?? undefined, { frameSize: ["100%", "820px"] }));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch { /* overlay is additive; never fail the audit for it */ }
|
|
341
|
+
let reportHtml;
|
|
342
|
+
try {
|
|
343
|
+
const { generateEmpathyAuditHtmlReport } = await import("../../analysis/accessibility-empathy.js");
|
|
344
|
+
reportHtml = generateEmpathyAuditHtmlReport(result);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
reportHtml = undefined;
|
|
348
|
+
}
|
|
349
|
+
attachUiResource(content, htmlUiResource(`ui://cbrowser/empathy-audit/${encodeURIComponent(testedPersona)}`, reportHtml, { frameSize: ["100%", "760px"] }));
|
|
350
|
+
}
|
|
351
|
+
return { content };
|
|
197
352
|
}
|
|
198
353
|
catch (error) {
|
|
199
354
|
// Categorize the error for better user feedback
|
|
@@ -747,9 +902,11 @@ export function registerAuditTools(server, context) {
|
|
|
747
902
|
const page = await browser.getPage();
|
|
748
903
|
const ssPath = join(tmpdir(), `story-${Date.now()}.png`);
|
|
749
904
|
await page.screenshot({ path: ssPath, fullPage: false });
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
905
|
+
// webDir removed: this pointed at /var/www/cbrowser-web/heatmaps, a
|
|
906
|
+
// FIFTH directory nginx does not serve (it serves /heatmaps/ from
|
|
907
|
+
// /var/www/cbrowser-data/heatmaps). All four story overlays below wrote
|
|
908
|
+
// there and returned cbrowser.ai URLs that 404'd. (2026-07-29)
|
|
909
|
+
// (directory creation is the artifact store's job now)
|
|
753
910
|
const ts = Date.now();
|
|
754
911
|
const images = [];
|
|
755
912
|
const urls = {};
|
|
@@ -768,8 +925,9 @@ export function registerAuditTools(server, context) {
|
|
|
768
925
|
const { generateHeatmapOverlay } = await import("../../visual/heatmap-overlay.js");
|
|
769
926
|
const heatB64 = await generateHeatmapOverlay(ssPath, attnResult.saliencyMap.cells, attnResult.saliencyMap.rows, attnResult.saliencyMap.cols, `${persona} Attention`);
|
|
770
927
|
const heatId = `story-attn-${persona}-${ts}`;
|
|
771
|
-
|
|
772
|
-
|
|
928
|
+
const w_attention = writeArtifact(Buffer.from(heatB64, "base64"), `${heatId}.png`);
|
|
929
|
+
if (w_attention)
|
|
930
|
+
urls.attention = w_attention.url;
|
|
773
931
|
images.push({ type: "image", data: heatB64, mimeType: "image/png" });
|
|
774
932
|
// Auto-save to gallery
|
|
775
933
|
try {
|
|
@@ -815,8 +973,9 @@ export function registerAuditTools(server, context) {
|
|
|
815
973
|
const cssVW = await page.evaluate(() => window.innerWidth).catch(() => 1920);
|
|
816
974
|
const motorB64 = await generateMotorOverlay(ssPath, motorElements, persona, cssVW);
|
|
817
975
|
const motorId = `story-motor-${persona}-${ts}`;
|
|
818
|
-
|
|
819
|
-
|
|
976
|
+
const w_motor = writeArtifact(Buffer.from(motorB64, "base64"), `${motorId}.png`);
|
|
977
|
+
if (w_motor)
|
|
978
|
+
urls.motor = w_motor.url;
|
|
820
979
|
images.push({ type: "image", data: motorB64, mimeType: "image/png" });
|
|
821
980
|
try {
|
|
822
981
|
const { saveVisualReport } = await import("../visual-report-saver.js");
|
|
@@ -892,8 +1051,9 @@ export function registerAuditTools(server, context) {
|
|
|
892
1051
|
const cssVW2 = await page.evaluate(() => window.innerWidth).catch(() => 1920);
|
|
893
1052
|
const qualB64 = await generateAttentionQualityOverlay(ssPath, overlayTargets, persona, cssVW2);
|
|
894
1053
|
const qualId = `story-quality-${persona}-${ts}`;
|
|
895
|
-
|
|
896
|
-
|
|
1054
|
+
const w_quality = writeArtifact(Buffer.from(qualB64, "base64"), `${qualId}.png`);
|
|
1055
|
+
if (w_quality)
|
|
1056
|
+
urls.quality = w_quality.url;
|
|
897
1057
|
images.push({ type: "image", data: qualB64, mimeType: "image/png" });
|
|
898
1058
|
try {
|
|
899
1059
|
const { saveVisualReport } = await import("../visual-report-saver.js");
|
|
@@ -963,8 +1123,9 @@ export function registerAuditTools(server, context) {
|
|
|
963
1123
|
const cssVW3 = await page.evaluate(() => window.innerWidth).catch(() => 1920);
|
|
964
1124
|
const combinedB64 = await generateCombinedStoryOverlay(ssPath, attentionData?.saliencyMap?.cells ?? null, attentionData?.saliencyMap?.rows ?? 0, attentionData?.saliencyMap?.cols ?? 0, motorEls, qualTargets, persona, cssVW3);
|
|
965
1125
|
const combId = `story-combined-${persona}-${ts}`;
|
|
966
|
-
|
|
967
|
-
|
|
1126
|
+
const w_combined = writeArtifact(Buffer.from(combinedB64, "base64"), `${combId}.png`);
|
|
1127
|
+
if (w_combined)
|
|
1128
|
+
urls.combined = w_combined.url;
|
|
968
1129
|
images.push({ type: "image", data: combinedB64, mimeType: "image/png" });
|
|
969
1130
|
try {
|
|
970
1131
|
const { saveVisualReport } = await import("../visual-report-saver.js");
|