gazesight 0.4.0 → 0.5.1

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,16 +1,15 @@
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";
4
+ import { existsSync as existsSync2 } from "node:fs";
6
5
  import { createInterface as createInterface2 } from "node:readline/promises";
7
- import { resolve, dirname as dirname2, join as join2, delimiter } from "node:path";
6
+ import { resolve as resolve2, dirname as dirname2, join as join2 } 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
11
  import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
13
- import { dirname, join } from "node:path";
12
+ import { dirname, posix, win32 } from "node:path";
14
13
  import { homedir } from "node:os";
15
14
 
16
15
  // apps/cli/src/config-browser.ts
@@ -42,22 +41,23 @@ var localConfigSchema = z.strictObject({
42
41
 
43
42
  // apps/cli/src/config.ts
44
43
  function configPathFor(platform, home, env) {
44
+ const pathJoin = platform === "win32" ? win32.join : posix.join;
45
45
  if (platform === "win32")
46
- return join(
47
- env.APPDATA ?? join(home, "AppData", "Roaming"),
46
+ return pathJoin(
47
+ env.APPDATA ?? pathJoin(home, "AppData", "Roaming"),
48
48
  "GazeSight",
49
49
  "config.json"
50
50
  );
51
51
  if (platform === "darwin")
52
- return join(
52
+ return pathJoin(
53
53
  home,
54
54
  "Library",
55
55
  "Application Support",
56
56
  "GazeSight",
57
57
  "config.json"
58
58
  );
59
- return join(
60
- env.XDG_CONFIG_HOME ?? join(home, ".config"),
59
+ return pathJoin(
60
+ env.XDG_CONFIG_HOME ?? pathJoin(home, ".config"),
61
61
  "gazesight",
62
62
  "config.json"
63
63
  );
@@ -102,7 +102,7 @@ function writeLocalConfig(path, value) {
102
102
  }
103
103
 
104
104
  // packages/protocol/src/release.ts
105
- var VERSION = "0.4.0";
105
+ var VERSION = "0.5.1";
106
106
 
107
107
  // apps/cli/src/doctor.ts
108
108
  import { z as z2 } from "zod";
@@ -326,9 +326,20 @@ var fullPageCaptureSchema = captureSchema.extend({
326
326
  });
327
327
  var navigateSchema = targetSchema.extend({ url: z3.url() });
328
328
  var diffSchema = z3.strictObject({
329
- baseline: z3.uuid(),
329
+ baseline: z3.uuid().optional().describe(
330
+ "A prior PNG imageReference from this session. Omit and use referenceImagePath instead to diff against an external design reference."
331
+ ),
332
+ referenceImagePath: z3.string().min(1).max(4096).optional().describe(
333
+ "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."
334
+ ),
330
335
  current: z3.uuid().optional(),
331
- threshold: z3.number().finite().min(0).max(1).default(0.1)
336
+ tabId: z3.number().int().nonnegative().optional().describe(
337
+ "Only used with referenceImagePath when current is omitted, to select which tab to capture. Ignored otherwise (baseline's own tab is used)."
338
+ ),
339
+ threshold: z3.number().finite().min(0).max(1).default(0.1),
340
+ checkpoint: z3.string().min(1).max(200).regex(/^[A-Za-z0-9_-]+$/).optional().describe(
341
+ "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."
342
+ )
332
343
  });
333
344
  var viewportProfileSchema = z3.enum([
334
345
  "mobile-small",
@@ -341,7 +352,10 @@ var setViewportSchema = targetSchema.extend({
341
352
  profile: viewportProfileSchema.optional(),
342
353
  width: z3.number().int().min(200).max(8192).optional(),
343
354
  height: z3.number().int().min(200).max(8192).optional(),
344
- dedicatedPreview: z3.boolean().default(true)
355
+ dedicatedPreview: z3.boolean().default(true),
356
+ close: z3.boolean().optional().describe(
357
+ "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."
358
+ )
345
359
  });
346
360
  var viewportResultSchema = z3.strictObject({
347
361
  tabId: z3.number().int().nonnegative(),
@@ -363,7 +377,10 @@ var viewportResultSchema = z3.strictObject({
363
377
  exact: z3.boolean().optional(),
364
378
  attempts: z3.number().int().min(0).max(3).optional(),
365
379
  requiresTargetSelection: z3.boolean().optional(),
366
- sourceWindowUnchanged: z3.boolean().optional()
380
+ sourceWindowUnchanged: z3.boolean().optional(),
381
+ previewClosed: z3.boolean().optional().describe(
382
+ "Present when close:true was requested. True if a dedicated preview window was actually open and closed; false if there was none to close."
383
+ )
367
384
  });
368
385
  var domSchema = targetSchema.extend({
369
386
  selector: z3.string().min(1).max(2048).optional(),
@@ -703,7 +720,7 @@ var RequestRouter = class {
703
720
  type,
704
721
  payload
705
722
  });
706
- return new Promise((resolve2, reject) => {
723
+ return new Promise((resolve3, reject) => {
707
724
  const timer = setTimeout(() => {
708
725
  this.pending.delete(request.id);
709
726
  reject(
@@ -713,7 +730,7 @@ var RequestRouter = class {
713
730
  )
714
731
  );
715
732
  }, this.timeoutMs);
716
- this.pending.set(request.id, { resolve: resolve2, reject, timer });
733
+ this.pending.set(request.id, { resolve: resolve3, reject, timer });
717
734
  try {
718
735
  this.send(request);
719
736
  } catch (error) {
@@ -1092,6 +1109,7 @@ var PairingService = class {
1092
1109
 
1093
1110
  // apps/mcp-server/src/bridge/websocket-server.ts
1094
1111
  var EMPTY_STATE = { selectedTab: null, url: null };
1112
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 2e4;
1095
1113
  function browserFromOrigin(origin) {
1096
1114
  if (origin === void 0) return void 0;
1097
1115
  if (/^moz-extension:\/\/[a-f0-9-]+\/?$/i.test(origin)) return "firefox";
@@ -1121,6 +1139,12 @@ var Bridge = class {
1121
1139
  response.setHeader("Content-Type", "application/json");
1122
1140
  response.end(JSON.stringify(this.status()));
1123
1141
  });
1142
+ this.http.once("error", (error) => {
1143
+ this.listenError = error.code === "EADDRINUSE" ? new GSError(
1144
+ "GS_PORT_IN_USE",
1145
+ `Port ${config.port} is already in use by another GazeSight MCP server. Only one can run at a time -- close the other session first.`
1146
+ ) : error;
1147
+ });
1124
1148
  this.server = new WebSocketServer({
1125
1149
  server: this.http,
1126
1150
  maxPayload: 17 * 1024 * 1024,
@@ -1143,6 +1167,11 @@ var Bridge = class {
1143
1167
  this.wiring.delete(connection.connectionId);
1144
1168
  this.registry.remove(connection.connectionId);
1145
1169
  };
1170
+ this.alive.set(socket, { pongReceived: true });
1171
+ socket.on("pong", () => {
1172
+ const state = this.alive.get(socket);
1173
+ if (state) state.pongReceived = true;
1174
+ });
1146
1175
  socket.on("error", () => {
1147
1176
  });
1148
1177
  socket.on("message", (bytes, binary) => {
@@ -1197,12 +1226,31 @@ var Bridge = class {
1197
1226
  });
1198
1227
  socket.on("close", teardown);
1199
1228
  });
1229
+ this.heartbeat = setInterval(() => {
1230
+ for (const socket of this.server.clients) {
1231
+ const state = this.alive.get(socket);
1232
+ if (!state) continue;
1233
+ if (!state.pongReceived) {
1234
+ socket.terminate();
1235
+ continue;
1236
+ }
1237
+ state.pongReceived = false;
1238
+ socket.ping();
1239
+ }
1240
+ }, config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS);
1241
+ this.heartbeat.unref?.();
1200
1242
  }
1201
1243
  registry = new BrowserConnectionRegistry();
1202
1244
  activeTargetRouter = new ActiveTargetRouter();
1203
1245
  wiring = /* @__PURE__ */ new Map();
1246
+ alive = /* @__PURE__ */ new WeakMap();
1247
+ heartbeat;
1204
1248
  server;
1205
1249
  http;
1250
+ listenError;
1251
+ closePromise;
1252
+ mcpClientCount = 0;
1253
+ desktopIpcReady = false;
1206
1254
  pairing;
1207
1255
  // Resolves the connection currently backing the global active target, or
1208
1256
  // undefined if no browser is active (nothing selected yet, or the
@@ -1265,6 +1313,8 @@ var Bridge = class {
1265
1313
  })
1266
1314
  );
1267
1315
  return {
1316
+ mcpClients: this.mcpClientCount,
1317
+ desktopIpcReady: this.desktopIpcReady,
1268
1318
  connected: this.connected,
1269
1319
  paired: this.connected,
1270
1320
  authorizedPairings: this.pairing.listPairings().filter((item) => !item.revokedAt).length,
@@ -1277,18 +1327,47 @@ var Bridge = class {
1277
1327
  activeTarget: active ? { browser: active.browser, url: active.state.url } : null
1278
1328
  };
1279
1329
  }
1330
+ setMcpClientCount(count) {
1331
+ this.mcpClientCount = Math.max(0, Math.min(32, Math.floor(count)));
1332
+ }
1333
+ setDesktopIpcReady(ready) {
1334
+ this.desktopIpcReady = ready;
1335
+ }
1280
1336
  async ready() {
1337
+ if (this.listenError) throw this.listenError;
1281
1338
  if (this.server.address()) return;
1282
- await new Promise((resolve2, reject) => {
1283
- this.server.once("listening", resolve2);
1284
- this.server.once("error", reject);
1339
+ await new Promise((resolve3, reject) => {
1340
+ const cleanup = () => {
1341
+ this.server.off("listening", onListening);
1342
+ this.server.off("error", onError);
1343
+ this.http.off("error", onError);
1344
+ };
1345
+ const onListening = () => {
1346
+ cleanup();
1347
+ resolve3();
1348
+ };
1349
+ const onError = () => {
1350
+ cleanup();
1351
+ reject(this.listenError);
1352
+ };
1353
+ this.server.once("listening", onListening);
1354
+ this.server.once("error", onError);
1355
+ this.http.once("error", onError);
1285
1356
  });
1286
1357
  }
1287
1358
  async close() {
1288
- for (const wiring of this.wiring.values()) wiring.router.disconnect();
1289
- for (const client of this.server.clients) client.terminate();
1290
- await new Promise((resolve2) => this.server.close(() => resolve2()));
1291
- await new Promise((resolve2) => this.http.close(() => resolve2()));
1359
+ if (this.closePromise) return this.closePromise;
1360
+ this.closePromise = (async () => {
1361
+ clearInterval(this.heartbeat);
1362
+ for (const wiring of this.wiring.values()) wiring.router.disconnect();
1363
+ this.wiring.clear();
1364
+ for (const client of this.server.clients) client.terminate();
1365
+ if (this.http.listening)
1366
+ await new Promise((resolve3) => this.http.close(() => resolve3()));
1367
+ else if (this.server.address())
1368
+ await new Promise((resolve3) => this.server.close(() => resolve3()));
1369
+ })();
1370
+ return this.closePromise;
1292
1371
  }
1293
1372
  };
1294
1373
 
@@ -1334,7 +1413,7 @@ async function pairingFlow(config, configPath, revoke) {
1334
1413
  const requests = (await response.json()).requests;
1335
1414
  const request = requests[0];
1336
1415
  if (!request) {
1337
- await new Promise((resolve2) => setTimeout(resolve2, 500));
1416
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
1338
1417
  continue;
1339
1418
  }
1340
1419
  console.log(`Pairing request: ${request.browser} \xB7 verification code ${request.code}`);
@@ -1351,7 +1430,7 @@ async function pairingFlow(config, configPath, revoke) {
1351
1430
  delivered = true;
1352
1431
  break;
1353
1432
  }
1354
- await new Promise((resolve2) => setTimeout(resolve2, 100));
1433
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
1355
1434
  }
1356
1435
  if (!delivered) throw new Error("The approved credential was not delivered; keep the extension open and retry pairing.");
1357
1436
  console.log("Pairing approved and delivered. The extension can now reconnect securely.");
@@ -1366,8 +1445,314 @@ async function pairingFlow(config, configPath, revoke) {
1366
1445
  }
1367
1446
  }
1368
1447
 
1448
+ // apps/agent-integration/src/index.ts
1449
+ import { spawn as nodeSpawn } from "node:child_process";
1450
+ import { constants as fsConstants, existsSync, readFileSync as readFileSync2 } from "node:fs";
1451
+ import { access, realpath, stat } from "node:fs/promises";
1452
+ import { delimiter, join, posix as posix2, resolve, win32 as win322 } from "node:path";
1453
+ var PROCESS_TIMEOUT_MS = 12e3;
1454
+ var PROCESS_OUTPUT_BYTES = 16 * 1024;
1455
+ function createBoundedRunner(spawnProcess = nodeSpawn) {
1456
+ return (command, args2, options = {}) => runBounded(command, args2, options, spawnProcess);
1457
+ }
1458
+ var DEFAULT_SPAWN = createBoundedRunner();
1459
+ var runAgentCommand = DEFAULT_SPAWN;
1460
+ function runBounded(command, args2, options = {}, spawnProcess = nodeSpawn) {
1461
+ return new Promise((resolveResult) => {
1462
+ let child;
1463
+ try {
1464
+ child = spawnProcess(command, [...args2], {
1465
+ shell: false,
1466
+ windowsHide: true,
1467
+ cwd: options.cwd,
1468
+ env: options.env ?? process.env,
1469
+ stdio: ["ignore", "pipe", "pipe"]
1470
+ });
1471
+ } catch {
1472
+ resolveResult({ code: null, stdout: "", stderr: "", timedOut: false });
1473
+ return;
1474
+ }
1475
+ let stdout = "";
1476
+ let stderr = "";
1477
+ let timedOut = false;
1478
+ const append = (current, chunk) => {
1479
+ const value = current + String(chunk);
1480
+ return Buffer.byteLength(value) <= PROCESS_OUTPUT_BYTES ? value : Buffer.from(value).subarray(0, PROCESS_OUTPUT_BYTES).toString();
1481
+ };
1482
+ child.stdout?.on("data", (chunk) => {
1483
+ stdout = append(stdout, chunk);
1484
+ });
1485
+ child.stderr?.on("data", (chunk) => {
1486
+ stderr = append(stderr, chunk);
1487
+ });
1488
+ const timer = setTimeout(() => {
1489
+ timedOut = true;
1490
+ child.kill();
1491
+ }, options.timeoutMs ?? PROCESS_TIMEOUT_MS);
1492
+ child.once("error", () => {
1493
+ clearTimeout(timer);
1494
+ resolveResult({ code: null, stdout, stderr, timedOut });
1495
+ });
1496
+ child.once("close", (code) => {
1497
+ clearTimeout(timer);
1498
+ resolveResult({ code, stdout, stderr, timedOut });
1499
+ });
1500
+ });
1501
+ }
1502
+ function pathCandidates(agent, platform, env, home) {
1503
+ const name = agent === "claude" ? "claude" : "codex";
1504
+ const pathJoin = (platform === "win32" ? win322 : posix2).join;
1505
+ const native = [];
1506
+ const packageScripts = [];
1507
+ if (platform === "win32") {
1508
+ const packageManagerBins = [env.PNPM_HOME, env.NPM_CONFIG_PREFIX].filter((value) => Boolean(value)).flatMap((root2) => [pathJoin(root2, `${name}.exe`), pathJoin(root2, `${name}.cmd`)]);
1509
+ if (agent === "claude") {
1510
+ native.push(
1511
+ pathJoin(home, ".local", "bin", "claude.exe"),
1512
+ pathJoin(env.LOCALAPPDATA ?? pathJoin(home, "AppData", "Local"), "Programs", "Claude", "claude.exe"),
1513
+ pathJoin(env.ProgramFiles ?? "C:\\Program Files", "Claude", "claude.exe")
1514
+ );
1515
+ } else {
1516
+ native.push(
1517
+ pathJoin(env.LOCALAPPDATA ?? pathJoin(home, "AppData", "Local"), "Programs", "OpenAI", "Codex", "bin", "codex.exe")
1518
+ );
1519
+ }
1520
+ const npm = pathJoin(env.APPDATA ?? pathJoin(home, "AppData", "Roaming"), "npm", `${name}.cmd`);
1521
+ const modules = [
1522
+ pathJoin(env.APPDATA ?? pathJoin(home, "AppData", "Roaming"), "npm", "node_modules"),
1523
+ ...env.NPM_CONFIG_PREFIX ? [pathJoin(env.NPM_CONFIG_PREFIX, "node_modules")] : [],
1524
+ ...env.PNPM_HOME ? [pathJoin(env.PNPM_HOME, "global", "5", "node_modules"), pathJoin(env.PNPM_HOME, "node_modules")] : [],
1525
+ pathJoin(home, "AppData", "Local", "pnpm", "global", "5", "node_modules")
1526
+ ];
1527
+ const packageName = agent === "claude" ? "@anthropic-ai/claude-code" : "@openai/codex";
1528
+ const nodeCandidates = [
1529
+ env.ProgramFiles ? pathJoin(env.ProgramFiles, "nodejs", "node.exe") : "C:\\Program Files\\nodejs\\node.exe",
1530
+ ...env.LOCALAPPDATA ? [pathJoin(env.LOCALAPPDATA, "Programs", "nodejs", "node.exe")] : [],
1531
+ ...(env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean).map((directory) => pathJoin(directory, "node.exe"))
1532
+ ];
1533
+ for (const modulesRoot of modules) {
1534
+ const root2 = pathJoin(modulesRoot, packageName);
1535
+ try {
1536
+ const metadata = JSON.parse(readFileSync2(pathJoin(root2, "package.json"), "utf8"));
1537
+ const bin = typeof metadata.bin === "string" ? metadata.bin : metadata.bin && typeof metadata.bin === "object" ? metadata.bin[name] : void 0;
1538
+ if (typeof bin !== "string" || bin.startsWith("/") || bin.split(/[\\/]/).includes("..")) continue;
1539
+ const script = pathJoin(root2, bin);
1540
+ for (const node of nodeCandidates)
1541
+ packageScripts.push({ command: node, prefixArgs: [script], source: "verified npm package" });
1542
+ } catch {
1543
+ }
1544
+ }
1545
+ native.push(npm, ...packageManagerBins);
1546
+ } else {
1547
+ native.push(
1548
+ pathJoin(home, ".local", "bin", name),
1549
+ pathJoin(home, ".npm-global", "bin", name),
1550
+ pathJoin(home, ".local", "share", "pnpm", name),
1551
+ ...env.PNPM_HOME ? [pathJoin(env.PNPM_HOME, name)] : [],
1552
+ ...env.NPM_CONFIG_PREFIX ? [pathJoin(env.NPM_CONFIG_PREFIX, "bin", name)] : []
1553
+ );
1554
+ if (platform === "darwin")
1555
+ native.push(
1556
+ pathJoin(home, "Library", "pnpm", name),
1557
+ "/opt/homebrew/bin/" + name,
1558
+ "/usr/local/bin/" + name
1559
+ );
1560
+ else if (platform === "linux")
1561
+ native.push("/usr/bin/" + name, "/usr/local/bin/" + name);
1562
+ }
1563
+ const fromPath = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean).flatMap(
1564
+ (directory) => platform === "win32" ? [pathJoin(directory, `${name}.exe`), pathJoin(directory, `${name}.cmd`)] : [pathJoin(directory, name)]
1565
+ );
1566
+ const seen = /* @__PURE__ */ new Set();
1567
+ const direct = [...native, ...fromPath].filter((path) => {
1568
+ const key = platform === "win32" ? path.toLowerCase() : path;
1569
+ if (seen.has(key)) return false;
1570
+ seen.add(key);
1571
+ return true;
1572
+ }).map((command) => ({ command, prefixArgs: [], source: "candidate" }));
1573
+ return [...packageScripts, ...direct];
1574
+ }
1575
+ async function verifyCandidate(candidate, platform) {
1576
+ try {
1577
+ const canonical = await realpath(candidate.command);
1578
+ if (canonical.includes("..")) return void 0;
1579
+ const info = await stat(canonical);
1580
+ if (!info.isFile()) return void 0;
1581
+ for (const argument of candidate.prefixArgs) {
1582
+ if (!argument.toLowerCase().endsWith(".js")) continue;
1583
+ const script = await realpath(argument);
1584
+ if (!(await stat(script)).isFile()) return void 0;
1585
+ if (platform === "win32" && !script.toLowerCase().endsWith(".js")) return void 0;
1586
+ }
1587
+ if (platform !== "win32") {
1588
+ await access(canonical, fsConstants.X_OK);
1589
+ if ((info.mode & 73) === 0) return void 0;
1590
+ } else if (!/\.(exe|cmd)$/i.test(canonical)) return void 0;
1591
+ return { ...candidate, command: canonical };
1592
+ } catch {
1593
+ return void 0;
1594
+ }
1595
+ }
1596
+ function parseVersion(agent, output) {
1597
+ const value = output.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/)?.[1];
1598
+ if (!value) return void 0;
1599
+ if (agent === "claude" && Number(value.split(".")[0]) < 2) return void 0;
1600
+ return value;
1601
+ }
1602
+ async function findAgentBinary(id, options = {}) {
1603
+ const platform = options.platform ?? process.platform;
1604
+ const env = options.env ?? process.env;
1605
+ const home = options.home ?? env.USERPROFILE ?? env.HOME ?? "";
1606
+ const run = options.spawn ?? DEFAULT_SPAWN;
1607
+ let sawCandidate = false;
1608
+ let sawDiscoveryError = false;
1609
+ for (const pathCandidate of pathCandidates(id, platform, env, home)) {
1610
+ const candidate = await verifyCandidate(pathCandidate, platform);
1611
+ if (!candidate) continue;
1612
+ sawCandidate = true;
1613
+ const versionResult = await run(candidate.command, [...candidate.prefixArgs, "--version"], { timeoutMs: PROCESS_TIMEOUT_MS, cwd: home });
1614
+ const version = versionResult.code === 0 ? parseVersion(id, `${versionResult.stdout}
1615
+ ${versionResult.stderr}`) : void 0;
1616
+ const help = await run(candidate.command, [...candidate.prefixArgs, "mcp", "--help"], { timeoutMs: PROCESS_TIMEOUT_MS, cwd: home });
1617
+ if (help.code === 0 && /\badd\b/i.test(help.stdout + help.stderr) && /\bremove\b/i.test(help.stdout + help.stderr))
1618
+ return { candidate, installation: version ? "INSTALLED" : "INSTALLED_VERSION_UNKNOWN", ...version ? { version } : {} };
1619
+ if (!version || help.timedOut || versionResult.timedOut) sawDiscoveryError = true;
1620
+ }
1621
+ return {
1622
+ installation: sawDiscoveryError ? "DISCOVERY_ERROR" : sawCandidate ? "UNSUPPORTED_VERSION" : "NOT_INSTALLED"
1623
+ };
1624
+ }
1625
+ function parseMcpEntry(agent, output) {
1626
+ const command = output.match(/^\s*(?:Command|command):\s*(.+)$/im)?.[1]?.trim();
1627
+ const args2 = output.match(/^\s*(?:Args|args):\s*(.*)$/im)?.[1]?.trim();
1628
+ if (!command || args2 === void 0) return void 0;
1629
+ const scopeText = output.match(/^\s*Scope:\s*(.+)$/im)?.[1]?.trim().toLowerCase() ?? "user";
1630
+ const scope = scopeText.match(/^(user|project|local)\b/)?.[1] ?? "user";
1631
+ const legacyNpm = /(?:^|\s)(?:npx(?:\.cmd)?\s+(?:(?:--yes|-y)\s+)?(?:gazesight|@gazesight\/mcp-server)|gazesight(?:@\d+\.\d+\.\d+)?\s+mcp)(?:\s|$)/i.test(`${command} ${args2}`) || /[\\/]_npx[\\/]/i.test(`${command} ${args2}`);
1632
+ const knownOwner = legacyNpm || /(?:^|[\\/])dist[\\/]cli[\\/]index\.js(?:\s|$)/i.test(args2) && /\bmcp\b/i.test(args2);
1633
+ const oldCli = args2.match(/^(.*?[\\/]dist[\\/]cli[\\/]index\.js)\s+mcp(?:\s+--config\s+(.+?))?(?:\s+(--desktop-adapter))?$/i);
1634
+ const oldNpm = args2.match(/^(--yes\s+)?(gazesight(?:@\d+\.\d+\.\d+)?|@gazesight\/mcp-server)(?:\s+--yes)?\s+mcp(?:\s+--config\s+(.+))?$/i);
1635
+ const argsVector = oldCli ? [oldCli[1], "mcp", ...oldCli[2] ? ["--config", oldCli[2]] : [], ...oldCli[3] ? [oldCli[3]] : []] : oldNpm ? [...oldNpm[1] ? ["--yes"] : [], oldNpm[2], "mcp", ...oldNpm[3] ? ["--config", oldNpm[3]] : []] : /(?:^|[\\/])gazesight(?:-mcp)?(?:\.cmd|\.exe)?$/i.test(command) && /^mcp\b/i.test(args2) ? ["mcp", ...args2.match(/^mcp\s+--config\s+(.+)$/i)?.[1] ? ["--config", args2.match(/^mcp\s+--config\s+(.+)$/i)[1]] : []] : args2.split(/\s+/).filter(Boolean);
1636
+ return { command, args: args2, argsVector, scope: agent === "claude" ? scope : "user", legacyNpm, knownOwner };
1637
+ }
1638
+ async function getEntry(agent, binary, run) {
1639
+ const result = await run(binary.command, [...binary.prefixArgs, "mcp", "get", "gazesight"], { timeoutMs: PROCESS_TIMEOUT_MS });
1640
+ const output = `${result.stdout}
1641
+ ${result.stderr}`;
1642
+ const headers2 = output.match(/(?:^|\n)\s*gazesight\s*:?\s*(?=\r?\n|$)/gi) ?? [];
1643
+ const entry = result.code === 0 && headers2.length <= 1 ? parseMcpEntry(agent, output) : void 0;
1644
+ const named = headers2.length > 0;
1645
+ return { entry, named, code: result.code, output };
1646
+ }
1647
+ function cliArgs(agent, operation, launch, scope) {
1648
+ if (operation === "remove") return agent === "claude" ? ["mcp", "remove", "gazesight", "-s", scope ?? "user"] : ["mcp", "remove", "gazesight"];
1649
+ const spec = launch;
1650
+ return agent === "claude" ? ["mcp", "add", "--transport", "stdio", "-s", scope ?? "user", "gazesight", "--", spec.command, ...spec.args] : ["mcp", "add", "gazesight", "--", spec.command, ...spec.args];
1651
+ }
1652
+ function sameLaunch(entry, launch) {
1653
+ const norm = (value) => {
1654
+ return value.trim().replace(/^['"]|['"]$/g, "").replaceAll("\\", "/").toLowerCase();
1655
+ };
1656
+ return norm(entry.command) === norm(launch.command) && norm(entry.args) === norm(launch.args.join(" "));
1657
+ }
1658
+ async function verifyCurrent(agent, binary, launch, run) {
1659
+ const result = await getEntry(agent, binary, run);
1660
+ return result.entry && result.entry.knownOwner && sameLaunch(result.entry, launch);
1661
+ }
1662
+ function safeCliFailure(result) {
1663
+ if (result.timedOut) return "TIMEOUT";
1664
+ const value = `${result.stderr}
1665
+ ${result.stdout}`;
1666
+ if (/eacces|eperm|access denied|permission denied/i.test(value)) return "PERMISSION";
1667
+ if (/scope/i.test(value)) return "SCOPE";
1668
+ if (/managed|read.only|policy/i.test(value)) return "MANAGED";
1669
+ if (/not found|not configured|no (?:mcp )?server|does not exist/i.test(value)) return "ENTRY";
1670
+ if (result.code === null) return "SPAWN";
1671
+ return "OTHER";
1672
+ }
1673
+ async function reconcileMcpEntry(id, binary, launch, action, run = DEFAULT_SPAWN, cwd) {
1674
+ const invoke = (args2) => run(binary.command, [...binary.prefixArgs, ...args2], { timeoutMs: PROCESS_TIMEOUT_MS, cwd });
1675
+ const before = await getEntry(id, binary, (command, args2, options) => run(command, args2, { ...options, cwd }));
1676
+ if (before.named && !before.entry) return { ok: false, state: "conflict", message: "AGENT_ENTRY_AMBIGUOUS" };
1677
+ if (before.entry && !before.entry.knownOwner) return { ok: false, state: "conflict", message: "AGENT_ENTRY_AMBIGUOUS" };
1678
+ if (action === "configure" && before.entry) {
1679
+ return sameLaunch(before.entry, launch) ? { ok: true, state: "identical", message: "Already configured and verified." } : { ok: false, state: "outdated", message: "AGENT_ALREADY_CONFIGURED" };
1680
+ }
1681
+ if (action === "repair" && !before.entry)
1682
+ return { ok: false, state: "failed", message: "AGENT_NOT_CONFIGURED" };
1683
+ if (action === "repair" && before.entry && sameLaunch(before.entry, launch))
1684
+ return { ok: true, state: "identical", message: "Already configured and verified." };
1685
+ if (action === "remove" && !before.entry)
1686
+ return { ok: false, state: "failed", message: "AGENT_NOT_CONFIGURED" };
1687
+ const old = before.entry;
1688
+ const safeRun = (command, args2, options) => run(command, args2, { ...options, cwd });
1689
+ if (old) {
1690
+ const removed = await invoke(cliArgs(id, "remove", void 0, old.scope));
1691
+ if (removed.code !== 0)
1692
+ return {
1693
+ ok: false,
1694
+ state: "failed",
1695
+ message: `AGENT_REMOVE_FAILED_${safeCliFailure(removed)}`
1696
+ };
1697
+ const after = await getEntry(id, binary, safeRun);
1698
+ if (after.entry || after.named) {
1699
+ await invoke(cliArgs(id, "add", { command: old.command, args: old.argsVector, version: "legacy" }, old.scope));
1700
+ return { ok: false, state: "failed", message: "AGENT_REMOVE_VERIFY_FAILED" };
1701
+ }
1702
+ if (action === "remove") return { ok: true, state: "removed", message: "Removed and verified." };
1703
+ }
1704
+ const added = await invoke(cliArgs(id, "add", launch));
1705
+ if (added.code === 0 && await verifyCurrent(id, binary, launch, safeRun))
1706
+ return { ok: true, state: old ? "updated" : "configured", message: old ? "Repaired and verified." : "Configured and verified." };
1707
+ if (old) {
1708
+ const maybe = await getEntry(id, binary, safeRun);
1709
+ if (maybe.entry?.knownOwner) await invoke(cliArgs(id, "remove", void 0, maybe.entry.scope));
1710
+ await invoke(cliArgs(id, "add", { command: old.command, args: old.argsVector, version: "legacy" }, old.scope));
1711
+ }
1712
+ return {
1713
+ ok: false,
1714
+ state: "failed",
1715
+ message: added.timedOut ? "AGENT_OPERATION_TIMEOUT" : "AGENT_CONFIGURE_VERIFY_FAILED"
1716
+ };
1717
+ }
1718
+
1719
+ // apps/cli/src/agent-config.ts
1720
+ import { homedir as homedir2 } from "node:os";
1721
+ function safeFailure(result) {
1722
+ const value = result.stderr || result.stdout || "unknown error";
1723
+ return value.trim().split(/\r?\n/)[0].slice(0, 180);
1724
+ }
1725
+ async function configureAgent(agent, binaryPath, proposed, allowUpdate, spawn) {
1726
+ const run = async (command, args2) => {
1727
+ if (!spawn) return runAgentCommand(command, args2);
1728
+ const result2 = spawn(command, args2);
1729
+ return {
1730
+ code: result2.status,
1731
+ stdout: result2.stdout,
1732
+ stderr: result2.stderr,
1733
+ timedOut: false
1734
+ };
1735
+ };
1736
+ const binary = typeof binaryPath === "string" ? { command: binaryPath, prefixArgs: [], source: "cli setup" } : binaryPath;
1737
+ const result = await reconcileMcpEntry(
1738
+ agent,
1739
+ binary,
1740
+ proposed,
1741
+ allowUpdate ? "repair" : "configure",
1742
+ run,
1743
+ homedir2()
1744
+ );
1745
+ const states = ["configured", "identical", "outdated", "updated", "conflict", "failed"];
1746
+ const state = states.includes(result.state) ? result.state : "failed";
1747
+ return {
1748
+ ok: result.ok,
1749
+ state,
1750
+ message: result.ok ? result.message : `${result.message}${result.message.startsWith("AGENT_") ? "" : ` \u2014 ${safeFailure({ stdout: "", stderr: result.message })}`}`
1751
+ };
1752
+ }
1753
+
1369
1754
  // apps/cli/src/index.ts
1370
- var root = resolve(dirname2(fileURLToPath(import.meta.url)), "..");
1755
+ var root = resolve2(dirname2(fileURLToPath(import.meta.url)), "..");
1371
1756
  var args = process.argv.slice(2);
1372
1757
  function option(name) {
1373
1758
  const i = args.indexOf(name);
@@ -1376,14 +1761,6 @@ function option(name) {
1376
1761
  throw new Error("Missing option value.");
1377
1762
  return args[i + 1];
1378
1763
  }
1379
- function detectOnPath(binary) {
1380
- for (const directory of (process.env.PATH ?? "").split(delimiter))
1381
- for (const name of [binary, `${binary}.exe`, `${binary}.cmd`]) {
1382
- const candidate = join2(directory, name);
1383
- if (existsSync(candidate)) return candidate;
1384
- }
1385
- return void 0;
1386
- }
1387
1764
  async function promptClientChoice() {
1388
1765
  if (!process.stdin.isTTY) return ["claude", "codex"];
1389
1766
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
@@ -1398,38 +1775,8 @@ async function promptClientChoice() {
1398
1775
  rl.close();
1399
1776
  }
1400
1777
  }
1401
- function configureAgent(agent, binaryPath, serverArgs) {
1402
- if (agent === "claude") {
1403
- const existing = spawnSync(binaryPath, ["mcp", "get", "gazesight"], {
1404
- encoding: "utf8"
1405
- });
1406
- if (existing.status === 0)
1407
- return { ok: true, message: "Already configured (unchanged)." };
1408
- const add2 = spawnSync(
1409
- binaryPath,
1410
- ["mcp", "add", "gazesight", "--scope", "user", "--", ...serverArgs],
1411
- { encoding: "utf8" }
1412
- );
1413
- if (add2.status === 0) return { ok: true, message: "Configured." };
1414
- return {
1415
- ok: false,
1416
- message: `Failed: ${(add2.stderr || add2.stdout || "unknown error").trim().split("\n")[0]}`
1417
- };
1418
- }
1419
- const list = spawnSync(binaryPath, ["mcp", "list"], { encoding: "utf8" });
1420
- if (list.status === 0 && list.stdout.includes("gazesight"))
1421
- return { ok: true, message: "Already configured (unchanged)." };
1422
- const add = spawnSync(binaryPath, ["mcp", "add", "gazesight", "--", ...serverArgs], {
1423
- encoding: "utf8"
1424
- });
1425
- if (add.status === 0) return { ok: true, message: "Configured." };
1426
- return {
1427
- ok: false,
1428
- message: `Failed: ${(add.stderr || add.stdout || "unknown error").trim().split("\n")[0]}`
1429
- };
1430
- }
1431
1778
  try {
1432
- const configPath = resolve(
1779
+ const configPath = resolve2(
1433
1780
  option("--config") ?? process.env.GAZESIGHT_CONFIG_FILE ?? defaultConfigPath()
1434
1781
  );
1435
1782
  if (args[0] === "mcp") {
@@ -1439,24 +1786,31 @@ try {
1439
1786
  else if (!args[0] || args.includes("--help"))
1440
1787
  console.log(
1441
1788
  `GazeSight ${VERSION}
1442
- gazesight setup [--config PATH] [--port PORT] [--client claude|codex|both]
1789
+ gazesight setup [--config PATH] [--port PORT] [--client claude|codex|both] [--update]
1443
1790
  gazesight pair [--config PATH] [--revoke ID|all]
1444
1791
  gazesight doctor [--config PATH]
1445
1792
  gazesight mcp [--config PATH]
1793
+ --update replaces only an outdated GazeSight MCP entry after showing the existing and proposed configurations.
1794
+ 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.
1446
1795
  No browser is launched. Secrets are never printed.`
1447
1796
  );
1448
1797
  else {
1449
1798
  if (!["setup", "doctor", "pair"].includes(args[0]))
1450
1799
  throw new Error("Unknown command.");
1451
- if (args.slice(1).some(
1452
- (value, i) => i % 2 === 0 && !["--config", "--port", "--client", "--revoke"].includes(value)
1453
- ))
1454
- throw new Error("Unknown option.");
1800
+ const valueOptions = /* @__PURE__ */ new Set(["--config", "--port", "--client", "--revoke"]);
1801
+ const flagOptions = /* @__PURE__ */ new Set(["--update"]);
1802
+ for (let i = 1; i < args.length; i++) {
1803
+ const value = args[i];
1804
+ if (flagOptions.has(value)) continue;
1805
+ if (!valueOptions.has(value) || !args[i + 1] || args[i + 1].startsWith("--"))
1806
+ throw new Error("Unknown option or missing option value.");
1807
+ i++;
1808
+ }
1455
1809
  const clientOption = option("--client");
1456
1810
  if (clientOption && !["claude", "codex", "both"].includes(clientOption))
1457
1811
  throw new Error("--client must be claude, codex, or both.");
1458
1812
  const node = Number(process.versions.node.split(".")[0]) >= 20;
1459
- const build = existsSync(join2(root, "mcp/index.js")) && existsSync(join2(root, "firefox/manifest.json"));
1813
+ const build = existsSync2(join2(root, "mcp/index.js")) && existsSync2(join2(root, "firefox/manifest.json"));
1460
1814
  console.log(
1461
1815
  `Node: ${node ? "PASS" : "FAIL"}
1462
1816
  GazeSight build: ${build ? "PASS" : "FAIL"}`
@@ -1481,17 +1835,28 @@ Agent guidance: ${config.agentGuidance.enabled ? "Enabled" : "Disabled"}${config
1481
1835
  "--config",
1482
1836
  configPath
1483
1837
  ];
1838
+ if (chosen.length > 1)
1839
+ console.log(
1840
+ "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."
1841
+ );
1842
+ const proposed = {
1843
+ command: serverArgs[0],
1844
+ args: serverArgs.slice(1),
1845
+ version: VERSION
1846
+ };
1847
+ const available = /* @__PURE__ */ new Set();
1484
1848
  for (const agent of chosen) {
1485
1849
  const label = agent === "claude" ? "Claude Code" : "Codex";
1486
- const binaryPath = detectOnPath(agent);
1487
- if (!binaryPath) {
1850
+ const binary = await findAgentBinary(agent, { home: process.env.USERPROFILE ?? process.env.HOME, env: process.env });
1851
+ if (!binary.candidate) {
1488
1852
  console.log(`${label}: not detected on PATH \u2014 configure manually below.`);
1489
1853
  continue;
1490
1854
  }
1491
- const result2 = configureAgent(agent, binaryPath, serverArgs);
1855
+ available.add(agent);
1856
+ const result2 = await configureAgent(agent, binary.candidate, proposed, args.includes("--update"));
1492
1857
  console.log(`${label}: ${result2.ok ? "PASS" : "FAIL"} \u2014 ${result2.message}`);
1493
1858
  }
1494
- const undetected = explicit ? chosen.filter((agent) => !detectOnPath(agent)) : ["claude", "codex"];
1859
+ const undetected = explicit ? chosen.filter((agent) => !available.has(agent)) : ["claude", "codex"];
1495
1860
  if (undetected.length) {
1496
1861
  const server = { command: process.execPath, args: serverArgs.slice(1) };
1497
1862
  if (undetected.includes("claude")) {
@@ -1511,10 +1876,9 @@ args = ${JSON.stringify(server.args)}`
1511
1876
  }
1512
1877
  console.log(
1513
1878
  `Open your GazeSight browser extension and click Connect to GazeSight.
1514
- Chrome (manual install, not yet on the Chrome Web Store): open chrome://extensions, enable Developer mode, "Load unpacked", select ${join2(root, "chrome")}.
1515
- Development only (Firefox): about:debugging > Load Temporary Add-on > ${join2(root, "firefox/manifest.json")}
1516
- Unsigned XPI: ${join2(root, "gazesight-firefox.xpi")}
1517
- External Firefox Release/Beta installation requires the separately supplied Mozilla-signed XPI.
1879
+ Firefox: install or update the signed extension from the official listing \u2014 https://addons.mozilla.org/addon/6a2319d4fa544731884d/.
1880
+ Chrome: install or update the public extension from the Chrome Web Store \u2014 https://chromewebstore.google.com/detail/gazesight/hikhblabkhbaoilcffenldphdkjdmphc.
1881
+ 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")}.
1518
1882
  Manual JSON import remains available under Advanced settings.`
1519
1883
  );
1520
1884
  if (process.stdin.isTTY) await pairingFlow(config, configPath);