gazesight 0.5.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
@@ -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.1";
105
106
 
106
107
  // apps/cli/src/doctor.ts
107
108
  import { z as z2 } from "zod";
@@ -1247,6 +1248,9 @@ var Bridge = class {
1247
1248
  server;
1248
1249
  http;
1249
1250
  listenError;
1251
+ closePromise;
1252
+ mcpClientCount = 0;
1253
+ desktopIpcReady = false;
1250
1254
  pairing;
1251
1255
  // Resolves the connection currently backing the global active target, or
1252
1256
  // undefined if no browser is active (nothing selected yet, or the
@@ -1309,6 +1313,8 @@ var Bridge = class {
1309
1313
  })
1310
1314
  );
1311
1315
  return {
1316
+ mcpClients: this.mcpClientCount,
1317
+ desktopIpcReady: this.desktopIpcReady,
1312
1318
  connected: this.connected,
1313
1319
  paired: this.connected,
1314
1320
  authorizedPairings: this.pairing.listPairings().filter((item) => !item.revokedAt).length,
@@ -1321,22 +1327,47 @@ var Bridge = class {
1321
1327
  activeTarget: active ? { browser: active.browser, url: active.state.url } : null
1322
1328
  };
1323
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
+ }
1324
1336
  async ready() {
1325
1337
  if (this.listenError) throw this.listenError;
1326
1338
  if (this.server.address()) return;
1327
1339
  await new Promise((resolve3, reject) => {
1328
- this.server.once("listening", resolve3);
1329
- const onError = () => reject(this.listenError);
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);
1330
1354
  this.server.once("error", onError);
1331
1355
  this.http.once("error", onError);
1332
1356
  });
1333
1357
  }
1334
1358
  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()));
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;
1340
1371
  }
1341
1372
  };
1342
1373
 
@@ -1414,159 +1445,309 @@ async function pairingFlow(config, configPath, revoke) {
1414
1445
  }
1415
1446
  }
1416
1447
 
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;
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);
1436
1457
  }
1437
- function normalize(value) {
1438
- const normalized = value.trim().replace(/^['"]|['"]$/g, "").replaceAll("\\", "/");
1439
- return process.platform === "win32" ? normalized.toLowerCase() : normalized;
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
+ });
1440
1501
  }
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";
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];
1446
1574
  }
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;
1575
+ async function verifyCandidate(candidate, platform) {
1455
1576
  try {
1456
- const parsed = JSON.parse(readFileSync2(packagePath, "utf8"));
1457
- return typeof parsed.version === "string" ? parsed.version : void 0;
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 };
1458
1592
  } catch {
1459
1593
  return void 0;
1460
1594
  }
1461
1595
  }
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);
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
+ }
1470
1621
  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
1622
+ installation: sawDiscoveryError ? "DISCOVERY_ERROR" : sawCandidate ? "UNSUPPORTED_VERSION" : "NOT_INSTALLED"
1478
1623
  };
1479
1624
  }
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);
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 };
1482
1637
  }
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}`;
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 };
1487
1646
  }
1488
- function failure(result) {
1489
- return (result.stderr || result.stdout || "unknown error").trim().split(/\r?\n/)[0];
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];
1490
1651
  }
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)}` };
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));
1506
1711
  }
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
1712
  return {
1565
1713
  ok: false,
1566
1714
  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.`
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 })}`}`
1570
1751
  };
1571
1752
  }
1572
1753
 
@@ -1580,14 +1761,6 @@ function option(name) {
1580
1761
  throw new Error("Missing option value.");
1581
1762
  return args[i + 1];
1582
1763
  }
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
1764
  async function promptClientChoice() {
1592
1765
  if (!process.stdin.isTTY) return ["claude", "codex"];
1593
1766
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
@@ -1671,17 +1844,19 @@ Agent guidance: ${config.agentGuidance.enabled ? "Enabled" : "Disabled"}${config
1671
1844
  args: serverArgs.slice(1),
1672
1845
  version: VERSION
1673
1846
  };
1847
+ const available = /* @__PURE__ */ new Set();
1674
1848
  for (const agent of chosen) {
1675
1849
  const label = agent === "claude" ? "Claude Code" : "Codex";
1676
- const binaryPath = detectOnPath(agent);
1677
- if (!binaryPath) {
1850
+ const binary = await findAgentBinary(agent, { home: process.env.USERPROFILE ?? process.env.HOME, env: process.env });
1851
+ if (!binary.candidate) {
1678
1852
  console.log(`${label}: not detected on PATH \u2014 configure manually below.`);
1679
1853
  continue;
1680
1854
  }
1681
- const result2 = configureAgent(agent, binaryPath, proposed, args.includes("--update"));
1855
+ available.add(agent);
1856
+ const result2 = await configureAgent(agent, binary.candidate, proposed, args.includes("--update"));
1682
1857
  console.log(`${label}: ${result2.ok ? "PASS" : "FAIL"} \u2014 ${result2.message}`);
1683
1858
  }
1684
- const undetected = explicit ? chosen.filter((agent) => !detectOnPath(agent)) : ["claude", "codex"];
1859
+ const undetected = explicit ? chosen.filter((agent) => !available.has(agent)) : ["claude", "codex"];
1685
1860
  if (undetected.length) {
1686
1861
  const server = { command: process.execPath, args: serverArgs.slice(1) };
1687
1862
  if (undetected.includes("claude")) {
@@ -1701,8 +1876,8 @@ args = ${JSON.stringify(server.args)}`
1701
1876
  }
1702
1877
  console.log(
1703
1878
  `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")}.
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.
1706
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")}.
1707
1882
  Manual JSON import remains available under Advanced settings.`
1708
1883
  );