@dev-anywhere/proxy 0.2.0 → 0.2.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.
package/dist/serve.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  StreamJsonEventSchema,
7
7
  disposeSeqCounter,
8
8
  getSeqCounterFor
9
- } from "./chunk-4YQ2JUM7.js";
9
+ } from "./chunk-ENA4NCWK.js";
10
10
  import {
11
11
  decidePtySemanticTransition,
12
12
  extractOscSequences,
@@ -35,7 +35,7 @@ import {
35
35
  serializeControl,
36
36
  serializeIpc,
37
37
  serializeWorkerMsg
38
- } from "./chunk-NBRBO5GS.js";
38
+ } from "./chunk-NZZXBVO2.js";
39
39
  import {
40
40
  buildProviderEnv,
41
41
  loadConfig,
@@ -65,7 +65,7 @@ import {
65
65
 
66
66
  // src/serve.ts
67
67
  import { createServer as createServer2 } from "net";
68
- import { unlinkSync as unlinkSync4, writeFileSync as writeFileSync3, chmodSync, rmSync as rmSync2 } from "fs";
68
+ import { unlinkSync as unlinkSync4, writeFileSync as writeFileSync4, chmodSync, rmSync as rmSync2 } from "fs";
69
69
 
70
70
  // src/serve/session-manager.ts
71
71
  import { readFileSync, existsSync } from "fs";
@@ -219,7 +219,18 @@ var SessionManager = class {
219
219
  this.sessions.delete(id);
220
220
  this.save();
221
221
  serviceLogger.info({ sessionId: id, mode: session.mode, pid }, "Session terminated");
222
- this.onSessionRemoved?.(id, context);
222
+ try {
223
+ this.onSessionRemoved?.(id, context);
224
+ } catch (err) {
225
+ const error = err instanceof Error ? err : new Error(String(err));
226
+ serviceLogger.warn(
227
+ {
228
+ sessionId: id,
229
+ err: { message: error.message, stack: error.stack, cause: error.cause }
230
+ },
231
+ "onSessionRemoved callback threw; session already removed from registry"
232
+ );
233
+ }
223
234
  return { success: true, pid };
224
235
  }
225
236
  terminateAll() {
@@ -957,7 +968,7 @@ function extractConversationText(msg) {
957
968
  return null;
958
969
  }
959
970
  async function extractTitleAndCwd(filePath) {
960
- return new Promise((resolve3) => {
971
+ return new Promise((resolve5) => {
961
972
  const rl = createInterface({
962
973
  input: createReadStream(filePath, { encoding: "utf-8" }),
963
974
  crlfDelay: Infinity
@@ -985,10 +996,10 @@ async function extractTitleAndCwd(filePath) {
985
996
  }
986
997
  });
987
998
  rl.on("close", () => {
988
- if (!resolved) resolve3({ title, cwd });
989
- else resolve3({ title, cwd });
999
+ if (!resolved) resolve5({ title, cwd });
1000
+ else resolve5({ title, cwd });
990
1001
  });
991
- rl.on("error", () => resolve3({ title, cwd }));
1002
+ rl.on("error", () => resolve5({ title, cwd }));
992
1003
  });
993
1004
  }
994
1005
  async function collectJsonlFiles(root) {
@@ -1010,7 +1021,7 @@ async function collectJsonlFiles(root) {
1010
1021
  return files;
1011
1022
  }
1012
1023
  async function extractCodexTitleAndCwd(filePath) {
1013
- return new Promise((resolve3) => {
1024
+ return new Promise((resolve5) => {
1014
1025
  const rl = createInterface({
1015
1026
  input: createReadStream(filePath, { encoding: "utf-8" }),
1016
1027
  crlfDelay: Infinity
@@ -1034,8 +1045,8 @@ async function extractCodexTitleAndCwd(filePath) {
1034
1045
  } catch {
1035
1046
  }
1036
1047
  });
1037
- rl.on("close", () => resolve3({ id, title, cwd }));
1038
- rl.on("error", () => resolve3({ id, title, cwd }));
1048
+ rl.on("close", () => resolve5({ id, title, cwd }));
1049
+ rl.on("error", () => resolve5({ id, title, cwd }));
1039
1050
  });
1040
1051
  }
1041
1052
  function extractCodexUserText(payload) {
@@ -1600,7 +1611,7 @@ var WorkerRegistry = class {
1600
1611
  return workerPid;
1601
1612
  }
1602
1613
  connect(sessionId, sockPath) {
1603
- return new Promise((resolve3) => {
1614
+ return new Promise((resolve5) => {
1604
1615
  const sock = connect(sockPath);
1605
1616
  sock.on("connect", () => {
1606
1617
  this.sockets.set(sessionId, sock);
@@ -1616,9 +1627,9 @@ var WorkerRegistry = class {
1616
1627
  );
1617
1628
  sock.on("close", () => this.onDisconnect(sessionId));
1618
1629
  sock.on("error", () => this.onDisconnect(sessionId));
1619
- resolve3(sock);
1630
+ resolve5(sock);
1620
1631
  });
1621
- sock.on("error", () => resolve3(null));
1632
+ sock.on("error", () => resolve5(null));
1622
1633
  });
1623
1634
  }
1624
1635
  // 枚举 DATA_DIR 下所有 session 目录,尝试连接存活的 worker.sock;失败则清理 stale socket。
@@ -2034,7 +2045,6 @@ function saveClipboardImageUpload(request, options = {}) {
2034
2045
  if (!extension) {
2035
2046
  return {
2036
2047
  success: false,
2037
- path: "",
2038
2048
  error: "\u4E0D\u652F\u6301\u8FD9\u79CD\u56FE\u7247\u683C\u5F0F",
2039
2049
  errorCode: ControlErrorCode.UNKNOWN
2040
2050
  };
@@ -2060,34 +2070,228 @@ function saveClipboardImageUpload(request, options = {}) {
2060
2070
  } catch (err) {
2061
2071
  return {
2062
2072
  success: false,
2063
- path: "",
2064
2073
  error: err instanceof Error ? err.message : String(err),
2065
2074
  errorCode: ControlErrorCode.UNKNOWN
2066
2075
  };
2067
2076
  }
2068
2077
  }
2069
2078
 
2070
- // src/serve/image-preview.ts
2079
+ // src/serve/file-download.ts
2071
2080
  import { readFileSync as readFileSync5, realpathSync, statSync as statSync2 } from "fs";
2081
+ import { extname, isAbsolute as isAbsolute3, resolve as resolve2 } from "path";
2082
+ var MAX_FILE_DOWNLOAD_BYTES = 100 * 1024 * 1024;
2083
+ var EXT_MIME_MAP = {
2084
+ ".png": "image/png",
2085
+ ".jpg": "image/jpeg",
2086
+ ".jpeg": "image/jpeg",
2087
+ ".gif": "image/gif",
2088
+ ".webp": "image/webp",
2089
+ ".svg": "image/svg+xml",
2090
+ ".bmp": "image/bmp",
2091
+ ".pdf": "application/pdf",
2092
+ ".json": "application/json",
2093
+ ".xml": "application/xml",
2094
+ ".html": "text/html",
2095
+ ".htm": "text/html",
2096
+ ".txt": "text/plain",
2097
+ ".md": "text/markdown",
2098
+ ".log": "text/plain",
2099
+ ".csv": "text/csv",
2100
+ ".js": "application/javascript",
2101
+ ".mjs": "application/javascript",
2102
+ ".ts": "application/typescript",
2103
+ ".tsx": "application/typescript",
2104
+ ".zip": "application/zip",
2105
+ ".tar": "application/x-tar",
2106
+ ".gz": "application/gzip",
2107
+ ".mp4": "video/mp4",
2108
+ ".mp3": "audio/mpeg",
2109
+ ".wav": "audio/wav"
2110
+ };
2111
+ function guessMimeType(filePath) {
2112
+ const ext = extname(filePath).toLowerCase();
2113
+ return EXT_MIME_MAP[ext] ?? "application/octet-stream";
2114
+ }
2115
+ function resolveDownloadPath(rawPath, cwd) {
2116
+ const candidate = isAbsolute3(rawPath) ? resolve2(rawPath) : resolve2(cwd, rawPath);
2117
+ return realpathSync(candidate);
2118
+ }
2119
+ function errorCode(err) {
2120
+ if (err instanceof Error && "errorCode" in err && typeof err.errorCode === "string") {
2121
+ return err.errorCode;
2122
+ }
2123
+ return classifyPathError(err);
2124
+ }
2125
+ function loadFileDownload(request, options) {
2126
+ try {
2127
+ const resolvedPath = resolveDownloadPath(request.path, options.cwd);
2128
+ const stat2 = statSync2(resolvedPath);
2129
+ if (!stat2.isFile()) {
2130
+ return {
2131
+ success: false,
2132
+ sessionId: request.sessionId,
2133
+ path: request.path,
2134
+ error: "\u8DEF\u5F84\u4E0D\u662F\u666E\u901A\u6587\u4EF6",
2135
+ errorCode: ControlErrorCode.INVALID_PATH
2136
+ };
2137
+ }
2138
+ const maxBytes = options.maxBytes ?? MAX_FILE_DOWNLOAD_BYTES;
2139
+ if (stat2.size > maxBytes) {
2140
+ return {
2141
+ success: false,
2142
+ sessionId: request.sessionId,
2143
+ path: request.path,
2144
+ error: `\u6587\u4EF6\u8D85\u8FC7 ${Math.round(maxBytes / 1024 / 1024)}MB \u9650\u5236`,
2145
+ errorCode: ControlErrorCode.UNKNOWN
2146
+ };
2147
+ }
2148
+ const buffer = readFileSync5(resolvedPath);
2149
+ return {
2150
+ success: true,
2151
+ sessionId: request.sessionId,
2152
+ path: request.path,
2153
+ mimeType: guessMimeType(resolvedPath),
2154
+ dataBase64: buffer.toString("base64"),
2155
+ size: buffer.length
2156
+ };
2157
+ } catch (err) {
2158
+ return {
2159
+ success: false,
2160
+ sessionId: request.sessionId,
2161
+ path: request.path,
2162
+ error: err instanceof Error ? err.message : String(err),
2163
+ errorCode: errorCode(err)
2164
+ };
2165
+ }
2166
+ }
2167
+
2168
+ // src/serve/file-upload.ts
2169
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
2170
+ import { basename, isAbsolute as isAbsolute4, join as join6, relative as relative2, resolve as resolve3 } from "path";
2171
+ import { nanoid as nanoid4 } from "nanoid";
2172
+ var MAX_FILE_UPLOAD_BYTES = 100 * 1024 * 1024;
2173
+ var MAX_FILE_UPLOAD_BASE64_LENGTH = Math.ceil(MAX_FILE_UPLOAD_BYTES / 3) * 4;
2174
+ var SAFE_FILENAME_RE = /^[A-Za-z0-9._-]+$/;
2175
+ function normalizeBase642(input) {
2176
+ return input.replace(/^data:[^;]+;base64,/i, "").replace(/\s/g, "");
2177
+ }
2178
+ function decodeBase64File(dataBase64) {
2179
+ const normalized = normalizeBase642(dataBase64);
2180
+ if (normalized.length > MAX_FILE_UPLOAD_BASE64_LENGTH) {
2181
+ throw new Error("\u6587\u4EF6\u8D85\u8FC7 100MB \u9650\u5236");
2182
+ }
2183
+ if (!normalized || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
2184
+ throw new Error("\u6587\u4EF6\u6570\u636E\u4E0D\u662F\u6709\u6548\u7684 base64");
2185
+ }
2186
+ const buffer = Buffer.from(normalized, "base64");
2187
+ if (buffer.length === 0) throw new Error("\u6587\u4EF6\u6570\u636E\u4E3A\u7A7A");
2188
+ if (buffer.length > MAX_FILE_UPLOAD_BYTES) {
2189
+ throw new Error("\u6587\u4EF6\u8D85\u8FC7 100MB \u9650\u5236");
2190
+ }
2191
+ return buffer;
2192
+ }
2193
+ function sanitizeFileName(fileName, fallbackPrefix, suffix) {
2194
+ const base = basename(fileName).trim();
2195
+ if (base && SAFE_FILENAME_RE.test(base)) return base;
2196
+ const extMatch = base.match(/\.([A-Za-z0-9]{1,6})$/);
2197
+ const ext = extMatch ? `.${extMatch[1]}` : "";
2198
+ return `${fallbackPrefix}-${suffix}${ext}`;
2199
+ }
2200
+ function resolveChildDir2(rootPath, ...segments) {
2201
+ const root = resolve3(rootPath);
2202
+ const target = resolve3(root, ...segments);
2203
+ const rel = relative2(root, target);
2204
+ if (!rel || rel.startsWith("..") || isAbsolute4(rel)) {
2205
+ throw new Error("\u4F1A\u8BDD\u8DEF\u5F84\u65E0\u6548");
2206
+ }
2207
+ return target;
2208
+ }
2209
+ function normalizeGitignoreLine2(line) {
2210
+ return line.trim().replace(/^\/+/, "").replace(/\/+$/, "");
2211
+ }
2212
+ function ensureProjectUploadIgnored(cwd) {
2213
+ const gitignorePath = join6(cwd, ".gitignore");
2214
+ if (!existsSync5(gitignorePath)) return;
2215
+ try {
2216
+ const current = readFileSync6(gitignorePath, "utf-8");
2217
+ const alreadyIgnored = current.split(/\r?\n/).some((line) => normalizeGitignoreLine2(line) === ".dev-anywhere");
2218
+ if (alreadyIgnored) return;
2219
+ const separator = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
2220
+ writeFileSync2(gitignorePath, `${current}${separator}.dev-anywhere/
2221
+ `);
2222
+ } catch {
2223
+ }
2224
+ }
2225
+ function trySaveProjectUpload(options) {
2226
+ if (!options.cwd) return null;
2227
+ try {
2228
+ const cwd = resolve3(options.cwd);
2229
+ if (!statSync3(cwd).isDirectory()) return null;
2230
+ const uploadsRoot = resolve3(cwd, ".dev-anywhere", "uploads");
2231
+ const uploadDir = resolveChildDir2(uploadsRoot, options.sessionId);
2232
+ const path = join6(uploadDir, options.fileName);
2233
+ mkdirSync2(uploadDir, { recursive: true });
2234
+ writeFileSync2(path, options.buffer, { mode: 384 });
2235
+ ensureProjectUploadIgnored(cwd);
2236
+ return { success: true, path: relative2(cwd, path) };
2237
+ } catch (err) {
2238
+ serviceLogger.warn(
2239
+ { sessionId: options.sessionId, cwd: options.cwd, error: String(err) },
2240
+ "Project upload write failed; falling back to data dir"
2241
+ );
2242
+ return null;
2243
+ }
2244
+ }
2245
+ async function saveFileUpload(request, options = {}) {
2246
+ try {
2247
+ const buffer = decodeBase64File(request.dataBase64);
2248
+ const now = options.now ?? Date.now;
2249
+ const suffix = options.randomSuffix?.() ?? nanoid4(6);
2250
+ const stamped = new Date(now()).toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
2251
+ const fileName = sanitizeFileName(request.fileName, `upload-${stamped}`, suffix);
2252
+ const projectResult = trySaveProjectUpload({
2253
+ cwd: options.cwd,
2254
+ sessionId: request.sessionId,
2255
+ fileName,
2256
+ buffer
2257
+ });
2258
+ if (projectResult) return projectResult;
2259
+ const dataDir = options.dataDir ?? DATA_DIR;
2260
+ const uploadDir = resolveChildDir2(dataDir, request.sessionId, "uploads");
2261
+ const path = join6(uploadDir, fileName);
2262
+ mkdirSync2(uploadDir, { recursive: true });
2263
+ writeFileSync2(path, buffer, { mode: 384 });
2264
+ return { success: true, path };
2265
+ } catch (err) {
2266
+ return {
2267
+ success: false,
2268
+ error: err instanceof Error ? err.message : String(err),
2269
+ errorCode: ControlErrorCode.UNKNOWN
2270
+ };
2271
+ }
2272
+ }
2273
+
2274
+ // src/serve/image-preview.ts
2275
+ import { readFileSync as readFileSync7, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
2072
2276
  import { tmpdir } from "os";
2073
- import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve2 } from "path";
2277
+ import { isAbsolute as isAbsolute5, relative as relative3, resolve as resolve4 } from "path";
2074
2278
  var MAX_IMAGE_PREVIEW_BYTES = 10 * 1024 * 1024;
2075
2279
  function isInsideRoot(realFilePath, realRootPath) {
2076
- const rel = relative2(realRootPath, realFilePath);
2077
- return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
2280
+ const rel = relative3(realRootPath, realFilePath);
2281
+ return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
2078
2282
  }
2079
2283
  function allowedRoots(options) {
2080
2284
  return [options.cwd, options.tmpDir ?? tmpdir(), ...options.previewRoots ?? []].map((root) => root.trim()).filter(Boolean).flatMap((root) => {
2081
2285
  try {
2082
- return [realpathSync(root)];
2286
+ return [realpathSync2(root)];
2083
2287
  } catch {
2084
2288
  return [];
2085
2289
  }
2086
2290
  });
2087
2291
  }
2088
2292
  function resolvePreviewPath(rawPath, options) {
2089
- const candidate = isAbsolute3(rawPath) ? resolve2(rawPath) : resolve2(options.cwd, rawPath);
2090
- const realCandidate = realpathSync(candidate);
2293
+ const candidate = isAbsolute5(rawPath) ? resolve4(rawPath) : resolve4(options.cwd, rawPath);
2294
+ const realCandidate = realpathSync2(candidate);
2091
2295
  if (!allowedRoots(options).some((root) => isInsideRoot(realCandidate, root))) {
2092
2296
  throw Object.assign(new Error("\u56FE\u7247\u8DEF\u5F84\u4E0D\u5728\u5141\u8BB8\u9884\u89C8\u7684\u76EE\u5F55\u5185"), {
2093
2297
  errorCode: ControlErrorCode.INVALID_PATH
@@ -2110,7 +2314,7 @@ function detectImageMime(buffer) {
2110
2314
  }
2111
2315
  return void 0;
2112
2316
  }
2113
- function errorCode(err) {
2317
+ function errorCode2(err) {
2114
2318
  if (err instanceof Error && "errorCode" in err && typeof err.errorCode === "string") {
2115
2319
  return err.errorCode;
2116
2320
  }
@@ -2119,7 +2323,7 @@ function errorCode(err) {
2119
2323
  function loadImagePreview(request, options) {
2120
2324
  try {
2121
2325
  const resolvedPath = resolvePreviewPath(request.path, options);
2122
- const stat2 = statSync2(resolvedPath);
2326
+ const stat2 = statSync4(resolvedPath);
2123
2327
  if (!stat2.isFile()) {
2124
2328
  return {
2125
2329
  success: false,
@@ -2139,7 +2343,7 @@ function loadImagePreview(request, options) {
2139
2343
  errorCode: ControlErrorCode.UNKNOWN
2140
2344
  };
2141
2345
  }
2142
- const buffer = readFileSync5(resolvedPath);
2346
+ const buffer = readFileSync7(resolvedPath);
2143
2347
  const mimeType = detectImageMime(buffer);
2144
2348
  if (!mimeType) {
2145
2349
  return {
@@ -2164,7 +2368,7 @@ function loadImagePreview(request, options) {
2164
2368
  sessionId: request.sessionId,
2165
2369
  path: request.path,
2166
2370
  error: err instanceof Error ? err.message : String(err),
2167
- errorCode: errorCode(err)
2371
+ errorCode: errorCode2(err)
2168
2372
  };
2169
2373
  }
2170
2374
  }
@@ -2248,7 +2452,6 @@ var RelayInputHandlers = class {
2248
2452
  requestId,
2249
2453
  sessionId,
2250
2454
  success: false,
2251
- path: "",
2252
2455
  error: "\u4F1A\u8BDD\u4E0D\u5B58\u5728",
2253
2456
  errorCode: ControlErrorCode.SESSION_NOT_FOUND
2254
2457
  })
@@ -2310,7 +2513,92 @@ var RelayInputHandlers = class {
2310
2513
  ...result
2311
2514
  })
2312
2515
  );
2313
- serviceLogger.info({ sessionId, success: result.success }, "Image preview handled");
2516
+ if (result.success) {
2517
+ serviceLogger.info({ sessionId, path, size: result.size }, "Image preview handled");
2518
+ } else {
2519
+ serviceLogger.warn(
2520
+ { sessionId, path, errorCode: result.errorCode, error: result.error },
2521
+ "Image preview failed"
2522
+ );
2523
+ }
2524
+ }
2525
+ onFileDownloadRequest(msg) {
2526
+ const { sessionId, requestId, path } = msg;
2527
+ if (!sessionId || !path) return;
2528
+ const session = this.deps.sessionManager.getSession(sessionId);
2529
+ if (!session) {
2530
+ this.deps.relayConnection.sendRaw(
2531
+ serializeControl({
2532
+ type: "file_download_response",
2533
+ requestId,
2534
+ sessionId,
2535
+ success: false,
2536
+ path,
2537
+ error: "\u4F1A\u8BDD\u4E0D\u5B58\u5728",
2538
+ errorCode: ControlErrorCode.SESSION_NOT_FOUND
2539
+ })
2540
+ );
2541
+ serviceLogger.warn({ sessionId }, "File download rejected: session not found");
2542
+ return;
2543
+ }
2544
+ const result = loadFileDownload({ sessionId, path }, { cwd: session.cwd });
2545
+ this.deps.relayConnection.sendRaw(
2546
+ serializeControl({
2547
+ type: "file_download_response",
2548
+ requestId,
2549
+ ...result
2550
+ })
2551
+ );
2552
+ if (result.success) {
2553
+ serviceLogger.info(
2554
+ { sessionId, path, size: result.size },
2555
+ "File download handled"
2556
+ );
2557
+ } else {
2558
+ serviceLogger.warn(
2559
+ { sessionId, path, errorCode: result.errorCode, error: result.error },
2560
+ "File download failed"
2561
+ );
2562
+ }
2563
+ }
2564
+ async onFileUploadRequest(msg) {
2565
+ const { sessionId, requestId, mimeType, dataBase64, fileName } = msg;
2566
+ if (!sessionId) return;
2567
+ const session = this.deps.sessionManager.getSession(sessionId);
2568
+ if (!session) {
2569
+ this.deps.relayConnection.sendRaw(
2570
+ serializeControl({
2571
+ type: "file_upload_response",
2572
+ requestId,
2573
+ sessionId,
2574
+ success: false,
2575
+ error: "\u4F1A\u8BDD\u4E0D\u5B58\u5728",
2576
+ errorCode: ControlErrorCode.SESSION_NOT_FOUND
2577
+ })
2578
+ );
2579
+ serviceLogger.warn({ sessionId }, "File upload rejected: session not found");
2580
+ return;
2581
+ }
2582
+ const result = await saveFileUpload(
2583
+ { sessionId, mimeType, dataBase64, fileName },
2584
+ { cwd: session.cwd }
2585
+ );
2586
+ this.deps.relayConnection.sendRaw(
2587
+ serializeControl({
2588
+ type: "file_upload_response",
2589
+ requestId,
2590
+ sessionId,
2591
+ ...result
2592
+ })
2593
+ );
2594
+ if (result.success) {
2595
+ serviceLogger.info({ sessionId, fileName, path: result.path }, "File upload handled");
2596
+ } else {
2597
+ serviceLogger.warn(
2598
+ { sessionId, fileName, errorCode: result.errorCode, error: result.error },
2599
+ "File upload failed"
2600
+ );
2601
+ }
2314
2602
  }
2315
2603
  };
2316
2604
 
@@ -2505,14 +2793,14 @@ var RelayPermissionHandlers = class {
2505
2793
 
2506
2794
  // src/serve/relay-resource-handlers.ts
2507
2795
  import { homedir as homedir4 } from "os";
2508
- import { accessSync, constants, statSync as statSync3 } from "fs";
2796
+ import { accessSync, constants, statSync as statSync5 } from "fs";
2509
2797
  function errorMessage(err) {
2510
2798
  return err instanceof Error ? err.message : String(err);
2511
2799
  }
2512
2800
  function validateExecutablePath(path) {
2513
2801
  const normalized = path.trim();
2514
2802
  if (!normalized.startsWith("/")) throw new Error("CLI \u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
2515
- const stat2 = statSync3(normalized);
2803
+ const stat2 = statSync5(normalized);
2516
2804
  if (!stat2.isFile()) throw new Error("CLI \u8DEF\u5F84\u4E0D\u662F\u53EF\u6267\u884C\u6587\u4EF6");
2517
2805
  accessSync(normalized, constants.X_OK);
2518
2806
  return normalized;
@@ -2620,9 +2908,9 @@ var RelayResourceHandlers = class {
2620
2908
  };
2621
2909
 
2622
2910
  // src/serve/relay-session-create-handler.ts
2623
- import { rmSync, statSync as statSync4 } from "fs";
2624
- import { isAbsolute as isAbsolute4 } from "path";
2625
- import { nanoid as nanoid4 } from "nanoid";
2911
+ import { rmSync, statSync as statSync6 } from "fs";
2912
+ import { isAbsolute as isAbsolute6 } from "path";
2913
+ import { nanoid as nanoid5 } from "nanoid";
2626
2914
 
2627
2915
  // src/serve/hosted-pty-registry.ts
2628
2916
  import * as pty from "node-pty";
@@ -2873,11 +3161,11 @@ function validateSessionCwd(cwd) {
2873
3161
  return { message: "\u8BF7\u8F93\u5165\u5DE5\u4F5C\u76EE\u5F55", code: ControlErrorCode.INVALID_PATH };
2874
3162
  }
2875
3163
  const trimmed = cwd.trim();
2876
- if (!isAbsolute4(trimmed)) {
3164
+ if (!isAbsolute6(trimmed)) {
2877
3165
  return { message: "\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84", code: ControlErrorCode.INVALID_PATH };
2878
3166
  }
2879
3167
  try {
2880
- const stat2 = statSync4(trimmed);
3168
+ const stat2 = statSync6(trimmed);
2881
3169
  return stat2.isDirectory() ? null : { message: "\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u76EE\u5F55", code: ControlErrorCode.PATH_NOT_DIRECTORY };
2882
3170
  } catch (err) {
2883
3171
  return {
@@ -2940,7 +3228,7 @@ var RelaySessionCreateHandler = class {
2940
3228
  const resumeSessionId = msg.resumeSessionId;
2941
3229
  const streamDelta = false;
2942
3230
  const name = tildify(sessionCwd);
2943
- const pendingId = nanoid4();
3231
+ const pendingId = nanoid5();
2944
3232
  const hook = this.deps.createHookContext(pendingId, provider);
2945
3233
  const workerPid = this.deps.workerRegistry.spawn(pendingId, {
2946
3234
  cwd: sessionCwd,
@@ -3033,7 +3321,7 @@ var RelaySessionCreateHandler = class {
3033
3321
  return;
3034
3322
  }
3035
3323
  const resumeSessionId = msg.resumeSessionId;
3036
- const pendingId = nanoid4();
3324
+ const pendingId = nanoid5();
3037
3325
  const name = tildify(cwd);
3038
3326
  const hook = this.deps.createHookContext(pendingId, provider);
3039
3327
  try {
@@ -3226,6 +3514,12 @@ var RelayRouter = class {
3226
3514
  case "image_preview_request":
3227
3515
  this.inputHandlers.onImagePreviewRequest(msg);
3228
3516
  return;
3517
+ case "file_download_request":
3518
+ this.inputHandlers.onFileDownloadRequest(msg);
3519
+ return;
3520
+ case "file_upload_request":
3521
+ void this.inputHandlers.onFileUploadRequest(msg);
3522
+ return;
3229
3523
  case "tool_approve":
3230
3524
  this.permissionHandlers.onToolApprove(msg);
3231
3525
  return;
@@ -3468,11 +3762,11 @@ var PermissionBroker = class {
3468
3762
  if (this.pending.has(request.requestId)) {
3469
3763
  return Promise.resolve(DUPLICATE_DECISION);
3470
3764
  }
3471
- return new Promise((resolve3) => {
3765
+ return new Promise((resolve5) => {
3472
3766
  this.pending.set(request.requestId, {
3473
3767
  ...request,
3474
3768
  source: "hook",
3475
- resolve: resolve3,
3769
+ resolve: resolve5,
3476
3770
  createdAt: Date.now()
3477
3771
  });
3478
3772
  });
@@ -3811,11 +4105,32 @@ function createEventBridge(deps) {
3811
4105
  deps.relayConnection.sendRaw(serializeControl({ type: "agent_status", sessionId, payload }));
3812
4106
  };
3813
4107
  const cleanupSessionResources = (sessionId) => {
3814
- deps.controlHandlers.cleanup(sessionId);
3815
- deps.agentStatusRegistry.delete(sessionId);
3816
- disposeSeqCounter(sessionId);
3817
- deps.permissionBroker.cleanupSession(sessionId, "Session closed");
3818
- broadcastSessionList(deps.relayConnection, deps.sessionManager);
4108
+ const safe = (fn, step) => {
4109
+ try {
4110
+ fn();
4111
+ } catch (err) {
4112
+ const error = err instanceof Error ? err : new Error(String(err));
4113
+ serviceLogger.warn(
4114
+ {
4115
+ sessionId,
4116
+ step,
4117
+ err: { message: error.message, stack: error.stack, cause: error.cause }
4118
+ },
4119
+ "Session cleanup step failed; continuing"
4120
+ );
4121
+ }
4122
+ };
4123
+ safe(() => deps.controlHandlers.cleanup(sessionId), "controlHandlers.cleanup");
4124
+ safe(() => deps.agentStatusRegistry.delete(sessionId), "agentStatusRegistry.delete");
4125
+ safe(() => disposeSeqCounter(sessionId), "disposeSeqCounter");
4126
+ safe(
4127
+ () => deps.permissionBroker.cleanupSession(sessionId, "Session closed"),
4128
+ "permissionBroker.cleanupSession"
4129
+ );
4130
+ safe(
4131
+ () => broadcastSessionList(deps.relayConnection, deps.sessionManager),
4132
+ "broadcastSessionList"
4133
+ );
3819
4134
  };
3820
4135
  return {
3821
4136
  changeSessionState: changeState,
@@ -3827,14 +4142,14 @@ function createEventBridge(deps) {
3827
4142
 
3828
4143
  // src/serve/service-files.ts
3829
4144
  import { execSync } from "child_process";
3830
- import { existsSync as existsSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2 } from "fs";
4145
+ import { existsSync as existsSync6, readFileSync as readFileSync8, unlinkSync as unlinkSync2 } from "fs";
3831
4146
  import { hostname } from "os";
3832
4147
  import { connect as connect2 } from "net";
3833
4148
  function tryConnectSocket(sockPath) {
3834
- return new Promise((resolve3) => {
4149
+ return new Promise((resolve5) => {
3835
4150
  const s = connect2(sockPath);
3836
- s.on("connect", () => resolve3(s));
3837
- s.on("error", () => resolve3(null));
4151
+ s.on("connect", () => resolve5(s));
4152
+ s.on("error", () => resolve5(null));
3838
4153
  });
3839
4154
  }
3840
4155
  function isProcessAlive(pid) {
@@ -3846,7 +4161,7 @@ function isProcessAlive(pid) {
3846
4161
  }
3847
4162
  }
3848
4163
  async function cleanupStaleResources() {
3849
- if (existsSync5(SOCK_PATH)) {
4164
+ if (existsSync6(SOCK_PATH)) {
3850
4165
  const existing = await tryConnectSocket(SOCK_PATH);
3851
4166
  if (existing) {
3852
4167
  existing.destroy();
@@ -3859,8 +4174,8 @@ async function cleanupStaleResources() {
3859
4174
  unlinkSync2(SOCK_PATH);
3860
4175
  serviceLogger.info("Removed stale socket file");
3861
4176
  }
3862
- if (existsSync5(PID_PATH)) {
3863
- const pidStr = readFileSync6(PID_PATH, "utf-8").trim();
4177
+ if (existsSync6(PID_PATH)) {
4178
+ const pidStr = readFileSync8(PID_PATH, "utf-8").trim();
3864
4179
  const pid = parseInt(pidStr, 10);
3865
4180
  if (!isNaN(pid) && isProcessAlive(pid)) {
3866
4181
  const msg = `Another service is already running with PID ${pid}`;
@@ -4141,8 +4456,14 @@ function handleTerminalConnection(socket, deps) {
4141
4456
  relayConnection.sendBinary(encodeBinaryFrame(sessionId, outputSeq, data));
4142
4457
  },
4143
4458
  (err, line) => {
4459
+ const cause = err instanceof Error ? err.cause : void 0;
4144
4460
  serviceLogger.warn(
4145
- { err: err.message, lineLen: line.length },
4461
+ {
4462
+ err: err.message,
4463
+ cause: cause instanceof Error ? cause.message : cause,
4464
+ lineLen: line.length,
4465
+ linePreview: line.slice(0, 200)
4466
+ },
4146
4467
  "Terminal IPC message dropped (parse/schema error)"
4147
4468
  );
4148
4469
  }
@@ -4186,7 +4507,7 @@ function handleTerminalConnection(socket, deps) {
4186
4507
 
4187
4508
  // src/serve/hook-registry.ts
4188
4509
  import { createHash, randomBytes } from "crypto";
4189
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, renameSync, writeFileSync as writeFileSync2 } from "fs";
4510
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync3 } from "fs";
4190
4511
  import { dirname } from "path";
4191
4512
  import { z } from "zod";
4192
4513
  var PersistedHookSessionBindingSchema = z.object({
@@ -4247,10 +4568,10 @@ var HookRegistry = class {
4247
4568
  }
4248
4569
  }
4249
4570
  load() {
4250
- if (!this.persistPath || !existsSync6(this.persistPath)) return;
4571
+ if (!this.persistPath || !existsSync7(this.persistPath)) return;
4251
4572
  try {
4252
4573
  const parsed = PersistedHookRegistrySchema.parse(
4253
- JSON.parse(readFileSync7(this.persistPath, "utf8"))
4574
+ JSON.parse(readFileSync9(this.persistPath, "utf8"))
4254
4575
  );
4255
4576
  this.bindingsBySession.clear();
4256
4577
  for (const binding of parsed.bindings) {
@@ -4266,9 +4587,9 @@ var HookRegistry = class {
4266
4587
  save() {
4267
4588
  if (!this.persistPath) return;
4268
4589
  try {
4269
- mkdirSync2(dirname(this.persistPath), { recursive: true });
4590
+ mkdirSync3(dirname(this.persistPath), { recursive: true });
4270
4591
  const tmpPath = `${this.persistPath}.${process.pid}.${Date.now()}.tmp`;
4271
- writeFileSync2(
4592
+ writeFileSync3(
4272
4593
  tmpPath,
4273
4594
  JSON.stringify(
4274
4595
  {
@@ -4314,7 +4635,7 @@ var HookServer = class {
4314
4635
  this.writeJson(res, 500, { error: "internal_error" });
4315
4636
  });
4316
4637
  });
4317
- return new Promise((resolve3, reject) => {
4638
+ return new Promise((resolve5, reject) => {
4318
4639
  const onError = (err) => {
4319
4640
  this.server?.off("listening", onListening);
4320
4641
  reject(err);
@@ -4322,7 +4643,7 @@ var HookServer = class {
4322
4643
  const onListening = () => {
4323
4644
  this.server?.off("error", onError);
4324
4645
  serviceLogger.info({ host: this.host, port: this.options.port }, "Hook server listening");
4325
- resolve3();
4646
+ resolve5();
4326
4647
  };
4327
4648
  this.server.once("error", onError);
4328
4649
  this.server.once("listening", onListening);
@@ -4333,8 +4654,8 @@ var HookServer = class {
4333
4654
  if (!this.server) return Promise.resolve();
4334
4655
  const server = this.server;
4335
4656
  this.server = null;
4336
- return new Promise((resolve3, reject) => {
4337
- server.close((err) => err ? reject(err) : resolve3());
4657
+ return new Promise((resolve5, reject) => {
4658
+ server.close((err) => err ? reject(err) : resolve5());
4338
4659
  });
4339
4660
  }
4340
4661
  getListeningPort() {
@@ -4448,7 +4769,7 @@ var HookServer = class {
4448
4769
  this.writeJson(res, 200, payload);
4449
4770
  }
4450
4771
  readBody(req) {
4451
- return new Promise((resolve3, reject) => {
4772
+ return new Promise((resolve5, reject) => {
4452
4773
  let body = "";
4453
4774
  let size = 0;
4454
4775
  req.setEncoding("utf8");
@@ -4461,7 +4782,7 @@ var HookServer = class {
4461
4782
  }
4462
4783
  body += chunk;
4463
4784
  });
4464
- req.on("end", () => resolve3(body));
4785
+ req.on("end", () => resolve5(body));
4465
4786
  req.on("error", reject);
4466
4787
  });
4467
4788
  }
@@ -4808,7 +5129,7 @@ async function startService(options) {
4808
5129
  });
4809
5130
  });
4810
5131
  server.listen(SOCK_PATH, () => {
4811
- writeFileSync3(PID_PATH, String(process.pid));
5132
+ writeFileSync4(PID_PATH, String(process.pid));
4812
5133
  chmodSync(SOCK_PATH, 384);
4813
5134
  serviceLogger.info({ pid: process.pid, sock: SOCK_PATH }, "Service started");
4814
5135
  });