@cloudhal/mcp-server 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +18 -2
- package/dist/src/analytics-reports.d.ts +164 -0
- package/dist/src/analytics-reports.js +632 -0
- package/dist/src/analytics-tools.d.ts +4 -0
- package/dist/src/analytics-tools.js +597 -0
- package/dist/src/asset-tools.d.ts +5 -0
- package/dist/src/asset-tools.js +685 -0
- package/dist/src/body-tools.d.ts +46 -0
- package/dist/src/body-tools.js +708 -0
- package/dist/src/changes.d.ts +10 -0
- package/dist/src/changes.js +112 -0
- package/dist/src/chrome-tools.d.ts +8 -0
- package/dist/src/chrome-tools.js +207 -0
- package/dist/src/comment-tools.d.ts +9 -0
- package/dist/src/comment-tools.js +743 -0
- package/dist/src/comments.d.ts +184 -0
- package/dist/src/comments.js +408 -0
- package/dist/src/common.d.ts +258 -0
- package/dist/src/common.js +488 -0
- package/dist/src/content-shared.d.ts +50 -0
- package/dist/src/content-shared.js +102 -0
- package/dist/src/design-tools.d.ts +9 -0
- package/dist/src/design-tools.js +349 -0
- package/dist/src/elevation-tools.d.ts +3 -0
- package/dist/src/elevation-tools.js +331 -0
- package/dist/src/entry-terms.d.ts +14 -0
- package/dist/src/entry-terms.js +51 -0
- package/dist/src/entry-tools.d.ts +4 -0
- package/dist/src/entry-tools.js +442 -0
- package/dist/src/http.d.ts +48 -0
- package/dist/src/http.js +101 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +10 -0
- package/dist/src/menu-tools.d.ts +41 -0
- package/dist/src/menu-tools.js +1058 -0
- package/dist/src/page-tools.d.ts +5 -0
- package/dist/src/page-tools.js +311 -0
- package/dist/src/preview-doc.d.ts +9 -0
- package/dist/src/preview-doc.js +56 -0
- package/dist/src/preview-view.d.ts +19 -0
- package/dist/src/preview-view.js +71 -0
- package/dist/src/publish-tools.d.ts +10 -0
- package/dist/src/publish-tools.js +248 -0
- package/dist/src/render/cdp.d.ts +29 -0
- package/dist/src/render/cdp.js +190 -0
- package/dist/src/render/checks/components.d.ts +5 -0
- package/dist/src/render/checks/components.js +95 -0
- package/dist/src/render/checks/content.d.ts +5 -0
- package/dist/src/render/checks/content.js +63 -0
- package/dist/src/render/checks/contrast.d.ts +5 -0
- package/dist/src/render/checks/contrast.js +72 -0
- package/dist/src/render/checks/index.d.ts +1 -0
- package/dist/src/render/checks/index.js +29 -0
- package/dist/src/render/checks/layout.d.ts +5 -0
- package/dist/src/render/checks/layout.js +44 -0
- package/dist/src/render/checks/motion.d.ts +5 -0
- package/dist/src/render/checks/motion.js +66 -0
- package/dist/src/render/checks/navigation.d.ts +5 -0
- package/dist/src/render/checks/navigation.js +78 -0
- package/dist/src/render/checks/prelude.d.ts +1 -0
- package/dist/src/render/checks/prelude.js +64 -0
- package/dist/src/render/limiter.d.ts +11 -0
- package/dist/src/render/limiter.js +29 -0
- package/dist/src/render/page.d.ts +19 -0
- package/dist/src/render/page.js +135 -0
- package/dist/src/render/types.d.ts +89 -0
- package/dist/src/render/types.js +140 -0
- package/dist/src/render-tools.d.ts +14 -0
- package/dist/src/render-tools.js +314 -0
- package/dist/src/render.d.ts +14 -0
- package/dist/src/render.js +33 -0
- package/dist/src/schedule-tools.d.ts +2 -0
- package/dist/src/schedule-tools.js +1596 -0
- package/dist/src/schedules.d.ts +292 -0
- package/dist/src/schedules.js +890 -0
- package/dist/src/schema-transforms.d.ts +14 -0
- package/dist/src/schema-transforms.js +50 -0
- package/dist/src/search-tools.d.ts +2 -0
- package/dist/src/search-tools.js +112 -0
- package/dist/src/server-approvals.d.ts +51 -0
- package/dist/src/server-approvals.js +273 -0
- package/dist/src/server-call.d.ts +28 -0
- package/dist/src/server-call.js +259 -0
- package/dist/src/server-instructions.d.ts +4 -0
- package/dist/src/server-instructions.js +74 -0
- package/dist/src/server-resources.d.ts +4 -0
- package/dist/src/server-resources.js +83 -0
- package/dist/src/server-schema.d.ts +20 -0
- package/dist/src/server-schema.js +82 -0
- package/dist/src/server.d.ts +80 -0
- package/dist/src/server.js +135 -0
- package/dist/src/stdio.d.ts +9 -0
- package/dist/src/stdio.js +18 -0
- package/dist/src/surface-tools.d.ts +4 -0
- package/dist/src/surface-tools.js +357 -0
- package/dist/src/taxonomy-tools.d.ts +16 -0
- package/dist/src/taxonomy-tools.js +1433 -0
- package/dist/src/terms.d.ts +47 -0
- package/dist/src/terms.js +271 -0
- package/dist/src/tools.d.ts +39 -0
- package/dist/src/tools.js +511 -0
- package/dist/src/translation-tools.d.ts +3 -0
- package/dist/src/translation-tools.js +422 -0
- package/package.json +86 -3
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** The `render_page` contract: widths, check ids, options and results, the DevTools socket shapes. */
|
|
2
|
+
/** The widths `render_page` renders at, and the viewport height used at each. */
|
|
3
|
+
export declare const RENDER_WIDTHS: readonly [1440, 768, 390];
|
|
4
|
+
export type RenderWidth = (typeof RENDER_WIDTHS)[number];
|
|
5
|
+
export declare const VIEWPORT_HEIGHT: Record<RenderWidth, number>;
|
|
6
|
+
/** A text socket to a browser's DevTools endpoint (browser-level, not a single page). */
|
|
7
|
+
export interface CdpSocket {
|
|
8
|
+
send(message: string): void;
|
|
9
|
+
/** Called with each complete message. */
|
|
10
|
+
onMessage(listener: (message: string) => void): void;
|
|
11
|
+
onClose(listener: () => void): void;
|
|
12
|
+
close(): void;
|
|
13
|
+
}
|
|
14
|
+
/** A browser for one `render_page` call; `close` releases it. */
|
|
15
|
+
export interface RenderSession {
|
|
16
|
+
open(url: string, viewport: {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
scheme?: RenderScheme;
|
|
20
|
+
reducedMotion?: boolean;
|
|
21
|
+
}): Promise<RenderTab>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface RenderTab {
|
|
25
|
+
/** Evaluates an expression in the page, awaiting promises; the value comes back as JSON. */
|
|
26
|
+
evaluate<T>(expression: string): Promise<T>;
|
|
27
|
+
/** A JPEG of the viewport as it is now. */
|
|
28
|
+
screenshot(quality: number): Promise<Uint8Array>;
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
/** Where `render_page` gets its browsers from. */
|
|
32
|
+
export interface PageRenderer {
|
|
33
|
+
session(): Promise<RenderSession>;
|
|
34
|
+
}
|
|
35
|
+
/** One automatic check (RFC-0021 §4). `examples` name the section and component where known. */
|
|
36
|
+
export interface RenderCheck {
|
|
37
|
+
id: RenderCheckId;
|
|
38
|
+
ok: boolean;
|
|
39
|
+
count: number;
|
|
40
|
+
examples: string[];
|
|
41
|
+
/** The widths it failed at. */
|
|
42
|
+
widths: number[];
|
|
43
|
+
}
|
|
44
|
+
export declare const RENDER_CHECK_IDS: readonly ["overflow-x", "img-alt", "img-broken", "heading-order", "contrast", "empty-section", "tap-target", "long-line", "input-label", "chart-empty", "reduced-motion", "motion-cls", "motion-budget", "flash", "skip-link", "landmarks", "header-contrast", "focus-obscured", "overlay-open", "carousel-label", "carousel-lcp", "carousel-focus", "carousel-autoplay", "image-fit"];
|
|
45
|
+
export type RenderCheckId = (typeof RENDER_CHECK_IDS)[number];
|
|
46
|
+
export interface RenderShot {
|
|
47
|
+
width: number;
|
|
48
|
+
/** The page's full height at this width. */
|
|
49
|
+
height: number;
|
|
50
|
+
/** The screenshots stop before the end of the page. */
|
|
51
|
+
truncated: boolean;
|
|
52
|
+
/** JPEGs, top to bottom, one viewport each. */
|
|
53
|
+
images: Uint8Array[];
|
|
54
|
+
}
|
|
55
|
+
export interface RenderResult {
|
|
56
|
+
shots: RenderShot[];
|
|
57
|
+
checks: RenderCheck[];
|
|
58
|
+
}
|
|
59
|
+
/** The `prefers-color-scheme` a page is rendered in (RFC-0023: sites with a dark or auto mode). */
|
|
60
|
+
export declare const RENDER_SCHEMES: readonly ["light", "dark"];
|
|
61
|
+
export type RenderScheme = (typeof RENDER_SCHEMES)[number];
|
|
62
|
+
/**
|
|
63
|
+
* How the page may move while it renders (RFC-0024 §8): `static` (the default) and `reduce`
|
|
64
|
+
* render with `prefers-reduced-motion: reduce`, so screenshots show final states and the
|
|
65
|
+
* `reduced-motion` check runs; `full` lets every preset play, for `motion-cls`.
|
|
66
|
+
*/
|
|
67
|
+
export declare const RENDER_MOTIONS: readonly ["static", "reduce", "full"];
|
|
68
|
+
export type RenderMotion = (typeof RENDER_MOTIONS)[number];
|
|
69
|
+
export interface RenderOptions {
|
|
70
|
+
widths: readonly RenderWidth[];
|
|
71
|
+
/** Default light. */
|
|
72
|
+
scheme?: RenderScheme;
|
|
73
|
+
/** Default `static`. */
|
|
74
|
+
motion?: RenderMotion;
|
|
75
|
+
/**
|
|
76
|
+
* RFC-0024 §8: screenshots at this many evenly spaced scroll positions (at most 6), each after
|
|
77
|
+
* the page settles, in place of one per viewport, to show a scroll story or scroll effects.
|
|
78
|
+
*/
|
|
79
|
+
frames?: number;
|
|
80
|
+
/** Screenshots down the page (up to `maxImages` viewports per width); otherwise the first one. */
|
|
81
|
+
fullPage: boolean;
|
|
82
|
+
maxImages?: number;
|
|
83
|
+
/** JPEG quality, 1–100. */
|
|
84
|
+
quality?: number;
|
|
85
|
+
timeoutMs?: number;
|
|
86
|
+
}
|
|
87
|
+
export declare class RenderError extends Error {
|
|
88
|
+
readonly name = "RenderError";
|
|
89
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** The `render_page` contract: widths, check ids, options and results, the DevTools socket shapes. */
|
|
2
|
+
|
|
3
|
+
/** The widths `render_page` renders at, and the viewport height used at each. */
|
|
4
|
+
export const RENDER_WIDTHS = [1440, 768, 390] ;
|
|
5
|
+
|
|
6
|
+
export const VIEWPORT_HEIGHT = { 1440: 900, 768: 1024, 390: 844 };
|
|
7
|
+
|
|
8
|
+
/** A text socket to a browser's DevTools endpoint (browser-level, not a single page). */
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/** A browser for one `render_page` call; `close` releases it. */
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/** Where `render_page` gets its browsers from. */
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
/** One automatic check (RFC-0021 §4). `examples` name the section and component where known. */
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
export const RENDER_CHECK_IDS = [
|
|
50
|
+
"overflow-x",
|
|
51
|
+
"img-alt",
|
|
52
|
+
"img-broken",
|
|
53
|
+
"heading-order",
|
|
54
|
+
"contrast",
|
|
55
|
+
"empty-section",
|
|
56
|
+
"tap-target",
|
|
57
|
+
"long-line",
|
|
58
|
+
// RFC-0022 §2: a form control a reader cannot identify.
|
|
59
|
+
"input-label",
|
|
60
|
+
// RFC-0022 §3: a chart with no rows to draw.
|
|
61
|
+
"chart-empty",
|
|
62
|
+
// RFC-0024 §8: content hidden or moving for a reader who asked for reduced motion.
|
|
63
|
+
"reduced-motion",
|
|
64
|
+
// RFC-0024 §8: layout shift while the page loads and scrolls (reveals included).
|
|
65
|
+
"motion-cls",
|
|
66
|
+
// RFC-0024 §2: more scroll effects or scroll stories than a page should carry.
|
|
67
|
+
"motion-budget",
|
|
68
|
+
// RFC-0024 §8, WCAG 2.3.1: something that flashes more than three times a second.
|
|
69
|
+
"flash",
|
|
70
|
+
// RFC-0025 §4: the first thing Tab reaches skips to the main content.
|
|
71
|
+
"skip-link",
|
|
72
|
+
// RFC-0025 §4: one banner header, one main, one footer, and every menu named.
|
|
73
|
+
"landmarks",
|
|
74
|
+
// RFC-0025 §2: a transparent header's text against the band under it.
|
|
75
|
+
"header-contrast",
|
|
76
|
+
// RFC-0025 §4, WCAG 2.4.11: a sticky or fixed header covering what has focus.
|
|
77
|
+
"focus-obscured",
|
|
78
|
+
// RFC-0025 §3: each overlay opens from its trigger, takes focus, closes and gives focus back.
|
|
79
|
+
"overlay-open",
|
|
80
|
+
// RFC-0027: carousels and their slides are named (and not just "carousel").
|
|
81
|
+
"carousel-label",
|
|
82
|
+
// RFC-0027: a hero carousel's first slide loads first; the others wait.
|
|
83
|
+
"carousel-lcp",
|
|
84
|
+
// RFC-0027: Tab never lands in a hero slide out of view; the controls are working.
|
|
85
|
+
"carousel-focus",
|
|
86
|
+
// RFC-0027 27d: a playing carousel has its rotation control first; at most one plays per page.
|
|
87
|
+
"carousel-autoplay",
|
|
88
|
+
// RFC-0028: an image cropped hard by cover with no focal point to keep its subject in view.
|
|
89
|
+
"image-fit",
|
|
90
|
+
] ;
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
/** The `prefers-color-scheme` a page is rendered in (RFC-0023: sites with a dark or auto mode). */
|
|
109
|
+
export const RENDER_SCHEMES = ["light", "dark"] ;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* How the page may move while it renders (RFC-0024 §8): `static` (the default) and `reduce`
|
|
113
|
+
* render with `prefers-reduced-motion: reduce`, so screenshots show final states and the
|
|
114
|
+
* `reduced-motion` check runs; `full` lets every preset play, for `motion-cls`.
|
|
115
|
+
*/
|
|
116
|
+
export const RENDER_MOTIONS = ["static", "reduce", "full"] ;
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
export class RenderError extends Error {
|
|
139
|
+
name = "RenderError";
|
|
140
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `render_page` (RFC-0021 §4): an agent looks at what it built. The page's draft opens through its
|
|
3
|
+
* preview link in a real browser at desktop, tablet and phone widths; the result is screenshots
|
|
4
|
+
* (one viewport each, so they stay readable) and automatic checks that name the section and
|
|
5
|
+
* component behind each problem. Read-only: nothing is stored, and the preview link expires.
|
|
6
|
+
*/
|
|
7
|
+
import { type ToolContext, type ToolDefinition } from "./common.js";
|
|
8
|
+
type PreviewDoc = (args: Record<string, unknown>, ctx: ToolContext) => Promise<{
|
|
9
|
+
title: string;
|
|
10
|
+
path: string;
|
|
11
|
+
url: string | null;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function renderTools(previewDoc: PreviewDoc): ToolDefinition[];
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `render_page` (RFC-0021 §4): an agent looks at what it built. The page's draft opens through its
|
|
3
|
+
* preview link in a real browser at desktop, tablet and phone widths; the result is screenshots
|
|
4
|
+
* (one viewport each, so they stay readable) and automatic checks that name the section and
|
|
5
|
+
* component behind each problem. Read-only: nothing is stored, and the preview link expires.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
id,
|
|
10
|
+
obj,
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
ToolError,
|
|
15
|
+
} from "./common.js";
|
|
16
|
+
import {
|
|
17
|
+
bytesToBase64,
|
|
18
|
+
RENDER_CHECK_IDS,
|
|
19
|
+
RENDER_MOTIONS,
|
|
20
|
+
RENDER_SCHEMES,
|
|
21
|
+
RENDER_WIDTHS,
|
|
22
|
+
|
|
23
|
+
RenderError,
|
|
24
|
+
RenderLimiter,
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
renderPage,
|
|
29
|
+
} from "./render.js";
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/** Shared by every server in this process (or Worker isolate) unless a deployment brings its own. */
|
|
37
|
+
const SHARED_LIMITER = new RenderLimiter();
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
const WHAT = {
|
|
51
|
+
"overflow-x": "scrolls sideways",
|
|
52
|
+
"img-alt": "images without alt text",
|
|
53
|
+
"img-broken": "images that did not load",
|
|
54
|
+
"heading-order": "heading levels",
|
|
55
|
+
contrast: "text contrast",
|
|
56
|
+
"empty-section": "empty sections",
|
|
57
|
+
"tap-target": "small tap targets",
|
|
58
|
+
"long-line": "long lines",
|
|
59
|
+
"input-label": "unlabelled inputs",
|
|
60
|
+
"chart-empty": "empty charts",
|
|
61
|
+
"reduced-motion": "motion under reduced motion",
|
|
62
|
+
"motion-cls": "layout shift",
|
|
63
|
+
"motion-budget": "too much motion",
|
|
64
|
+
flash: "flashing",
|
|
65
|
+
"skip-link": "skip link",
|
|
66
|
+
landmarks: "page landmarks",
|
|
67
|
+
"header-contrast": "transparent header contrast",
|
|
68
|
+
"focus-obscured": "focus under the sticky header",
|
|
69
|
+
"overlay-open": "overlays",
|
|
70
|
+
"carousel-label": "carousel names",
|
|
71
|
+
"carousel-lcp": "carousel image loading",
|
|
72
|
+
"carousel-focus": "carousel focus",
|
|
73
|
+
"carousel-autoplay": "carousel autoplay",
|
|
74
|
+
"image-fit": "image crops",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function summary(r ) {
|
|
78
|
+
const failed = r.checks.filter((c) => !c.ok);
|
|
79
|
+
return [
|
|
80
|
+
`Rendered ${r.path} (the draft) at ${r.widths.map((w) => `${w.width}`).join(", ")} px, ${r.scheme} scheme, motion ${r.motion}.`,
|
|
81
|
+
...r.widths.map(
|
|
82
|
+
(w) =>
|
|
83
|
+
`${w.width} px: ${w.images} screenshot(s) of a ${w.height} px page${w.truncated ? " (truncated: the screenshots stop there; preview_page has the whole page as HTML)" : ""}.`,
|
|
84
|
+
),
|
|
85
|
+
failed.length
|
|
86
|
+
? `Checks: ${failed.length} of ${r.checks.length} found problems.`
|
|
87
|
+
: `Checks: all ${r.checks.length} passed.`,
|
|
88
|
+
...failed.map(
|
|
89
|
+
(c) =>
|
|
90
|
+
`✗ ${c.id} (${WHAT[c.id]}): ${c.count} at ${c.widths.join(", ")} px\n${c.examples.map((e) => ` - ${e}`).join("\n")}`,
|
|
91
|
+
),
|
|
92
|
+
...(failed.length
|
|
93
|
+
? [
|
|
94
|
+
"Fix what you can in the draft (examples name the section and component), render again, and tell the person about anything you leave.",
|
|
95
|
+
]
|
|
96
|
+
: []),
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The other scheme's contrast problems folded into the contrast check, each example marked. */
|
|
101
|
+
function mergeContrast(
|
|
102
|
+
checks ,
|
|
103
|
+
extra ,
|
|
104
|
+
scheme ,
|
|
105
|
+
) {
|
|
106
|
+
const add = extra.find((c) => c.id === "contrast");
|
|
107
|
+
if (!add || add.ok) return checks;
|
|
108
|
+
return checks.map((c) =>
|
|
109
|
+
c.id === "contrast"
|
|
110
|
+
? {
|
|
111
|
+
...c,
|
|
112
|
+
ok: false,
|
|
113
|
+
count: c.count + add.count,
|
|
114
|
+
widths: [...new Set([...c.widths, ...add.widths])],
|
|
115
|
+
examples: [
|
|
116
|
+
...c.examples,
|
|
117
|
+
...add.examples.map((e) =>
|
|
118
|
+
e.startsWith("@") ? `${scheme} ${e}` : `${scheme} @${add.widths[0]}: ${e}`,
|
|
119
|
+
),
|
|
120
|
+
].slice(0, 8),
|
|
121
|
+
}
|
|
122
|
+
: c,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function renderTools(previewDoc ) {
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
name: "render_page",
|
|
130
|
+
title: "Render a page in a browser",
|
|
131
|
+
description:
|
|
132
|
+
"Look at a page's draft as people will see it: renders it in a real browser at desktop (1440), tablet (768) and phone (390) widths and returns screenshots plus automatic checks (sideways scrolling, alt text, broken images, heading order, text contrast, empty sections, tap targets, line length, motion under reduced motion, layout shift) that name the section and component behind each problem. Use it after building or changing a page and before asking a person to publish; fix what it finds and render again. Read-only. Rate-limited (about 10 a minute); when no browser is available, use preview_page.",
|
|
133
|
+
inputSchema: obj(
|
|
134
|
+
{
|
|
135
|
+
siteId: id,
|
|
136
|
+
pageId: id,
|
|
137
|
+
entryId: id,
|
|
138
|
+
widths: {
|
|
139
|
+
type: "array",
|
|
140
|
+
items: { type: "integer", enum: [...RENDER_WIDTHS] },
|
|
141
|
+
minItems: 1,
|
|
142
|
+
maxItems: 3,
|
|
143
|
+
uniqueItems: true,
|
|
144
|
+
description: "Default [1440, 390]",
|
|
145
|
+
},
|
|
146
|
+
fullPage: {
|
|
147
|
+
type: "boolean",
|
|
148
|
+
description: "Screenshots down the page, up to 4 per width (default true)",
|
|
149
|
+
},
|
|
150
|
+
frames: {
|
|
151
|
+
type: "integer",
|
|
152
|
+
minimum: 1,
|
|
153
|
+
maximum: 6,
|
|
154
|
+
description:
|
|
155
|
+
"Screenshots at this many evenly spaced scroll positions instead of one per viewport, with motion full unless set, to see a scroll story (Scrolly) or scroll effects (RFC-0024)",
|
|
156
|
+
},
|
|
157
|
+
motion: {
|
|
158
|
+
type: "string",
|
|
159
|
+
enum: [...RENDER_MOTIONS],
|
|
160
|
+
description:
|
|
161
|
+
"static (default): the reader asks for reduced motion, so screenshots show final states and the reduced-motion check runs; full: every motion preset plays, for the motion-cls check (RFC-0024).",
|
|
162
|
+
},
|
|
163
|
+
scheme: {
|
|
164
|
+
type: "string",
|
|
165
|
+
enum: [...RENDER_SCHEMES],
|
|
166
|
+
description:
|
|
167
|
+
"The reader's color scheme: light by default, dark for a site whose mode is dark. When the mode is dark or auto, text contrast is also checked in the other scheme (its examples are marked).",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
["siteId"],
|
|
171
|
+
),
|
|
172
|
+
outputSchema: obj(
|
|
173
|
+
{
|
|
174
|
+
url: { type: "string" },
|
|
175
|
+
path: { type: "string" },
|
|
176
|
+
scheme: { type: "string", enum: [...RENDER_SCHEMES] },
|
|
177
|
+
motion: { type: "string", enum: [...RENDER_MOTIONS] },
|
|
178
|
+
widths: {
|
|
179
|
+
type: "array",
|
|
180
|
+
items: obj(
|
|
181
|
+
{
|
|
182
|
+
width: { type: "integer" },
|
|
183
|
+
height: { type: "integer" },
|
|
184
|
+
truncated: { type: "boolean" },
|
|
185
|
+
images: { type: "integer" },
|
|
186
|
+
},
|
|
187
|
+
["width", "height", "truncated", "images"],
|
|
188
|
+
),
|
|
189
|
+
},
|
|
190
|
+
checks: {
|
|
191
|
+
type: "array",
|
|
192
|
+
items: obj(
|
|
193
|
+
{
|
|
194
|
+
id: { type: "string", enum: [...RENDER_CHECK_IDS] },
|
|
195
|
+
ok: { type: "boolean" },
|
|
196
|
+
count: { type: "integer" },
|
|
197
|
+
examples: { type: "array", items: { type: "string" } },
|
|
198
|
+
widths: { type: "array", items: { type: "integer" } },
|
|
199
|
+
},
|
|
200
|
+
["id", "ok", "count", "examples", "widths"],
|
|
201
|
+
),
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
["url", "path", "widths", "checks"],
|
|
205
|
+
),
|
|
206
|
+
annotations: {
|
|
207
|
+
readOnlyHint: true,
|
|
208
|
+
destructiveHint: false,
|
|
209
|
+
idempotentHint: true,
|
|
210
|
+
openWorldHint: false,
|
|
211
|
+
},
|
|
212
|
+
scopes: ["preview:create"],
|
|
213
|
+
renderContent: (raw) => {
|
|
214
|
+
const { images = [], ...structured } = raw ;
|
|
215
|
+
const content = [{ type: "text", text: summary(structured) }];
|
|
216
|
+
for (const shot of images) {
|
|
217
|
+
shot.jpegs.forEach((jpeg, i) => {
|
|
218
|
+
content.push({
|
|
219
|
+
type: "text",
|
|
220
|
+
text: `${shot.width} px, screenshot ${i + 1} of ${shot.jpegs.length}:`,
|
|
221
|
+
});
|
|
222
|
+
content.push({ type: "image", data: bytesToBase64(jpeg), mimeType: "image/jpeg" });
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return { content, structured: structured };
|
|
226
|
+
},
|
|
227
|
+
handler: async (a, ctx) => {
|
|
228
|
+
if (!a.pageId && !a.entryId) throw new ToolError("invalid", "pass pageId or entryId");
|
|
229
|
+
const doc = await previewDoc(a, ctx);
|
|
230
|
+
if (!ctx.render)
|
|
231
|
+
throw new ToolError(
|
|
232
|
+
"render_unavailable",
|
|
233
|
+
`this server has no browser for render_page; use preview_page${doc.url ? ` or open ${doc.url}` : ""}`,
|
|
234
|
+
{ url: doc.url },
|
|
235
|
+
);
|
|
236
|
+
if (!doc.url)
|
|
237
|
+
throw new ToolError(
|
|
238
|
+
"render_unavailable",
|
|
239
|
+
"this page has no preview link to open (the site has no domain and no `cloudhal dev` is running); use preview_page",
|
|
240
|
+
);
|
|
241
|
+
const limiter = ctx.render.limiter ?? SHARED_LIMITER;
|
|
242
|
+
const refused = limiter.take(
|
|
243
|
+
ctx.principal.actor.id,
|
|
244
|
+
a.siteId ,
|
|
245
|
+
ctx.now?.() ?? new Date(),
|
|
246
|
+
);
|
|
247
|
+
if (refused) throw new ToolError("render_limited", refused);
|
|
248
|
+
const widths = (
|
|
249
|
+
Array.isArray(a.widths) && a.widths.length ? a.widths : [1440, 390]
|
|
250
|
+
) ;
|
|
251
|
+
// RFC-0023: a dark site renders dark by default; a dark or automatic one also has its
|
|
252
|
+
// contrast checked in the other scheme, so a page cannot pass light and fail dark.
|
|
253
|
+
const mode = (await ctx.store.getSite(a.siteId ))?.theme.config?.mode;
|
|
254
|
+
const scheme =
|
|
255
|
+
a.scheme === "dark" || a.scheme === "light"
|
|
256
|
+
? a.scheme
|
|
257
|
+
: mode === "dark"
|
|
258
|
+
? "dark"
|
|
259
|
+
: "light";
|
|
260
|
+
const other = scheme === "light" ? "dark" : "light";
|
|
261
|
+
const frames = Number.isInteger(a.frames)
|
|
262
|
+
? Math.min(6, Math.max(1, a.frames ))
|
|
263
|
+
: 0;
|
|
264
|
+
const motion =
|
|
265
|
+
a.motion === "full" || a.motion === "reduce" || a.motion === "static"
|
|
266
|
+
? a.motion
|
|
267
|
+
: frames
|
|
268
|
+
? "full"
|
|
269
|
+
: "static";
|
|
270
|
+
const checkOther = mode === "auto" || (mode === "dark" && scheme === "light");
|
|
271
|
+
try {
|
|
272
|
+
const r = await renderPage(ctx.render.renderer, doc.url, {
|
|
273
|
+
widths,
|
|
274
|
+
fullPage: a.fullPage !== false,
|
|
275
|
+
scheme,
|
|
276
|
+
motion,
|
|
277
|
+
...(frames ? { frames } : {}),
|
|
278
|
+
timeoutMs: ctx.render.timeoutMs ?? 20_000,
|
|
279
|
+
});
|
|
280
|
+
if (checkOther) {
|
|
281
|
+
const extra = await renderPage(ctx.render.renderer, doc.url, {
|
|
282
|
+
widths: [widths[0] ],
|
|
283
|
+
fullPage: false,
|
|
284
|
+
scheme: other,
|
|
285
|
+
timeoutMs: ctx.render.timeoutMs ?? 20_000,
|
|
286
|
+
});
|
|
287
|
+
r.checks = mergeContrast(r.checks, extra.checks, other);
|
|
288
|
+
}
|
|
289
|
+
const result = {
|
|
290
|
+
url: doc.url,
|
|
291
|
+
path: doc.path,
|
|
292
|
+
scheme,
|
|
293
|
+
motion,
|
|
294
|
+
widths: r.shots.map((s) => ({
|
|
295
|
+
width: s.width,
|
|
296
|
+
height: s.height,
|
|
297
|
+
truncated: s.truncated,
|
|
298
|
+
images: s.images.length,
|
|
299
|
+
})),
|
|
300
|
+
checks: r.checks,
|
|
301
|
+
images: r.shots.map((s) => ({ width: s.width, jpegs: s.images })),
|
|
302
|
+
};
|
|
303
|
+
return result;
|
|
304
|
+
} catch (e) {
|
|
305
|
+
if (e instanceof RenderError)
|
|
306
|
+
throw new ToolError("render_failed", `${e.message}; use preview_page instead`, {
|
|
307
|
+
url: doc.url,
|
|
308
|
+
});
|
|
309
|
+
throw e;
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sight for agents (RFC-0021 §4): renders a page in a real browser at a few widths and returns
|
|
3
|
+
* screenshots plus automatic checks (overflow, alt text, broken images, heading order, contrast,
|
|
4
|
+
* empty sections, tap targets, line length). The browser is a DevTools socket: local Chrome under
|
|
5
|
+
* Node (`@cloudhal/cli`), Cloudflare Browser Rendering on Workers (`@cloudhal/cloudflare`). One
|
|
6
|
+
* small DevTools client serves both, so there is no browser library to bundle.
|
|
7
|
+
*
|
|
8
|
+
* Worker-safe: no Node imports.
|
|
9
|
+
*/
|
|
10
|
+
export { base64ToBytes, bytesToBase64, Cdp, cdpSession } from "./render/cdp.js";
|
|
11
|
+
export { RENDER_CHECK_SCRIPT } from "./render/checks/index.js";
|
|
12
|
+
export { RenderLimiter } from "./render/limiter.js";
|
|
13
|
+
export { renderPage } from "./render/page.js";
|
|
14
|
+
export { type CdpSocket, type PageRenderer, RENDER_CHECK_IDS, RENDER_MOTIONS, RENDER_SCHEMES, RENDER_WIDTHS, type RenderCheck, type RenderCheckId, RenderError, type RenderMotion, type RenderOptions, type RenderResult, type RenderScheme, type RenderSession, type RenderShot, type RenderTab, type RenderWidth, } from "./render/types.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sight for agents (RFC-0021 §4): renders a page in a real browser at a few widths and returns
|
|
3
|
+
* screenshots plus automatic checks (overflow, alt text, broken images, heading order, contrast,
|
|
4
|
+
* empty sections, tap targets, line length). The browser is a DevTools socket: local Chrome under
|
|
5
|
+
* Node (`@cloudhal/cli`), Cloudflare Browser Rendering on Workers (`@cloudhal/cloudflare`). One
|
|
6
|
+
* small DevTools client serves both, so there is no browser library to bundle.
|
|
7
|
+
*
|
|
8
|
+
* Worker-safe: no Node imports.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { base64ToBytes, bytesToBase64, Cdp, cdpSession } from "./render/cdp.js";
|
|
12
|
+
export { RENDER_CHECK_SCRIPT } from "./render/checks/index.js";
|
|
13
|
+
export { RenderLimiter } from "./render/limiter.js";
|
|
14
|
+
export { renderPage } from "./render/page.js";
|
|
15
|
+
export {
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
RENDER_CHECK_IDS,
|
|
19
|
+
RENDER_MOTIONS,
|
|
20
|
+
RENDER_SCHEMES,
|
|
21
|
+
RENDER_WIDTHS,
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
RenderError,
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
} from "./render/types.js";
|