visual-remote 0.3.0 → 0.3.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.
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { realpathSync } from "node:fs";
4
+ import { realpathSync as realpathSync2 } from "node:fs";
5
5
  import { resolve as resolve8 } from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import { Command, InvalidArgumentError } from "commander";
8
8
 
9
+ // src/bridge.ts
10
+ import { createConnection } from "node:net";
11
+
9
12
  // ../../packages/bridge-core/src/bridge/control-service.ts
10
13
  var ControlServiceError = class extends Error {
11
14
  statusCode;
@@ -161,6 +164,11 @@ function pairingTokensMatch(expected, candidate) {
161
164
  const candidateBytes = Buffer.from(candidate);
162
165
  return expectedBytes.length === candidateBytes.length && timingSafeEqual(expectedBytes, candidateBytes);
163
166
  }
167
+ function createPairingUrl(baseUrl, token) {
168
+ const url = new URL(baseUrl);
169
+ url.hash = `visual-pair=${encodeURIComponent(token)}`;
170
+ return url.toString();
171
+ }
164
172
 
165
173
  // ../../packages/bridge-core/src/runtime/ports.ts
166
174
  import { createServer } from "node:net";
@@ -284,7 +292,7 @@ function originAllowed(request, allowedOrigins) {
284
292
  }
285
293
  }
286
294
  function tokenAccess(token, controlToken, viewerSessions, now) {
287
- if (token === void 0 || token.length === 0) return "control";
295
+ if (token === void 0 || token.length === 0) return void 0;
288
296
  if (pairingTokensMatch(controlToken, token)) return "control";
289
297
  const expiresAt = viewerSessions.get(token);
290
298
  if (expiresAt !== void 0) {
@@ -689,7 +697,12 @@ function createGatewayServer(options) {
689
697
  const access3 = requestAccess(request, options.pairingToken, viewerSessions);
690
698
  if (access3 === void 0) {
691
699
  response.setHeader("www-authenticate", "Bearer");
692
- writeApiError(response, 401, "unauthorized", "A valid viewer token is required");
700
+ writeApiError(
701
+ response,
702
+ 401,
703
+ "unauthorized",
704
+ "A valid control or viewer token is required"
705
+ );
693
706
  return;
694
707
  }
695
708
  try {
@@ -982,7 +995,65 @@ function itemFiles(item) {
982
995
  if (direct) files.push(direct);
983
996
  return [...new Set(files)];
984
997
  }
985
- function parseCodexJsonLine(line) {
998
+ function stringArray(value) {
999
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : void 0;
1000
+ }
1001
+ function formatArgv(argv) {
1002
+ return argv.map((argument) => /^[A-Za-z0-9_./:=@%+,-]+$/u.test(argument) ? argument : JSON.stringify(argument)).join(" ");
1003
+ }
1004
+ function isDirectExecItem(item) {
1005
+ return item.type === "mcp_tool_call" && item.server === "visual_remote_exec" && item.tool === "run_readonly";
1006
+ }
1007
+ function directExecSummary(item) {
1008
+ const arguments_ = asRecord(item.arguments);
1009
+ const commands = Array.isArray(arguments_?.commands) ? arguments_.commands : [];
1010
+ const summaries = commands.flatMap((candidate) => {
1011
+ const command = asRecord(candidate);
1012
+ const argv = stringArray(command?.argv);
1013
+ return argv === void 0 ? [] : [formatArgv(argv)];
1014
+ });
1015
+ return summaries.length === 0 ? void 0 : summaries.join(" \xB7 ");
1016
+ }
1017
+ function directExecResults(item, defaultCwd) {
1018
+ const result = asRecord(item.result);
1019
+ const structured = asRecord(result?.structured_content ?? result?.structuredContent);
1020
+ const results = Array.isArray(structured?.results) ? structured.results : [];
1021
+ return results.flatMap((candidate) => {
1022
+ const command = asRecord(candidate);
1023
+ const argv = stringArray(command?.argv);
1024
+ if (argv === void 0) return [];
1025
+ const exitCode = typeof command?.exitCode === "number" ? command.exitCode : void 0;
1026
+ const durationMs = typeof command?.durationMs === "number" ? command.durationMs : void 0;
1027
+ return [{
1028
+ command: formatArgv(argv),
1029
+ cwd: asText(command?.cwd) ?? defaultCwd,
1030
+ ok: exitCode === 0,
1031
+ ...exitCode === void 0 ? {} : { exitCode },
1032
+ ...durationMs === void 0 ? {} : { durationMs },
1033
+ ...typeof command?.usedRtk === "boolean" ? { usedRtk: command.usedRtk } : {},
1034
+ ...typeof command?.timedOut === "boolean" ? { timedOut: command.timedOut } : {},
1035
+ ...typeof command?.truncated === "boolean" ? { truncated: command.truncated } : {}
1036
+ }];
1037
+ });
1038
+ }
1039
+ function normalizedUsage(record) {
1040
+ const result = asRecord(record.result);
1041
+ const usage = asRecord(record.usage) ?? (result ? asRecord(result.usage) : void 0);
1042
+ if (usage === void 0) return void 0;
1043
+ const inputTokens = usage.input_tokens ?? usage.inputTokens;
1044
+ const outputTokens = usage.output_tokens ?? usage.outputTokens;
1045
+ const cachedInputTokens = usage.cached_input_tokens ?? usage.cachedInputTokens;
1046
+ if (typeof inputTokens !== "number" || typeof outputTokens !== "number") {
1047
+ return void 0;
1048
+ }
1049
+ return {
1050
+ type: "usage",
1051
+ inputTokens,
1052
+ outputTokens,
1053
+ ...typeof cachedInputTokens === "number" ? { cachedInputTokens } : {}
1054
+ };
1055
+ }
1056
+ function parseCodexJsonLine(line, defaultCwd = "") {
986
1057
  const trimmed = line.trim();
987
1058
  if (!trimmed) return [];
988
1059
  let value;
@@ -1002,7 +1073,12 @@ function parseCodexJsonLine(line) {
1002
1073
  if (type === "turn.completed") {
1003
1074
  const result = asRecord(record.result);
1004
1075
  const summary = asText(record.summary) ?? (result ? asText(result.summary) : void 0);
1005
- return [...events, summary ? { type: "complete", summary } : { type: "complete" }];
1076
+ const usage = normalizedUsage(record);
1077
+ return [
1078
+ ...events,
1079
+ ...usage === void 0 ? [] : [usage],
1080
+ summary ? { type: "complete", summary } : { type: "complete" }
1081
+ ];
1006
1082
  }
1007
1083
  if (type === "turn.failed" || type === "error") {
1008
1084
  const error = asRecord(record.error);
@@ -1012,8 +1088,9 @@ function parseCodexJsonLine(line) {
1012
1088
  const item = asRecord(record.item);
1013
1089
  if (type === "item.started" && item) {
1014
1090
  const itemType = asText(item.type) ?? "item";
1015
- const summary = asText(item.command) ?? asText(item.text);
1016
- const start = summary ? { type: "tool_start", name: itemType, summary } : { type: "tool_start", name: itemType };
1091
+ const directExec = isDirectExecItem(item);
1092
+ const summary = directExec ? directExecSummary(item) : asText(item.command) ?? asText(item.text);
1093
+ const start = summary ? { type: "tool_start", name: directExec ? "direct_exec" : itemType, summary } : { type: "tool_start", name: directExec ? "direct_exec" : itemType };
1017
1094
  return [...events, start];
1018
1095
  }
1019
1096
  if (type === "item.completed" && item) {
@@ -1028,13 +1105,30 @@ function parseCodexJsonLine(line) {
1028
1105
  events.push({
1029
1106
  type: "command",
1030
1107
  command,
1031
- cwd: asText(item.cwd) ?? ""
1108
+ cwd: asText(item.cwd) ?? defaultCwd
1032
1109
  });
1033
1110
  }
1034
1111
  }
1112
+ const directResults = isDirectExecItem(item) ? directExecResults(item, defaultCwd) : [];
1113
+ for (const result of directResults) {
1114
+ events.push({
1115
+ type: "command",
1116
+ command: result.command,
1117
+ cwd: result.cwd,
1118
+ ...result.exitCode === void 0 ? {} : { exitCode: result.exitCode },
1119
+ ...result.durationMs === void 0 ? {} : { durationMs: result.durationMs },
1120
+ ...result.usedRtk === void 0 ? {} : { usedRtk: result.usedRtk },
1121
+ ...result.timedOut === void 0 ? {} : { timedOut: result.timedOut },
1122
+ ...result.truncated === void 0 ? {} : { truncated: result.truncated }
1123
+ });
1124
+ }
1035
1125
  for (const path of itemFiles(item)) events.push({ type: "file_hint", path });
1036
1126
  const exitCode = typeof item.exit_code === "number" ? item.exit_code : void 0;
1037
- events.push({ type: "tool_end", name: itemType, ok: exitCode === void 0 || exitCode === 0 });
1127
+ events.push({
1128
+ type: "tool_end",
1129
+ name: isDirectExecItem(item) ? "direct_exec" : itemType,
1130
+ ok: directResults.length > 0 ? directResults.every((result) => result.ok) : exitCode === void 0 || exitCode === 0
1131
+ });
1038
1132
  return events;
1039
1133
  }
1040
1134
  const message = asText(record.message);
@@ -1043,7 +1137,10 @@ function parseCodexJsonLine(line) {
1043
1137
  }
1044
1138
 
1045
1139
  // ../../packages/bridge-core/src/agents/codex-adapter.ts
1046
- import { spawn as spawn2 } from "node:child_process";
1140
+ import { execFile, spawn as spawn2 } from "node:child_process";
1141
+ import { existsSync } from "node:fs";
1142
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1143
+ import { promisify } from "node:util";
1047
1144
 
1048
1145
  // ../../packages/bridge-core/src/agents/async-queue.ts
1049
1146
  var AsyncQueue = class {
@@ -1242,6 +1339,7 @@ async function startManagedProcess(options) {
1242
1339
  }
1243
1340
 
1244
1341
  // ../../packages/bridge-core/src/agents/codex-adapter.ts
1342
+ var execFileAsync = promisify(execFile);
1245
1343
  var INHERITED_ENVIRONMENT = [
1246
1344
  "PATH",
1247
1345
  "HOME",
@@ -1282,13 +1380,72 @@ function splitLines(chunk, previous, onLine) {
1282
1380
  for (const line of lines) onLine(line);
1283
1381
  return remainder;
1284
1382
  }
1383
+ function defaultDirectExecMcpScript() {
1384
+ const candidates = [
1385
+ fileURLToPath2(new URL("./direct-exec-mcp.js", import.meta.url)),
1386
+ fileURLToPath2(
1387
+ new URL("../../../../apps/cli/dist/direct-exec-mcp.js", import.meta.url)
1388
+ )
1389
+ ];
1390
+ return candidates.find((candidate) => existsSync(candidate));
1391
+ }
1285
1392
  var CodexAdapter = class {
1286
1393
  id = "codex";
1287
1394
  #executable;
1288
1395
  #killGraceMs;
1396
+ #rtkExecutable;
1397
+ #directExecMcpScript;
1398
+ #rtkVersion;
1289
1399
  constructor(options = {}) {
1290
1400
  this.#executable = options.executable ?? "codex";
1291
1401
  this.#killGraceMs = options.killGraceMs ?? 2e3;
1402
+ this.#rtkExecutable = options.rtkExecutable ?? "rtk";
1403
+ this.#directExecMcpScript = options.directExecMcpScript === false ? void 0 : options.directExecMcpScript ?? defaultDirectExecMcpScript();
1404
+ }
1405
+ #probeRtk(environment) {
1406
+ if (this.#rtkExecutable === false) return Promise.resolve(void 0);
1407
+ this.#rtkVersion ??= execFileAsync(this.#rtkExecutable, ["--version"], {
1408
+ encoding: "utf8",
1409
+ env: environment,
1410
+ timeout: 1e3,
1411
+ windowsHide: true,
1412
+ maxBuffer: 16 * 1024
1413
+ }).then(({ stdout }) => stdout.trim().split(/\r?\n/, 1)[0] || void 0).catch(() => void 0);
1414
+ return this.#rtkVersion;
1415
+ }
1416
+ async #runtimePrompt(input, environment) {
1417
+ const commandGuidance = this.#rtkExecutable === false ? "" : await this.#probeRtk(environment).then((version) => version ? `RTK command proxy:
1418
+ - ${version} is installed and available in this runtime.
1419
+ - Prefix shell commands with RTK by default (for example: rtk git status, rtk rg <pattern>, rtk read <file>, rtk npm test).
1420
+ - Use the native command only when RTK has no suitable proxy or RTK execution fails. Do not spend time rediscovering or reinstalling RTK.` : `RTK command proxy:
1421
+ - RTK was not detected in this runtime. Use native repository commands directly and do not spend time searching for RTK.`);
1422
+ const directExecGuidance = this.#directExecMcpScript === void 0 ? "" : `Direct read-only command runner:
1423
+ - Use the visual_remote_exec run_readonly MCP tool (mcp__visual_remote_exec__run_readonly) for repository inspection by default: pwd, version checks, file listing/reading/search, and read-only Git status/diff/log/show.
1424
+ - Send argv arrays, batch independent reads in one tool call, and keep cwd at the registered workspace unless a known subdirectory is required.
1425
+ - The tool executes without a shell and applies RTK automatically when supported.
1426
+ - Use command_execution only for edits, tests/builds, or commands that genuinely require shell syntax. Do not retry a policy-rejected command through another shell unless the requested work requires that non-read-only operation.`;
1427
+ const guidance = [directExecGuidance, commandGuidance].filter(Boolean).join("\n\n");
1428
+ return guidance.length === 0 ? input.prompt : `${input.prompt.trimEnd()}
1429
+
1430
+ ${guidance}
1431
+ `;
1432
+ }
1433
+ #directExecConfig(input) {
1434
+ if (this.#directExecMcpScript === void 0) return [];
1435
+ const serverArgs = [
1436
+ this.#directExecMcpScript,
1437
+ "--repo-root",
1438
+ input.repoRoot,
1439
+ "--workspace-root",
1440
+ input.workspaceRoot,
1441
+ ...this.#rtkExecutable === false ? ["--no-rtk"] : ["--rtk", this.#rtkExecutable]
1442
+ ];
1443
+ return [
1444
+ "-c",
1445
+ `mcp_servers.visual_remote_exec.command=${JSON.stringify(process.execPath)}`,
1446
+ "-c",
1447
+ `mcp_servers.visual_remote_exec.args=${JSON.stringify(serverArgs)}`
1448
+ ];
1292
1449
  }
1293
1450
  async probe() {
1294
1451
  return await new Promise((resolve9) => {
@@ -1324,7 +1481,8 @@ var CodexAdapter = class {
1324
1481
  "-s",
1325
1482
  "workspace-write",
1326
1483
  "-C",
1327
- input.repoRoot,
1484
+ input.workspaceRoot,
1485
+ ...this.#directExecConfig(input),
1328
1486
  "-"
1329
1487
  ];
1330
1488
  yield* this.#execute(input, signal, args);
@@ -1341,7 +1499,8 @@ var CodexAdapter = class {
1341
1499
  "-s",
1342
1500
  "workspace-write",
1343
1501
  "-C",
1344
- input.repoRoot,
1502
+ input.workspaceRoot,
1503
+ ...this.#directExecConfig(input),
1345
1504
  "resume",
1346
1505
  input.sessionId,
1347
1506
  "-"
@@ -1350,9 +1509,11 @@ var CodexAdapter = class {
1350
1509
  }
1351
1510
  async *#execute(input, signal, args) {
1352
1511
  const queue = new AsyncQueue();
1512
+ const environment = processEnv(input.environment);
1513
+ const prompt = await this.#runtimePrompt(input, environment);
1353
1514
  const child = spawn2(this.#executable, args, {
1354
- cwd: input.repoRoot,
1355
- env: processEnv(input.environment),
1515
+ cwd: input.workspaceRoot,
1516
+ env: environment,
1356
1517
  detached: process.platform !== "win32",
1357
1518
  shell: false,
1358
1519
  stdio: ["pipe", "pipe", "pipe"]
@@ -1383,7 +1544,7 @@ var CodexAdapter = class {
1383
1544
  if (signal.aborted) abort();
1384
1545
  child.stdout.on("data", (chunk) => {
1385
1546
  stdoutRemainder = splitLines(chunk, stdoutRemainder, (line) => {
1386
- for (const event of parseCodexJsonLine(line)) queue.push(event);
1547
+ for (const event of parseCodexJsonLine(line, input.workspaceRoot)) queue.push(event);
1387
1548
  });
1388
1549
  });
1389
1550
  child.stderr.on("data", (chunk) => {
@@ -1398,7 +1559,9 @@ var CodexAdapter = class {
1398
1559
  void (async () => {
1399
1560
  await requestTermination();
1400
1561
  if (stdoutRemainder.trim()) {
1401
- for (const event of parseCodexJsonLine(stdoutRemainder)) queue.push(event);
1562
+ for (const event of parseCodexJsonLine(stdoutRemainder, input.workspaceRoot)) {
1563
+ queue.push(event);
1564
+ }
1402
1565
  }
1403
1566
  if (stderrRemainder.trim()) queue.push({ type: "warning", text: stderrRemainder });
1404
1567
  if (timedOut) queue.end(new AgentTimeoutError());
@@ -1419,7 +1582,7 @@ var CodexAdapter = class {
1419
1582
  child.stdin.on("error", (error) => {
1420
1583
  if (error.code !== "EPIPE") queue.end(error);
1421
1584
  });
1422
- child.stdin.end(input.prompt);
1585
+ child.stdin.end(prompt);
1423
1586
  try {
1424
1587
  for await (const event of queue) yield event;
1425
1588
  } finally {
@@ -1438,6 +1601,9 @@ var CodexAdapter = class {
1438
1601
  }
1439
1602
  };
1440
1603
 
1604
+ // ../../packages/bridge-core/src/agents/direct-exec.ts
1605
+ var DEFAULT_OUTPUT_BYTES = 64 * 1024;
1606
+
1441
1607
  // ../../packages/bridge-core/src/config/loader.ts
1442
1608
  import { readFile, realpath, stat as stat2 } from "node:fs/promises";
1443
1609
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -1867,6 +2033,20 @@ function isWithin(root, candidate) {
1867
2033
  const path = relative2(root, candidate);
1868
2034
  return path === "" || !path.startsWith(`..${sep2}`) && path !== ".." && !isAbsolute2(path);
1869
2035
  }
2036
+ function rebaseWorkspacePatterns(repoRoot, workspaceRoot, patterns) {
2037
+ const repository = resolve2(repoRoot);
2038
+ const workspace = resolve2(workspaceRoot);
2039
+ if (!isWithin(repository, workspace)) {
2040
+ throw new PathSafetyError(
2041
+ "PATH_OUTSIDE_REPOSITORY",
2042
+ workspaceRoot,
2043
+ `Workspace is outside the repository: ${workspaceRoot}`
2044
+ );
2045
+ }
2046
+ const prefix = repositoryRelative(repository, workspace);
2047
+ if (!prefix) return [...patterns];
2048
+ return patterns.map((pattern) => normalizeSlashes(`${prefix}/${pattern}`));
2049
+ }
1870
2050
  var PathPolicy = class {
1871
2051
  repoRoot;
1872
2052
  allowedPatterns;
@@ -2710,6 +2890,9 @@ var TERMINAL = /* @__PURE__ */ new Set(["accepted", "reverted", "failed", "cance
2710
2890
  function isActiveTaskStatus(status) {
2711
2891
  return status !== "queued" && status !== "review" && !TERMINAL.has(status);
2712
2892
  }
2893
+ function isWorkingTaskStatus(status) {
2894
+ return status === "queued" || isActiveTaskStatus(status);
2895
+ }
2713
2896
  function isTerminalTaskStatus(status) {
2714
2897
  return TERMINAL.has(status);
2715
2898
  }
@@ -2764,14 +2947,18 @@ Follow-up context:
2764
2947
  - Previous request: ${options.parent.requestText}
2765
2948
  - Previous diff summary: ${options.parent.diffSummary || "No file changes"}
2766
2949
  ` : "";
2767
- return `You are editing the repository at: ${options.repoRoot}
2768
- Workspace: ${options.workspaceRoot}
2950
+ return `Target service context (authoritative):
2951
+ - Repository worktree: ${options.repoRoot}
2952
+ - Workspace: ${options.workspaceRoot}
2953
+ - Browser URL: ${options.context.page.url}
2954
+ ${options.upstreamUrl ? `- Local upstream URL: ${options.upstreamUrl}
2955
+ ` : ""}
2956
+ Treat the workspace above as the already-resolved service directory and the repository worktree as its safety boundary. Run project commands from the workspace; do not search parent directories or run directory-discovery commands to locate the project again.
2769
2957
 
2770
2958
  User request:
2771
2959
  ${options.context.request.text}
2772
2960
 
2773
2961
  Selected UI context:
2774
- - URL: ${options.context.page.url}
2775
2962
  - Route: ${options.context.page.pathname}
2776
2963
  - Selection mode: ${options.context.selection.mode}
2777
2964
  ${targets || "- No concrete target; use the page context and repository search."}
@@ -3246,6 +3433,7 @@ function repositoryGuardMessage(result) {
3246
3433
  var TaskService = class {
3247
3434
  #projectId;
3248
3435
  #workspaceRoot;
3436
+ #upstreamUrl;
3249
3437
  #adapter;
3250
3438
  #store;
3251
3439
  #git;
@@ -3269,6 +3457,7 @@ var TaskService = class {
3269
3457
  constructor(options) {
3270
3458
  this.#projectId = options.projectId;
3271
3459
  this.#workspaceRoot = resolve6(options.workspaceRoot ?? options.git.repoRoot);
3460
+ this.#upstreamUrl = options.upstreamUrl;
3272
3461
  if (!isWithin3(options.git.repoRoot, this.#workspaceRoot)) {
3273
3462
  throw new TaskServiceError(
3274
3463
  "WORKSPACE_OUTSIDE_REPOSITORY",
@@ -3655,6 +3844,7 @@ var TaskService = class {
3655
3844
  const prompt = buildAgentPrompt({
3656
3845
  repoRoot: this.#git.repoRoot,
3657
3846
  workspaceRoot: this.#workspaceRoot,
3847
+ ...this.#upstreamUrl === void 0 ? {} : { upstreamUrl: this.#upstreamUrl },
3658
3848
  contextBundlePath: contextPath,
3659
3849
  context,
3660
3850
  allowedPatterns: this.#git.pathPolicy.allowedPatterns,
@@ -3887,6 +4077,18 @@ var TaskService = class {
3887
4077
  };
3888
4078
 
3889
4079
  // ../../packages/bridge-core/src/verification/browser-sessions.ts
4080
+ function errorSignature(event) {
4081
+ return `${event.level}\0${event.message}`;
4082
+ }
4083
+ function samePage(left, right) {
4084
+ try {
4085
+ const leftUrl = new URL(left);
4086
+ const rightUrl = new URL(right);
4087
+ return leftUrl.origin === rightUrl.origin && leftUrl.pathname === rightUrl.pathname && leftUrl.search === rightUrl.search;
4088
+ } catch {
4089
+ return left === right;
4090
+ }
4091
+ }
3890
4092
  var BrowserSessionManager = class {
3891
4093
  #sessions = /* @__PURE__ */ new Map();
3892
4094
  #maxConsoleEvents;
@@ -3962,7 +4164,13 @@ var BrowserSessionManager = class {
3962
4164
  return {
3963
4165
  browserSessionId: id,
3964
4166
  renderRevision: session.renderRevision,
3965
- startedAt: now.toISOString()
4167
+ startedAt: now.toISOString(),
4168
+ url: session.url,
4169
+ knownErrorSignatures: [
4170
+ ...new Set(
4171
+ session.consoleEvents.filter((event) => event.level === "error" || event.level === "unhandled").map(errorSignature)
4172
+ )
4173
+ ]
3966
4174
  };
3967
4175
  }
3968
4176
  verify(baseline, options = {}) {
@@ -3975,10 +4183,16 @@ var BrowserSessionManager = class {
3975
4183
  summary: "Origin browser session is disconnected."
3976
4184
  };
3977
4185
  }
3978
- const newErrors = session.consoleEvents.filter(
3979
- (event) => event.createdAt >= baseline.startedAt && (event.level === "error" || event.level === "unhandled")
3980
- );
4186
+ const knownErrors = new Set(baseline.knownErrorSignatures);
4187
+ const newErrors = [
4188
+ ...new Map(
4189
+ session.consoleEvents.filter(
4190
+ (event) => event.createdAt > baseline.startedAt && (event.level === "error" || event.level === "unhandled") && !knownErrors.has(errorSignature(event))
4191
+ ).map((event) => [errorSignature(event), event])
4192
+ ).values()
4193
+ ];
3981
4194
  const renderChanged = session.renderRevision > baseline.renderRevision;
4195
+ const pageUnchanged = samePage(baseline.url, session.url);
3982
4196
  const targetResult = options.taskId === void 0 ? void 0 : session.targetResults.filter(
3983
4197
  (result) => result.taskId === options.taskId && result.createdAt >= baseline.startedAt
3984
4198
  ).at(-1);
@@ -3991,6 +4205,15 @@ var BrowserSessionManager = class {
3991
4205
  summary: `${newErrors.length} new browser error${newErrors.length === 1 ? "" : "s"} detected.`
3992
4206
  };
3993
4207
  }
4208
+ if (!pageUnchanged) {
4209
+ return {
4210
+ status: "partial",
4211
+ renderChanged,
4212
+ newErrors: [],
4213
+ ...targetResult ? { targetResult } : {},
4214
+ summary: "Origin browser navigated to a different page during verification."
4215
+ };
4216
+ }
3994
4217
  if (options.targetEvidenceRequired) {
3995
4218
  const targetChanged = targetResult?.state === "found-and-changed" && targetResult.targetCount > 0 && targetResult.foundCount === targetResult.targetCount && targetResult.changedCount > 0 && targetResult.renderRevision > baseline.renderRevision;
3996
4219
  if (targetChanged) {
@@ -4579,7 +4802,7 @@ ${output2}` : ""}`
4579
4802
  status: "ok",
4580
4803
  bridge: "online",
4581
4804
  projectId: options.project.id,
4582
- activeTask: taskService.list().find((task) => !["accepted", "reverted", "failed", "canceled", "unsafe"].includes(task.status))?.id ?? null
4805
+ activeTask: taskService.list().find((task) => isWorkingTaskStatus(task.status))?.id ?? null
4583
4806
  }),
4584
4807
  project: () => ({
4585
4808
  id: options.project.id,
@@ -4630,14 +4853,23 @@ async function createDefaultControlService(context, environment = process.env) {
4630
4853
  );
4631
4854
  }
4632
4855
  const git = await GitTransactionManager.open(context.repoRoot, {
4633
- allowed: loaded.config.paths.allowed,
4634
- denied: loaded.config.paths.denied
4856
+ allowed: rebaseWorkspacePatterns(
4857
+ context.repoRoot,
4858
+ context.workspaceRoot,
4859
+ loaded.config.paths.allowed
4860
+ ),
4861
+ denied: rebaseWorkspacePatterns(
4862
+ context.repoRoot,
4863
+ context.workspaceRoot,
4864
+ loaded.config.paths.denied
4865
+ )
4635
4866
  });
4636
4867
  const storagePaths = await resolveStoragePaths(context.repoRoot, environment);
4637
4868
  const store = new SqliteTaskStore(storagePaths.databasePath);
4638
4869
  const taskService = new TaskService({
4639
4870
  projectId: context.projectId,
4640
4871
  workspaceRoot: context.workspaceRoot,
4872
+ upstreamUrl: context.upstreamUrl,
4641
4873
  adapter: new CodexAdapter(),
4642
4874
  store,
4643
4875
  git,
@@ -4646,7 +4878,7 @@ async function createDefaultControlService(context, environment = process.env) {
4646
4878
  resumeMode: loaded.config.agent.resumeMode,
4647
4879
  environment: {}
4648
4880
  });
4649
- return createTaskControlService({
4881
+ const controlService = createTaskControlService({
4650
4882
  taskService,
4651
4883
  hmrWaitMs: loaded.config.verification.hmrWaitMs,
4652
4884
  verificationCommands: loaded.config.verification.commands,
@@ -4658,10 +4890,32 @@ async function createDefaultControlService(context, environment = process.env) {
4658
4890
  upstreamUrl: context.upstreamUrl
4659
4891
  }
4660
4892
  });
4893
+ const reportRuntimeState = () => {
4894
+ const activeTask = taskService.list().find((task) => isWorkingTaskStatus(task.status));
4895
+ context.onRuntimeState?.({
4896
+ status: activeTask === void 0 ? "idle" : "working",
4897
+ ...activeTask === void 0 ? {} : { activeTaskId: activeTask.id }
4898
+ });
4899
+ };
4900
+ const unsubscribeRuntime = taskService.subscribe(reportRuntimeState);
4901
+ reportRuntimeState();
4902
+ return {
4903
+ ...controlService,
4904
+ close: async () => {
4905
+ unsubscribeRuntime();
4906
+ await controlService.close?.();
4907
+ }
4908
+ };
4661
4909
  }
4662
4910
 
4663
4911
  // ../../packages/bridge-core/src/runtime/registry.ts
4664
4912
  import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
4913
+ import {
4914
+ readFileSync,
4915
+ realpathSync,
4916
+ rmSync,
4917
+ unlinkSync
4918
+ } from "node:fs";
4665
4919
  import {
4666
4920
  mkdir as mkdir2,
4667
4921
  link,
@@ -4702,6 +4956,11 @@ async function repositoryKey(repositoryRoot) {
4702
4956
  const canonicalRoot = await realpath8(repositoryRoot);
4703
4957
  return createHash3("sha256").update(canonicalRoot).digest("hex");
4704
4958
  }
4959
+ function runtimeDirectoryForSync(repositoryRoot, options) {
4960
+ const canonicalRoot = realpathSync(repositoryRoot);
4961
+ const repoKey = createHash3("sha256").update(canonicalRoot).digest("hex");
4962
+ return join3(runtimeRoot(options), repoKey);
4963
+ }
4705
4964
  async function runtimeDirectoryFor(repositoryRoot, options = {}) {
4706
4965
  return join3(runtimeRoot(options), await repositoryKey(repositoryRoot));
4707
4966
  }
@@ -4740,6 +4999,13 @@ async function readJson(path) {
4740
4999
  return void 0;
4741
5000
  }
4742
5001
  }
5002
+ function readJsonSync(path) {
5003
+ try {
5004
+ return JSON.parse(readFileSync(path, "utf8"));
5005
+ } catch {
5006
+ return void 0;
5007
+ }
5008
+ }
4743
5009
  async function readInstance(repositoryRoot, options = {}) {
4744
5010
  const directory = await runtimeDirectoryFor(repositoryRoot, options);
4745
5011
  const value = await readJson(join3(directory, "instance.json"));
@@ -4757,6 +5023,21 @@ async function writeInstance(repositoryRoot, instance, options = {}) {
4757
5023
  });
4758
5024
  await rename(temporaryPath, path);
4759
5025
  }
5026
+ async function updateInstance(repositoryRoot, expectedPid, update, options = {}) {
5027
+ const current = await readInstance(repositoryRoot, options);
5028
+ if (current === void 0 || current.pid !== expectedPid) return void 0;
5029
+ const next = {
5030
+ ...current,
5031
+ ...update.status === void 0 ? {} : { status: update.status }
5032
+ };
5033
+ if (update.activeTaskId === null) {
5034
+ delete next.activeTaskId;
5035
+ } else if (update.activeTaskId !== void 0) {
5036
+ next.activeTaskId = update.activeTaskId;
5037
+ }
5038
+ await writeInstance(repositoryRoot, next, options);
5039
+ return next;
5040
+ }
4760
5041
  async function removeInstance(repositoryRoot, expectedPid, options = {}) {
4761
5042
  const directory = await runtimeDirectoryFor(repositoryRoot, options);
4762
5043
  const path = join3(directory, "instance.json");
@@ -4768,6 +5049,17 @@ async function removeInstance(repositoryRoot, expectedPid, options = {}) {
4768
5049
  }
4769
5050
  await rm3(path, { force: true });
4770
5051
  }
5052
+ function removeInstanceSync(repositoryRoot, expectedPid, options = {}) {
5053
+ const directory = runtimeDirectoryForSync(repositoryRoot, options);
5054
+ const path = join3(directory, "instance.json");
5055
+ if (expectedPid !== void 0) {
5056
+ const current = readJsonSync(path);
5057
+ if (!isBridgeInstanceRecord(current) || current.pid !== expectedPid) {
5058
+ return;
5059
+ }
5060
+ }
5061
+ rmSync(path, { force: true });
5062
+ }
4771
5063
  async function acquireWorktreeLock(repositoryRoot, options = {}) {
4772
5064
  const repoRoot = await realpath8(repositoryRoot);
4773
5065
  const repoKey = await repositoryKey(repoRoot);
@@ -4799,23 +5091,43 @@ async function acquireWorktreeLock(repositoryRoot, options = {}) {
4799
5091
  await rm3(candidatePath, { force: true });
4800
5092
  }
4801
5093
  let released = false;
5094
+ let releasePromise;
4802
5095
  return {
4803
5096
  repoKey,
4804
5097
  runtimeDirectory,
4805
5098
  lockPath,
4806
- async release() {
5099
+ release() {
4807
5100
  if (released) {
5101
+ return Promise.resolve();
5102
+ }
5103
+ releasePromise ??= (async () => {
5104
+ const current = await readJson(lockPath);
5105
+ if (isLockRecord(current) && current.ownerId === ownerId) {
5106
+ await unlink(lockPath).catch((error) => {
5107
+ if (error.code !== "ENOENT") {
5108
+ throw error;
5109
+ }
5110
+ });
5111
+ }
5112
+ released = true;
5113
+ })();
5114
+ return releasePromise;
5115
+ },
5116
+ releaseSync() {
5117
+ if (released || releasePromise !== void 0) {
4808
5118
  return;
4809
5119
  }
4810
- released = true;
4811
- const current = await readJson(lockPath);
5120
+ const current = readJsonSync(lockPath);
4812
5121
  if (isLockRecord(current) && current.ownerId === ownerId) {
4813
- await unlink(lockPath).catch((error) => {
4814
- if (error.code !== "ENOENT") {
5122
+ try {
5123
+ unlinkSync(lockPath);
5124
+ } catch (error) {
5125
+ if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "ENOENT") {
4815
5126
  throw error;
4816
5127
  }
4817
- });
5128
+ }
4818
5129
  }
5130
+ released = true;
4819
5131
  }
4820
5132
  };
4821
5133
  } catch (error) {
@@ -4835,10 +5147,10 @@ async function acquireWorktreeLock(repositoryRoot, options = {}) {
4835
5147
  }
4836
5148
 
4837
5149
  // ../../packages/bridge-core/src/runtime/repository.ts
4838
- import { execFile } from "node:child_process";
5150
+ import { execFile as execFile2 } from "node:child_process";
4839
5151
  import { realpath as realpath9 } from "node:fs/promises";
4840
- import { promisify } from "node:util";
4841
- var execFileAsync = promisify(execFile);
5152
+ import { promisify as promisify2 } from "node:util";
5153
+ var execFileAsync2 = promisify2(execFile2);
4842
5154
  var GitWorktreeNotFoundError = class extends Error {
4843
5155
  constructor(cwd, options = {}) {
4844
5156
  super(
@@ -4850,7 +5162,7 @@ var GitWorktreeNotFoundError = class extends Error {
4850
5162
  };
4851
5163
  async function discoverGitWorktreeRoot(cwd = process.cwd()) {
4852
5164
  try {
4853
- const { stdout } = await execFileAsync(
5165
+ const { stdout } = await execFileAsync2(
4854
5166
  "git",
4855
5167
  ["-C", cwd, "rev-parse", "--show-toplevel"],
4856
5168
  {
@@ -4869,6 +5181,91 @@ async function discoverGitWorktreeRoot(cwd = process.cwd()) {
4869
5181
  }
4870
5182
 
4871
5183
  // src/bridge.ts
5184
+ var DEFAULT_UPSTREAM_MONITOR_INTERVAL_MS = 1e3;
5185
+ var DEFAULT_UPSTREAM_CONNECT_TIMEOUT_MS = 500;
5186
+ var DEFAULT_UPSTREAM_FAILURE_GRACE_MS = 5e3;
5187
+ function positiveMilliseconds(value, fallback) {
5188
+ return value === void 0 || !Number.isFinite(value) || value <= 0 ? fallback : Math.max(1, Math.floor(value));
5189
+ }
5190
+ async function probeUpstream(upstreamUrl, timeoutMs) {
5191
+ const upstream = new URL(upstreamUrl);
5192
+ const hostname = upstream.hostname.startsWith("[") ? upstream.hostname.slice(1, -1) : upstream.hostname;
5193
+ const port = Number(
5194
+ upstream.port || (upstream.protocol === "https:" ? 443 : 80)
5195
+ );
5196
+ return await new Promise((resolve9) => {
5197
+ let settled = false;
5198
+ let timeout;
5199
+ const socket = createConnection({ host: hostname, port });
5200
+ socket.unref();
5201
+ const finish = (reachable) => {
5202
+ if (settled) return;
5203
+ settled = true;
5204
+ if (timeout !== void 0) clearTimeout(timeout);
5205
+ socket.destroy();
5206
+ resolve9(reachable);
5207
+ };
5208
+ socket.once("connect", () => finish(true));
5209
+ socket.once("error", () => finish(false));
5210
+ timeout = setTimeout(() => finish(false), timeoutMs);
5211
+ timeout.unref();
5212
+ });
5213
+ }
5214
+ function monitorUpstream(bridge, options) {
5215
+ const intervalMs = positiveMilliseconds(
5216
+ options.intervalMs,
5217
+ DEFAULT_UPSTREAM_MONITOR_INTERVAL_MS
5218
+ );
5219
+ const connectTimeoutMs = positiveMilliseconds(
5220
+ options.connectTimeoutMs,
5221
+ DEFAULT_UPSTREAM_CONNECT_TIMEOUT_MS
5222
+ );
5223
+ const initialTimeoutMs = positiveMilliseconds(
5224
+ options.initialTimeoutMs,
5225
+ 6e4
5226
+ );
5227
+ const failureGraceMs = positiveMilliseconds(
5228
+ options.failureGraceMs,
5229
+ DEFAULT_UPSTREAM_FAILURE_GRACE_MS
5230
+ );
5231
+ let connected = false;
5232
+ let unavailableSince;
5233
+ let stopped = false;
5234
+ let timer;
5235
+ const stop = () => {
5236
+ stopped = true;
5237
+ if (timer !== void 0) clearTimeout(timer);
5238
+ };
5239
+ const schedule = () => {
5240
+ if (stopped) return;
5241
+ timer = setTimeout(() => {
5242
+ void check().catch(() => void 0);
5243
+ }, intervalMs);
5244
+ timer.unref();
5245
+ };
5246
+ const check = async () => {
5247
+ if (stopped) return;
5248
+ const reachable = await probeUpstream(bridge.upstreamUrl, connectTimeoutMs);
5249
+ if (stopped) return;
5250
+ const now = Date.now();
5251
+ if (reachable) {
5252
+ connected = true;
5253
+ unavailableSince = void 0;
5254
+ schedule();
5255
+ return;
5256
+ }
5257
+ unavailableSince ??= now;
5258
+ const timeoutMs = connected ? failureGraceMs : initialTimeoutMs;
5259
+ if (now - unavailableSince >= timeoutMs) {
5260
+ stop();
5261
+ await bridge.close();
5262
+ return;
5263
+ }
5264
+ schedule();
5265
+ };
5266
+ void bridge.closed.then(stop);
5267
+ void check().catch(() => void 0);
5268
+ }
4872
5269
  function normalizeUpstream(value) {
4873
5270
  const url = new URL(value);
4874
5271
  if (url.protocol !== "http:" && url.protocol !== "https:") {
@@ -4905,6 +5302,26 @@ async function startBridgeCore(options, dependencies) {
4905
5302
  let gateway;
4906
5303
  let controlService;
4907
5304
  let registryWritten = false;
5305
+ let registryUpdate = Promise.resolve();
5306
+ let latestRuntimeState = { status: "idle" };
5307
+ const updateRuntimeState = (status, activeTaskId) => {
5308
+ latestRuntimeState = {
5309
+ status,
5310
+ ...activeTaskId === void 0 ? {} : { activeTaskId }
5311
+ };
5312
+ if (!registryWritten) return;
5313
+ registryUpdate = registryUpdate.then(async () => {
5314
+ await updateInstance(
5315
+ loadedConfig.repoRoot,
5316
+ process.pid,
5317
+ {
5318
+ status,
5319
+ activeTaskId: activeTaskId ?? null
5320
+ },
5321
+ { environment }
5322
+ );
5323
+ }).catch(() => void 0);
5324
+ };
4908
5325
  try {
4909
5326
  lock = options.lock ?? await acquireWorktreeLock(loadedConfig.repoRoot, { environment });
4910
5327
  const host = options.host ?? loadedConfig.config.gateway.host;
@@ -4918,7 +5335,10 @@ async function startBridgeCore(options, dependencies) {
4918
5335
  repoRoot: loadedConfig.repoRoot,
4919
5336
  configRoot: loadedConfig.configRoot,
4920
5337
  workspaceRoot: loadedConfig.workspaceRoot,
4921
- upstreamUrl: options.upstreamUrl
5338
+ upstreamUrl: options.upstreamUrl,
5339
+ onRuntimeState: (state) => {
5340
+ updateRuntimeState(state.status, state.activeTaskId);
5341
+ }
4922
5342
  };
4923
5343
  controlService = await resolveControlService(dependencies, controlContext);
4924
5344
  const allowedOrigins = new Set(loadedConfig.config.security.allowedOrigins);
@@ -4933,20 +5353,33 @@ async function startBridgeCore(options, dependencies) {
4933
5353
  allowedOrigins: [...allowedOrigins]
4934
5354
  });
4935
5355
  const address = await gateway.start();
4936
- const openUrl = publicUrl ?? address.url;
5356
+ const openUrl = createPairingUrl(publicUrl ?? address.url, token);
4937
5357
  const instance = {
4938
5358
  projectId: loadedConfig.config.project.id,
4939
5359
  repoRoot: loadedConfig.repoRoot,
4940
5360
  pid: process.pid,
4941
5361
  gatewayUrl: address.url,
4942
5362
  upstreamUrl: options.upstreamUrl,
4943
- status: "idle",
5363
+ status: latestRuntimeState.status,
4944
5364
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5365
+ ...latestRuntimeState.activeTaskId === void 0 ? {} : { activeTaskId: latestRuntimeState.activeTaskId },
4945
5366
  ...publicUrl === void 0 ? {} : { publicUrl }
4946
5367
  };
4947
5368
  await writeInstance(loadedConfig.repoRoot, instance, { environment });
4948
5369
  registryWritten = true;
4949
- let closed = false;
5370
+ const emergencyExitCleanup = () => {
5371
+ try {
5372
+ removeInstanceSync(loadedConfig.repoRoot, process.pid, { environment });
5373
+ lock?.releaseSync();
5374
+ } catch {
5375
+ }
5376
+ };
5377
+ process.once("exit", emergencyExitCleanup);
5378
+ let resolveClosed = () => void 0;
5379
+ const closed = new Promise((resolve9) => {
5380
+ resolveClosed = resolve9;
5381
+ });
5382
+ let closePromise;
4950
5383
  return {
4951
5384
  mode: options.mode,
4952
5385
  projectId: loadedConfig.config.project.id,
@@ -4958,18 +5391,33 @@ async function startBridgeCore(options, dependencies) {
4958
5391
  openUrl,
4959
5392
  gateway,
4960
5393
  ...options.managedProcess === void 0 ? {} : { managedProcess: options.managedProcess },
4961
- async close() {
4962
- if (closed) {
4963
- return;
4964
- }
4965
- closed = true;
4966
- await Promise.allSettled([
4967
- gateway?.close(),
4968
- options.managedProcess?.stop(),
4969
- controlService?.close?.()
4970
- ]);
4971
- await removeInstance(loadedConfig.repoRoot, process.pid, { environment });
4972
- await lock?.release();
5394
+ closed,
5395
+ close() {
5396
+ closePromise ??= (async () => {
5397
+ process.off("exit", emergencyExitCleanup);
5398
+ try {
5399
+ await registryUpdate;
5400
+ await updateInstance(
5401
+ loadedConfig.repoRoot,
5402
+ process.pid,
5403
+ { status: "stopping", activeTaskId: null },
5404
+ { environment }
5405
+ );
5406
+ await Promise.allSettled([
5407
+ gateway?.close(),
5408
+ options.managedProcess?.stop(),
5409
+ controlService?.close?.()
5410
+ ]);
5411
+ await removeInstance(loadedConfig.repoRoot, process.pid, { environment });
5412
+ } finally {
5413
+ try {
5414
+ await lock?.release();
5415
+ } finally {
5416
+ resolveClosed();
5417
+ }
5418
+ }
5419
+ })();
5420
+ return closePromise;
4973
5421
  }
4974
5422
  };
4975
5423
  } catch (error) {
@@ -4990,7 +5438,7 @@ async function startAttachBridge(options, dependencies = {}) {
4990
5438
  const repoRoot = await discoverGitWorktreeRoot(cwd);
4991
5439
  const configRoot = await discoverVisualDevConfigRoot(cwd, repoRoot);
4992
5440
  const loadedConfig = await loadVisualDevConfig(repoRoot, { configRoot });
4993
- return await startBridgeCore(
5441
+ const bridge = await startBridgeCore(
4994
5442
  {
4995
5443
  mode: "attach",
4996
5444
  loadedConfig,
@@ -5001,6 +5449,13 @@ async function startAttachBridge(options, dependencies = {}) {
5001
5449
  },
5002
5450
  dependencies
5003
5451
  );
5452
+ if (dependencies.upstreamMonitor !== false) {
5453
+ monitorUpstream(bridge, {
5454
+ initialTimeoutMs: loadedConfig.config.upstream.ready.timeoutMs,
5455
+ ...dependencies.upstreamMonitor
5456
+ });
5457
+ }
5458
+ return bridge;
5004
5459
  }
5005
5460
  async function startManagedBridge(options = {}, dependencies = {}) {
5006
5461
  const cwd = dependencies.cwd ?? process.cwd();
@@ -5102,6 +5557,7 @@ async function runBridgeUntilSignal(bridge, processLike = process, gracefulTimeo
5102
5557
  };
5103
5558
  for (const signal of signals) processLike.once(signal, shutdown);
5104
5559
  void bridge.managedProcess?.exit.then(shutdown);
5560
+ void bridge.closed?.then(shutdown);
5105
5561
  });
5106
5562
  }
5107
5563
  function formatBridgeSummary(bridge) {
@@ -5120,9 +5576,9 @@ function formatBridgeSummary(bridge) {
5120
5576
  import { constants } from "node:fs";
5121
5577
  import { access as access2, stat as stat3 } from "node:fs/promises";
5122
5578
  import { delimiter, isAbsolute as isAbsolute5, join as join4, relative as relative6, resolve as resolve7 } from "node:path";
5123
- import { execFile as execFile2 } from "node:child_process";
5124
- import { promisify as promisify2 } from "node:util";
5125
- var execFileAsync2 = promisify2(execFile2);
5579
+ import { execFile as execFile3 } from "node:child_process";
5580
+ import { promisify as promisify3 } from "node:util";
5581
+ var execFileAsync3 = promisify3(execFile3);
5126
5582
  async function fileExists(path) {
5127
5583
  try {
5128
5584
  await stat3(path);
@@ -5136,7 +5592,7 @@ async function fileExists(path) {
5136
5592
  }
5137
5593
  async function isIgnored(repoRoot, path) {
5138
5594
  try {
5139
- await execFileAsync2("git", ["-C", repoRoot, "check-ignore", "--quiet", "--", path], {
5595
+ await execFileAsync3("git", ["-C", repoRoot, "check-ignore", "--quiet", "--", path], {
5140
5596
  windowsHide: true
5141
5597
  });
5142
5598
  return true;
@@ -5178,6 +5634,7 @@ async function runDoctor(dependencies = {}) {
5178
5634
  repoRoot
5179
5635
  );
5180
5636
  const loaded = await loadVisualDevConfig(repoRoot, { configRoot });
5637
+ const environment = dependencies.environment ?? process.env;
5181
5638
  checks.push({
5182
5639
  name: "config",
5183
5640
  status: loaded.loadedFiles.length === 0 ? "warning" : "pass",
@@ -5198,7 +5655,7 @@ async function runDoctor(dependencies = {}) {
5198
5655
  const available = executable !== void 0 && await executableAvailable(
5199
5656
  executable,
5200
5657
  loaded.workspaceRoot,
5201
- dependencies.environment ?? process.env
5658
+ environment
5202
5659
  );
5203
5660
  checks.push({
5204
5661
  name: "dev-command",
@@ -5206,6 +5663,65 @@ async function runDoctor(dependencies = {}) {
5206
5663
  message: available ? `${executable} is executable.` : `${executable ?? "<empty>"} was not found or is not executable.`
5207
5664
  });
5208
5665
  }
5666
+ const adapter = loaded.config.agent.adapter;
5667
+ const adapterSupported = adapter === "codex";
5668
+ const agentAvailable = await executableAvailable(
5669
+ adapter,
5670
+ loaded.workspaceRoot,
5671
+ environment
5672
+ );
5673
+ checks.push({
5674
+ name: "agent",
5675
+ status: adapterSupported && agentAvailable ? "pass" : "fail",
5676
+ message: !adapterSupported ? `${adapter} is configured but is not implemented in this build.` : agentAvailable ? `${adapter} is executable.` : `${adapter} was not found or is not executable.`
5677
+ });
5678
+ const rtkAvailable = await executableAvailable(
5679
+ "rtk",
5680
+ loaded.workspaceRoot,
5681
+ environment
5682
+ );
5683
+ checks.push({
5684
+ name: "rtk",
5685
+ status: rtkAvailable ? "pass" : "warning",
5686
+ message: rtkAvailable ? "rtk is available for token-efficient command output." : "rtk was not found; agent commands will use their native output."
5687
+ });
5688
+ const verificationCommands = loaded.config.verification.commands;
5689
+ if (verificationCommands.length === 0) {
5690
+ checks.push({
5691
+ name: "verification",
5692
+ status: "warning",
5693
+ message: "No verification commands are configured."
5694
+ });
5695
+ } else {
5696
+ const availability = await Promise.all(
5697
+ verificationCommands.map(async ({ command: command2, name }) => ({
5698
+ name,
5699
+ available: await executableAvailable(
5700
+ command2[0] ?? "",
5701
+ loaded.workspaceRoot,
5702
+ environment
5703
+ )
5704
+ }))
5705
+ );
5706
+ const missing = availability.filter(({ available }) => !available).map(({ name }) => name);
5707
+ checks.push({
5708
+ name: "verification",
5709
+ status: missing.length === 0 ? "pass" : "fail",
5710
+ message: missing.length === 0 ? `${verificationCommands.length} verification command(s) are ready.` : `Missing executable for: ${missing.join(", ")}.`
5711
+ });
5712
+ }
5713
+ const publicUrl = loaded.config.gateway.publicUrl;
5714
+ checks.push({
5715
+ name: "public-url",
5716
+ status: publicUrl === void 0 ? "warning" : "pass",
5717
+ message: publicUrl === void 0 ? "No gateway.publicUrl is configured; pairing links will use the local gateway URL." : `Pairing links will use ${publicUrl}.`
5718
+ });
5719
+ const allowedOrigins = loaded.config.security.allowedOrigins;
5720
+ checks.push({
5721
+ name: "allowed-origins",
5722
+ status: allowedOrigins.length > 0 || publicUrl !== void 0 ? "pass" : "warning",
5723
+ message: allowedOrigins.length > 0 ? `${allowedOrigins.length} browser origin(s) are explicitly allowed.` : publicUrl !== void 0 ? "The configured public URL origin will be allowed automatically." : "No browser origins are configured; add security.allowedOrigins before remote access."
5724
+ });
5209
5725
  } catch (error) {
5210
5726
  checks.push({
5211
5727
  name: "config",
@@ -5666,13 +6182,18 @@ function formatBridgeStatus(status) {
5666
6182
  return "No Visual Bridge is running for this worktree.";
5667
6183
  }
5668
6184
  const { instance } = status;
5669
- return [
6185
+ const rows = [
5670
6186
  `Project: ${instance.projectId}`,
5671
6187
  `Status: ${instance.status}`,
5672
6188
  `PID: ${instance.pid}`,
5673
6189
  `Gateway: ${instance.gatewayUrl}`,
5674
6190
  `Upstream: ${instance.upstreamUrl}`
5675
- ].join("\n");
6191
+ ];
6192
+ if (instance.publicUrl !== void 0) rows.push(`Public: ${instance.publicUrl}`);
6193
+ if (instance.activeTaskId !== void 0) {
6194
+ rows.push(`Active: ${instance.activeTaskId}`);
6195
+ }
6196
+ return rows.join("\n");
5676
6197
  }
5677
6198
 
5678
6199
  // src/index.ts
@@ -5697,7 +6218,7 @@ function setExitCode(dependencies, code) {
5697
6218
  }
5698
6219
  }
5699
6220
  function createCli(dependencies = {}) {
5700
- const program = new Command().name("visual").description("Visual Remote Dev Bridge").version("0.3.0");
6221
+ const program = new Command().name("visual").description("Visual Remote Dev Bridge").version("0.3.1");
5701
6222
  program.command("init").description("Configure Visual Remote for the current Vite or Next.js project").action(async () => {
5702
6223
  const result = await initializeVisualDev(dependencies);
5703
6224
  output(dependencies, formatInitResult(result));
@@ -5749,7 +6270,7 @@ async function main(argv = process.argv, dependencies = {}) {
5749
6270
  }
5750
6271
  function isDirectEntry(entryPath2) {
5751
6272
  try {
5752
- return pathToFileURL(realpathSync(resolve8(entryPath2))).href === import.meta.url;
6273
+ return pathToFileURL(realpathSync2(resolve8(entryPath2))).href === import.meta.url;
5753
6274
  } catch {
5754
6275
  return false;
5755
6276
  }