@dev-anywhere/proxy 0.1.2 → 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。
@@ -2175,6 +2176,108 @@ function saveClipboardImageUpload(request, options = {}) {
2175
2176
  }
2176
2177
  }
2177
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
+
2178
2281
  // src/serve/pty-input.ts
2179
2282
  function serializeRawPtyInput(sessionId, data) {
2180
2283
  return serializeIpc({ type: "pty_input", sessionId, data });
@@ -2289,6 +2392,43 @@ var RelayInputHandlers = class {
2289
2392
  );
2290
2393
  serviceLogger.info({ sessionId, success: result.success }, "Clipboard image upload handled");
2291
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
+ }
2292
2432
  };
2293
2433
 
2294
2434
  // src/serve/relay-history-handlers.ts
@@ -2488,14 +2628,14 @@ var RelayPermissionHandlers = class {
2488
2628
 
2489
2629
  // src/serve/relay-resource-handlers.ts
2490
2630
  import { homedir as homedir4 } from "os";
2491
- import { accessSync, constants, statSync as statSync2 } from "fs";
2631
+ import { accessSync, constants, statSync as statSync3 } from "fs";
2492
2632
  function errorMessage(err) {
2493
2633
  return err instanceof Error ? err.message : String(err);
2494
2634
  }
2495
2635
  function validateExecutablePath(path) {
2496
2636
  const normalized = path.trim();
2497
2637
  if (!normalized.startsWith("/")) throw new Error("CLI \u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
2498
- const stat2 = statSync2(normalized);
2638
+ const stat2 = statSync3(normalized);
2499
2639
  if (!stat2.isFile()) throw new Error("CLI \u8DEF\u5F84\u4E0D\u662F\u53EF\u6267\u884C\u6587\u4EF6");
2500
2640
  accessSync(normalized, constants.X_OK);
2501
2641
  return normalized;
@@ -2604,8 +2744,8 @@ var RelayResourceHandlers = class {
2604
2744
  };
2605
2745
 
2606
2746
  // src/serve/relay-session-create-handler.ts
2607
- import { rmSync, statSync as statSync3 } from "fs";
2608
- import { isAbsolute as isAbsolute4 } from "path";
2747
+ import { rmSync, statSync as statSync4 } from "fs";
2748
+ import { isAbsolute as isAbsolute5 } from "path";
2609
2749
  import { nanoid as nanoid4 } from "nanoid";
2610
2750
 
2611
2751
  // src/serve/hosted-pty-registry.ts
@@ -2895,11 +3035,11 @@ function validateSessionCwd(cwd) {
2895
3035
  return { message: "\u8BF7\u8F93\u5165\u5DE5\u4F5C\u76EE\u5F55", code: ControlErrorCode.INVALID_PATH };
2896
3036
  }
2897
3037
  const trimmed = cwd.trim();
2898
- if (!isAbsolute4(trimmed)) {
3038
+ if (!isAbsolute5(trimmed)) {
2899
3039
  return { message: "\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84", code: ControlErrorCode.INVALID_PATH };
2900
3040
  }
2901
3041
  try {
2902
- const stat2 = statSync3(trimmed);
3042
+ const stat2 = statSync4(trimmed);
2903
3043
  return stat2.isDirectory() ? null : { message: "\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u76EE\u5F55", code: ControlErrorCode.PATH_NOT_DIRECTORY };
2904
3044
  } catch (err) {
2905
3045
  return {
@@ -3148,7 +3288,8 @@ var RelayRouter = class {
3148
3288
  relayConnection: deps.relayConnection,
3149
3289
  terminalSockets: deps.terminalSockets,
3150
3290
  hostedPtyRegistry: deps.hostedPtyRegistry,
3151
- jsonObserver: deps.jsonObserver
3291
+ jsonObserver: deps.jsonObserver,
3292
+ previewRoots: deps.getPreviewRoots?.()
3152
3293
  });
3153
3294
  this.resourceHandlers = new RelayResourceHandlers({
3154
3295
  relaySend: deps.relaySend,
@@ -3206,6 +3347,7 @@ var RelayRouter = class {
3206
3347
  user_input: (msg) => this.inputHandlers.onUserInput(msg),
3207
3348
  remote_input_raw: (msg) => this.inputHandlers.onRemoteInputRaw(msg),
3208
3349
  clipboard_image_upload: (msg) => this.inputHandlers.onClipboardImageUpload(msg),
3350
+ image_preview_request: (msg) => this.inputHandlers.onImagePreviewRequest(msg),
3209
3351
  tool_approve: (msg) => this.permissionHandlers.onToolApprove(msg),
3210
3352
  tool_deny: (msg) => this.permissionHandlers.onToolDeny(msg),
3211
3353
  proxy_info_request: (msg) => this.resourceHandlers.onProxyInfoRequest(msg),
@@ -3396,11 +3538,11 @@ var PermissionBroker = class {
3396
3538
  message: "Duplicate permission request id."
3397
3539
  });
3398
3540
  }
3399
- return new Promise((resolve2) => {
3541
+ return new Promise((resolve3) => {
3400
3542
  this.pending.set(request.requestId, {
3401
3543
  ...request,
3402
3544
  source: "hook",
3403
- resolve: resolve2,
3545
+ resolve: resolve3,
3404
3546
  createdAt: Date.now()
3405
3547
  });
3406
3548
  });
@@ -3695,14 +3837,14 @@ function touchSessionActivity(sessionManager, relay, sessionId, now = Date.now()
3695
3837
 
3696
3838
  // src/serve/service-files.ts
3697
3839
  import { execSync } from "child_process";
3698
- import { existsSync as existsSync6, readFileSync as readFileSync6, unlinkSync as unlinkSync2 } from "fs";
3840
+ import { existsSync as existsSync6, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
3699
3841
  import { hostname } from "os";
3700
3842
  import { connect as connect2 } from "net";
3701
3843
  function tryConnectSocket(sockPath) {
3702
- return new Promise((resolve2) => {
3844
+ return new Promise((resolve3) => {
3703
3845
  const s = connect2(sockPath);
3704
- s.on("connect", () => resolve2(s));
3705
- s.on("error", () => resolve2(null));
3846
+ s.on("connect", () => resolve3(s));
3847
+ s.on("error", () => resolve3(null));
3706
3848
  });
3707
3849
  }
3708
3850
  function isProcessAlive(pid) {
@@ -3727,7 +3869,7 @@ async function cleanupStaleResources() {
3727
3869
  serviceLogger.info("Removed stale socket file");
3728
3870
  }
3729
3871
  if (existsSync6(PID_PATH)) {
3730
- const pidStr = readFileSync6(PID_PATH, "utf-8").trim();
3872
+ const pidStr = readFileSync7(PID_PATH, "utf-8").trim();
3731
3873
  const pid = parseInt(pidStr, 10);
3732
3874
  if (!isNaN(pid) && isProcessAlive(pid)) {
3733
3875
  const msg = `Another service is already running with PID ${pid}`;
@@ -4102,7 +4244,7 @@ function handleTerminalConnection(socket, deps) {
4102
4244
 
4103
4245
  // src/serve/hook-registry.ts
4104
4246
  import { createHash, randomBytes } from "crypto";
4105
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync7, 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";
4106
4248
  import { dirname as dirname4 } from "path";
4107
4249
  import { z } from "zod";
4108
4250
  var PersistedHookSessionBindingSchema = z.object({
@@ -4166,7 +4308,7 @@ var HookRegistry = class {
4166
4308
  if (!this.persistPath || !existsSync7(this.persistPath)) return;
4167
4309
  try {
4168
4310
  const parsed = PersistedHookRegistrySchema.parse(
4169
- JSON.parse(readFileSync7(this.persistPath, "utf8"))
4311
+ JSON.parse(readFileSync8(this.persistPath, "utf8"))
4170
4312
  );
4171
4313
  this.bindingsBySession.clear();
4172
4314
  for (const binding of parsed.bindings) {
@@ -4236,7 +4378,7 @@ var HookServer = class {
4236
4378
  this.writeJson(res, 500, { error: "internal_error" });
4237
4379
  });
4238
4380
  });
4239
- return new Promise((resolve2, reject) => {
4381
+ return new Promise((resolve3, reject) => {
4240
4382
  const onError = (err) => {
4241
4383
  this.server?.off("listening", onListening);
4242
4384
  reject(err);
@@ -4244,7 +4386,7 @@ var HookServer = class {
4244
4386
  const onListening = () => {
4245
4387
  this.server?.off("error", onError);
4246
4388
  serviceLogger.info({ host: this.host, port: this.options.port }, "Hook server listening");
4247
- resolve2();
4389
+ resolve3();
4248
4390
  };
4249
4391
  this.server.once("error", onError);
4250
4392
  this.server.once("listening", onListening);
@@ -4255,8 +4397,8 @@ var HookServer = class {
4255
4397
  if (!this.server) return Promise.resolve();
4256
4398
  const server = this.server;
4257
4399
  this.server = null;
4258
- return new Promise((resolve2, reject) => {
4259
- server.close((err) => err ? reject(err) : resolve2());
4400
+ return new Promise((resolve3, reject) => {
4401
+ server.close((err) => err ? reject(err) : resolve3());
4260
4402
  });
4261
4403
  }
4262
4404
  getListeningPort() {
@@ -4370,7 +4512,7 @@ var HookServer = class {
4370
4512
  this.writeJson(res, 200, payload);
4371
4513
  }
4372
4514
  readBody(req) {
4373
- return new Promise((resolve2, reject) => {
4515
+ return new Promise((resolve3, reject) => {
4374
4516
  let body = "";
4375
4517
  let size = 0;
4376
4518
  req.setEncoding("utf8");
@@ -4383,7 +4525,7 @@ var HookServer = class {
4383
4525
  }
4384
4526
  body += chunk;
4385
4527
  });
4386
- req.on("end", () => resolve2(body));
4528
+ req.on("end", () => resolve3(body));
4387
4529
  req.on("error", reject);
4388
4530
  });
4389
4531
  }
@@ -4531,6 +4673,7 @@ async function startService(options) {
4531
4673
  let proxyConfig = loadConfig({ relayName: options?.relayName });
4532
4674
  const getProviderEnv = () => buildProviderEnv(proxyConfig, process.env);
4533
4675
  const getAgentCliSuggestions = () => proxyConfig.agentCliSuggestions;
4676
+ const getPreviewRoots = () => proxyConfig.previewRoots;
4534
4677
  const setAgentCliPath = (provider, path) => {
4535
4678
  const field = provider === "claude" ? "claudeBin" : "codexBin";
4536
4679
  const existing = proxyConfig.agentCliSuggestions[provider] ?? [];
@@ -4658,7 +4801,8 @@ async function startService(options) {
4658
4801
  agentStatusRegistry,
4659
4802
  getProviderEnv,
4660
4803
  getAgentCliSuggestions,
4661
- setAgentCliPath
4804
+ setAgentCliPath,
4805
+ getPreviewRoots
4662
4806
  });
4663
4807
  relayConnection.on("message", (msg) => relayRouter.handle(msg));
4664
4808
  relayConnection.on("connected", () => {