gazesight 0.3.5 → 0.5.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/cli/index.js CHANGED
@@ -1,15 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // apps/cli/src/index.ts
4
- import { existsSync } from "node:fs";
5
- import { spawnSync } from "node:child_process";
6
- import { createInterface } from "node:readline/promises";
7
- import { resolve, dirname as dirname2, join as join2, delimiter } from "node:path";
4
+ import { existsSync as existsSync2 } from "node:fs";
5
+ import { createInterface as createInterface2 } from "node:readline/promises";
6
+ import { resolve as resolve2, dirname as dirname2, join as join2, delimiter } from "node:path";
8
7
  import { fileURLToPath, pathToFileURL } from "node:url";
9
8
 
10
9
  // apps/cli/src/config.ts
11
10
  import { randomBytes } from "node:crypto";
12
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
+ import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
13
12
  import { dirname, join } from "node:path";
14
13
  import { homedir } from "node:os";
15
14
 
@@ -22,6 +21,17 @@ var agentGuidanceSchema = z.strictObject({
22
21
  var localConfigSchema = z.strictObject({
23
22
  token: z.string().regex(/^[a-f0-9]{64}$/),
24
23
  port: z.number().int().min(1024).max(65535),
24
+ pairings: z.array(
25
+ z.strictObject({
26
+ id: z.string().uuid(),
27
+ browser: z.enum(["firefox", "chrome"]),
28
+ extensionId: z.string().min(1).max(256),
29
+ origin: z.string().min(1).max(300),
30
+ credentialHash: z.string().regex(/^[a-f0-9]{64}$/),
31
+ createdAt: z.string().datetime(),
32
+ revokedAt: z.string().datetime().optional()
33
+ })
34
+ ).max(32).default([]),
25
35
  allowedOrigins: z.array(z.url()).default([]),
26
36
  agentGuidance: agentGuidanceSchema.default({
27
37
  enabled: true,
@@ -81,9 +91,17 @@ function setupConfig(path, port) {
81
91
  }
82
92
  return config;
83
93
  }
94
+ function writeLocalConfig(path, value) {
95
+ const config = localConfigSchema.parse(value);
96
+ mkdirSync(dirname(path), { recursive: true });
97
+ const temporary = `${path}.${process.pid}.tmp`;
98
+ writeFileSync(temporary, JSON.stringify(config, null, 2), { mode: 384 });
99
+ renameSync(temporary, path);
100
+ return config;
101
+ }
84
102
 
85
103
  // packages/protocol/src/release.ts
86
- var VERSION = "0.3.5";
104
+ var VERSION = "0.5.0";
87
105
 
88
106
  // apps/cli/src/doctor.ts
89
107
  import { z as z2 } from "zod";
@@ -102,12 +120,12 @@ function diagnosticRows(status, reachable = false, authenticated = false) {
102
120
  {
103
121
  name: "extension connected",
104
122
  pass: !!status?.connected,
105
- action: "Load the Firefox or Chrome extension and import your local config in its extension options."
123
+ action: status?.authorizedPairings ? "An extension is paired but disconnected. Start it and verify the configured bridge port." : "Open Firefox or Chrome, then click Connect to GazeSight in the extension."
106
124
  },
107
125
  {
108
126
  name: "pairing",
109
127
  pass: authenticated && !!status?.paired,
110
- action: "Import the same local configuration into Firefox options; verify port and pairing."
128
+ action: "Run gazesight pair, click Connect to GazeSight, and approve the matching code."
111
129
  },
112
130
  {
113
131
  name: "target selected",
@@ -145,11 +163,1415 @@ var statusSchema = z2.object({
145
163
  connected: z2.boolean(),
146
164
  paired: z2.boolean(),
147
165
  targetState: z2.enum(["none", "authorized", "reauthorization_required"]),
148
- backgroundCapture: z2.object({ enabled: z2.boolean(), permissionGranted: z2.boolean() }).optional()
166
+ backgroundCapture: z2.object({ enabled: z2.boolean(), permissionGranted: z2.boolean() }).optional(),
167
+ authorizedPairings: z2.number().int().nonnegative().optional()
149
168
  });
150
169
 
170
+ // apps/cli/src/pair.ts
171
+ import { createInterface } from "node:readline/promises";
172
+
173
+ // apps/mcp-server/src/bridge/websocket-server.ts
174
+ import { WebSocketServer, WebSocket } from "ws";
175
+
176
+ // packages/protocol/src/schemas.ts
177
+ import { z as z3 } from "zod";
178
+ var targetSchema = z3.strictObject({
179
+ tabId: z3.number().int().nonnegative().optional()
180
+ });
181
+ var stabilityDiagnosticsSchema = z3.strictObject({
182
+ stable: z3.boolean(),
183
+ stabilityWaitMs: z3.number().finite().nonnegative(),
184
+ lastMutationAgoMs: z3.number().finite().nonnegative(),
185
+ fontsReady: z3.boolean(),
186
+ pendingImages: z3.number().int().nonnegative().max(1e6),
187
+ timeoutReached: z3.boolean()
188
+ });
189
+ var diagnosticsQuerySchema = targetSchema.extend({
190
+ maxEntries: z3.number().int().min(1).max(100).default(50),
191
+ reset: z3.boolean().default(true)
192
+ });
193
+ var highlightSchema = targetSchema.extend({
194
+ selector: z3.string().min(1).max(2048),
195
+ durationMs: z3.number().int().min(500).max(3e4).default(3e3)
196
+ });
197
+ var highlightResultSchema = z3.strictObject({
198
+ tabId: z3.number().int().nonnegative(),
199
+ url: z3.url(),
200
+ highlighted: z3.boolean()
201
+ });
202
+ var consoleEntrySchema = z3.strictObject({
203
+ level: z3.enum(["warn", "error"]),
204
+ message: z3.string().max(512),
205
+ timestamp: z3.number().finite().nonnegative()
206
+ });
207
+ var networkEntrySchema = z3.strictObject({
208
+ url: z3.url().max(1024),
209
+ method: z3.string().max(16),
210
+ resourceType: z3.string().max(32),
211
+ status: z3.number().int().min(100).max(599).optional(),
212
+ error: z3.string().max(128).optional(),
213
+ timestamp: z3.number().finite().nonnegative()
214
+ });
215
+ var consoleResultSchema = z3.strictObject({
216
+ tabId: z3.number().int().nonnegative(),
217
+ entries: z3.array(consoleEntrySchema).max(100),
218
+ dropped: z3.number().int().nonnegative(),
219
+ forwardingDropped: z3.number().int().nonnegative()
220
+ });
221
+ var networkResultSchema = z3.strictObject({
222
+ tabId: z3.number().int().nonnegative(),
223
+ entries: z3.array(networkEntrySchema).max(100),
224
+ dropped: z3.number().int().nonnegative()
225
+ });
226
+ var pageStatusContentSchema = z3.strictObject({
227
+ url: z3.url(),
228
+ title: z3.string().max(160),
229
+ readyState: z3.enum(["loading", "interactive", "complete"]),
230
+ viewport: z3.strictObject({
231
+ width: z3.number().int().positive(),
232
+ height: z3.number().int().positive(),
233
+ devicePixelRatio: z3.number().finite().positive()
234
+ }),
235
+ scroll: z3.strictObject({ x: z3.number().finite(), y: z3.number().finite() }),
236
+ stabilityPending: z3.boolean(),
237
+ consoleInstrumentationActive: z3.boolean(),
238
+ consoleForwardingDropped: z3.number().int().nonnegative()
239
+ });
240
+ var pageStatusResultSchema = pageStatusContentSchema.extend({
241
+ tabId: z3.number().int().nonnegative(),
242
+ targetState: z3.enum(["authorized", "reauthorization_required", "none"]),
243
+ runtimeErrorCount: z3.number().int().nonnegative(),
244
+ consoleEntryCount: z3.number().int().nonnegative(),
245
+ networkFailureCount: z3.number().int().nonnegative()
246
+ });
247
+ var interactionTypeSchema = z3.enum([
248
+ "click",
249
+ "type",
250
+ "press_key",
251
+ "hover",
252
+ "scroll",
253
+ "select",
254
+ "focus"
255
+ ]);
256
+ var interactionTargetSchema = targetSchema.extend({
257
+ selector: z3.string().min(1).max(2048)
258
+ });
259
+ var typeSchema = interactionTargetSchema.extend({
260
+ text: z3.string().max(4096),
261
+ clear: z3.boolean().default(true)
262
+ });
263
+ var pressKeySchema = targetSchema.extend({
264
+ selector: z3.string().min(1).max(2048).optional(),
265
+ key: z3.string().max(12).refine(
266
+ (key) => key.length === 1 || [
267
+ "Enter",
268
+ "Escape",
269
+ "Tab",
270
+ "Backspace",
271
+ "Delete",
272
+ "ArrowUp",
273
+ "ArrowDown",
274
+ "ArrowLeft",
275
+ "ArrowRight",
276
+ "Home",
277
+ "End"
278
+ ].includes(key)
279
+ ),
280
+ shift: z3.boolean().default(false)
281
+ });
282
+ var scrollSchema = targetSchema.extend({
283
+ selector: z3.string().min(1).max(2048).optional(),
284
+ x: z3.number().finite().min(-1e6).max(1e6).default(0),
285
+ y: z3.number().finite().min(-1e6).max(1e6).default(0),
286
+ mode: z3.enum(["by", "to"]).default("by")
287
+ });
288
+ var selectSchema = interactionTargetSchema.extend({
289
+ value: z3.string().max(4096)
290
+ });
291
+ var interactionResultSchema = z3.strictObject({
292
+ tabId: z3.number().int().nonnegative(),
293
+ url: z3.url(),
294
+ action: interactionTypeSchema,
295
+ inputSource: z3.literal("dom"),
296
+ completed: z3.literal(true),
297
+ scroll: z3.strictObject({ x: z3.number().finite(), y: z3.number().finite() }).optional()
298
+ });
299
+ var captureSchema = targetSchema.extend({
300
+ url: z3.url().optional(),
301
+ format: z3.enum(["png", "jpeg"]).default("jpeg"),
302
+ quality: z3.number().int().min(0).max(100).default(85),
303
+ waitForStable: z3.boolean().default(false),
304
+ includeOverlay: z3.boolean().optional(),
305
+ stableWindowMs: z3.number().int().min(20).max(1e3).default(100),
306
+ maxWaitMs: z3.number().int().min(50).max(5e3).default(2e3)
307
+ });
308
+ var reloadSchema = targetSchema.extend({
309
+ bypassCache: z3.boolean().default(false)
310
+ });
311
+ var regionCaptureSchema = captureSchema.extend({
312
+ x: z3.number().finite().nonnegative(),
313
+ y: z3.number().finite().nonnegative(),
314
+ width: z3.number().finite().positive().max(32768),
315
+ height: z3.number().finite().positive().max(32768)
316
+ });
317
+ var elementCaptureSchema = captureSchema.extend({
318
+ selector: z3.string().min(1).max(2048),
319
+ padding: z3.number().finite().min(0).max(1e3).default(0)
320
+ });
321
+ var fullPageCaptureSchema = captureSchema.extend({
322
+ maxPixels: z3.number().int().min(1).max(32e6).default(16e6),
323
+ maxSegments: z3.number().int().min(1).max(64).default(32),
324
+ timeoutMs: z3.number().int().min(500).max(8e3).default(7e3)
325
+ });
326
+ var navigateSchema = targetSchema.extend({ url: z3.url() });
327
+ var diffSchema = z3.strictObject({
328
+ baseline: z3.uuid().optional().describe(
329
+ "A prior PNG imageReference from this session. Omit and use referenceImagePath instead to diff against an external design reference."
330
+ ),
331
+ referenceImagePath: z3.string().min(1).max(4096).optional().describe(
332
+ "Path (relative to the MCP server's working directory, or absolute) to a local PNG design reference to diff the live capture against. Exactly one of baseline/referenceImagePath is required."
333
+ ),
334
+ current: z3.uuid().optional(),
335
+ tabId: z3.number().int().nonnegative().optional().describe(
336
+ "Only used with referenceImagePath when current is omitted, to select which tab to capture. Ignored otherwise (baseline's own tab is used)."
337
+ ),
338
+ threshold: z3.number().finite().min(0).max(1).default(0.1),
339
+ checkpoint: z3.string().min(1).max(200).regex(/^[A-Za-z0-9_-]+$/).optional().describe(
340
+ "Visual Convergence best-so-far tracking id (e.g. 'hero-desktop'). Only meaningful together with referenceImagePath: persists the best-known result locally and objectively reports isBest/regressed on every call, independent of the agent's own assessment."
341
+ )
342
+ });
343
+ var viewportProfileSchema = z3.enum([
344
+ "mobile-small",
345
+ "mobile",
346
+ "tablet",
347
+ "desktop",
348
+ "desktop-wide"
349
+ ]);
350
+ var setViewportSchema = targetSchema.extend({
351
+ profile: viewportProfileSchema.optional(),
352
+ width: z3.number().int().min(200).max(8192).optional(),
353
+ height: z3.number().int().min(200).max(8192).optional(),
354
+ dedicatedPreview: z3.boolean().default(true),
355
+ close: z3.boolean().optional().describe(
356
+ "Close the current dedicated preview window instead of creating or resizing one. Width/height/profile are ignored when true. Idempotent: has no effect if no preview is open."
357
+ )
358
+ });
359
+ var viewportResultSchema = z3.strictObject({
360
+ tabId: z3.number().int().nonnegative(),
361
+ url: z3.url(),
362
+ windowId: z3.number().int().nonnegative(),
363
+ width: z3.number().int().positive(),
364
+ height: z3.number().int().positive(),
365
+ devicePixelRatio: z3.number().finite().positive(),
366
+ zoom: z3.number().finite().positive(),
367
+ outerWidth: z3.number().int().positive(),
368
+ outerHeight: z3.number().int().positive(),
369
+ dedicatedPreview: z3.boolean(),
370
+ sourceTabId: z3.number().int().nonnegative().optional(),
371
+ requested: z3.strictObject({
372
+ width: z3.number().int().positive(),
373
+ height: z3.number().int().positive(),
374
+ profile: viewportProfileSchema.optional()
375
+ }).optional(),
376
+ exact: z3.boolean().optional(),
377
+ attempts: z3.number().int().min(0).max(3).optional(),
378
+ requiresTargetSelection: z3.boolean().optional(),
379
+ sourceWindowUnchanged: z3.boolean().optional(),
380
+ previewClosed: z3.boolean().optional().describe(
381
+ "Present when close:true was requested. True if a dedicated preview window was actually open and closed; false if there was none to close."
382
+ )
383
+ });
384
+ var domSchema = targetSchema.extend({
385
+ selector: z3.string().min(1).max(2048).optional(),
386
+ depth: z3.number().int().min(0).max(8).default(3),
387
+ maxNodes: z3.number().int().min(1).max(300).default(100),
388
+ textLength: z3.number().int().min(0).max(300).default(160),
389
+ maxBytes: z3.number().int().min(2048).max(65536).default(32768)
390
+ });
391
+ var getElementSchema = domSchema.extend({
392
+ selector: z3.string().min(1).max(2048)
393
+ });
394
+ var changedRegionsSchema = targetSchema.extend({
395
+ maxRegions: z3.number().int().min(1).max(100).default(50),
396
+ reset: z3.boolean().default(true)
397
+ });
398
+ var capabilitySchema = z3.enum([
399
+ "highlight",
400
+ "clear_highlight",
401
+ "capture_region",
402
+ "capture_element",
403
+ "capture_full_page",
404
+ "get_dom",
405
+ "get_element",
406
+ "get_changed_regions",
407
+ "get_viewport",
408
+ "set_viewport",
409
+ ...interactionTypeSchema.options,
410
+ "get_console",
411
+ "get_network_failures",
412
+ "get_page_status"
413
+ ]);
414
+ var boundsSchema = z3.strictObject({
415
+ x: z3.number().finite(),
416
+ y: z3.number().finite(),
417
+ width: z3.number().finite().nonnegative(),
418
+ height: z3.number().finite().nonnegative()
419
+ });
420
+ var domNodeSchema = z3.strictObject({
421
+ index: z3.number().int().nonnegative(),
422
+ parent: z3.number().int().nonnegative().optional(),
423
+ depth: z3.number().int().min(0).max(8),
424
+ tag: z3.string().max(80),
425
+ id: z3.string().max(160).optional(),
426
+ classes: z3.array(z3.string().max(80)).max(6).optional(),
427
+ role: z3.string().max(80).optional(),
428
+ text: z3.string().max(300).optional(),
429
+ ariaLabel: z3.string().max(300).optional(),
430
+ name: z3.string().max(160).optional(),
431
+ type: z3.string().max(80).optional(),
432
+ href: z3.string().max(1024).optional(),
433
+ bounds: boundsSchema,
434
+ visible: z3.boolean(),
435
+ redacted: z3.boolean().optional(),
436
+ disabled: z3.boolean().optional(),
437
+ checked: z3.boolean().optional(),
438
+ selected: z3.boolean().optional()
439
+ });
440
+ var domResultSchema = z3.strictObject({
441
+ tabId: z3.number().int().nonnegative(),
442
+ url: z3.url(),
443
+ nodes: z3.array(domNodeSchema).max(300),
444
+ truncated: z3.boolean(),
445
+ payloadBytes: z3.number().int().max(65536)
446
+ });
447
+ var changedRegionsResultSchema = z3.strictObject({
448
+ tabId: z3.number().int().nonnegative(),
449
+ url: z3.url(),
450
+ regions: z3.array(
451
+ boundsSchema.extend({
452
+ reason: z3.enum(["mutation", "resize", "attribute", "text"]),
453
+ selector: z3.string().max(240).optional()
454
+ })
455
+ ).max(100),
456
+ trackingStarted: z3.boolean(),
457
+ dropped: z3.number().int().nonnegative(),
458
+ coverage: z3.literal("potential-dom-and-layout-changes")
459
+ });
460
+ var errorSchema = z3.strictObject({
461
+ type: z3.enum(["error", "unhandledrejection", "console-error"]),
462
+ message: z3.string().max(4096),
463
+ source: z3.string().max(2048).optional(),
464
+ line: z3.number().int().nonnegative().optional(),
465
+ column: z3.number().int().nonnegative().optional(),
466
+ timestamp: z3.number().nonnegative()
467
+ });
468
+ var stateSchema = z3.strictObject({
469
+ extensionVersion: z3.string().max(80).optional(),
470
+ extensionId: z3.string().min(1).max(256).optional(),
471
+ capabilities: z3.array(capabilitySchema).max(20).optional(),
472
+ selectedTab: z3.number().int().nonnegative().nullable(),
473
+ url: z3.string().max(2048).nullable(),
474
+ targetState: z3.enum(["none", "authorized", "reauthorization_required"]).optional(),
475
+ browser: z3.strictObject({
476
+ name: z3.enum(["firefox", "chrome"]),
477
+ version: z3.string().max(80)
478
+ }).optional(),
479
+ backgroundCapture: z3.strictObject({
480
+ enabled: z3.boolean(),
481
+ permissionGranted: z3.boolean()
482
+ }).optional()
483
+ });
484
+ var requestSchema = z3.discriminatedUnion("type", [
485
+ z3.strictObject({
486
+ id: z3.uuid(),
487
+ version: z3.literal(1),
488
+ type: z3.literal("highlight"),
489
+ payload: highlightSchema
490
+ }),
491
+ z3.strictObject({
492
+ id: z3.uuid(),
493
+ version: z3.literal(1),
494
+ type: z3.literal("clear_highlight"),
495
+ payload: targetSchema
496
+ }),
497
+ z3.strictObject({
498
+ id: z3.uuid(),
499
+ version: z3.literal(1),
500
+ type: z3.literal("get_console"),
501
+ payload: diagnosticsQuerySchema
502
+ }),
503
+ z3.strictObject({
504
+ id: z3.uuid(),
505
+ version: z3.literal(1),
506
+ type: z3.literal("get_network_failures"),
507
+ payload: diagnosticsQuerySchema
508
+ }),
509
+ z3.strictObject({
510
+ id: z3.uuid(),
511
+ version: z3.literal(1),
512
+ type: z3.literal("get_page_status"),
513
+ payload: targetSchema
514
+ }),
515
+ z3.strictObject({
516
+ id: z3.uuid(),
517
+ version: z3.literal(1),
518
+ type: z3.literal("click"),
519
+ payload: interactionTargetSchema
520
+ }),
521
+ z3.strictObject({
522
+ id: z3.uuid(),
523
+ version: z3.literal(1),
524
+ type: z3.literal("type"),
525
+ payload: typeSchema
526
+ }),
527
+ z3.strictObject({
528
+ id: z3.uuid(),
529
+ version: z3.literal(1),
530
+ type: z3.literal("press_key"),
531
+ payload: pressKeySchema
532
+ }),
533
+ z3.strictObject({
534
+ id: z3.uuid(),
535
+ version: z3.literal(1),
536
+ type: z3.literal("hover"),
537
+ payload: interactionTargetSchema
538
+ }),
539
+ z3.strictObject({
540
+ id: z3.uuid(),
541
+ version: z3.literal(1),
542
+ type: z3.literal("scroll"),
543
+ payload: scrollSchema
544
+ }),
545
+ z3.strictObject({
546
+ id: z3.uuid(),
547
+ version: z3.literal(1),
548
+ type: z3.literal("select"),
549
+ payload: selectSchema
550
+ }),
551
+ z3.strictObject({
552
+ id: z3.uuid(),
553
+ version: z3.literal(1),
554
+ type: z3.literal("focus"),
555
+ payload: interactionTargetSchema
556
+ }),
557
+ z3.strictObject({
558
+ id: z3.uuid(),
559
+ version: z3.literal(1),
560
+ type: z3.literal("get_viewport"),
561
+ payload: targetSchema
562
+ }),
563
+ z3.strictObject({
564
+ id: z3.uuid(),
565
+ version: z3.literal(1),
566
+ type: z3.literal("set_viewport"),
567
+ payload: setViewportSchema
568
+ }),
569
+ z3.strictObject({
570
+ id: z3.uuid(),
571
+ version: z3.literal(1),
572
+ type: z3.literal("get_dom"),
573
+ payload: domSchema
574
+ }),
575
+ z3.strictObject({
576
+ id: z3.uuid(),
577
+ version: z3.literal(1),
578
+ type: z3.literal("get_element"),
579
+ payload: getElementSchema
580
+ }),
581
+ z3.strictObject({
582
+ id: z3.uuid(),
583
+ version: z3.literal(1),
584
+ type: z3.literal("get_changed_regions"),
585
+ payload: changedRegionsSchema
586
+ }),
587
+ z3.strictObject({
588
+ id: z3.uuid(),
589
+ version: z3.literal(1),
590
+ type: z3.literal("capture_region"),
591
+ payload: regionCaptureSchema
592
+ }),
593
+ z3.strictObject({
594
+ id: z3.uuid(),
595
+ version: z3.literal(1),
596
+ type: z3.literal("capture_element"),
597
+ payload: elementCaptureSchema
598
+ }),
599
+ z3.strictObject({
600
+ id: z3.uuid(),
601
+ version: z3.literal(1),
602
+ type: z3.literal("capture_full_page"),
603
+ payload: fullPageCaptureSchema
604
+ }),
605
+ z3.strictObject({
606
+ id: z3.uuid(),
607
+ version: z3.literal(1),
608
+ type: z3.literal("capture"),
609
+ payload: captureSchema
610
+ }),
611
+ z3.strictObject({
612
+ id: z3.uuid(),
613
+ version: z3.literal(1),
614
+ type: z3.literal("reload"),
615
+ payload: reloadSchema
616
+ }),
617
+ z3.strictObject({
618
+ id: z3.uuid(),
619
+ version: z3.literal(1),
620
+ type: z3.literal("navigate"),
621
+ payload: navigateSchema
622
+ }),
623
+ z3.strictObject({
624
+ id: z3.uuid(),
625
+ version: z3.literal(1),
626
+ type: z3.literal("get_errors"),
627
+ payload: targetSchema
628
+ })
629
+ ]);
630
+ var responseSchema = z3.discriminatedUnion("ok", [
631
+ z3.strictObject({
632
+ id: z3.uuid(),
633
+ version: z3.literal(1),
634
+ ok: z3.literal(true),
635
+ result: z3.unknown()
636
+ }),
637
+ z3.strictObject({
638
+ id: z3.uuid(),
639
+ version: z3.literal(1),
640
+ ok: z3.literal(false),
641
+ error: z3.strictObject({
642
+ code: z3.string().max(80),
643
+ message: z3.string().max(4096),
644
+ stability: stabilityDiagnosticsSchema.optional()
645
+ })
646
+ })
647
+ ]);
648
+ var helloSchema = z3.strictObject({
649
+ version: z3.literal(1),
650
+ type: z3.literal("hello"),
651
+ token: z3.string().regex(/^[a-f0-9]{64}$/),
652
+ state: stateSchema
653
+ });
654
+ var stateMessageSchema = z3.strictObject({
655
+ version: z3.literal(1),
656
+ type: z3.literal("state"),
657
+ state: stateSchema
658
+ });
659
+ var captureResultSchema = z3.strictObject({
660
+ tabId: z3.number().int().nonnegative(),
661
+ url: z3.url(),
662
+ width: z3.number().int().positive(),
663
+ height: z3.number().int().positive(),
664
+ format: z3.enum(["png", "jpeg"]),
665
+ captureMs: z3.number().nonnegative(),
666
+ processingMs: z3.number().nonnegative().optional(),
667
+ totalMs: z3.number().nonnegative().optional(),
668
+ segments: z3.number().int().positive().max(64).optional(),
669
+ scrollRestored: z3.boolean().optional(),
670
+ stabilityWaitMs: z3.number().nonnegative().optional(),
671
+ extensionTotalMs: z3.number().nonnegative().optional(),
672
+ renderAppearsStable: z3.boolean().optional(),
673
+ stable: z3.boolean().optional(),
674
+ lastMutationAgoMs: z3.number().finite().nonnegative().optional(),
675
+ fontsReady: z3.boolean().optional(),
676
+ pendingImages: z3.number().int().nonnegative().optional(),
677
+ timeoutReached: z3.boolean().optional(),
678
+ dataUrl: z3.string().max(16 * 1024 * 1024)
679
+ });
680
+ var tabResultSchema = z3.strictObject({
681
+ tabId: z3.number().int().nonnegative(),
682
+ url: z3.url()
683
+ });
684
+ var errorsResultSchema = z3.strictObject({
685
+ tabId: z3.number().int().nonnegative(),
686
+ errors: z3.array(errorSchema).max(200)
687
+ });
688
+ var GSError = class extends Error {
689
+ constructor(code, message, stability) {
690
+ super(message);
691
+ this.code = code;
692
+ this.stability = stability;
693
+ }
694
+ };
695
+
696
+ // apps/mcp-server/src/bridge/pairing.ts
697
+ import { timingSafeEqual } from "node:crypto";
698
+ function tokensMatch(actual, expected) {
699
+ const a = Buffer.from(actual);
700
+ const b = Buffer.from(expected);
701
+ return a.length === b.length && timingSafeEqual(a, b);
702
+ }
703
+ function originAllowed(origin) {
704
+ return origin === void 0 || /^moz-extension:\/\/[a-f0-9-]+\/?$/i.test(origin) || /^chrome-extension:\/\/[a-p]{32}\/?$/.test(origin);
705
+ }
706
+
707
+ // apps/mcp-server/src/bridge/request-router.ts
708
+ import { randomUUID } from "node:crypto";
709
+ var RequestRouter = class {
710
+ constructor(send, timeoutMs = 1e4) {
711
+ this.send = send;
712
+ this.timeoutMs = timeoutMs;
713
+ }
714
+ pending = /* @__PURE__ */ new Map();
715
+ request(type, payload) {
716
+ const request = requestSchema.parse({
717
+ id: randomUUID(),
718
+ version: 1,
719
+ type,
720
+ payload
721
+ });
722
+ return new Promise((resolve3, reject) => {
723
+ const timer = setTimeout(() => {
724
+ this.pending.delete(request.id);
725
+ reject(
726
+ new GSError(
727
+ "GS_TIMEOUT",
728
+ "Firefox did not respond in time. Check the extension connection."
729
+ )
730
+ );
731
+ }, this.timeoutMs);
732
+ this.pending.set(request.id, { resolve: resolve3, reject, timer });
733
+ try {
734
+ this.send(request);
735
+ } catch (error) {
736
+ clearTimeout(timer);
737
+ this.pending.delete(request.id);
738
+ reject(error);
739
+ }
740
+ });
741
+ }
742
+ receive(value) {
743
+ const response = responseSchema.parse(value);
744
+ const call = this.pending.get(response.id);
745
+ if (!call)
746
+ throw new GSError(
747
+ "GS_INVALID_MESSAGE",
748
+ "Unknown or duplicate response ID."
749
+ );
750
+ clearTimeout(call.timer);
751
+ this.pending.delete(response.id);
752
+ if (response.ok) call.resolve(response.result);
753
+ else
754
+ call.reject(
755
+ new GSError(
756
+ response.error.code,
757
+ response.error.message,
758
+ response.error.stability
759
+ )
760
+ );
761
+ }
762
+ disconnect() {
763
+ for (const call of this.pending.values()) {
764
+ clearTimeout(call.timer);
765
+ call.reject(
766
+ new GSError(
767
+ "GS_NOT_CONNECTED",
768
+ "Firefox disconnected. Wait for reconnection."
769
+ )
770
+ );
771
+ }
772
+ this.pending.clear();
773
+ }
774
+ };
775
+
776
+ // apps/mcp-server/src/bridge/connection-registry.ts
777
+ function originOf(url) {
778
+ if (url === null) return null;
779
+ try {
780
+ return new URL(url).origin;
781
+ } catch {
782
+ return null;
783
+ }
784
+ }
785
+ var BrowserConnectionRegistry = class {
786
+ connections = /* @__PURE__ */ new Map();
787
+ nextGeneration = 1;
788
+ nextSuffix = 1;
789
+ get(browser) {
790
+ return this.connections.get(browser);
791
+ }
792
+ byConnectionId(connectionId) {
793
+ for (const connection of this.connections.values())
794
+ if (connection.connectionId === connectionId) return connection;
795
+ return void 0;
796
+ }
797
+ all() {
798
+ return [...this.connections.values()];
799
+ }
800
+ isDuplicate(browser) {
801
+ return this.connections.has(browser);
802
+ }
803
+ // Returns the new connection, or undefined if `browser` already has a live
804
+ // connection (duplicate — caller must refuse the new socket, not replace).
805
+ register(browser, initialState, now = Date.now()) {
806
+ if (this.connections.has(browser)) return void 0;
807
+ const connection = {
808
+ connectionId: `${browser}-${this.nextSuffix++}`,
809
+ browser,
810
+ lifecycleGeneration: this.nextGeneration++,
811
+ connectedAt: now,
812
+ state: initialState,
813
+ lastSeenAt: now
814
+ };
815
+ this.connections.set(browser, connection);
816
+ return connection;
817
+ }
818
+ // Removes a connection by id, only if it is still the one currently
819
+ // registered for its browser family (guards against a stale remove for a
820
+ // connection that was already superseded).
821
+ remove(connectionId) {
822
+ for (const [browser, connection] of this.connections)
823
+ if (connection.connectionId === connectionId) {
824
+ this.connections.delete(browser);
825
+ return connection;
826
+ }
827
+ return void 0;
828
+ }
829
+ updateState(connectionId, state, now = Date.now()) {
830
+ const connection = this.byConnectionId(connectionId);
831
+ if (!connection) return void 0;
832
+ const previousTargetState = connection.state.targetState;
833
+ connection.state = state;
834
+ connection.lastSeenAt = now;
835
+ return { connection, previousTargetState };
836
+ }
837
+ };
838
+ function isExplicitActivation(nextTargetState, isCurrentlyActive) {
839
+ return nextTargetState === "authorized" && !isCurrentlyActive;
840
+ }
841
+ var ActiveTargetRouter = class {
842
+ route;
843
+ sequence = 0;
844
+ get current() {
845
+ return this.route;
846
+ }
847
+ activate(connection) {
848
+ this.route = {
849
+ connectionId: connection.connectionId,
850
+ browser: connection.browser,
851
+ activationSequence: ++this.sequence,
852
+ activatedAt: Date.now(),
853
+ tabId: connection.state.selectedTab,
854
+ origin: originOf(connection.state.url)
855
+ };
856
+ return this.route;
857
+ }
858
+ considerStateUpdate(connection) {
859
+ const isCurrentlyActive = this.route?.connectionId === connection.connectionId;
860
+ if (!isExplicitActivation(connection.state.targetState, isCurrentlyActive))
861
+ return this.route;
862
+ return this.activate(connection);
863
+ }
864
+ // Reconnect-only restoration (GAZESIGHT_MASTER_SPEC.md §142/§143): a
865
+ // newly-registered connection may rebind the EXISTING active route only if
866
+ // it is provably a resumption of the exact same logical session that
867
+ // already owned it — never a new activation path. This is deliberately
868
+ // NOT the same operation as considerStateUpdate: a hello can never create a
869
+ // route from nothing here (requirement 1), and it can never take a route
870
+ // away from a different browser family that has since become active
871
+ // (requirement 2 — see below), including after an MCP restart, where
872
+ // `this.route` starts undefined and requirement 1 alone already blocks it.
873
+ //
874
+ // Every one of the following must hold, in order, fail-closed on the first
875
+ // mismatch (no partial/fuzzy match — restoration is authoritative identity
876
+ // matching, never time-based trust):
877
+ // 1. A route already exists — hello alone never creates one.
878
+ // 2. The route belongs to the SAME browser family as this connection —
879
+ // if a different browser was explicitly activated in the meantime
880
+ // (including while this one was disconnected), that activation always
881
+ // wins and is never reclaimed.
882
+ // 3. The reconnecting hello reports the target as already authorized
883
+ // (the browser's own extension, e.g. Continuous Access, already
884
+ // independently re-validated the tab/origin/allow-list before ever
885
+ // setting this).
886
+ // 4. The reconnecting tab id matches the tab id recorded at the route's
887
+ // last activation — never trusts "same origin, different tab".
888
+ // 5. The reconnecting origin matches the origin recorded at the route's
889
+ // last activation, exactly (no aliasing) — an unresolvable origin on
890
+ // either side is treated as incompatible, not as a pass.
891
+ // A stale connectionId from before a teardown can never reach this
892
+ // function at all (the registry only looks up currently-registered
893
+ // connections by id — see BrowserConnectionRegistry.byConnectionId/
894
+ // updateState), so a delayed message from an already-torn-down socket
895
+ // cannot reclaim anything.
896
+ considerReconnectRestore(connection) {
897
+ if (!this.route) return void 0;
898
+ if (this.route.connectionId === connection.connectionId) return this.route;
899
+ if (this.route.browser !== connection.browser) return this.route;
900
+ if (connection.state.targetState !== "authorized") return this.route;
901
+ if (this.route.tabId === null || this.route.tabId !== connection.state.selectedTab)
902
+ return this.route;
903
+ const origin = originOf(connection.state.url);
904
+ if (this.route.origin === null || origin === null || this.route.origin !== origin)
905
+ return this.route;
906
+ return this.activate(connection);
907
+ }
908
+ };
909
+
910
+ // apps/mcp-server/src/bridge/websocket-server.ts
911
+ import { createServer } from "node:http";
912
+
913
+ // apps/mcp-server/src/bridge/pairing-service.ts
914
+ import { createHash, randomBytes as randomBytes2, randomInt, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
915
+ var hash = (value) => createHash("sha256").update(value).digest("hex");
916
+ var equalHash = (value, expected) => {
917
+ const actual = Buffer.from(hash(value));
918
+ const wanted = Buffer.from(expected);
919
+ return actual.length === wanted.length && timingSafeEqual2(actual, wanted);
920
+ };
921
+ var json = (response, status, value) => {
922
+ response.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
923
+ response.end(JSON.stringify(value));
924
+ };
925
+ async function body(request) {
926
+ const chunks = [];
927
+ let length = 0;
928
+ for await (const chunk of request) {
929
+ length += chunk.length;
930
+ if (length > 4096) throw new Error("body too large");
931
+ chunks.push(chunk);
932
+ }
933
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
934
+ }
935
+ function browserForOrigin(origin) {
936
+ if (/^moz-extension:\/\/[a-f0-9-]+\/?$/i.test(origin)) return "firefox";
937
+ if (/^chrome-extension:\/\/[a-p]{32}\/?$/.test(origin)) return "chrome";
938
+ }
939
+ var PairingService = class {
940
+ constructor(token, configPath, initial) {
941
+ this.token = token;
942
+ this.configPath = configPath;
943
+ this.pairings = initial;
944
+ }
945
+ pending = /* @__PURE__ */ new Map();
946
+ recent = /* @__PURE__ */ new Map();
947
+ pairings;
948
+ authenticate(credential, origin, browser) {
949
+ if (!origin) return false;
950
+ return this.pairings.some((pairing) => !pairing.revokedAt && pairing.browser === browser && pairing.origin === origin && equalHash(credential, pairing.credentialHash));
951
+ }
952
+ listPending() {
953
+ this.expire();
954
+ return [...this.pending.values()].filter((item) => item.state === "pending").map(({ id, browser, extensionId, code, createdAt, expiresAt }) => ({ id, browser, extensionId, code, createdAt, expiresAt }));
955
+ }
956
+ listPairings() {
957
+ return this.pairings.map(({ id, browser, extensionId, origin, createdAt, revokedAt }) => ({ id, browser, extensionId, origin, createdAt, ...revokedAt ? { revokedAt } : {} }));
958
+ }
959
+ deliveryState(id) {
960
+ const item = this.pending.get(id);
961
+ return item ? { state: item.state, redeemed: item.redeemed === true, expiresAt: item.expiresAt } : void 0;
962
+ }
963
+ approve(id, code) {
964
+ this.expire();
965
+ const item = this.pending.get(id);
966
+ if (!item || item.state !== "pending" || item.code !== code) return false;
967
+ const credential = randomBytes2(32).toString("hex");
968
+ const pairing = { id: crypto.randomUUID(), browser: item.browser, extensionId: item.extensionId, origin: item.origin, credentialHash: hash(credential), createdAt: (/* @__PURE__ */ new Date()).toISOString() };
969
+ this.pairings.push(pairing);
970
+ item.credential = credential;
971
+ item.state = "approved";
972
+ this.persist();
973
+ return true;
974
+ }
975
+ reject(id) {
976
+ const item = this.pending.get(id);
977
+ if (!item || item.state !== "pending") return false;
978
+ item.state = "rejected";
979
+ return true;
980
+ }
981
+ revoke(id) {
982
+ const item = this.pairings.find((pairing) => pairing.id === id && !pairing.revokedAt);
983
+ if (!item) return false;
984
+ item.revokedAt = (/* @__PURE__ */ new Date()).toISOString();
985
+ this.persist();
986
+ return true;
987
+ }
988
+ persist() {
989
+ if (!this.configPath) return;
990
+ const config = readLocalConfig(this.configPath);
991
+ writeLocalConfig(this.configPath, { ...config, pairings: this.pairings });
992
+ }
993
+ expire() {
994
+ const now = Date.now();
995
+ for (const item of this.pending.values()) if (item.expiresAt <= now && item.state === "pending") item.state = "rejected";
996
+ }
997
+ admin(request) {
998
+ return request.headers.origin === void 0 && tokensMatch(request.headers.authorization?.replace(/^Bearer /, "") ?? "", this.token);
999
+ }
1000
+ async handle(request, response) {
1001
+ const url = request.url;
1002
+ if (!url?.startsWith("/gazesight/pairing/")) return false;
1003
+ const origin = request.headers.origin;
1004
+ if (origin && originAllowed(origin)) response.setHeader("Access-Control-Allow-Origin", origin);
1005
+ response.setHeader("Vary", "Origin");
1006
+ if (request.method === "OPTIONS" && origin && originAllowed(origin)) {
1007
+ response.writeHead(204, { "Access-Control-Allow-Methods": "POST", "Access-Control-Allow-Headers": "content-type, authorization" }).end();
1008
+ return true;
1009
+ }
1010
+ try {
1011
+ if (url === "/gazesight/pairing/request" && request.method === "POST") {
1012
+ const browser = origin ? browserForOrigin(origin) : void 0;
1013
+ if (!origin || !browser) {
1014
+ json(response, 403, { error: "ineligible_origin" });
1015
+ return true;
1016
+ }
1017
+ const now = Date.now();
1018
+ const attempts = (this.recent.get(origin) ?? []).filter((time) => now - time < 6e4);
1019
+ if (attempts.length >= 5) {
1020
+ json(response, 429, { error: "rate_limited" });
1021
+ return true;
1022
+ }
1023
+ attempts.push(now);
1024
+ this.recent.set(origin, attempts);
1025
+ const value = await body(request);
1026
+ if (value.browser !== browser || typeof value.extensionId !== "string" || value.extensionId.length > 256 || typeof value.secret !== "string" || !/^[a-f0-9]{64}$/.test(value.secret)) {
1027
+ json(response, 400, { error: "invalid_request" });
1028
+ return true;
1029
+ }
1030
+ const item = { id: crypto.randomUUID(), browser, extensionId: value.extensionId, origin, secretHash: hash(value.secret), code: String(randomInt(0, 1e6)).padStart(6, "0"), createdAt: now, expiresAt: now + 12e4, attempts: 0, state: "pending" };
1031
+ this.pending.set(item.id, item);
1032
+ json(response, 201, { requestId: item.id, code: item.code, expiresAt: item.expiresAt });
1033
+ return true;
1034
+ }
1035
+ if (url === "/gazesight/pairing/status" && request.method === "POST") {
1036
+ const value = await body(request);
1037
+ const item = typeof value.requestId === "string" ? this.pending.get(value.requestId) : void 0;
1038
+ if (!item || item.origin !== origin || typeof value.secret !== "string") {
1039
+ json(response, 404, { state: "unavailable" });
1040
+ return true;
1041
+ }
1042
+ item.attempts += 1;
1043
+ if (item.attempts > 180 || !equalHash(value.secret, item.secretHash)) {
1044
+ json(response, 403, { state: "rejected" });
1045
+ return true;
1046
+ }
1047
+ if (Date.now() >= item.expiresAt) {
1048
+ json(response, 410, { state: "expired" });
1049
+ return true;
1050
+ }
1051
+ if (item.state === "approved" && item.credential && !item.redeemed) {
1052
+ const pairing = this.pairings.find((value2) => value2.origin === item.origin && value2.extensionId === item.extensionId && value2.credentialHash === hash(item.credential));
1053
+ item.redeemed = true;
1054
+ json(response, 200, { state: "approved", credential: item.credential, pairingId: pairing?.id });
1055
+ item.credential = void 0;
1056
+ return true;
1057
+ }
1058
+ json(response, 200, { state: item.state });
1059
+ return true;
1060
+ }
1061
+ if (url === "/gazesight/pairing/revoke-self" && request.method === "POST" && origin) {
1062
+ const value = await body(request);
1063
+ const id = typeof value.id === "string" ? value.id : "";
1064
+ const credential = request.headers.authorization?.replace(/^Bearer /, "") ?? "";
1065
+ const pairing = this.pairings.find((item) => item.id === id && item.origin === origin && !item.revokedAt && equalHash(credential, item.credentialHash));
1066
+ if (!pairing) {
1067
+ json(response, 403, { ok: false });
1068
+ return true;
1069
+ }
1070
+ pairing.revokedAt = (/* @__PURE__ */ new Date()).toISOString();
1071
+ this.persist();
1072
+ json(response, 200, { ok: true });
1073
+ return true;
1074
+ }
1075
+ if (!this.admin(request)) {
1076
+ json(response, 404, { error: "not_found" });
1077
+ return true;
1078
+ }
1079
+ if (url === "/gazesight/pairing/delivery" && request.method === "POST") {
1080
+ const value = await body(request);
1081
+ const delivery = typeof value.id === "string" ? this.deliveryState(value.id) : void 0;
1082
+ json(response, delivery ? 200 : 404, delivery ?? { error: "not_found" });
1083
+ return true;
1084
+ }
1085
+ if (url === "/gazesight/pairing/pending" && request.method === "GET") {
1086
+ json(response, 200, { requests: this.listPending() });
1087
+ return true;
1088
+ }
1089
+ if (url === "/gazesight/pairing/list" && request.method === "GET") {
1090
+ json(response, 200, { pairings: this.listPairings() });
1091
+ return true;
1092
+ }
1093
+ if (request.method === "POST" && ["approve", "reject", "revoke"].some((action) => url === `/gazesight/pairing/${action}`)) {
1094
+ const value = await body(request);
1095
+ const id = typeof value.id === "string" ? value.id : "";
1096
+ const ok = url.endsWith("approve") ? this.approve(id, typeof value.code === "string" ? value.code : "") : url.endsWith("reject") ? this.reject(id) : this.revoke(id);
1097
+ json(response, ok ? 200 : 404, { ok });
1098
+ return true;
1099
+ }
1100
+ json(response, 404, { error: "not_found" });
1101
+ return true;
1102
+ } catch {
1103
+ json(response, 400, { error: "invalid_request" });
1104
+ return true;
1105
+ }
1106
+ }
1107
+ };
1108
+
1109
+ // apps/mcp-server/src/bridge/websocket-server.ts
1110
+ var EMPTY_STATE = { selectedTab: null, url: null };
1111
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 2e4;
1112
+ function browserFromOrigin(origin) {
1113
+ if (origin === void 0) return void 0;
1114
+ if (/^moz-extension:\/\/[a-f0-9-]+\/?$/i.test(origin)) return "firefox";
1115
+ if (/^chrome-extension:\/\/[a-p]{32}\/?$/.test(origin)) return "chrome";
1116
+ return void 0;
1117
+ }
1118
+ var Bridge = class {
1119
+ constructor(config) {
1120
+ this.config = config;
1121
+ this.pairing = new PairingService(config.token, config.configPath, config.pairings ?? []);
1122
+ const verifyClient = (info) => originAllowed(info.origin || void 0);
1123
+ this.http = createServer(async (request, response) => {
1124
+ response.setHeader("Cache-Control", "no-store");
1125
+ response.setHeader("X-GazeSight-Version", VERSION);
1126
+ if (await this.pairing.handle(request, response)) return;
1127
+ if (request.method !== "GET" || request.url !== "/gazesight/status" || request.headers.origin !== void 0) {
1128
+ response.writeHead(404).end();
1129
+ return;
1130
+ }
1131
+ if (!tokensMatch(
1132
+ request.headers.authorization?.replace(/^Bearer /, "") ?? "",
1133
+ config.token
1134
+ )) {
1135
+ response.writeHead(401).end();
1136
+ return;
1137
+ }
1138
+ response.setHeader("Content-Type", "application/json");
1139
+ response.end(JSON.stringify(this.status()));
1140
+ });
1141
+ this.http.once("error", (error) => {
1142
+ this.listenError = error.code === "EADDRINUSE" ? new GSError(
1143
+ "GS_PORT_IN_USE",
1144
+ `Port ${config.port} is already in use by another GazeSight MCP server. Only one can run at a time -- close the other session first.`
1145
+ ) : error;
1146
+ });
1147
+ this.server = new WebSocketServer({
1148
+ server: this.http,
1149
+ maxPayload: 17 * 1024 * 1024,
1150
+ perMessageDeflate: false,
1151
+ verifyClient
1152
+ });
1153
+ this.http.listen(config.port, "127.0.0.1");
1154
+ this.server.on("connection", (socket, request) => {
1155
+ let paired = false;
1156
+ let connection;
1157
+ const originBrowser = browserFromOrigin(request.headers.origin);
1158
+ const timer = setTimeout(
1159
+ () => socket.close(1008, "Pairing required"),
1160
+ 3e3
1161
+ );
1162
+ const teardown = () => {
1163
+ clearTimeout(timer);
1164
+ if (!connection) return;
1165
+ this.wiring.get(connection.connectionId)?.router.disconnect();
1166
+ this.wiring.delete(connection.connectionId);
1167
+ this.registry.remove(connection.connectionId);
1168
+ };
1169
+ this.alive.set(socket, { pongReceived: true });
1170
+ socket.on("pong", () => {
1171
+ const state = this.alive.get(socket);
1172
+ if (state) state.pongReceived = true;
1173
+ });
1174
+ socket.on("error", () => {
1175
+ });
1176
+ socket.on("message", (bytes, binary) => {
1177
+ try {
1178
+ if (!paired && Buffer.byteLength(bytes.toString()) > 4096)
1179
+ throw new Error("Pairing message too large");
1180
+ if (binary) throw new Error("Binary messages forbidden");
1181
+ const message = JSON.parse(bytes.toString());
1182
+ if (!paired) {
1183
+ const hello = helloSchema.parse(message);
1184
+ const claimedBrowser = hello.state.browser?.name;
1185
+ if (originBrowser !== void 0 && claimedBrowser !== void 0 && originBrowser !== claimedBrowser)
1186
+ throw new Error("Origin/browser mismatch");
1187
+ const browser = claimedBrowser ?? originBrowser;
1188
+ if (browser === void 0)
1189
+ throw new Error("Unidentifiable browser");
1190
+ if (!tokensMatch(hello.token, config.token) && !this.pairing.authenticate(hello.token, request.headers.origin, browser))
1191
+ throw new Error("Pairing rejected");
1192
+ if (this.registry.isDuplicate(browser))
1193
+ throw new Error("Duplicate connection");
1194
+ connection = this.registry.register(browser, hello.state);
1195
+ if (!connection) throw new Error("Duplicate connection");
1196
+ const router = new RequestRouter((req) => {
1197
+ if (socket.readyState !== WebSocket.OPEN)
1198
+ throw new GSError(
1199
+ "GS_NOT_CONNECTED",
1200
+ "Start Firefox and pair the GazeSight extension in its settings."
1201
+ );
1202
+ socket.send(JSON.stringify(req));
1203
+ }, config.timeoutMs);
1204
+ this.wiring.set(connection.connectionId, { socket, router });
1205
+ this.activeTargetRouter.considerReconnectRestore(connection);
1206
+ paired = true;
1207
+ clearTimeout(timer);
1208
+ socket.send(JSON.stringify({ version: 1, type: "paired" }));
1209
+ return;
1210
+ }
1211
+ if (!connection) return;
1212
+ const state = stateMessageSchema.safeParse(message);
1213
+ if (state.success) {
1214
+ const result = this.registry.updateState(
1215
+ connection.connectionId,
1216
+ state.data.state
1217
+ );
1218
+ if (result) this.activeTargetRouter.considerStateUpdate(result.connection);
1219
+ return;
1220
+ }
1221
+ this.wiring.get(connection.connectionId)?.router.receive(message);
1222
+ } catch {
1223
+ socket.close(1008, "GS_INVALID_MESSAGE");
1224
+ }
1225
+ });
1226
+ socket.on("close", teardown);
1227
+ });
1228
+ this.heartbeat = setInterval(() => {
1229
+ for (const socket of this.server.clients) {
1230
+ const state = this.alive.get(socket);
1231
+ if (!state) continue;
1232
+ if (!state.pongReceived) {
1233
+ socket.terminate();
1234
+ continue;
1235
+ }
1236
+ state.pongReceived = false;
1237
+ socket.ping();
1238
+ }
1239
+ }, config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS);
1240
+ this.heartbeat.unref?.();
1241
+ }
1242
+ registry = new BrowserConnectionRegistry();
1243
+ activeTargetRouter = new ActiveTargetRouter();
1244
+ wiring = /* @__PURE__ */ new Map();
1245
+ alive = /* @__PURE__ */ new WeakMap();
1246
+ heartbeat;
1247
+ server;
1248
+ http;
1249
+ listenError;
1250
+ pairing;
1251
+ // Resolves the connection currently backing the global active target, or
1252
+ // undefined if no browser is active (nothing selected yet, or the
1253
+ // previously active browser disconnected — no automatic fallback).
1254
+ resolveActive() {
1255
+ const route = this.activeTargetRouter.current;
1256
+ if (!route) return void 0;
1257
+ return this.registry.byConnectionId(route.connectionId);
1258
+ }
1259
+ resolveActiveWiring() {
1260
+ const route = this.activeTargetRouter.current;
1261
+ if (!route) return void 0;
1262
+ return this.wiring.get(route.connectionId);
1263
+ }
1264
+ get connected() {
1265
+ const wiring = this.resolveActiveWiring();
1266
+ return wiring?.socket.readyState === WebSocket.OPEN;
1267
+ }
1268
+ get state() {
1269
+ return this.resolveActive()?.state ?? EMPTY_STATE;
1270
+ }
1271
+ get port() {
1272
+ return this.config.port;
1273
+ }
1274
+ // Same call shape as before (`bridge.router.request(type, payload)`) so
1275
+ // server.ts and its 25 tools need no changes at all — routing to whichever
1276
+ // browser is active is entirely internal to Bridge.
1277
+ router = {
1278
+ request: async (type, payload) => {
1279
+ const route = this.activeTargetRouter.current;
1280
+ if (!route)
1281
+ throw new GSError(
1282
+ "GS_NOT_CONNECTED",
1283
+ "Start Firefox and pair the GazeSight extension in its settings."
1284
+ );
1285
+ const wiring = this.wiring.get(route.connectionId);
1286
+ if (!wiring || wiring.socket.readyState !== WebSocket.OPEN)
1287
+ throw new GSError(
1288
+ "GS_TARGET_UNAVAILABLE",
1289
+ "The active browser disconnected. Select the target again in that browser, or switch to another browser with its own 'Use this tab'."
1290
+ );
1291
+ return wiring.router.request(type, payload);
1292
+ }
1293
+ };
1294
+ status() {
1295
+ const active = this.resolveActive();
1296
+ const connectedBrowsers = Object.fromEntries(
1297
+ ["firefox", "chrome"].map((browser) => [
1298
+ browser,
1299
+ this.registry.get(browser) !== void 0 && this.wiring.get(this.registry.get(browser).connectionId)?.socket.readyState === WebSocket.OPEN
1300
+ ])
1301
+ );
1302
+ const authorizedTargets = Object.fromEntries(
1303
+ ["firefox", "chrome"].map((browser) => {
1304
+ const connection = this.registry.get(browser);
1305
+ return [
1306
+ browser,
1307
+ connection?.state.targetState === "authorized" ? { url: connection.state.url } : null
1308
+ ];
1309
+ })
1310
+ );
1311
+ return {
1312
+ connected: this.connected,
1313
+ paired: this.connected,
1314
+ authorizedPairings: this.pairing.listPairings().filter((item) => !item.revokedAt).length,
1315
+ targetState: this.state.targetState ?? "none",
1316
+ tabId: this.state.selectedTab,
1317
+ bridge: { port: this.port },
1318
+ ...this.state,
1319
+ connectedBrowsers,
1320
+ authorizedTargets,
1321
+ activeTarget: active ? { browser: active.browser, url: active.state.url } : null
1322
+ };
1323
+ }
1324
+ async ready() {
1325
+ if (this.listenError) throw this.listenError;
1326
+ if (this.server.address()) return;
1327
+ await new Promise((resolve3, reject) => {
1328
+ this.server.once("listening", resolve3);
1329
+ const onError = () => reject(this.listenError);
1330
+ this.server.once("error", onError);
1331
+ this.http.once("error", onError);
1332
+ });
1333
+ }
1334
+ async close() {
1335
+ clearInterval(this.heartbeat);
1336
+ for (const wiring of this.wiring.values()) wiring.router.disconnect();
1337
+ for (const client of this.server.clients) client.terminate();
1338
+ await new Promise((resolve3) => this.server.close(() => resolve3()));
1339
+ await new Promise((resolve3) => this.http.close(() => resolve3()));
1340
+ }
1341
+ };
1342
+
1343
+ // apps/cli/src/pair.ts
1344
+ var headers = (token) => ({ Authorization: `Bearer ${token}`, "Content-Type": "application/json" });
1345
+ async function admin(port, token, path, method = "GET", value) {
1346
+ return fetch(`http://127.0.0.1:${port}/gazesight/pairing/${path}`, {
1347
+ method,
1348
+ headers: headers(token),
1349
+ body: value === void 0 ? void 0 : JSON.stringify(value),
1350
+ signal: AbortSignal.timeout(1500)
1351
+ });
1352
+ }
1353
+ async function pairingFlow(config, configPath, revoke) {
1354
+ let bridge;
1355
+ try {
1356
+ const check = await admin(config.port, config.token, "list").catch(() => void 0);
1357
+ if (!check?.ok) {
1358
+ bridge = new Bridge({ ...config, configPath, timeoutMs: 1e4 });
1359
+ await bridge.ready();
1360
+ }
1361
+ if (revoke) {
1362
+ const current = await admin(config.port, config.token, "list");
1363
+ if (!current.ok) throw new Error("Pairing administration unavailable.");
1364
+ const pairings = (await current.json()).pairings;
1365
+ const ids = revoke === "all" ? pairings.filter((item) => !item.revokedAt).map((item) => item.id) : [revoke];
1366
+ let count = 0;
1367
+ for (const id of ids) if ((await admin(config.port, config.token, "revoke", "POST", { id })).ok) count++;
1368
+ console.log(`Revoked pairings: ${count}. Extensions must pair again.`);
1369
+ return;
1370
+ }
1371
+ if (!process.stdin.isTTY) {
1372
+ console.log("Pairing listener ready. Open the GazeSight extension and click Connect to GazeSight.");
1373
+ return;
1374
+ }
1375
+ console.log("Pairing listener ready. Open the GazeSight browser extension and click Connect to GazeSight.");
1376
+ console.log("Waiting for a request (Ctrl+C to stop)\u2026");
1377
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1378
+ try {
1379
+ for (; ; ) {
1380
+ const response = await admin(config.port, config.token, "pending");
1381
+ if (!response.ok) throw new Error("Pairing administration unavailable.");
1382
+ const requests = (await response.json()).requests;
1383
+ const request = requests[0];
1384
+ if (!request) {
1385
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
1386
+ continue;
1387
+ }
1388
+ console.log(`Pairing request: ${request.browser} \xB7 verification code ${request.code}`);
1389
+ const answer = (await rl.question("Approve this matching request? [y/N] ")).trim().toLowerCase();
1390
+ const approved = answer === "y" || answer === "yes";
1391
+ const result = await admin(config.port, config.token, approved ? "approve" : "reject", "POST", { id: request.id, code: request.code });
1392
+ if (!result.ok) throw new Error("The pairing request expired or was already used.");
1393
+ if (approved) {
1394
+ const deadline = Date.now() + 5e3;
1395
+ let delivered = false;
1396
+ while (Date.now() < deadline) {
1397
+ const delivery = await admin(config.port, config.token, "delivery", "POST", { id: request.id });
1398
+ if (delivery.ok && (await delivery.json()).redeemed) {
1399
+ delivered = true;
1400
+ break;
1401
+ }
1402
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
1403
+ }
1404
+ if (!delivered) throw new Error("The approved credential was not delivered; keep the extension open and retry pairing.");
1405
+ console.log("Pairing approved and delivered. The extension can now reconnect securely.");
1406
+ } else console.log("Pairing rejected.");
1407
+ return;
1408
+ }
1409
+ } finally {
1410
+ rl.close();
1411
+ }
1412
+ } finally {
1413
+ if (bridge) await bridge.close();
1414
+ }
1415
+ }
1416
+
1417
+ // apps/cli/src/agent-config.ts
1418
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1419
+ import { basename, resolve } from "node:path";
1420
+ import { spawnSync } from "node:child_process";
1421
+ function parseKnownGazeSightArgs(command, argsText) {
1422
+ const cli = argsText.match(/^(.*?[\\/]dist[\\/]cli[\\/]index\.js)\s+mcp(?:\s+--config\s+(.+))?$/i);
1423
+ if (cli) return [cli[1], "mcp", ...cli[2] ? ["--config", cli[2]] : []];
1424
+ if (/^npx(?:\.cmd|\.exe)?$/i.test(basename(command))) {
1425
+ const npx = argsText.match(/^(.*?)\b(gazesight(?:@\d+\.\d+\.\d+)?)\s+mcp(?:\s+--config\s+(.+))?$/i);
1426
+ if (npx) {
1427
+ const prefix = npx[1].trim().split(/\s+/).filter(Boolean);
1428
+ return [...prefix, npx[2], "mcp", ...npx[3] ? ["--config", npx[3]] : []];
1429
+ }
1430
+ }
1431
+ if (/^gazesight(?:-mcp)?(?:\.cmd|\.exe)?$/i.test(basename(command))) {
1432
+ const direct = argsText.match(/^mcp(?:\s+--config\s+(.+))?$/i);
1433
+ if (direct) return ["mcp", ...direct[1] ? ["--config", direct[1]] : []];
1434
+ }
1435
+ return void 0;
1436
+ }
1437
+ function normalize(value) {
1438
+ const normalized = value.trim().replace(/^['"]|['"]$/g, "").replaceAll("\\", "/");
1439
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
1440
+ }
1441
+ function parseScope(output) {
1442
+ const value = output.match(/^\s*Scope:\s*(.+)$/im)?.[1]?.toLowerCase() ?? "user";
1443
+ if (value.includes("project")) return "project";
1444
+ if (value.includes("local")) return "local";
1445
+ return "user";
1446
+ }
1447
+ function versionFromPackageRoot(argsText) {
1448
+ const normalized = argsText.replaceAll("\\", "/");
1449
+ const marker = "/dist/cli/index.js";
1450
+ const markerIndex = normalized.toLowerCase().indexOf(marker);
1451
+ if (markerIndex < 0) return void 0;
1452
+ const root2 = argsText.slice(0, markerIndex);
1453
+ const packagePath = resolve(root2, "package.json");
1454
+ if (!existsSync(packagePath)) return void 0;
1455
+ try {
1456
+ const parsed = JSON.parse(readFileSync2(packagePath, "utf8"));
1457
+ return typeof parsed.version === "string" ? parsed.version : void 0;
1458
+ } catch {
1459
+ return void 0;
1460
+ }
1461
+ }
1462
+ function parseRegisteredConfig(agent, output) {
1463
+ const command = output.match(/^\s*(?:Command|command):\s*(.+)$/im)?.[1]?.trim();
1464
+ const argsText = output.match(/^\s*(?:Args|args):\s*(.*)$/im)?.[1]?.trim();
1465
+ if (!command || argsText === void 0) return void 0;
1466
+ const combined = `${command} ${argsText}`;
1467
+ const explicitVersion = combined.match(/(?:^|\s)gazesight@(\d+\.\d+\.\d+)(?:\s|$)/i)?.[1];
1468
+ const commandName = basename(normalize(command));
1469
+ const relatedToGazeSight = /(?:^|[\\/])gazesight(?:-mcp)?(?:\.cmd|\.exe)?$/i.test(command) || /(?:^|[\\/])dist[\\/]cli[\\/]index\.js(?:\s|$)/i.test(argsText) || /(?:^|\s)gazesight(?:@\d+\.\d+\.\d+)?(?:\s|$)/i.test(argsText) || ["npx", "npx.cmd", "npx.exe"].includes(commandName) && /gazesight/i.test(argsText);
1470
+ return {
1471
+ command,
1472
+ argsText,
1473
+ args: parseKnownGazeSightArgs(command, argsText),
1474
+ scope: agent === "claude" ? parseScope(output) : "user",
1475
+ version: explicitVersion ?? versionFromPackageRoot(argsText),
1476
+ obsoleteNpxCache: /(?:^|[\\/])_npx(?:[\\/]|$)/i.test(combined),
1477
+ relatedToGazeSight
1478
+ };
1479
+ }
1480
+ function configurationsMatch(existing, proposed) {
1481
+ return !existing.obsoleteNpxCache && normalize(existing.command) === normalize(proposed.command) && normalize(existing.argsText) === normalize(proposed.args.join(" ")) && (!existing.version || existing.version === proposed.version);
1482
+ }
1483
+ function display(config) {
1484
+ if ("argsText" in config)
1485
+ return `command=${JSON.stringify(config.command)} args=${JSON.stringify(config.argsText)}${config.version ? ` version=${config.version}` : " version=unknown"} scope=${config.scope}`;
1486
+ return `command=${JSON.stringify(config.command)} args=${JSON.stringify(config.args.join(" "))} version=${config.version}`;
1487
+ }
1488
+ function failure(result) {
1489
+ return (result.stderr || result.stdout || "unknown error").trim().split(/\r?\n/)[0];
1490
+ }
1491
+ function configureAgent(agent, binaryPath, proposed, allowUpdate, spawn = (command, commandArgs) => spawnSync(command, commandArgs, { encoding: "utf8", windowsHide: true })) {
1492
+ const get = spawn(binaryPath, ["mcp", "get", "gazesight"]);
1493
+ const existing = get.status === 0 ? parseRegisteredConfig(agent, get.stdout) : void 0;
1494
+ if (!existing) {
1495
+ const addArgs2 = [
1496
+ "mcp",
1497
+ "add",
1498
+ "gazesight",
1499
+ ...agent === "claude" ? ["--scope", "user"] : [],
1500
+ "--",
1501
+ proposed.command,
1502
+ ...proposed.args
1503
+ ];
1504
+ const add2 = spawn(binaryPath, addArgs2);
1505
+ return add2.status === 0 ? { ok: true, state: "configured", message: "Configured." } : { ok: false, state: "failed", message: `Failed: ${failure(add2)}` };
1506
+ }
1507
+ if (!existing.relatedToGazeSight)
1508
+ return {
1509
+ ok: false,
1510
+ state: "conflict",
1511
+ message: `Refusing to overwrite an unrelated MCP entry named gazesight. Existing: ${display(existing)}`
1512
+ };
1513
+ if (!existing.args && allowUpdate)
1514
+ return {
1515
+ ok: false,
1516
+ state: "conflict",
1517
+ message: `Refusing an update because the existing GazeSight arguments cannot be reconstructed safely for rollback. Existing: ${display(existing)}`
1518
+ };
1519
+ if (configurationsMatch(existing, proposed))
1520
+ return { ok: true, state: "identical", message: "Already configured (identical; unchanged)." };
1521
+ const comparison = `Configuration differs.
1522
+ Existing: ${display(existing)}
1523
+ Proposed: ${display(proposed)}`;
1524
+ if (!allowUpdate)
1525
+ return {
1526
+ ok: false,
1527
+ state: "outdated",
1528
+ message: `${comparison}
1529
+ Re-run setup with --update to replace only this GazeSight entry while preserving its ${existing.scope} scope.`
1530
+ };
1531
+ const removeArgs = [
1532
+ "mcp",
1533
+ "remove",
1534
+ "gazesight",
1535
+ ...agent === "claude" ? ["--scope", existing.scope] : []
1536
+ ];
1537
+ const remove = spawn(binaryPath, removeArgs);
1538
+ if (remove.status !== 0)
1539
+ return { ok: false, state: "failed", message: `${comparison}
1540
+ Update failed before replacement: ${failure(remove)}` };
1541
+ const addArgs = [
1542
+ "mcp",
1543
+ "add",
1544
+ "gazesight",
1545
+ ...agent === "claude" ? ["--scope", existing.scope] : [],
1546
+ "--",
1547
+ proposed.command,
1548
+ ...proposed.args
1549
+ ];
1550
+ const add = spawn(binaryPath, addArgs);
1551
+ if (add.status === 0)
1552
+ return { ok: true, state: "updated", message: `${comparison}
1553
+ Updated safely.` };
1554
+ const restoreArgs = [
1555
+ "mcp",
1556
+ "add",
1557
+ "gazesight",
1558
+ ...agent === "claude" ? ["--scope", existing.scope] : [],
1559
+ "--",
1560
+ existing.command,
1561
+ ...existing.args
1562
+ ];
1563
+ const restore = spawn(binaryPath, restoreArgs);
1564
+ return {
1565
+ ok: false,
1566
+ state: "failed",
1567
+ message: restore.status === 0 ? `${comparison}
1568
+ Replacement failed: ${failure(add)}. The previous entry was restored.` : `${comparison}
1569
+ Replacement failed: ${failure(add)}. Automatic restoration also failed: ${failure(restore)}. Follow the displayed existing configuration to restore it manually.`
1570
+ };
1571
+ }
1572
+
151
1573
  // apps/cli/src/index.ts
152
- var root = resolve(dirname2(fileURLToPath(import.meta.url)), "..");
1574
+ var root = resolve2(dirname2(fileURLToPath(import.meta.url)), "..");
153
1575
  var args = process.argv.slice(2);
154
1576
  function option(name) {
155
1577
  const i = args.indexOf(name);
@@ -162,13 +1584,13 @@ function detectOnPath(binary) {
162
1584
  for (const directory of (process.env.PATH ?? "").split(delimiter))
163
1585
  for (const name of [binary, `${binary}.exe`, `${binary}.cmd`]) {
164
1586
  const candidate = join2(directory, name);
165
- if (existsSync(candidate)) return candidate;
1587
+ if (existsSync2(candidate)) return candidate;
166
1588
  }
167
1589
  return void 0;
168
1590
  }
169
1591
  async function promptClientChoice() {
170
1592
  if (!process.stdin.isTTY) return ["claude", "codex"];
171
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1593
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
172
1594
  try {
173
1595
  const answer = (await rl.question(
174
1596
  "Configure MCP for:\n 1) Claude Code\n 2) Codex\n 3) Both\nEnter choice [1-3] (default 3): "
@@ -180,38 +1602,8 @@ async function promptClientChoice() {
180
1602
  rl.close();
181
1603
  }
182
1604
  }
183
- function configureAgent(agent, binaryPath, serverArgs) {
184
- if (agent === "claude") {
185
- const existing = spawnSync(binaryPath, ["mcp", "get", "gazesight"], {
186
- encoding: "utf8"
187
- });
188
- if (existing.status === 0)
189
- return { ok: true, message: "Already configured (unchanged)." };
190
- const add2 = spawnSync(
191
- binaryPath,
192
- ["mcp", "add", "gazesight", "--scope", "user", "--", ...serverArgs],
193
- { encoding: "utf8" }
194
- );
195
- if (add2.status === 0) return { ok: true, message: "Configured." };
196
- return {
197
- ok: false,
198
- message: `Failed: ${(add2.stderr || add2.stdout || "unknown error").trim().split("\n")[0]}`
199
- };
200
- }
201
- const list = spawnSync(binaryPath, ["mcp", "list"], { encoding: "utf8" });
202
- if (list.status === 0 && list.stdout.includes("gazesight"))
203
- return { ok: true, message: "Already configured (unchanged)." };
204
- const add = spawnSync(binaryPath, ["mcp", "add", "gazesight", "--", ...serverArgs], {
205
- encoding: "utf8"
206
- });
207
- if (add.status === 0) return { ok: true, message: "Configured." };
208
- return {
209
- ok: false,
210
- message: `Failed: ${(add.stderr || add.stdout || "unknown error").trim().split("\n")[0]}`
211
- };
212
- }
213
1605
  try {
214
- const configPath = resolve(
1606
+ const configPath = resolve2(
215
1607
  option("--config") ?? process.env.GAZESIGHT_CONFIG_FILE ?? defaultConfigPath()
216
1608
  );
217
1609
  if (args[0] === "mcp") {
@@ -221,23 +1613,31 @@ try {
221
1613
  else if (!args[0] || args.includes("--help"))
222
1614
  console.log(
223
1615
  `GazeSight ${VERSION}
224
- gazesight setup [--config PATH] [--port PORT] [--client claude|codex|both]
1616
+ gazesight setup [--config PATH] [--port PORT] [--client claude|codex|both] [--update]
1617
+ gazesight pair [--config PATH] [--revoke ID|all]
225
1618
  gazesight doctor [--config PATH]
226
1619
  gazesight mcp [--config PATH]
1620
+ --update replaces only an outdated GazeSight MCP entry after showing the existing and proposed configurations.
1621
+ Claude Code and Codex may both be configured, but only one MCP process can own a configured bridge port at a time. A second process exits with GS_PORT_IN_USE; close the first client session or configure a separate port.
227
1622
  No browser is launched. Secrets are never printed.`
228
1623
  );
229
1624
  else {
230
- if (!["setup", "doctor"].includes(args[0]))
1625
+ if (!["setup", "doctor", "pair"].includes(args[0]))
231
1626
  throw new Error("Unknown command.");
232
- if (args.slice(1).some(
233
- (value, i) => i % 2 === 0 && !["--config", "--port", "--client"].includes(value)
234
- ))
235
- throw new Error("Unknown option.");
1627
+ const valueOptions = /* @__PURE__ */ new Set(["--config", "--port", "--client", "--revoke"]);
1628
+ const flagOptions = /* @__PURE__ */ new Set(["--update"]);
1629
+ for (let i = 1; i < args.length; i++) {
1630
+ const value = args[i];
1631
+ if (flagOptions.has(value)) continue;
1632
+ if (!valueOptions.has(value) || !args[i + 1] || args[i + 1].startsWith("--"))
1633
+ throw new Error("Unknown option or missing option value.");
1634
+ i++;
1635
+ }
236
1636
  const clientOption = option("--client");
237
1637
  if (clientOption && !["claude", "codex", "both"].includes(clientOption))
238
1638
  throw new Error("--client must be claude, codex, or both.");
239
1639
  const node = Number(process.versions.node.split(".")[0]) >= 20;
240
- const build = existsSync(join2(root, "mcp/index.js")) && existsSync(join2(root, "firefox/manifest.json"));
1640
+ const build = existsSync2(join2(root, "mcp/index.js")) && existsSync2(join2(root, "firefox/manifest.json"));
241
1641
  console.log(
242
1642
  `Node: ${node ? "PASS" : "FAIL"}
243
1643
  GazeSight build: ${build ? "PASS" : "FAIL"}`
@@ -262,6 +1662,15 @@ Agent guidance: ${config.agentGuidance.enabled ? "Enabled" : "Disabled"}${config
262
1662
  "--config",
263
1663
  configPath
264
1664
  ];
1665
+ if (chosen.length > 1)
1666
+ console.log(
1667
+ "Shared-use notice: Claude Code and Codex will both be configured, but they cannot use the same bridge port simultaneously. The first MCP process owns the port; a second exits explicitly with GS_PORT_IN_USE. Close the first client session before starting the other, or configure a separate port/config."
1668
+ );
1669
+ const proposed = {
1670
+ command: serverArgs[0],
1671
+ args: serverArgs.slice(1),
1672
+ version: VERSION
1673
+ };
265
1674
  for (const agent of chosen) {
266
1675
  const label = agent === "claude" ? "Claude Code" : "Codex";
267
1676
  const binaryPath = detectOnPath(agent);
@@ -269,7 +1678,7 @@ Agent guidance: ${config.agentGuidance.enabled ? "Enabled" : "Disabled"}${config
269
1678
  console.log(`${label}: not detected on PATH \u2014 configure manually below.`);
270
1679
  continue;
271
1680
  }
272
- const result2 = configureAgent(agent, binaryPath, serverArgs);
1681
+ const result2 = configureAgent(agent, binaryPath, proposed, args.includes("--update"));
273
1682
  console.log(`${label}: ${result2.ok ? "PASS" : "FAIL"} \u2014 ${result2.message}`);
274
1683
  }
275
1684
  const undetected = explicit ? chosen.filter((agent) => !detectOnPath(agent)) : ["claude", "codex"];
@@ -291,21 +1700,24 @@ args = ${JSON.stringify(server.args)}`
291
1700
  }
292
1701
  }
293
1702
  console.log(
294
- `Pairing: Firefox options > Import local configuration > ${configPath} > Enregistrer. The token stays in the private file.
295
- Chrome (manual install, not yet on the Chrome Web Store): open chrome://extensions, enable Developer mode, "Load unpacked", select ${join2(root, "chrome")}, then Options > Import local configuration.
296
- Development only (Firefox): about:debugging > Load Temporary Add-on > ${join2(root, "firefox/manifest.json")}
297
- Unsigned XPI: ${join2(root, "gazesight-firefox.xpi")}
298
- External Firefox Release/Beta installation requires the separately supplied Mozilla-signed XPI.
299
- Start your coding agent, open your local app in your browser, click GazeSight, then run gazesight doctor.`
1703
+ `Open your GazeSight browser extension and click Connect to GazeSight.
1704
+ Firefox: install or update the signed extension from the official listing \u2014 https://addons.mozilla.org/addon/6a2319d4fa544731884d/ (matches gazesight@${VERSION}).
1705
+ Chrome (manual install, not yet on the Chrome Web Store): open chrome://extensions, enable Developer mode, "Load unpacked", select ${join2(root, "chrome")}.
1706
+ Development only (building from this checkout, Firefox): about:debugging > Load Temporary Add-on > ${join2(root, "firefox/manifest.json")}, or install the unsigned XPI at ${join2(root, "gazesight-firefox.xpi")}.
1707
+ Manual JSON import remains available under Advanced settings.`
300
1708
  );
1709
+ if (process.stdin.isTTY) await pairingFlow(config, configPath);
1710
+ }
1711
+ if (args[0] === "pair") {
1712
+ await pairingFlow(config, configPath, option("--revoke"));
301
1713
  }
302
- const result = await probe(config.port, config.token);
1714
+ const result = args[0] === "pair" ? void 0 : await probe(config.port, config.token);
303
1715
  const rows = diagnosticRows(
304
- result.status,
305
- result.reachable,
306
- result.authenticated
1716
+ result?.status,
1717
+ result?.reachable ?? false,
1718
+ result?.authenticated ?? false
307
1719
  );
308
- for (const row of rows)
1720
+ if (args[0] !== "pair") for (const row of rows)
309
1721
  console.log(
310
1722
  "status" in row ? `${row.name}: ${row.status}` : `${row.name}: ${row.pass ? "PASS" : `FAIL \u2014 ${row.action}`}`
311
1723
  );