gazesight 0.5.0 → 0.5.2

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
@@ -3,13 +3,13 @@
3
3
  // apps/cli/src/index.ts
4
4
  import { existsSync as existsSync2 } from "node:fs";
5
5
  import { createInterface as createInterface2 } from "node:readline/promises";
6
- import { resolve as resolve2, dirname as dirname2, join as join2, delimiter } from "node:path";
6
+ import { resolve as resolve2, dirname as dirname2, join as join2 } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
 
9
9
  // apps/cli/src/config.ts
10
10
  import { randomBytes } from "node:crypto";
11
11
  import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
12
- import { dirname, join } from "node:path";
12
+ import { dirname, posix, win32 } from "node:path";
13
13
  import { homedir } from "node:os";
14
14
 
15
15
  // apps/cli/src/config-browser.ts
@@ -41,22 +41,23 @@ var localConfigSchema = z.strictObject({
41
41
 
42
42
  // apps/cli/src/config.ts
43
43
  function configPathFor(platform, home, env) {
44
+ const pathJoin = platform === "win32" ? win32.join : posix.join;
44
45
  if (platform === "win32")
45
- return join(
46
- env.APPDATA ?? join(home, "AppData", "Roaming"),
46
+ return pathJoin(
47
+ env.APPDATA ?? pathJoin(home, "AppData", "Roaming"),
47
48
  "GazeSight",
48
49
  "config.json"
49
50
  );
50
51
  if (platform === "darwin")
51
- return join(
52
+ return pathJoin(
52
53
  home,
53
54
  "Library",
54
55
  "Application Support",
55
56
  "GazeSight",
56
57
  "config.json"
57
58
  );
58
- return join(
59
- env.XDG_CONFIG_HOME ?? join(home, ".config"),
59
+ return pathJoin(
60
+ env.XDG_CONFIG_HOME ?? pathJoin(home, ".config"),
60
61
  "gazesight",
61
62
  "config.json"
62
63
  );
@@ -101,7 +102,7 @@ function writeLocalConfig(path, value) {
101
102
  }
102
103
 
103
104
  // packages/protocol/src/release.ts
104
- var VERSION = "0.5.0";
105
+ var VERSION = "0.5.2";
105
106
 
106
107
  // apps/cli/src/doctor.ts
107
108
  import { z as z2 } from "zod";
@@ -119,18 +120,20 @@ function diagnosticRows(status, reachable = false, authenticated = false) {
119
120
  },
120
121
  {
121
122
  name: "extension connected",
122
- pass: !!status?.connected,
123
+ pass: !!status?.connected || Object.values(status?.connectedBrowsers ?? {}).some(Boolean),
123
124
  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."
124
125
  },
125
126
  {
126
127
  name: "pairing",
127
- pass: authenticated && !!status?.paired,
128
+ // Pairing is durable authorization, while `paired` means a live
129
+ // WebSocket connection. An approved extension can be offline.
130
+ pass: authenticated && (status?.paired === true || (status?.authorizedPairings ?? 0) > 0),
128
131
  action: "Run gazesight pair, click Connect to GazeSight, and approve the matching code."
129
132
  },
130
133
  {
131
134
  name: "target selected",
132
135
  pass: !!status && status.targetState !== "none",
133
- action: "Open your local preview in Firefox and click the GazeSight icon."
136
+ action: `Open your local preview in ${status?.activeTarget?.browser === "chrome" ? "Chrome" : "Firefox or Chrome"} and click the GazeSight icon.`
134
137
  },
135
138
  {
136
139
  name: "capture permission",
@@ -164,7 +167,9 @@ var statusSchema = z2.object({
164
167
  paired: z2.boolean(),
165
168
  targetState: z2.enum(["none", "authorized", "reauthorization_required"]),
166
169
  backgroundCapture: z2.object({ enabled: z2.boolean(), permissionGranted: z2.boolean() }).optional(),
167
- authorizedPairings: z2.number().int().nonnegative().optional()
170
+ authorizedPairings: z2.number().int().nonnegative().optional(),
171
+ connectedBrowsers: z2.object({ firefox: z2.boolean().optional(), chrome: z2.boolean().optional() }).optional(),
172
+ activeTarget: z2.object({ browser: z2.enum(["firefox", "chrome"]).optional() }).nullable().optional()
168
173
  });
169
174
 
170
175
  // apps/cli/src/pair.ts
@@ -951,7 +956,7 @@ var PairingService = class {
951
956
  }
952
957
  listPending() {
953
958
  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 }));
959
+ return [...this.pending.values()].filter((item) => item.state === "pending").map(({ id, browser, extensionId, code, createdAt, expiresAt }) => ({ id, browser, extensionId, code, createdAt, expiresAt })).sort((a, b) => b.createdAt - a.createdAt);
955
960
  }
956
961
  listPairings() {
957
962
  return this.pairings.map(({ id, browser, extensionId, origin, createdAt, revokedAt }) => ({ id, browser, extensionId, origin, createdAt, ...revokedAt ? { revokedAt } : {} }));
@@ -1247,6 +1252,9 @@ var Bridge = class {
1247
1252
  server;
1248
1253
  http;
1249
1254
  listenError;
1255
+ closePromise;
1256
+ mcpClientCount = 0;
1257
+ desktopIpcReady = false;
1250
1258
  pairing;
1251
1259
  // Resolves the connection currently backing the global active target, or
1252
1260
  // undefined if no browser is active (nothing selected yet, or the
@@ -1309,6 +1317,8 @@ var Bridge = class {
1309
1317
  })
1310
1318
  );
1311
1319
  return {
1320
+ mcpClients: this.mcpClientCount,
1321
+ desktopIpcReady: this.desktopIpcReady,
1312
1322
  connected: this.connected,
1313
1323
  paired: this.connected,
1314
1324
  authorizedPairings: this.pairing.listPairings().filter((item) => !item.revokedAt).length,
@@ -1321,22 +1331,47 @@ var Bridge = class {
1321
1331
  activeTarget: active ? { browser: active.browser, url: active.state.url } : null
1322
1332
  };
1323
1333
  }
1334
+ setMcpClientCount(count) {
1335
+ this.mcpClientCount = Math.max(0, Math.min(32, Math.floor(count)));
1336
+ }
1337
+ setDesktopIpcReady(ready) {
1338
+ this.desktopIpcReady = ready;
1339
+ }
1324
1340
  async ready() {
1325
1341
  if (this.listenError) throw this.listenError;
1326
1342
  if (this.server.address()) return;
1327
1343
  await new Promise((resolve3, reject) => {
1328
- this.server.once("listening", resolve3);
1329
- const onError = () => reject(this.listenError);
1344
+ const cleanup = () => {
1345
+ this.server.off("listening", onListening);
1346
+ this.server.off("error", onError);
1347
+ this.http.off("error", onError);
1348
+ };
1349
+ const onListening = () => {
1350
+ cleanup();
1351
+ resolve3();
1352
+ };
1353
+ const onError = () => {
1354
+ cleanup();
1355
+ reject(this.listenError);
1356
+ };
1357
+ this.server.once("listening", onListening);
1330
1358
  this.server.once("error", onError);
1331
1359
  this.http.once("error", onError);
1332
1360
  });
1333
1361
  }
1334
1362
  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()));
1363
+ if (this.closePromise) return this.closePromise;
1364
+ this.closePromise = (async () => {
1365
+ clearInterval(this.heartbeat);
1366
+ for (const wiring of this.wiring.values()) wiring.router.disconnect();
1367
+ this.wiring.clear();
1368
+ for (const client of this.server.clients) client.terminate();
1369
+ if (this.http.listening)
1370
+ await new Promise((resolve3) => this.http.close(() => resolve3()));
1371
+ else if (this.server.address())
1372
+ await new Promise((resolve3) => this.server.close(() => resolve3()));
1373
+ })();
1374
+ return this.closePromise;
1340
1375
  }
1341
1376
  };
1342
1377
 
@@ -1389,7 +1424,10 @@ async function pairingFlow(config, configPath, revoke) {
1389
1424
  const answer = (await rl.question("Approve this matching request? [y/N] ")).trim().toLowerCase();
1390
1425
  const approved = answer === "y" || answer === "yes";
1391
1426
  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.");
1427
+ if (!result.ok) {
1428
+ console.log("That request expired or was replaced. Checking for the latest request\u2026");
1429
+ continue;
1430
+ }
1393
1431
  if (approved) {
1394
1432
  const deadline = Date.now() + 5e3;
1395
1433
  let delivered = false;
@@ -1402,8 +1440,32 @@ async function pairingFlow(config, configPath, revoke) {
1402
1440
  await new Promise((resolve3) => setTimeout(resolve3, 100));
1403
1441
  }
1404
1442
  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.");
1443
+ console.log("Pairing approved and delivered.");
1444
+ console.log("Waiting for the extension to connect to this GazeSight bridge (up to 15 seconds)\u2026");
1445
+ const connectionDeadline = Date.now() + 15e3;
1446
+ let connected = false;
1447
+ while (Date.now() < connectionDeadline) {
1448
+ const status = await fetch(`http://127.0.0.1:${config.port}/gazesight/status`, {
1449
+ headers: headers(config.token),
1450
+ signal: AbortSignal.timeout(1500)
1451
+ }).catch(() => void 0);
1452
+ if (status?.ok) {
1453
+ const value = await status.json();
1454
+ if (value.connectedBrowsers?.[request.browser] === true) {
1455
+ connected = true;
1456
+ break;
1457
+ }
1458
+ }
1459
+ await new Promise((resolve3) => setTimeout(resolve3, 250));
1460
+ }
1461
+ if (connected)
1462
+ console.log("Extension connected. Pairing is ready.");
1463
+ else
1464
+ console.log(`Credential delivered, but ${request.browser} did not connect. Keep the extension installed and enabled, open its popup, verify its configured bridge port is ${config.port}, then click Connect to GazeSight again.`);
1465
+ } else {
1466
+ console.log("Pairing rejected. Waiting for another request\u2026");
1467
+ continue;
1468
+ }
1407
1469
  return;
1408
1470
  }
1409
1471
  } finally {
@@ -1414,159 +1476,309 @@ async function pairingFlow(config, configPath, revoke) {
1414
1476
  }
1415
1477
  }
1416
1478
 
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;
1479
+ // apps/agent-integration/src/index.ts
1480
+ import { spawn as nodeSpawn } from "node:child_process";
1481
+ import { constants as fsConstants, existsSync, readFileSync as readFileSync2 } from "node:fs";
1482
+ import { access, realpath, stat } from "node:fs/promises";
1483
+ import { delimiter, join, posix as posix2, resolve, win32 as win322 } from "node:path";
1484
+ var PROCESS_TIMEOUT_MS = 12e3;
1485
+ var PROCESS_OUTPUT_BYTES = 16 * 1024;
1486
+ function createBoundedRunner(spawnProcess = nodeSpawn) {
1487
+ return (command, args2, options = {}) => runBounded(command, args2, options, spawnProcess);
1436
1488
  }
1437
- function normalize(value) {
1438
- const normalized = value.trim().replace(/^['"]|['"]$/g, "").replaceAll("\\", "/");
1439
- return process.platform === "win32" ? normalized.toLowerCase() : normalized;
1489
+ var DEFAULT_SPAWN = createBoundedRunner();
1490
+ var runAgentCommand = DEFAULT_SPAWN;
1491
+ function runBounded(command, args2, options = {}, spawnProcess = nodeSpawn) {
1492
+ return new Promise((resolveResult) => {
1493
+ let child;
1494
+ try {
1495
+ child = spawnProcess(command, [...args2], {
1496
+ shell: false,
1497
+ windowsHide: true,
1498
+ cwd: options.cwd,
1499
+ env: options.env ?? process.env,
1500
+ stdio: ["ignore", "pipe", "pipe"]
1501
+ });
1502
+ } catch {
1503
+ resolveResult({ code: null, stdout: "", stderr: "", timedOut: false });
1504
+ return;
1505
+ }
1506
+ let stdout = "";
1507
+ let stderr = "";
1508
+ let timedOut = false;
1509
+ const append = (current, chunk) => {
1510
+ const value = current + String(chunk);
1511
+ return Buffer.byteLength(value) <= PROCESS_OUTPUT_BYTES ? value : Buffer.from(value).subarray(0, PROCESS_OUTPUT_BYTES).toString();
1512
+ };
1513
+ child.stdout?.on("data", (chunk) => {
1514
+ stdout = append(stdout, chunk);
1515
+ });
1516
+ child.stderr?.on("data", (chunk) => {
1517
+ stderr = append(stderr, chunk);
1518
+ });
1519
+ const timer = setTimeout(() => {
1520
+ timedOut = true;
1521
+ child.kill();
1522
+ }, options.timeoutMs ?? PROCESS_TIMEOUT_MS);
1523
+ child.once("error", () => {
1524
+ clearTimeout(timer);
1525
+ resolveResult({ code: null, stdout, stderr, timedOut });
1526
+ });
1527
+ child.once("close", (code) => {
1528
+ clearTimeout(timer);
1529
+ resolveResult({ code, stdout, stderr, timedOut });
1530
+ });
1531
+ });
1440
1532
  }
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";
1533
+ function pathCandidates(agent, platform, env, home) {
1534
+ const name = agent === "claude" ? "claude" : "codex";
1535
+ const pathJoin = (platform === "win32" ? win322 : posix2).join;
1536
+ const native = [];
1537
+ const packageScripts = [];
1538
+ if (platform === "win32") {
1539
+ const packageManagerBins = [env.PNPM_HOME, env.NPM_CONFIG_PREFIX].filter((value) => Boolean(value)).flatMap((root2) => [pathJoin(root2, `${name}.exe`), pathJoin(root2, `${name}.cmd`)]);
1540
+ if (agent === "claude") {
1541
+ native.push(
1542
+ pathJoin(home, ".local", "bin", "claude.exe"),
1543
+ pathJoin(env.LOCALAPPDATA ?? pathJoin(home, "AppData", "Local"), "Programs", "Claude", "claude.exe"),
1544
+ pathJoin(env.ProgramFiles ?? "C:\\Program Files", "Claude", "claude.exe")
1545
+ );
1546
+ } else {
1547
+ native.push(
1548
+ pathJoin(env.LOCALAPPDATA ?? pathJoin(home, "AppData", "Local"), "Programs", "OpenAI", "Codex", "bin", "codex.exe")
1549
+ );
1550
+ }
1551
+ const npm = pathJoin(env.APPDATA ?? pathJoin(home, "AppData", "Roaming"), "npm", `${name}.cmd`);
1552
+ const modules = [
1553
+ pathJoin(env.APPDATA ?? pathJoin(home, "AppData", "Roaming"), "npm", "node_modules"),
1554
+ ...env.NPM_CONFIG_PREFIX ? [pathJoin(env.NPM_CONFIG_PREFIX, "node_modules")] : [],
1555
+ ...env.PNPM_HOME ? [pathJoin(env.PNPM_HOME, "global", "5", "node_modules"), pathJoin(env.PNPM_HOME, "node_modules")] : [],
1556
+ pathJoin(home, "AppData", "Local", "pnpm", "global", "5", "node_modules")
1557
+ ];
1558
+ const packageName = agent === "claude" ? "@anthropic-ai/claude-code" : "@openai/codex";
1559
+ const nodeCandidates = [
1560
+ env.ProgramFiles ? pathJoin(env.ProgramFiles, "nodejs", "node.exe") : "C:\\Program Files\\nodejs\\node.exe",
1561
+ ...env.LOCALAPPDATA ? [pathJoin(env.LOCALAPPDATA, "Programs", "nodejs", "node.exe")] : [],
1562
+ ...(env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean).map((directory) => pathJoin(directory, "node.exe"))
1563
+ ];
1564
+ for (const modulesRoot of modules) {
1565
+ const root2 = pathJoin(modulesRoot, packageName);
1566
+ try {
1567
+ const metadata = JSON.parse(readFileSync2(pathJoin(root2, "package.json"), "utf8"));
1568
+ const bin = typeof metadata.bin === "string" ? metadata.bin : metadata.bin && typeof metadata.bin === "object" ? metadata.bin[name] : void 0;
1569
+ if (typeof bin !== "string" || bin.startsWith("/") || bin.split(/[\\/]/).includes("..")) continue;
1570
+ const script = pathJoin(root2, bin);
1571
+ for (const node of nodeCandidates)
1572
+ packageScripts.push({ command: node, prefixArgs: [script], source: "verified npm package" });
1573
+ } catch {
1574
+ }
1575
+ }
1576
+ native.push(npm, ...packageManagerBins);
1577
+ } else {
1578
+ native.push(
1579
+ pathJoin(home, ".local", "bin", name),
1580
+ pathJoin(home, ".npm-global", "bin", name),
1581
+ pathJoin(home, ".local", "share", "pnpm", name),
1582
+ ...env.PNPM_HOME ? [pathJoin(env.PNPM_HOME, name)] : [],
1583
+ ...env.NPM_CONFIG_PREFIX ? [pathJoin(env.NPM_CONFIG_PREFIX, "bin", name)] : []
1584
+ );
1585
+ if (platform === "darwin")
1586
+ native.push(
1587
+ pathJoin(home, "Library", "pnpm", name),
1588
+ "/opt/homebrew/bin/" + name,
1589
+ "/usr/local/bin/" + name
1590
+ );
1591
+ else if (platform === "linux")
1592
+ native.push("/usr/bin/" + name, "/usr/local/bin/" + name);
1593
+ }
1594
+ const fromPath = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean).flatMap(
1595
+ (directory) => platform === "win32" ? [pathJoin(directory, `${name}.exe`), pathJoin(directory, `${name}.cmd`)] : [pathJoin(directory, name)]
1596
+ );
1597
+ const seen = /* @__PURE__ */ new Set();
1598
+ const direct = [...native, ...fromPath].filter((path) => {
1599
+ const key = platform === "win32" ? path.toLowerCase() : path;
1600
+ if (seen.has(key)) return false;
1601
+ seen.add(key);
1602
+ return true;
1603
+ }).map((command) => ({ command, prefixArgs: [], source: "candidate" }));
1604
+ return [...packageScripts, ...direct];
1446
1605
  }
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;
1606
+ async function verifyCandidate(candidate, platform) {
1455
1607
  try {
1456
- const parsed = JSON.parse(readFileSync2(packagePath, "utf8"));
1457
- return typeof parsed.version === "string" ? parsed.version : void 0;
1608
+ const canonical = await realpath(candidate.command);
1609
+ if (canonical.includes("..")) return void 0;
1610
+ const info = await stat(canonical);
1611
+ if (!info.isFile()) return void 0;
1612
+ for (const argument of candidate.prefixArgs) {
1613
+ if (!argument.toLowerCase().endsWith(".js")) continue;
1614
+ const script = await realpath(argument);
1615
+ if (!(await stat(script)).isFile()) return void 0;
1616
+ if (platform === "win32" && !script.toLowerCase().endsWith(".js")) return void 0;
1617
+ }
1618
+ if (platform !== "win32") {
1619
+ await access(canonical, fsConstants.X_OK);
1620
+ if ((info.mode & 73) === 0) return void 0;
1621
+ } else if (!/\.(exe|cmd)$/i.test(canonical)) return void 0;
1622
+ return { ...candidate, command: canonical };
1458
1623
  } catch {
1459
1624
  return void 0;
1460
1625
  }
1461
1626
  }
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);
1627
+ function parseVersion(agent, output) {
1628
+ const value = output.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/)?.[1];
1629
+ if (!value) return void 0;
1630
+ if (agent === "claude" && Number(value.split(".")[0]) < 2) return void 0;
1631
+ return value;
1632
+ }
1633
+ async function findAgentBinary(id, options = {}) {
1634
+ const platform = options.platform ?? process.platform;
1635
+ const env = options.env ?? process.env;
1636
+ const home = options.home ?? env.USERPROFILE ?? env.HOME ?? "";
1637
+ const run = options.spawn ?? DEFAULT_SPAWN;
1638
+ let sawCandidate = false;
1639
+ let sawDiscoveryError = false;
1640
+ for (const pathCandidate of pathCandidates(id, platform, env, home)) {
1641
+ const candidate = await verifyCandidate(pathCandidate, platform);
1642
+ if (!candidate) continue;
1643
+ sawCandidate = true;
1644
+ const versionResult = await run(candidate.command, [...candidate.prefixArgs, "--version"], { timeoutMs: PROCESS_TIMEOUT_MS, cwd: home });
1645
+ const version = versionResult.code === 0 ? parseVersion(id, `${versionResult.stdout}
1646
+ ${versionResult.stderr}`) : void 0;
1647
+ const help = await run(candidate.command, [...candidate.prefixArgs, "mcp", "--help"], { timeoutMs: PROCESS_TIMEOUT_MS, cwd: home });
1648
+ if (help.code === 0 && /\badd\b/i.test(help.stdout + help.stderr) && /\bremove\b/i.test(help.stdout + help.stderr))
1649
+ return { candidate, installation: version ? "INSTALLED" : "INSTALLED_VERSION_UNKNOWN", ...version ? { version } : {} };
1650
+ if (!version || help.timedOut || versionResult.timedOut) sawDiscoveryError = true;
1651
+ }
1470
1652
  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
1653
+ installation: sawDiscoveryError ? "DISCOVERY_ERROR" : sawCandidate ? "UNSUPPORTED_VERSION" : "NOT_INSTALLED"
1478
1654
  };
1479
1655
  }
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);
1656
+ function parseMcpEntry(agent, output) {
1657
+ const command = output.match(/^\s*(?:Command|command):\s*(.+)$/im)?.[1]?.trim();
1658
+ const args2 = output.match(/^\s*(?:Args|args):\s*(.*)$/im)?.[1]?.trim();
1659
+ if (!command || args2 === void 0) return void 0;
1660
+ const scopeText = output.match(/^\s*Scope:\s*(.+)$/im)?.[1]?.trim().toLowerCase() ?? "user";
1661
+ const scope = scopeText.match(/^(user|project|local)\b/)?.[1] ?? "user";
1662
+ 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}`);
1663
+ const knownOwner = legacyNpm || /(?:^|[\\/])dist[\\/]cli[\\/]index\.js(?:\s|$)/i.test(args2) && /\bmcp\b/i.test(args2);
1664
+ const oldCli = args2.match(/^(.*?[\\/]dist[\\/]cli[\\/]index\.js)\s+mcp(?:\s+--config\s+(.+?))?(?:\s+(--desktop-adapter))?$/i);
1665
+ const oldNpm = args2.match(/^(--yes\s+)?(gazesight(?:@\d+\.\d+\.\d+)?|@gazesight\/mcp-server)(?:\s+--yes)?\s+mcp(?:\s+--config\s+(.+))?$/i);
1666
+ 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);
1667
+ return { command, args: args2, argsVector, scope: agent === "claude" ? scope : "user", legacyNpm, knownOwner };
1482
1668
  }
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}`;
1669
+ async function getEntry(agent, binary, run) {
1670
+ const result = await run(binary.command, [...binary.prefixArgs, "mcp", "get", "gazesight"], { timeoutMs: PROCESS_TIMEOUT_MS });
1671
+ const output = `${result.stdout}
1672
+ ${result.stderr}`;
1673
+ const headers2 = output.match(/(?:^|\n)\s*gazesight\s*:?\s*(?=\r?\n|$)/gi) ?? [];
1674
+ const entry = result.code === 0 && headers2.length <= 1 ? parseMcpEntry(agent, output) : void 0;
1675
+ const named = headers2.length > 0;
1676
+ return { entry, named, code: result.code, output };
1487
1677
  }
1488
- function failure(result) {
1489
- return (result.stderr || result.stdout || "unknown error").trim().split(/\r?\n/)[0];
1678
+ function cliArgs(agent, operation, launch, scope) {
1679
+ if (operation === "remove") return agent === "claude" ? ["mcp", "remove", "gazesight", "-s", scope ?? "user"] : ["mcp", "remove", "gazesight"];
1680
+ const spec = launch;
1681
+ return agent === "claude" ? ["mcp", "add", "--transport", "stdio", "-s", scope ?? "user", "gazesight", "--", spec.command, ...spec.args] : ["mcp", "add", "gazesight", "--", spec.command, ...spec.args];
1490
1682
  }
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)}` };
1683
+ function sameLaunch(entry, launch) {
1684
+ const norm = (value) => {
1685
+ return value.trim().replace(/^['"]|['"]$/g, "").replaceAll("\\", "/").toLowerCase();
1686
+ };
1687
+ return norm(entry.command) === norm(launch.command) && norm(entry.args) === norm(launch.args.join(" "));
1688
+ }
1689
+ async function verifyCurrent(agent, binary, launch, run) {
1690
+ const result = await getEntry(agent, binary, run);
1691
+ return result.entry && result.entry.knownOwner && sameLaunch(result.entry, launch);
1692
+ }
1693
+ function safeCliFailure(result) {
1694
+ if (result.timedOut) return "TIMEOUT";
1695
+ const value = `${result.stderr}
1696
+ ${result.stdout}`;
1697
+ if (/eacces|eperm|access denied|permission denied/i.test(value)) return "PERMISSION";
1698
+ if (/scope/i.test(value)) return "SCOPE";
1699
+ if (/managed|read.only|policy/i.test(value)) return "MANAGED";
1700
+ if (/not found|not configured|no (?:mcp )?server|does not exist/i.test(value)) return "ENTRY";
1701
+ if (result.code === null) return "SPAWN";
1702
+ return "OTHER";
1703
+ }
1704
+ async function reconcileMcpEntry(id, binary, launch, action, run = DEFAULT_SPAWN, cwd) {
1705
+ const invoke = (args2) => run(binary.command, [...binary.prefixArgs, ...args2], { timeoutMs: PROCESS_TIMEOUT_MS, cwd });
1706
+ const before = await getEntry(id, binary, (command, args2, options) => run(command, args2, { ...options, cwd }));
1707
+ if (before.named && !before.entry) return { ok: false, state: "conflict", message: "AGENT_ENTRY_AMBIGUOUS" };
1708
+ if (before.entry && !before.entry.knownOwner) return { ok: false, state: "conflict", message: "AGENT_ENTRY_AMBIGUOUS" };
1709
+ if (action === "configure" && before.entry) {
1710
+ return sameLaunch(before.entry, launch) ? { ok: true, state: "identical", message: "Already configured and verified." } : { ok: false, state: "outdated", message: "AGENT_ALREADY_CONFIGURED" };
1711
+ }
1712
+ if (action === "repair" && !before.entry)
1713
+ return { ok: false, state: "failed", message: "AGENT_NOT_CONFIGURED" };
1714
+ if (action === "repair" && before.entry && sameLaunch(before.entry, launch))
1715
+ return { ok: true, state: "identical", message: "Already configured and verified." };
1716
+ if (action === "remove" && !before.entry)
1717
+ return { ok: false, state: "failed", message: "AGENT_NOT_CONFIGURED" };
1718
+ const old = before.entry;
1719
+ const safeRun = (command, args2, options) => run(command, args2, { ...options, cwd });
1720
+ if (old) {
1721
+ const removed = await invoke(cliArgs(id, "remove", void 0, old.scope));
1722
+ if (removed.code !== 0)
1723
+ return {
1724
+ ok: false,
1725
+ state: "failed",
1726
+ message: `AGENT_REMOVE_FAILED_${safeCliFailure(removed)}`
1727
+ };
1728
+ const after = await getEntry(id, binary, safeRun);
1729
+ if (after.entry || after.named) {
1730
+ await invoke(cliArgs(id, "add", { command: old.command, args: old.argsVector, version: "legacy" }, old.scope));
1731
+ return { ok: false, state: "failed", message: "AGENT_REMOVE_VERIFY_FAILED" };
1732
+ }
1733
+ if (action === "remove") return { ok: true, state: "removed", message: "Removed and verified." };
1734
+ }
1735
+ const added = await invoke(cliArgs(id, "add", launch));
1736
+ if (added.code === 0 && await verifyCurrent(id, binary, launch, safeRun))
1737
+ return { ok: true, state: old ? "updated" : "configured", message: old ? "Repaired and verified." : "Configured and verified." };
1738
+ if (old) {
1739
+ const maybe = await getEntry(id, binary, safeRun);
1740
+ if (maybe.entry?.knownOwner) await invoke(cliArgs(id, "remove", void 0, maybe.entry.scope));
1741
+ await invoke(cliArgs(id, "add", { command: old.command, args: old.argsVector, version: "legacy" }, old.scope));
1506
1742
  }
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
1743
  return {
1565
1744
  ok: false,
1566
1745
  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.`
1746
+ message: added.timedOut ? "AGENT_OPERATION_TIMEOUT" : "AGENT_CONFIGURE_VERIFY_FAILED"
1747
+ };
1748
+ }
1749
+
1750
+ // apps/cli/src/agent-config.ts
1751
+ import { homedir as homedir2 } from "node:os";
1752
+ function safeFailure(result) {
1753
+ const value = result.stderr || result.stdout || "unknown error";
1754
+ return value.trim().split(/\r?\n/)[0].slice(0, 180);
1755
+ }
1756
+ async function configureAgent(agent, binaryPath, proposed, allowUpdate, spawn) {
1757
+ const run = async (command, args2) => {
1758
+ if (!spawn) return runAgentCommand(command, args2);
1759
+ const result2 = spawn(command, args2);
1760
+ return {
1761
+ code: result2.status,
1762
+ stdout: result2.stdout,
1763
+ stderr: result2.stderr,
1764
+ timedOut: false
1765
+ };
1766
+ };
1767
+ const binary = typeof binaryPath === "string" ? { command: binaryPath, prefixArgs: [], source: "cli setup" } : binaryPath;
1768
+ const result = await reconcileMcpEntry(
1769
+ agent,
1770
+ binary,
1771
+ proposed,
1772
+ allowUpdate ? "repair" : "configure",
1773
+ run,
1774
+ homedir2()
1775
+ );
1776
+ const states = ["configured", "identical", "outdated", "updated", "conflict", "failed"];
1777
+ const state = states.includes(result.state) ? result.state : "failed";
1778
+ return {
1779
+ ok: result.ok,
1780
+ state,
1781
+ message: result.ok ? result.message : `${result.message}${result.message.startsWith("AGENT_") ? "" : ` \u2014 ${safeFailure({ stdout: "", stderr: result.message })}`}`
1570
1782
  };
1571
1783
  }
1572
1784
 
@@ -1580,14 +1792,6 @@ function option(name) {
1580
1792
  throw new Error("Missing option value.");
1581
1793
  return args[i + 1];
1582
1794
  }
1583
- function detectOnPath(binary) {
1584
- for (const directory of (process.env.PATH ?? "").split(delimiter))
1585
- for (const name of [binary, `${binary}.exe`, `${binary}.cmd`]) {
1586
- const candidate = join2(directory, name);
1587
- if (existsSync2(candidate)) return candidate;
1588
- }
1589
- return void 0;
1590
- }
1591
1795
  async function promptClientChoice() {
1592
1796
  if (!process.stdin.isTTY) return ["claude", "codex"];
1593
1797
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
@@ -1671,17 +1875,19 @@ Agent guidance: ${config.agentGuidance.enabled ? "Enabled" : "Disabled"}${config
1671
1875
  args: serverArgs.slice(1),
1672
1876
  version: VERSION
1673
1877
  };
1878
+ const available = /* @__PURE__ */ new Set();
1674
1879
  for (const agent of chosen) {
1675
1880
  const label = agent === "claude" ? "Claude Code" : "Codex";
1676
- const binaryPath = detectOnPath(agent);
1677
- if (!binaryPath) {
1881
+ const binary = await findAgentBinary(agent, { home: process.env.USERPROFILE ?? process.env.HOME, env: process.env });
1882
+ if (!binary.candidate) {
1678
1883
  console.log(`${label}: not detected on PATH \u2014 configure manually below.`);
1679
1884
  continue;
1680
1885
  }
1681
- const result2 = configureAgent(agent, binaryPath, proposed, args.includes("--update"));
1886
+ available.add(agent);
1887
+ const result2 = await configureAgent(agent, binary.candidate, proposed, args.includes("--update"));
1682
1888
  console.log(`${label}: ${result2.ok ? "PASS" : "FAIL"} \u2014 ${result2.message}`);
1683
1889
  }
1684
- const undetected = explicit ? chosen.filter((agent) => !detectOnPath(agent)) : ["claude", "codex"];
1890
+ const undetected = explicit ? chosen.filter((agent) => !available.has(agent)) : ["claude", "codex"];
1685
1891
  if (undetected.length) {
1686
1892
  const server = { command: process.execPath, args: serverArgs.slice(1) };
1687
1893
  if (undetected.includes("claude")) {
@@ -1701,8 +1907,8 @@ args = ${JSON.stringify(server.args)}`
1701
1907
  }
1702
1908
  console.log(
1703
1909
  `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")}.
1910
+ Firefox: install or update the signed extension from the official listing \u2014 https://addons.mozilla.org/addon/6a2319d4fa544731884d/.
1911
+ Chrome: install or update the public extension from the Chrome Web Store \u2014 https://chromewebstore.google.com/detail/gazesight/hikhblabkhbaoilcffenldphdkjdmphc.
1706
1912
  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
1913
  Manual JSON import remains available under Advanced settings.`
1708
1914
  );
@@ -1724,9 +1930,19 @@ Manual JSON import remains available under Advanced settings.`
1724
1930
  if (args[0] === "doctor" && rows.some((row) => !row.pass))
1725
1931
  process.exitCode = 1;
1726
1932
  }
1727
- } catch {
1728
- console.error(
1729
- "FAIL \u2014 Check the command/options, Node >=20, installed package, private configuration and port 1024\u201365535. Run gazesight --help. Secrets are redacted."
1730
- );
1731
- process.exitCode = 1;
1933
+ } catch (error) {
1934
+ const message = error instanceof Error ? error.message : "";
1935
+ if (args[0] === "pair" && [
1936
+ "Pairing administration unavailable.",
1937
+ "The pairing request expired or was already used.",
1938
+ "The approved credential was not delivered; keep the extension open and retry pairing."
1939
+ ].includes(message)) {
1940
+ console.error(`Pairing failed \u2014 ${message}`);
1941
+ process.exitCode = 1;
1942
+ } else {
1943
+ console.error(
1944
+ "FAIL \u2014 Check the command/options, Node >=20, installed package, private configuration and port 1024\u201365535. Run gazesight --help. Secrets are redacted."
1945
+ );
1946
+ process.exitCode = 1;
1947
+ }
1732
1948
  }