@rahmantech/litedesign 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2088 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/cli/src/commands/doctor.ts
4
+ import { existsSync as existsSync3 } from "node:fs";
5
+ import { readFile as readFile3 } from "node:fs/promises";
6
+ import path2 from "node:path";
7
+
8
+ // packages/cli/src/config.ts
9
+ import { existsSync } from "node:fs";
10
+ import { readFile } from "node:fs/promises";
11
+
12
+ // packages/cli/src/paths.ts
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+ var HOME = path.join(os.homedir(), ".litedesign");
16
+ var EXT_DIR = path.join(HOME, "extension");
17
+ var CONFIG = path.join(HOME, "config.json");
18
+ var DEFAULT_PORT = 8787;
19
+ function wsState(port) {
20
+ return path.join(HOME, `ws-${port}.json`);
21
+ }
22
+ var BUNDLED_EXTENSION = new URL("./extension/", import.meta.url);
23
+
24
+ // packages/cli/src/config.ts
25
+ var MODES = ["auto", "playwright", "extension"];
26
+ var FALLBACK = { port: DEFAULT_PORT, mode: "auto" };
27
+ async function readConfig() {
28
+ if (!existsSync(CONFIG)) return { ...FALLBACK };
29
+ try {
30
+ const raw = JSON.parse(await readFile(CONFIG, "utf8"));
31
+ return {
32
+ port: isPort(raw.port) ? raw.port : FALLBACK.port,
33
+ mode: typeof raw.mode === "string" && isMode(raw.mode) ? raw.mode : FALLBACK.mode
34
+ };
35
+ } catch {
36
+ return { ...FALLBACK };
37
+ }
38
+ }
39
+ function isPort(value) {
40
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value < 65536;
41
+ }
42
+ function isMode(value) {
43
+ return MODES.includes(value);
44
+ }
45
+
46
+ // packages/cli/src/version.ts
47
+ var VERSION = "0.1.0";
48
+
49
+ // packages/cli/src/ws.ts
50
+ import { spawn } from "node:child_process";
51
+ import { existsSync as existsSync2 } from "node:fs";
52
+ import { readFile as readFile2, rm, writeFile } from "node:fs/promises";
53
+ import { createConnection } from "node:net";
54
+
55
+ // packages/cli/src/hub.ts
56
+ import { randomUUID } from "node:crypto";
57
+ import { WebSocket, WebSocketServer } from "ws";
58
+
59
+ // packages/cli/src/mcp/handoff.ts
60
+ function handoffMessage(url) {
61
+ return [
62
+ `Open ${url} in Chrome and keep it in the active tab.`,
63
+ "",
64
+ "The extension attaches by itself, so there is nothing to click.",
65
+ "If nothing connects:",
66
+ "- make sure the extension is loaded unpacked at chrome://extensions",
67
+ "- make sure Chrome is running",
68
+ "",
69
+ "I am waiting for that page to connect."
70
+ ].join("\n");
71
+ }
72
+ function mismatchMessage(expected, actual) {
73
+ return [
74
+ `The active tab is ${actual}, but I am waiting for ${expected}.`,
75
+ "",
76
+ `Switch to ${expected} and call await_page,`,
77
+ "or call inspect again with the URL you actually have open."
78
+ ].join("\n");
79
+ }
80
+
81
+ // packages/cli/src/mcp/errors.ts
82
+ var ToolError = class extends Error {
83
+ constructor(message) {
84
+ super(message);
85
+ this.name = "ToolError";
86
+ }
87
+ };
88
+ var AwaitingUserError = class extends ToolError {
89
+ url;
90
+ constructor(url) {
91
+ super(`the page ${url} is not connected. Open it and keep it in the active tab.`);
92
+ this.name = "AwaitingUserError";
93
+ this.url = url;
94
+ }
95
+ };
96
+ var MismatchError = class extends ToolError {
97
+ expected;
98
+ actual;
99
+ constructor(expected, actual) {
100
+ super(mismatchMessage(expected, actual));
101
+ this.name = "MismatchError";
102
+ this.expected = expected;
103
+ this.actual = actual;
104
+ }
105
+ };
106
+
107
+ // packages/cli/src/hub.ts
108
+ var PING_INTERVAL = 2e4;
109
+ var Hub = class _Hub {
110
+ #server;
111
+ #clients = /* @__PURE__ */ new Map();
112
+ #socketTab = /* @__PURE__ */ new Map();
113
+ #sockets = /* @__PURE__ */ new Set();
114
+ #controllers = /* @__PURE__ */ new Set();
115
+ #pending = /* @__PURE__ */ new Map();
116
+ #waiters = /* @__PURE__ */ new Set();
117
+ #ping;
118
+ #closed = false;
119
+ constructor(server) {
120
+ this.#server = server;
121
+ }
122
+ static start(port) {
123
+ return new Promise((resolve, reject) => {
124
+ const server = new WebSocketServer({ host: "127.0.0.1", port });
125
+ const hub = new _Hub(server);
126
+ const onError = (error) => {
127
+ server.off("listening", onListening);
128
+ reject(error);
129
+ };
130
+ const onListening = () => {
131
+ server.off("error", onError);
132
+ server.on("connection", (socket) => hub.#accept(socket));
133
+ hub.#ping = setInterval(() => hub.#broadcastPing(), PING_INTERVAL);
134
+ resolve(hub);
135
+ };
136
+ server.once("error", onError);
137
+ server.once("listening", onListening);
138
+ });
139
+ }
140
+ /** Bound port. Useful for tests, which start the hub on port 0. */
141
+ get port() {
142
+ const address = this.#server.address();
143
+ return typeof address === "object" && address !== null ? address.port : 0;
144
+ }
145
+ pages() {
146
+ return [...this.#clients.values()].map(({ socket: _socket, ...page }) => page);
147
+ }
148
+ find(url) {
149
+ for (const client of this.#clients.values()) {
150
+ if (sameUrl(client.url, url)) return client;
151
+ }
152
+ return void 0;
153
+ }
154
+ async request(tabId, action, payload, timeoutMs = 3e4) {
155
+ const client = this.#clients.get(tabId);
156
+ if (client === void 0) {
157
+ throw new ToolError("the page is no longer connected. Call inspect again.");
158
+ }
159
+ return this.#ask(client.socket, action, payload, timeoutMs);
160
+ }
161
+ /**
162
+ * Asks any connected worker, not a specific tab. This is how the server
163
+ * attaches to a page: the worker picks the tab the human is looking at.
164
+ */
165
+ async requestAny(action, payload, timeoutMs = 3e4) {
166
+ const socket = this.#firstSocket();
167
+ if (socket === void 0) {
168
+ throw new ToolError(
169
+ "the extension is not connected. Is Chrome open with the extension loaded?"
170
+ );
171
+ }
172
+ return this.#ask(socket, action, payload, timeoutMs);
173
+ }
174
+ /** True when at least one extension worker is connected. */
175
+ hasClient() {
176
+ return this.#firstSocket() !== void 0;
177
+ }
178
+ /** Number of connected extension workers, with or without an attached page. */
179
+ clientCount() {
180
+ let count = 0;
181
+ for (const socket of this.#sockets) {
182
+ if (socket.readyState === WebSocket.OPEN) count += 1;
183
+ }
184
+ return count;
185
+ }
186
+ #firstSocket() {
187
+ for (const socket of this.#sockets) {
188
+ if (socket.readyState === WebSocket.OPEN) return socket;
189
+ }
190
+ return void 0;
191
+ }
192
+ #ask(socket, action, payload, timeoutMs) {
193
+ const id = randomUUID();
194
+ return new Promise((resolve, reject) => {
195
+ const timer = setTimeout(() => {
196
+ this.#pending.delete(id);
197
+ reject(
198
+ new ToolError(
199
+ `the extension did not answer "${action}" within ${Math.round(timeoutMs / 1e3)}s`
200
+ )
201
+ );
202
+ }, timeoutMs);
203
+ this.#pending.set(id, {
204
+ resolve,
205
+ reject,
206
+ timer,
207
+ socket,
208
+ action
209
+ });
210
+ socket.send(JSON.stringify({ type: "request", id, action, payload }));
211
+ });
212
+ }
213
+ /**
214
+ * Resolves on the next `hello`, or immediately when a matching page is
215
+ * already connected. A page that connects with another URL resolves as
216
+ * `mismatch` so the agent can tell the user exactly what is wrong.
217
+ */
218
+ waitForPage(url, timeoutMs) {
219
+ const existing = this.find(url);
220
+ if (existing !== void 0) return Promise.resolve({ status: "ready", page: existing });
221
+ return new Promise((resolve) => {
222
+ const waiter = {
223
+ url,
224
+ resolve: (outcome) => {
225
+ clearTimeout(timer);
226
+ resolve(outcome);
227
+ }
228
+ };
229
+ const timer = setTimeout(() => {
230
+ this.#waiters.delete(waiter);
231
+ resolve({ status: "timeout" });
232
+ }, timeoutMs);
233
+ this.#waiters.add(waiter);
234
+ });
235
+ }
236
+ async close() {
237
+ if (this.#closed) return;
238
+ this.#closed = true;
239
+ if (this.#ping !== void 0) clearInterval(this.#ping);
240
+ this.#ping = void 0;
241
+ for (const waiter of [...this.#waiters]) waiter.resolve({ status: "timeout" });
242
+ this.#waiters.clear();
243
+ for (const pending of this.#pending.values()) {
244
+ clearTimeout(pending.timer);
245
+ pending.reject(new ToolError("the extension bridge is shutting down"));
246
+ }
247
+ this.#pending.clear();
248
+ const sockets = [...this.#sockets];
249
+ for (const socket of sockets) socket.close(1001, "litedesign server closing");
250
+ for (const socket of this.#controllers) socket.close(1001, "litedesign server closing");
251
+ await new Promise((resolve) => {
252
+ const timer = setTimeout(() => {
253
+ for (const socket of sockets) socket.terminate();
254
+ resolve();
255
+ }, 500);
256
+ this.#server.close(() => {
257
+ clearTimeout(timer);
258
+ resolve();
259
+ });
260
+ });
261
+ }
262
+ #accept(socket) {
263
+ this.#sockets.add(socket);
264
+ this.#broadcast({ type: "extensions", count: this.clientCount() });
265
+ socket.on("message", (data) => this.#receive(socket, data.toString()));
266
+ socket.on("close", () => this.#drop(socket));
267
+ socket.on("error", () => this.#drop(socket));
268
+ }
269
+ #receive(socket, raw) {
270
+ let message;
271
+ try {
272
+ const parsed = JSON.parse(raw);
273
+ if (typeof parsed !== "object" || parsed === null) return;
274
+ message = parsed;
275
+ } catch {
276
+ return;
277
+ }
278
+ switch (message.type) {
279
+ case "hello":
280
+ if (message.role === "mcp") this.#controllerHello(socket);
281
+ else this.#hello(socket, message);
282
+ return;
283
+ case "request":
284
+ this.#controllerRequest(socket, message);
285
+ return;
286
+ case "response":
287
+ this.#response(message);
288
+ return;
289
+ case "bye":
290
+ this.#drop(socket);
291
+ socket.close();
292
+ return;
293
+ default:
294
+ return;
295
+ }
296
+ }
297
+ #controllerHello(socket) {
298
+ this.#sockets.delete(socket);
299
+ this.#controllers.add(socket);
300
+ this.#broadcast({ type: "extensions", count: this.clientCount() });
301
+ socket.send(
302
+ JSON.stringify({ type: "pages", pages: this.pages(), extensions: this.clientCount() })
303
+ );
304
+ }
305
+ /**
306
+ * An MCP client asks the daemon to run something. The daemon forwards to the
307
+ * right extension socket and routes the answer back.
308
+ */
309
+ #controllerRequest(socket, message) {
310
+ const { id, action } = message;
311
+ if (typeof id !== "string" || typeof action !== "string") return;
312
+ if (action === "pages") {
313
+ this.#reply(socket, id, true, this.pages());
314
+ return;
315
+ }
316
+ if (action === "has-client") {
317
+ this.#reply(socket, id, true, this.hasClient());
318
+ return;
319
+ }
320
+ const tabId = typeof message.tabId === "number" ? message.tabId : void 0;
321
+ const target = tabId === void 0 ? this.#firstSocket() : this.#clients.get(tabId)?.socket;
322
+ if (target === void 0 || target.readyState !== WebSocket.OPEN) {
323
+ this.#reply(
324
+ socket,
325
+ id,
326
+ false,
327
+ void 0,
328
+ tabId === void 0 ? "the extension is not connected. Is Chrome open with the extension loaded?" : "the page is no longer connected. Call inspect again."
329
+ );
330
+ return;
331
+ }
332
+ const forwardId = randomUUID();
333
+ const timer = setTimeout(() => {
334
+ this.#pending.delete(forwardId);
335
+ this.#reply(
336
+ socket,
337
+ id,
338
+ false,
339
+ void 0,
340
+ `the extension did not answer "${action}" within 30s`
341
+ );
342
+ }, 3e4);
343
+ this.#pending.set(forwardId, {
344
+ resolve: () => void 0,
345
+ reject: () => void 0,
346
+ timer,
347
+ socket: target,
348
+ action,
349
+ controller: socket,
350
+ controllerId: id
351
+ });
352
+ target.send(
353
+ JSON.stringify({ type: "request", id: forwardId, action, payload: message.payload })
354
+ );
355
+ }
356
+ #reply(socket, id, ok, result, error) {
357
+ if (socket.readyState !== WebSocket.OPEN) return;
358
+ socket.send(JSON.stringify({ type: "response", id, ok, result, error }));
359
+ }
360
+ #hello(socket, message) {
361
+ const { tabId, url } = message;
362
+ if (typeof tabId !== "number" || !Number.isInteger(tabId) || typeof url !== "string" || url === "") {
363
+ return;
364
+ }
365
+ const page = {
366
+ tabId,
367
+ url,
368
+ title: typeof message.title === "string" ? message.title : "",
369
+ version: typeof message.version === "string" ? message.version : ""
370
+ };
371
+ const previous = this.#socketTab.get(socket);
372
+ if (previous !== void 0 && previous !== tabId) {
373
+ const old = this.#clients.get(previous);
374
+ if (old?.socket === socket) this.#clients.delete(previous);
375
+ }
376
+ this.#clients.set(tabId, { ...page, socket });
377
+ this.#socketTab.set(socket, tabId);
378
+ this.#broadcast({ type: "page", page });
379
+ this.#broadcast({ type: "extensions", count: this.clientCount() });
380
+ for (const waiter of [...this.#waiters]) {
381
+ this.#waiters.delete(waiter);
382
+ waiter.resolve(
383
+ sameUrl(page.url, waiter.url) ? { status: "ready", page } : { status: "mismatch", page }
384
+ );
385
+ }
386
+ }
387
+ #response(message) {
388
+ const { id } = message;
389
+ if (typeof id !== "string") return;
390
+ const pending = this.#pending.get(id);
391
+ if (pending === void 0) return;
392
+ this.#pending.delete(id);
393
+ clearTimeout(pending.timer);
394
+ if (pending.controller !== void 0) {
395
+ const ok = message.ok === true;
396
+ this.#reply(
397
+ pending.controller,
398
+ pending.controllerId ?? "",
399
+ ok,
400
+ message.result,
401
+ typeof message.error === "string" && message.error !== "" ? message.error : `the extension failed to run "${pending.action}"`
402
+ );
403
+ return;
404
+ }
405
+ if (message.ok === true) {
406
+ pending.resolve(message.result);
407
+ return;
408
+ }
409
+ pending.reject(
410
+ new ToolError(
411
+ typeof message.error === "string" && message.error !== "" ? message.error : `the extension failed to run "${pending.action}"`
412
+ )
413
+ );
414
+ }
415
+ #broadcast(message) {
416
+ const raw = JSON.stringify(message);
417
+ for (const socket of this.#controllers) {
418
+ if (socket.readyState === WebSocket.OPEN) socket.send(raw);
419
+ }
420
+ }
421
+ #drop(socket) {
422
+ const tabId = this.#socketTab.get(socket);
423
+ this.#socketTab.delete(socket);
424
+ this.#sockets.delete(socket);
425
+ this.#controllers.delete(socket);
426
+ if (tabId !== void 0 && this.#clients.get(tabId)?.socket === socket) {
427
+ this.#clients.delete(tabId);
428
+ this.#broadcast({ type: "page-gone", tabId });
429
+ }
430
+ this.#broadcast({ type: "extensions", count: this.clientCount() });
431
+ for (const [id, pending] of this.#pending) {
432
+ if (pending.socket !== socket) continue;
433
+ this.#pending.delete(id);
434
+ clearTimeout(pending.timer);
435
+ if (pending.controller !== void 0) {
436
+ this.#reply(
437
+ pending.controller,
438
+ pending.controllerId ?? "",
439
+ false,
440
+ void 0,
441
+ "the page disconnected before it answered"
442
+ );
443
+ } else {
444
+ pending.reject(new ToolError("the page disconnected before it answered"));
445
+ }
446
+ }
447
+ }
448
+ #broadcastPing() {
449
+ for (const socket of this.#sockets) {
450
+ if (socket.readyState === WebSocket.OPEN) {
451
+ socket.send(JSON.stringify({ type: "ping" }));
452
+ }
453
+ }
454
+ }
455
+ };
456
+ function sameUrl(a, b) {
457
+ return normalize(a) === normalize(b);
458
+ }
459
+ function normalize(url) {
460
+ try {
461
+ const parsed = new URL(url);
462
+ parsed.hash = "";
463
+ return parsed.href;
464
+ } catch {
465
+ return url;
466
+ }
467
+ }
468
+ var current = null;
469
+ var currentError = null;
470
+ function setHub(hub) {
471
+ current = hub;
472
+ currentError = null;
473
+ }
474
+ function setHubError(message) {
475
+ current = null;
476
+ currentError = message;
477
+ }
478
+ function getHub() {
479
+ return current;
480
+ }
481
+ function getHubError() {
482
+ return currentError;
483
+ }
484
+
485
+ // packages/cli/src/hub-client.ts
486
+ import { randomUUID as randomUUID2 } from "node:crypto";
487
+ import { WebSocket as WebSocket2 } from "ws";
488
+ var DEFAULT_TIMEOUT = 3e4;
489
+ var HubClient = class _HubClient {
490
+ #socket;
491
+ #pages = /* @__PURE__ */ new Map();
492
+ #pending = /* @__PURE__ */ new Map();
493
+ #waiters = /* @__PURE__ */ new Set();
494
+ #extensions = 0;
495
+ #closed = false;
496
+ constructor(socket) {
497
+ this.#socket = socket;
498
+ }
499
+ static connect(port, timeoutMs = 2e3) {
500
+ return new Promise((resolve, reject) => {
501
+ const socket = new WebSocket2(`ws://127.0.0.1:${port}`);
502
+ const client = new _HubClient(socket);
503
+ const timer = setTimeout(() => {
504
+ socket.terminate();
505
+ reject(new ToolError(`the bridge on port ${port} did not answer`));
506
+ }, timeoutMs);
507
+ socket.once("open", () => {
508
+ socket.send(JSON.stringify({ type: "hello", role: "mcp" }));
509
+ });
510
+ socket.once("error", (error) => {
511
+ clearTimeout(timer);
512
+ reject(error instanceof Error ? error : new ToolError(String(error)));
513
+ });
514
+ client.#listen(() => {
515
+ clearTimeout(timer);
516
+ resolve(client);
517
+ });
518
+ });
519
+ }
520
+ pages() {
521
+ return [...this.#pages.values()];
522
+ }
523
+ find(url) {
524
+ for (const page of this.#pages.values()) {
525
+ if (sameUrl(page.url, url)) return page;
526
+ }
527
+ return void 0;
528
+ }
529
+ hasClient() {
530
+ return this.#extensions > 0;
531
+ }
532
+ clientCount() {
533
+ return this.#extensions;
534
+ }
535
+ async request(tabId, action, payload, timeoutMs = DEFAULT_TIMEOUT) {
536
+ return this.#ask({ tabId, action, payload }, action, timeoutMs);
537
+ }
538
+ async requestAny(action, payload, timeoutMs = DEFAULT_TIMEOUT) {
539
+ return this.#ask({ action, payload }, action, timeoutMs);
540
+ }
541
+ waitForPage(url, timeoutMs) {
542
+ const existing = this.find(url);
543
+ if (existing !== void 0) return Promise.resolve({ status: "ready", page: existing });
544
+ return new Promise((resolve) => {
545
+ const waiter = {
546
+ url,
547
+ resolve: (outcome) => {
548
+ clearTimeout(timer);
549
+ resolve(outcome);
550
+ }
551
+ };
552
+ const timer = setTimeout(() => {
553
+ this.#waiters.delete(waiter);
554
+ resolve({ status: "timeout" });
555
+ }, timeoutMs);
556
+ this.#waiters.add(waiter);
557
+ });
558
+ }
559
+ async close() {
560
+ if (this.#closed) return;
561
+ this.#closed = true;
562
+ for (const waiter of [...this.#waiters]) waiter.resolve({ status: "timeout" });
563
+ this.#waiters.clear();
564
+ for (const pending of this.#pending.values()) {
565
+ clearTimeout(pending.timer);
566
+ pending.reject(new ToolError("the bridge client is closing"));
567
+ }
568
+ this.#pending.clear();
569
+ this.#socket.close(1e3, "mcp session ended");
570
+ }
571
+ #ask(message, action, timeoutMs) {
572
+ if (this.#closed || this.#socket.readyState !== WebSocket2.OPEN) {
573
+ return Promise.reject(
574
+ new ToolError("the bridge is not running. Start it with `litedesign ws start`.")
575
+ );
576
+ }
577
+ const id = randomUUID2();
578
+ return new Promise((resolve, reject) => {
579
+ const timer = setTimeout(() => {
580
+ this.#pending.delete(id);
581
+ reject(
582
+ new ToolError(
583
+ `the extension did not answer "${action}" within ${Math.round(timeoutMs / 1e3)}s`
584
+ )
585
+ );
586
+ }, timeoutMs);
587
+ this.#pending.set(id, {
588
+ resolve,
589
+ reject,
590
+ timer
591
+ });
592
+ this.#socket.send(JSON.stringify({ type: "request", id, ...message }));
593
+ });
594
+ }
595
+ #listen(ready) {
596
+ this.#socket.on("message", (data) => {
597
+ let message;
598
+ try {
599
+ const parsed = JSON.parse(data.toString());
600
+ if (typeof parsed !== "object" || parsed === null) return;
601
+ message = parsed;
602
+ } catch {
603
+ return;
604
+ }
605
+ switch (message.type) {
606
+ case "pages":
607
+ this.#takePages(message);
608
+ ready();
609
+ return;
610
+ case "page":
611
+ this.#takePage(message.page);
612
+ return;
613
+ case "page-gone":
614
+ if (typeof message.tabId === "number") this.#pages.delete(message.tabId);
615
+ return;
616
+ case "extensions":
617
+ if (typeof message.count === "number") this.#extensions = message.count;
618
+ return;
619
+ case "response":
620
+ this.#response(message);
621
+ return;
622
+ default:
623
+ return;
624
+ }
625
+ });
626
+ this.#socket.on("close", () => {
627
+ this.#closed = true;
628
+ for (const waiter of [...this.#waiters]) waiter.resolve({ status: "timeout" });
629
+ this.#waiters.clear();
630
+ for (const pending of this.#pending.values()) {
631
+ clearTimeout(pending.timer);
632
+ pending.reject(new ToolError("the bridge stopped running"));
633
+ }
634
+ this.#pending.clear();
635
+ });
636
+ }
637
+ #takePages(message) {
638
+ if (typeof message.extensions === "number") this.#extensions = message.extensions;
639
+ if (!Array.isArray(message.pages)) return;
640
+ this.#pages.clear();
641
+ for (const page of message.pages) this.#takePage(page);
642
+ }
643
+ #takePage(value) {
644
+ if (typeof value !== "object" || value === null) return;
645
+ const page = value;
646
+ if (typeof page.tabId !== "number" || typeof page.url !== "string") return;
647
+ this.#pages.set(page.tabId, page);
648
+ for (const waiter of [...this.#waiters]) {
649
+ this.#waiters.delete(waiter);
650
+ waiter.resolve(
651
+ sameUrl(page.url, waiter.url) ? { status: "ready", page } : { status: "mismatch", page }
652
+ );
653
+ }
654
+ }
655
+ #response(message) {
656
+ const { id } = message;
657
+ if (typeof id !== "string") return;
658
+ const pending = this.#pending.get(id);
659
+ if (pending === void 0) return;
660
+ this.#pending.delete(id);
661
+ clearTimeout(pending.timer);
662
+ if (message.ok === true) {
663
+ pending.resolve(message.result);
664
+ return;
665
+ }
666
+ pending.reject(
667
+ new ToolError(
668
+ typeof message.error === "string" && message.error !== "" ? message.error : "the extension failed to run the request"
669
+ )
670
+ );
671
+ }
672
+ };
673
+
674
+ // packages/cli/src/ws.ts
675
+ var START_TIMEOUT = 5e3;
676
+ function portOpen(port, timeoutMs = 400) {
677
+ return new Promise((resolve) => {
678
+ const socket = createConnection({ host: "127.0.0.1", port });
679
+ const done = (value) => {
680
+ socket.destroy();
681
+ resolve(value);
682
+ };
683
+ socket.setTimeout(timeoutMs);
684
+ socket.once("connect", () => done(true));
685
+ socket.once("timeout", () => done(false));
686
+ socket.once("error", () => done(false));
687
+ });
688
+ }
689
+ async function readState(port) {
690
+ const file = wsState(port);
691
+ if (!existsSync2(file)) return void 0;
692
+ try {
693
+ const raw = JSON.parse(await readFile2(file, "utf8"));
694
+ if (typeof raw.pid !== "number" || typeof raw.port !== "number") return void 0;
695
+ return { pid: raw.pid, port: raw.port };
696
+ } catch {
697
+ return void 0;
698
+ }
699
+ }
700
+ async function writeState(state) {
701
+ await writeFile(wsState(state.port), `${JSON.stringify(state, null, 2)}
702
+ `);
703
+ }
704
+ async function clearState(port) {
705
+ await rm(wsState(port), { force: true });
706
+ }
707
+ function isAlive(pid) {
708
+ try {
709
+ process.kill(pid, 0);
710
+ return true;
711
+ } catch {
712
+ return false;
713
+ }
714
+ }
715
+ async function runDaemon(port) {
716
+ let hub;
717
+ try {
718
+ hub = await Hub.start(port);
719
+ } catch (error) {
720
+ const detail = error instanceof Error ? error.message : String(error);
721
+ throw new ToolError(`could not listen on port ${port}: ${detail}`);
722
+ }
723
+ await writeState({ pid: process.pid, port: hub.port });
724
+ console.log(` bridge ws://127.0.0.1:${hub.port} \xB7 pid ${process.pid} \xB7 Ctrl+C to stop`);
725
+ const stop = () => {
726
+ void (async () => {
727
+ await clearState(port);
728
+ await hub.close();
729
+ process.exit(0);
730
+ })();
731
+ };
732
+ process.once("SIGINT", stop);
733
+ process.once("SIGTERM", stop);
734
+ }
735
+ async function startDaemon(port) {
736
+ if (await portOpen(port)) {
737
+ const state2 = await readState(port);
738
+ return state2 ?? { pid: 0, port };
739
+ }
740
+ const script = process.argv[1];
741
+ if (script === void 0) {
742
+ throw new ToolError("could not find the litedesign entry point to start the bridge");
743
+ }
744
+ const child = spawn(process.execPath, [script, "ws", "run", "--port", String(port)], {
745
+ detached: true,
746
+ stdio: "ignore",
747
+ env: process.env
748
+ });
749
+ child.unref();
750
+ const started = await waitFor(async () => portOpen(port), START_TIMEOUT);
751
+ if (!started) {
752
+ throw new ToolError(
753
+ `the bridge did not start on port ${port}. Run "litedesign ws run" to see the error.`
754
+ );
755
+ }
756
+ const state = await waitForValue(() => readState(port), 2e3);
757
+ return state ?? { pid: 0, port };
758
+ }
759
+ async function ensureBridge(port) {
760
+ try {
761
+ return await HubClient.connect(port, 500);
762
+ } catch {
763
+ }
764
+ await startDaemon(port);
765
+ try {
766
+ return await HubClient.connect(port, 3e3);
767
+ } catch (error) {
768
+ const detail = error instanceof Error ? error.message : String(error);
769
+ throw new ToolError(`the bridge started but did not accept a connection: ${detail}`);
770
+ }
771
+ }
772
+ async function stopDaemon(port) {
773
+ const state = await readState(port);
774
+ if (state === void 0) {
775
+ await clearState(port);
776
+ return false;
777
+ }
778
+ if (isAlive(state.pid)) {
779
+ process.kill(state.pid, "SIGTERM");
780
+ const stopped = await waitFor(async () => !isAlive(state.pid), 3e3);
781
+ if (!stopped) throw new ToolError(`the bridge (pid ${state.pid}) did not stop`);
782
+ }
783
+ await clearState(port);
784
+ return true;
785
+ }
786
+ async function wsStatus(port) {
787
+ const state = await readState(port);
788
+ const listening = await portOpen(port);
789
+ if (!listening) {
790
+ if (state !== void 0) await clearState(port);
791
+ return { running: false, port };
792
+ }
793
+ let extensions;
794
+ let pages;
795
+ try {
796
+ const client = await HubClient.connect(port, 1e3);
797
+ extensions = client.clientCount();
798
+ pages = client.pages();
799
+ await client.close();
800
+ } catch {
801
+ }
802
+ return {
803
+ running: true,
804
+ port,
805
+ ...state !== void 0 ? { pid: state.pid } : {},
806
+ ...extensions !== void 0 ? { extensions } : {},
807
+ ...pages !== void 0 ? { pages } : {}
808
+ };
809
+ }
810
+ async function waitFor(check, timeoutMs) {
811
+ const deadline = Date.now() + timeoutMs;
812
+ while (Date.now() < deadline) {
813
+ if (await check()) return true;
814
+ await delay(100);
815
+ }
816
+ return check();
817
+ }
818
+ async function waitForValue(read, timeoutMs) {
819
+ const deadline = Date.now() + timeoutMs;
820
+ while (Date.now() < deadline) {
821
+ const value = await read();
822
+ if (value !== void 0) return value;
823
+ await delay(100);
824
+ }
825
+ return read();
826
+ }
827
+ function delay(ms) {
828
+ return new Promise((resolve) => setTimeout(resolve, ms));
829
+ }
830
+
831
+ // packages/cli/src/commands/doctor.ts
832
+ function line(status, label, detail) {
833
+ const mark = status === "ok" ? "ok " : status === "warn" ? "warn" : "fail";
834
+ console.log(` ${mark} ${label.padEnd(12)} ${detail}`);
835
+ }
836
+ async function doctor() {
837
+ console.log(`
838
+ litedesign doctor \xB7 v${VERSION}
839
+ `);
840
+ const major = Number(process.versions.node.split(".")[0]);
841
+ line(major >= 20 ? "ok" : "fail", "node", `v${process.versions.node} (requires >= 20)`);
842
+ const manifestPath = path2.join(EXT_DIR, "manifest.json");
843
+ if (existsSync3(manifestPath)) {
844
+ const manifest = JSON.parse(await readFile3(manifestPath, "utf8"));
845
+ line("ok", "extension", `${EXT_DIR} (v${manifest.version ?? "?"})`);
846
+ } else {
847
+ line("fail", "extension", `missing at ${EXT_DIR} \u2014 run: litedesign setup`);
848
+ }
849
+ const { port, mode } = await readConfig();
850
+ if (existsSync3(CONFIG)) {
851
+ line("ok", "config", `port ${port}, mode ${mode}`);
852
+ } else {
853
+ line("warn", "config", "missing \u2014 run: litedesign setup");
854
+ }
855
+ const status = await wsStatus(port);
856
+ line(
857
+ status.running ? "ok" : "warn",
858
+ "bridge",
859
+ status.running ? `running on ${port} \xB7 ${status.extensions ?? 0} extension(s) \xB7 ${status.pages?.length ?? 0} page(s)` : `stopped \u2014 run: litedesign ws start`
860
+ );
861
+ console.log("");
862
+ }
863
+
864
+ // packages/cli/src/commands/setup.ts
865
+ import { existsSync as existsSync4 } from "node:fs";
866
+ import { cp, mkdir, writeFile as writeFile2 } from "node:fs/promises";
867
+ import path3 from "node:path";
868
+ import { fileURLToPath } from "node:url";
869
+ async function setup() {
870
+ const source = fileURLToPath(BUNDLED_EXTENSION);
871
+ if (!existsSync4(source)) {
872
+ throw new Error(
873
+ "extension assets missing from the package. Reinstall: npm i -g @rahmantech/litedesign"
874
+ );
875
+ }
876
+ await mkdir(HOME, { recursive: true });
877
+ await cp(source, EXT_DIR, { recursive: true, force: true });
878
+ let configCreated = false;
879
+ if (!existsSync4(CONFIG)) {
880
+ await writeFile2(
881
+ CONFIG,
882
+ `${JSON.stringify({ port: DEFAULT_PORT, mode: "auto" }, null, 2)}
883
+ `,
884
+ "utf8"
885
+ );
886
+ configCreated = true;
887
+ }
888
+ const config = await readConfig();
889
+ await writeFile2(
890
+ path3.join(EXT_DIR, "config.json"),
891
+ `${JSON.stringify({ port: config.port }, null, 2)}
892
+ `,
893
+ "utf8"
894
+ );
895
+ console.log(`
896
+ litedesign setup
897
+
898
+ [1/3] extension -> ${EXT_DIR}
899
+ [2/3] config -> ${CONFIG}${configCreated ? "" : " (already exists)"}
900
+ [3/3] done
901
+
902
+ One manual step:
903
+
904
+ 1. open chrome://extensions
905
+ 2. enable Developer mode
906
+ 3. Load unpacked -> ${EXT_DIR}
907
+
908
+ Register with your MCP client:
909
+
910
+ {
911
+ "mcpServers": {
912
+ "litedesign": {
913
+ "command": "litedesign",
914
+ "args": ["mcp"]
915
+ }
916
+ }
917
+ }
918
+
919
+ port ${config.port} \xB7 mode ${config.mode}
920
+ `);
921
+ }
922
+
923
+ // packages/cli/src/commands/extension.ts
924
+ async function extensionCommand(args) {
925
+ const sub = args[0];
926
+ if (sub === "path") {
927
+ console.log(EXT_DIR);
928
+ return;
929
+ }
930
+ if (sub === "update") {
931
+ await setup();
932
+ return;
933
+ }
934
+ throw new Error("usage: litedesign extension <path|update>");
935
+ }
936
+
937
+ // packages/cli/src/commands/ws.ts
938
+ var HELP = `
939
+ Usage:
940
+ litedesign ws start start the bridge in the background
941
+ litedesign ws stop stop the bridge
942
+ litedesign ws status show whether it runs and what is connected
943
+ litedesign ws run run it in the foreground (used by start)
944
+ `;
945
+ async function wsCommand(argv) {
946
+ const [action, ...rest] = argv;
947
+ const port = portFrom(rest) ?? (await readConfig()).port;
948
+ switch (action) {
949
+ case void 0:
950
+ case "help":
951
+ case "--help":
952
+ console.log(HELP);
953
+ return;
954
+ case "run":
955
+ await runDaemon(port);
956
+ return;
957
+ case "start": {
958
+ const state = await startDaemon(port);
959
+ const pid = state.pid > 0 ? ` \xB7 pid ${state.pid}` : "";
960
+ console.log(` bridge running \xB7 ws://127.0.0.1:${state.port}${pid}`);
961
+ return;
962
+ }
963
+ case "stop": {
964
+ const stopped = await stopDaemon(port);
965
+ console.log(stopped ? " bridge stopped" : " bridge was not running");
966
+ return;
967
+ }
968
+ case "status":
969
+ await printStatus(port);
970
+ return;
971
+ default:
972
+ throw new Error(`unknown ws command: ${action}. Try: litedesign ws --help`);
973
+ }
974
+ }
975
+ async function printStatus(port) {
976
+ const status = await wsStatus(port);
977
+ if (!status.running) {
978
+ console.log(` bridge stopped \xB7 port ${port}`);
979
+ return;
980
+ }
981
+ const pid = status.pid === void 0 ? "" : ` \xB7 pid ${status.pid}`;
982
+ const extensions = status.extensions ?? 0;
983
+ const pages = status.pages?.length ?? 0;
984
+ console.log(
985
+ ` bridge running \xB7 ws://127.0.0.1:${status.port}${pid} \xB7 ${extensions} extension(s) \xB7 ${pages} page(s)`
986
+ );
987
+ for (const page of status.pages ?? []) {
988
+ console.log(` tab ${page.tabId} ${page.url}`);
989
+ }
990
+ }
991
+ function portFrom(args) {
992
+ const index = args.indexOf("--port");
993
+ if (index === -1) return void 0;
994
+ const value = Number(args[index + 1]);
995
+ return Number.isInteger(value) && value > 0 && value < 65536 ? value : void 0;
996
+ }
997
+
998
+ // packages/core/dist/core.js
999
+ function right(rect) {
1000
+ return rect.x + rect.w;
1001
+ }
1002
+ function bottom(rect) {
1003
+ return rect.y + rect.h;
1004
+ }
1005
+ function centerX(rect) {
1006
+ return rect.x + rect.w / 2;
1007
+ }
1008
+ function centerY(rect) {
1009
+ return rect.y + rect.h / 2;
1010
+ }
1011
+ var EDGES = [
1012
+ { axis: "x", edge: "left", value: (rect) => rect.x },
1013
+ { axis: "x", edge: "right", value: right },
1014
+ { axis: "x", edge: "center", value: centerX },
1015
+ { axis: "y", edge: "top", value: (rect) => rect.y },
1016
+ { axis: "y", edge: "bottom", value: bottom },
1017
+ { axis: "y", edge: "center", value: centerY }
1018
+ ];
1019
+ function parseColor(value) {
1020
+ const text = value.trim().toLowerCase();
1021
+ if (text === "" || text === "transparent") return null;
1022
+ if (text.startsWith("#")) return parseHex(text);
1023
+ const match = text.match(/^rgba?\(([^)]+)\)$/);
1024
+ if (match === null) return null;
1025
+ const body = match[1];
1026
+ if (body === void 0) return null;
1027
+ const parts = body.split(/[,/\s]+/).filter((part) => part !== "");
1028
+ if (parts.length < 3) return null;
1029
+ const [r, g, b, a] = parts;
1030
+ return {
1031
+ r: Number(r),
1032
+ g: Number(g),
1033
+ b: Number(b),
1034
+ a: a === void 0 ? 1 : parseAlpha(a)
1035
+ };
1036
+ }
1037
+ function parseAlpha(value) {
1038
+ if (value.endsWith("%")) return Number(value.slice(0, -1)) / 100;
1039
+ return Number(value);
1040
+ }
1041
+ function parseHex(text) {
1042
+ const hex = text.slice(1);
1043
+ if (hex.length === 3 || hex.length === 4) {
1044
+ const [r, g, b, a] = hex.split("");
1045
+ if (r === void 0 || g === void 0 || b === void 0) return null;
1046
+ return {
1047
+ r: Number.parseInt(r + r, 16),
1048
+ g: Number.parseInt(g + g, 16),
1049
+ b: Number.parseInt(b + b, 16),
1050
+ a: a === void 0 ? 1 : Number.parseInt(a + a, 16) / 255
1051
+ };
1052
+ }
1053
+ if (hex.length === 6 || hex.length === 8) {
1054
+ return {
1055
+ r: Number.parseInt(hex.slice(0, 2), 16),
1056
+ g: Number.parseInt(hex.slice(2, 4), 16),
1057
+ b: Number.parseInt(hex.slice(4, 6), 16),
1058
+ a: hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) / 255 : 1
1059
+ };
1060
+ }
1061
+ return null;
1062
+ }
1063
+ function blend(source, destination) {
1064
+ const a = source.a + destination.a * (1 - source.a);
1065
+ if (a === 0) return { r: 0, g: 0, b: 0, a: 0 };
1066
+ const mix = (src, dst) => (src * source.a + dst * destination.a * (1 - source.a)) / a;
1067
+ return {
1068
+ r: mix(source.r, destination.r),
1069
+ g: mix(source.g, destination.g),
1070
+ b: mix(source.b, destination.b),
1071
+ a
1072
+ };
1073
+ }
1074
+ function luminance(color) {
1075
+ const channel = (value) => {
1076
+ const s = value / 255;
1077
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
1078
+ };
1079
+ return 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b);
1080
+ }
1081
+ function contrastRatio(a, b) {
1082
+ const first = luminance(a);
1083
+ const second = luminance(b);
1084
+ const lighter = Math.max(first, second);
1085
+ const darker = Math.min(first, second);
1086
+ return (lighter + 0.05) / (darker + 0.05);
1087
+ }
1088
+ var SPACING_SCALE = 4;
1089
+ function defineRule(id, run2) {
1090
+ return Object.assign(run2, { id });
1091
+ }
1092
+ function issue(input) {
1093
+ const suffix = input.key === void 0 ? "" : `:${input.key}`;
1094
+ return {
1095
+ id: `${input.rule}:${input.selector}${suffix}`,
1096
+ selector: input.selector,
1097
+ rule: input.rule,
1098
+ severity: input.severity,
1099
+ actual: input.actual,
1100
+ expected: input.expected,
1101
+ autoFix: input.autoFix
1102
+ };
1103
+ }
1104
+ var TOLERANCE2 = 2;
1105
+ var WINDOW = 4;
1106
+ var alignmentNearMiss = defineRule("alignment-near-miss", (graph) => {
1107
+ const issues = [];
1108
+ for (const spec of EDGES) {
1109
+ const items = graph.nodes.filter((node) => node.parent !== null).map((node) => ({ node, value: spec.value(node.rect) })).sort((a, b) => a.value - b.value);
1110
+ let start = 0;
1111
+ for (let i = 1; i <= items.length; i++) {
1112
+ const previous = items[i - 1];
1113
+ const current2 = items[i];
1114
+ const breaks = current2 === void 0 || previous === void 0 || current2.value - previous.value > WINDOW;
1115
+ if (!breaks) continue;
1116
+ const cluster = items.slice(start, i);
1117
+ start = i;
1118
+ if (cluster.length < 2) continue;
1119
+ const first = cluster[0];
1120
+ const last = cluster[cluster.length - 1];
1121
+ if (first === void 0 || last === void 0) continue;
1122
+ if (last.value - first.value <= TOLERANCE2) continue;
1123
+ const target = cluster[Math.floor(cluster.length / 2)];
1124
+ if (target === void 0) continue;
1125
+ let worst = cluster[0];
1126
+ if (worst === void 0) continue;
1127
+ for (const item of cluster) {
1128
+ if (Math.abs(item.value - target.value) > Math.abs(worst.value - target.value)) worst = item;
1129
+ }
1130
+ issues.push(
1131
+ issue({
1132
+ rule: "alignment-near-miss",
1133
+ selector: worst.node.id,
1134
+ key: spec.edge,
1135
+ severity: "P2",
1136
+ actual: `${Math.round(worst.value * 10) / 10}px`,
1137
+ expected: `${Math.round(target.value * 10) / 10}px`,
1138
+ autoFix: false
1139
+ })
1140
+ );
1141
+ }
1142
+ }
1143
+ return issues;
1144
+ });
1145
+ var WHITE = { r: 255, g: 255, b: 255, a: 1 };
1146
+ var NORMAL_MINIMUM = 4.5;
1147
+ var LARGE_MINIMUM = 3;
1148
+ var contrastMinimum = defineRule("contrast", (graph) => {
1149
+ const byId = new Map(graph.nodes.map((node) => [node.id, node]));
1150
+ const issues = [];
1151
+ for (const node of graph.nodes) {
1152
+ if (node.text === "") continue;
1153
+ const foreground = parseColor(node.styles.color);
1154
+ if (foreground === null) continue;
1155
+ const background = backgroundOf(node, byId);
1156
+ const ratio = contrastRatio(blend(foreground, background), background);
1157
+ const minimum = isLargeText(node) ? LARGE_MINIMUM : NORMAL_MINIMUM;
1158
+ if (ratio >= minimum) continue;
1159
+ issues.push(
1160
+ issue({
1161
+ rule: "contrast",
1162
+ selector: node.id,
1163
+ severity: "P1",
1164
+ actual: `${Math.round(ratio * 100) / 100}:1`,
1165
+ expected: `>= ${minimum}:1`,
1166
+ autoFix: false
1167
+ })
1168
+ );
1169
+ }
1170
+ return issues;
1171
+ });
1172
+ function isLargeText(node) {
1173
+ const size = Number.parseFloat(node.styles.fontSize);
1174
+ const weight = Number.parseInt(node.styles.fontWeight, 10);
1175
+ if (!Number.isFinite(size)) return false;
1176
+ return size >= 24 || size >= 18.66 && weight >= 700;
1177
+ }
1178
+ function backgroundOf(node, byId) {
1179
+ const layers = [];
1180
+ let current2 = node;
1181
+ while (current2 !== void 0) {
1182
+ const color = parseColor(current2.styles.backgroundColor);
1183
+ if (color !== null && color.a > 0) {
1184
+ layers.push(color);
1185
+ if (color.a === 1) break;
1186
+ }
1187
+ current2 = current2.parent === null ? void 0 : byId.get(current2.parent);
1188
+ }
1189
+ let background = WHITE;
1190
+ for (let i = layers.length - 1; i >= 0; i--) {
1191
+ const layer = layers[i];
1192
+ if (layer === void 0) continue;
1193
+ background = blend(layer, background);
1194
+ }
1195
+ return background;
1196
+ }
1197
+ var SLACK = 1;
1198
+ var overflowClipped = defineRule("overflow", (graph) => {
1199
+ const issues = [];
1200
+ for (const node of graph.nodes) {
1201
+ if (node.styles.display.startsWith("inline")) continue;
1202
+ if (node.box.clientW === 0 && node.box.clientH === 0) continue;
1203
+ if (node.box.scrollW > node.box.clientW + SLACK) {
1204
+ issues.push(
1205
+ issue({
1206
+ rule: "overflow",
1207
+ selector: node.id,
1208
+ key: "x",
1209
+ severity: "P2",
1210
+ actual: `${node.box.scrollW}px content in ${node.box.clientW}px`,
1211
+ expected: "content fits horizontally",
1212
+ autoFix: false
1213
+ })
1214
+ );
1215
+ }
1216
+ if (node.box.scrollH > node.box.clientH + SLACK) {
1217
+ issues.push(
1218
+ issue({
1219
+ rule: "overflow",
1220
+ selector: node.id,
1221
+ key: "y",
1222
+ severity: "P2",
1223
+ actual: `${node.box.scrollH}px content in ${node.box.clientH}px`,
1224
+ expected: "content fits vertically",
1225
+ autoFix: false
1226
+ })
1227
+ );
1228
+ }
1229
+ }
1230
+ return issues;
1231
+ });
1232
+ var MIN_SAMPLES = 4;
1233
+ var radiusConsistency = defineRule("radius-consistency", (graph) => {
1234
+ const groups = /* @__PURE__ */ new Map();
1235
+ for (const node of graph.nodes) {
1236
+ const radius = normalizeRadius(node.styles.borderRadius);
1237
+ if (radius === null) continue;
1238
+ const group = groups.get(radius);
1239
+ if (group) group.push(node);
1240
+ else groups.set(radius, [node]);
1241
+ }
1242
+ if (groups.size < 2) return [];
1243
+ const total = [...groups.values()].reduce((sum, group) => sum + group.length, 0);
1244
+ if (total < MIN_SAMPLES) return [];
1245
+ const ranked = [...groups.entries()].sort((a, b) => b[1].length - a[1].length);
1246
+ const expected = ranked[0];
1247
+ if (expected === void 0) return [];
1248
+ const [expectedValue, expectedNodes] = expected;
1249
+ const issues = [];
1250
+ for (const [value, nodes] of ranked.slice(1)) {
1251
+ if (nodes.length >= expectedNodes.length) continue;
1252
+ for (const node of nodes) {
1253
+ if (isPill(node)) continue;
1254
+ issues.push(
1255
+ issue({
1256
+ rule: "radius-consistency",
1257
+ selector: node.id,
1258
+ severity: "P3",
1259
+ actual: value,
1260
+ expected: expectedValue,
1261
+ autoFix: true
1262
+ })
1263
+ );
1264
+ }
1265
+ }
1266
+ return issues;
1267
+ });
1268
+ function normalizeRadius(value) {
1269
+ const text = value.trim();
1270
+ if (text === "" || text === "0px" || text === "0") return null;
1271
+ const parts = text.split(/\s+/);
1272
+ const unique = [...new Set(parts)];
1273
+ if (unique.length === 1) return unique[0] ?? null;
1274
+ return parts.join(" ");
1275
+ }
1276
+ function isPill(node) {
1277
+ const text = node.styles.borderRadius;
1278
+ if (text.includes("%")) return true;
1279
+ const radius = Number.parseFloat(text);
1280
+ if (!Number.isFinite(radius)) return false;
1281
+ return radius >= Math.min(node.rect.w, node.rect.h) / 2;
1282
+ }
1283
+ var spacingScale = defineRule("spacing-scale", (graph) => {
1284
+ const issues = [];
1285
+ for (const gap of graph.gaps) {
1286
+ if (gap.kind !== "sibling") continue;
1287
+ const actual = Math.round(gap.distance);
1288
+ if (actual <= 0 || actual % SPACING_SCALE === 0) continue;
1289
+ issues.push(
1290
+ issue({
1291
+ rule: "spacing-scale",
1292
+ selector: gap.a,
1293
+ key: `${gap.axis}:${gap.b}`,
1294
+ severity: "P2",
1295
+ actual: `${actual}px`,
1296
+ expected: `${Math.round(actual / SPACING_SCALE) * SPACING_SCALE}px`,
1297
+ autoFix: true
1298
+ })
1299
+ );
1300
+ }
1301
+ return issues;
1302
+ });
1303
+ var defaultRules = [
1304
+ spacingScale,
1305
+ alignmentNearMiss,
1306
+ radiusConsistency,
1307
+ overflowClipped,
1308
+ contrastMinimum
1309
+ ];
1310
+ var RULE_IDS = defaultRules.map((rule) => rule.id);
1311
+ var LAYERS = ["grid", "guide", "gap", "issue", "hover"];
1312
+
1313
+ // packages/cli/src/drivers/extension.ts
1314
+ var ExtensionDriver = class {
1315
+ #hub;
1316
+ #target;
1317
+ constructor(hub) {
1318
+ this.#hub = hub;
1319
+ }
1320
+ async open(url) {
1321
+ this.#target = url;
1322
+ if (this.#hub.find(url) !== void 0) return;
1323
+ if (!this.#hub.hasClient()) throw new AwaitingUserError(url);
1324
+ const page = await this.#hub.requestAny("attach");
1325
+ if (!sameUrl(page.url, url)) throw new MismatchError(url, page.url);
1326
+ this.#target = page.url;
1327
+ }
1328
+ async waitForPage(url, timeoutMs) {
1329
+ const outcome = await this.#hub.waitForPage(url, timeoutMs);
1330
+ if (outcome.status === "timeout") return { status: "timeout" };
1331
+ if (outcome.status === "mismatch") return { status: "mismatch", url: outcome.page.url };
1332
+ this.#target = outcome.page.url;
1333
+ return { status: "ready", url: outcome.page.url };
1334
+ }
1335
+ async graph() {
1336
+ return this.#request("graph");
1337
+ }
1338
+ async issues(rules) {
1339
+ const graph = await this.graph();
1340
+ if (rules === void 0 || rules.length === 0) return graph.issues;
1341
+ const wanted = new Set(rules);
1342
+ return graph.issues.filter((issue2) => wanted.has(issue2.rule));
1343
+ }
1344
+ async measure(selector) {
1345
+ return this.#request("measure", { selector });
1346
+ }
1347
+ async screenshot(options = {}) {
1348
+ return decode(await this.#request("screenshot", options));
1349
+ }
1350
+ async annotate(options = {}) {
1351
+ return decode(await this.#request("annotate", options));
1352
+ }
1353
+ async overlay(options) {
1354
+ const result = await this.#request("overlay", options);
1355
+ if (result === void 0) {
1356
+ throw new ToolError(
1357
+ "the extension did not report the overlay state. Reload the litedesign extension at chrome://extensions, then try again."
1358
+ );
1359
+ }
1360
+ return result;
1361
+ }
1362
+ async close() {
1363
+ }
1364
+ #request(action, payload) {
1365
+ const page = this.#require();
1366
+ return this.#hub.request(page.tabId, action, payload);
1367
+ }
1368
+ #require() {
1369
+ if (this.#target === void 0) {
1370
+ throw new ToolError("no page is open. Call inspect with a url first");
1371
+ }
1372
+ const page = this.#hub.find(this.#target);
1373
+ if (page === void 0) {
1374
+ throw new ToolError("the page is no longer connected. Open it and call inspect again.");
1375
+ }
1376
+ return page;
1377
+ }
1378
+ };
1379
+ function decode(payload) {
1380
+ if (typeof payload?.data !== "string" || payload.data === "") {
1381
+ throw new ToolError("the extension returned an empty screenshot");
1382
+ }
1383
+ return Buffer.from(payload.data, "base64");
1384
+ }
1385
+
1386
+ // packages/cli/src/drivers/playwright.ts
1387
+ import { existsSync as existsSync5 } from "node:fs";
1388
+ import path4 from "node:path";
1389
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1390
+ var DEFAULT_VIEWPORT = { width: 1280, height: 800 };
1391
+ var PlaywrightDriver = class {
1392
+ #playwright;
1393
+ #corePath;
1394
+ #browser;
1395
+ #context;
1396
+ #page;
1397
+ constructor(playwright, options = {}) {
1398
+ this.#playwright = playwright;
1399
+ this.#corePath = resolveCorePath(options.corePath);
1400
+ }
1401
+ async open(url, viewport) {
1402
+ const page = await this.#ensurePage();
1403
+ await page.setViewportSize(
1404
+ viewport === void 0 ? DEFAULT_VIEWPORT : { width: viewport.w, height: viewport.h }
1405
+ );
1406
+ await page.goto(url, { waitUntil: "load" });
1407
+ await this.#inject(page);
1408
+ }
1409
+ async graph() {
1410
+ const page = await this.#requirePage();
1411
+ await this.#inject(page);
1412
+ return page.evaluate(() => {
1413
+ const core = window.__litedesign;
1414
+ return core.buildGraph(document.body);
1415
+ });
1416
+ }
1417
+ async issues(rules) {
1418
+ const graph = await this.graph();
1419
+ if (rules === void 0 || rules.length === 0) return graph.issues;
1420
+ const wanted = new Set(rules);
1421
+ return graph.issues.filter((issue2) => wanted.has(issue2.rule));
1422
+ }
1423
+ async measure(selector) {
1424
+ const page = await this.#requirePage();
1425
+ await this.#inject(page);
1426
+ const found = await page.evaluate((target) => {
1427
+ const core = window.__litedesign;
1428
+ const graph = core.buildGraph(document.body);
1429
+ const node = core.resolveNode(graph, target);
1430
+ if (node === null) return null;
1431
+ const id = node.id;
1432
+ const gaps = graph.gaps.filter((gap) => gap.a === id || gap.b === id).map((gap) => ({
1433
+ to: gap.a === id ? gap.b : gap.a,
1434
+ axis: gap.axis,
1435
+ distance: gap.distance,
1436
+ kind: gap.kind
1437
+ }));
1438
+ return { node, gaps };
1439
+ }, selector);
1440
+ if (found === null) {
1441
+ throw new ToolError(`selector "${selector}" did not match any element on the page`);
1442
+ }
1443
+ return {
1444
+ id: found.node.id,
1445
+ selector: found.node.selector,
1446
+ tag: found.node.tag,
1447
+ rect: found.node.rect,
1448
+ styles: found.node.styles,
1449
+ gaps: found.gaps
1450
+ };
1451
+ }
1452
+ async screenshot(options = {}) {
1453
+ const page = await this.#requirePage();
1454
+ if (options.selector !== void 0) {
1455
+ const locator = page.locator(options.selector).first();
1456
+ if (await locator.count() === 0) {
1457
+ throw new ToolError(`selector "${options.selector}" did not match any element on the page`);
1458
+ }
1459
+ return locator.screenshot({ type: "png" });
1460
+ }
1461
+ return page.screenshot({ type: "png", fullPage: options.fullPage ?? false });
1462
+ }
1463
+ async annotate(options = {}) {
1464
+ const page = await this.#requirePage();
1465
+ await this.#inject(page);
1466
+ if (options.selector !== void 0) {
1467
+ const locator = page.locator(options.selector).first();
1468
+ if (await locator.count() === 0) {
1469
+ throw new ToolError(`selector "${options.selector}" did not match any element on the page`);
1470
+ }
1471
+ await locator.scrollIntoViewIfNeeded();
1472
+ }
1473
+ await page.evaluate(
1474
+ (input) => {
1475
+ const target = window;
1476
+ target.__litedesign_handle__?.destroy();
1477
+ let graph = target.__litedesign.buildGraph(document.body);
1478
+ if (input.rules !== void 0 && input.rules.length > 0) {
1479
+ const wanted = new Set(input.rules);
1480
+ graph = {
1481
+ ...graph,
1482
+ issues: graph.issues.filter((issue2) => wanted.has(issue2.rule))
1483
+ };
1484
+ }
1485
+ target.__litedesign_handle__ = target.__litedesign.render(graph, { layers: input.layers });
1486
+ },
1487
+ { layers: ["grid", "guide", "gap", "issue"], rules: options.rules }
1488
+ );
1489
+ await page.evaluate(
1490
+ () => new Promise((resolve) => {
1491
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
1492
+ })
1493
+ );
1494
+ return page.screenshot({ type: "png" });
1495
+ }
1496
+ async overlay(options) {
1497
+ const page = await this.#requirePage();
1498
+ await this.#inject(page);
1499
+ const layers = options.enabled ? options.layers ?? [...LAYERS] : [];
1500
+ await page.evaluate(
1501
+ (input) => {
1502
+ const target = window;
1503
+ target.__litedesign_handle__?.destroy();
1504
+ target.__litedesign_handle__ = void 0;
1505
+ if (!input.enabled) return;
1506
+ const graph = target.__litedesign.buildGraph(document.body);
1507
+ target.__litedesign_handle__ = target.__litedesign.render(graph, { layers: input.layers });
1508
+ },
1509
+ { enabled: options.enabled, layers }
1510
+ );
1511
+ return { enabled: options.enabled, layers };
1512
+ }
1513
+ async close() {
1514
+ await this.#context?.close().catch(() => void 0);
1515
+ await this.#browser?.close().catch(() => void 0);
1516
+ this.#context = void 0;
1517
+ this.#browser = void 0;
1518
+ this.#page = void 0;
1519
+ }
1520
+ async #ensurePage() {
1521
+ if (this.#page !== void 0 && !this.#page.isClosed()) return this.#page;
1522
+ const browser = await this.#ensureBrowser();
1523
+ const context = await browser.newContext({
1524
+ viewport: DEFAULT_VIEWPORT,
1525
+ // Injected measurement code must run even on pages with a strict CSP.
1526
+ bypassCSP: true
1527
+ });
1528
+ const page = await context.newPage();
1529
+ this.#context = context;
1530
+ this.#page = page;
1531
+ return page;
1532
+ }
1533
+ async #ensureBrowser() {
1534
+ if (this.#browser?.isConnected() === true) return this.#browser;
1535
+ let last;
1536
+ for (const options of launchCandidates(this.#playwright)) {
1537
+ try {
1538
+ this.#browser = await this.#playwright.chromium.launch({
1539
+ ...options,
1540
+ args: ["--no-sandbox"]
1541
+ });
1542
+ return this.#browser;
1543
+ } catch (error) {
1544
+ last = error;
1545
+ }
1546
+ }
1547
+ const detail = last instanceof Error ? `: ${last.message}` : "";
1548
+ throw new ToolError(`could not launch Chrome for Playwright${detail}`);
1549
+ }
1550
+ async #requirePage() {
1551
+ if (this.#page === void 0 || this.#page.isClosed()) {
1552
+ throw new ToolError("no page is open. Call inspect with a url first");
1553
+ }
1554
+ return this.#page;
1555
+ }
1556
+ async #inject(page) {
1557
+ const ready = await page.evaluate(
1558
+ () => typeof window.__litedesign !== "undefined"
1559
+ );
1560
+ if (ready) return;
1561
+ try {
1562
+ await page.addScriptTag({ path: this.#corePath });
1563
+ } catch (error) {
1564
+ const detail = error instanceof Error ? `: ${error.message}` : "";
1565
+ throw new ToolError(`could not inject the measurement core${detail}`);
1566
+ }
1567
+ }
1568
+ };
1569
+ function launchCandidates(playwright) {
1570
+ const candidates = [];
1571
+ if (process.env.CHROME_PATH !== void 0) {
1572
+ candidates.push({ executablePath: process.env.CHROME_PATH });
1573
+ }
1574
+ candidates.push({ channel: "chrome" });
1575
+ const bundled = playwright.chromium.executablePath();
1576
+ if (existsSync5(bundled)) candidates.push({ executablePath: bundled });
1577
+ for (const candidate of [
1578
+ "/usr/bin/google-chrome",
1579
+ "/usr/bin/chromium",
1580
+ "/usr/bin/chromium-browser"
1581
+ ]) {
1582
+ if (existsSync5(candidate)) candidates.push({ executablePath: candidate });
1583
+ }
1584
+ return candidates;
1585
+ }
1586
+ function resolveCorePath(explicit) {
1587
+ const candidates = [
1588
+ explicit,
1589
+ process.env.LITEDESIGN_CORE,
1590
+ fileURLToPath2(new URL("./extension/core.iife.js", import.meta.url)),
1591
+ path4.resolve(
1592
+ path4.dirname(fileURLToPath2(import.meta.url)),
1593
+ "../../../../core/dist/core.iife.js"
1594
+ )
1595
+ ].filter((value) => value !== void 0);
1596
+ for (const candidate of candidates) {
1597
+ if (existsSync5(candidate)) return candidate;
1598
+ }
1599
+ throw new ToolError(
1600
+ "measurement core not found. Run `litedesign setup` or set LITEDESIGN_CORE to core.iife.js"
1601
+ );
1602
+ }
1603
+
1604
+ // packages/cli/src/drivers/index.ts
1605
+ async function loadPlaywright() {
1606
+ try {
1607
+ return await import("playwright-core");
1608
+ } catch {
1609
+ return null;
1610
+ }
1611
+ }
1612
+ async function detectPlaywright() {
1613
+ return await loadPlaywright() !== null;
1614
+ }
1615
+ async function selectDriver(mode) {
1616
+ if (mode === "extension") return extensionDriver();
1617
+ const playwright = await loadPlaywright();
1618
+ if (playwright !== null) return new PlaywrightDriver(playwright);
1619
+ if (mode === "playwright") {
1620
+ throw new ToolError(
1621
+ "Playwright is not installed. Install it with `npm i -D playwright` (or `pnpm add -D playwright`) and restart the MCP server."
1622
+ );
1623
+ }
1624
+ return extensionDriver();
1625
+ }
1626
+ function extensionDriver() {
1627
+ const hub = getHub();
1628
+ if (hub === null) {
1629
+ const reason = getHubError() ?? "the WebSocket hub is not running";
1630
+ throw new ToolError(
1631
+ `the extension bridge is unavailable: ${reason}. Free the port, or install Playwright.`
1632
+ );
1633
+ }
1634
+ return new ExtensionDriver(hub);
1635
+ }
1636
+
1637
+ // packages/cli/src/mcp/result.ts
1638
+ function textResult(value) {
1639
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
1640
+ }
1641
+ function imageResult(value, image) {
1642
+ return {
1643
+ content: [
1644
+ { type: "text", text: JSON.stringify(value, null, 2) },
1645
+ { type: "image", data: image.toString("base64"), mimeType: "image/png" }
1646
+ ]
1647
+ };
1648
+ }
1649
+ function errorResult(error) {
1650
+ const message = error instanceof Error && error.message !== "" ? error.message : typeof error === "string" && error !== "" ? error : "unexpected error";
1651
+ return { isError: true, content: [{ type: "text", text: message }] };
1652
+ }
1653
+
1654
+ // packages/cli/src/mcp/session.ts
1655
+ var session = null;
1656
+ async function openPage(mode, url, viewport) {
1657
+ if (session === null) {
1658
+ session = { driver: await selectDriver(mode), url, ready: false };
1659
+ }
1660
+ session.url = url;
1661
+ await session.driver.open(url, viewport);
1662
+ session.ready = true;
1663
+ return session.driver;
1664
+ }
1665
+ function requirePage() {
1666
+ if (session === null || !session.ready) {
1667
+ throw new ToolError("no page is open. Call inspect with a url first");
1668
+ }
1669
+ return session.driver;
1670
+ }
1671
+ async function waitForPage(timeoutMs) {
1672
+ if (session === null || session.url === "") {
1673
+ throw new ToolError("no page is requested. Call inspect with a url first");
1674
+ }
1675
+ const expected = session.url;
1676
+ const driver = session.driver;
1677
+ if (driver.waitForPage === void 0) {
1678
+ return { status: "ready", url: expected, expected };
1679
+ }
1680
+ const result = await driver.waitForPage(expected, timeoutMs);
1681
+ if (result.status === "ready") {
1682
+ session.ready = true;
1683
+ return { status: "ready", url: result.url, expected };
1684
+ }
1685
+ if (result.status === "timeout") return { status: "timeout", expected };
1686
+ return { status: "mismatch", url: result.url, expected };
1687
+ }
1688
+ async function closeSession() {
1689
+ if (session === null) return;
1690
+ const current2 = session;
1691
+ session = null;
1692
+ await current2.driver.close();
1693
+ }
1694
+
1695
+ // packages/cli/src/mcp/tools/await-page.ts
1696
+ var DEFAULT_TIMEOUT2 = 12e4;
1697
+ var MAX_TIMEOUT = 6e5;
1698
+ async function awaitPageTool(input) {
1699
+ const timeoutMs = Math.min(
1700
+ Math.max(Math.round(input.timeoutMs ?? DEFAULT_TIMEOUT2), 1e3),
1701
+ MAX_TIMEOUT
1702
+ );
1703
+ const result = await waitForPage(timeoutMs);
1704
+ if (result.status === "ready") {
1705
+ return textResult({
1706
+ status: "ready",
1707
+ url: result.url,
1708
+ message: "The page is connected. Call inspect to read it."
1709
+ });
1710
+ }
1711
+ if (result.status === "timeout") {
1712
+ return textResult({
1713
+ status: "timeout",
1714
+ url: result.expected,
1715
+ message: handoffMessage(result.expected)
1716
+ });
1717
+ }
1718
+ return textResult({
1719
+ status: "mismatch",
1720
+ expected: result.expected,
1721
+ actual: result.url,
1722
+ message: mismatchMessage(result.expected, result.url)
1723
+ });
1724
+ }
1725
+
1726
+ // packages/cli/src/mcp/format.ts
1727
+ var MAX_TREE_LINES = 200;
1728
+ var MAX_DEPTH_INDENT = 12;
1729
+ var MAX_TEXT = 48;
1730
+ function formatTree(graph, maxLines = MAX_TREE_LINES) {
1731
+ const lines = [];
1732
+ for (const node of graph.nodes) {
1733
+ if (lines.length >= maxLines) break;
1734
+ const indent = " ".repeat(Math.min(node.depth, MAX_DEPTH_INDENT));
1735
+ const size = `${Math.round(node.rect.w)}\xD7${Math.round(node.rect.h)}`;
1736
+ const text = node.text === "" ? "" : ` ${clip(node.text)}`;
1737
+ lines.push(`${indent}${node.tag} ${size}${text}`);
1738
+ }
1739
+ if (graph.nodes.length > maxLines) {
1740
+ lines.push(`\u2026 ${graph.nodes.length - maxLines} more nodes`);
1741
+ }
1742
+ return lines.join("\n");
1743
+ }
1744
+ function issuePayload(issues, limit) {
1745
+ return {
1746
+ issues: issues.slice(0, limit).map((issue2) => ({
1747
+ selector: issue2.selector,
1748
+ rule: issue2.rule,
1749
+ severity: issue2.severity,
1750
+ actual: issue2.actual,
1751
+ expected: issue2.expected,
1752
+ autoFix: issue2.autoFix
1753
+ })),
1754
+ total: issues.length,
1755
+ truncated: issues.length > limit
1756
+ };
1757
+ }
1758
+ function pngSize(buffer) {
1759
+ if (buffer.length < 24 || buffer.toString("ascii", 1, 4) !== "PNG") {
1760
+ return { width: 0, height: 0 };
1761
+ }
1762
+ return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
1763
+ }
1764
+ function clip(text) {
1765
+ return text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT - 1)}\u2026` : text;
1766
+ }
1767
+
1768
+ // packages/cli/src/mcp/rules.ts
1769
+ function validateRules(rules) {
1770
+ const unknown = rules.filter((rule) => !RULE_IDS.includes(rule));
1771
+ if (unknown.length > 0) {
1772
+ const label = unknown.length === 1 ? "rule" : "rules";
1773
+ throw new ToolError(
1774
+ `unknown ${label}: ${unknown.join(", ")}. Valid rules: ${RULE_IDS.join(", ")}`
1775
+ );
1776
+ }
1777
+ return rules.length === 0 ? void 0 : rules;
1778
+ }
1779
+
1780
+ // packages/cli/src/mcp/tools/find-issues.ts
1781
+ var DEFAULT_LIMIT = 50;
1782
+ var MAX_LIMIT = 200;
1783
+ async function findIssuesTool(input) {
1784
+ const rules = validateRules(input.rules ?? []);
1785
+ const limit = clamp(input.limit ?? DEFAULT_LIMIT, 1, MAX_LIMIT);
1786
+ const issues = await requirePage().issues(rules);
1787
+ return textResult(issuePayload(issues, limit));
1788
+ }
1789
+ function clamp(value, min, max) {
1790
+ return Math.min(Math.max(Math.round(value), min), max);
1791
+ }
1792
+
1793
+ // packages/cli/src/mcp/tools/inspect.ts
1794
+ async function inspectTool(input) {
1795
+ const config = await readConfig();
1796
+ let driver;
1797
+ try {
1798
+ driver = await openPage(config.mode, input.url, input.viewport);
1799
+ } catch (error) {
1800
+ if (error instanceof AwaitingUserError) {
1801
+ return textResult({
1802
+ status: "awaiting_user",
1803
+ url: input.url,
1804
+ next: "await_page",
1805
+ message: handoffMessage(input.url)
1806
+ });
1807
+ }
1808
+ if (error instanceof MismatchError) {
1809
+ return textResult({
1810
+ status: "mismatch",
1811
+ expected: error.expected,
1812
+ actual: error.actual,
1813
+ next: "await_page",
1814
+ message: error.message
1815
+ });
1816
+ }
1817
+ throw error;
1818
+ }
1819
+ const graph = await driver.graph();
1820
+ const payload = {
1821
+ status: "ready",
1822
+ url: graph.url,
1823
+ viewport: graph.viewport,
1824
+ summary: {
1825
+ nodes: graph.nodes.length,
1826
+ issues: graph.issues.length,
1827
+ truncated: graph.truncated
1828
+ },
1829
+ tree: formatTree(graph)
1830
+ };
1831
+ if (input.screenshot !== true) return textResult(payload);
1832
+ const image = await driver.screenshot();
1833
+ return imageResult(payload, image);
1834
+ }
1835
+
1836
+ // packages/cli/src/mcp/tools/measure.ts
1837
+ async function measureTool(input) {
1838
+ const measured = await requirePage().measure(input.selector);
1839
+ return textResult(measured);
1840
+ }
1841
+
1842
+ // packages/cli/src/mcp/tools/overlay.ts
1843
+ async function overlayTool(input) {
1844
+ const driver = requirePage();
1845
+ const enabled = input.enabled !== false;
1846
+ const layers = parseLayers(input.layers);
1847
+ const result = await driver.overlay({ enabled, layers });
1848
+ return textResult({
1849
+ status: result.enabled ? "shown" : "hidden",
1850
+ layers: result.layers
1851
+ });
1852
+ }
1853
+ function parseLayers(requested) {
1854
+ if (requested === void 0 || requested.length === 0) return void 0;
1855
+ const valid = new Set(LAYERS);
1856
+ const unknown = requested.filter((name) => !valid.has(name));
1857
+ if (unknown.length > 0) {
1858
+ throw new ToolError(`unknown layer: ${unknown.join(", ")}. Valid layers: ${LAYERS.join(", ")}`);
1859
+ }
1860
+ return requested;
1861
+ }
1862
+
1863
+ // packages/cli/src/mcp/tools/screenshot.ts
1864
+ async function screenshotTool(input) {
1865
+ const image = await requirePage().screenshot(input);
1866
+ return imageResult({ ...pngSize(image), bytes: image.length }, image);
1867
+ }
1868
+
1869
+ // packages/cli/src/mcp/tools/screenshot-annotated.ts
1870
+ async function screenshotAnnotatedTool(input) {
1871
+ const rules = validateRules(input.rules ?? []);
1872
+ const image = await requirePage().annotate({
1873
+ selector: input.selector,
1874
+ rules
1875
+ });
1876
+ return imageResult({ ...pngSize(image), bytes: image.length }, image);
1877
+ }
1878
+
1879
+ // packages/cli/src/mcp/server.ts
1880
+ async function guard(run2) {
1881
+ try {
1882
+ return await run2();
1883
+ } catch (error) {
1884
+ if (error instanceof ToolError) return errorResult(error);
1885
+ console.error("[litedesign] unexpected tool error:", error);
1886
+ return errorResult(error);
1887
+ }
1888
+ }
1889
+ async function runMcpServer() {
1890
+ const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
1891
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
1892
+ const { z } = await import("zod");
1893
+ const config = await readConfig();
1894
+ try {
1895
+ setHub(await ensureBridge(config.port));
1896
+ } catch (error) {
1897
+ const reason = error instanceof Error ? error.message : String(error);
1898
+ setHubError(reason);
1899
+ console.error(`[litedesign] extension bridge disabled: ${reason}`);
1900
+ }
1901
+ const server = new McpServer({ name: "litedesign", version: VERSION });
1902
+ const viewport = z.object({
1903
+ w: z.number().int().positive().describe("Viewport width in CSS pixels"),
1904
+ h: z.number().int().positive().describe("Viewport height in CSS pixels")
1905
+ }).optional();
1906
+ server.registerTool(
1907
+ "capabilities",
1908
+ {
1909
+ title: "Capabilities",
1910
+ description: "Server version, available drivers, active mode, connected extension pages, and valid rule names.",
1911
+ inputSchema: {}
1912
+ },
1913
+ async () => {
1914
+ const playwright = await detectPlaywright();
1915
+ const hub = getHub();
1916
+ return textResult({
1917
+ version: VERSION,
1918
+ drivers: { playwright, extension: hub !== null },
1919
+ mode: config.mode,
1920
+ port: config.port,
1921
+ rules: RULE_IDS,
1922
+ extension: {
1923
+ connected: hub?.clientCount() ?? 0,
1924
+ pages: hub?.pages() ?? [],
1925
+ error: getHubError()
1926
+ }
1927
+ });
1928
+ }
1929
+ );
1930
+ server.registerTool(
1931
+ "inspect",
1932
+ {
1933
+ title: "Inspect page",
1934
+ description: 'Open a page and return a tree summary. Call this before measure, find_issues, overlay, screenshot, or screenshot_annotated. No image by default: the numbers the agent needs come from the DOM, so set screenshot to true only when you really need to see the page. With the extension driver this attaches to the active tab by itself; returns "awaiting_user" when no extension is connected and "mismatch" when the active tab is a different page.',
1935
+ inputSchema: {
1936
+ url: z.string().url(),
1937
+ viewport,
1938
+ screenshot: z.boolean().optional().describe(
1939
+ "Set true to also return a viewport PNG. Off by default: ask for it only when visual context is essential."
1940
+ )
1941
+ }
1942
+ },
1943
+ async (input) => guard(() => inspectTool(input))
1944
+ );
1945
+ server.registerTool(
1946
+ "await_page",
1947
+ {
1948
+ title: "Await page",
1949
+ description: "Block until the page from the last inspect call connects through the extension. Use this after inspect returns awaiting_user. Returns ready, timeout, or mismatch; timeout is not an error.",
1950
+ inputSchema: {
1951
+ timeoutMs: z.number().int().min(1e3).max(6e5).optional().describe("How long to wait in milliseconds (default 120000)")
1952
+ }
1953
+ },
1954
+ async (input) => guard(() => awaitPageTool(input))
1955
+ );
1956
+ server.registerTool(
1957
+ "measure",
1958
+ {
1959
+ title: "Measure element",
1960
+ description: "Return the box model, computed styles, and gaps to neighbours for one element. Accepts a CSS selector or a structural selector returned by find_issues. Call inspect first.",
1961
+ inputSchema: {
1962
+ selector: z.string().min(1).describe("CSS selector or a structural selector returned by find_issues")
1963
+ }
1964
+ },
1965
+ async (input) => guard(() => measureTool(input))
1966
+ );
1967
+ server.registerTool(
1968
+ "find_issues",
1969
+ {
1970
+ title: "Find issues",
1971
+ description: "Run the rule engine on the open page and return violations with selector, actual, expected, severity, and autoFix. Call inspect first.",
1972
+ inputSchema: {
1973
+ rules: z.array(z.string()).optional().describe(`Rules to run. Default: all. Valid: ${RULE_IDS.join(", ")}`),
1974
+ limit: z.number().int().min(1).max(200).optional().describe("Maximum issues to return (default 50)")
1975
+ }
1976
+ },
1977
+ async (input) => guard(() => findIssuesTool(input))
1978
+ );
1979
+ server.registerTool(
1980
+ "overlay",
1981
+ {
1982
+ title: "Overlay",
1983
+ description: "Show or hide the measurement overlay in the page the human is looking at: grid, guides, gap labels, and issue boxes. Use it to let the human see spacing without clicking anything. Call inspect first.",
1984
+ inputSchema: {
1985
+ enabled: z.boolean().optional().describe("false hides the overlay (default true)"),
1986
+ layers: z.array(z.string()).optional().describe(`Layers to draw. Default: all. Valid: ${LAYERS.join(", ")}`)
1987
+ }
1988
+ },
1989
+ async (input) => guard(() => overlayTool(input))
1990
+ );
1991
+ server.registerTool(
1992
+ "screenshot",
1993
+ {
1994
+ title: "Screenshot",
1995
+ description: "Capture a PNG of the viewport or of one element. Call inspect first.",
1996
+ inputSchema: {
1997
+ selector: z.string().min(1).optional().describe("Capture only this element"),
1998
+ fullPage: z.boolean().optional().describe("Capture the full scrollable page (ignored when selector is set)")
1999
+ }
2000
+ },
2001
+ async (input) => guard(() => screenshotTool(input))
2002
+ );
2003
+ server.registerTool(
2004
+ "screenshot_annotated",
2005
+ {
2006
+ title: "Annotated screenshot",
2007
+ description: "Render the measurement overlay (grid, guides, gaps, issue boxes with severity and actual \u2192 expected labels) and capture a viewport PNG. Call inspect first.",
2008
+ inputSchema: {
2009
+ selector: z.string().min(1).optional().describe("Scroll this element into view before capturing"),
2010
+ rules: z.array(z.string()).optional().describe("Annotate only these rules. Default: all")
2011
+ }
2012
+ },
2013
+ async (input) => guard(() => screenshotAnnotatedTool(input))
2014
+ );
2015
+ let closing = false;
2016
+ const shutdown = () => {
2017
+ if (closing) return;
2018
+ closing = true;
2019
+ void closeSession().catch(() => void 0).finally(() => getHub()?.close()).finally(() => process.exit(0));
2020
+ };
2021
+ process.once("SIGINT", shutdown);
2022
+ process.once("SIGTERM", shutdown);
2023
+ process.stdin.once("end", shutdown);
2024
+ process.stdin.once("close", shutdown);
2025
+ await server.connect(new StdioServerTransport());
2026
+ console.error(`[litedesign] MCP server v${VERSION} ready (stdio)`);
2027
+ }
2028
+
2029
+ // packages/cli/src/cli.ts
2030
+ var HELP2 = `
2031
+ litedesign v${VERSION}
2032
+
2033
+ Usage:
2034
+ litedesign setup scaffold extension + config
2035
+ litedesign extension path print the folder to load unpacked
2036
+ litedesign extension update re-copy after an upgrade
2037
+ litedesign doctor check node, extension, and port
2038
+ litedesign ws start start the bridge in the background
2039
+ litedesign ws stop stop the bridge
2040
+ litedesign ws status show the bridge and connected pages
2041
+ litedesign mcp start the MCP server (stdio)
2042
+
2043
+ Options:
2044
+ -h, --help show this help
2045
+ -v, --version show the version
2046
+ `;
2047
+ async function run(argv) {
2048
+ const [command, ...rest] = argv;
2049
+ switch (command) {
2050
+ case void 0:
2051
+ case "help":
2052
+ case "-h":
2053
+ case "--help":
2054
+ console.log(HELP2);
2055
+ return;
2056
+ case "-v":
2057
+ case "--version":
2058
+ console.log(VERSION);
2059
+ return;
2060
+ case "setup":
2061
+ await setup();
2062
+ return;
2063
+ case "extension":
2064
+ await extensionCommand(rest);
2065
+ return;
2066
+ case "doctor":
2067
+ await doctor();
2068
+ return;
2069
+ case "ws":
2070
+ await wsCommand(rest);
2071
+ return;
2072
+ case "mcp":
2073
+ await runMcpServer();
2074
+ return;
2075
+ default:
2076
+ throw new Error(`unknown command: ${command}. Try: litedesign --help`);
2077
+ }
2078
+ }
2079
+
2080
+ // packages/cli/src/index.ts
2081
+ run(process.argv.slice(2)).catch((error) => {
2082
+ const message = error instanceof Error ? error.message : String(error);
2083
+ console.error(`
2084
+ error: ${message}
2085
+ `);
2086
+ process.exit(1);
2087
+ });
2088
+ //# sourceMappingURL=index.js.map