@oyerinde/caliper 0.1.3 → 0.2.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/CHANGELOG.md +30 -0
- package/README.md +134 -25
- package/dist/bridge.cjs +50 -0
- package/dist/bridge.d.cts +1649 -0
- package/dist/bridge.d.ts +1649 -0
- package/dist/bridge.js +1 -0
- package/dist/bridge.server.cjs +34 -0
- package/dist/bridge.server.js +31 -0
- package/dist/chunk-3W2YKDGV.cjs +18631 -0
- package/dist/chunk-ACV6FK43.js +18619 -0
- package/dist/chunk-OSQHATBH.cjs +5891 -0
- package/dist/chunk-XYBNY4BT.js +5885 -0
- package/dist/index.cjs +24 -5131
- package/dist/index.d.cts +975 -18
- package/dist/index.d.ts +975 -2
- package/dist/index.global.js +154 -4878
- package/dist/index.global.js.map +1 -0
- package/dist/index.global.min.js +327 -0
- package/dist/index.global.min.js.map +1 -0
- package/dist/index.js +1 -5128
- package/dist/index.server.cjs +19 -8
- package/dist/index.server.js +19 -9
- package/dist/mcp.cjs +2305 -0
- package/dist/mcp.js +2303 -0
- package/dist/preset.cjs +44 -0
- package/dist/preset.d.cts +964 -0
- package/dist/preset.d.ts +964 -0
- package/dist/preset.js +31 -0
- package/dist/preset.server.cjs +43 -0
- package/dist/preset.server.js +39 -0
- package/dist/version.json +2 -2
- package/package.json +47 -9
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,2303 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { JSONRPCErrorResponseSchema as JSONRPCErrorResponseSchema$1, JSONRPCResultResponseSchema as JSONRPCResultResponseSchema$1, JSONRPCRequestSchema as JSONRPCRequestSchema$1, JSONRPCNotificationSchema as JSONRPCNotificationSchema$1 } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { WebSocketServer } from 'ws';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @name @oyerinde/caliper
|
|
11
|
+
* @author Daniel Oyerinde
|
|
12
|
+
* @license MIT
|
|
13
|
+
*
|
|
14
|
+
* Caliper - Browser Measurement Tool
|
|
15
|
+
*
|
|
16
|
+
* This source code is licensed under the MIT license found in the
|
|
17
|
+
* LICENSE file in the root directory of this source tree.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
var PositionModeSchema = z.enum(["static", "relative", "absolute", "fixed", "sticky"]);
|
|
21
|
+
var CursorContextSchema = z.enum(["parent", "child", "sibling"]);
|
|
22
|
+
var RectSchema = z.object({
|
|
23
|
+
top: z.number(),
|
|
24
|
+
right: z.number(),
|
|
25
|
+
bottom: z.number(),
|
|
26
|
+
left: z.number(),
|
|
27
|
+
width: z.number(),
|
|
28
|
+
height: z.number(),
|
|
29
|
+
x: z.number(),
|
|
30
|
+
y: z.number()
|
|
31
|
+
});
|
|
32
|
+
var ScrollStateSchema = z.object({
|
|
33
|
+
initialScrollTop: z.number(),
|
|
34
|
+
initialScrollLeft: z.number(),
|
|
35
|
+
containerRect: RectSchema.nullable(),
|
|
36
|
+
absoluteDepth: z.number(),
|
|
37
|
+
hasStickyAncestor: z.boolean().optional()
|
|
38
|
+
});
|
|
39
|
+
var MeasurementLineSchema = z.object({
|
|
40
|
+
type: z.enum(["left", "top", "right", "bottom", "distance"]),
|
|
41
|
+
value: z.number(),
|
|
42
|
+
start: z.object({ x: z.number(), y: z.number() }),
|
|
43
|
+
end: z.object({ x: z.number(), y: z.number() }),
|
|
44
|
+
startSync: z.enum(["primary", "secondary"]).optional(),
|
|
45
|
+
endSync: z.enum(["primary", "secondary"]).optional()
|
|
46
|
+
});
|
|
47
|
+
var StickyConfigSchema = z.object({
|
|
48
|
+
top: z.number().nullable(),
|
|
49
|
+
bottom: z.number().nullable(),
|
|
50
|
+
left: z.number().nullable(),
|
|
51
|
+
right: z.number().nullable(),
|
|
52
|
+
naturalTop: z.number(),
|
|
53
|
+
naturalLeft: z.number(),
|
|
54
|
+
containerWidth: z.number(),
|
|
55
|
+
containerHeight: z.number(),
|
|
56
|
+
elementWidth: z.number(),
|
|
57
|
+
elementHeight: z.number(),
|
|
58
|
+
anchorAbsoluteDepth: z.number()
|
|
59
|
+
});
|
|
60
|
+
var MeasurementResultSchema = z.object({
|
|
61
|
+
context: CursorContextSchema.nullable(),
|
|
62
|
+
lines: z.array(MeasurementLineSchema),
|
|
63
|
+
primary: RectSchema,
|
|
64
|
+
secondary: RectSchema.nullable(),
|
|
65
|
+
timestamp: z.number(),
|
|
66
|
+
primaryHierarchy: z.array(ScrollStateSchema),
|
|
67
|
+
secondaryHierarchy: z.array(ScrollStateSchema),
|
|
68
|
+
primaryPosition: PositionModeSchema,
|
|
69
|
+
secondaryPosition: PositionModeSchema,
|
|
70
|
+
primaryWinX: z.number(),
|
|
71
|
+
primaryWinY: z.number(),
|
|
72
|
+
secondaryWinX: z.number(),
|
|
73
|
+
secondaryWinY: z.number(),
|
|
74
|
+
primarySticky: StickyConfigSchema.optional(),
|
|
75
|
+
secondarySticky: StickyConfigSchema.optional(),
|
|
76
|
+
primaryHasContainingBlock: z.boolean().optional(),
|
|
77
|
+
secondaryHasContainingBlock: z.boolean().optional()
|
|
78
|
+
});
|
|
79
|
+
var SelectionMetadataSchema = z.object({
|
|
80
|
+
rect: RectSchema.nullable(),
|
|
81
|
+
scrollHierarchy: z.array(ScrollStateSchema),
|
|
82
|
+
position: PositionModeSchema,
|
|
83
|
+
stickyConfig: StickyConfigSchema.optional(),
|
|
84
|
+
initialWindowX: z.number(),
|
|
85
|
+
initialWindowY: z.number(),
|
|
86
|
+
depth: z.number(),
|
|
87
|
+
hasContainingBlock: z.boolean().optional()
|
|
88
|
+
});
|
|
89
|
+
var ContextMetricsSchema = z.object({
|
|
90
|
+
rootFontSize: z.number(),
|
|
91
|
+
devicePixelRatio: z.number(),
|
|
92
|
+
viewportWidth: z.number(),
|
|
93
|
+
viewportHeight: z.number(),
|
|
94
|
+
visualViewportWidth: z.number().optional(),
|
|
95
|
+
visualViewportHeight: z.number().optional(),
|
|
96
|
+
scrollX: z.number(),
|
|
97
|
+
scrollY: z.number(),
|
|
98
|
+
documentWidth: z.number(),
|
|
99
|
+
documentHeight: z.number(),
|
|
100
|
+
orientation: z.enum(["portrait", "landscape"]),
|
|
101
|
+
preferences: z.object({
|
|
102
|
+
colorScheme: z.enum(["light", "dark", "no-preference"]),
|
|
103
|
+
reducedMotion: z.boolean()
|
|
104
|
+
})
|
|
105
|
+
});
|
|
106
|
+
var CaliperSelectorInputSchema = z.object({
|
|
107
|
+
selector: z.string(),
|
|
108
|
+
// The CSS selector or data-caliper-agent-id
|
|
109
|
+
tag: z.string(),
|
|
110
|
+
// e.g., "section"
|
|
111
|
+
timestamp: z.number(),
|
|
112
|
+
// When the copy was made
|
|
113
|
+
id: z.string().optional(),
|
|
114
|
+
// HTML id attribute
|
|
115
|
+
text: z.string().optional(),
|
|
116
|
+
// Visible text snippet
|
|
117
|
+
classes: z.array(z.string()).optional(),
|
|
118
|
+
// CSS classes (filtered)
|
|
119
|
+
tabId: z.string().optional(),
|
|
120
|
+
// Tab ID for cross-tab identification
|
|
121
|
+
nthChild: z.number().optional(),
|
|
122
|
+
x: z.number().optional(),
|
|
123
|
+
y: z.number().optional(),
|
|
124
|
+
depth: z.number().optional(),
|
|
125
|
+
marker: z.string().optional(),
|
|
126
|
+
// data-caliper-marker value
|
|
127
|
+
// Geometry Context for re-discovery
|
|
128
|
+
scrollHierarchy: z.array(ScrollStateSchema).optional(),
|
|
129
|
+
position: PositionModeSchema.optional(),
|
|
130
|
+
stickyConfig: StickyConfigSchema.optional(),
|
|
131
|
+
initialWindowX: z.number().optional(),
|
|
132
|
+
initialWindowY: z.number().optional(),
|
|
133
|
+
hasContainingBlock: z.boolean().optional(),
|
|
134
|
+
rect: RectSchema.optional()
|
|
135
|
+
});
|
|
136
|
+
var BoxEdgesSchema = z.object({
|
|
137
|
+
top: z.number(),
|
|
138
|
+
right: z.number(),
|
|
139
|
+
bottom: z.number(),
|
|
140
|
+
left: z.number()
|
|
141
|
+
});
|
|
142
|
+
var CaliperComputedStylesSchema = z.object({
|
|
143
|
+
// Box Model
|
|
144
|
+
display: z.string().optional(),
|
|
145
|
+
visibility: z.string().optional(),
|
|
146
|
+
position: z.string().optional(),
|
|
147
|
+
boxSizing: z.string().optional(),
|
|
148
|
+
// Spacing
|
|
149
|
+
padding: BoxEdgesSchema.optional(),
|
|
150
|
+
margin: BoxEdgesSchema.optional(),
|
|
151
|
+
border: BoxEdgesSchema.optional(),
|
|
152
|
+
// Flexbox/Grid
|
|
153
|
+
gap: z.number().nullable().optional(),
|
|
154
|
+
flexDirection: z.string().optional(),
|
|
155
|
+
justifyContent: z.string().optional(),
|
|
156
|
+
alignItems: z.string().optional(),
|
|
157
|
+
// Typography
|
|
158
|
+
fontSize: z.number().optional(),
|
|
159
|
+
fontWeight: z.string().optional(),
|
|
160
|
+
fontFamily: z.string().optional(),
|
|
161
|
+
lineHeight: z.union([z.number(), z.string()]).nullable().optional(),
|
|
162
|
+
letterSpacing: z.union([z.number(), z.string()]).optional(),
|
|
163
|
+
color: z.string().optional(),
|
|
164
|
+
// Visual
|
|
165
|
+
backgroundColor: z.string().optional(),
|
|
166
|
+
borderColor: z.string().optional(),
|
|
167
|
+
borderRadius: z.string().optional(),
|
|
168
|
+
boxShadow: z.string().optional(),
|
|
169
|
+
opacity: z.union([z.number(), z.string()]).optional(),
|
|
170
|
+
outline: z.string().optional(),
|
|
171
|
+
outlineColor: z.string().optional(),
|
|
172
|
+
zIndex: z.union([z.number(), z.string()]).nullable().optional(),
|
|
173
|
+
// Overflow
|
|
174
|
+
overflow: z.string().optional(),
|
|
175
|
+
overflowX: z.string().optional(),
|
|
176
|
+
overflowY: z.string().optional(),
|
|
177
|
+
contentVisibility: z.string().optional()
|
|
178
|
+
});
|
|
179
|
+
var ParentGapSchema = z.object({
|
|
180
|
+
top: z.number(),
|
|
181
|
+
// Distance from parent's padding-top edge to node's margin-top
|
|
182
|
+
left: z.number(),
|
|
183
|
+
// Distance from parent's padding-left edge to node's margin-left
|
|
184
|
+
bottom: z.number(),
|
|
185
|
+
// Distance from node's margin-bottom to parent's padding-bottom
|
|
186
|
+
right: z.number()
|
|
187
|
+
// Distance from node's margin-right to parent's padding-right
|
|
188
|
+
});
|
|
189
|
+
var SiblingGapSchema = z.object({
|
|
190
|
+
distance: z.number(),
|
|
191
|
+
direction: z.enum(["above", "below", "left", "right"]).nullable()
|
|
192
|
+
}).nullable();
|
|
193
|
+
var CaliperMeasurementsSchema = z.object({
|
|
194
|
+
toParent: ParentGapSchema,
|
|
195
|
+
toPreviousSibling: SiblingGapSchema,
|
|
196
|
+
toNextSibling: SiblingGapSchema,
|
|
197
|
+
indexInParent: z.number(),
|
|
198
|
+
siblingCount: z.number()
|
|
199
|
+
});
|
|
200
|
+
var CaliperNodeSchema = z.lazy(
|
|
201
|
+
() => z.object({
|
|
202
|
+
// Identity
|
|
203
|
+
agentId: z.string(),
|
|
204
|
+
selector: z.string(),
|
|
205
|
+
tag: z.string(),
|
|
206
|
+
htmlId: z.string().optional(),
|
|
207
|
+
classes: z.array(z.string()),
|
|
208
|
+
textContent: z.string().optional(),
|
|
209
|
+
// Geometry (document-relative)
|
|
210
|
+
rect: RectSchema,
|
|
211
|
+
// Viewport-relative position
|
|
212
|
+
viewportRect: z.object({
|
|
213
|
+
top: z.number(),
|
|
214
|
+
left: z.number()
|
|
215
|
+
}),
|
|
216
|
+
// Computed Styles
|
|
217
|
+
styles: CaliperComputedStylesSchema,
|
|
218
|
+
// Measurements
|
|
219
|
+
measurements: CaliperMeasurementsSchema,
|
|
220
|
+
// Tree structure
|
|
221
|
+
depth: z.number(),
|
|
222
|
+
parentAgentId: z.string().optional(),
|
|
223
|
+
childCount: z.number(),
|
|
224
|
+
children: z.array(CaliperNodeSchema),
|
|
225
|
+
marker: z.string().optional(),
|
|
226
|
+
ariaHidden: z.boolean().optional()
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
var WalkOptionsSchema = z.object({
|
|
230
|
+
maxDepth: z.number().optional(),
|
|
231
|
+
maxNodes: z.number().optional(),
|
|
232
|
+
continueFrom: z.string().optional(),
|
|
233
|
+
minElementSize: z.number().optional(),
|
|
234
|
+
ignoreSelectors: z.array(z.string()).optional()
|
|
235
|
+
});
|
|
236
|
+
var JSONRPCRequestSchema = JSONRPCRequestSchema$1;
|
|
237
|
+
var JSONRPCNotificationSchema = JSONRPCNotificationSchema$1;
|
|
238
|
+
var JSONRPCResultResponseSchema = JSONRPCResultResponseSchema$1;
|
|
239
|
+
var JSONRPCErrorResponseSchema = JSONRPCErrorResponseSchema$1;
|
|
240
|
+
var ViewportSchema = z.object({
|
|
241
|
+
width: z.number(),
|
|
242
|
+
height: z.number(),
|
|
243
|
+
scrollX: z.number(),
|
|
244
|
+
scrollY: z.number()
|
|
245
|
+
});
|
|
246
|
+
z.object({
|
|
247
|
+
top: z.number(),
|
|
248
|
+
left: z.number(),
|
|
249
|
+
width: z.number(),
|
|
250
|
+
height: z.number(),
|
|
251
|
+
absoluteX: z.number(),
|
|
252
|
+
absoluteY: z.number(),
|
|
253
|
+
zIndex: z.number().optional(),
|
|
254
|
+
agentId: z.string().optional()
|
|
255
|
+
});
|
|
256
|
+
var CaliperElementSummarySchema = z.object({
|
|
257
|
+
tagName: z.string(),
|
|
258
|
+
id: z.string().optional(),
|
|
259
|
+
classList: z.array(z.string()),
|
|
260
|
+
agentId: z.string().optional(),
|
|
261
|
+
text: z.string().optional()
|
|
262
|
+
});
|
|
263
|
+
var CALIPER_METHODS = {
|
|
264
|
+
SELECT: "CALIPER_SELECT",
|
|
265
|
+
MEASURE: "CALIPER_MEASURE",
|
|
266
|
+
INSPECT: "CALIPER_INSPECT",
|
|
267
|
+
FREEZE: "CALIPER_FREEZE",
|
|
268
|
+
CLEAR: "CALIPER_CLEAR",
|
|
269
|
+
WALK_DOM: "CALIPER_WALK_DOM",
|
|
270
|
+
WALK_AND_MEASURE: "CALIPER_WALK_AND_MEASURE",
|
|
271
|
+
GET_CONTEXT: "CALIPER_GET_CONTEXT",
|
|
272
|
+
REGISTER_TAB: "caliper/registerTab",
|
|
273
|
+
TAB_UPDATE: "caliper/tabUpdate",
|
|
274
|
+
STATE_UPDATE: "caliper/stateUpdate"
|
|
275
|
+
};
|
|
276
|
+
var CaliperMethodSchema = z.enum([
|
|
277
|
+
CALIPER_METHODS.SELECT,
|
|
278
|
+
CALIPER_METHODS.MEASURE,
|
|
279
|
+
CALIPER_METHODS.INSPECT,
|
|
280
|
+
CALIPER_METHODS.FREEZE,
|
|
281
|
+
CALIPER_METHODS.CLEAR,
|
|
282
|
+
CALIPER_METHODS.WALK_DOM,
|
|
283
|
+
CALIPER_METHODS.WALK_AND_MEASURE,
|
|
284
|
+
CALIPER_METHODS.GET_CONTEXT,
|
|
285
|
+
CALIPER_METHODS.REGISTER_TAB,
|
|
286
|
+
CALIPER_METHODS.TAB_UPDATE,
|
|
287
|
+
CALIPER_METHODS.STATE_UPDATE
|
|
288
|
+
]);
|
|
289
|
+
var SourceHintsSchema = z.object({
|
|
290
|
+
stableAnchors: z.array(z.string()),
|
|
291
|
+
suggestedGrep: z.string().optional(),
|
|
292
|
+
textContent: z.string().optional(),
|
|
293
|
+
accessibleName: z.string().optional(),
|
|
294
|
+
unstableClasses: z.array(z.string()),
|
|
295
|
+
tagName: z.string()
|
|
296
|
+
});
|
|
297
|
+
var CaliperActionResultSchema = z.union([
|
|
298
|
+
z.object({ success: z.literal(true), method: z.literal(CALIPER_METHODS.SELECT), selector: z.string(), selection: SelectionMetadataSchema, timestamp: z.number() }),
|
|
299
|
+
z.object({ success: z.literal(true), method: z.literal(CALIPER_METHODS.MEASURE), selector: z.string(), measurement: MeasurementResultSchema, timestamp: z.number() }),
|
|
300
|
+
z.object({
|
|
301
|
+
success: z.literal(true),
|
|
302
|
+
method: z.literal(CALIPER_METHODS.INSPECT),
|
|
303
|
+
selector: z.string(),
|
|
304
|
+
distances: z.object({
|
|
305
|
+
top: z.number(),
|
|
306
|
+
right: z.number(),
|
|
307
|
+
bottom: z.number(),
|
|
308
|
+
left: z.number(),
|
|
309
|
+
horizontal: z.number(),
|
|
310
|
+
vertical: z.number()
|
|
311
|
+
}),
|
|
312
|
+
computedStyles: CaliperComputedStylesSchema,
|
|
313
|
+
selection: SelectionMetadataSchema,
|
|
314
|
+
immediateChildCount: z.number().optional(),
|
|
315
|
+
descendantCount: z.number().optional(),
|
|
316
|
+
descendantsTruncated: z.boolean().optional(),
|
|
317
|
+
sourceHints: SourceHintsSchema.optional(),
|
|
318
|
+
timestamp: z.number()
|
|
319
|
+
}),
|
|
320
|
+
z.object({ success: z.literal(true), method: z.literal(CALIPER_METHODS.FREEZE), timestamp: z.number() }),
|
|
321
|
+
z.object({ success: z.literal(true), method: z.literal(CALIPER_METHODS.CLEAR), timestamp: z.number() }),
|
|
322
|
+
z.object({
|
|
323
|
+
success: z.literal(true),
|
|
324
|
+
method: z.literal(CALIPER_METHODS.WALK_DOM),
|
|
325
|
+
selector: z.string(),
|
|
326
|
+
domContext: z.object({
|
|
327
|
+
element: CaliperElementSummarySchema,
|
|
328
|
+
parent: CaliperElementSummarySchema.nullable(),
|
|
329
|
+
children: z.array(CaliperElementSummarySchema)
|
|
330
|
+
}),
|
|
331
|
+
timestamp: z.number()
|
|
332
|
+
}),
|
|
333
|
+
z.object({
|
|
334
|
+
success: z.literal(true),
|
|
335
|
+
method: z.literal(CALIPER_METHODS.WALK_AND_MEASURE),
|
|
336
|
+
selector: z.string(),
|
|
337
|
+
walkResult: z.object({
|
|
338
|
+
root: CaliperNodeSchema.optional(),
|
|
339
|
+
nodeCount: z.number(),
|
|
340
|
+
maxDepthReached: z.number(),
|
|
341
|
+
walkDurationMs: z.number(),
|
|
342
|
+
hasMore: z.boolean().optional(),
|
|
343
|
+
batchInstructions: z.string().optional(),
|
|
344
|
+
continuationToken: z.string().optional()
|
|
345
|
+
}),
|
|
346
|
+
timestamp: z.number(),
|
|
347
|
+
binaryPayload: z.custom().optional()
|
|
348
|
+
}),
|
|
349
|
+
z.object({
|
|
350
|
+
success: z.literal(true),
|
|
351
|
+
method: z.literal(CALIPER_METHODS.GET_CONTEXT),
|
|
352
|
+
context: ContextMetricsSchema,
|
|
353
|
+
timestamp: z.number()
|
|
354
|
+
}),
|
|
355
|
+
z.object({
|
|
356
|
+
success: z.literal(false),
|
|
357
|
+
method: CaliperMethodSchema,
|
|
358
|
+
selector: z.string().optional(),
|
|
359
|
+
error: z.string(),
|
|
360
|
+
timestamp: z.number(),
|
|
361
|
+
binaryPayload: z.custom().optional()
|
|
362
|
+
})
|
|
363
|
+
]);
|
|
364
|
+
var CaliperAgentStateSchema = z.object({
|
|
365
|
+
viewport: ViewportSchema,
|
|
366
|
+
activeSelection: SelectionMetadataSchema.nullable(),
|
|
367
|
+
selectionFingerprint: CaliperSelectorInputSchema.nullable(),
|
|
368
|
+
lastMeasurement: MeasurementResultSchema.nullable(),
|
|
369
|
+
measurementFingerprint: z.object({
|
|
370
|
+
primary: CaliperSelectorInputSchema,
|
|
371
|
+
secondary: CaliperSelectorInputSchema
|
|
372
|
+
}).nullable(),
|
|
373
|
+
lastUpdated: z.number()
|
|
374
|
+
});
|
|
375
|
+
var IdSchema = z.union([z.string(), z.number()]);
|
|
376
|
+
var RpcFactory = class {
|
|
377
|
+
static request(method, params, id) {
|
|
378
|
+
return JSONRPCRequestSchema.parse({ jsonrpc: "2.0", method, params, id });
|
|
379
|
+
}
|
|
380
|
+
static response(id, result) {
|
|
381
|
+
return JSONRPCResultResponseSchema.parse({ jsonrpc: "2.0", id, result });
|
|
382
|
+
}
|
|
383
|
+
static error(id, code, message, data) {
|
|
384
|
+
return JSONRPCErrorResponseSchema.parse({ jsonrpc: "2.0", id, error: { code, message, data } });
|
|
385
|
+
}
|
|
386
|
+
static notification(method, params) {
|
|
387
|
+
return JSONRPCNotificationSchema.parse({ jsonrpc: "2.0", method, params });
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
var isId = (value) => {
|
|
391
|
+
return typeof value === "string" || typeof value === "number";
|
|
392
|
+
};
|
|
393
|
+
var isCaliperActionResult = (value) => {
|
|
394
|
+
return CaliperActionResultSchema.safeParse(value).success;
|
|
395
|
+
};
|
|
396
|
+
var CaliperResponseSchema = z.union([
|
|
397
|
+
z.object({
|
|
398
|
+
jsonrpc: z.literal("2.0"),
|
|
399
|
+
id: IdSchema,
|
|
400
|
+
result: CaliperActionResultSchema
|
|
401
|
+
}),
|
|
402
|
+
JSONRPCErrorResponseSchema
|
|
403
|
+
]);
|
|
404
|
+
var CaliperNotificationSchema = z.union([
|
|
405
|
+
z.object({
|
|
406
|
+
jsonrpc: z.literal("2.0"),
|
|
407
|
+
method: z.literal(CALIPER_METHODS.REGISTER_TAB),
|
|
408
|
+
params: z.object({
|
|
409
|
+
tabId: z.string(),
|
|
410
|
+
url: z.string(),
|
|
411
|
+
title: z.string(),
|
|
412
|
+
isFocused: z.boolean()
|
|
413
|
+
})
|
|
414
|
+
}),
|
|
415
|
+
z.object({
|
|
416
|
+
jsonrpc: z.literal("2.0"),
|
|
417
|
+
method: z.literal(CALIPER_METHODS.TAB_UPDATE),
|
|
418
|
+
params: z.object({
|
|
419
|
+
isFocused: z.boolean()
|
|
420
|
+
})
|
|
421
|
+
}),
|
|
422
|
+
z.object({
|
|
423
|
+
jsonrpc: z.literal("2.0"),
|
|
424
|
+
method: z.literal(CALIPER_METHODS.STATE_UPDATE),
|
|
425
|
+
params: CaliperAgentStateSchema
|
|
426
|
+
})
|
|
427
|
+
]);
|
|
428
|
+
var BridgeMessageSchema = z.union([
|
|
429
|
+
CaliperResponseSchema,
|
|
430
|
+
CaliperNotificationSchema,
|
|
431
|
+
JSONRPCResultResponseSchema
|
|
432
|
+
]);
|
|
433
|
+
z.object({
|
|
434
|
+
selector: z.string()
|
|
435
|
+
});
|
|
436
|
+
z.object({
|
|
437
|
+
primarySelector: z.string(),
|
|
438
|
+
secondarySelector: z.string()
|
|
439
|
+
});
|
|
440
|
+
z.object({
|
|
441
|
+
selector: z.string()
|
|
442
|
+
});
|
|
443
|
+
z.object({
|
|
444
|
+
selector: z.string(),
|
|
445
|
+
depth: z.number().optional()
|
|
446
|
+
});
|
|
447
|
+
WalkOptionsSchema.extend({
|
|
448
|
+
selector: z.string()
|
|
449
|
+
});
|
|
450
|
+
z.object({});
|
|
451
|
+
var stringSerializer = (defaultValue = "") => ({
|
|
452
|
+
collectStrings: (value, collect) => collect(value),
|
|
453
|
+
serialize: (value, context) => {
|
|
454
|
+
context.view.setUint16(context.offset, context.getStringId(value));
|
|
455
|
+
context.offset += 2;
|
|
456
|
+
},
|
|
457
|
+
deserialize: (context) => {
|
|
458
|
+
const result = context.stringList[context.view.getUint16(context.offset)] || defaultValue || void 0;
|
|
459
|
+
context.offset += 2;
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
var optionalStringSerializer = () => ({
|
|
464
|
+
collectStrings: (value, collect) => collect(value),
|
|
465
|
+
serialize: (value, context) => {
|
|
466
|
+
context.view.setUint16(context.offset, context.getStringId(value));
|
|
467
|
+
context.offset += 2;
|
|
468
|
+
},
|
|
469
|
+
deserialize: (context) => {
|
|
470
|
+
const result = context.stringList[context.view.getUint16(context.offset)] || void 0;
|
|
471
|
+
context.offset += 2;
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
var floatSerializer = () => ({
|
|
476
|
+
collectStrings: () => {
|
|
477
|
+
},
|
|
478
|
+
serialize: (value, context) => {
|
|
479
|
+
context.view.setFloat32(context.offset, value ?? 0);
|
|
480
|
+
context.offset += 4;
|
|
481
|
+
},
|
|
482
|
+
deserialize: (context) => {
|
|
483
|
+
const result = context.view.getFloat32(context.offset);
|
|
484
|
+
context.offset += 4;
|
|
485
|
+
return result;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
var boxEdgesSerializer = () => ({
|
|
489
|
+
collectStrings: () => {
|
|
490
|
+
},
|
|
491
|
+
serialize: (edges, context) => {
|
|
492
|
+
context.view.setFloat32(context.offset, edges?.top ?? 0);
|
|
493
|
+
context.offset += 4;
|
|
494
|
+
context.view.setFloat32(context.offset, edges?.right ?? 0);
|
|
495
|
+
context.offset += 4;
|
|
496
|
+
context.view.setFloat32(context.offset, edges?.bottom ?? 0);
|
|
497
|
+
context.offset += 4;
|
|
498
|
+
context.view.setFloat32(context.offset, edges?.left ?? 0);
|
|
499
|
+
context.offset += 4;
|
|
500
|
+
},
|
|
501
|
+
deserialize: (context) => {
|
|
502
|
+
const edges = {
|
|
503
|
+
top: context.view.getFloat32(context.offset),
|
|
504
|
+
right: context.view.getFloat32(context.offset + 4),
|
|
505
|
+
bottom: context.view.getFloat32(context.offset + 8),
|
|
506
|
+
left: context.view.getFloat32(context.offset + 12)
|
|
507
|
+
};
|
|
508
|
+
context.offset += 16;
|
|
509
|
+
if (edges.top === 0 && edges.right === 0 && edges.bottom === 0 && edges.left === 0) {
|
|
510
|
+
return void 0;
|
|
511
|
+
}
|
|
512
|
+
return edges;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
var nullableNumberSerializer = () => ({
|
|
516
|
+
collectStrings: (value, collect) => collect(value == null ? null : String(value)),
|
|
517
|
+
serialize: (value, context) => {
|
|
518
|
+
context.view.setUint16(context.offset, context.getStringId(value == null ? null : String(value)));
|
|
519
|
+
context.offset += 2;
|
|
520
|
+
},
|
|
521
|
+
deserialize: (context) => {
|
|
522
|
+
const strValue = context.stringList[context.view.getUint16(context.offset)] || null;
|
|
523
|
+
context.offset += 2;
|
|
524
|
+
return strValue === null ? null : parseFloat(strValue);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
var numberOrStringSerializer = (defaultValue = "") => ({
|
|
528
|
+
collectStrings: (value, collect) => collect(value == null ? void 0 : String(value)),
|
|
529
|
+
serialize: (value, context) => {
|
|
530
|
+
context.view.setUint16(context.offset, context.getStringId(value == null ? void 0 : String(value)));
|
|
531
|
+
context.offset += 2;
|
|
532
|
+
},
|
|
533
|
+
deserialize: (context) => {
|
|
534
|
+
const id = context.view.getUint16(context.offset);
|
|
535
|
+
const strValue = context.stringList[id];
|
|
536
|
+
context.offset += 2;
|
|
537
|
+
if (!strValue && id === 0) return defaultValue;
|
|
538
|
+
if (!strValue) return void 0;
|
|
539
|
+
const numValue = parseFloat(strValue);
|
|
540
|
+
return isNaN(numValue) ? strValue : numValue;
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
var nullableNumberOrStringSerializer = () => ({
|
|
544
|
+
collectStrings: (value, collect) => collect(value == null ? null : String(value)),
|
|
545
|
+
serialize: (value, context) => {
|
|
546
|
+
context.view.setUint16(context.offset, context.getStringId(value == null ? null : String(value)));
|
|
547
|
+
context.offset += 2;
|
|
548
|
+
},
|
|
549
|
+
deserialize: (context) => {
|
|
550
|
+
const strValue = context.stringList[context.view.getUint16(context.offset)] || null;
|
|
551
|
+
context.offset += 2;
|
|
552
|
+
if (strValue === null) return null;
|
|
553
|
+
const numValue = parseFloat(strValue);
|
|
554
|
+
return isNaN(numValue) ? strValue : numValue;
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
var STYLE_SERIALIZERS = {
|
|
558
|
+
display: stringSerializer(),
|
|
559
|
+
visibility: stringSerializer(),
|
|
560
|
+
position: stringSerializer(),
|
|
561
|
+
boxSizing: stringSerializer(),
|
|
562
|
+
padding: boxEdgesSerializer(),
|
|
563
|
+
margin: boxEdgesSerializer(),
|
|
564
|
+
border: boxEdgesSerializer(),
|
|
565
|
+
gap: nullableNumberSerializer(),
|
|
566
|
+
flexDirection: optionalStringSerializer(),
|
|
567
|
+
justifyContent: optionalStringSerializer(),
|
|
568
|
+
alignItems: optionalStringSerializer(),
|
|
569
|
+
fontSize: floatSerializer(),
|
|
570
|
+
fontWeight: stringSerializer(),
|
|
571
|
+
fontFamily: stringSerializer(),
|
|
572
|
+
lineHeight: nullableNumberOrStringSerializer(),
|
|
573
|
+
letterSpacing: numberOrStringSerializer(0),
|
|
574
|
+
color: stringSerializer(),
|
|
575
|
+
backgroundColor: stringSerializer(),
|
|
576
|
+
borderColor: optionalStringSerializer(),
|
|
577
|
+
borderRadius: stringSerializer("0"),
|
|
578
|
+
boxShadow: optionalStringSerializer(),
|
|
579
|
+
opacity: numberOrStringSerializer(1),
|
|
580
|
+
outline: optionalStringSerializer(),
|
|
581
|
+
outlineColor: optionalStringSerializer(),
|
|
582
|
+
zIndex: nullableNumberOrStringSerializer(),
|
|
583
|
+
overflow: stringSerializer("visible"),
|
|
584
|
+
overflowX: stringSerializer("visible"),
|
|
585
|
+
overflowY: stringSerializer("visible"),
|
|
586
|
+
contentVisibility: stringSerializer("visible")
|
|
587
|
+
};
|
|
588
|
+
var STYLE_KEYS = Object.keys(STYLE_SERIALIZERS);
|
|
589
|
+
var BitBridge = class _BitBridge {
|
|
590
|
+
static MAGIC = 1128352841;
|
|
591
|
+
// "CALI"
|
|
592
|
+
static serialize(root) {
|
|
593
|
+
const strings = /* @__PURE__ */ new Map();
|
|
594
|
+
const stringList = [];
|
|
595
|
+
const getStringId = (str) => {
|
|
596
|
+
if (str === void 0 || str === null) return 0;
|
|
597
|
+
let id = strings.get(str);
|
|
598
|
+
if (id === void 0) {
|
|
599
|
+
id = stringList.length + 1;
|
|
600
|
+
strings.set(str, id);
|
|
601
|
+
stringList.push(str);
|
|
602
|
+
}
|
|
603
|
+
return id;
|
|
604
|
+
};
|
|
605
|
+
const stack = [root];
|
|
606
|
+
while (stack.length > 0) {
|
|
607
|
+
const node = stack.pop();
|
|
608
|
+
getStringId(node.tag);
|
|
609
|
+
getStringId(node.selector);
|
|
610
|
+
getStringId(node.agentId);
|
|
611
|
+
getStringId(node.htmlId);
|
|
612
|
+
getStringId(node.textContent);
|
|
613
|
+
getStringId(node.marker);
|
|
614
|
+
node.classes?.forEach((className) => getStringId(className));
|
|
615
|
+
for (const key of STYLE_KEYS) {
|
|
616
|
+
const serializer = STYLE_SERIALIZERS[key];
|
|
617
|
+
serializer.collectStrings(
|
|
618
|
+
node.styles[key],
|
|
619
|
+
getStringId
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
if (node.children) {
|
|
623
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
624
|
+
stack.push(node.children[i]);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const encoder = new TextEncoder();
|
|
629
|
+
const encodedStrings = stringList.map((rawString) => encoder.encode(rawString));
|
|
630
|
+
let dictSize = 8;
|
|
631
|
+
encodedStrings.forEach((bytes) => dictSize += 2 + bytes.length);
|
|
632
|
+
const buffer = new ArrayBuffer(dictSize + 1024 * 1024 * 5);
|
|
633
|
+
const view = new DataView(buffer);
|
|
634
|
+
let offset = 0;
|
|
635
|
+
view.setUint32(offset, _BitBridge.MAGIC);
|
|
636
|
+
offset += 4;
|
|
637
|
+
view.setUint32(offset, stringList.length);
|
|
638
|
+
offset += 4;
|
|
639
|
+
encodedStrings.forEach((bytes) => {
|
|
640
|
+
view.setUint16(offset, bytes.length);
|
|
641
|
+
offset += 2;
|
|
642
|
+
new Uint8Array(buffer, offset, bytes.length).set(bytes);
|
|
643
|
+
offset += bytes.length;
|
|
644
|
+
});
|
|
645
|
+
const nodeStack = [root];
|
|
646
|
+
while (nodeStack.length > 0) {
|
|
647
|
+
const node = nodeStack.pop();
|
|
648
|
+
view.setUint16(offset, getStringId(node.tag));
|
|
649
|
+
offset += 2;
|
|
650
|
+
view.setUint16(offset, getStringId(node.selector));
|
|
651
|
+
offset += 2;
|
|
652
|
+
view.setUint16(offset, getStringId(node.agentId));
|
|
653
|
+
offset += 2;
|
|
654
|
+
view.setUint16(offset, getStringId(node.htmlId));
|
|
655
|
+
offset += 2;
|
|
656
|
+
view.setUint16(offset, getStringId(node.textContent));
|
|
657
|
+
offset += 2;
|
|
658
|
+
view.setUint16(offset, getStringId(node.marker));
|
|
659
|
+
offset += 2;
|
|
660
|
+
view.setUint8(offset, node.ariaHidden ? 1 : 0);
|
|
661
|
+
offset += 1;
|
|
662
|
+
const classes = node.classes || [];
|
|
663
|
+
view.setUint16(offset, classes.length);
|
|
664
|
+
offset += 2;
|
|
665
|
+
classes.forEach((className) => {
|
|
666
|
+
view.setUint16(offset, getStringId(className));
|
|
667
|
+
offset += 2;
|
|
668
|
+
});
|
|
669
|
+
view.setFloat32(offset, node.rect.top);
|
|
670
|
+
offset += 4;
|
|
671
|
+
view.setFloat32(offset, node.rect.left);
|
|
672
|
+
offset += 4;
|
|
673
|
+
view.setFloat32(offset, node.rect.width);
|
|
674
|
+
offset += 4;
|
|
675
|
+
view.setFloat32(offset, node.rect.height);
|
|
676
|
+
offset += 4;
|
|
677
|
+
view.setFloat32(offset, node.rect.bottom);
|
|
678
|
+
offset += 4;
|
|
679
|
+
view.setFloat32(offset, node.rect.right);
|
|
680
|
+
offset += 4;
|
|
681
|
+
view.setFloat32(offset, node.viewportRect.top);
|
|
682
|
+
offset += 4;
|
|
683
|
+
view.setFloat32(offset, node.viewportRect.left);
|
|
684
|
+
offset += 4;
|
|
685
|
+
const ctx = { view, offset, getStringId };
|
|
686
|
+
for (const key of STYLE_KEYS) {
|
|
687
|
+
const serializer = STYLE_SERIALIZERS[key];
|
|
688
|
+
serializer.serialize(
|
|
689
|
+
node.styles[key],
|
|
690
|
+
ctx
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
offset = ctx.offset;
|
|
694
|
+
view.setUint16(offset, node.depth);
|
|
695
|
+
offset += 2;
|
|
696
|
+
const children = node.children || [];
|
|
697
|
+
view.setUint16(offset, children.length);
|
|
698
|
+
offset += 2;
|
|
699
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
700
|
+
nodeStack.push(children[i]);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return new Uint8Array(buffer, 0, offset);
|
|
704
|
+
}
|
|
705
|
+
static deserialize(data) {
|
|
706
|
+
const buffer = data.buffer;
|
|
707
|
+
const view = new DataView(buffer, data.byteOffset, data.byteLength);
|
|
708
|
+
let offset = 0;
|
|
709
|
+
const magic = view.getUint32(offset);
|
|
710
|
+
offset += 4;
|
|
711
|
+
if (magic !== _BitBridge.MAGIC) throw new Error("Invalid BitBridge format");
|
|
712
|
+
const stringCount = view.getUint32(offset);
|
|
713
|
+
offset += 4;
|
|
714
|
+
const stringList = [""];
|
|
715
|
+
const decoder = new TextDecoder();
|
|
716
|
+
for (let i = 0; i < stringCount; i++) {
|
|
717
|
+
const len = view.getUint16(offset);
|
|
718
|
+
offset += 2;
|
|
719
|
+
const str = decoder.decode(new Uint8Array(buffer, data.byteOffset + offset, len));
|
|
720
|
+
stringList.push(str);
|
|
721
|
+
offset += len;
|
|
722
|
+
}
|
|
723
|
+
const root = this.deserializeNode(view, offset, stringList);
|
|
724
|
+
offset = root.newOffset;
|
|
725
|
+
const nodeStack = [{
|
|
726
|
+
node: root.node,
|
|
727
|
+
childrenToRead: root.childCount
|
|
728
|
+
}];
|
|
729
|
+
while (nodeStack.length > 0) {
|
|
730
|
+
const current = nodeStack[nodeStack.length - 1];
|
|
731
|
+
if (current.childrenToRead > 0) {
|
|
732
|
+
const childResult = this.deserializeNode(view, offset, stringList);
|
|
733
|
+
offset = childResult.newOffset;
|
|
734
|
+
childResult.node.parentAgentId = current.node.agentId;
|
|
735
|
+
current.node.children.push(childResult.node);
|
|
736
|
+
current.childrenToRead--;
|
|
737
|
+
if (childResult.childCount > 0) {
|
|
738
|
+
nodeStack.push({
|
|
739
|
+
node: childResult.node,
|
|
740
|
+
childrenToRead: childResult.childCount
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
nodeStack.pop();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return root.node;
|
|
748
|
+
}
|
|
749
|
+
static deserializeNode(view, offset, stringList) {
|
|
750
|
+
const tag = stringList[view.getUint16(offset)] || "";
|
|
751
|
+
offset += 2;
|
|
752
|
+
const selector = stringList[view.getUint16(offset)] || "";
|
|
753
|
+
offset += 2;
|
|
754
|
+
const agentId = stringList[view.getUint16(offset)] || "";
|
|
755
|
+
offset += 2;
|
|
756
|
+
const htmlId = stringList[view.getUint16(offset)] || void 0;
|
|
757
|
+
offset += 2;
|
|
758
|
+
const textContent = stringList[view.getUint16(offset)] || void 0;
|
|
759
|
+
offset += 2;
|
|
760
|
+
const marker = stringList[view.getUint16(offset)] || void 0;
|
|
761
|
+
offset += 2;
|
|
762
|
+
const ariaHidden = view.getUint8(offset) === 1;
|
|
763
|
+
offset += 1;
|
|
764
|
+
const classCount = view.getUint16(offset);
|
|
765
|
+
offset += 2;
|
|
766
|
+
const classes = [];
|
|
767
|
+
for (let i = 0; i < classCount; i++) {
|
|
768
|
+
classes.push(stringList[view.getUint16(offset)] || "");
|
|
769
|
+
offset += 2;
|
|
770
|
+
}
|
|
771
|
+
const rect = {
|
|
772
|
+
top: view.getFloat32(offset),
|
|
773
|
+
left: view.getFloat32(offset + 4),
|
|
774
|
+
width: view.getFloat32(offset + 8),
|
|
775
|
+
height: view.getFloat32(offset + 12),
|
|
776
|
+
bottom: view.getFloat32(offset + 16),
|
|
777
|
+
right: view.getFloat32(offset + 20),
|
|
778
|
+
x: 0,
|
|
779
|
+
y: 0
|
|
780
|
+
};
|
|
781
|
+
rect.x = rect.left;
|
|
782
|
+
rect.y = rect.top;
|
|
783
|
+
offset += 24;
|
|
784
|
+
const vTop = view.getFloat32(offset);
|
|
785
|
+
offset += 4;
|
|
786
|
+
const vLeft = view.getFloat32(offset);
|
|
787
|
+
offset += 4;
|
|
788
|
+
const ctx = { view, offset, stringList };
|
|
789
|
+
const styles = {};
|
|
790
|
+
for (const key of STYLE_KEYS) {
|
|
791
|
+
const serializer = STYLE_SERIALIZERS[key];
|
|
792
|
+
styles[key] = serializer.deserialize(ctx);
|
|
793
|
+
}
|
|
794
|
+
offset = ctx.offset;
|
|
795
|
+
const depth = view.getUint16(offset);
|
|
796
|
+
offset += 2;
|
|
797
|
+
const childCount = view.getUint16(offset);
|
|
798
|
+
offset += 2;
|
|
799
|
+
const node = {
|
|
800
|
+
agentId,
|
|
801
|
+
selector,
|
|
802
|
+
tag,
|
|
803
|
+
htmlId,
|
|
804
|
+
classes,
|
|
805
|
+
textContent,
|
|
806
|
+
rect,
|
|
807
|
+
viewportRect: { top: vTop, left: vLeft },
|
|
808
|
+
depth,
|
|
809
|
+
childCount,
|
|
810
|
+
children: [],
|
|
811
|
+
styles,
|
|
812
|
+
marker,
|
|
813
|
+
ariaHidden,
|
|
814
|
+
measurements: {
|
|
815
|
+
toParent: { top: 0, left: 0, bottom: 0, right: 0 },
|
|
816
|
+
toPreviousSibling: null,
|
|
817
|
+
toNextSibling: null,
|
|
818
|
+
indexInParent: 0,
|
|
819
|
+
siblingCount: childCount
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
return { node, childCount, newOffset: offset };
|
|
823
|
+
}
|
|
824
|
+
static packEnvelope(json, payload) {
|
|
825
|
+
const jsonBytes = new TextEncoder().encode(json);
|
|
826
|
+
const combined = new Uint8Array(4 + jsonBytes.byteLength + payload.byteLength);
|
|
827
|
+
const view = new DataView(combined.buffer);
|
|
828
|
+
view.setUint32(0, jsonBytes.byteLength);
|
|
829
|
+
combined.set(jsonBytes, 4);
|
|
830
|
+
combined.set(payload, 4 + jsonBytes.byteLength);
|
|
831
|
+
return combined;
|
|
832
|
+
}
|
|
833
|
+
static unpackEnvelope(data) {
|
|
834
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
835
|
+
const jsonLen = view.getUint32(0);
|
|
836
|
+
const jsonStr = new TextDecoder().decode(data.subarray(4, 4 + jsonLen));
|
|
837
|
+
const payload = data.subarray(4 + jsonLen);
|
|
838
|
+
return { json: jsonStr, payload };
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
var MAX_DESCENDANT_COUNT = 1e3;
|
|
842
|
+
var RECOMMENDED_PAGINATION_THRESHOLD = 40;
|
|
843
|
+
|
|
844
|
+
// ../mcp-server/src/utils/logger.ts
|
|
845
|
+
var Logger = class {
|
|
846
|
+
enabled = true;
|
|
847
|
+
prefix;
|
|
848
|
+
constructor(prefix) {
|
|
849
|
+
this.prefix = `[Caliper:${prefix}]`;
|
|
850
|
+
}
|
|
851
|
+
setEnabled(enabled) {
|
|
852
|
+
this.enabled = enabled;
|
|
853
|
+
}
|
|
854
|
+
info(...args) {
|
|
855
|
+
if (this.enabled) console.error(this.prefix, ...args);
|
|
856
|
+
}
|
|
857
|
+
warn(...args) {
|
|
858
|
+
if (this.enabled) console.error(this.prefix, "WARN:", ...args);
|
|
859
|
+
}
|
|
860
|
+
error(...args) {
|
|
861
|
+
if (this.enabled) console.error(this.prefix, "ERROR:", ...args);
|
|
862
|
+
}
|
|
863
|
+
debug(...args) {
|
|
864
|
+
if (this.enabled) console.error(this.prefix, "DEBUG:", ...args);
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
var createLogger = (prefix) => new Logger(prefix);
|
|
868
|
+
|
|
869
|
+
// ../mcp-server/src/services/tab-manager.ts
|
|
870
|
+
var logger2 = createLogger("tab-manager");
|
|
871
|
+
var TabManager = class {
|
|
872
|
+
tabs = /* @__PURE__ */ new Map();
|
|
873
|
+
activeTabId = null;
|
|
874
|
+
registerTab(tab) {
|
|
875
|
+
this.tabs.set(tab.id, {
|
|
876
|
+
...tab,
|
|
877
|
+
lastActive: Date.now()
|
|
878
|
+
});
|
|
879
|
+
this.activeTabId = tab.id;
|
|
880
|
+
logger2.info(`Tab registered: ${tab.title} (${tab.id})`);
|
|
881
|
+
}
|
|
882
|
+
updateTab(id, isFocused) {
|
|
883
|
+
const tab = this.tabs.get(id);
|
|
884
|
+
if (tab) {
|
|
885
|
+
tab.lastActive = Date.now();
|
|
886
|
+
if (isFocused) {
|
|
887
|
+
this.activeTabId = id;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
removeTab(id, ws) {
|
|
892
|
+
const tab = this.tabs.get(id);
|
|
893
|
+
if (tab && tab.ws === ws) {
|
|
894
|
+
this.tabs.delete(id);
|
|
895
|
+
if (this.activeTabId === id) {
|
|
896
|
+
this.activeTabId = Array.from(this.tabs.keys())[0] || null;
|
|
897
|
+
}
|
|
898
|
+
logger2.info(`Tab disconnected: ${id}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
getActiveTab() {
|
|
902
|
+
if (!this.activeTabId) return null;
|
|
903
|
+
return this.tabs.get(this.activeTabId) || null;
|
|
904
|
+
}
|
|
905
|
+
getTabById(id) {
|
|
906
|
+
return this.tabs.get(id) || null;
|
|
907
|
+
}
|
|
908
|
+
getAllTabs() {
|
|
909
|
+
return Array.from(this.tabs.values());
|
|
910
|
+
}
|
|
911
|
+
switchTab(id) {
|
|
912
|
+
if (this.tabs.has(id)) {
|
|
913
|
+
this.activeTabId = id;
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
getTabCount() {
|
|
919
|
+
return this.tabs.size;
|
|
920
|
+
}
|
|
921
|
+
findTabByUrl(urlFragment) {
|
|
922
|
+
for (const tab of this.tabs.values()) {
|
|
923
|
+
if (tab.url.includes(urlFragment)) {
|
|
924
|
+
return tab;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
var tabManager = new TabManager();
|
|
931
|
+
|
|
932
|
+
// ../mcp-server/src/utils/id.ts
|
|
933
|
+
function generateId(prefix = "") {
|
|
934
|
+
const randomPart = Math.random().toString(36).substring(2, 11);
|
|
935
|
+
return prefix ? `${prefix}-${randomPart}` : randomPart;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// ../mcp-server/src/utils/errors.ts
|
|
939
|
+
var BridgeError = class _BridgeError extends Error {
|
|
940
|
+
constructor(message) {
|
|
941
|
+
super(message);
|
|
942
|
+
this.name = "BridgeError";
|
|
943
|
+
Object.setPrototypeOf(this, _BridgeError.prototype);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
var BridgeTimeoutError = class _BridgeTimeoutError extends BridgeError {
|
|
947
|
+
constructor(method, attempt, totalRetries) {
|
|
948
|
+
const message = attempt < totalRetries ? `Bridge request timed out for method: ${method}. Retrying... (Attempt ${attempt + 1}/${totalRetries + 1})` : `Bridge request timed out for method: ${method}. The bridge is connected, but the operation took too long.`;
|
|
949
|
+
super(message);
|
|
950
|
+
this.name = "BridgeTimeoutError";
|
|
951
|
+
Object.setPrototypeOf(this, _BridgeTimeoutError.prototype);
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
var BridgeValidationError = class _BridgeValidationError extends BridgeError {
|
|
955
|
+
constructor(message = "Data connection unauthorized or malformed.") {
|
|
956
|
+
super(message);
|
|
957
|
+
this.name = "BridgeValidationError";
|
|
958
|
+
Object.setPrototypeOf(this, _BridgeValidationError.prototype);
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// ../mcp-server/src/shared/constants.ts
|
|
963
|
+
var DEFAULT_BRIDGE_PORT = 9876;
|
|
964
|
+
var BRIDGE_REQUEST_TIMEOUT_MS = 9e4;
|
|
965
|
+
|
|
966
|
+
// ../mcp-server/src/shared/events.ts
|
|
967
|
+
var BRIDGE_EVENTS = {
|
|
968
|
+
STATE: "state"
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
// ../mcp-server/src/services/bridge-service.ts
|
|
972
|
+
var logger3 = createLogger("mcp-bridge");
|
|
973
|
+
function getExponentialBackoff(attempt, baseDelay) {
|
|
974
|
+
return Math.pow(2, attempt) * baseDelay;
|
|
975
|
+
}
|
|
976
|
+
var BridgeService = class extends EventEmitter {
|
|
977
|
+
wss = null;
|
|
978
|
+
pendingCalls = /* @__PURE__ */ new Map();
|
|
979
|
+
startupError = null;
|
|
980
|
+
constructor() {
|
|
981
|
+
super();
|
|
982
|
+
}
|
|
983
|
+
async start(port2 = DEFAULT_BRIDGE_PORT, retries = 3) {
|
|
984
|
+
if (this.wss) return;
|
|
985
|
+
const attemptStart = (currentPort, remaining) => {
|
|
986
|
+
return new Promise((resolve, reject) => {
|
|
987
|
+
try {
|
|
988
|
+
const server2 = new WebSocketServer({ port: currentPort });
|
|
989
|
+
server2.on("listening", () => {
|
|
990
|
+
this.wss = server2;
|
|
991
|
+
this.init();
|
|
992
|
+
resolve();
|
|
993
|
+
});
|
|
994
|
+
server2.on("error", (error) => {
|
|
995
|
+
const err = error;
|
|
996
|
+
if (err.code === "EADDRINUSE" && remaining > 0) {
|
|
997
|
+
const attemptNum = retries - remaining + 1;
|
|
998
|
+
const delay = getExponentialBackoff(attemptNum - 1, 500);
|
|
999
|
+
logger3.warn(
|
|
1000
|
+
`Port ${currentPort} is currently in use. Retrying in ${delay}ms... (Attempt ${attemptNum}/${retries})`
|
|
1001
|
+
);
|
|
1002
|
+
server2.close();
|
|
1003
|
+
server2.removeAllListeners();
|
|
1004
|
+
setTimeout(() => {
|
|
1005
|
+
attemptStart(currentPort, remaining - 1).then(resolve).catch(reject);
|
|
1006
|
+
}, delay);
|
|
1007
|
+
} else {
|
|
1008
|
+
const errorMessage = err.code === "EADDRINUSE" ? `Port ${currentPort} is already in use by another process. Please close the other instance of Caliper or use a different port.` : `WebSocket server error: ${String(error)}`;
|
|
1009
|
+
logger3.error(errorMessage);
|
|
1010
|
+
this.startupError = errorMessage;
|
|
1011
|
+
server2.close();
|
|
1012
|
+
reject(new Error(errorMessage));
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
reject(error);
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
};
|
|
1020
|
+
try {
|
|
1021
|
+
await attemptStart(port2, retries);
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
if (!this.startupError) {
|
|
1024
|
+
this.startupError = `Failed to start WebSocket relay after multiple attempts: ${String(error)}`;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
init() {
|
|
1029
|
+
if (!this.wss) return;
|
|
1030
|
+
this.wss.on("connection", (ws) => {
|
|
1031
|
+
let tabId = null;
|
|
1032
|
+
ws.on("message", (data) => {
|
|
1033
|
+
try {
|
|
1034
|
+
let rawMessage;
|
|
1035
|
+
let binaryPayload;
|
|
1036
|
+
if (Buffer.isBuffer(data)) {
|
|
1037
|
+
if (data[0] === 123) {
|
|
1038
|
+
rawMessage = JSON.parse(data.toString());
|
|
1039
|
+
} else {
|
|
1040
|
+
const { json, payload } = BitBridge.unpackEnvelope(new Uint8Array(data));
|
|
1041
|
+
rawMessage = JSON.parse(json);
|
|
1042
|
+
binaryPayload = payload;
|
|
1043
|
+
}
|
|
1044
|
+
} else {
|
|
1045
|
+
rawMessage = JSON.parse(data.toString());
|
|
1046
|
+
}
|
|
1047
|
+
const result = BridgeMessageSchema.safeParse(rawMessage);
|
|
1048
|
+
if (!result.success) {
|
|
1049
|
+
logger3.error("Invalid WS message format:", z.treeifyError(result.error));
|
|
1050
|
+
const msgObj = rawMessage;
|
|
1051
|
+
if (msgObj?.id !== void 0 && isId(msgObj.id)) {
|
|
1052
|
+
const resolve = this.pendingCalls.get(String(msgObj.id));
|
|
1053
|
+
if (resolve) {
|
|
1054
|
+
resolve({ error: new BridgeValidationError().message });
|
|
1055
|
+
this.pendingCalls.delete(String(msgObj.id));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
const message = result.data;
|
|
1061
|
+
if ("id" in message) {
|
|
1062
|
+
if ("result" in message) {
|
|
1063
|
+
const resolve = this.pendingCalls.get(String(message.id));
|
|
1064
|
+
if (resolve) {
|
|
1065
|
+
const finalResult = message.result;
|
|
1066
|
+
if (isCaliperActionResult(finalResult)) {
|
|
1067
|
+
if (finalResult.success && finalResult.method === CALIPER_METHODS.WALK_AND_MEASURE && binaryPayload) {
|
|
1068
|
+
try {
|
|
1069
|
+
const root = BitBridge.deserialize(binaryPayload);
|
|
1070
|
+
if ("walkResult" in finalResult) {
|
|
1071
|
+
finalResult.walkResult.root = root;
|
|
1072
|
+
}
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
logger3.error("Bit-Bridge reconstruction failed:", error);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
resolve(finalResult);
|
|
1078
|
+
} else if (finalResult && typeof finalResult === "object" && "error" in finalResult) {
|
|
1079
|
+
resolve(finalResult);
|
|
1080
|
+
} else {
|
|
1081
|
+
resolve({ error: "Unexpected result format received from bridge" });
|
|
1082
|
+
}
|
|
1083
|
+
this.pendingCalls.delete(String(message.id));
|
|
1084
|
+
}
|
|
1085
|
+
} else if ("error" in message) {
|
|
1086
|
+
if (message.id !== null) {
|
|
1087
|
+
const resolve = this.pendingCalls.get(String(message.id));
|
|
1088
|
+
if (resolve) {
|
|
1089
|
+
resolve({ error: message.error.message });
|
|
1090
|
+
this.pendingCalls.delete(String(message.id));
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
} else if ("method" in message) {
|
|
1095
|
+
if (message.method === CALIPER_METHODS.REGISTER_TAB) {
|
|
1096
|
+
const { tabId: newTabId, url, title, isFocused } = message.params;
|
|
1097
|
+
tabId = newTabId;
|
|
1098
|
+
tabManager.registerTab({
|
|
1099
|
+
id: tabId,
|
|
1100
|
+
ws,
|
|
1101
|
+
url,
|
|
1102
|
+
title,
|
|
1103
|
+
isFocused
|
|
1104
|
+
});
|
|
1105
|
+
} else if (message.method === CALIPER_METHODS.TAB_UPDATE) {
|
|
1106
|
+
const { isFocused } = message.params;
|
|
1107
|
+
if (tabId) {
|
|
1108
|
+
tabManager.updateTab(tabId, isFocused);
|
|
1109
|
+
}
|
|
1110
|
+
} else if (message.method === CALIPER_METHODS.STATE_UPDATE) {
|
|
1111
|
+
const state = message.params;
|
|
1112
|
+
this.emit(BRIDGE_EVENTS.STATE, state);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
logger3.error("WS Message Processing Error:", error);
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
ws.on("close", () => {
|
|
1120
|
+
if (tabId) {
|
|
1121
|
+
tabManager.removeTab(tabId, ws);
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
});
|
|
1125
|
+
logger3.info(`WebSocket Relay initialized on port ${this.wss.options.port}`);
|
|
1126
|
+
}
|
|
1127
|
+
async call(method, params, retries = 0) {
|
|
1128
|
+
if (this.startupError) {
|
|
1129
|
+
throw new Error(`Caliper Bridge Unavailable: ${this.startupError}`);
|
|
1130
|
+
}
|
|
1131
|
+
const tab = tabManager.getActiveTab();
|
|
1132
|
+
if (!tab) {
|
|
1133
|
+
throw new Error(
|
|
1134
|
+
"No active browser tab connected to Caliper Bridge. Ensure the browser is open with Caliper enabled."
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
const callWithTimeout = async (attempt) => {
|
|
1138
|
+
const callId = generateId("mcp-call");
|
|
1139
|
+
return new Promise((resolve, reject) => {
|
|
1140
|
+
const timeout = setTimeout(() => {
|
|
1141
|
+
this.pendingCalls.delete(callId);
|
|
1142
|
+
reject(new BridgeTimeoutError(method, attempt, retries));
|
|
1143
|
+
}, BRIDGE_REQUEST_TIMEOUT_MS);
|
|
1144
|
+
this.pendingCalls.set(callId, (res) => {
|
|
1145
|
+
clearTimeout(timeout);
|
|
1146
|
+
if ("error" in res && !("success" in res)) {
|
|
1147
|
+
if (res.error === new BridgeValidationError().message) {
|
|
1148
|
+
reject(new BridgeValidationError());
|
|
1149
|
+
} else {
|
|
1150
|
+
reject(new Error(res.error));
|
|
1151
|
+
}
|
|
1152
|
+
} else {
|
|
1153
|
+
resolve(res);
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
tab.ws.send(JSON.stringify(RpcFactory.request(method, params, callId)));
|
|
1157
|
+
});
|
|
1158
|
+
};
|
|
1159
|
+
let lastError = null;
|
|
1160
|
+
for (let i = 0; i <= retries; i++) {
|
|
1161
|
+
try {
|
|
1162
|
+
return await callWithTimeout(i);
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
lastError = err;
|
|
1165
|
+
const isRetryable = lastError instanceof BridgeTimeoutError || lastError instanceof BridgeValidationError;
|
|
1166
|
+
if (!isRetryable) {
|
|
1167
|
+
throw lastError;
|
|
1168
|
+
}
|
|
1169
|
+
if (i < retries) {
|
|
1170
|
+
const delay = getExponentialBackoff(i, 300);
|
|
1171
|
+
logger3.warn(`${lastError.message}. Retrying in ${delay}ms...`);
|
|
1172
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
throw lastError || new Error(`Failed bridge call: ${method}`);
|
|
1177
|
+
}
|
|
1178
|
+
async stop() {
|
|
1179
|
+
return new Promise((resolve) => {
|
|
1180
|
+
if (!this.wss) {
|
|
1181
|
+
resolve();
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
this.wss.close(() => {
|
|
1185
|
+
logger3.info("WebSocket relay stopped.");
|
|
1186
|
+
resolve();
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
var bridgeService = new BridgeService();
|
|
1192
|
+
|
|
1193
|
+
// ../mcp-server/src/utils/color-utils.ts
|
|
1194
|
+
var NAMED_COLORS = {
|
|
1195
|
+
aliceblue: "#f0f8ff",
|
|
1196
|
+
antiquewhite: "#faebd7",
|
|
1197
|
+
aqua: "#00ffff",
|
|
1198
|
+
aquamarine: "#7fffd4",
|
|
1199
|
+
azure: "#f0ffff",
|
|
1200
|
+
beige: "#f5f5dc",
|
|
1201
|
+
bisque: "#ffe4c4",
|
|
1202
|
+
black: "#000000",
|
|
1203
|
+
blanchedalmond: "#ffebcd",
|
|
1204
|
+
blue: "#0000ff",
|
|
1205
|
+
blueviolet: "#8a2be2",
|
|
1206
|
+
brown: "#a52a2a",
|
|
1207
|
+
burlywood: "#deb887",
|
|
1208
|
+
cadetblue: "#5f9ea0",
|
|
1209
|
+
chartreuse: "#7fff00",
|
|
1210
|
+
chocolate: "#d2691e",
|
|
1211
|
+
coral: "#ff7f50",
|
|
1212
|
+
cornflowerblue: "#6495ed",
|
|
1213
|
+
cornsilk: "#fff8dc",
|
|
1214
|
+
crimson: "#dc143c",
|
|
1215
|
+
cyan: "#00ffff",
|
|
1216
|
+
darkblue: "#00008b",
|
|
1217
|
+
darkcyan: "#008b8b",
|
|
1218
|
+
darkgoldenrod: "#b8860b",
|
|
1219
|
+
darkgray: "#a9a9a9",
|
|
1220
|
+
darkgreen: "#006400",
|
|
1221
|
+
darkgrey: "#a9a9a9",
|
|
1222
|
+
darkkhaki: "#bdb76b",
|
|
1223
|
+
darkmagenta: "#8b008b",
|
|
1224
|
+
darkolivegreen: "#556b2f",
|
|
1225
|
+
darkorange: "#ff8c00",
|
|
1226
|
+
darkorchid: "#9932cc",
|
|
1227
|
+
darkred: "#8b0000",
|
|
1228
|
+
darksalmon: "#e9967a",
|
|
1229
|
+
darkseagreen: "#8fbc8f",
|
|
1230
|
+
darkslateblue: "#483d8b",
|
|
1231
|
+
darkslategray: "#2f4f4f",
|
|
1232
|
+
darkslategrey: "#2f4f4f",
|
|
1233
|
+
darkturquoise: "#00ced1",
|
|
1234
|
+
darkviolet: "#9400d3",
|
|
1235
|
+
deeppink: "#ff1493",
|
|
1236
|
+
deepskyblue: "#00bfff",
|
|
1237
|
+
dimgray: "#696969",
|
|
1238
|
+
dimgrey: "#696969",
|
|
1239
|
+
dodgerblue: "#1e90ff",
|
|
1240
|
+
firebrick: "#b22222",
|
|
1241
|
+
floralwhite: "#fffaf0",
|
|
1242
|
+
forestgreen: "#228b22",
|
|
1243
|
+
fuchsia: "#ff00ff",
|
|
1244
|
+
gainsboro: "#dcdcdc",
|
|
1245
|
+
ghostwhite: "#f8f8ff",
|
|
1246
|
+
gold: "#ffd700",
|
|
1247
|
+
goldenrod: "#daa520",
|
|
1248
|
+
gray: "#808080",
|
|
1249
|
+
green: "#008000",
|
|
1250
|
+
greenyellow: "#adff2f",
|
|
1251
|
+
grey: "#808080",
|
|
1252
|
+
honeydew: "#f0fff0",
|
|
1253
|
+
hotpink: "#ff69b4",
|
|
1254
|
+
indianred: "#cd5c5c",
|
|
1255
|
+
indigo: "#4b0082",
|
|
1256
|
+
ivory: "#fffff0",
|
|
1257
|
+
khaki: "#f0e68c",
|
|
1258
|
+
lavender: "#e6e6fa",
|
|
1259
|
+
lavenderblush: "#fff0f5",
|
|
1260
|
+
lawngreen: "#7cfc00",
|
|
1261
|
+
lemonchiffon: "#fffacd",
|
|
1262
|
+
lightblue: "#add8e6",
|
|
1263
|
+
lightcoral: "#f08080",
|
|
1264
|
+
lightcyan: "#e0ffff",
|
|
1265
|
+
lightgoldenrodyellow: "#fafad2",
|
|
1266
|
+
lightgray: "#d3d3d3",
|
|
1267
|
+
lightgreen: "#90ee90",
|
|
1268
|
+
lightgrey: "#d3d3d3",
|
|
1269
|
+
lightpink: "#ffb6c1",
|
|
1270
|
+
lightsalmon: "#ffa07a",
|
|
1271
|
+
lightseagreen: "#20b2aa",
|
|
1272
|
+
lightskyblue: "#87cefa",
|
|
1273
|
+
lightslategray: "#778899",
|
|
1274
|
+
lightslategrey: "#778899",
|
|
1275
|
+
lightsteelblue: "#b0c4de",
|
|
1276
|
+
lightyellow: "#ffffe0",
|
|
1277
|
+
lime: "#00ff00",
|
|
1278
|
+
limegreen: "#32cd32",
|
|
1279
|
+
linen: "#faf0e6",
|
|
1280
|
+
magenta: "#ff00ff",
|
|
1281
|
+
maroon: "#800000",
|
|
1282
|
+
mediumaquamarine: "#66cdaa",
|
|
1283
|
+
mediumblue: "#0000cd",
|
|
1284
|
+
mediumorchid: "#ba55d3",
|
|
1285
|
+
mediumpurple: "#9370db",
|
|
1286
|
+
mediumseagreen: "#3cb371",
|
|
1287
|
+
mediumslateblue: "#7b68ee",
|
|
1288
|
+
mediumspringgreen: "#00fa9a",
|
|
1289
|
+
mediumturquoise: "#48d1cc",
|
|
1290
|
+
mediumvioletred: "#c71585",
|
|
1291
|
+
midnightblue: "#191970",
|
|
1292
|
+
mintcream: "#f5fffa",
|
|
1293
|
+
mistyrose: "#ffe4e1",
|
|
1294
|
+
moccasin: "#ffe4b5",
|
|
1295
|
+
navajowhite: "#ffdead",
|
|
1296
|
+
navy: "#000080",
|
|
1297
|
+
oldlace: "#fdf5e6",
|
|
1298
|
+
olive: "#808000",
|
|
1299
|
+
olivedrab: "#6b8e23",
|
|
1300
|
+
orange: "#ffa500",
|
|
1301
|
+
orangered: "#ff4500",
|
|
1302
|
+
orchid: "#da70d6",
|
|
1303
|
+
palegoldenrod: "#eee8aa",
|
|
1304
|
+
palegreen: "#98fb98",
|
|
1305
|
+
paleturquoise: "#afeeee",
|
|
1306
|
+
palevioletred: "#db7093",
|
|
1307
|
+
papayawhip: "#ffefd5",
|
|
1308
|
+
peachpuff: "#ffdab9",
|
|
1309
|
+
peru: "#cd853f",
|
|
1310
|
+
pink: "#ffc0cb",
|
|
1311
|
+
plum: "#dda0dd",
|
|
1312
|
+
powderblue: "#b0e0e6",
|
|
1313
|
+
purple: "#800080",
|
|
1314
|
+
rebeccapurple: "#663399",
|
|
1315
|
+
red: "#ff0000",
|
|
1316
|
+
rosybrown: "#bc8f8f",
|
|
1317
|
+
royalblue: "#4169e1",
|
|
1318
|
+
saddlebrown: "#8b4513",
|
|
1319
|
+
salmon: "#fa8072",
|
|
1320
|
+
sandybrown: "#f4a460",
|
|
1321
|
+
seagreen: "#2e8b57",
|
|
1322
|
+
seashell: "#fff5ee",
|
|
1323
|
+
sienna: "#a0522d",
|
|
1324
|
+
silver: "#c0c0c0",
|
|
1325
|
+
skyblue: "#87ceeb",
|
|
1326
|
+
slateblue: "#6a5acd",
|
|
1327
|
+
slategray: "#708090",
|
|
1328
|
+
slategrey: "#708090",
|
|
1329
|
+
snow: "#fffafa",
|
|
1330
|
+
springgreen: "#00ff7f",
|
|
1331
|
+
steelblue: "#4682b4",
|
|
1332
|
+
tan: "#d2b48c",
|
|
1333
|
+
teal: "#008080",
|
|
1334
|
+
thistle: "#d8bfd8",
|
|
1335
|
+
tomato: "#ff6347",
|
|
1336
|
+
turquoise: "#40e0d0",
|
|
1337
|
+
violet: "#ee82ee",
|
|
1338
|
+
wheat: "#f5deb3",
|
|
1339
|
+
white: "#ffffff",
|
|
1340
|
+
whitesmoke: "#f5f5f5",
|
|
1341
|
+
yellow: "#ffff00",
|
|
1342
|
+
yellowgreen: "#9acd32"
|
|
1343
|
+
};
|
|
1344
|
+
function parseColor(colorStr) {
|
|
1345
|
+
const trimmed = colorStr.trim().toLowerCase();
|
|
1346
|
+
if (trimmed.startsWith("color-mix(")) {
|
|
1347
|
+
return parseColorMix(trimmed);
|
|
1348
|
+
}
|
|
1349
|
+
if (NAMED_COLORS[trimmed]) {
|
|
1350
|
+
return parseHex(NAMED_COLORS[trimmed]);
|
|
1351
|
+
}
|
|
1352
|
+
if (trimmed.startsWith("#")) {
|
|
1353
|
+
return parseHex(trimmed);
|
|
1354
|
+
}
|
|
1355
|
+
if (trimmed.startsWith("rgb")) {
|
|
1356
|
+
return parseRgb(trimmed);
|
|
1357
|
+
}
|
|
1358
|
+
if (trimmed.startsWith("hsl")) return parseHsl(trimmed);
|
|
1359
|
+
if (trimmed.startsWith("hwb")) return parseHwb(trimmed);
|
|
1360
|
+
if (trimmed.startsWith("oklch")) return parseOklch(trimmed);
|
|
1361
|
+
if (trimmed.startsWith("oklab")) return parseOklab(trimmed);
|
|
1362
|
+
if (trimmed.startsWith("lch")) return parseLch(trimmed);
|
|
1363
|
+
if (trimmed.startsWith("lab")) return parseLab(trimmed);
|
|
1364
|
+
if (trimmed.startsWith("color(")) return parseGenericColor(trimmed);
|
|
1365
|
+
return { l: 0, a: 0, b: 0, alpha: 0, raw: colorStr };
|
|
1366
|
+
}
|
|
1367
|
+
function parseNumericValue(val, scale = 1) {
|
|
1368
|
+
if (!val || val === "none") return 0;
|
|
1369
|
+
const trimmed = val.trim().toLowerCase();
|
|
1370
|
+
if (trimmed.endsWith("%")) return parseFloat(trimmed) / 100 * scale;
|
|
1371
|
+
if (trimmed.endsWith("deg")) return parseFloat(trimmed);
|
|
1372
|
+
if (trimmed.endsWith("rad")) return parseFloat(trimmed) * 180 / Math.PI;
|
|
1373
|
+
if (trimmed.endsWith("grad")) return parseFloat(trimmed) * 0.9;
|
|
1374
|
+
if (trimmed.endsWith("turn")) return parseFloat(trimmed) * 360;
|
|
1375
|
+
return parseFloat(trimmed);
|
|
1376
|
+
}
|
|
1377
|
+
function parseHex(hex) {
|
|
1378
|
+
let r = 0, g = 0, b = 0, a = 1;
|
|
1379
|
+
if (hex.length === 4) {
|
|
1380
|
+
r = parseInt(hex[1] + hex[1], 16);
|
|
1381
|
+
g = parseInt(hex[2] + hex[2], 16);
|
|
1382
|
+
b = parseInt(hex[3] + hex[3], 16);
|
|
1383
|
+
} else if (hex.length === 7) {
|
|
1384
|
+
r = parseInt(hex.slice(1, 3), 16);
|
|
1385
|
+
g = parseInt(hex.slice(3, 5), 16);
|
|
1386
|
+
b = parseInt(hex.slice(5, 7), 16);
|
|
1387
|
+
} else if (hex.length === 9) {
|
|
1388
|
+
r = parseInt(hex.slice(1, 3), 16);
|
|
1389
|
+
g = parseInt(hex.slice(3, 5), 16);
|
|
1390
|
+
b = parseInt(hex.slice(5, 7), 16);
|
|
1391
|
+
a = parseInt(hex.slice(7, 9), 16) / 255;
|
|
1392
|
+
}
|
|
1393
|
+
return rgbToNormalized(r, g, b, a, hex);
|
|
1394
|
+
}
|
|
1395
|
+
function parseRgb(rgb) {
|
|
1396
|
+
const parts = rgb.match(/rgba?\(([^)]+)\)/i)?.[1]?.split(/[\s,#/]+/).filter(Boolean) || [];
|
|
1397
|
+
if (parts.length < 3) return { l: 0, a: 0, b: 0, alpha: 1, raw: rgb };
|
|
1398
|
+
const r = parseNumericValue(parts[0], 255);
|
|
1399
|
+
const g = parseNumericValue(parts[1], 255);
|
|
1400
|
+
const b = parseNumericValue(parts[2], 255);
|
|
1401
|
+
const a = parts[3] ? parseNumericValue(parts[3], 1) : 1;
|
|
1402
|
+
return rgbToNormalized(r, g, b, a, rgb);
|
|
1403
|
+
}
|
|
1404
|
+
function parseHsl(hsl) {
|
|
1405
|
+
const parts = hsl.match(/hsla?\(([^)]+)\)/i)?.[1]?.split(/[\s,#/]+/).filter(Boolean) || [];
|
|
1406
|
+
if (parts.length < 3) return { l: 0, a: 0, b: 0, alpha: 1, raw: hsl };
|
|
1407
|
+
const h = parseNumericValue(parts[0], 360) / 360;
|
|
1408
|
+
const s = parseNumericValue(parts[1], 1);
|
|
1409
|
+
const l = parseNumericValue(parts[2], 1);
|
|
1410
|
+
const a = parts[3] ? parseNumericValue(parts[3], 1) : 1;
|
|
1411
|
+
let r, g, b;
|
|
1412
|
+
if (s === 0) {
|
|
1413
|
+
r = g = b = l;
|
|
1414
|
+
} else {
|
|
1415
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
1416
|
+
const p = 2 * l - q;
|
|
1417
|
+
r = hueToRgb(p, q, h + 1 / 3);
|
|
1418
|
+
g = hueToRgb(p, q, h);
|
|
1419
|
+
b = hueToRgb(p, q, h - 1 / 3);
|
|
1420
|
+
}
|
|
1421
|
+
return rgbToNormalized(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), a, hsl);
|
|
1422
|
+
}
|
|
1423
|
+
function hueToRgb(pVal, qVal, normalizedHue) {
|
|
1424
|
+
if (normalizedHue < 0) normalizedHue += 1;
|
|
1425
|
+
if (normalizedHue > 1) normalizedHue -= 1;
|
|
1426
|
+
if (normalizedHue < 1 / 6) return pVal + (qVal - pVal) * 6 * normalizedHue;
|
|
1427
|
+
if (normalizedHue < 1 / 2) return qVal;
|
|
1428
|
+
if (normalizedHue < 2 / 3) return pVal + (qVal - pVal) * (2 / 3 - normalizedHue) * 6;
|
|
1429
|
+
return pVal;
|
|
1430
|
+
}
|
|
1431
|
+
function parseHwb(hwb) {
|
|
1432
|
+
const parts = hwb.match(/hwb\(([^)]+)\)/i)?.[1]?.split(/[\s,#/]+/).filter(Boolean) || [];
|
|
1433
|
+
if (parts.length < 3) return { l: 0, a: 0, b: 0, alpha: 1, raw: hwb };
|
|
1434
|
+
const h = parseNumericValue(parts[0], 360) / 360;
|
|
1435
|
+
let w = parseNumericValue(parts[1], 1);
|
|
1436
|
+
let b = parseNumericValue(parts[2], 1);
|
|
1437
|
+
const a = parts[3] ? parseNumericValue(parts[3], 1) : 1;
|
|
1438
|
+
if (w + b > 1) {
|
|
1439
|
+
const sum = w + b;
|
|
1440
|
+
w /= sum;
|
|
1441
|
+
b /= sum;
|
|
1442
|
+
}
|
|
1443
|
+
const r_base = hueToRgb(0, 1, h + 1 / 3);
|
|
1444
|
+
const g_base = hueToRgb(0, 1, h);
|
|
1445
|
+
const b_base = hueToRgb(0, 1, h - 1 / 3);
|
|
1446
|
+
const factor = 1 - w - b;
|
|
1447
|
+
const r = r_base * factor + w;
|
|
1448
|
+
const g = g_base * factor + w;
|
|
1449
|
+
const b_final = b_base * factor + w;
|
|
1450
|
+
return rgbToNormalized(r * 255, g * 255, b_final * 255, a, hwb);
|
|
1451
|
+
}
|
|
1452
|
+
function parseLab(lab) {
|
|
1453
|
+
const parts = lab.match(/lab\(([^)]+)\)/i)?.[1]?.split(/[\s,#/]+/).filter(Boolean) || [];
|
|
1454
|
+
if (parts.length < 3) return { l: 0, a: 0, b: 0, alpha: 1, raw: lab };
|
|
1455
|
+
const L = parseNumericValue(parts[0], 100);
|
|
1456
|
+
const A = parseNumericValue(parts[1], 125);
|
|
1457
|
+
const B = parseNumericValue(parts[2], 125);
|
|
1458
|
+
const alpha = parts[3] ? parseNumericValue(parts[3], 1) : 1;
|
|
1459
|
+
const y = (L + 16) / 116;
|
|
1460
|
+
const x = A / 500 + y;
|
|
1461
|
+
const z4 = y - B / 200;
|
|
1462
|
+
const [X50, Y50, Z50] = [f_inv(x) * 0.96422, f_inv(y) * 1, f_inv(z4) * 0.82521];
|
|
1463
|
+
const X65 = X50 * 0.9555766 + Y50 * -0.0230393 + Z50 * 0.0631636;
|
|
1464
|
+
const Y65 = X50 * -0.0282895 + Y50 * 1.0099416 + Z50 * 0.0210077;
|
|
1465
|
+
const Z65 = X50 * 0.0122982 + Y50 * -0.020483 + Z50 * 1.3299098;
|
|
1466
|
+
return xyzToOklab(X65, Y65, Z65, alpha, lab);
|
|
1467
|
+
}
|
|
1468
|
+
function f_inv(t) {
|
|
1469
|
+
return t > 6 / 29 ? t ** 3 : 3 * (6 / 29) ** 2 * (t - 4 / 29);
|
|
1470
|
+
}
|
|
1471
|
+
function parseLch(lch) {
|
|
1472
|
+
const parts = lch.match(/lch\(([^)]+)\)/i)?.[1]?.split(/[\s,#/]+/).filter(Boolean) || [];
|
|
1473
|
+
if (parts.length < 3) return { l: 0, a: 0, b: 0, alpha: 1, raw: lch };
|
|
1474
|
+
const L = parseNumericValue(parts[0], 100);
|
|
1475
|
+
const C = parseNumericValue(parts[1], 150);
|
|
1476
|
+
const H = parseNumericValue(parts[2], 1);
|
|
1477
|
+
const alpha = parts[3] ? parseNumericValue(parts[3], 1) : 1;
|
|
1478
|
+
const hRad = H * Math.PI / 180;
|
|
1479
|
+
const a = C * Math.cos(hRad);
|
|
1480
|
+
const b = C * Math.sin(hRad);
|
|
1481
|
+
return parseLab(`lab(${L} ${a} ${b} / ${alpha})`);
|
|
1482
|
+
}
|
|
1483
|
+
function parseOklab(oklab) {
|
|
1484
|
+
const parts = oklab.match(/oklab\(([^)]+)\)/i)?.[1]?.split(/[\s,#/]+/).filter(Boolean) || [];
|
|
1485
|
+
if (parts.length < 3) return { l: 0, a: 0, b: 0, alpha: 1, raw: oklab };
|
|
1486
|
+
return {
|
|
1487
|
+
l: parseNumericValue(parts[0], 1),
|
|
1488
|
+
a: parseNumericValue(parts[1], 0.4),
|
|
1489
|
+
b: parseNumericValue(parts[2], 0.4),
|
|
1490
|
+
alpha: parts[3] ? parseNumericValue(parts[3], 1) : 1,
|
|
1491
|
+
raw: oklab
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
function parseOklch(oklch) {
|
|
1495
|
+
const parts = oklch.match(/oklch\(([^)]+)\)/i)?.[1]?.split(/[\s,#/]+/).filter(Boolean) || [];
|
|
1496
|
+
if (parts.length < 3) return { l: 0, a: 0, b: 0, alpha: 1, raw: oklch };
|
|
1497
|
+
const L = parseNumericValue(parts[0], 1);
|
|
1498
|
+
const C = parseNumericValue(parts[1], 1);
|
|
1499
|
+
const H = parseNumericValue(parts[2], 1);
|
|
1500
|
+
const A = parts[3] ? parseNumericValue(parts[3], 1) : 1;
|
|
1501
|
+
const hRad = H * Math.PI / 180;
|
|
1502
|
+
const a = C * Math.cos(hRad);
|
|
1503
|
+
const b = C * Math.sin(hRad);
|
|
1504
|
+
return { l: L, a, b, alpha: A, raw: oklch };
|
|
1505
|
+
}
|
|
1506
|
+
function parseGenericColor(str) {
|
|
1507
|
+
const match = str.match(/color\(([^ ]+) ([^)]+)\)/i);
|
|
1508
|
+
if (!match) return { l: 0, a: 0, b: 0, alpha: 1, raw: str };
|
|
1509
|
+
const space = match[1].toLowerCase();
|
|
1510
|
+
const parts = match[2].split(/[\s/]+/).filter(Boolean);
|
|
1511
|
+
const c1 = parseNumericValue(parts[0], 1);
|
|
1512
|
+
const c2 = parseNumericValue(parts[1], 1);
|
|
1513
|
+
const c3 = parseNumericValue(parts[2], 1);
|
|
1514
|
+
const a = parts[3] ? parseNumericValue(parts[3], 1) : 1;
|
|
1515
|
+
if (space === "display-p3") {
|
|
1516
|
+
return p3ToNormalized(c1, c2, c3, a, str);
|
|
1517
|
+
}
|
|
1518
|
+
if (space === "srgb") {
|
|
1519
|
+
return rgbToNormalized(c1 * 255, c2 * 255, c3 * 255, a, str);
|
|
1520
|
+
}
|
|
1521
|
+
return rgbToNormalized(c1 * 255, c2 * 255, c3 * 255, a, str);
|
|
1522
|
+
}
|
|
1523
|
+
function parseColorMix(str) {
|
|
1524
|
+
const match = str.match(/color-mix\(in ([^,]+),\s*([^,]+),\s*([^)]+)\)/i);
|
|
1525
|
+
if (!match) return { l: 0, a: 0, b: 0, alpha: 0, raw: str };
|
|
1526
|
+
match[1].trim().toLowerCase();
|
|
1527
|
+
const c1Str = match[2].trim();
|
|
1528
|
+
const c2Str = match[3].trim();
|
|
1529
|
+
const p1Match = c1Str.match(/(.+)\s+(\d+(\.\d+)?%)/);
|
|
1530
|
+
const p2Match = c2Str.match(/(.+)\s+(\d+(\.\d+)?%)/);
|
|
1531
|
+
const c1 = parseColor(p1Match ? p1Match[1] : c1Str);
|
|
1532
|
+
const c2 = parseColor(p2Match ? p2Match[1] : c2Str);
|
|
1533
|
+
let weight = 0.5;
|
|
1534
|
+
if (p1Match) weight = parseFloat(p1Match[2]) / 100;
|
|
1535
|
+
else if (p2Match) weight = 1 - parseFloat(p2Match[2]) / 100;
|
|
1536
|
+
return {
|
|
1537
|
+
l: c1.l * weight + c2.l * (1 - weight),
|
|
1538
|
+
a: c1.a * weight + c2.a * (1 - weight),
|
|
1539
|
+
b: c1.b * weight + c2.b * (1 - weight),
|
|
1540
|
+
alpha: c1.alpha * weight + c2.alpha * (1 - weight),
|
|
1541
|
+
raw: str
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
function xyzToOklab(x, y, z4, alpha, raw) {
|
|
1545
|
+
if (alpha === 0) return { l: 0, a: 0, b: 0, alpha: 0, raw };
|
|
1546
|
+
const lValue = 0.8189330101 * x + 0.3618667424 * y - 0.1288597137 * z4;
|
|
1547
|
+
const mValue = 0.0329845436 * x + 0.9293118715 * y + 0.0361456387 * z4;
|
|
1548
|
+
const sValue = 0.0482003018 * x + 0.2643662691 * y + 0.633851707 * z4;
|
|
1549
|
+
const lCubeRoot = Math.cbrt(lValue);
|
|
1550
|
+
const mCubeRoot = Math.cbrt(mValue);
|
|
1551
|
+
const sCubeRoot = Math.cbrt(sValue);
|
|
1552
|
+
return {
|
|
1553
|
+
l: 0.2104542553 * lCubeRoot + 0.793617785 * mCubeRoot - 0.0040720403 * sCubeRoot,
|
|
1554
|
+
a: 1.9779984951 * lCubeRoot - 2.428592205 * mCubeRoot + 0.4505937099 * sCubeRoot,
|
|
1555
|
+
b: 0.0259040371 * lCubeRoot + 0.7827717662 * mCubeRoot - 0.808675766 * sCubeRoot,
|
|
1556
|
+
alpha,
|
|
1557
|
+
raw
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
function rgbToNormalized(red, green, blue, alpha, raw) {
|
|
1561
|
+
const linearRed = pivotRgb(red / 255);
|
|
1562
|
+
const linearGreen = pivotRgb(green / 255);
|
|
1563
|
+
const linearBlue = pivotRgb(blue / 255);
|
|
1564
|
+
const x = 0.4124564 * linearRed + 0.3575761 * linearGreen + 0.1804375 * linearBlue;
|
|
1565
|
+
const y = 0.2126729 * linearRed + 0.7151522 * linearGreen + 0.072175 * linearBlue;
|
|
1566
|
+
const z4 = 0.0193339 * linearRed + 0.119192 * linearGreen + 0.9503041 * linearBlue;
|
|
1567
|
+
return xyzToOklab(x, y, z4, alpha, raw);
|
|
1568
|
+
}
|
|
1569
|
+
function p3ToNormalized(red, green, blue, alpha, raw) {
|
|
1570
|
+
const lp3 = (val) => val > 0.04045 ? Math.pow((val + 0.055) / 1.055, 2.4) : val / 12.92;
|
|
1571
|
+
const linearRed = lp3(red);
|
|
1572
|
+
const linearGreen = lp3(green);
|
|
1573
|
+
const linearBlue = lp3(blue);
|
|
1574
|
+
const x = 0.4865709 * linearRed + 0.2656677 * linearGreen + 0.1982119 * linearBlue;
|
|
1575
|
+
const y = 0.2289748 * linearRed + 0.6917385 * linearGreen + 0.0592867 * linearBlue;
|
|
1576
|
+
const z4 = 0 * linearRed + 0.0451104 * linearGreen + 1.0439444 * linearBlue;
|
|
1577
|
+
return xyzToOklab(x, y, z4, alpha, raw);
|
|
1578
|
+
}
|
|
1579
|
+
function pivotRgb(colorValue) {
|
|
1580
|
+
return colorValue > 0.04045 ? Math.pow((colorValue + 0.055) / 1.055, 2.4) : colorValue / 12.92;
|
|
1581
|
+
}
|
|
1582
|
+
function calculateDeltaE(color1, color2) {
|
|
1583
|
+
const deltaL = color1.l - color2.l;
|
|
1584
|
+
const deltaA = color1.a - color2.a;
|
|
1585
|
+
const deltaB = color1.b - color2.b;
|
|
1586
|
+
const deltaAlpha = color1.alpha - color2.alpha;
|
|
1587
|
+
return Math.sqrt(
|
|
1588
|
+
deltaL * deltaL + deltaA * deltaA + deltaB * deltaB + deltaAlpha * deltaAlpha * 0.5
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
function unpivotRgb(c) {
|
|
1592
|
+
return c > 31308e-7 ? 1.055 * Math.pow(c, 1 / 2.4) - 0.055 : 12.92 * c;
|
|
1593
|
+
}
|
|
1594
|
+
function oklabToSrgbComponents(color) {
|
|
1595
|
+
const lmsPrimeL = color.l + 0.3963377774 * color.a + 0.2158037573 * color.b;
|
|
1596
|
+
const lmsPrimeM = color.l - 0.1055613458 * color.a - 0.0638541728 * color.b;
|
|
1597
|
+
const lmsPrimeS = color.l - 0.0894841775 * color.a - 1.291485548 * color.b;
|
|
1598
|
+
const lmsL = lmsPrimeL * lmsPrimeL * lmsPrimeL;
|
|
1599
|
+
const lmsM = lmsPrimeM * lmsPrimeM * lmsPrimeM;
|
|
1600
|
+
const lmsS = lmsPrimeS * lmsPrimeS * lmsPrimeS;
|
|
1601
|
+
const linearR = 4.0767416621 * lmsL - 3.3077115913 * lmsM + 0.2309699292 * lmsS;
|
|
1602
|
+
const linearG = -1.2684380046 * lmsL + 2.6097574011 * lmsM - 0.3413193965 * lmsS;
|
|
1603
|
+
const linearB = -0.0041960863 * lmsL - 0.7034186147 * lmsM + 1.707612701 * lmsS;
|
|
1604
|
+
return {
|
|
1605
|
+
red: Math.min(1, Math.max(0, unpivotRgb(linearR))),
|
|
1606
|
+
green: Math.min(1, Math.max(0, unpivotRgb(linearG))),
|
|
1607
|
+
blue: Math.min(1, Math.max(0, unpivotRgb(linearB)))
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
function getLuminance(color) {
|
|
1611
|
+
const { red, green, blue } = oklabToSrgbComponents(color);
|
|
1612
|
+
const rLin = red <= 0.04045 ? red / 12.92 : Math.pow((red + 0.055) / 1.055, 2.4);
|
|
1613
|
+
const gLin = green <= 0.04045 ? green / 12.92 : Math.pow((green + 0.055) / 1.055, 2.4);
|
|
1614
|
+
const bLin = blue <= 0.04045 ? blue / 12.92 : Math.pow((blue + 0.055) / 1.055, 2.4);
|
|
1615
|
+
return 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin;
|
|
1616
|
+
}
|
|
1617
|
+
function calculateContrastRatio(fg, bg) {
|
|
1618
|
+
const l1 = getLuminance(fg);
|
|
1619
|
+
const l2 = getLuminance(bg);
|
|
1620
|
+
const lighter = Math.max(l1, l2);
|
|
1621
|
+
const darker = Math.min(l1, l2);
|
|
1622
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// ../mcp-server/src/services/mcp-server.ts
|
|
1626
|
+
var logger4 = createLogger("mcp-server");
|
|
1627
|
+
var CaliperMcpServer = class {
|
|
1628
|
+
server;
|
|
1629
|
+
port;
|
|
1630
|
+
lastState = null;
|
|
1631
|
+
constructor(port2 = DEFAULT_BRIDGE_PORT) {
|
|
1632
|
+
this.port = port2;
|
|
1633
|
+
this.server = new McpServer({
|
|
1634
|
+
name: "caliper-mcp-server",
|
|
1635
|
+
version: "0.2.0"
|
|
1636
|
+
});
|
|
1637
|
+
this.registerTools();
|
|
1638
|
+
this.registerResources();
|
|
1639
|
+
this.registerPrompts();
|
|
1640
|
+
}
|
|
1641
|
+
registerTools() {
|
|
1642
|
+
this.server.registerTool(
|
|
1643
|
+
"caliper_list_tabs",
|
|
1644
|
+
{
|
|
1645
|
+
description: "List all browser tabs currently connected to the Caliper Agent Bridge.",
|
|
1646
|
+
inputSchema: z.object({})
|
|
1647
|
+
},
|
|
1648
|
+
async () => {
|
|
1649
|
+
const tabs = tabManager.getAllTabs().map((tab) => ({
|
|
1650
|
+
id: tab.id,
|
|
1651
|
+
title: tab.title,
|
|
1652
|
+
url: tab.url,
|
|
1653
|
+
isActive: tab.id === tabManager.getActiveTab()?.id
|
|
1654
|
+
}));
|
|
1655
|
+
return { content: [{ type: "text", text: JSON.stringify(tabs, null, 2) }] };
|
|
1656
|
+
}
|
|
1657
|
+
);
|
|
1658
|
+
this.server.registerTool(
|
|
1659
|
+
"caliper_switch_tab",
|
|
1660
|
+
{
|
|
1661
|
+
description: "Switch the active targeting to a specific browser tab.",
|
|
1662
|
+
inputSchema: z.object({
|
|
1663
|
+
tabId: z.string().describe("The ID of the tab to target")
|
|
1664
|
+
})
|
|
1665
|
+
},
|
|
1666
|
+
async ({ tabId }) => {
|
|
1667
|
+
if (tabManager.switchTab(tabId)) {
|
|
1668
|
+
return {
|
|
1669
|
+
content: [
|
|
1670
|
+
{ type: "text", text: `Switched to tab: ${tabManager.getActiveTab()?.title}` }
|
|
1671
|
+
]
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
return {
|
|
1675
|
+
content: [{ type: "text", text: `Error: Tab ${tabId} not found` }],
|
|
1676
|
+
isError: true
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
);
|
|
1680
|
+
this.server.registerTool(
|
|
1681
|
+
"caliper_inspect",
|
|
1682
|
+
{
|
|
1683
|
+
description: `Get full geometry and computed visibility for an element.
|
|
1684
|
+
|
|
1685
|
+
Returns comprehensive element data including:
|
|
1686
|
+
- distances: Position relative to viewport edges
|
|
1687
|
+
- computedStyles: All CSS computed values
|
|
1688
|
+
- selection: Scroll hierarchy and sticky configuration
|
|
1689
|
+
- immediateChildCount: Direct children count
|
|
1690
|
+
- descendantCount: Total descendants (capped at ${MAX_DESCENDANT_COUNT})
|
|
1691
|
+
- descendantsTruncated: True if descendant count was capped
|
|
1692
|
+
- sourceHints: Metadata for code discovery. Includes:
|
|
1693
|
+
* suggestedGrep: A reliable pattern for finding this element in the codebase.
|
|
1694
|
+
* stableAnchors: Prioritized list of stable attributes (data-testid, id, etc.).
|
|
1695
|
+
* textContent: Truncated direct text content.
|
|
1696
|
+
* unstableClasses: Classes that look like generated/hashed strings (to be ignored).
|
|
1697
|
+
|
|
1698
|
+
Use descendantCount to decide whether to paginate caliper_walk_and_measure.
|
|
1699
|
+
If descendantCount > ${RECOMMENDED_PAGINATION_THRESHOLD} or descendantsTruncated is true, use maxNodes parameter.`,
|
|
1700
|
+
inputSchema: z.object({
|
|
1701
|
+
selector: z.string().describe(
|
|
1702
|
+
"Element identifier. PRIORITIZE JSON Fingerprint, Caliper Agent ID (caliper-***), or CSS selector for maximum stabilization."
|
|
1703
|
+
)
|
|
1704
|
+
})
|
|
1705
|
+
},
|
|
1706
|
+
async ({ selector }) => {
|
|
1707
|
+
try {
|
|
1708
|
+
const result = await bridgeService.call(CALIPER_METHODS.INSPECT, { selector });
|
|
1709
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1710
|
+
} catch (error) {
|
|
1711
|
+
return {
|
|
1712
|
+
content: [
|
|
1713
|
+
{
|
|
1714
|
+
type: "text",
|
|
1715
|
+
text: `Error inspecting element: ${error instanceof Error ? error.message : String(error)}`
|
|
1716
|
+
}
|
|
1717
|
+
],
|
|
1718
|
+
isError: true
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
);
|
|
1723
|
+
this.server.registerTool(
|
|
1724
|
+
"caliper_measure",
|
|
1725
|
+
{
|
|
1726
|
+
description: "Perform a high-precision measurement between two elements.",
|
|
1727
|
+
inputSchema: z.object({
|
|
1728
|
+
primarySelector: z.string().describe(
|
|
1729
|
+
"Identifier for the primary element. PRIORITIZE JSON Fingerprint, Caliper Agent ID (caliper-***), or CSS selector."
|
|
1730
|
+
),
|
|
1731
|
+
secondarySelector: z.string().describe(
|
|
1732
|
+
"Identifier for the target element. PRIORITIZE JSON Fingerprint, Caliper Agent ID (caliper-***), or CSS selector."
|
|
1733
|
+
)
|
|
1734
|
+
})
|
|
1735
|
+
},
|
|
1736
|
+
async ({ primarySelector, secondarySelector }) => {
|
|
1737
|
+
try {
|
|
1738
|
+
const result = await bridgeService.call(CALIPER_METHODS.MEASURE, {
|
|
1739
|
+
primarySelector,
|
|
1740
|
+
secondarySelector
|
|
1741
|
+
});
|
|
1742
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
return {
|
|
1745
|
+
content: [
|
|
1746
|
+
{
|
|
1747
|
+
type: "text",
|
|
1748
|
+
text: `Error performing measurement: ${error instanceof Error ? error.message : String(error)}`
|
|
1749
|
+
}
|
|
1750
|
+
],
|
|
1751
|
+
isError: true
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
);
|
|
1756
|
+
this.server.registerTool(
|
|
1757
|
+
"caliper_clear",
|
|
1758
|
+
{
|
|
1759
|
+
description: "Clear all measurements and selections in the browser UI.",
|
|
1760
|
+
inputSchema: z.object({})
|
|
1761
|
+
},
|
|
1762
|
+
async () => {
|
|
1763
|
+
try {
|
|
1764
|
+
const result = await bridgeService.call(CALIPER_METHODS.CLEAR, {});
|
|
1765
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
return {
|
|
1768
|
+
content: [
|
|
1769
|
+
{
|
|
1770
|
+
type: "text",
|
|
1771
|
+
text: `Error clearing UI: ${error instanceof Error ? error.message : String(error)}`
|
|
1772
|
+
}
|
|
1773
|
+
],
|
|
1774
|
+
isError: true
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
);
|
|
1779
|
+
this.server.registerTool(
|
|
1780
|
+
"caliper_walk_dom",
|
|
1781
|
+
{
|
|
1782
|
+
description: "Get the semantic context of an element by traversing its parents and children. Useful for understanding component hierarchy.",
|
|
1783
|
+
inputSchema: z.object({
|
|
1784
|
+
selector: z.string().describe(
|
|
1785
|
+
"Element identifier. PRIORITIZE JSON Fingerprint, Caliper Agent ID (caliper-***), or CSS selector for maximum stabilization."
|
|
1786
|
+
)
|
|
1787
|
+
})
|
|
1788
|
+
},
|
|
1789
|
+
async ({ selector }) => {
|
|
1790
|
+
try {
|
|
1791
|
+
const result = await bridgeService.call(CALIPER_METHODS.WALK_DOM, { selector });
|
|
1792
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
return {
|
|
1795
|
+
content: [
|
|
1796
|
+
{
|
|
1797
|
+
type: "text",
|
|
1798
|
+
text: `DOM Walk failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1799
|
+
}
|
|
1800
|
+
],
|
|
1801
|
+
isError: true
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
);
|
|
1806
|
+
this.server.registerTool(
|
|
1807
|
+
"caliper_walk_and_measure",
|
|
1808
|
+
{
|
|
1809
|
+
description: `Walk the DOM tree starting from a selector and capture precise measurements.
|
|
1810
|
+
|
|
1811
|
+
This is the HARNESS tool for comprehensive audits. It:
|
|
1812
|
+
- Walks the DOM tree using BFS (breadth-first)
|
|
1813
|
+
- Captures computed styles for each node (padding, margin, gap, typography, colors)
|
|
1814
|
+
- Measures gaps between siblings and distance to parent edges
|
|
1815
|
+
- Returns a full tree structure ready for reconciliation with Figma
|
|
1816
|
+
|
|
1817
|
+
Use this BEFORE making any code changes to gather complete context.
|
|
1818
|
+
|
|
1819
|
+
SAFETY: Calling 'caliper_inspect' is REQUIRED before initiating an initial audit on a component. This provides the 'descendantCount' metrics needed to set 'maxNodes' and avoid timeouts. You may skip 'inspect' only when resuming an audit using a 'continuationToken'.
|
|
1820
|
+
|
|
1821
|
+
STYLE PRUNING: To reduce payload size, default styles are omitted. If a style is missing, assume:
|
|
1822
|
+
- display: "block"
|
|
1823
|
+
- visibility: "visible"
|
|
1824
|
+
- position: "static"
|
|
1825
|
+
- boxSizing: "border-box"
|
|
1826
|
+
- padding/margin/border: { top: 0, right: 0, bottom: 0, left: 0 }
|
|
1827
|
+
- gap: 0
|
|
1828
|
+
- flexDirection: "row" (flex only)
|
|
1829
|
+
- justifyContent: "normal" (flex only)
|
|
1830
|
+
- alignItems: "normal" (flex only)
|
|
1831
|
+
- lineHeight: "normal"
|
|
1832
|
+
- letterSpacing: "normal"
|
|
1833
|
+
- backgroundColor: "rgba(0, 0, 0, 0)" (transparent)
|
|
1834
|
+
- borderRadius: "0px"
|
|
1835
|
+
- boxShadow: "none"
|
|
1836
|
+
- opacity: 1
|
|
1837
|
+
- overflow/overflowX/overflowY: "visible"
|
|
1838
|
+
|
|
1839
|
+
The output includes:
|
|
1840
|
+
- root: The full measured tree (recursive CaliperNode structure)
|
|
1841
|
+
- nodeCount: Total nodes captured
|
|
1842
|
+
- maxDepthReached: Deepest level reached
|
|
1843
|
+
- walkDurationMs: Time taken to complete the walk
|
|
1844
|
+
- hasMore: Whether tree was truncated (pagination)
|
|
1845
|
+
- continuationToken: Selector to resume from (if hasMore is true)
|
|
1846
|
+
- batchInstructions: Guidance for next batch call (if hasMore is true)`,
|
|
1847
|
+
inputSchema: z.object({
|
|
1848
|
+
selector: z.string().describe(
|
|
1849
|
+
"Root element identifier. PRIORITIZE JSON Fingerprint, Caliper Agent ID (caliper-***), or CSS selector for maximum stabilization."
|
|
1850
|
+
),
|
|
1851
|
+
maxDepth: z.number().optional().describe("Maximum depth to walk (default: 5)"),
|
|
1852
|
+
maxNodes: z.number().optional().describe(
|
|
1853
|
+
"Maximum nodes to return per batch (default: unlimited). Use for large trees."
|
|
1854
|
+
),
|
|
1855
|
+
continueFrom: z.string().optional().describe("Continuation token from previous call to resume pagination"),
|
|
1856
|
+
minElementSize: z.number().optional().describe(
|
|
1857
|
+
"Minimum element size (width/height) to include in the walk (default: 0). Use to filter out icons or tiny decorative elements."
|
|
1858
|
+
),
|
|
1859
|
+
ignoreSelectors: z.array(z.string()).optional().describe("List of CSS selectors or Agent IDs to skip during the walk.")
|
|
1860
|
+
})
|
|
1861
|
+
},
|
|
1862
|
+
async ({ selector, maxDepth, maxNodes, continueFrom, minElementSize, ignoreSelectors }) => {
|
|
1863
|
+
try {
|
|
1864
|
+
const auditResult = await bridgeService.call(CALIPER_METHODS.WALK_AND_MEASURE, {
|
|
1865
|
+
selector,
|
|
1866
|
+
maxDepth: maxDepth ?? 5,
|
|
1867
|
+
maxNodes,
|
|
1868
|
+
continueFrom,
|
|
1869
|
+
minElementSize,
|
|
1870
|
+
ignoreSelectors
|
|
1871
|
+
});
|
|
1872
|
+
const auditResponse = auditResult;
|
|
1873
|
+
let reportContent = JSON.stringify(auditResult);
|
|
1874
|
+
if (auditResponse.walkResult?.hasMore && auditResponse.walkResult?.batchInstructions) {
|
|
1875
|
+
reportContent = `${auditResponse.walkResult.batchInstructions}
|
|
1876
|
+
|
|
1877
|
+
${reportContent}`;
|
|
1878
|
+
}
|
|
1879
|
+
return { content: [{ type: "text", text: reportContent }] };
|
|
1880
|
+
} catch (error) {
|
|
1881
|
+
return {
|
|
1882
|
+
content: [
|
|
1883
|
+
{
|
|
1884
|
+
type: "text",
|
|
1885
|
+
text: `Walk and Measure failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1886
|
+
}
|
|
1887
|
+
],
|
|
1888
|
+
isError: true
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
);
|
|
1893
|
+
this.server.registerTool(
|
|
1894
|
+
"caliper_get_context",
|
|
1895
|
+
{
|
|
1896
|
+
description: "Get comprehensive window, viewport, and accessibility metrics from the current browser tab.",
|
|
1897
|
+
inputSchema: z.object({})
|
|
1898
|
+
},
|
|
1899
|
+
async () => {
|
|
1900
|
+
try {
|
|
1901
|
+
const result = await bridgeService.call(CALIPER_METHODS.GET_CONTEXT, {});
|
|
1902
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
return {
|
|
1905
|
+
content: [
|
|
1906
|
+
{
|
|
1907
|
+
type: "text",
|
|
1908
|
+
text: `Get Context failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1909
|
+
}
|
|
1910
|
+
],
|
|
1911
|
+
isError: true
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
);
|
|
1916
|
+
this.server.registerTool(
|
|
1917
|
+
"caliper_check_contrast",
|
|
1918
|
+
{
|
|
1919
|
+
description: `Check WCAG 2.1 contrast ratio between two colors.
|
|
1920
|
+
|
|
1921
|
+
Returns the contrast ratio (1-21) and pass/fail status for:
|
|
1922
|
+
- AA Normal Text (4.5:1)
|
|
1923
|
+
- AA Large Text (3:1)
|
|
1924
|
+
- AAA Normal Text (7:1)
|
|
1925
|
+
- AAA Large Text (4.5:1)`,
|
|
1926
|
+
inputSchema: z.object({
|
|
1927
|
+
foreground: z.string().describe("Foreground color (any CSS format: hex, rgb, hsl, oklch, etc.)"),
|
|
1928
|
+
background: z.string().describe("Background color (any CSS format: hex, rgb, hsl, oklch, etc.)")
|
|
1929
|
+
})
|
|
1930
|
+
},
|
|
1931
|
+
async ({ foreground, background }) => {
|
|
1932
|
+
try {
|
|
1933
|
+
const fg = parseColor(foreground);
|
|
1934
|
+
const bg = parseColor(background);
|
|
1935
|
+
const ratio = calculateContrastRatio(fg, bg);
|
|
1936
|
+
return {
|
|
1937
|
+
content: [
|
|
1938
|
+
{
|
|
1939
|
+
type: "text",
|
|
1940
|
+
text: JSON.stringify(
|
|
1941
|
+
{
|
|
1942
|
+
foreground,
|
|
1943
|
+
background,
|
|
1944
|
+
ratio: Number(ratio.toFixed(2)),
|
|
1945
|
+
passAA: ratio >= 4.5,
|
|
1946
|
+
passAALarge: ratio >= 3,
|
|
1947
|
+
passAAA: ratio >= 7,
|
|
1948
|
+
passAAALarge: ratio >= 4.5
|
|
1949
|
+
},
|
|
1950
|
+
null,
|
|
1951
|
+
2
|
|
1952
|
+
)
|
|
1953
|
+
}
|
|
1954
|
+
]
|
|
1955
|
+
};
|
|
1956
|
+
} catch (error) {
|
|
1957
|
+
return {
|
|
1958
|
+
content: [
|
|
1959
|
+
{
|
|
1960
|
+
type: "text",
|
|
1961
|
+
text: `Contrast check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1962
|
+
}
|
|
1963
|
+
],
|
|
1964
|
+
isError: true
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
);
|
|
1969
|
+
this.server.registerTool(
|
|
1970
|
+
"caliper_delta_e",
|
|
1971
|
+
{
|
|
1972
|
+
description: `Calculate perceptual color difference (Delta E) between two colors using Oklab.
|
|
1973
|
+
|
|
1974
|
+
Returns the Delta E value and a human-readable interpretation:
|
|
1975
|
+
- < 0.02: Imperceptible
|
|
1976
|
+
- 0.02-0.05: Just noticeable
|
|
1977
|
+
- 0.05-0.1: Noticeable
|
|
1978
|
+
- 0.1-0.3: Distinct
|
|
1979
|
+
- > 0.3: Very different`,
|
|
1980
|
+
inputSchema: z.object({
|
|
1981
|
+
color1: z.string().describe("First color (any CSS format: hex, rgb, hsl, oklch, etc.)"),
|
|
1982
|
+
color2: z.string().describe("Second color (any CSS format: hex, rgb, hsl, oklch, etc.)")
|
|
1983
|
+
})
|
|
1984
|
+
},
|
|
1985
|
+
async ({ color1, color2 }) => {
|
|
1986
|
+
try {
|
|
1987
|
+
const c1 = parseColor(color1);
|
|
1988
|
+
const c2 = parseColor(color2);
|
|
1989
|
+
const deltaE = calculateDeltaE(c1, c2);
|
|
1990
|
+
let interpretation;
|
|
1991
|
+
if (deltaE < 0.02) interpretation = "Imperceptible (< 0.02)";
|
|
1992
|
+
else if (deltaE < 0.05) interpretation = "Just noticeable (0.02-0.05)";
|
|
1993
|
+
else if (deltaE < 0.1) interpretation = "Noticeable (0.05-0.1)";
|
|
1994
|
+
else if (deltaE < 0.3) interpretation = "Distinct (0.1-0.3)";
|
|
1995
|
+
else interpretation = "Very different (> 0.3)";
|
|
1996
|
+
return {
|
|
1997
|
+
content: [
|
|
1998
|
+
{
|
|
1999
|
+
type: "text",
|
|
2000
|
+
text: JSON.stringify(
|
|
2001
|
+
{
|
|
2002
|
+
color1,
|
|
2003
|
+
color2,
|
|
2004
|
+
deltaE: Number(deltaE.toFixed(4)),
|
|
2005
|
+
interpretation
|
|
2006
|
+
},
|
|
2007
|
+
null,
|
|
2008
|
+
2
|
|
2009
|
+
)
|
|
2010
|
+
}
|
|
2011
|
+
]
|
|
2012
|
+
};
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
return {
|
|
2015
|
+
content: [
|
|
2016
|
+
{
|
|
2017
|
+
type: "text",
|
|
2018
|
+
text: `Delta E calculation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2019
|
+
}
|
|
2020
|
+
],
|
|
2021
|
+
isError: true
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
registerResources() {
|
|
2028
|
+
this.server.registerResource(
|
|
2029
|
+
"caliper-tabs",
|
|
2030
|
+
"caliper://tabs",
|
|
2031
|
+
{ description: "List of all browser tabs currently connected to the bridge." },
|
|
2032
|
+
async () => {
|
|
2033
|
+
const tabs = tabManager.getAllTabs().map((tab) => ({
|
|
2034
|
+
id: tab.id,
|
|
2035
|
+
title: tab.title,
|
|
2036
|
+
url: tab.url,
|
|
2037
|
+
isActive: tab.id === tabManager.getActiveTab()?.id
|
|
2038
|
+
}));
|
|
2039
|
+
return {
|
|
2040
|
+
contents: [
|
|
2041
|
+
{
|
|
2042
|
+
uri: "caliper://tabs",
|
|
2043
|
+
mimeType: "application/json",
|
|
2044
|
+
text: JSON.stringify(tabs, null, 2)
|
|
2045
|
+
}
|
|
2046
|
+
]
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
);
|
|
2050
|
+
this.server.registerResource(
|
|
2051
|
+
"caliper-state",
|
|
2052
|
+
"caliper://state",
|
|
2053
|
+
{
|
|
2054
|
+
description: `Provides a real-time stream of the active browser environment. This is your primary source for "listening" to what the user is doing.
|
|
2055
|
+
|
|
2056
|
+
It contains:
|
|
2057
|
+
- viewport: Current scroll positions and dimensions.
|
|
2058
|
+
- activeSelection: Visual metadata of the currently selected element.
|
|
2059
|
+
- selectionFingerprint: A stable JSON identifier (agentId, tag, text content) for the active selection. Use the 'selector' property from this object as input for 'caliper_inspect' or 'caliper_walk_and_measure' to perform high-precision audits on what the user just picked.
|
|
2060
|
+
- lastMeasurement & measurementFingerprint: Context for the most recent distance measurement between two elements.
|
|
2061
|
+
|
|
2062
|
+
USE CASE: When you receive a notification that this resource has updated, read it to understand the user's current focus. If a 'selectionFingerprint' is present, you can immediately offer to 'Inspect' or 'Audit' that element without asking the user for a selector.`
|
|
2063
|
+
},
|
|
2064
|
+
async () => {
|
|
2065
|
+
return {
|
|
2066
|
+
contents: [
|
|
2067
|
+
{
|
|
2068
|
+
uri: "caliper://state",
|
|
2069
|
+
mimeType: "application/json",
|
|
2070
|
+
text: JSON.stringify(this.lastState, null, 2)
|
|
2071
|
+
}
|
|
2072
|
+
]
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
);
|
|
2076
|
+
bridgeService.on(BRIDGE_EVENTS.STATE, (state) => {
|
|
2077
|
+
this.lastState = state;
|
|
2078
|
+
this.server.server.sendResourceUpdated({ uri: "caliper://state" }).catch((error) => {
|
|
2079
|
+
logger4.warn("Failed to notify resource update", error);
|
|
2080
|
+
});
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
registerPrompts() {
|
|
2084
|
+
this.server.registerPrompt(
|
|
2085
|
+
"caliper-selector-audit",
|
|
2086
|
+
{
|
|
2087
|
+
description: "Perform a comprehensive, structured audit of a specific selector: from source discovery to precision styling analysis.",
|
|
2088
|
+
argsSchema: {
|
|
2089
|
+
selector: z.string().describe(
|
|
2090
|
+
"The Caliper Selector (JSON Fingerprint, Caliper Agent ID (caliper-***), or CSS selector) to audit. PRIORITIZE JSON Fingerprint for maximum stabilization."
|
|
2091
|
+
)
|
|
2092
|
+
}
|
|
2093
|
+
},
|
|
2094
|
+
async ({ selector }) => ({
|
|
2095
|
+
messages: [
|
|
2096
|
+
{
|
|
2097
|
+
role: "user",
|
|
2098
|
+
content: {
|
|
2099
|
+
type: "text",
|
|
2100
|
+
text: `## CALIPER SELECTOR AUDIT: ${selector}
|
|
2101
|
+
|
|
2102
|
+
You are about to perform a structured, comprehensive audit of the element '${selector}'. Follow these phases precisely.
|
|
2103
|
+
|
|
2104
|
+
### PHASE 1: CONTEXT GATHERING
|
|
2105
|
+
1. **Deep Inspection**: Call 'caliper_inspect' to get all computed styles, colors, typography, and **sourceHints**.
|
|
2106
|
+
2. **Hierarchy Walk**: Call 'caliper_walk_and_measure' with maxDepth: 5 to understand its position in the tree, its children, and the spacing (gaps) to its neighbors.
|
|
2107
|
+
3. **Source Discovery**: Use the \`suggestedGrep\` from \`sourceHints\` (or another stable anchor) to locate the exact source file and line number in the codebase.
|
|
2108
|
+
|
|
2109
|
+
### PHASE 2: ANALYSIS & CONSISTENCY
|
|
2110
|
+
4. **Spacing Check**: Analyze if padding, margins, and gaps follow a consistent scale (e.g., multiples of 4px/8px).
|
|
2111
|
+
5. **System Alignment**: Check if font sizes, weights, and colors match the project's design system or established patterns.
|
|
2112
|
+
6. **Intent Audit**: Identify hardcoded "magic numbers", layout inconsistencies, or unusual offsets.
|
|
2113
|
+
|
|
2114
|
+
### PHASE 3: REMEDIATION
|
|
2115
|
+
7. **Recommendations**: Provide a clear report of discrepancies and the exact CSS/Code fixes needed.
|
|
2116
|
+
8. **Verification**: After applying changes, re-run 'caliper_inspect' or 'caliper_walk_and_measure' to confirm parity.
|
|
2117
|
+
|
|
2118
|
+
---
|
|
2119
|
+
BEGIN PHASE 1 NOW. Do not skip any steps.`
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
]
|
|
2123
|
+
})
|
|
2124
|
+
);
|
|
2125
|
+
this.server.registerPrompt(
|
|
2126
|
+
"caliper-selectors-compare",
|
|
2127
|
+
{
|
|
2128
|
+
description: "Compare two selections (A and B) to learn from A and fix B. Supports cross-tab comparisons.",
|
|
2129
|
+
argsSchema: {
|
|
2130
|
+
selectorA: z.string().describe(
|
|
2131
|
+
"Caliper Selector for the REFERENCE element. PRIORITIZE JSON Fingerprint, Caliper Agent ID (caliper-***), or CSS selector for stabilization."
|
|
2132
|
+
),
|
|
2133
|
+
selectorB: z.string().describe(
|
|
2134
|
+
"Caliper Selector for the TARGET element. PRIORITIZE JSON Fingerprint, Caliper Agent ID (caliper-***), or CSS selector for stabilization."
|
|
2135
|
+
),
|
|
2136
|
+
tabIdA: z.string().optional().describe("Tab ID containing Selection A (use caliper_list_tabs to find IDs)"),
|
|
2137
|
+
tabIdB: z.string().optional().describe("Tab ID containing Selection B (defaults to same tab as A)"),
|
|
2138
|
+
properties: z.array(z.enum(["spacing", "typography", "colors", "layout", "all"])).default(["all"]).describe("Which properties to compare")
|
|
2139
|
+
}
|
|
2140
|
+
},
|
|
2141
|
+
async ({ selectorA, selectorB, tabIdA, tabIdB, properties }) => ({
|
|
2142
|
+
messages: [
|
|
2143
|
+
{
|
|
2144
|
+
role: "user",
|
|
2145
|
+
content: {
|
|
2146
|
+
type: "text",
|
|
2147
|
+
text: `## CALIPER SELECTORS COMPARISON
|
|
2148
|
+
|
|
2149
|
+
You are comparing TWO elements to understand the styling of one (A) and apply corrections to the other (B).
|
|
2150
|
+
|
|
2151
|
+
**Inputs:**
|
|
2152
|
+
- Selection A (REFERENCE): \`${selectorA}\`${tabIdA ? ` in Tab: ${tabIdA}` : ""}
|
|
2153
|
+
- Selection B (TARGET): \`${selectorB}\`${tabIdB ? ` in Tab: ${tabIdB}` : " (same tab as A)"}
|
|
2154
|
+
- Properties to Compare: ${properties.join(", ")}
|
|
2155
|
+
|
|
2156
|
+
---
|
|
2157
|
+
|
|
2158
|
+
### IMPORTANT: TAB MANAGEMENT
|
|
2159
|
+
|
|
2160
|
+
${tabIdA || tabIdB ? `
|
|
2161
|
+
You are working across multiple tabs. The agent-ID is **tab-specific** - if you send a command to the wrong tab, it will fail.
|
|
2162
|
+
|
|
2163
|
+
**Before Each Command:**
|
|
2164
|
+
1. Use \`caliper_list_tabs\` to see all connected tabs
|
|
2165
|
+
2. Use \`caliper_switch_tab\` to switch to the correct tab BEFORE calling walk/inspect
|
|
2166
|
+
` : `
|
|
2167
|
+
Both selections are on the SAME tab. No tab switching required.
|
|
2168
|
+
`}
|
|
2169
|
+
|
|
2170
|
+
### PHASE 1: WALK SELECTION A (REFERENCE)
|
|
2171
|
+
|
|
2172
|
+
${tabIdA ? `1. **Switch to Tab A**
|
|
2173
|
+
Call \`caliper_switch_tab\` with tabId: "${tabIdA}"
|
|
2174
|
+
|
|
2175
|
+
2. ` : "1. "}**Walk and Measure A**
|
|
2176
|
+
Call \`caliper_walk_and_measure\` with:
|
|
2177
|
+
- selector: "${selectorA}"
|
|
2178
|
+
- maxDepth: 5
|
|
2179
|
+
|
|
2180
|
+
Record A's styles as the REFERENCE:
|
|
2181
|
+
${properties.includes("all") || properties.includes("spacing") ? "- padding, margin, gap values" : ""}
|
|
2182
|
+
${properties.includes("all") || properties.includes("typography") ? "- font-size, font-weight, line-height" : ""}
|
|
2183
|
+
${properties.includes("all") || properties.includes("colors") ? "- background-color, color, border-color" : ""}
|
|
2184
|
+
${properties.includes("all") || properties.includes("layout") ? "- display, flex-direction, justify-content, align-items" : ""}
|
|
2185
|
+
|
|
2186
|
+
### PHASE 2: WALK SELECTION B (TARGET)
|
|
2187
|
+
|
|
2188
|
+
${tabIdB ? `${tabIdA ? "3" : "2"}. **Switch to Tab B**
|
|
2189
|
+
Call \`caliper_switch_tab\` with tabId: "${tabIdB}"
|
|
2190
|
+
|
|
2191
|
+
${tabIdA ? "4" : "3"}. ` : `${tabIdA ? "3" : "2"}. `}**Walk and Measure B**
|
|
2192
|
+
Call \`caliper_walk_and_measure\` with:
|
|
2193
|
+
- selector: "${selectorB}"
|
|
2194
|
+
- maxDepth: 5
|
|
2195
|
+
|
|
2196
|
+
### PHASE 3: COMPARE AND ANALYZE
|
|
2197
|
+
|
|
2198
|
+
${tabIdB ? tabIdA ? "5" : "4" : tabIdA ? "4" : "3"}. **Generate Comparison Report**
|
|
2199
|
+
For each property category, list:
|
|
2200
|
+
- Property name
|
|
2201
|
+
- Value in A (REFERENCE)
|
|
2202
|
+
- Value in B (TARGET)
|
|
2203
|
+
- Delta (difference)
|
|
2204
|
+
- Recommended fix for B
|
|
2205
|
+
|
|
2206
|
+
Focus on making B match A's styling approach.
|
|
2207
|
+
|
|
2208
|
+
### PHASE 4: FIND SOURCE FOR B
|
|
2209
|
+
|
|
2210
|
+
${tabIdB ? tabIdA ? "6" : "5" : tabIdA ? "5" : "4"}. **Locate B's Source**
|
|
2211
|
+
${tabIdB ? `Switch back to Tab B if needed.` : ""}
|
|
2212
|
+
Use internal agent search (grep) with B's best anchor.
|
|
2213
|
+
Get the exact file path and line number.
|
|
2214
|
+
|
|
2215
|
+
### PHASE 5: APPLY FIXES TO B
|
|
2216
|
+
|
|
2217
|
+
${tabIdB ? tabIdA ? "7" : "6" : tabIdA ? "6" : "5"}. **Generate CSS Fixes**
|
|
2218
|
+
Create CSS rules that will make B match A:
|
|
2219
|
+
\`\`\`css
|
|
2220
|
+
${selectorB.startsWith("#") ? selectorB : `.${selectorB}`} {
|
|
2221
|
+
/* Fixes to match Selection A */
|
|
2222
|
+
}
|
|
2223
|
+
\`\`\`
|
|
2224
|
+
|
|
2225
|
+
${tabIdB ? tabIdA ? "8" : "7" : tabIdA ? "7" : "6"}. **Edit Source File**
|
|
2226
|
+
Apply the CSS fixes to B's source file.
|
|
2227
|
+
|
|
2228
|
+
${tabIdB ? tabIdA ? "9" : "8" : tabIdA ? "8" : "7"}. **Verify**
|
|
2229
|
+
Re-run \`caliper_walk_and_measure\` on B to confirm the fix.
|
|
2230
|
+
|
|
2231
|
+
---
|
|
2232
|
+
**BEGIN PHASE 1 NOW. Remember to switch tabs before each walk command if working across tabs.**`
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
]
|
|
2236
|
+
})
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2239
|
+
async start() {
|
|
2240
|
+
try {
|
|
2241
|
+
await bridgeService.start(this.port);
|
|
2242
|
+
} catch (err) {
|
|
2243
|
+
logger4.error("Bridge startup failed:", err);
|
|
2244
|
+
}
|
|
2245
|
+
try {
|
|
2246
|
+
const transport = new StdioServerTransport();
|
|
2247
|
+
await this.server.connect(transport);
|
|
2248
|
+
logger4.info("Server running on STDIO transport");
|
|
2249
|
+
} catch (err) {
|
|
2250
|
+
logger4.error("MCP Startup failed:", err);
|
|
2251
|
+
process.exit(1);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
async stop() {
|
|
2255
|
+
await bridgeService.stop();
|
|
2256
|
+
logger4.info("Server stopped.");
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
|
|
2260
|
+
// ../mcp-server/src/index.ts
|
|
2261
|
+
function parseArgs() {
|
|
2262
|
+
const args = process.argv.slice(2);
|
|
2263
|
+
let port2 = DEFAULT_BRIDGE_PORT;
|
|
2264
|
+
for (let i = 0; i < args.length; i++) {
|
|
2265
|
+
if (args[i] === "--port" || args[i] === "-p") {
|
|
2266
|
+
const nextArg = args[i + 1];
|
|
2267
|
+
if (nextArg) {
|
|
2268
|
+
const val = parseInt(nextArg, 10);
|
|
2269
|
+
if (!isNaN(val) && val > 0 && val < 65536) {
|
|
2270
|
+
port2 = val;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
i++;
|
|
2274
|
+
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
2275
|
+
console.error(`
|
|
2276
|
+
Caliper MCP Server - AI-powered UI measurement tool
|
|
2277
|
+
|
|
2278
|
+
Usage:
|
|
2279
|
+
npx @oyerinde/caliper-mcp [options]
|
|
2280
|
+
|
|
2281
|
+
Options:
|
|
2282
|
+
-p, --port <number> WebSocket relay port (default: ${DEFAULT_BRIDGE_PORT})
|
|
2283
|
+
-d, --docs Open documentation: https://caliper.danieloyerinde.com/
|
|
2284
|
+
-h, --help Show this help message
|
|
2285
|
+
`);
|
|
2286
|
+
process.exit(0);
|
|
2287
|
+
} else if (args[i] === "--docs" || args[i] === "-d") {
|
|
2288
|
+
console.log("\n\u{1F4DA} View Documentation: https://caliper.danieloyerinde.com/\n");
|
|
2289
|
+
process.exit(0);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
return { port: port2 };
|
|
2293
|
+
}
|
|
2294
|
+
var { port } = parseArgs();
|
|
2295
|
+
var server = new CaliperMcpServer(port);
|
|
2296
|
+
server.start();
|
|
2297
|
+
var shutdown = async () => {
|
|
2298
|
+
await server.stop();
|
|
2299
|
+
process.exit(0);
|
|
2300
|
+
};
|
|
2301
|
+
process.on("SIGINT", shutdown);
|
|
2302
|
+
process.on("SIGTERM", shutdown);
|
|
2303
|
+
process.stdin.on("end", shutdown);
|