@dev-anywhere/proxy 0.0.6 → 0.1.0

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-2XDZRLST.js";
8
+ } from "./chunk-JPJMOVQ5.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-XRTTHTM2.js";
17
+ } from "./chunk-WXWH6L7J.js";
18
18
  import {
19
19
  spawnScript
20
20
  } from "./chunk-ZUWAB67J.js";
@@ -41,11 +41,11 @@ import {
41
41
  serializeWorkerMsg,
42
42
  sessionPaths,
43
43
  tildify
44
- } from "./chunk-VHCL7NVJ.js";
44
+ } from "./chunk-QWPI6YON.js";
45
45
 
46
46
  // src/serve.ts
47
47
  import { createServer as createServer2 } from "net";
48
- import { unlinkSync as unlinkSync3, writeFileSync as writeFileSync5, chmodSync, rmSync as rmSync2 } from "fs";
48
+ import { unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, chmodSync, rmSync as rmSync2 } from "fs";
49
49
 
50
50
  // src/serve/session-manager.ts
51
51
  import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from "fs";
@@ -183,6 +183,14 @@ var SessionManager = class {
183
183
  serviceLogger.info({ sessionId: id, from: oldState, to: newState }, "Session state changed");
184
184
  return true;
185
185
  }
186
+ touchSession(id, now = Date.now(), minIntervalMs = 0) {
187
+ const session = this.sessions.get(id);
188
+ if (!session) return false;
189
+ if (now - session.updatedAt < minIntervalMs) return false;
190
+ session.updatedAt = now;
191
+ this.save();
192
+ return true;
193
+ }
186
194
  terminateSession(id, context) {
187
195
  const session = this.sessions.get(id);
188
196
  if (!session) {
@@ -1078,7 +1086,7 @@ function extractConversationText(msg) {
1078
1086
  return null;
1079
1087
  }
1080
1088
  async function extractTitleAndCwd(filePath) {
1081
- return new Promise((resolve) => {
1089
+ return new Promise((resolve2) => {
1082
1090
  const rl = createInterface({
1083
1091
  input: createReadStream(filePath, { encoding: "utf-8" }),
1084
1092
  crlfDelay: Infinity
@@ -1106,10 +1114,10 @@ async function extractTitleAndCwd(filePath) {
1106
1114
  }
1107
1115
  });
1108
1116
  rl.on("close", () => {
1109
- if (!resolved) resolve({ title, cwd });
1110
- else resolve({ title, cwd });
1117
+ if (!resolved) resolve2({ title, cwd });
1118
+ else resolve2({ title, cwd });
1111
1119
  });
1112
- rl.on("error", () => resolve({ title, cwd }));
1120
+ rl.on("error", () => resolve2({ title, cwd }));
1113
1121
  });
1114
1122
  }
1115
1123
  async function collectJsonlFiles(root) {
@@ -1131,7 +1139,7 @@ async function collectJsonlFiles(root) {
1131
1139
  return files;
1132
1140
  }
1133
1141
  async function extractCodexTitleAndCwd(filePath) {
1134
- return new Promise((resolve) => {
1142
+ return new Promise((resolve2) => {
1135
1143
  const rl = createInterface({
1136
1144
  input: createReadStream(filePath, { encoding: "utf-8" }),
1137
1145
  crlfDelay: Infinity
@@ -1155,8 +1163,8 @@ async function extractCodexTitleAndCwd(filePath) {
1155
1163
  } catch {
1156
1164
  }
1157
1165
  });
1158
- rl.on("close", () => resolve({ id, title, cwd }));
1159
- rl.on("error", () => resolve({ id, title, cwd }));
1166
+ rl.on("close", () => resolve2({ id, title, cwd }));
1167
+ rl.on("error", () => resolve2({ id, title, cwd }));
1160
1168
  });
1161
1169
  }
1162
1170
  function extractCodexUserText(payload) {
@@ -1721,16 +1729,16 @@ var WorkerRegistry = class {
1721
1729
  return workerPid;
1722
1730
  }
1723
1731
  connect(sessionId, sockPath) {
1724
- return new Promise((resolve) => {
1732
+ return new Promise((resolve2) => {
1725
1733
  const sock = connect(sockPath);
1726
1734
  sock.on("connect", () => {
1727
1735
  this.sockets.set(sessionId, sock);
1728
1736
  createWorkerReader(sock, (msg) => this.handleWorkerMessage(sessionId, msg));
1729
1737
  sock.on("close", () => this.onDisconnect(sessionId));
1730
1738
  sock.on("error", () => this.onDisconnect(sessionId));
1731
- resolve(sock);
1739
+ resolve2(sock);
1732
1740
  });
1733
- sock.on("error", () => resolve(null));
1741
+ sock.on("error", () => resolve2(null));
1734
1742
  });
1735
1743
  }
1736
1744
  // 枚举 DATA_DIR 下所有 session 目录,尝试连接存活的 worker.sock;失败则清理 stale socket。
@@ -1856,6 +1864,7 @@ var WorkerRegistry = class {
1856
1864
  return;
1857
1865
  }
1858
1866
  const ev = parsed.data;
1867
+ this.deps.touchSessionActivity?.(sessionId);
1859
1868
  const isStreamDeltaSession = this.streamDeltaSessions.has(sessionId);
1860
1869
  if (ev.type === "stream_event") {
1861
1870
  const delta = ContentBlockDeltaSchema.safeParse(ev.event);
@@ -2053,6 +2062,80 @@ function terminateSessionByOwnership(deps, sessionId) {
2053
2062
  return { success: false, action: "not_found" };
2054
2063
  }
2055
2064
 
2065
+ // src/serve/clipboard-image-upload.ts
2066
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
2067
+ import { isAbsolute as isAbsolute3, join as join5, relative, resolve } from "path";
2068
+ import { nanoid as nanoid3 } from "nanoid";
2069
+ var MAX_CLIPBOARD_IMAGE_BYTES = 10 * 1024 * 1024;
2070
+ var MAX_CLIPBOARD_IMAGE_BASE64_LENGTH = Math.ceil(MAX_CLIPBOARD_IMAGE_BYTES / 3) * 4;
2071
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Map([
2072
+ ["image/png", "png"],
2073
+ ["image/jpeg", "jpg"],
2074
+ ["image/webp", "webp"],
2075
+ ["image/gif", "gif"]
2076
+ ]);
2077
+ function formatTimestamp(ms) {
2078
+ const [date, time = "000000"] = new Date(ms).toISOString().replace(/\.\d{3}Z$/, "").split("T");
2079
+ return `${date.replace(/-/g, "")}-${time.replace(/:/g, "")}`;
2080
+ }
2081
+ function normalizeBase64(input) {
2082
+ return input.replace(/^data:[^;]+;base64,/i, "").replace(/\s/g, "");
2083
+ }
2084
+ function decodeBase64Image(dataBase64) {
2085
+ const normalized = normalizeBase64(dataBase64);
2086
+ if (normalized.length > MAX_CLIPBOARD_IMAGE_BASE64_LENGTH) {
2087
+ throw new Error("\u56FE\u7247\u8D85\u8FC7 10MB \u9650\u5236");
2088
+ }
2089
+ if (!normalized || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
2090
+ throw new Error("\u56FE\u7247\u6570\u636E\u4E0D\u662F\u6709\u6548\u7684 base64");
2091
+ }
2092
+ const buffer = Buffer.from(normalized, "base64");
2093
+ if (buffer.length === 0) throw new Error("\u56FE\u7247\u6570\u636E\u4E3A\u7A7A");
2094
+ if (buffer.length > MAX_CLIPBOARD_IMAGE_BYTES) {
2095
+ throw new Error("\u56FE\u7247\u8D85\u8FC7 10MB \u9650\u5236");
2096
+ }
2097
+ return buffer;
2098
+ }
2099
+ function resolveSessionClipboardDir(dataDir, sessionId) {
2100
+ const root = resolve(dataDir);
2101
+ const uploadDir = resolve(root, sessionId, "clipboard");
2102
+ const relativePath = relative(root, uploadDir);
2103
+ if (!relativePath || relativePath.startsWith("..") || isAbsolute3(relativePath)) {
2104
+ throw new Error("\u4F1A\u8BDD\u8DEF\u5F84\u65E0\u6548");
2105
+ }
2106
+ return uploadDir;
2107
+ }
2108
+ function saveClipboardImageUpload(request, options = {}) {
2109
+ const extension = IMAGE_EXTENSIONS.get(request.mimeType);
2110
+ if (!extension) {
2111
+ return {
2112
+ success: false,
2113
+ path: "",
2114
+ error: "\u4E0D\u652F\u6301\u8FD9\u79CD\u56FE\u7247\u683C\u5F0F",
2115
+ errorCode: ControlErrorCode.UNKNOWN
2116
+ };
2117
+ }
2118
+ try {
2119
+ const dataDir = options.dataDir ?? DATA_DIR;
2120
+ const uploadDir = resolveSessionClipboardDir(dataDir, request.sessionId);
2121
+ const buffer = decodeBase64Image(request.dataBase64);
2122
+ const now = options.now ?? Date.now;
2123
+ const suffix = options.randomSuffix?.() ?? nanoid3(6);
2124
+ const fileName = `pasted-${formatTimestamp(now())}-${suffix}.${extension}`;
2125
+ const path = join5(uploadDir, fileName);
2126
+ mkdirSync4(uploadDir, { recursive: true });
2127
+ writeFileSync4(path, buffer, { mode: 384 });
2128
+ return { success: true, path };
2129
+ } catch (err) {
2130
+ return {
2131
+ success: false,
2132
+ path: "",
2133
+ error: err instanceof Error ? err.message : String(err),
2134
+ errorCode: ControlErrorCode.UNKNOWN
2135
+ };
2136
+ }
2137
+ }
2138
+
2056
2139
  // src/serve/pty-input.ts
2057
2140
  function serializeRawPtyInput(sessionId, data) {
2058
2141
  return serializeIpc({ type: "pty_input", sessionId, data });
@@ -2126,6 +2209,42 @@ var RelayInputHandlers = class {
2126
2209
  ts.write(serializeRawPtyInput(sessionId, data));
2127
2210
  serviceLogger.info({ sessionId, bytes: data.length }, "Raw PTY input forwarded");
2128
2211
  }
2212
+ onClipboardImageUpload(msg) {
2213
+ const sessionId = msg.sessionId;
2214
+ const requestId = msg.requestId;
2215
+ if (!sessionId) return;
2216
+ const session = this.deps.sessionManager.getSession(sessionId);
2217
+ if (!session) {
2218
+ this.deps.relayConnection.sendRaw(
2219
+ JSON.stringify({
2220
+ type: "clipboard_image_upload_response",
2221
+ requestId,
2222
+ sessionId,
2223
+ success: false,
2224
+ path: "",
2225
+ error: "\u4F1A\u8BDD\u4E0D\u5B58\u5728",
2226
+ errorCode: ControlErrorCode.SESSION_NOT_FOUND
2227
+ })
2228
+ );
2229
+ serviceLogger.warn({ sessionId }, "Clipboard image upload rejected: session not found");
2230
+ return;
2231
+ }
2232
+ const result = saveClipboardImageUpload({
2233
+ sessionId,
2234
+ mimeType: typeof msg.mimeType === "string" ? msg.mimeType : "",
2235
+ dataBase64: typeof msg.dataBase64 === "string" ? msg.dataBase64 : "",
2236
+ fileName: typeof msg.fileName === "string" ? msg.fileName : void 0
2237
+ });
2238
+ this.deps.relayConnection.sendRaw(
2239
+ JSON.stringify({
2240
+ type: "clipboard_image_upload_response",
2241
+ requestId,
2242
+ sessionId,
2243
+ ...result
2244
+ })
2245
+ );
2246
+ serviceLogger.info({ sessionId, success: result.success }, "Clipboard image upload handled");
2247
+ }
2129
2248
  };
2130
2249
 
2131
2250
  // src/serve/relay-history-handlers.ts
@@ -2442,8 +2561,8 @@ var RelayResourceHandlers = class {
2442
2561
 
2443
2562
  // src/serve/relay-session-create-handler.ts
2444
2563
  import { rmSync, statSync as statSync2 } from "fs";
2445
- import { isAbsolute as isAbsolute3 } from "path";
2446
- import { nanoid as nanoid3 } from "nanoid";
2564
+ import { isAbsolute as isAbsolute4 } from "path";
2565
+ import { nanoid as nanoid4 } from "nanoid";
2447
2566
 
2448
2567
  // src/serve/hosted-pty-registry.ts
2449
2568
  import * as pty from "node-pty";
@@ -2595,6 +2714,7 @@ var HostedPtyRegistry = class {
2595
2714
  hosted.lastOutputTime = Date.now();
2596
2715
  hosted.outputSeq += 1;
2597
2716
  hosted.terminal.write(data);
2717
+ this.deps.touchSessionActivity(sessionId);
2598
2718
  this.sendBinary(sessionId, Buffer.from(data, "utf-8"), hosted.outputSeq);
2599
2719
  const oscSequences = extractOscSequences(data);
2600
2720
  const session = this.deps.sessionManager.getSession(sessionId);
@@ -2731,7 +2851,7 @@ function validateSessionCwd(cwd) {
2731
2851
  return { message: "\u8BF7\u8F93\u5165\u5DE5\u4F5C\u76EE\u5F55", code: ControlErrorCode.INVALID_PATH };
2732
2852
  }
2733
2853
  const trimmed = cwd.trim();
2734
- if (!isAbsolute3(trimmed)) {
2854
+ if (!isAbsolute4(trimmed)) {
2735
2855
  return { message: "\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84", code: ControlErrorCode.INVALID_PATH };
2736
2856
  }
2737
2857
  try {
@@ -2790,7 +2910,7 @@ var RelaySessionCreateHandler = class {
2790
2910
  const resumeSessionId = msg.resumeSessionId;
2791
2911
  const streamDelta = msg.streamDelta === true;
2792
2912
  const name = tildify(sessionCwd);
2793
- const pendingId = nanoid3();
2913
+ const pendingId = nanoid4();
2794
2914
  const hook = this.deps.createHookContext(pendingId, provider);
2795
2915
  const workerPid = this.deps.workerRegistry.spawn(pendingId, {
2796
2916
  cwd: sessionCwd,
@@ -2875,7 +2995,7 @@ var RelaySessionCreateHandler = class {
2875
2995
  return;
2876
2996
  }
2877
2997
  const resumeSessionId = msg.resumeSessionId;
2878
- const pendingId = nanoid3();
2998
+ const pendingId = nanoid4();
2879
2999
  const name = tildify(cwd);
2880
3000
  const hook = this.deps.createHookContext(pendingId, provider);
2881
3001
  try {
@@ -3042,6 +3162,7 @@ var RelayRouter = class {
3042
3162
  handlers = {
3043
3163
  user_input: (msg) => this.inputHandlers.onUserInput(msg),
3044
3164
  remote_input_raw: (msg) => this.inputHandlers.onRemoteInputRaw(msg),
3165
+ clipboard_image_upload: (msg) => this.inputHandlers.onClipboardImageUpload(msg),
3045
3166
  tool_approve: (msg) => this.permissionHandlers.onToolApprove(msg),
3046
3167
  tool_deny: (msg) => this.permissionHandlers.onToolDeny(msg),
3047
3168
  proxy_info_request: (msg) => this.resourceHandlers.onProxyInfoRequest(msg),
@@ -3232,11 +3353,11 @@ var PermissionBroker = class {
3232
3353
  message: "Duplicate permission request id."
3233
3354
  });
3234
3355
  }
3235
- return new Promise((resolve) => {
3356
+ return new Promise((resolve2) => {
3236
3357
  this.pending.set(request.requestId, {
3237
3358
  ...request,
3238
3359
  source: "hook",
3239
- resolve,
3360
+ resolve: resolve2,
3240
3361
  createdAt: Date.now()
3241
3362
  });
3242
3363
  });
@@ -3460,6 +3581,7 @@ var AgentStatusRegistry = class {
3460
3581
  };
3461
3582
 
3462
3583
  // src/serve/session-broadcast.ts
3584
+ var ACTIVITY_STATUS_PUSH_INTERVAL_MS = 15e3;
3463
3585
  function toSessionListPayload(s) {
3464
3586
  return {
3465
3587
  sessionId: s.id,
@@ -3522,6 +3644,11 @@ function changeSessionState(sessionManager, relay, sessionId, next) {
3522
3644
  if (changed) pushSessionStatus(relay, sessionManager, sessionId);
3523
3645
  return changed;
3524
3646
  }
3647
+ function touchSessionActivity(sessionManager, relay, sessionId, now = Date.now()) {
3648
+ const touched = sessionManager.touchSession(sessionId, now, ACTIVITY_STATUS_PUSH_INTERVAL_MS);
3649
+ if (touched) pushSessionStatus(relay, sessionManager, sessionId);
3650
+ return touched;
3651
+ }
3525
3652
 
3526
3653
  // src/serve/service-files.ts
3527
3654
  import { execSync } from "child_process";
@@ -3529,10 +3656,10 @@ import { existsSync as existsSync5, readFileSync as readFileSync5, unlinkSync as
3529
3656
  import { hostname } from "os";
3530
3657
  import { connect as connect2 } from "net";
3531
3658
  function tryConnectSocket(sockPath) {
3532
- return new Promise((resolve) => {
3659
+ return new Promise((resolve2) => {
3533
3660
  const s = connect2(sockPath);
3534
- s.on("connect", () => resolve(s));
3535
- s.on("error", () => resolve(null));
3661
+ s.on("connect", () => resolve2(s));
3662
+ s.on("error", () => resolve2(null));
3536
3663
  });
3537
3664
  }
3538
3665
  function isProcessAlive(pid) {
@@ -3876,6 +4003,7 @@ function handleTerminalConnection(socket, deps) {
3876
4003
  },
3877
4004
  (sessionId, data, outputSeq) => {
3878
4005
  if (!sessionManager.getSession(sessionId)) return;
4006
+ touchSessionActivity(sessionManager, relayConnection, sessionId);
3879
4007
  const sessionIdBuf = Buffer.from(sessionId, "utf-8");
3880
4008
  const wsFrame = Buffer.alloc(1 + sessionIdBuf.length + 4 + data.length);
3881
4009
  wsFrame[0] = sessionIdBuf.length;
@@ -3926,7 +4054,7 @@ function handleTerminalConnection(socket, deps) {
3926
4054
 
3927
4055
  // src/serve/hook-registry.ts
3928
4056
  import { createHash, randomBytes } from "crypto";
3929
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
4057
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
3930
4058
  import { dirname as dirname4 } from "path";
3931
4059
  import { z } from "zod";
3932
4060
  var PersistedHookSessionBindingSchema = z.object({
@@ -4006,9 +4134,9 @@ var HookRegistry = class {
4006
4134
  save() {
4007
4135
  if (!this.persistPath) return;
4008
4136
  try {
4009
- mkdirSync4(dirname4(this.persistPath), { recursive: true });
4137
+ mkdirSync5(dirname4(this.persistPath), { recursive: true });
4010
4138
  const tmpPath = `${this.persistPath}.${process.pid}.${Date.now()}.tmp`;
4011
- writeFileSync4(
4139
+ writeFileSync5(
4012
4140
  tmpPath,
4013
4141
  JSON.stringify(
4014
4142
  {
@@ -4060,7 +4188,7 @@ var HookServer = class {
4060
4188
  this.writeJson(res, 500, { error: "internal_error" });
4061
4189
  });
4062
4190
  });
4063
- return new Promise((resolve, reject) => {
4191
+ return new Promise((resolve2, reject) => {
4064
4192
  const onError = (err) => {
4065
4193
  this.server?.off("listening", onListening);
4066
4194
  reject(err);
@@ -4068,7 +4196,7 @@ var HookServer = class {
4068
4196
  const onListening = () => {
4069
4197
  this.server?.off("error", onError);
4070
4198
  serviceLogger.info({ host: this.host, port: this.options.port }, "Hook server listening");
4071
- resolve();
4199
+ resolve2();
4072
4200
  };
4073
4201
  this.server.once("error", onError);
4074
4202
  this.server.once("listening", onListening);
@@ -4079,8 +4207,8 @@ var HookServer = class {
4079
4207
  if (!this.server) return Promise.resolve();
4080
4208
  const server = this.server;
4081
4209
  this.server = null;
4082
- return new Promise((resolve, reject) => {
4083
- server.close((err) => err ? reject(err) : resolve());
4210
+ return new Promise((resolve2, reject) => {
4211
+ server.close((err) => err ? reject(err) : resolve2());
4084
4212
  });
4085
4213
  }
4086
4214
  getListeningPort() {
@@ -4194,7 +4322,7 @@ var HookServer = class {
4194
4322
  this.writeJson(res, 200, payload);
4195
4323
  }
4196
4324
  readBody(req) {
4197
- return new Promise((resolve, reject) => {
4325
+ return new Promise((resolve2, reject) => {
4198
4326
  let body = "";
4199
4327
  let size = 0;
4200
4328
  req.setEncoding("utf8");
@@ -4207,7 +4335,7 @@ var HookServer = class {
4207
4335
  }
4208
4336
  body += chunk;
4209
4337
  });
4210
- req.on("end", () => resolve(body));
4338
+ req.on("end", () => resolve2(body));
4211
4339
  req.on("error", reject);
4212
4340
  });
4213
4341
  }
@@ -4391,6 +4519,7 @@ async function startService(options) {
4391
4519
  const relaySend = (data) => relayConnection.sendRaw(data);
4392
4520
  const controlHandlers = createControlMessageHandlers(relaySend, sessionManager);
4393
4521
  const observerChangeState = (sessionId, next) => changeSessionState(sessionManager, relayConnection, sessionId, next);
4522
+ const observerTouchActivity = (sessionId) => touchSessionActivity(sessionManager, relayConnection, sessionId);
4394
4523
  const emitAgentStatus = (sessionId, phase) => {
4395
4524
  const session = sessionManager.getSession(sessionId);
4396
4525
  if (!session) return;
@@ -4421,6 +4550,7 @@ async function startService(options) {
4421
4550
  permissionBroker,
4422
4551
  relayConnection,
4423
4552
  jsonObserver,
4553
+ touchSessionActivity: observerTouchActivity,
4424
4554
  getProviderEnv
4425
4555
  });
4426
4556
  const hostedPtyRegistry = new HostedPtyRegistry({
@@ -4428,6 +4558,7 @@ async function startService(options) {
4428
4558
  relayConnection,
4429
4559
  getProviderEnv,
4430
4560
  changeSessionState: observerChangeState,
4561
+ touchSessionActivity: observerTouchActivity,
4431
4562
  onTurnComplete: (sessionId) => {
4432
4563
  resolveInterruptedApprovals(
4433
4564
  permissionBroker,
@@ -4513,7 +4644,7 @@ async function startService(options) {
4513
4644
  });
4514
4645
  });
4515
4646
  server.listen(SOCK_PATH, () => {
4516
- writeFileSync5(PID_PATH, String(process.pid));
4647
+ writeFileSync6(PID_PATH, String(process.pid));
4517
4648
  chmodSync(SOCK_PATH, 384);
4518
4649
  serviceLogger.info({ pid: process.pid, sock: SOCK_PATH }, "Service started");
4519
4650
  });