@dev-anywhere/proxy 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/serve.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  KnownContentBlockSchema,
6
6
  SeqCounter,
7
7
  StreamJsonEventSchema
8
- } from "./chunk-QXOARRC2.js";
8
+ } from "./chunk-ODK6N2NP.js";
9
9
  import {
10
10
  createFSM,
11
11
  defineFSM,
@@ -14,7 +14,7 @@ import {
14
14
  serviceLogger,
15
15
  shouldReleaseApprovalWait,
16
16
  stateAfterApprovalRelease
17
- } from "./chunk-TG7JPHE5.js";
17
+ } from "./chunk-JGGDVMY5.js";
18
18
  import {
19
19
  spawnScript
20
20
  } from "./chunk-ZUWAB67J.js";
@@ -46,7 +46,7 @@ import {
46
46
  serializeWorkerMsg,
47
47
  sessionPaths,
48
48
  tildify
49
- } from "./chunk-DFLQ3TFT.js";
49
+ } from "./chunk-RFBTVZ2X.js";
50
50
 
51
51
  // src/serve.ts
52
52
  import { createServer as createServer2 } from "net";
@@ -702,6 +702,7 @@ function loadConfig(options) {
702
702
  hookPort: parsePort(process.env.DEV_ANYWHERE_HOOK_PORT, "DEV_ANYWHERE_HOOK_PORT") ?? defaultHookPortForProfile(PROFILE_NAME),
703
703
  claudeBin,
704
704
  codexBin,
705
+ previewRoots: uniqueAbsolutePaths(fromFile.previewRoots ?? []),
705
706
  agentCliSuggestions: {
706
707
  claude: uniqueAbsolutePaths([
707
708
  process.env.CLAUDE_BIN,
@@ -1083,7 +1084,7 @@ function extractConversationText(msg) {
1083
1084
  return null;
1084
1085
  }
1085
1086
  async function extractTitleAndCwd(filePath) {
1086
- return new Promise((resolve2) => {
1087
+ return new Promise((resolve3) => {
1087
1088
  const rl = createInterface({
1088
1089
  input: createReadStream(filePath, { encoding: "utf-8" }),
1089
1090
  crlfDelay: Infinity
@@ -1111,10 +1112,10 @@ async function extractTitleAndCwd(filePath) {
1111
1112
  }
1112
1113
  });
1113
1114
  rl.on("close", () => {
1114
- if (!resolved) resolve2({ title, cwd });
1115
- else resolve2({ title, cwd });
1115
+ if (!resolved) resolve3({ title, cwd });
1116
+ else resolve3({ title, cwd });
1116
1117
  });
1117
- rl.on("error", () => resolve2({ title, cwd }));
1118
+ rl.on("error", () => resolve3({ title, cwd }));
1118
1119
  });
1119
1120
  }
1120
1121
  async function collectJsonlFiles(root) {
@@ -1136,7 +1137,7 @@ async function collectJsonlFiles(root) {
1136
1137
  return files;
1137
1138
  }
1138
1139
  async function extractCodexTitleAndCwd(filePath) {
1139
- return new Promise((resolve2) => {
1140
+ return new Promise((resolve3) => {
1140
1141
  const rl = createInterface({
1141
1142
  input: createReadStream(filePath, { encoding: "utf-8" }),
1142
1143
  crlfDelay: Infinity
@@ -1160,8 +1161,8 @@ async function extractCodexTitleAndCwd(filePath) {
1160
1161
  } catch {
1161
1162
  }
1162
1163
  });
1163
- rl.on("close", () => resolve2({ id, title, cwd }));
1164
- rl.on("error", () => resolve2({ id, title, cwd }));
1164
+ rl.on("close", () => resolve3({ id, title, cwd }));
1165
+ rl.on("error", () => resolve3({ id, title, cwd }));
1165
1166
  });
1166
1167
  }
1167
1168
  function extractCodexUserText(payload) {
@@ -1726,16 +1727,16 @@ var WorkerRegistry = class {
1726
1727
  return workerPid;
1727
1728
  }
1728
1729
  connect(sessionId, sockPath) {
1729
- return new Promise((resolve2) => {
1730
+ return new Promise((resolve3) => {
1730
1731
  const sock = connect(sockPath);
1731
1732
  sock.on("connect", () => {
1732
1733
  this.sockets.set(sessionId, sock);
1733
1734
  createWorkerReader(sock, (msg) => this.handleWorkerMessage(sessionId, msg));
1734
1735
  sock.on("close", () => this.onDisconnect(sessionId));
1735
1736
  sock.on("error", () => this.onDisconnect(sessionId));
1736
- resolve2(sock);
1737
+ resolve3(sock);
1737
1738
  });
1738
- sock.on("error", () => resolve2(null));
1739
+ sock.on("error", () => resolve3(null));
1739
1740
  });
1740
1741
  }
1741
1742
  // 枚举 DATA_DIR 下所有 session 目录,尝试连接存活的 worker.sock;失败则清理 stale socket。
@@ -2060,7 +2061,7 @@ function terminateSessionByOwnership(deps, sessionId) {
2060
2061
  }
2061
2062
 
2062
2063
  // src/serve/clipboard-image-upload.ts
2063
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
2064
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync4 } from "fs";
2064
2065
  import { isAbsolute as isAbsolute3, join as join5, relative, resolve } from "path";
2065
2066
  import { nanoid as nanoid3 } from "nanoid";
2066
2067
  var MAX_CLIPBOARD_IMAGE_BYTES = 10 * 1024 * 1024;
@@ -2093,15 +2094,50 @@ function decodeBase64Image(dataBase64) {
2093
2094
  }
2094
2095
  return buffer;
2095
2096
  }
2096
- function resolveSessionClipboardDir(dataDir, sessionId) {
2097
- const root = resolve(dataDir);
2098
- const uploadDir = resolve(root, sessionId, "clipboard");
2097
+ function resolveChildDir(rootPath, ...segments) {
2098
+ const root = resolve(rootPath);
2099
+ const uploadDir = resolve(root, ...segments);
2099
2100
  const relativePath = relative(root, uploadDir);
2100
2101
  if (!relativePath || relativePath.startsWith("..") || isAbsolute3(relativePath)) {
2101
2102
  throw new Error("\u4F1A\u8BDD\u8DEF\u5F84\u65E0\u6548");
2102
2103
  }
2103
2104
  return uploadDir;
2104
2105
  }
2106
+ function resolveSessionClipboardDir(dataDir, sessionId) {
2107
+ return resolveChildDir(dataDir, sessionId, "clipboard");
2108
+ }
2109
+ function normalizeGitignoreLine(line) {
2110
+ return line.trim().replace(/^\/+/, "").replace(/\/+$/, "");
2111
+ }
2112
+ function ensureProjectClipboardIgnored(cwd) {
2113
+ const gitignorePath = join5(cwd, ".gitignore");
2114
+ if (!existsSync5(gitignorePath)) return;
2115
+ try {
2116
+ const current = readFileSync5(gitignorePath, "utf-8");
2117
+ const alreadyIgnored = current.split(/\r?\n/).some((line) => normalizeGitignoreLine(line) === ".dev-anywhere");
2118
+ if (alreadyIgnored) return;
2119
+ const separator = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
2120
+ writeFileSync4(gitignorePath, `${current}${separator}.dev-anywhere/
2121
+ `);
2122
+ } catch {
2123
+ }
2124
+ }
2125
+ function trySaveProjectClipboardImage(options) {
2126
+ if (!options.cwd) return null;
2127
+ try {
2128
+ const cwd = resolve(options.cwd);
2129
+ if (!statSync(cwd).isDirectory()) return null;
2130
+ const clipboardRoot = resolve(cwd, ".dev-anywhere", "clipboard");
2131
+ const uploadDir = resolveChildDir(clipboardRoot, options.sessionId);
2132
+ const path = join5(uploadDir, options.fileName);
2133
+ mkdirSync4(uploadDir, { recursive: true });
2134
+ writeFileSync4(path, options.buffer, { mode: 384 });
2135
+ ensureProjectClipboardIgnored(cwd);
2136
+ return { success: true, path: relative(cwd, path) };
2137
+ } catch {
2138
+ return null;
2139
+ }
2140
+ }
2105
2141
  function saveClipboardImageUpload(request, options = {}) {
2106
2142
  const extension = IMAGE_EXTENSIONS.get(request.mimeType);
2107
2143
  if (!extension) {
@@ -2113,12 +2149,19 @@ function saveClipboardImageUpload(request, options = {}) {
2113
2149
  };
2114
2150
  }
2115
2151
  try {
2116
- const dataDir = options.dataDir ?? DATA_DIR;
2117
- const uploadDir = resolveSessionClipboardDir(dataDir, request.sessionId);
2118
2152
  const buffer = decodeBase64Image(request.dataBase64);
2119
2153
  const now = options.now ?? Date.now;
2120
2154
  const suffix = options.randomSuffix?.() ?? nanoid3(6);
2121
2155
  const fileName = `pasted-${formatTimestamp(now())}-${suffix}.${extension}`;
2156
+ const projectResult = trySaveProjectClipboardImage({
2157
+ cwd: options.cwd,
2158
+ sessionId: request.sessionId,
2159
+ fileName,
2160
+ buffer
2161
+ });
2162
+ if (projectResult) return projectResult;
2163
+ const dataDir = options.dataDir ?? DATA_DIR;
2164
+ const uploadDir = resolveSessionClipboardDir(dataDir, request.sessionId);
2122
2165
  const path = join5(uploadDir, fileName);
2123
2166
  mkdirSync4(uploadDir, { recursive: true });
2124
2167
  writeFileSync4(path, buffer, { mode: 384 });
@@ -2133,6 +2176,108 @@ function saveClipboardImageUpload(request, options = {}) {
2133
2176
  }
2134
2177
  }
2135
2178
 
2179
+ // src/serve/image-preview.ts
2180
+ import { readFileSync as readFileSync6, realpathSync, statSync as statSync2 } from "fs";
2181
+ import { tmpdir } from "os";
2182
+ import { isAbsolute as isAbsolute4, relative as relative2, resolve as resolve2 } from "path";
2183
+ var MAX_IMAGE_PREVIEW_BYTES = 10 * 1024 * 1024;
2184
+ function isInsideRoot(realFilePath, realRootPath) {
2185
+ const rel = relative2(realRootPath, realFilePath);
2186
+ return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
2187
+ }
2188
+ function allowedRoots(options) {
2189
+ return [options.cwd, options.tmpDir ?? tmpdir(), ...options.previewRoots ?? []].map((root) => root.trim()).filter(Boolean).flatMap((root) => {
2190
+ try {
2191
+ return [realpathSync(root)];
2192
+ } catch {
2193
+ return [];
2194
+ }
2195
+ });
2196
+ }
2197
+ function resolvePreviewPath(rawPath, options) {
2198
+ const candidate = isAbsolute4(rawPath) ? resolve2(rawPath) : resolve2(options.cwd, rawPath);
2199
+ const realCandidate = realpathSync(candidate);
2200
+ if (!allowedRoots(options).some((root) => isInsideRoot(realCandidate, root))) {
2201
+ throw Object.assign(new Error("\u56FE\u7247\u8DEF\u5F84\u4E0D\u5728\u5141\u8BB8\u9884\u89C8\u7684\u76EE\u5F55\u5185"), {
2202
+ errorCode: ControlErrorCode.INVALID_PATH
2203
+ });
2204
+ }
2205
+ return realCandidate;
2206
+ }
2207
+ function detectImageMime(buffer) {
2208
+ if (buffer.length >= 8 && buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71 && buffer[4] === 13 && buffer[5] === 10 && buffer[6] === 26 && buffer[7] === 10) {
2209
+ return "image/png";
2210
+ }
2211
+ if (buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
2212
+ return "image/jpeg";
2213
+ }
2214
+ if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
2215
+ return "image/webp";
2216
+ }
2217
+ if (buffer.length >= 6 && (buffer.subarray(0, 6).toString("ascii") === "GIF87a" || buffer.subarray(0, 6).toString("ascii") === "GIF89a")) {
2218
+ return "image/gif";
2219
+ }
2220
+ return void 0;
2221
+ }
2222
+ function errorCode(err) {
2223
+ if (err instanceof Error && "errorCode" in err && typeof err.errorCode === "string") {
2224
+ return err.errorCode;
2225
+ }
2226
+ return classifyPathError(err);
2227
+ }
2228
+ function loadImagePreview(request, options) {
2229
+ try {
2230
+ const resolvedPath = resolvePreviewPath(request.path, options);
2231
+ const stat2 = statSync2(resolvedPath);
2232
+ if (!stat2.isFile()) {
2233
+ return {
2234
+ success: false,
2235
+ sessionId: request.sessionId,
2236
+ path: request.path,
2237
+ error: "\u8DEF\u5F84\u4E0D\u662F\u56FE\u7247\u6587\u4EF6",
2238
+ errorCode: ControlErrorCode.INVALID_PATH
2239
+ };
2240
+ }
2241
+ const maxBytes = options.maxBytes ?? MAX_IMAGE_PREVIEW_BYTES;
2242
+ if (stat2.size > maxBytes) {
2243
+ return {
2244
+ success: false,
2245
+ sessionId: request.sessionId,
2246
+ path: request.path,
2247
+ error: "\u56FE\u7247\u8D85\u8FC7 10MB \u9650\u5236",
2248
+ errorCode: ControlErrorCode.UNKNOWN
2249
+ };
2250
+ }
2251
+ const buffer = readFileSync6(resolvedPath);
2252
+ const mimeType = detectImageMime(buffer);
2253
+ if (!mimeType) {
2254
+ return {
2255
+ success: false,
2256
+ sessionId: request.sessionId,
2257
+ path: request.path,
2258
+ error: "\u4E0D\u652F\u6301\u8FD9\u79CD\u56FE\u7247\u683C\u5F0F",
2259
+ errorCode: ControlErrorCode.UNKNOWN
2260
+ };
2261
+ }
2262
+ return {
2263
+ success: true,
2264
+ sessionId: request.sessionId,
2265
+ path: request.path,
2266
+ mimeType,
2267
+ dataBase64: buffer.toString("base64"),
2268
+ size: buffer.length
2269
+ };
2270
+ } catch (err) {
2271
+ return {
2272
+ success: false,
2273
+ sessionId: request.sessionId,
2274
+ path: request.path,
2275
+ error: err instanceof Error ? err.message : String(err),
2276
+ errorCode: errorCode(err)
2277
+ };
2278
+ }
2279
+ }
2280
+
2136
2281
  // src/serve/pty-input.ts
2137
2282
  function serializeRawPtyInput(sessionId, data) {
2138
2283
  return serializeIpc({ type: "pty_input", sessionId, data });
@@ -2226,12 +2371,17 @@ var RelayInputHandlers = class {
2226
2371
  serviceLogger.warn({ sessionId }, "Clipboard image upload rejected: session not found");
2227
2372
  return;
2228
2373
  }
2229
- const result = saveClipboardImageUpload({
2230
- sessionId,
2231
- mimeType: typeof msg.mimeType === "string" ? msg.mimeType : "",
2232
- dataBase64: typeof msg.dataBase64 === "string" ? msg.dataBase64 : "",
2233
- fileName: typeof msg.fileName === "string" ? msg.fileName : void 0
2234
- });
2374
+ const result = saveClipboardImageUpload(
2375
+ {
2376
+ sessionId,
2377
+ mimeType: typeof msg.mimeType === "string" ? msg.mimeType : "",
2378
+ dataBase64: typeof msg.dataBase64 === "string" ? msg.dataBase64 : "",
2379
+ fileName: typeof msg.fileName === "string" ? msg.fileName : void 0
2380
+ },
2381
+ {
2382
+ cwd: session.cwd
2383
+ }
2384
+ );
2235
2385
  this.deps.relayConnection.sendRaw(
2236
2386
  JSON.stringify({
2237
2387
  type: "clipboard_image_upload_response",
@@ -2242,6 +2392,43 @@ var RelayInputHandlers = class {
2242
2392
  );
2243
2393
  serviceLogger.info({ sessionId, success: result.success }, "Clipboard image upload handled");
2244
2394
  }
2395
+ onImagePreviewRequest(msg) {
2396
+ const sessionId = msg.sessionId;
2397
+ const requestId = msg.requestId;
2398
+ const path = msg.path;
2399
+ if (!sessionId || !path) return;
2400
+ const session = this.deps.sessionManager.getSession(sessionId);
2401
+ if (!session) {
2402
+ this.deps.relayConnection.sendRaw(
2403
+ JSON.stringify({
2404
+ type: "image_preview_response",
2405
+ requestId,
2406
+ sessionId,
2407
+ success: false,
2408
+ path,
2409
+ error: "\u4F1A\u8BDD\u4E0D\u5B58\u5728",
2410
+ errorCode: ControlErrorCode.SESSION_NOT_FOUND
2411
+ })
2412
+ );
2413
+ serviceLogger.warn({ sessionId }, "Image preview rejected: session not found");
2414
+ return;
2415
+ }
2416
+ const result = loadImagePreview(
2417
+ { sessionId, path },
2418
+ {
2419
+ cwd: session.cwd,
2420
+ previewRoots: this.deps.previewRoots
2421
+ }
2422
+ );
2423
+ this.deps.relayConnection.sendRaw(
2424
+ JSON.stringify({
2425
+ type: "image_preview_response",
2426
+ requestId,
2427
+ ...result
2428
+ })
2429
+ );
2430
+ serviceLogger.info({ sessionId, success: result.success }, "Image preview handled");
2431
+ }
2245
2432
  };
2246
2433
 
2247
2434
  // src/serve/relay-history-handlers.ts
@@ -2441,14 +2628,14 @@ var RelayPermissionHandlers = class {
2441
2628
 
2442
2629
  // src/serve/relay-resource-handlers.ts
2443
2630
  import { homedir as homedir4 } from "os";
2444
- import { accessSync, constants, statSync } from "fs";
2631
+ import { accessSync, constants, statSync as statSync3 } from "fs";
2445
2632
  function errorMessage(err) {
2446
2633
  return err instanceof Error ? err.message : String(err);
2447
2634
  }
2448
2635
  function validateExecutablePath(path) {
2449
2636
  const normalized = path.trim();
2450
2637
  if (!normalized.startsWith("/")) throw new Error("CLI \u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
2451
- const stat2 = statSync(normalized);
2638
+ const stat2 = statSync3(normalized);
2452
2639
  if (!stat2.isFile()) throw new Error("CLI \u8DEF\u5F84\u4E0D\u662F\u53EF\u6267\u884C\u6587\u4EF6");
2453
2640
  accessSync(normalized, constants.X_OK);
2454
2641
  return normalized;
@@ -2557,8 +2744,8 @@ var RelayResourceHandlers = class {
2557
2744
  };
2558
2745
 
2559
2746
  // src/serve/relay-session-create-handler.ts
2560
- import { rmSync, statSync as statSync2 } from "fs";
2561
- import { isAbsolute as isAbsolute4 } from "path";
2747
+ import { rmSync, statSync as statSync4 } from "fs";
2748
+ import { isAbsolute as isAbsolute5 } from "path";
2562
2749
  import { nanoid as nanoid4 } from "nanoid";
2563
2750
 
2564
2751
  // src/serve/hosted-pty-registry.ts
@@ -2848,11 +3035,11 @@ function validateSessionCwd(cwd) {
2848
3035
  return { message: "\u8BF7\u8F93\u5165\u5DE5\u4F5C\u76EE\u5F55", code: ControlErrorCode.INVALID_PATH };
2849
3036
  }
2850
3037
  const trimmed = cwd.trim();
2851
- if (!isAbsolute4(trimmed)) {
3038
+ if (!isAbsolute5(trimmed)) {
2852
3039
  return { message: "\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84", code: ControlErrorCode.INVALID_PATH };
2853
3040
  }
2854
3041
  try {
2855
- const stat2 = statSync2(trimmed);
3042
+ const stat2 = statSync4(trimmed);
2856
3043
  return stat2.isDirectory() ? null : { message: "\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u76EE\u5F55", code: ControlErrorCode.PATH_NOT_DIRECTORY };
2857
3044
  } catch (err) {
2858
3045
  return {
@@ -3101,7 +3288,8 @@ var RelayRouter = class {
3101
3288
  relayConnection: deps.relayConnection,
3102
3289
  terminalSockets: deps.terminalSockets,
3103
3290
  hostedPtyRegistry: deps.hostedPtyRegistry,
3104
- jsonObserver: deps.jsonObserver
3291
+ jsonObserver: deps.jsonObserver,
3292
+ previewRoots: deps.getPreviewRoots?.()
3105
3293
  });
3106
3294
  this.resourceHandlers = new RelayResourceHandlers({
3107
3295
  relaySend: deps.relaySend,
@@ -3159,6 +3347,7 @@ var RelayRouter = class {
3159
3347
  user_input: (msg) => this.inputHandlers.onUserInput(msg),
3160
3348
  remote_input_raw: (msg) => this.inputHandlers.onRemoteInputRaw(msg),
3161
3349
  clipboard_image_upload: (msg) => this.inputHandlers.onClipboardImageUpload(msg),
3350
+ image_preview_request: (msg) => this.inputHandlers.onImagePreviewRequest(msg),
3162
3351
  tool_approve: (msg) => this.permissionHandlers.onToolApprove(msg),
3163
3352
  tool_deny: (msg) => this.permissionHandlers.onToolDeny(msg),
3164
3353
  proxy_info_request: (msg) => this.resourceHandlers.onProxyInfoRequest(msg),
@@ -3349,11 +3538,11 @@ var PermissionBroker = class {
3349
3538
  message: "Duplicate permission request id."
3350
3539
  });
3351
3540
  }
3352
- return new Promise((resolve2) => {
3541
+ return new Promise((resolve3) => {
3353
3542
  this.pending.set(request.requestId, {
3354
3543
  ...request,
3355
3544
  source: "hook",
3356
- resolve: resolve2,
3545
+ resolve: resolve3,
3357
3546
  createdAt: Date.now()
3358
3547
  });
3359
3548
  });
@@ -3648,14 +3837,14 @@ function touchSessionActivity(sessionManager, relay, sessionId, now = Date.now()
3648
3837
 
3649
3838
  // src/serve/service-files.ts
3650
3839
  import { execSync } from "child_process";
3651
- import { existsSync as existsSync5, readFileSync as readFileSync5, unlinkSync as unlinkSync2 } from "fs";
3840
+ import { existsSync as existsSync6, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
3652
3841
  import { hostname } from "os";
3653
3842
  import { connect as connect2 } from "net";
3654
3843
  function tryConnectSocket(sockPath) {
3655
- return new Promise((resolve2) => {
3844
+ return new Promise((resolve3) => {
3656
3845
  const s = connect2(sockPath);
3657
- s.on("connect", () => resolve2(s));
3658
- s.on("error", () => resolve2(null));
3846
+ s.on("connect", () => resolve3(s));
3847
+ s.on("error", () => resolve3(null));
3659
3848
  });
3660
3849
  }
3661
3850
  function isProcessAlive(pid) {
@@ -3667,7 +3856,7 @@ function isProcessAlive(pid) {
3667
3856
  }
3668
3857
  }
3669
3858
  async function cleanupStaleResources() {
3670
- if (existsSync5(SOCK_PATH)) {
3859
+ if (existsSync6(SOCK_PATH)) {
3671
3860
  const existing = await tryConnectSocket(SOCK_PATH);
3672
3861
  if (existing) {
3673
3862
  existing.destroy();
@@ -3679,8 +3868,8 @@ async function cleanupStaleResources() {
3679
3868
  unlinkSync2(SOCK_PATH);
3680
3869
  serviceLogger.info("Removed stale socket file");
3681
3870
  }
3682
- if (existsSync5(PID_PATH)) {
3683
- const pidStr = readFileSync5(PID_PATH, "utf-8").trim();
3871
+ if (existsSync6(PID_PATH)) {
3872
+ const pidStr = readFileSync7(PID_PATH, "utf-8").trim();
3684
3873
  const pid = parseInt(pidStr, 10);
3685
3874
  if (!isNaN(pid) && isProcessAlive(pid)) {
3686
3875
  const msg = `Another service is already running with PID ${pid}`;
@@ -4055,7 +4244,7 @@ function handleTerminalConnection(socket, deps) {
4055
4244
 
4056
4245
  // src/serve/hook-registry.ts
4057
4246
  import { createHash, randomBytes } from "crypto";
4058
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
4247
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
4059
4248
  import { dirname as dirname4 } from "path";
4060
4249
  import { z } from "zod";
4061
4250
  var PersistedHookSessionBindingSchema = z.object({
@@ -4116,10 +4305,10 @@ var HookRegistry = class {
4116
4305
  }
4117
4306
  }
4118
4307
  load() {
4119
- if (!this.persistPath || !existsSync6(this.persistPath)) return;
4308
+ if (!this.persistPath || !existsSync7(this.persistPath)) return;
4120
4309
  try {
4121
4310
  const parsed = PersistedHookRegistrySchema.parse(
4122
- JSON.parse(readFileSync6(this.persistPath, "utf8"))
4311
+ JSON.parse(readFileSync8(this.persistPath, "utf8"))
4123
4312
  );
4124
4313
  this.bindingsBySession.clear();
4125
4314
  for (const binding of parsed.bindings) {
@@ -4189,7 +4378,7 @@ var HookServer = class {
4189
4378
  this.writeJson(res, 500, { error: "internal_error" });
4190
4379
  });
4191
4380
  });
4192
- return new Promise((resolve2, reject) => {
4381
+ return new Promise((resolve3, reject) => {
4193
4382
  const onError = (err) => {
4194
4383
  this.server?.off("listening", onListening);
4195
4384
  reject(err);
@@ -4197,7 +4386,7 @@ var HookServer = class {
4197
4386
  const onListening = () => {
4198
4387
  this.server?.off("error", onError);
4199
4388
  serviceLogger.info({ host: this.host, port: this.options.port }, "Hook server listening");
4200
- resolve2();
4389
+ resolve3();
4201
4390
  };
4202
4391
  this.server.once("error", onError);
4203
4392
  this.server.once("listening", onListening);
@@ -4208,8 +4397,8 @@ var HookServer = class {
4208
4397
  if (!this.server) return Promise.resolve();
4209
4398
  const server = this.server;
4210
4399
  this.server = null;
4211
- return new Promise((resolve2, reject) => {
4212
- server.close((err) => err ? reject(err) : resolve2());
4400
+ return new Promise((resolve3, reject) => {
4401
+ server.close((err) => err ? reject(err) : resolve3());
4213
4402
  });
4214
4403
  }
4215
4404
  getListeningPort() {
@@ -4323,7 +4512,7 @@ var HookServer = class {
4323
4512
  this.writeJson(res, 200, payload);
4324
4513
  }
4325
4514
  readBody(req) {
4326
- return new Promise((resolve2, reject) => {
4515
+ return new Promise((resolve3, reject) => {
4327
4516
  let body = "";
4328
4517
  let size = 0;
4329
4518
  req.setEncoding("utf8");
@@ -4336,7 +4525,7 @@ var HookServer = class {
4336
4525
  }
4337
4526
  body += chunk;
4338
4527
  });
4339
- req.on("end", () => resolve2(body));
4528
+ req.on("end", () => resolve3(body));
4340
4529
  req.on("error", reject);
4341
4530
  });
4342
4531
  }
@@ -4484,6 +4673,7 @@ async function startService(options) {
4484
4673
  let proxyConfig = loadConfig({ relayName: options?.relayName });
4485
4674
  const getProviderEnv = () => buildProviderEnv(proxyConfig, process.env);
4486
4675
  const getAgentCliSuggestions = () => proxyConfig.agentCliSuggestions;
4676
+ const getPreviewRoots = () => proxyConfig.previewRoots;
4487
4677
  const setAgentCliPath = (provider, path) => {
4488
4678
  const field = provider === "claude" ? "claudeBin" : "codexBin";
4489
4679
  const existing = proxyConfig.agentCliSuggestions[provider] ?? [];
@@ -4611,7 +4801,8 @@ async function startService(options) {
4611
4801
  agentStatusRegistry,
4612
4802
  getProviderEnv,
4613
4803
  getAgentCliSuggestions,
4614
- setAgentCliPath
4804
+ setAgentCliPath,
4805
+ getPreviewRoots
4615
4806
  });
4616
4807
  relayConnection.on("message", (msg) => relayRouter.handle(msg));
4617
4808
  relayConnection.on("connected", () => {