visual-remote 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 {
@@ -958,131 +971,124 @@ var AgentCanceledError = class extends Error {
958
971
  }
959
972
  };
960
973
 
961
- // ../../packages/bridge-core/src/agents/codex-event-parser.ts
974
+ // ../../packages/bridge-core/src/agents/claude-event-parser.ts
962
975
  function asRecord(value) {
963
976
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
964
977
  }
965
978
  function asText(value) {
966
979
  return typeof value === "string" && value.length > 0 ? value : void 0;
967
980
  }
968
- function sessionId(record) {
969
- const thread = asRecord(record.thread);
970
- return asText(record.thread_id) ?? asText(record.threadId) ?? asText(record.session_id) ?? asText(record.sessionId) ?? (thread ? asText(thread.id) : void 0);
971
- }
972
- function itemFiles(item) {
973
- const changes = Array.isArray(item.changes) ? item.changes : [];
974
- const files = changes.flatMap((change) => {
975
- const record = asRecord(change);
976
- if (!record) return [];
977
- return [asText(record.path) ?? asText(record.file_path) ?? asText(record.filePath)].filter(
978
- (path) => path !== void 0
979
- );
980
- });
981
- const direct = asText(item.path) ?? asText(item.file_path) ?? asText(item.filePath);
982
- if (direct) files.push(direct);
983
- return [...new Set(files)];
984
- }
985
- function parseCodexJsonLine(line) {
986
- const trimmed = line.trim();
987
- if (!trimmed) return [];
988
- let value;
989
- try {
990
- value = JSON.parse(trimmed);
991
- } catch {
992
- return [{ type: "warning", text: trimmed }];
993
- }
994
- const record = asRecord(value);
995
- if (!record) return [{ type: "message", text: trimmed }];
996
- const type = asText(record.type) ?? "unknown";
997
- const events = [];
998
- const foundSession = sessionId(record);
999
- if (foundSession) events.push({ type: "session", sessionId: foundSession });
1000
- if (type === "thread.started" || type === "thread.created") return events;
1001
- if (type === "turn.started") return [...events, { type: "phase", name: "turn.started" }];
1002
- if (type === "turn.completed") {
1003
- const result = asRecord(record.result);
1004
- const summary = asText(record.summary) ?? (result ? asText(result.summary) : void 0);
1005
- return [...events, summary ? { type: "complete", summary } : { type: "complete" }];
1006
- }
1007
- if (type === "turn.failed" || type === "error") {
1008
- const error = asRecord(record.error);
1009
- const text = asText(record.message) ?? (error ? asText(error.message) : void 0) ?? "Codex reported an error";
1010
- return [...events, { type: "error", text }];
1011
- }
1012
- const item = asRecord(record.item);
1013
- if (type === "item.started" && item) {
1014
- 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 };
1017
- return [...events, start];
1018
- }
1019
- if (type === "item.completed" && item) {
1020
- const itemType = asText(item.type) ?? "item";
1021
- if (itemType === "agent_message") {
1022
- const text = asText(item.text) ?? asText(item.message);
1023
- return text ? [...events, { type: "message", text }] : events;
981
+ function asNumber(value) {
982
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
983
+ }
984
+ function contentBlocks(record) {
985
+ const message = asRecord(record.message);
986
+ return Array.isArray(message?.content) ? message.content.flatMap((block) => {
987
+ const parsed = asRecord(block);
988
+ return parsed === void 0 ? [] : [parsed];
989
+ }) : [];
990
+ }
991
+ function toolSummary(name, input) {
992
+ return asText(input.description) ?? asText(input.command) ?? asText(input.file_path) ?? asText(input.path) ?? (name === "Bash" ? "Run command" : void 0);
993
+ }
994
+ function filePath(input) {
995
+ return asText(input.file_path) ?? asText(input.path) ?? asText(input.notebook_path);
996
+ }
997
+ var ClaudeEventParser = class {
998
+ #defaultCwd;
999
+ #tools = /* @__PURE__ */ new Map();
1000
+ #sessionId;
1001
+ constructor(defaultCwd = "") {
1002
+ this.#defaultCwd = defaultCwd;
1003
+ }
1004
+ parse(line) {
1005
+ const trimmed = line.trim();
1006
+ if (!trimmed) return [];
1007
+ let value;
1008
+ try {
1009
+ value = JSON.parse(trimmed);
1010
+ } catch {
1011
+ return [{ type: "warning", text: trimmed }];
1012
+ }
1013
+ const record = asRecord(value);
1014
+ if (!record) return [{ type: "message", text: trimmed }];
1015
+ const events = [];
1016
+ const foundSession = asText(record.session_id) ?? asText(record.sessionId);
1017
+ if (foundSession !== void 0 && foundSession !== this.#sessionId) {
1018
+ this.#sessionId = foundSession;
1019
+ events.push({ type: "session", sessionId: foundSession });
1020
+ }
1021
+ const type = asText(record.type) ?? "unknown";
1022
+ if (type === "system") {
1023
+ const subtype = asText(record.subtype);
1024
+ if (subtype) events.push({ type: "phase", name: subtype });
1025
+ return events;
1026
+ }
1027
+ if (type === "assistant") {
1028
+ for (const block of contentBlocks(record)) {
1029
+ if (block.type === "text") {
1030
+ const text = asText(block.text);
1031
+ if (text) events.push({ type: "message", text });
1032
+ continue;
1033
+ }
1034
+ if (block.type !== "tool_use") continue;
1035
+ const name = asText(block.name) ?? "tool";
1036
+ const id = asText(block.id);
1037
+ const input = asRecord(block.input) ?? {};
1038
+ if (id) this.#tools.set(id, name);
1039
+ const summary = toolSummary(name, input);
1040
+ events.push(
1041
+ summary ? { type: "tool_start", name, summary } : { type: "tool_start", name }
1042
+ );
1043
+ const command = name === "Bash" ? asText(input.command) : void 0;
1044
+ if (command) {
1045
+ events.push({ type: "command", command, cwd: this.#defaultCwd });
1046
+ }
1047
+ const path = filePath(input);
1048
+ if (path && ["Edit", "Write", "NotebookEdit"].includes(name)) {
1049
+ events.push({ type: "file_hint", path });
1050
+ }
1051
+ }
1052
+ return events;
1053
+ }
1054
+ if (type === "user") {
1055
+ for (const block of contentBlocks(record)) {
1056
+ if (block.type !== "tool_result") continue;
1057
+ const id = asText(block.tool_use_id);
1058
+ const name = (id ? this.#tools.get(id) : void 0) ?? "tool";
1059
+ if (id) this.#tools.delete(id);
1060
+ events.push({ type: "tool_end", name, ok: block.is_error !== true });
1061
+ }
1062
+ return events;
1024
1063
  }
1025
- if (itemType === "command_execution") {
1026
- const command = asText(item.command);
1027
- if (command) {
1064
+ if (type === "result") {
1065
+ const usage = asRecord(record.usage);
1066
+ if (usage) {
1067
+ const cachedInputTokens = asNumber(usage.cache_read_input_tokens);
1028
1068
  events.push({
1029
- type: "command",
1030
- command,
1031
- cwd: asText(item.cwd) ?? ""
1069
+ type: "usage",
1070
+ inputTokens: asNumber(usage.input_tokens) + asNumber(usage.cache_creation_input_tokens) + cachedInputTokens,
1071
+ outputTokens: asNumber(usage.output_tokens),
1072
+ ...cachedInputTokens > 0 ? { cachedInputTokens } : {}
1032
1073
  });
1033
1074
  }
1075
+ const result = asText(record.result);
1076
+ const subtype = asText(record.subtype);
1077
+ if (record.is_error === true || subtype?.startsWith("error") === true) {
1078
+ events.push({ type: "error", text: result ?? "Claude reported an error" });
1079
+ } else {
1080
+ events.push(result ? { type: "complete", summary: result } : { type: "complete" });
1081
+ }
1082
+ return events;
1034
1083
  }
1035
- for (const path of itemFiles(item)) events.push({ type: "file_hint", path });
1036
- 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 });
1038
1084
  return events;
1039
1085
  }
1040
- const message = asText(record.message);
1041
- if (message) events.push({ type: "message", text: message });
1042
- return events;
1043
- }
1044
-
1045
- // ../../packages/bridge-core/src/agents/codex-adapter.ts
1046
- import { spawn as spawn2 } from "node:child_process";
1047
-
1048
- // ../../packages/bridge-core/src/agents/async-queue.ts
1049
- var AsyncQueue = class {
1050
- #values = [];
1051
- #waiters = [];
1052
- #ended = false;
1053
- #error;
1054
- push(value) {
1055
- if (this.#ended) return;
1056
- const waiter = this.#waiters.shift();
1057
- if (waiter) waiter.resolve({ value, done: false });
1058
- else this.#values.push(value);
1059
- }
1060
- end(error) {
1061
- if (this.#ended) return;
1062
- this.#ended = true;
1063
- this.#error = error;
1064
- for (const waiter of this.#waiters.splice(0)) {
1065
- if (error !== void 0) waiter.reject(error);
1066
- else waiter.resolve({ value: void 0, done: true });
1067
- }
1068
- }
1069
- [Symbol.asyncIterator]() {
1070
- return {
1071
- next: async () => {
1072
- const value = this.#values.shift();
1073
- if (value !== void 0) return { value, done: false };
1074
- if (this.#ended) {
1075
- if (this.#error !== void 0) throw this.#error;
1076
- return { value: void 0, done: true };
1077
- }
1078
- return await new Promise((resolve9, reject) => {
1079
- this.#waiters.push({ resolve: resolve9, reject });
1080
- });
1081
- }
1082
- };
1083
- }
1084
1086
  };
1085
1087
 
1088
+ // ../../packages/bridge-core/src/agents/claude-adapter.ts
1089
+ import { execFile, spawn as spawn2 } from "node:child_process";
1090
+ import { promisify } from "node:util";
1091
+
1086
1092
  // ../../packages/bridge-core/src/runtime/managed-process.ts
1087
1093
  import { spawn } from "node:child_process";
1088
1094
  function replacePortPlaceholder(value, port) {
@@ -1184,65 +1190,527 @@ async function startManagedProcess(options) {
1184
1190
  if (executable === void 0) {
1185
1191
  throw new Error("Managed dev command is empty");
1186
1192
  }
1187
- const detached = process.platform !== "win32";
1188
- const child = spawn(
1189
- replacePortPlaceholder(executable, options.upstreamPort),
1190
- rawArguments.map((argument) => replacePortPlaceholder(argument, options.upstreamPort)),
1191
- {
1192
- cwd: options.cwd,
1193
- env: {
1194
- ...options.environment ?? process.env,
1195
- HOST: "0.0.0.0",
1196
- PORT: String(options.upstreamPort)
1197
- },
1198
- detached,
1199
- stdio: [
1200
- "inherit",
1201
- options.stdout === void 0 ? "inherit" : "pipe",
1202
- options.stderr === void 0 ? "inherit" : "pipe"
1203
- ],
1204
- windowsHide: true
1193
+ const detached = process.platform !== "win32";
1194
+ const child = spawn(
1195
+ replacePortPlaceholder(executable, options.upstreamPort),
1196
+ rawArguments.map((argument) => replacePortPlaceholder(argument, options.upstreamPort)),
1197
+ {
1198
+ cwd: options.cwd,
1199
+ env: {
1200
+ ...options.environment ?? process.env,
1201
+ HOST: "0.0.0.0",
1202
+ PORT: String(options.upstreamPort)
1203
+ },
1204
+ detached,
1205
+ stdio: [
1206
+ "inherit",
1207
+ options.stdout === void 0 ? "inherit" : "pipe",
1208
+ options.stderr === void 0 ? "inherit" : "pipe"
1209
+ ],
1210
+ windowsHide: true
1211
+ }
1212
+ );
1213
+ const removeEmergencyExitHook = installEmergencyChildExitHook(child);
1214
+ const exit = new Promise(
1215
+ (resolve9) => {
1216
+ child.once("exit", (code, signal) => {
1217
+ resolve9({ code, signal });
1218
+ });
1219
+ }
1220
+ );
1221
+ if (child.stdout !== null && options.stdout !== void 0) {
1222
+ child.stdout.pipe(options.stdout, { end: false });
1223
+ }
1224
+ if (child.stderr !== null && options.stderr !== void 0) {
1225
+ child.stderr.pipe(options.stderr, { end: false });
1226
+ }
1227
+ try {
1228
+ await new Promise((resolve9, reject) => {
1229
+ child.once("spawn", resolve9);
1230
+ child.once("error", reject);
1231
+ });
1232
+ } catch (error) {
1233
+ removeEmergencyExitHook();
1234
+ throw error;
1235
+ }
1236
+ let stopPromise;
1237
+ return {
1238
+ child,
1239
+ exit,
1240
+ stop() {
1241
+ stopPromise ??= terminateChildProcessTree(
1242
+ child,
1243
+ options.killGraceMs
1244
+ ).finally(removeEmergencyExitHook);
1245
+ return stopPromise;
1246
+ }
1247
+ };
1248
+ }
1249
+
1250
+ // ../../packages/bridge-core/src/agents/async-queue.ts
1251
+ var AsyncQueue = class {
1252
+ #values = [];
1253
+ #waiters = [];
1254
+ #ended = false;
1255
+ #error;
1256
+ push(value) {
1257
+ if (this.#ended) return;
1258
+ const waiter = this.#waiters.shift();
1259
+ if (waiter) waiter.resolve({ value, done: false });
1260
+ else this.#values.push(value);
1261
+ }
1262
+ end(error) {
1263
+ if (this.#ended) return;
1264
+ this.#ended = true;
1265
+ this.#error = error;
1266
+ for (const waiter of this.#waiters.splice(0)) {
1267
+ if (error !== void 0) waiter.reject(error);
1268
+ else waiter.resolve({ value: void 0, done: true });
1269
+ }
1270
+ }
1271
+ [Symbol.asyncIterator]() {
1272
+ return {
1273
+ next: async () => {
1274
+ const value = this.#values.shift();
1275
+ if (value !== void 0) return { value, done: false };
1276
+ if (this.#ended) {
1277
+ if (this.#error !== void 0) throw this.#error;
1278
+ return { value: void 0, done: true };
1279
+ }
1280
+ return await new Promise((resolve9, reject) => {
1281
+ this.#waiters.push({ resolve: resolve9, reject });
1282
+ });
1283
+ }
1284
+ };
1285
+ }
1286
+ };
1287
+
1288
+ // ../../packages/bridge-core/src/agents/claude-adapter.ts
1289
+ var execFileAsync = promisify(execFile);
1290
+ var INHERITED_ENVIRONMENT = [
1291
+ "PATH",
1292
+ "HOME",
1293
+ "USER",
1294
+ "LOGNAME",
1295
+ "SHELL",
1296
+ "LANG",
1297
+ "LC_ALL",
1298
+ "TERM",
1299
+ "TMPDIR",
1300
+ "XDG_CONFIG_HOME",
1301
+ "XDG_DATA_HOME",
1302
+ "XDG_STATE_HOME",
1303
+ "ANTHROPIC_API_KEY",
1304
+ "CLAUDE_CODE_OAUTH_TOKEN",
1305
+ "HTTPS_PROXY",
1306
+ "HTTP_PROXY",
1307
+ "NO_PROXY",
1308
+ "USERPROFILE",
1309
+ "APPDATA",
1310
+ "LOCALAPPDATA",
1311
+ "SystemRoot",
1312
+ "COMSPEC",
1313
+ "PATHEXT"
1314
+ ];
1315
+ var EMPTY_MCP_CONFIG = JSON.stringify({ mcpServers: {} });
1316
+ var CLAUDE_SETTINGS = JSON.stringify({
1317
+ permissions: {
1318
+ disableBypassPermissionsMode: "disable",
1319
+ deny: [
1320
+ "Read(./.env)",
1321
+ "Read(./.env.*)",
1322
+ "Read(./**/*.pem)",
1323
+ "Read(./**/*.key)",
1324
+ "Edit(./.git/**)",
1325
+ "Edit(./.visualdev/runtime/**)",
1326
+ "Edit(./node_modules/**)"
1327
+ ]
1328
+ },
1329
+ sandbox: {
1330
+ enabled: true,
1331
+ autoAllowBashIfSandboxed: true,
1332
+ allowUnsandboxedCommands: false,
1333
+ network: { strictAllowlist: true }
1334
+ }
1335
+ });
1336
+ function processEnv(overrides) {
1337
+ const environment = {};
1338
+ for (const key of INHERITED_ENVIRONMENT) {
1339
+ const value = process.env[key];
1340
+ if (value !== void 0) environment[key] = value;
1341
+ }
1342
+ return { ...environment, ...overrides };
1343
+ }
1344
+ function splitLines(chunk, previous, onLine) {
1345
+ const combined = previous + chunk.toString();
1346
+ const lines = combined.split(/\r?\n/);
1347
+ const remainder = lines.pop() ?? "";
1348
+ for (const line of lines) onLine(line);
1349
+ return remainder;
1350
+ }
1351
+ var ClaudeAdapter = class {
1352
+ id = "claude";
1353
+ #executable;
1354
+ #killGraceMs;
1355
+ #model;
1356
+ #reasoningEffort;
1357
+ #rtkExecutable;
1358
+ #rtkVersion;
1359
+ constructor(options = {}) {
1360
+ this.#executable = options.executable ?? "claude";
1361
+ this.#killGraceMs = options.killGraceMs ?? 2e3;
1362
+ this.#model = options.model;
1363
+ this.#reasoningEffort = options.reasoningEffort;
1364
+ this.#rtkExecutable = options.rtkExecutable ?? "rtk";
1365
+ }
1366
+ #probeRtk(environment) {
1367
+ if (this.#rtkExecutable === false) return Promise.resolve(void 0);
1368
+ this.#rtkVersion ??= execFileAsync(this.#rtkExecutable, ["--version"], {
1369
+ encoding: "utf8",
1370
+ env: environment,
1371
+ timeout: 1e3,
1372
+ windowsHide: true,
1373
+ maxBuffer: 16 * 1024
1374
+ }).then(({ stdout }) => stdout.trim().split(/\r?\n/, 1)[0] || void 0).catch(() => void 0);
1375
+ return this.#rtkVersion;
1376
+ }
1377
+ async #runtimePrompt(input, environment) {
1378
+ if (this.#rtkExecutable === false) return input.prompt;
1379
+ const guidance = await this.#probeRtk(environment).then(
1380
+ (version) => version ? `RTK command proxy:
1381
+ - ${version} is installed and available in this runtime.
1382
+ - Prefix shell commands with RTK by default (for example: rtk git status, rtk rg <pattern>, rtk read <file>, rtk npm test).
1383
+ - 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:
1384
+ - RTK was not detected in this runtime. Use native repository commands directly and do not spend time searching for RTK.`
1385
+ );
1386
+ return `${input.prompt.trimEnd()}
1387
+
1388
+ ${guidance}
1389
+ `;
1390
+ }
1391
+ #baseArgs() {
1392
+ return [
1393
+ "-p",
1394
+ "--output-format",
1395
+ "stream-json",
1396
+ "--verbose",
1397
+ "--permission-mode",
1398
+ "acceptEdits",
1399
+ "--strict-mcp-config",
1400
+ "--mcp-config",
1401
+ EMPTY_MCP_CONFIG,
1402
+ "--no-chrome",
1403
+ "--tools",
1404
+ "Read,Glob,Grep,Edit,Write,Bash",
1405
+ "--settings",
1406
+ CLAUDE_SETTINGS,
1407
+ ...this.#model === void 0 ? [] : ["--model", this.#model],
1408
+ ...this.#reasoningEffort === void 0 ? [] : ["--effort", this.#reasoningEffort]
1409
+ ];
1410
+ }
1411
+ async probe() {
1412
+ return await new Promise((resolve9) => {
1413
+ const child = spawn2(this.#executable, ["--version"], {
1414
+ stdio: ["ignore", "pipe", "ignore"],
1415
+ shell: false
1416
+ });
1417
+ let output2 = "";
1418
+ child.stdout?.on("data", (chunk) => {
1419
+ output2 += chunk.toString();
1420
+ });
1421
+ child.once("error", () => {
1422
+ resolve9({ available: false, supportsResume: true, structuredOutput: true });
1423
+ });
1424
+ child.once("close", (code) => {
1425
+ const match = output2.match(/(\d+\.\d+\.\d+(?:[-+][^\s]+)?)/);
1426
+ const capabilities = {
1427
+ available: code === 0,
1428
+ supportsResume: true,
1429
+ structuredOutput: true
1430
+ };
1431
+ if (match?.[1]) capabilities.version = match[1];
1432
+ resolve9(capabilities);
1433
+ });
1434
+ });
1435
+ }
1436
+ async *run(input, signal) {
1437
+ yield* this.#execute(input, signal, this.#baseArgs());
1438
+ }
1439
+ async *resume(input, signal) {
1440
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(input.sessionId)) {
1441
+ throw new Error("Refusing to resume an invalid Claude session id");
1442
+ }
1443
+ yield* this.#execute(
1444
+ input,
1445
+ signal,
1446
+ [...this.#baseArgs(), "--resume", input.sessionId]
1447
+ );
1448
+ }
1449
+ async *#execute(input, signal, args) {
1450
+ const queue = new AsyncQueue();
1451
+ const environment = processEnv(input.environment);
1452
+ const prompt = await this.#runtimePrompt(input, environment);
1453
+ const parser = new ClaudeEventParser(input.workspaceRoot);
1454
+ const child = spawn2(this.#executable, args, {
1455
+ cwd: input.workspaceRoot,
1456
+ env: environment,
1457
+ detached: process.platform !== "win32",
1458
+ shell: false,
1459
+ stdio: ["pipe", "pipe", "pipe"]
1460
+ });
1461
+ const removeEmergencyExitHook = installEmergencyChildExitHook(child);
1462
+ let stdoutRemainder = "";
1463
+ let stderrRemainder = "";
1464
+ let timedOut = false;
1465
+ let aborted = signal.aborted;
1466
+ let termination;
1467
+ const requestTermination = () => {
1468
+ termination ??= terminateChildProcessTree(
1469
+ child,
1470
+ this.#killGraceMs
1471
+ ).finally(removeEmergencyExitHook);
1472
+ return termination;
1473
+ };
1474
+ const timeout = setTimeout(() => {
1475
+ timedOut = true;
1476
+ void requestTermination();
1477
+ }, Math.max(1, input.maxRunMs));
1478
+ timeout.unref();
1479
+ const abort = () => {
1480
+ aborted = true;
1481
+ void requestTermination();
1482
+ };
1483
+ signal.addEventListener("abort", abort, { once: true });
1484
+ if (signal.aborted) abort();
1485
+ child.stdout.on("data", (chunk) => {
1486
+ stdoutRemainder = splitLines(chunk, stdoutRemainder, (line) => {
1487
+ for (const event of parser.parse(line)) queue.push(event);
1488
+ });
1489
+ });
1490
+ child.stderr.on("data", (chunk) => {
1491
+ stderrRemainder = splitLines(chunk, stderrRemainder, (line) => {
1492
+ if (line.trim()) queue.push({ type: "warning", text: line });
1493
+ });
1494
+ });
1495
+ child.once("error", (error) => queue.end(error));
1496
+ child.once("close", (code, closeSignal) => {
1497
+ clearTimeout(timeout);
1498
+ signal.removeEventListener("abort", abort);
1499
+ void (async () => {
1500
+ await requestTermination();
1501
+ if (stdoutRemainder.trim()) {
1502
+ for (const event of parser.parse(stdoutRemainder)) queue.push(event);
1503
+ }
1504
+ if (stderrRemainder.trim()) queue.push({ type: "warning", text: stderrRemainder });
1505
+ if (timedOut) queue.end(new AgentTimeoutError());
1506
+ else if (aborted) queue.end(new AgentCanceledError());
1507
+ else if (code !== 0) {
1508
+ queue.end(
1509
+ new AgentProcessError(
1510
+ `Claude exited with code ${String(code)}`,
1511
+ code,
1512
+ closeSignal
1513
+ )
1514
+ );
1515
+ } else {
1516
+ queue.end();
1517
+ }
1518
+ })().catch((error) => queue.end(error));
1519
+ });
1520
+ child.stdin.on("error", (error) => {
1521
+ if (error.code !== "EPIPE") queue.end(error);
1522
+ });
1523
+ child.stdin.end(prompt);
1524
+ try {
1525
+ for await (const event of queue) yield event;
1526
+ } finally {
1527
+ clearTimeout(timeout);
1528
+ signal.removeEventListener("abort", abort);
1529
+ try {
1530
+ if (child.exitCode === null && child.signalCode === null) {
1531
+ await requestTermination();
1532
+ } else if (termination) {
1533
+ await termination;
1534
+ }
1535
+ } finally {
1536
+ removeEmergencyExitHook();
1537
+ }
1538
+ }
1539
+ }
1540
+ };
1541
+
1542
+ // ../../packages/bridge-core/src/agents/codex-event-parser.ts
1543
+ function asRecord2(value) {
1544
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1545
+ }
1546
+ function asText2(value) {
1547
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1548
+ }
1549
+ function sessionId(record) {
1550
+ const thread = asRecord2(record.thread);
1551
+ return asText2(record.thread_id) ?? asText2(record.threadId) ?? asText2(record.session_id) ?? asText2(record.sessionId) ?? (thread ? asText2(thread.id) : void 0);
1552
+ }
1553
+ function itemFiles(item) {
1554
+ const changes = Array.isArray(item.changes) ? item.changes : [];
1555
+ const files = changes.flatMap((change) => {
1556
+ const record = asRecord2(change);
1557
+ if (!record) return [];
1558
+ return [asText2(record.path) ?? asText2(record.file_path) ?? asText2(record.filePath)].filter(
1559
+ (path) => path !== void 0
1560
+ );
1561
+ });
1562
+ const direct = asText2(item.path) ?? asText2(item.file_path) ?? asText2(item.filePath);
1563
+ if (direct) files.push(direct);
1564
+ return [...new Set(files)];
1565
+ }
1566
+ function stringArray(value) {
1567
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : void 0;
1568
+ }
1569
+ function formatArgv(argv) {
1570
+ return argv.map((argument) => /^[A-Za-z0-9_./:=@%+,-]+$/u.test(argument) ? argument : JSON.stringify(argument)).join(" ");
1571
+ }
1572
+ function isDirectExecItem(item) {
1573
+ return item.type === "mcp_tool_call" && item.server === "visual_remote_exec" && item.tool === "run_readonly";
1574
+ }
1575
+ function directExecSummary(item) {
1576
+ const arguments_ = asRecord2(item.arguments);
1577
+ const commands = Array.isArray(arguments_?.commands) ? arguments_.commands : [];
1578
+ const summaries = commands.flatMap((candidate) => {
1579
+ const command = asRecord2(candidate);
1580
+ const argv = stringArray(command?.argv);
1581
+ return argv === void 0 ? [] : [formatArgv(argv)];
1582
+ });
1583
+ return summaries.length === 0 ? void 0 : summaries.join(" \xB7 ");
1584
+ }
1585
+ function directExecResults(item, defaultCwd) {
1586
+ const result = asRecord2(item.result);
1587
+ const structured = asRecord2(result?.structured_content ?? result?.structuredContent);
1588
+ const results = Array.isArray(structured?.results) ? structured.results : [];
1589
+ return results.flatMap((candidate) => {
1590
+ const command = asRecord2(candidate);
1591
+ const argv = stringArray(command?.argv);
1592
+ if (argv === void 0) return [];
1593
+ const exitCode = typeof command?.exitCode === "number" ? command.exitCode : void 0;
1594
+ const durationMs = typeof command?.durationMs === "number" ? command.durationMs : void 0;
1595
+ return [{
1596
+ command: formatArgv(argv),
1597
+ cwd: asText2(command?.cwd) ?? defaultCwd,
1598
+ ok: exitCode === 0,
1599
+ ...exitCode === void 0 ? {} : { exitCode },
1600
+ ...durationMs === void 0 ? {} : { durationMs },
1601
+ ...typeof command?.usedRtk === "boolean" ? { usedRtk: command.usedRtk } : {},
1602
+ ...typeof command?.timedOut === "boolean" ? { timedOut: command.timedOut } : {},
1603
+ ...typeof command?.truncated === "boolean" ? { truncated: command.truncated } : {}
1604
+ }];
1605
+ });
1606
+ }
1607
+ function normalizedUsage(record) {
1608
+ const result = asRecord2(record.result);
1609
+ const usage = asRecord2(record.usage) ?? (result ? asRecord2(result.usage) : void 0);
1610
+ if (usage === void 0) return void 0;
1611
+ const inputTokens = usage.input_tokens ?? usage.inputTokens;
1612
+ const outputTokens = usage.output_tokens ?? usage.outputTokens;
1613
+ const cachedInputTokens = usage.cached_input_tokens ?? usage.cachedInputTokens;
1614
+ if (typeof inputTokens !== "number" || typeof outputTokens !== "number") {
1615
+ return void 0;
1616
+ }
1617
+ return {
1618
+ type: "usage",
1619
+ inputTokens,
1620
+ outputTokens,
1621
+ ...typeof cachedInputTokens === "number" ? { cachedInputTokens } : {}
1622
+ };
1623
+ }
1624
+ function parseCodexJsonLine(line, defaultCwd = "") {
1625
+ const trimmed = line.trim();
1626
+ if (!trimmed) return [];
1627
+ let value;
1628
+ try {
1629
+ value = JSON.parse(trimmed);
1630
+ } catch {
1631
+ return [{ type: "warning", text: trimmed }];
1632
+ }
1633
+ const record = asRecord2(value);
1634
+ if (!record) return [{ type: "message", text: trimmed }];
1635
+ const type = asText2(record.type) ?? "unknown";
1636
+ const events = [];
1637
+ const foundSession = sessionId(record);
1638
+ if (foundSession) events.push({ type: "session", sessionId: foundSession });
1639
+ if (type === "thread.started" || type === "thread.created") return events;
1640
+ if (type === "turn.started") return [...events, { type: "phase", name: "turn.started" }];
1641
+ if (type === "turn.completed") {
1642
+ const result = asRecord2(record.result);
1643
+ const summary = asText2(record.summary) ?? (result ? asText2(result.summary) : void 0);
1644
+ const usage = normalizedUsage(record);
1645
+ return [
1646
+ ...events,
1647
+ ...usage === void 0 ? [] : [usage],
1648
+ summary ? { type: "complete", summary } : { type: "complete" }
1649
+ ];
1650
+ }
1651
+ if (type === "turn.failed" || type === "error") {
1652
+ const error = asRecord2(record.error);
1653
+ const text = asText2(record.message) ?? (error ? asText2(error.message) : void 0) ?? "Codex reported an error";
1654
+ return [...events, { type: "error", text }];
1655
+ }
1656
+ const item = asRecord2(record.item);
1657
+ if (type === "item.started" && item) {
1658
+ const itemType = asText2(item.type) ?? "item";
1659
+ const directExec = isDirectExecItem(item);
1660
+ const summary = directExec ? directExecSummary(item) : asText2(item.command) ?? asText2(item.text);
1661
+ const start = summary ? { type: "tool_start", name: directExec ? "direct_exec" : itemType, summary } : { type: "tool_start", name: directExec ? "direct_exec" : itemType };
1662
+ return [...events, start];
1663
+ }
1664
+ if (type === "item.completed" && item) {
1665
+ const itemType = asText2(item.type) ?? "item";
1666
+ if (itemType === "agent_message") {
1667
+ const text = asText2(item.text) ?? asText2(item.message);
1668
+ return text ? [...events, { type: "message", text }] : events;
1205
1669
  }
1206
- );
1207
- const removeEmergencyExitHook = installEmergencyChildExitHook(child);
1208
- const exit = new Promise(
1209
- (resolve9) => {
1210
- child.once("exit", (code, signal) => {
1211
- resolve9({ code, signal });
1670
+ if (itemType === "command_execution") {
1671
+ const command = asText2(item.command);
1672
+ if (command) {
1673
+ events.push({
1674
+ type: "command",
1675
+ command,
1676
+ cwd: asText2(item.cwd) ?? defaultCwd
1677
+ });
1678
+ }
1679
+ }
1680
+ const directResults = isDirectExecItem(item) ? directExecResults(item, defaultCwd) : [];
1681
+ for (const result of directResults) {
1682
+ events.push({
1683
+ type: "command",
1684
+ command: result.command,
1685
+ cwd: result.cwd,
1686
+ ...result.exitCode === void 0 ? {} : { exitCode: result.exitCode },
1687
+ ...result.durationMs === void 0 ? {} : { durationMs: result.durationMs },
1688
+ ...result.usedRtk === void 0 ? {} : { usedRtk: result.usedRtk },
1689
+ ...result.timedOut === void 0 ? {} : { timedOut: result.timedOut },
1690
+ ...result.truncated === void 0 ? {} : { truncated: result.truncated }
1212
1691
  });
1213
1692
  }
1214
- );
1215
- if (child.stdout !== null && options.stdout !== void 0) {
1216
- child.stdout.pipe(options.stdout, { end: false });
1217
- }
1218
- if (child.stderr !== null && options.stderr !== void 0) {
1219
- child.stderr.pipe(options.stderr, { end: false });
1220
- }
1221
- try {
1222
- await new Promise((resolve9, reject) => {
1223
- child.once("spawn", resolve9);
1224
- child.once("error", reject);
1693
+ for (const path of itemFiles(item)) events.push({ type: "file_hint", path });
1694
+ const exitCode = typeof item.exit_code === "number" ? item.exit_code : void 0;
1695
+ events.push({
1696
+ type: "tool_end",
1697
+ name: isDirectExecItem(item) ? "direct_exec" : itemType,
1698
+ ok: directResults.length > 0 ? directResults.every((result) => result.ok) : exitCode === void 0 || exitCode === 0
1225
1699
  });
1226
- } catch (error) {
1227
- removeEmergencyExitHook();
1228
- throw error;
1700
+ return events;
1229
1701
  }
1230
- let stopPromise;
1231
- return {
1232
- child,
1233
- exit,
1234
- stop() {
1235
- stopPromise ??= terminateChildProcessTree(
1236
- child,
1237
- options.killGraceMs
1238
- ).finally(removeEmergencyExitHook);
1239
- return stopPromise;
1240
- }
1241
- };
1702
+ const message = asText2(record.message);
1703
+ if (message) events.push({ type: "message", text: message });
1704
+ return events;
1242
1705
  }
1243
1706
 
1244
1707
  // ../../packages/bridge-core/src/agents/codex-adapter.ts
1245
- var INHERITED_ENVIRONMENT = [
1708
+ import { execFile as execFile2, spawn as spawn3 } from "node:child_process";
1709
+ import { existsSync } from "node:fs";
1710
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1711
+ import { promisify as promisify2 } from "node:util";
1712
+ var execFileAsync2 = promisify2(execFile2);
1713
+ var INHERITED_ENVIRONMENT2 = [
1246
1714
  "PATH",
1247
1715
  "HOME",
1248
1716
  "USER",
@@ -1267,32 +1735,104 @@ var INHERITED_ENVIRONMENT = [
1267
1735
  "COMSPEC",
1268
1736
  "PATHEXT"
1269
1737
  ];
1270
- function processEnv(overrides) {
1738
+ function processEnv2(overrides) {
1271
1739
  const environment = {};
1272
- for (const key of INHERITED_ENVIRONMENT) {
1740
+ for (const key of INHERITED_ENVIRONMENT2) {
1273
1741
  const value = process.env[key];
1274
1742
  if (value !== void 0) environment[key] = value;
1275
1743
  }
1276
1744
  return { ...environment, ...overrides };
1277
1745
  }
1278
- function splitLines(chunk, previous, onLine) {
1746
+ function splitLines2(chunk, previous, onLine) {
1279
1747
  const combined = previous + chunk.toString();
1280
1748
  const lines = combined.split(/\r?\n/);
1281
1749
  const remainder = lines.pop() ?? "";
1282
1750
  for (const line of lines) onLine(line);
1283
1751
  return remainder;
1284
1752
  }
1753
+ function defaultDirectExecMcpScript() {
1754
+ const candidates = [
1755
+ fileURLToPath2(new URL("./direct-exec-mcp.js", import.meta.url)),
1756
+ fileURLToPath2(
1757
+ new URL("../../../../apps/cli/dist/direct-exec-mcp.js", import.meta.url)
1758
+ )
1759
+ ];
1760
+ return candidates.find((candidate) => existsSync(candidate));
1761
+ }
1285
1762
  var CodexAdapter = class {
1286
1763
  id = "codex";
1287
1764
  #executable;
1288
1765
  #killGraceMs;
1766
+ #model;
1767
+ #profile;
1768
+ #reasoningEffort;
1769
+ #rtkExecutable;
1770
+ #directExecMcpScript;
1771
+ #rtkVersion;
1289
1772
  constructor(options = {}) {
1290
1773
  this.#executable = options.executable ?? "codex";
1291
1774
  this.#killGraceMs = options.killGraceMs ?? 2e3;
1775
+ this.#model = options.model;
1776
+ this.#profile = options.profile;
1777
+ this.#reasoningEffort = options.reasoningEffort;
1778
+ this.#rtkExecutable = options.rtkExecutable ?? "rtk";
1779
+ this.#directExecMcpScript = options.directExecMcpScript === false ? void 0 : options.directExecMcpScript ?? defaultDirectExecMcpScript();
1780
+ }
1781
+ #probeRtk(environment) {
1782
+ if (this.#rtkExecutable === false) return Promise.resolve(void 0);
1783
+ this.#rtkVersion ??= execFileAsync2(this.#rtkExecutable, ["--version"], {
1784
+ encoding: "utf8",
1785
+ env: environment,
1786
+ timeout: 1e3,
1787
+ windowsHide: true,
1788
+ maxBuffer: 16 * 1024
1789
+ }).then(({ stdout }) => stdout.trim().split(/\r?\n/, 1)[0] || void 0).catch(() => void 0);
1790
+ return this.#rtkVersion;
1791
+ }
1792
+ async #runtimePrompt(input, environment) {
1793
+ const commandGuidance = this.#rtkExecutable === false ? "" : await this.#probeRtk(environment).then((version) => version ? `RTK command proxy:
1794
+ - ${version} is installed and available in this runtime.
1795
+ - Prefix shell commands with RTK by default (for example: rtk git status, rtk rg <pattern>, rtk read <file>, rtk npm test).
1796
+ - 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:
1797
+ - RTK was not detected in this runtime. Use native repository commands directly and do not spend time searching for RTK.`);
1798
+ const directExecGuidance = this.#directExecMcpScript === void 0 ? "" : `Direct read-only command runner:
1799
+ - 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.
1800
+ - Send argv arrays, batch independent reads in one tool call, and keep cwd at the registered workspace unless a known subdirectory is required.
1801
+ - The tool executes without a shell and applies RTK automatically when supported.
1802
+ - 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.`;
1803
+ const guidance = [directExecGuidance, commandGuidance].filter(Boolean).join("\n\n");
1804
+ return guidance.length === 0 ? input.prompt : `${input.prompt.trimEnd()}
1805
+
1806
+ ${guidance}
1807
+ `;
1808
+ }
1809
+ #directExecConfig(input) {
1810
+ if (this.#directExecMcpScript === void 0) return [];
1811
+ const serverArgs = [
1812
+ this.#directExecMcpScript,
1813
+ "--repo-root",
1814
+ input.repoRoot,
1815
+ "--workspace-root",
1816
+ input.workspaceRoot,
1817
+ ...this.#rtkExecutable === false ? ["--no-rtk"] : ["--rtk", this.#rtkExecutable]
1818
+ ];
1819
+ return [
1820
+ "-c",
1821
+ `mcp_servers.visual_remote_exec.command=${JSON.stringify(process.execPath)}`,
1822
+ "-c",
1823
+ `mcp_servers.visual_remote_exec.args=${JSON.stringify(serverArgs)}`
1824
+ ];
1825
+ }
1826
+ #modelConfig() {
1827
+ return [
1828
+ ...this.#profile === void 0 ? [] : ["--profile", this.#profile],
1829
+ ...this.#model === void 0 ? [] : ["--model", this.#model],
1830
+ ...this.#reasoningEffort === void 0 ? [] : ["-c", `model_reasoning_effort=${JSON.stringify(this.#reasoningEffort)}`]
1831
+ ];
1292
1832
  }
1293
1833
  async probe() {
1294
1834
  return await new Promise((resolve9) => {
1295
- const child = spawn2(this.#executable, ["--version"], {
1835
+ const child = spawn3(this.#executable, ["--version"], {
1296
1836
  stdio: ["ignore", "pipe", "ignore"],
1297
1837
  shell: false
1298
1838
  });
@@ -1324,7 +1864,9 @@ var CodexAdapter = class {
1324
1864
  "-s",
1325
1865
  "workspace-write",
1326
1866
  "-C",
1327
- input.repoRoot,
1867
+ input.workspaceRoot,
1868
+ ...this.#modelConfig(),
1869
+ ...this.#directExecConfig(input),
1328
1870
  "-"
1329
1871
  ];
1330
1872
  yield* this.#execute(input, signal, args);
@@ -1341,7 +1883,9 @@ var CodexAdapter = class {
1341
1883
  "-s",
1342
1884
  "workspace-write",
1343
1885
  "-C",
1344
- input.repoRoot,
1886
+ input.workspaceRoot,
1887
+ ...this.#modelConfig(),
1888
+ ...this.#directExecConfig(input),
1345
1889
  "resume",
1346
1890
  input.sessionId,
1347
1891
  "-"
@@ -1350,9 +1894,11 @@ var CodexAdapter = class {
1350
1894
  }
1351
1895
  async *#execute(input, signal, args) {
1352
1896
  const queue = new AsyncQueue();
1353
- const child = spawn2(this.#executable, args, {
1354
- cwd: input.repoRoot,
1355
- env: processEnv(input.environment),
1897
+ const environment = processEnv2(input.environment);
1898
+ const prompt = await this.#runtimePrompt(input, environment);
1899
+ const child = spawn3(this.#executable, args, {
1900
+ cwd: input.workspaceRoot,
1901
+ env: environment,
1356
1902
  detached: process.platform !== "win32",
1357
1903
  shell: false,
1358
1904
  stdio: ["pipe", "pipe", "pipe"]
@@ -1382,12 +1928,12 @@ var CodexAdapter = class {
1382
1928
  signal.addEventListener("abort", abort, { once: true });
1383
1929
  if (signal.aborted) abort();
1384
1930
  child.stdout.on("data", (chunk) => {
1385
- stdoutRemainder = splitLines(chunk, stdoutRemainder, (line) => {
1386
- for (const event of parseCodexJsonLine(line)) queue.push(event);
1931
+ stdoutRemainder = splitLines2(chunk, stdoutRemainder, (line) => {
1932
+ for (const event of parseCodexJsonLine(line, input.workspaceRoot)) queue.push(event);
1387
1933
  });
1388
1934
  });
1389
1935
  child.stderr.on("data", (chunk) => {
1390
- stderrRemainder = splitLines(chunk, stderrRemainder, (line) => {
1936
+ stderrRemainder = splitLines2(chunk, stderrRemainder, (line) => {
1391
1937
  if (line.trim()) queue.push({ type: "warning", text: line });
1392
1938
  });
1393
1939
  });
@@ -1398,7 +1944,9 @@ var CodexAdapter = class {
1398
1944
  void (async () => {
1399
1945
  await requestTermination();
1400
1946
  if (stdoutRemainder.trim()) {
1401
- for (const event of parseCodexJsonLine(stdoutRemainder)) queue.push(event);
1947
+ for (const event of parseCodexJsonLine(stdoutRemainder, input.workspaceRoot)) {
1948
+ queue.push(event);
1949
+ }
1402
1950
  }
1403
1951
  if (stderrRemainder.trim()) queue.push({ type: "warning", text: stderrRemainder });
1404
1952
  if (timedOut) queue.end(new AgentTimeoutError());
@@ -1419,7 +1967,7 @@ var CodexAdapter = class {
1419
1967
  child.stdin.on("error", (error) => {
1420
1968
  if (error.code !== "EPIPE") queue.end(error);
1421
1969
  });
1422
- child.stdin.end(input.prompt);
1970
+ child.stdin.end(prompt);
1423
1971
  try {
1424
1972
  for await (const event of queue) yield event;
1425
1973
  } finally {
@@ -1438,6 +1986,9 @@ var CodexAdapter = class {
1438
1986
  }
1439
1987
  };
1440
1988
 
1989
+ // ../../packages/bridge-core/src/agents/direct-exec.ts
1990
+ var DEFAULT_OUTPUT_BYTES = 64 * 1024;
1991
+
1441
1992
  // ../../packages/bridge-core/src/config/loader.ts
1442
1993
  import { readFile, realpath, stat as stat2 } from "node:fs/promises";
1443
1994
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -1448,6 +1999,7 @@ import { ZodError } from "zod";
1448
1999
  import { z } from "zod";
1449
2000
  var servicePortSchema = z.number().int().min(10001).max(65535);
1450
2001
  var commandSchema = z.array(z.string().min(1)).min(1);
2002
+ var environmentVariableSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "Expected an environment variable name");
1451
2003
  var readySchema = z.object({
1452
2004
  path: z.string().startsWith("/").default("/"),
1453
2005
  timeoutMs: z.number().int().positive().default(6e4)
@@ -1476,9 +2028,38 @@ var visualDevConfigSchema = z.object({
1476
2028
  }).strict(),
1477
2029
  agent: z.object({
1478
2030
  adapter: z.enum(["codex", "claude", "opencode"]),
2031
+ model: z.string().trim().min(1).optional(),
2032
+ reasoningEffort: z.enum(["minimal", "low", "medium", "high", "xhigh", "max"]).optional(),
2033
+ profile: z.string().trim().regex(
2034
+ /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/,
2035
+ "Expected a Codex profile name"
2036
+ ).optional(),
2037
+ inheritEnv: z.array(environmentVariableSchema).default([]),
1479
2038
  maxRunMs: z.number().int().positive(),
1480
2039
  resumeMode: z.enum(["auto", "new"]).default("auto")
1481
- }).strict(),
2040
+ }).strict().superRefine((agent, context) => {
2041
+ if (agent.adapter === "claude" && agent.reasoningEffort === "minimal") {
2042
+ context.addIssue({
2043
+ code: "custom",
2044
+ path: ["reasoningEffort"],
2045
+ message: "minimal reasoning effort is only supported by the Codex adapter"
2046
+ });
2047
+ }
2048
+ if (agent.adapter === "codex" && agent.reasoningEffort === "max") {
2049
+ context.addIssue({
2050
+ code: "custom",
2051
+ path: ["reasoningEffort"],
2052
+ message: "max reasoning effort is only supported by the Claude adapter"
2053
+ });
2054
+ }
2055
+ if (agent.adapter !== "codex" && agent.profile !== void 0) {
2056
+ context.addIssue({
2057
+ code: "custom",
2058
+ path: ["profile"],
2059
+ message: "agent.profile is only supported by the Codex adapter"
2060
+ });
2061
+ }
2062
+ }),
1482
2063
  queue: z.object({
1483
2064
  maxPending: z.number().int().positive()
1484
2065
  }).strict(),
@@ -1522,6 +2103,7 @@ function createDefaultConfig(projectId) {
1522
2103
  },
1523
2104
  agent: {
1524
2105
  adapter: "codex",
2106
+ inheritEnv: [],
1525
2107
  maxRunMs: 9e5,
1526
2108
  resumeMode: "auto"
1527
2109
  },
@@ -1594,17 +2176,17 @@ function mergeConfigValues(base, override) {
1594
2176
  }
1595
2177
  return merged;
1596
2178
  }
1597
- async function readYamlMapping(filePath, required) {
2179
+ async function readYamlMapping(filePath2, required) {
1598
2180
  let source;
1599
2181
  try {
1600
- source = await readFile(filePath, "utf8");
2182
+ source = await readFile(filePath2, "utf8");
1601
2183
  } catch (error) {
1602
2184
  if (!required && typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
1603
2185
  return void 0;
1604
2186
  }
1605
- throw new VisualDevConfigError(`Unable to read config: ${filePath}`, {
2187
+ throw new VisualDevConfigError(`Unable to read config: ${filePath2}`, {
1606
2188
  cause: error,
1607
- filePath
2189
+ filePath: filePath2
1608
2190
  });
1609
2191
  }
1610
2192
  try {
@@ -1617,9 +2199,9 @@ async function readYamlMapping(filePath, required) {
1617
2199
  }
1618
2200
  return value;
1619
2201
  } catch (error) {
1620
- throw new VisualDevConfigError(`Invalid YAML in ${filePath}`, {
2202
+ throw new VisualDevConfigError(`Invalid YAML in ${filePath2}`, {
1621
2203
  cause: error,
1622
- filePath
2204
+ filePath: filePath2
1623
2205
  });
1624
2206
  }
1625
2207
  }
@@ -1758,10 +2340,10 @@ var RevertConflictError = class extends RepositorySafetyError {
1758
2340
  };
1759
2341
 
1760
2342
  // ../../packages/bridge-core/src/git/git-command.ts
1761
- import { spawn as spawn3 } from "node:child_process";
2343
+ import { spawn as spawn4 } from "node:child_process";
1762
2344
  async function runGit(args, options) {
1763
2345
  return await new Promise((resolve9, reject) => {
1764
- const child = spawn3("git", [...args], {
2346
+ const child = spawn4("git", [...args], {
1765
2347
  cwd: options.cwd,
1766
2348
  env: { ...process.env, ...options.env ?? {} },
1767
2349
  shell: false,
@@ -1867,6 +2449,20 @@ function isWithin(root, candidate) {
1867
2449
  const path = relative2(root, candidate);
1868
2450
  return path === "" || !path.startsWith(`..${sep2}`) && path !== ".." && !isAbsolute2(path);
1869
2451
  }
2452
+ function rebaseWorkspacePatterns(repoRoot, workspaceRoot, patterns) {
2453
+ const repository = resolve2(repoRoot);
2454
+ const workspace = resolve2(workspaceRoot);
2455
+ if (!isWithin(repository, workspace)) {
2456
+ throw new PathSafetyError(
2457
+ "PATH_OUTSIDE_REPOSITORY",
2458
+ workspaceRoot,
2459
+ `Workspace is outside the repository: ${workspaceRoot}`
2460
+ );
2461
+ }
2462
+ const prefix = repositoryRelative(repository, workspace);
2463
+ if (!prefix) return [...patterns];
2464
+ return patterns.map((pattern) => normalizeSlashes(`${prefix}/${pattern}`));
2465
+ }
1870
2466
  var PathPolicy = class {
1871
2467
  repoRoot;
1872
2468
  allowedPatterns;
@@ -2710,6 +3306,9 @@ var TERMINAL = /* @__PURE__ */ new Set(["accepted", "reverted", "failed", "cance
2710
3306
  function isActiveTaskStatus(status) {
2711
3307
  return status !== "queued" && status !== "review" && !TERMINAL.has(status);
2712
3308
  }
3309
+ function isWorkingTaskStatus(status) {
3310
+ return status === "queued" || isActiveTaskStatus(status);
3311
+ }
2713
3312
  function isTerminalTaskStatus(status) {
2714
3313
  return TERMINAL.has(status);
2715
3314
  }
@@ -2764,14 +3363,18 @@ Follow-up context:
2764
3363
  - Previous request: ${options.parent.requestText}
2765
3364
  - Previous diff summary: ${options.parent.diffSummary || "No file changes"}
2766
3365
  ` : "";
2767
- return `You are editing the repository at: ${options.repoRoot}
2768
- Workspace: ${options.workspaceRoot}
3366
+ return `Target service context (authoritative):
3367
+ - Repository worktree: ${options.repoRoot}
3368
+ - Workspace: ${options.workspaceRoot}
3369
+ - Browser URL: ${options.context.page.url}
3370
+ ${options.upstreamUrl ? `- Local upstream URL: ${options.upstreamUrl}
3371
+ ` : ""}
3372
+ 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
3373
 
2770
3374
  User request:
2771
3375
  ${options.context.request.text}
2772
3376
 
2773
3377
  Selected UI context:
2774
- - URL: ${options.context.page.url}
2775
3378
  - Route: ${options.context.page.pathname}
2776
3379
  - Selection mode: ${options.context.selection.mode}
2777
3380
  ${targets || "- No concrete target; use the page context and repository search."}
@@ -2809,7 +3412,7 @@ import { relative as relative5, resolve as resolve6, sep as sep5 } from "node:pa
2809
3412
  // ../../packages/bridge-core/src/source/path-normalizer.ts
2810
3413
  import { access, readFile as readFile3, realpath as realpath6 } from "node:fs/promises";
2811
3414
  import { isAbsolute as isAbsolute3, relative as relative4, resolve as resolve5, sep as sep4 } from "node:path";
2812
- import { spawn as spawn4 } from "node:child_process";
3415
+ import { spawn as spawn5 } from "node:child_process";
2813
3416
  function toPosix(value) {
2814
3417
  return value.split(sep4).join("/").replaceAll("\\", "/");
2815
3418
  }
@@ -2844,7 +3447,7 @@ function isWithin2(root, candidate) {
2844
3447
  }
2845
3448
  async function gitFiles(repoRoot) {
2846
3449
  return await new Promise((resolvePromise, reject) => {
2847
- const child = spawn4(
3450
+ const child = spawn5(
2848
3451
  "git",
2849
3452
  ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
2850
3453
  {
@@ -2914,8 +3517,8 @@ async function normalizeSourceLocation(input, repoRootInput) {
2914
3517
  candidates: matches.slice(0, 20)
2915
3518
  };
2916
3519
  }
2917
- const filePath = matches[0];
2918
- const absolutePath = resolve5(repoRoot, filePath);
3520
+ const filePath2 = matches[0];
3521
+ const absolutePath = resolve5(repoRoot, filePath2);
2919
3522
  const canonical = await realpath6(absolutePath);
2920
3523
  if (!isWithin2(repoRoot, canonical)) {
2921
3524
  return { input, confidence: "unknown", candidates: [] };
@@ -2923,7 +3526,7 @@ async function normalizeSourceLocation(input, repoRootInput) {
2923
3526
  const lineNumber = await boundedLine(canonical, input.lineNumber);
2924
3527
  return {
2925
3528
  input,
2926
- filePath,
3529
+ filePath: filePath2,
2927
3530
  absolutePath: canonical,
2928
3531
  ...lineNumber === void 0 ? {} : { lineNumber },
2929
3532
  ...input.columnNumber === void 0 ? {} : { columnNumber: input.columnNumber },
@@ -3246,6 +3849,7 @@ function repositoryGuardMessage(result) {
3246
3849
  var TaskService = class {
3247
3850
  #projectId;
3248
3851
  #workspaceRoot;
3852
+ #upstreamUrl;
3249
3853
  #adapter;
3250
3854
  #store;
3251
3855
  #git;
@@ -3269,6 +3873,7 @@ var TaskService = class {
3269
3873
  constructor(options) {
3270
3874
  this.#projectId = options.projectId;
3271
3875
  this.#workspaceRoot = resolve6(options.workspaceRoot ?? options.git.repoRoot);
3876
+ this.#upstreamUrl = options.upstreamUrl;
3272
3877
  if (!isWithin3(options.git.repoRoot, this.#workspaceRoot)) {
3273
3878
  throw new TaskServiceError(
3274
3879
  "WORKSPACE_OUTSIDE_REPOSITORY",
@@ -3655,6 +4260,7 @@ var TaskService = class {
3655
4260
  const prompt = buildAgentPrompt({
3656
4261
  repoRoot: this.#git.repoRoot,
3657
4262
  workspaceRoot: this.#workspaceRoot,
4263
+ ...this.#upstreamUrl === void 0 ? {} : { upstreamUrl: this.#upstreamUrl },
3658
4264
  contextBundlePath: contextPath,
3659
4265
  context,
3660
4266
  allowedPatterns: this.#git.pathPolicy.allowedPatterns,
@@ -3848,11 +4454,11 @@ var TaskService = class {
3848
4454
  for (const target of result.selection.targets) {
3849
4455
  const sanitize = async (location) => {
3850
4456
  try {
3851
- const filePath = await this.#git.pathPolicy.assertFilesystemPathAllowed(
4457
+ const filePath2 = await this.#git.pathPolicy.assertFilesystemPathAllowed(
3852
4458
  location.filePath,
3853
4459
  false
3854
4460
  );
3855
- return { ...location, filePath };
4461
+ return { ...location, filePath: filePath2 };
3856
4462
  } catch {
3857
4463
  return void 0;
3858
4464
  }
@@ -3887,6 +4493,18 @@ var TaskService = class {
3887
4493
  };
3888
4494
 
3889
4495
  // ../../packages/bridge-core/src/verification/browser-sessions.ts
4496
+ function errorSignature(event) {
4497
+ return `${event.level}\0${event.message}`;
4498
+ }
4499
+ function samePage(left, right) {
4500
+ try {
4501
+ const leftUrl = new URL(left);
4502
+ const rightUrl = new URL(right);
4503
+ return leftUrl.origin === rightUrl.origin && leftUrl.pathname === rightUrl.pathname && leftUrl.search === rightUrl.search;
4504
+ } catch {
4505
+ return left === right;
4506
+ }
4507
+ }
3890
4508
  var BrowserSessionManager = class {
3891
4509
  #sessions = /* @__PURE__ */ new Map();
3892
4510
  #maxConsoleEvents;
@@ -3962,7 +4580,13 @@ var BrowserSessionManager = class {
3962
4580
  return {
3963
4581
  browserSessionId: id,
3964
4582
  renderRevision: session.renderRevision,
3965
- startedAt: now.toISOString()
4583
+ startedAt: now.toISOString(),
4584
+ url: session.url,
4585
+ knownErrorSignatures: [
4586
+ ...new Set(
4587
+ session.consoleEvents.filter((event) => event.level === "error" || event.level === "unhandled").map(errorSignature)
4588
+ )
4589
+ ]
3966
4590
  };
3967
4591
  }
3968
4592
  verify(baseline, options = {}) {
@@ -3975,10 +4599,16 @@ var BrowserSessionManager = class {
3975
4599
  summary: "Origin browser session is disconnected."
3976
4600
  };
3977
4601
  }
3978
- const newErrors = session.consoleEvents.filter(
3979
- (event) => event.createdAt >= baseline.startedAt && (event.level === "error" || event.level === "unhandled")
3980
- );
4602
+ const knownErrors = new Set(baseline.knownErrorSignatures);
4603
+ const newErrors = [
4604
+ ...new Map(
4605
+ session.consoleEvents.filter(
4606
+ (event) => event.createdAt > baseline.startedAt && (event.level === "error" || event.level === "unhandled") && !knownErrors.has(errorSignature(event))
4607
+ ).map((event) => [errorSignature(event), event])
4608
+ ).values()
4609
+ ];
3981
4610
  const renderChanged = session.renderRevision > baseline.renderRevision;
4611
+ const pageUnchanged = samePage(baseline.url, session.url);
3982
4612
  const targetResult = options.taskId === void 0 ? void 0 : session.targetResults.filter(
3983
4613
  (result) => result.taskId === options.taskId && result.createdAt >= baseline.startedAt
3984
4614
  ).at(-1);
@@ -3991,6 +4621,15 @@ var BrowserSessionManager = class {
3991
4621
  summary: `${newErrors.length} new browser error${newErrors.length === 1 ? "" : "s"} detected.`
3992
4622
  };
3993
4623
  }
4624
+ if (!pageUnchanged) {
4625
+ return {
4626
+ status: "partial",
4627
+ renderChanged,
4628
+ newErrors: [],
4629
+ ...targetResult ? { targetResult } : {},
4630
+ summary: "Origin browser navigated to a different page during verification."
4631
+ };
4632
+ }
3994
4633
  if (options.targetEvidenceRequired) {
3995
4634
  const targetChanged = targetResult?.state === "found-and-changed" && targetResult.targetCount > 0 && targetResult.foundCount === targetResult.targetCount && targetResult.changedCount > 0 && targetResult.renderRevision > baseline.renderRevision;
3996
4635
  if (targetChanged) {
@@ -4031,7 +4670,7 @@ var BrowserSessionManager = class {
4031
4670
  };
4032
4671
 
4033
4672
  // ../../packages/bridge-core/src/verification/commands.ts
4034
- import { spawn as spawn5 } from "node:child_process";
4673
+ import { spawn as spawn6 } from "node:child_process";
4035
4674
  var MAX_OUTPUT_CHARS = 8e3;
4036
4675
  var KILL_GRACE_MS = 250;
4037
4676
  function appendOutput(current, chunk) {
@@ -4045,7 +4684,7 @@ async function runVerificationCommand(configured, cwd, signal) {
4045
4684
  }
4046
4685
  const startedAt = Date.now();
4047
4686
  return await new Promise((resolveResult, rejectResult) => {
4048
- const child = spawn5(executable, arguments_, {
4687
+ const child = spawn6(executable, arguments_, {
4049
4688
  cwd,
4050
4689
  detached: process.platform !== "win32",
4051
4690
  shell: false,
@@ -4579,7 +5218,7 @@ ${output2}` : ""}`
4579
5218
  status: "ok",
4580
5219
  bridge: "online",
4581
5220
  projectId: options.project.id,
4582
- activeTask: taskService.list().find((task) => !["accepted", "reverted", "failed", "canceled", "unsafe"].includes(task.status))?.id ?? null
5221
+ activeTask: taskService.list().find((task) => isWorkingTaskStatus(task.status))?.id ?? null
4583
5222
  }),
4584
5223
  project: () => ({
4585
5224
  id: options.project.id,
@@ -4620,33 +5259,70 @@ ${output2}` : ""}`
4620
5259
  }
4621
5260
 
4622
5261
  // ../../packages/bridge-core/src/bridge/default-control-service.ts
5262
+ function createAgentAdapter(agent) {
5263
+ if (agent.adapter === "claude") {
5264
+ if (agent.reasoningEffort === "minimal") {
5265
+ throw new Error("Claude does not support minimal reasoning effort");
5266
+ }
5267
+ return new ClaudeAdapter({
5268
+ ...agent.model === void 0 ? {} : { model: agent.model },
5269
+ ...agent.reasoningEffort === void 0 ? {} : { reasoningEffort: agent.reasoningEffort }
5270
+ });
5271
+ }
5272
+ if (agent.adapter === "codex") {
5273
+ if (agent.reasoningEffort === "max") {
5274
+ throw new Error("Codex does not support max reasoning effort");
5275
+ }
5276
+ return new CodexAdapter({
5277
+ ...agent.model === void 0 ? {} : { model: agent.model },
5278
+ ...agent.reasoningEffort === void 0 ? {} : { reasoningEffort: agent.reasoningEffort },
5279
+ ...agent.profile === void 0 ? {} : { profile: agent.profile }
5280
+ });
5281
+ }
5282
+ throw new Error(`Agent adapter ${agent.adapter} is not implemented in this build`);
5283
+ }
5284
+ function inheritedAgentEnvironment(names, environment) {
5285
+ return Object.fromEntries(
5286
+ names.flatMap((name) => {
5287
+ const value = environment[name];
5288
+ return value === void 0 ? [] : [[name, value]];
5289
+ })
5290
+ );
5291
+ }
4623
5292
  async function createDefaultControlService(context, environment = process.env) {
4624
5293
  const loaded = await loadVisualDevConfig(context.repoRoot, {
4625
5294
  ...context.configRoot === void 0 ? {} : { configRoot: context.configRoot }
4626
5295
  });
4627
- if (loaded.config.agent.adapter !== "codex") {
4628
- throw new Error(
4629
- `Agent adapter ${loaded.config.agent.adapter} is not implemented in this MVP build`
4630
- );
4631
- }
4632
5296
  const git = await GitTransactionManager.open(context.repoRoot, {
4633
- allowed: loaded.config.paths.allowed,
4634
- denied: loaded.config.paths.denied
5297
+ allowed: rebaseWorkspacePatterns(
5298
+ context.repoRoot,
5299
+ context.workspaceRoot,
5300
+ loaded.config.paths.allowed
5301
+ ),
5302
+ denied: rebaseWorkspacePatterns(
5303
+ context.repoRoot,
5304
+ context.workspaceRoot,
5305
+ loaded.config.paths.denied
5306
+ )
4635
5307
  });
4636
5308
  const storagePaths = await resolveStoragePaths(context.repoRoot, environment);
4637
5309
  const store = new SqliteTaskStore(storagePaths.databasePath);
4638
5310
  const taskService = new TaskService({
4639
5311
  projectId: context.projectId,
4640
5312
  workspaceRoot: context.workspaceRoot,
4641
- adapter: new CodexAdapter(),
5313
+ upstreamUrl: context.upstreamUrl,
5314
+ adapter: createAgentAdapter(loaded.config.agent),
4642
5315
  store,
4643
5316
  git,
4644
5317
  maxRunMs: loaded.config.agent.maxRunMs,
4645
5318
  maxPending: loaded.config.queue.maxPending,
4646
5319
  resumeMode: loaded.config.agent.resumeMode,
4647
- environment: {}
5320
+ environment: inheritedAgentEnvironment(
5321
+ loaded.config.agent.inheritEnv,
5322
+ environment
5323
+ )
4648
5324
  });
4649
- return createTaskControlService({
5325
+ const controlService = createTaskControlService({
4650
5326
  taskService,
4651
5327
  hmrWaitMs: loaded.config.verification.hmrWaitMs,
4652
5328
  verificationCommands: loaded.config.verification.commands,
@@ -4658,10 +5334,32 @@ async function createDefaultControlService(context, environment = process.env) {
4658
5334
  upstreamUrl: context.upstreamUrl
4659
5335
  }
4660
5336
  });
5337
+ const reportRuntimeState = () => {
5338
+ const activeTask = taskService.list().find((task) => isWorkingTaskStatus(task.status));
5339
+ context.onRuntimeState?.({
5340
+ status: activeTask === void 0 ? "idle" : "working",
5341
+ ...activeTask === void 0 ? {} : { activeTaskId: activeTask.id }
5342
+ });
5343
+ };
5344
+ const unsubscribeRuntime = taskService.subscribe(reportRuntimeState);
5345
+ reportRuntimeState();
5346
+ return {
5347
+ ...controlService,
5348
+ close: async () => {
5349
+ unsubscribeRuntime();
5350
+ await controlService.close?.();
5351
+ }
5352
+ };
4661
5353
  }
4662
5354
 
4663
5355
  // ../../packages/bridge-core/src/runtime/registry.ts
4664
5356
  import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
5357
+ import {
5358
+ readFileSync,
5359
+ realpathSync,
5360
+ rmSync,
5361
+ unlinkSync
5362
+ } from "node:fs";
4665
5363
  import {
4666
5364
  mkdir as mkdir2,
4667
5365
  link,
@@ -4702,6 +5400,11 @@ async function repositoryKey(repositoryRoot) {
4702
5400
  const canonicalRoot = await realpath8(repositoryRoot);
4703
5401
  return createHash3("sha256").update(canonicalRoot).digest("hex");
4704
5402
  }
5403
+ function runtimeDirectoryForSync(repositoryRoot, options) {
5404
+ const canonicalRoot = realpathSync(repositoryRoot);
5405
+ const repoKey = createHash3("sha256").update(canonicalRoot).digest("hex");
5406
+ return join3(runtimeRoot(options), repoKey);
5407
+ }
4705
5408
  async function runtimeDirectoryFor(repositoryRoot, options = {}) {
4706
5409
  return join3(runtimeRoot(options), await repositoryKey(repositoryRoot));
4707
5410
  }
@@ -4740,6 +5443,13 @@ async function readJson(path) {
4740
5443
  return void 0;
4741
5444
  }
4742
5445
  }
5446
+ function readJsonSync(path) {
5447
+ try {
5448
+ return JSON.parse(readFileSync(path, "utf8"));
5449
+ } catch {
5450
+ return void 0;
5451
+ }
5452
+ }
4743
5453
  async function readInstance(repositoryRoot, options = {}) {
4744
5454
  const directory = await runtimeDirectoryFor(repositoryRoot, options);
4745
5455
  const value = await readJson(join3(directory, "instance.json"));
@@ -4757,6 +5467,21 @@ async function writeInstance(repositoryRoot, instance, options = {}) {
4757
5467
  });
4758
5468
  await rename(temporaryPath, path);
4759
5469
  }
5470
+ async function updateInstance(repositoryRoot, expectedPid, update, options = {}) {
5471
+ const current = await readInstance(repositoryRoot, options);
5472
+ if (current === void 0 || current.pid !== expectedPid) return void 0;
5473
+ const next = {
5474
+ ...current,
5475
+ ...update.status === void 0 ? {} : { status: update.status }
5476
+ };
5477
+ if (update.activeTaskId === null) {
5478
+ delete next.activeTaskId;
5479
+ } else if (update.activeTaskId !== void 0) {
5480
+ next.activeTaskId = update.activeTaskId;
5481
+ }
5482
+ await writeInstance(repositoryRoot, next, options);
5483
+ return next;
5484
+ }
4760
5485
  async function removeInstance(repositoryRoot, expectedPid, options = {}) {
4761
5486
  const directory = await runtimeDirectoryFor(repositoryRoot, options);
4762
5487
  const path = join3(directory, "instance.json");
@@ -4768,6 +5493,17 @@ async function removeInstance(repositoryRoot, expectedPid, options = {}) {
4768
5493
  }
4769
5494
  await rm3(path, { force: true });
4770
5495
  }
5496
+ function removeInstanceSync(repositoryRoot, expectedPid, options = {}) {
5497
+ const directory = runtimeDirectoryForSync(repositoryRoot, options);
5498
+ const path = join3(directory, "instance.json");
5499
+ if (expectedPid !== void 0) {
5500
+ const current = readJsonSync(path);
5501
+ if (!isBridgeInstanceRecord(current) || current.pid !== expectedPid) {
5502
+ return;
5503
+ }
5504
+ }
5505
+ rmSync(path, { force: true });
5506
+ }
4771
5507
  async function acquireWorktreeLock(repositoryRoot, options = {}) {
4772
5508
  const repoRoot = await realpath8(repositoryRoot);
4773
5509
  const repoKey = await repositoryKey(repoRoot);
@@ -4799,23 +5535,43 @@ async function acquireWorktreeLock(repositoryRoot, options = {}) {
4799
5535
  await rm3(candidatePath, { force: true });
4800
5536
  }
4801
5537
  let released = false;
5538
+ let releasePromise;
4802
5539
  return {
4803
5540
  repoKey,
4804
5541
  runtimeDirectory,
4805
5542
  lockPath,
4806
- async release() {
5543
+ release() {
4807
5544
  if (released) {
5545
+ return Promise.resolve();
5546
+ }
5547
+ releasePromise ??= (async () => {
5548
+ const current = await readJson(lockPath);
5549
+ if (isLockRecord(current) && current.ownerId === ownerId) {
5550
+ await unlink(lockPath).catch((error) => {
5551
+ if (error.code !== "ENOENT") {
5552
+ throw error;
5553
+ }
5554
+ });
5555
+ }
5556
+ released = true;
5557
+ })();
5558
+ return releasePromise;
5559
+ },
5560
+ releaseSync() {
5561
+ if (released || releasePromise !== void 0) {
4808
5562
  return;
4809
5563
  }
4810
- released = true;
4811
- const current = await readJson(lockPath);
5564
+ const current = readJsonSync(lockPath);
4812
5565
  if (isLockRecord(current) && current.ownerId === ownerId) {
4813
- await unlink(lockPath).catch((error) => {
4814
- if (error.code !== "ENOENT") {
5566
+ try {
5567
+ unlinkSync(lockPath);
5568
+ } catch (error) {
5569
+ if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "ENOENT") {
4815
5570
  throw error;
4816
5571
  }
4817
- });
5572
+ }
4818
5573
  }
5574
+ released = true;
4819
5575
  }
4820
5576
  };
4821
5577
  } catch (error) {
@@ -4835,10 +5591,10 @@ async function acquireWorktreeLock(repositoryRoot, options = {}) {
4835
5591
  }
4836
5592
 
4837
5593
  // ../../packages/bridge-core/src/runtime/repository.ts
4838
- import { execFile } from "node:child_process";
5594
+ import { execFile as execFile3 } from "node:child_process";
4839
5595
  import { realpath as realpath9 } from "node:fs/promises";
4840
- import { promisify } from "node:util";
4841
- var execFileAsync = promisify(execFile);
5596
+ import { promisify as promisify3 } from "node:util";
5597
+ var execFileAsync3 = promisify3(execFile3);
4842
5598
  var GitWorktreeNotFoundError = class extends Error {
4843
5599
  constructor(cwd, options = {}) {
4844
5600
  super(
@@ -4850,7 +5606,7 @@ var GitWorktreeNotFoundError = class extends Error {
4850
5606
  };
4851
5607
  async function discoverGitWorktreeRoot(cwd = process.cwd()) {
4852
5608
  try {
4853
- const { stdout } = await execFileAsync(
5609
+ const { stdout } = await execFileAsync3(
4854
5610
  "git",
4855
5611
  ["-C", cwd, "rev-parse", "--show-toplevel"],
4856
5612
  {
@@ -4869,6 +5625,91 @@ async function discoverGitWorktreeRoot(cwd = process.cwd()) {
4869
5625
  }
4870
5626
 
4871
5627
  // src/bridge.ts
5628
+ var DEFAULT_UPSTREAM_MONITOR_INTERVAL_MS = 1e3;
5629
+ var DEFAULT_UPSTREAM_CONNECT_TIMEOUT_MS = 500;
5630
+ var DEFAULT_UPSTREAM_FAILURE_GRACE_MS = 5e3;
5631
+ function positiveMilliseconds(value, fallback) {
5632
+ return value === void 0 || !Number.isFinite(value) || value <= 0 ? fallback : Math.max(1, Math.floor(value));
5633
+ }
5634
+ async function probeUpstream(upstreamUrl, timeoutMs) {
5635
+ const upstream = new URL(upstreamUrl);
5636
+ const hostname = upstream.hostname.startsWith("[") ? upstream.hostname.slice(1, -1) : upstream.hostname;
5637
+ const port = Number(
5638
+ upstream.port || (upstream.protocol === "https:" ? 443 : 80)
5639
+ );
5640
+ return await new Promise((resolve9) => {
5641
+ let settled = false;
5642
+ let timeout;
5643
+ const socket = createConnection({ host: hostname, port });
5644
+ socket.unref();
5645
+ const finish = (reachable) => {
5646
+ if (settled) return;
5647
+ settled = true;
5648
+ if (timeout !== void 0) clearTimeout(timeout);
5649
+ socket.destroy();
5650
+ resolve9(reachable);
5651
+ };
5652
+ socket.once("connect", () => finish(true));
5653
+ socket.once("error", () => finish(false));
5654
+ timeout = setTimeout(() => finish(false), timeoutMs);
5655
+ timeout.unref();
5656
+ });
5657
+ }
5658
+ function monitorUpstream(bridge, options) {
5659
+ const intervalMs = positiveMilliseconds(
5660
+ options.intervalMs,
5661
+ DEFAULT_UPSTREAM_MONITOR_INTERVAL_MS
5662
+ );
5663
+ const connectTimeoutMs = positiveMilliseconds(
5664
+ options.connectTimeoutMs,
5665
+ DEFAULT_UPSTREAM_CONNECT_TIMEOUT_MS
5666
+ );
5667
+ const initialTimeoutMs = positiveMilliseconds(
5668
+ options.initialTimeoutMs,
5669
+ 6e4
5670
+ );
5671
+ const failureGraceMs = positiveMilliseconds(
5672
+ options.failureGraceMs,
5673
+ DEFAULT_UPSTREAM_FAILURE_GRACE_MS
5674
+ );
5675
+ let connected = false;
5676
+ let unavailableSince;
5677
+ let stopped = false;
5678
+ let timer;
5679
+ const stop = () => {
5680
+ stopped = true;
5681
+ if (timer !== void 0) clearTimeout(timer);
5682
+ };
5683
+ const schedule = () => {
5684
+ if (stopped) return;
5685
+ timer = setTimeout(() => {
5686
+ void check().catch(() => void 0);
5687
+ }, intervalMs);
5688
+ timer.unref();
5689
+ };
5690
+ const check = async () => {
5691
+ if (stopped) return;
5692
+ const reachable = await probeUpstream(bridge.upstreamUrl, connectTimeoutMs);
5693
+ if (stopped) return;
5694
+ const now = Date.now();
5695
+ if (reachable) {
5696
+ connected = true;
5697
+ unavailableSince = void 0;
5698
+ schedule();
5699
+ return;
5700
+ }
5701
+ unavailableSince ??= now;
5702
+ const timeoutMs = connected ? failureGraceMs : initialTimeoutMs;
5703
+ if (now - unavailableSince >= timeoutMs) {
5704
+ stop();
5705
+ await bridge.close();
5706
+ return;
5707
+ }
5708
+ schedule();
5709
+ };
5710
+ void bridge.closed.then(stop);
5711
+ void check().catch(() => void 0);
5712
+ }
4872
5713
  function normalizeUpstream(value) {
4873
5714
  const url = new URL(value);
4874
5715
  if (url.protocol !== "http:" && url.protocol !== "https:") {
@@ -4905,10 +5746,30 @@ async function startBridgeCore(options, dependencies) {
4905
5746
  let gateway;
4906
5747
  let controlService;
4907
5748
  let registryWritten = false;
5749
+ let registryUpdate = Promise.resolve();
5750
+ let latestRuntimeState = { status: "idle" };
5751
+ const updateRuntimeState = (status, activeTaskId) => {
5752
+ latestRuntimeState = {
5753
+ status,
5754
+ ...activeTaskId === void 0 ? {} : { activeTaskId }
5755
+ };
5756
+ if (!registryWritten) return;
5757
+ registryUpdate = registryUpdate.then(async () => {
5758
+ await updateInstance(
5759
+ loadedConfig.repoRoot,
5760
+ process.pid,
5761
+ {
5762
+ status,
5763
+ activeTaskId: activeTaskId ?? null
5764
+ },
5765
+ { environment }
5766
+ );
5767
+ }).catch(() => void 0);
5768
+ };
4908
5769
  try {
4909
5770
  lock = options.lock ?? await acquireWorktreeLock(loadedConfig.repoRoot, { environment });
4910
5771
  const host = options.host ?? loadedConfig.config.gateway.host;
4911
- const configuredPublicUrl = options.publicUrl ?? loadedConfig.config.gateway.publicUrl;
5772
+ const configuredPublicUrl = options.publicUrl ?? loadedConfig.config.gateway.publicUrl ?? options.fallbackPublicUrl;
4912
5773
  const publicUrl = configuredPublicUrl === void 0 ? void 0 : normalizePublicUrl(configuredPublicUrl);
4913
5774
  const gatewayPort = await findAvailablePort(startPort(loadedConfig, options.listen), host);
4914
5775
  const token = generatePairingToken();
@@ -4918,7 +5779,10 @@ async function startBridgeCore(options, dependencies) {
4918
5779
  repoRoot: loadedConfig.repoRoot,
4919
5780
  configRoot: loadedConfig.configRoot,
4920
5781
  workspaceRoot: loadedConfig.workspaceRoot,
4921
- upstreamUrl: options.upstreamUrl
5782
+ upstreamUrl: options.upstreamUrl,
5783
+ onRuntimeState: (state) => {
5784
+ updateRuntimeState(state.status, state.activeTaskId);
5785
+ }
4922
5786
  };
4923
5787
  controlService = await resolveControlService(dependencies, controlContext);
4924
5788
  const allowedOrigins = new Set(loadedConfig.config.security.allowedOrigins);
@@ -4933,20 +5797,33 @@ async function startBridgeCore(options, dependencies) {
4933
5797
  allowedOrigins: [...allowedOrigins]
4934
5798
  });
4935
5799
  const address = await gateway.start();
4936
- const openUrl = publicUrl ?? address.url;
5800
+ const openUrl = createPairingUrl(publicUrl ?? address.url, token);
4937
5801
  const instance = {
4938
5802
  projectId: loadedConfig.config.project.id,
4939
5803
  repoRoot: loadedConfig.repoRoot,
4940
5804
  pid: process.pid,
4941
5805
  gatewayUrl: address.url,
4942
5806
  upstreamUrl: options.upstreamUrl,
4943
- status: "idle",
5807
+ status: latestRuntimeState.status,
4944
5808
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5809
+ ...latestRuntimeState.activeTaskId === void 0 ? {} : { activeTaskId: latestRuntimeState.activeTaskId },
4945
5810
  ...publicUrl === void 0 ? {} : { publicUrl }
4946
5811
  };
4947
5812
  await writeInstance(loadedConfig.repoRoot, instance, { environment });
4948
5813
  registryWritten = true;
4949
- let closed = false;
5814
+ const emergencyExitCleanup = () => {
5815
+ try {
5816
+ removeInstanceSync(loadedConfig.repoRoot, process.pid, { environment });
5817
+ lock?.releaseSync();
5818
+ } catch {
5819
+ }
5820
+ };
5821
+ process.once("exit", emergencyExitCleanup);
5822
+ let resolveClosed = () => void 0;
5823
+ const closed = new Promise((resolve9) => {
5824
+ resolveClosed = resolve9;
5825
+ });
5826
+ let closePromise;
4950
5827
  return {
4951
5828
  mode: options.mode,
4952
5829
  projectId: loadedConfig.config.project.id,
@@ -4958,18 +5835,33 @@ async function startBridgeCore(options, dependencies) {
4958
5835
  openUrl,
4959
5836
  gateway,
4960
5837
  ...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();
5838
+ closed,
5839
+ close() {
5840
+ closePromise ??= (async () => {
5841
+ process.off("exit", emergencyExitCleanup);
5842
+ try {
5843
+ await registryUpdate;
5844
+ await updateInstance(
5845
+ loadedConfig.repoRoot,
5846
+ process.pid,
5847
+ { status: "stopping", activeTaskId: null },
5848
+ { environment }
5849
+ );
5850
+ await Promise.allSettled([
5851
+ gateway?.close(),
5852
+ options.managedProcess?.stop(),
5853
+ controlService?.close?.()
5854
+ ]);
5855
+ await removeInstance(loadedConfig.repoRoot, process.pid, { environment });
5856
+ } finally {
5857
+ try {
5858
+ await lock?.release();
5859
+ } finally {
5860
+ resolveClosed();
5861
+ }
5862
+ }
5863
+ })();
5864
+ return closePromise;
4973
5865
  }
4974
5866
  };
4975
5867
  } catch (error) {
@@ -4990,17 +5882,25 @@ async function startAttachBridge(options, dependencies = {}) {
4990
5882
  const repoRoot = await discoverGitWorktreeRoot(cwd);
4991
5883
  const configRoot = await discoverVisualDevConfigRoot(cwd, repoRoot);
4992
5884
  const loadedConfig = await loadVisualDevConfig(repoRoot, { configRoot });
4993
- return await startBridgeCore(
5885
+ const bridge = await startBridgeCore(
4994
5886
  {
4995
5887
  mode: "attach",
4996
5888
  loadedConfig,
4997
5889
  upstreamUrl: normalizeUpstream(options.upstream),
4998
5890
  ...options.listen === void 0 ? {} : { listen: options.listen },
4999
5891
  ...options.host === void 0 ? {} : { host: options.host },
5000
- ...options.publicUrl === void 0 ? {} : { publicUrl: options.publicUrl }
5892
+ ...options.publicUrl === void 0 ? {} : { publicUrl: options.publicUrl },
5893
+ ...options.fallbackPublicUrl === void 0 ? {} : { fallbackPublicUrl: options.fallbackPublicUrl }
5001
5894
  },
5002
5895
  dependencies
5003
5896
  );
5897
+ if (dependencies.upstreamMonitor !== false) {
5898
+ monitorUpstream(bridge, {
5899
+ initialTimeoutMs: loadedConfig.config.upstream.ready.timeoutMs,
5900
+ ...dependencies.upstreamMonitor
5901
+ });
5902
+ }
5903
+ return bridge;
5004
5904
  }
5005
5905
  async function startManagedBridge(options = {}, dependencies = {}) {
5006
5906
  const cwd = dependencies.cwd ?? process.cwd();
@@ -5102,6 +6002,7 @@ async function runBridgeUntilSignal(bridge, processLike = process, gracefulTimeo
5102
6002
  };
5103
6003
  for (const signal of signals) processLike.once(signal, shutdown);
5104
6004
  void bridge.managedProcess?.exit.then(shutdown);
6005
+ void bridge.closed?.then(shutdown);
5105
6006
  });
5106
6007
  }
5107
6008
  function formatBridgeSummary(bridge) {
@@ -5120,9 +6021,9 @@ function formatBridgeSummary(bridge) {
5120
6021
  import { constants } from "node:fs";
5121
6022
  import { access as access2, stat as stat3 } from "node:fs/promises";
5122
6023
  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);
6024
+ import { execFile as execFile4 } from "node:child_process";
6025
+ import { promisify as promisify4 } from "node:util";
6026
+ var execFileAsync4 = promisify4(execFile4);
5126
6027
  async function fileExists(path) {
5127
6028
  try {
5128
6029
  await stat3(path);
@@ -5136,7 +6037,7 @@ async function fileExists(path) {
5136
6037
  }
5137
6038
  async function isIgnored(repoRoot, path) {
5138
6039
  try {
5139
- await execFileAsync2("git", ["-C", repoRoot, "check-ignore", "--quiet", "--", path], {
6040
+ await execFileAsync4("git", ["-C", repoRoot, "check-ignore", "--quiet", "--", path], {
5140
6041
  windowsHide: true
5141
6042
  });
5142
6043
  return true;
@@ -5178,6 +6079,7 @@ async function runDoctor(dependencies = {}) {
5178
6079
  repoRoot
5179
6080
  );
5180
6081
  const loaded = await loadVisualDevConfig(repoRoot, { configRoot });
6082
+ const environment = dependencies.environment ?? process.env;
5181
6083
  checks.push({
5182
6084
  name: "config",
5183
6085
  status: loaded.loadedFiles.length === 0 ? "warning" : "pass",
@@ -5198,7 +6100,7 @@ async function runDoctor(dependencies = {}) {
5198
6100
  const available = executable !== void 0 && await executableAvailable(
5199
6101
  executable,
5200
6102
  loaded.workspaceRoot,
5201
- dependencies.environment ?? process.env
6103
+ environment
5202
6104
  );
5203
6105
  checks.push({
5204
6106
  name: "dev-command",
@@ -5206,6 +6108,93 @@ async function runDoctor(dependencies = {}) {
5206
6108
  message: available ? `${executable} is executable.` : `${executable ?? "<empty>"} was not found or is not executable.`
5207
6109
  });
5208
6110
  }
6111
+ const adapter = loaded.config.agent.adapter;
6112
+ const adapterSupported = adapter === "codex" || adapter === "claude";
6113
+ const agentAvailable = await executableAvailable(
6114
+ adapter,
6115
+ loaded.workspaceRoot,
6116
+ environment
6117
+ );
6118
+ checks.push({
6119
+ name: "agent",
6120
+ status: adapterSupported && agentAvailable ? "pass" : "fail",
6121
+ message: !adapterSupported ? `${adapter} is configured but is not implemented in this build.` : agentAvailable ? `${adapter} is executable.` : `${adapter} was not found or is not executable.`
6122
+ });
6123
+ if (loaded.config.agent.inheritEnv.length > 0) {
6124
+ const missing = loaded.config.agent.inheritEnv.filter(
6125
+ (name) => environment[name] === void 0
6126
+ );
6127
+ checks.push({
6128
+ name: "agent-environment",
6129
+ status: missing.length === 0 ? "pass" : "fail",
6130
+ message: missing.length === 0 ? `${loaded.config.agent.inheritEnv.length} agent environment variable(s) are available.` : `Missing agent environment variable(s): ${missing.join(", ")}.`
6131
+ });
6132
+ }
6133
+ if (adapter === "claude") {
6134
+ const sandboxDependencies = await Promise.all(
6135
+ ["bwrap", "socat"].map(async (executable) => ({
6136
+ executable,
6137
+ available: await executableAvailable(
6138
+ executable,
6139
+ loaded.workspaceRoot,
6140
+ environment
6141
+ )
6142
+ }))
6143
+ );
6144
+ const missing = sandboxDependencies.filter(({ available }) => !available).map(({ executable }) => executable);
6145
+ checks.push({
6146
+ name: "claude-sandbox",
6147
+ status: missing.length === 0 ? "pass" : "warning",
6148
+ message: missing.length === 0 ? "Claude Bash sandbox dependencies are available." : `Claude Bash sandbox is unavailable without: ${missing.join(", ")}.`
6149
+ });
6150
+ }
6151
+ const rtkAvailable = await executableAvailable(
6152
+ "rtk",
6153
+ loaded.workspaceRoot,
6154
+ environment
6155
+ );
6156
+ checks.push({
6157
+ name: "rtk",
6158
+ status: rtkAvailable ? "pass" : "warning",
6159
+ message: rtkAvailable ? "rtk is available for token-efficient command output." : "rtk was not found; agent commands will use their native output."
6160
+ });
6161
+ const verificationCommands = loaded.config.verification.commands;
6162
+ if (verificationCommands.length === 0) {
6163
+ checks.push({
6164
+ name: "verification",
6165
+ status: "warning",
6166
+ message: "No verification commands are configured."
6167
+ });
6168
+ } else {
6169
+ const availability = await Promise.all(
6170
+ verificationCommands.map(async ({ command: command2, name }) => ({
6171
+ name,
6172
+ available: await executableAvailable(
6173
+ command2[0] ?? "",
6174
+ loaded.workspaceRoot,
6175
+ environment
6176
+ )
6177
+ }))
6178
+ );
6179
+ const missing = availability.filter(({ available }) => !available).map(({ name }) => name);
6180
+ checks.push({
6181
+ name: "verification",
6182
+ status: missing.length === 0 ? "pass" : "fail",
6183
+ message: missing.length === 0 ? `${verificationCommands.length} verification command(s) are ready.` : `Missing executable for: ${missing.join(", ")}.`
6184
+ });
6185
+ }
6186
+ const publicUrl = loaded.config.gateway.publicUrl;
6187
+ checks.push({
6188
+ name: "public-url",
6189
+ status: publicUrl === void 0 ? "warning" : "pass",
6190
+ message: publicUrl === void 0 ? "No gateway.publicUrl is configured; pairing links will use the local gateway URL." : `Pairing links will use ${publicUrl}.`
6191
+ });
6192
+ const allowedOrigins = loaded.config.security.allowedOrigins;
6193
+ checks.push({
6194
+ name: "allowed-origins",
6195
+ status: allowedOrigins.length > 0 || publicUrl !== void 0 ? "pass" : "warning",
6196
+ 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."
6197
+ });
5209
6198
  } catch (error) {
5210
6199
  checks.push({
5211
6200
  name: "config",
@@ -5223,7 +6212,7 @@ function formatDoctorChecks(checks) {
5223
6212
  }
5224
6213
 
5225
6214
  // src/init.ts
5226
- import { spawn as spawn6 } from "node:child_process";
6215
+ import { spawn as spawn7 } from "node:child_process";
5227
6216
  import { readFile as readFile5, mkdir as mkdir3, realpath as realpath10, stat as stat4, writeFile as writeFile3 } from "node:fs/promises";
5228
6217
  import { basename as basename2, dirname as dirname4, join as join5, relative as relative7 } from "node:path";
5229
6218
  import { stringify as stringifyYaml } from "yaml";
@@ -5543,7 +6532,7 @@ function installCommand(request) {
5543
6532
  async function installPackage(request) {
5544
6533
  const { command, args } = installCommand(request);
5545
6534
  await new Promise((resolvePromise, reject) => {
5546
- const child = spawn6(command, args, {
6535
+ const child = spawn7(command, args, {
5547
6536
  cwd: request.cwd,
5548
6537
  env: process.env,
5549
6538
  stdio: "inherit",
@@ -5666,15 +6655,124 @@ function formatBridgeStatus(status) {
5666
6655
  return "No Visual Bridge is running for this worktree.";
5667
6656
  }
5668
6657
  const { instance } = status;
5669
- return [
6658
+ const rows = [
5670
6659
  `Project: ${instance.projectId}`,
5671
6660
  `Status: ${instance.status}`,
5672
6661
  `PID: ${instance.pid}`,
5673
6662
  `Gateway: ${instance.gatewayUrl}`,
5674
6663
  `Upstream: ${instance.upstreamUrl}`
5675
- ].join("\n");
6664
+ ];
6665
+ if (instance.publicUrl !== void 0) rows.push(`Public: ${instance.publicUrl}`);
6666
+ if (instance.activeTaskId !== void 0) {
6667
+ rows.push(`Active: ${instance.activeTaskId}`);
6668
+ }
6669
+ return rows.join("\n");
5676
6670
  }
5677
6671
 
6672
+ // ../../package.json
6673
+ var package_default = {
6674
+ name: "visual-remote",
6675
+ version: "0.3.2",
6676
+ description: "Visual bridge from a running web UI to a coding agent in its Git worktree",
6677
+ type: "module",
6678
+ packageManager: "pnpm@10.34.5",
6679
+ repository: {
6680
+ type: "git",
6681
+ url: "git+https://github.com/elicie/visual-remote.git"
6682
+ },
6683
+ homepage: "https://github.com/elicie/visual-remote#readme",
6684
+ bugs: {
6685
+ url: "https://github.com/elicie/visual-remote/issues"
6686
+ },
6687
+ files: [
6688
+ "apps/cli/dist/index.js",
6689
+ "apps/cli/dist/direct-exec-mcp.js",
6690
+ "apps/cli/dist/vite.js",
6691
+ "apps/cli/dist/next.js",
6692
+ "apps/cli/dist/next-client.js",
6693
+ "apps/cli/vite.d.ts",
6694
+ "apps/cli/next.d.ts",
6695
+ "apps/cli/next-client.d.ts",
6696
+ "packages/overlay/dist/client.js",
6697
+ "packages/overlay/dist/viewer.js"
6698
+ ],
6699
+ bin: {
6700
+ visual: "./apps/cli/dist/index.js",
6701
+ "visual-remote": "./apps/cli/dist/index.js"
6702
+ },
6703
+ exports: {
6704
+ "./vite": {
6705
+ types: "./apps/cli/vite.d.ts",
6706
+ import: "./apps/cli/dist/vite.js"
6707
+ },
6708
+ "./next": {
6709
+ types: "./apps/cli/next.d.ts",
6710
+ import: "./apps/cli/dist/next.js",
6711
+ default: "./apps/cli/dist/next.js"
6712
+ },
6713
+ "./next/client": {
6714
+ types: "./apps/cli/next-client.d.ts",
6715
+ import: "./apps/cli/dist/next-client.js",
6716
+ default: "./apps/cli/dist/next-client.js"
6717
+ }
6718
+ },
6719
+ publishConfig: {
6720
+ access: "public",
6721
+ registry: "https://registry.npmjs.org"
6722
+ },
6723
+ engines: {
6724
+ node: ">=24"
6725
+ },
6726
+ scripts: {
6727
+ build: "corepack pnpm run build:overlay && corepack pnpm run build:server",
6728
+ "build:overlay": "corepack pnpm --filter @visual-remote/overlay build",
6729
+ "build:server": "corepack pnpm --filter @visual-remote/cli build",
6730
+ dev: "corepack pnpm run build:overlay && tsx apps/cli/src/index.ts",
6731
+ test: "vitest run",
6732
+ "test:e2e": "corepack pnpm build && corepack pnpm exec playwright test --config tests/e2e/playwright.config.ts",
6733
+ "test:watch": "vitest",
6734
+ typecheck: "corepack pnpm -r --if-present typecheck && tsc --noEmit -p tsconfig.tests.json",
6735
+ prepack: "corepack pnpm build"
6736
+ },
6737
+ dependencies: {
6738
+ commander: "^15.0.0",
6739
+ "http-proxy": "^1.18.1",
6740
+ ws: "^8.21.1",
6741
+ yaml: "^2.9.0",
6742
+ zod: "^4.4.3"
6743
+ },
6744
+ peerDependencies: {
6745
+ vite: ">=5"
6746
+ },
6747
+ peerDependenciesMeta: {
6748
+ vite: {
6749
+ optional: true
6750
+ }
6751
+ },
6752
+ devDependencies: {
6753
+ "@playwright/test": "^1.62.1",
6754
+ "@types/http-proxy": "^1.17.17",
6755
+ "@types/node": "^26.1.2",
6756
+ "@types/ws": "^8.18.1",
6757
+ "@visual-remote/bridge-core": "workspace:*",
6758
+ "@visual-remote/cli": "workspace:*",
6759
+ "@visual-remote/gateway": "workspace:*",
6760
+ "@visual-remote/overlay": "workspace:*",
6761
+ "@visual-remote/protocol": "workspace:*",
6762
+ esbuild: "^0.28.1",
6763
+ next: "15.5.16",
6764
+ react: "19.1.0",
6765
+ "react-dom": "19.1.0",
6766
+ tsx: "^4.23.1",
6767
+ typescript: "^7.0.2",
6768
+ vite: "^8.1.5",
6769
+ vitest: "^4.1.10"
6770
+ }
6771
+ };
6772
+
6773
+ // src/version.ts
6774
+ var VISUAL_REMOTE_VERSION = package_default.version;
6775
+
5678
6776
  // src/index.ts
5679
6777
  function parsePort(value) {
5680
6778
  const port = Number(value);
@@ -5697,7 +6795,7 @@ function setExitCode(dependencies, code) {
5697
6795
  }
5698
6796
  }
5699
6797
  function createCli(dependencies = {}) {
5700
- const program = new Command().name("visual").description("Visual Remote Dev Bridge").version("0.3.0");
6798
+ const program = new Command().name("visual").description("Visual Remote Dev Bridge").version(VISUAL_REMOTE_VERSION);
5701
6799
  program.command("init").description("Configure Visual Remote for the current Vite or Next.js project").action(async () => {
5702
6800
  const result = await initializeVisualDev(dependencies);
5703
6801
  output(dependencies, formatInitResult(result));
@@ -5749,7 +6847,7 @@ async function main(argv = process.argv, dependencies = {}) {
5749
6847
  }
5750
6848
  function isDirectEntry(entryPath2) {
5751
6849
  try {
5752
- return pathToFileURL(realpathSync(resolve8(entryPath2))).href === import.meta.url;
6850
+ return pathToFileURL(realpathSync2(resolve8(entryPath2))).href === import.meta.url;
5753
6851
  } catch {
5754
6852
  return false;
5755
6853
  }
@@ -5765,6 +6863,7 @@ if (entryPath !== void 0 && isDirectEntry(entryPath)) {
5765
6863
  });
5766
6864
  }
5767
6865
  export {
6866
+ VISUAL_REMOTE_VERSION,
5768
6867
  createCli,
5769
6868
  formatBridgeStatus,
5770
6869
  formatBridgeSummary,