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/README.md +129 -56
- package/dist/chrome/background.js +7 -7
- package/dist/chrome/content.js +7 -7
- package/dist/chrome/manifest.json +1 -1
- package/dist/chrome/options.js +37 -12
- package/dist/chrome/popup.html +3 -1
- package/dist/chrome/popup.js +1 -1
- package/dist/cli/index.js +443 -79
- package/dist/firefox/background.js +1 -1
- package/dist/firefox/content.js +6 -6
- package/dist/firefox/manifest.json +1 -1
- package/dist/firefox/options.js +29 -4
- package/dist/firefox/popup.html +3 -1
- package/dist/gazesight-firefox.xpi +0 -0
- package/dist/mcp/index.js +1946 -529
- package/dist/worker/visual-diff.js +21 -4
- package/package.json +141 -130
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
|
|
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,
|
|
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
|
|
47
|
-
env.APPDATA ??
|
|
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
|
|
52
|
+
return pathJoin(
|
|
53
53
|
home,
|
|
54
54
|
"Library",
|
|
55
55
|
"Application Support",
|
|
56
56
|
"GazeSight",
|
|
57
57
|
"config.json"
|
|
58
58
|
);
|
|
59
|
-
return
|
|
60
|
-
env.XDG_CONFIG_HOME ??
|
|
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.
|
|
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
|
-
|
|
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((
|
|
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:
|
|
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((
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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((
|
|
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((
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1452
|
-
|
|
1453
|
-
)
|
|
1454
|
-
|
|
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 =
|
|
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
|
|
1487
|
-
if (!
|
|
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
|
-
|
|
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) => !
|
|
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
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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);
|