@dev-anywhere/proxy 0.0.6 → 0.1.1

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-QXOARRC2.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-TG7JPHE5.js";
18
18
  import {
19
19
  spawnScript
20
20
  } from "./chunk-ZUWAB67J.js";
@@ -27,9 +27,12 @@ import {
27
27
  CONFIG_PATH,
28
28
  ControlErrorCode,
29
29
  DATA_DIR,
30
+ DEFAULT_PROXY_PROFILE,
30
31
  HOOK_REGISTRY_PATH,
31
32
  MessageEnvelopeSchema,
32
33
  PID_PATH,
34
+ PROFILE_NAME,
35
+ PROXY_ID_PATH,
33
36
  SESSIONS_PATH,
34
37
  SOCK_PATH,
35
38
  STOPPED_PATH,
@@ -37,15 +40,17 @@ import {
37
40
  buildMessage,
38
41
  createIpcReader,
39
42
  createWorkerReader,
43
+ defaultHookPortForProfile,
44
+ ensureProfileWorkspace,
40
45
  serializeIpc,
41
46
  serializeWorkerMsg,
42
47
  sessionPaths,
43
48
  tildify
44
- } from "./chunk-VHCL7NVJ.js";
49
+ } from "./chunk-DFLQ3TFT.js";
45
50
 
46
51
  // src/serve.ts
47
52
  import { createServer as createServer2 } from "net";
48
- import { unlinkSync as unlinkSync3, writeFileSync as writeFileSync5, chmodSync, rmSync as rmSync2 } from "fs";
53
+ import { unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, chmodSync, rmSync as rmSync2 } from "fs";
49
54
 
50
55
  // src/serve/session-manager.ts
51
56
  import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from "fs";
@@ -183,6 +188,14 @@ var SessionManager = class {
183
188
  serviceLogger.info({ sessionId: id, from: oldState, to: newState }, "Session state changed");
184
189
  return true;
185
190
  }
191
+ touchSession(id, now = Date.now(), minIntervalMs = 0) {
192
+ const session = this.sessions.get(id);
193
+ if (!session) return false;
194
+ if (now - session.updatedAt < minIntervalMs) return false;
195
+ session.updatedAt = now;
196
+ this.save();
197
+ return true;
198
+ }
186
199
  terminateSession(id, context) {
187
200
  const session = this.sessions.get(id);
188
201
  if (!session) {
@@ -612,31 +625,20 @@ function parsePort(value, source) {
612
625
  }
613
626
  return port;
614
627
  }
615
- function resolveFileConfig(fromFile, requestedEnv) {
616
- if (!fromFile.envs) {
617
- return {
618
- envName: void 0,
619
- envNameSource: fromFile.relayUrl || fromFile.relayToken || fromFile.hookPort ? "single" : "none",
620
- config: fromFile
621
- };
622
- }
623
- const envName = requestedEnv ?? fromFile.defaultEnv ?? "local";
624
- const config = fromFile.envs[envName];
625
- if (!config) {
626
- const available = Object.keys(fromFile.envs).sort();
627
- throw new Error(
628
- `Unknown config env "${envName}". Available envs: ${available.length > 0 ? available.join(", ") : "(none)"}`
629
- );
628
+ function isRecord(value) {
629
+ return typeof value === "object" && value !== null && !Array.isArray(value);
630
+ }
631
+ function validateConfigShape(value) {
632
+ if (!isRecord(value) || !isRecord(value.profiles) || !isRecord(value.relays)) {
633
+ throw new Error(`Invalid config shape in ${CONFIG_PATH}: expected "profiles" and "relays".`);
630
634
  }
631
- return {
632
- envName,
633
- envNameSource: requestedEnv ? "cli" : fromFile.defaultEnv ? "file" : "default",
634
- config
635
- };
635
+ return value;
636
636
  }
637
637
  function readConfigFile() {
638
- if (!existsSync3(CONFIG_PATH)) return {};
639
- return JSON.parse(readFileSync3(CONFIG_PATH, "utf-8"));
638
+ if (!existsSync3(CONFIG_PATH)) {
639
+ throw new Error(`Dev Anywhere config not found at ${CONFIG_PATH}. Run "dev-anywhere init".`);
640
+ }
641
+ return validateConfigShape(JSON.parse(readFileSync3(CONFIG_PATH, "utf-8")));
640
642
  }
641
643
  function agentCliField(provider) {
642
644
  return provider === "claude" ? "claudeBin" : "codexBin";
@@ -661,68 +663,71 @@ function uniqueAbsolutePaths(paths) {
661
663
  }
662
664
  return result;
663
665
  }
664
- function updateAgentCliPathInEnvConfig(config, provider, path) {
665
- const field = agentCliField(provider);
666
- const historyField = agentCliHistoryField(provider);
667
- const history = uniqueAbsolutePaths([path, ...config[historyField] ?? []]).slice(0, 8);
666
+ function resolveRelayConfig(fromFile, requestedRelayName) {
667
+ const profile = fromFile.profiles[PROFILE_NAME];
668
+ if (!profile) {
669
+ const available = Object.keys(fromFile.profiles).sort();
670
+ throw new Error(
671
+ `Unknown profile "${PROFILE_NAME}". Available profiles: ${available.length > 0 ? available.join(", ") : "(none)"}`
672
+ );
673
+ }
674
+ const relayName = requestedRelayName?.trim() || profile.relay?.trim();
675
+ if (!relayName) {
676
+ throw new Error(`Profile "${PROFILE_NAME}" must specify a relay.`);
677
+ }
678
+ const relay = fromFile.relays[relayName];
679
+ if (!relay) {
680
+ const available = Object.keys(fromFile.relays).sort();
681
+ throw new Error(
682
+ `Unknown relay "${relayName}". Available relays: ${available.length > 0 ? available.join(", ") : "(none)"}`
683
+ );
684
+ }
668
685
  return {
669
- ...config,
670
- [field]: path,
671
- [historyField]: history
686
+ relayName,
687
+ relayNameSource: requestedRelayName?.trim() ? "cli" : "profile",
688
+ relay
672
689
  };
673
690
  }
674
691
  function loadConfig(options) {
675
- let fromFile = {};
676
- if (existsSync3(CONFIG_PATH)) {
677
- try {
678
- fromFile = readConfigFile();
679
- } catch (err) {
680
- serviceLogger.warn(
681
- { path: CONFIG_PATH, err: err instanceof Error ? err.message : String(err) },
682
- "Failed to parse config file, falling back to env-only"
683
- );
684
- }
685
- } else {
686
- serviceLogger.debug({ path: CONFIG_PATH }, "Config file not found, using env-only");
687
- }
688
- const resolved = resolveFileConfig(fromFile, options?.envName);
689
- const hookPortFromFile = resolved.config.hookPort ?? fromFile.hookPort;
690
- const claudeBinFromFile = resolved.config.claudeBin ?? fromFile.claudeBin;
691
- const codexBinFromFile = resolved.config.codexBin ?? fromFile.codexBin;
692
- const claudeBinHistory = [
693
- ...resolved.config.claudeBinHistory ?? [],
694
- ...fromFile.claudeBinHistory ?? []
695
- ];
696
- const codexBinHistory = [
697
- ...resolved.config.codexBinHistory ?? [],
698
- ...fromFile.codexBinHistory ?? []
699
- ];
700
- const claudeBin = process.env.CLAUDE_BIN ?? claudeBinFromFile;
701
- const codexBin = process.env.CODEX_BIN ?? codexBinFromFile;
692
+ const fromFile = readConfigFile();
693
+ const agentCli = fromFile.agentCli ?? {};
694
+ const resolved = resolveRelayConfig(fromFile, options?.relayName);
695
+ const claudeBin = process.env.CLAUDE_BIN ?? agentCli.claudeBin;
696
+ const codexBin = process.env.CODEX_BIN ?? agentCli.codexBin;
702
697
  const config = {
703
- envName: resolved.envName,
704
- relayUrl: process.env.RELAY_URL ?? resolved.config.relayUrl,
705
- relayToken: process.env.RELAY_PROXY_TOKEN ?? resolved.config.relayToken,
706
- hookPort: parsePort(process.env.DEV_ANYWHERE_HOOK_PORT, "DEV_ANYWHERE_HOOK_PORT") ?? hookPortFromFile ?? 17654,
698
+ profileName: PROFILE_NAME,
699
+ relayName: resolved.relayName,
700
+ relayUrl: process.env.RELAY_URL ?? resolved.relay.url,
701
+ relayToken: process.env.RELAY_PROXY_TOKEN ?? resolved.relay.proxyToken,
702
+ hookPort: parsePort(process.env.DEV_ANYWHERE_HOOK_PORT, "DEV_ANYWHERE_HOOK_PORT") ?? defaultHookPortForProfile(PROFILE_NAME),
707
703
  claudeBin,
708
704
  codexBin,
709
705
  agentCliSuggestions: {
710
- claude: uniqueAbsolutePaths([process.env.CLAUDE_BIN, claudeBinFromFile, ...claudeBinHistory]),
711
- codex: uniqueAbsolutePaths([process.env.CODEX_BIN, codexBinFromFile, ...codexBinHistory])
706
+ claude: uniqueAbsolutePaths([
707
+ process.env.CLAUDE_BIN,
708
+ agentCli.claudeBin,
709
+ ...agentCli.claudeBinHistory ?? []
710
+ ]),
711
+ codex: uniqueAbsolutePaths([
712
+ process.env.CODEX_BIN,
713
+ agentCli.codexBin,
714
+ ...agentCli.codexBinHistory ?? []
715
+ ])
712
716
  },
713
717
  sources: {
714
- envName: resolved.envNameSource,
715
- relayUrl: process.env.RELAY_URL ? "env" : resolved.config.relayUrl ? "file" : "none",
716
- relayToken: process.env.RELAY_PROXY_TOKEN ? "env" : resolved.config.relayToken ? "file" : "none",
717
- hookPort: process.env.DEV_ANYWHERE_HOOK_PORT ? "env" : hookPortFromFile ? "file" : "default",
718
- claudeBin: process.env.CLAUDE_BIN ? "env" : claudeBinFromFile ? "file" : "none",
719
- codexBin: process.env.CODEX_BIN ? "env" : codexBinFromFile ? "file" : "none"
718
+ relayName: resolved.relayNameSource,
719
+ relayUrl: process.env.RELAY_URL ? "env" : resolved.relay.url ? "file" : "none",
720
+ relayToken: process.env.RELAY_PROXY_TOKEN ? "env" : resolved.relay.proxyToken ? "file" : "none",
721
+ hookPort: process.env.DEV_ANYWHERE_HOOK_PORT ? "env" : "default",
722
+ claudeBin: process.env.CLAUDE_BIN ? "env" : agentCli.claudeBin ? "file" : "none",
723
+ codexBin: process.env.CODEX_BIN ? "env" : agentCli.codexBin ? "file" : "none"
720
724
  }
721
725
  };
722
726
  serviceLogger.info(
723
727
  {
724
- envName: config.envName ?? "(single)",
725
- envNameSource: config.sources.envName,
728
+ profile: config.profileName,
729
+ relayName: config.relayName,
730
+ relayNameSource: config.sources.relayName,
726
731
  relayUrl: config.relayUrl ?? "(unset)",
727
732
  relayUrlSource: config.sources.relayUrl,
728
733
  relayTokenSource: config.sources.relayToken,
@@ -742,20 +747,20 @@ function buildProviderEnv(config, baseEnv = process.env) {
742
747
  ...config.codexBin ? { CODEX_BIN: config.codexBin } : {}
743
748
  };
744
749
  }
745
- function saveAgentCliPath(provider, path, options) {
750
+ function updateAgentCliConfig(config, provider, path) {
751
+ const field = agentCliField(provider);
752
+ const historyField = agentCliHistoryField(provider);
753
+ const history = uniqueAbsolutePaths([path, ...config[historyField] ?? []]).slice(0, 8);
754
+ return {
755
+ ...config,
756
+ [field]: path,
757
+ [historyField]: history
758
+ };
759
+ }
760
+ function saveAgentCliPath(provider, path) {
746
761
  const normalized = validateAgentCliPath(path);
747
762
  const fromFile = readConfigFile();
748
- const resolved = resolveFileConfig(fromFile, options?.envName);
749
- if (fromFile.envs) {
750
- const envName = resolved.envName ?? options?.envName ?? fromFile.defaultEnv ?? "local";
751
- fromFile.envs[envName] = updateAgentCliPathInEnvConfig(
752
- fromFile.envs[envName] ?? {},
753
- provider,
754
- normalized
755
- );
756
- } else {
757
- Object.assign(fromFile, updateAgentCliPathInEnvConfig(fromFile, provider, normalized));
758
- }
763
+ fromFile.agentCli = updateAgentCliConfig(fromFile.agentCli ?? {}, provider, normalized);
759
764
  mkdirSync3(dirname3(CONFIG_PATH), { recursive: true });
760
765
  writeFileSync3(CONFIG_PATH, `${JSON.stringify(fromFile, null, 2)}
761
766
  `, "utf-8");
@@ -1078,7 +1083,7 @@ function extractConversationText(msg) {
1078
1083
  return null;
1079
1084
  }
1080
1085
  async function extractTitleAndCwd(filePath) {
1081
- return new Promise((resolve) => {
1086
+ return new Promise((resolve2) => {
1082
1087
  const rl = createInterface({
1083
1088
  input: createReadStream(filePath, { encoding: "utf-8" }),
1084
1089
  crlfDelay: Infinity
@@ -1106,10 +1111,10 @@ async function extractTitleAndCwd(filePath) {
1106
1111
  }
1107
1112
  });
1108
1113
  rl.on("close", () => {
1109
- if (!resolved) resolve({ title, cwd });
1110
- else resolve({ title, cwd });
1114
+ if (!resolved) resolve2({ title, cwd });
1115
+ else resolve2({ title, cwd });
1111
1116
  });
1112
- rl.on("error", () => resolve({ title, cwd }));
1117
+ rl.on("error", () => resolve2({ title, cwd }));
1113
1118
  });
1114
1119
  }
1115
1120
  async function collectJsonlFiles(root) {
@@ -1131,7 +1136,7 @@ async function collectJsonlFiles(root) {
1131
1136
  return files;
1132
1137
  }
1133
1138
  async function extractCodexTitleAndCwd(filePath) {
1134
- return new Promise((resolve) => {
1139
+ return new Promise((resolve2) => {
1135
1140
  const rl = createInterface({
1136
1141
  input: createReadStream(filePath, { encoding: "utf-8" }),
1137
1142
  crlfDelay: Infinity
@@ -1155,8 +1160,8 @@ async function extractCodexTitleAndCwd(filePath) {
1155
1160
  } catch {
1156
1161
  }
1157
1162
  });
1158
- rl.on("close", () => resolve({ id, title, cwd }));
1159
- rl.on("error", () => resolve({ id, title, cwd }));
1163
+ rl.on("close", () => resolve2({ id, title, cwd }));
1164
+ rl.on("error", () => resolve2({ id, title, cwd }));
1160
1165
  });
1161
1166
  }
1162
1167
  function extractCodexUserText(payload) {
@@ -1721,16 +1726,16 @@ var WorkerRegistry = class {
1721
1726
  return workerPid;
1722
1727
  }
1723
1728
  connect(sessionId, sockPath) {
1724
- return new Promise((resolve) => {
1729
+ return new Promise((resolve2) => {
1725
1730
  const sock = connect(sockPath);
1726
1731
  sock.on("connect", () => {
1727
1732
  this.sockets.set(sessionId, sock);
1728
1733
  createWorkerReader(sock, (msg) => this.handleWorkerMessage(sessionId, msg));
1729
1734
  sock.on("close", () => this.onDisconnect(sessionId));
1730
1735
  sock.on("error", () => this.onDisconnect(sessionId));
1731
- resolve(sock);
1736
+ resolve2(sock);
1732
1737
  });
1733
- sock.on("error", () => resolve(null));
1738
+ sock.on("error", () => resolve2(null));
1734
1739
  });
1735
1740
  }
1736
1741
  // 枚举 DATA_DIR 下所有 session 目录,尝试连接存活的 worker.sock;失败则清理 stale socket。
@@ -1856,6 +1861,7 @@ var WorkerRegistry = class {
1856
1861
  return;
1857
1862
  }
1858
1863
  const ev = parsed.data;
1864
+ this.deps.touchSessionActivity?.(sessionId);
1859
1865
  const isStreamDeltaSession = this.streamDeltaSessions.has(sessionId);
1860
1866
  if (ev.type === "stream_event") {
1861
1867
  const delta = ContentBlockDeltaSchema.safeParse(ev.event);
@@ -2053,6 +2059,80 @@ function terminateSessionByOwnership(deps, sessionId) {
2053
2059
  return { success: false, action: "not_found" };
2054
2060
  }
2055
2061
 
2062
+ // src/serve/clipboard-image-upload.ts
2063
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
2064
+ import { isAbsolute as isAbsolute3, join as join5, relative, resolve } from "path";
2065
+ import { nanoid as nanoid3 } from "nanoid";
2066
+ var MAX_CLIPBOARD_IMAGE_BYTES = 10 * 1024 * 1024;
2067
+ var MAX_CLIPBOARD_IMAGE_BASE64_LENGTH = Math.ceil(MAX_CLIPBOARD_IMAGE_BYTES / 3) * 4;
2068
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Map([
2069
+ ["image/png", "png"],
2070
+ ["image/jpeg", "jpg"],
2071
+ ["image/webp", "webp"],
2072
+ ["image/gif", "gif"]
2073
+ ]);
2074
+ function formatTimestamp(ms) {
2075
+ const [date, time = "000000"] = new Date(ms).toISOString().replace(/\.\d{3}Z$/, "").split("T");
2076
+ return `${date.replace(/-/g, "")}-${time.replace(/:/g, "")}`;
2077
+ }
2078
+ function normalizeBase64(input) {
2079
+ return input.replace(/^data:[^;]+;base64,/i, "").replace(/\s/g, "");
2080
+ }
2081
+ function decodeBase64Image(dataBase64) {
2082
+ const normalized = normalizeBase64(dataBase64);
2083
+ if (normalized.length > MAX_CLIPBOARD_IMAGE_BASE64_LENGTH) {
2084
+ throw new Error("\u56FE\u7247\u8D85\u8FC7 10MB \u9650\u5236");
2085
+ }
2086
+ if (!normalized || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
2087
+ throw new Error("\u56FE\u7247\u6570\u636E\u4E0D\u662F\u6709\u6548\u7684 base64");
2088
+ }
2089
+ const buffer = Buffer.from(normalized, "base64");
2090
+ if (buffer.length === 0) throw new Error("\u56FE\u7247\u6570\u636E\u4E3A\u7A7A");
2091
+ if (buffer.length > MAX_CLIPBOARD_IMAGE_BYTES) {
2092
+ throw new Error("\u56FE\u7247\u8D85\u8FC7 10MB \u9650\u5236");
2093
+ }
2094
+ return buffer;
2095
+ }
2096
+ function resolveSessionClipboardDir(dataDir, sessionId) {
2097
+ const root = resolve(dataDir);
2098
+ const uploadDir = resolve(root, sessionId, "clipboard");
2099
+ const relativePath = relative(root, uploadDir);
2100
+ if (!relativePath || relativePath.startsWith("..") || isAbsolute3(relativePath)) {
2101
+ throw new Error("\u4F1A\u8BDD\u8DEF\u5F84\u65E0\u6548");
2102
+ }
2103
+ return uploadDir;
2104
+ }
2105
+ function saveClipboardImageUpload(request, options = {}) {
2106
+ const extension = IMAGE_EXTENSIONS.get(request.mimeType);
2107
+ if (!extension) {
2108
+ return {
2109
+ success: false,
2110
+ path: "",
2111
+ error: "\u4E0D\u652F\u6301\u8FD9\u79CD\u56FE\u7247\u683C\u5F0F",
2112
+ errorCode: ControlErrorCode.UNKNOWN
2113
+ };
2114
+ }
2115
+ try {
2116
+ const dataDir = options.dataDir ?? DATA_DIR;
2117
+ const uploadDir = resolveSessionClipboardDir(dataDir, request.sessionId);
2118
+ const buffer = decodeBase64Image(request.dataBase64);
2119
+ const now = options.now ?? Date.now;
2120
+ const suffix = options.randomSuffix?.() ?? nanoid3(6);
2121
+ const fileName = `pasted-${formatTimestamp(now())}-${suffix}.${extension}`;
2122
+ const path = join5(uploadDir, fileName);
2123
+ mkdirSync4(uploadDir, { recursive: true });
2124
+ writeFileSync4(path, buffer, { mode: 384 });
2125
+ return { success: true, path };
2126
+ } catch (err) {
2127
+ return {
2128
+ success: false,
2129
+ path: "",
2130
+ error: err instanceof Error ? err.message : String(err),
2131
+ errorCode: ControlErrorCode.UNKNOWN
2132
+ };
2133
+ }
2134
+ }
2135
+
2056
2136
  // src/serve/pty-input.ts
2057
2137
  function serializeRawPtyInput(sessionId, data) {
2058
2138
  return serializeIpc({ type: "pty_input", sessionId, data });
@@ -2126,6 +2206,42 @@ var RelayInputHandlers = class {
2126
2206
  ts.write(serializeRawPtyInput(sessionId, data));
2127
2207
  serviceLogger.info({ sessionId, bytes: data.length }, "Raw PTY input forwarded");
2128
2208
  }
2209
+ onClipboardImageUpload(msg) {
2210
+ const sessionId = msg.sessionId;
2211
+ const requestId = msg.requestId;
2212
+ if (!sessionId) return;
2213
+ const session = this.deps.sessionManager.getSession(sessionId);
2214
+ if (!session) {
2215
+ this.deps.relayConnection.sendRaw(
2216
+ JSON.stringify({
2217
+ type: "clipboard_image_upload_response",
2218
+ requestId,
2219
+ sessionId,
2220
+ success: false,
2221
+ path: "",
2222
+ error: "\u4F1A\u8BDD\u4E0D\u5B58\u5728",
2223
+ errorCode: ControlErrorCode.SESSION_NOT_FOUND
2224
+ })
2225
+ );
2226
+ serviceLogger.warn({ sessionId }, "Clipboard image upload rejected: session not found");
2227
+ return;
2228
+ }
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
+ });
2235
+ this.deps.relayConnection.sendRaw(
2236
+ JSON.stringify({
2237
+ type: "clipboard_image_upload_response",
2238
+ requestId,
2239
+ sessionId,
2240
+ ...result
2241
+ })
2242
+ );
2243
+ serviceLogger.info({ sessionId, success: result.success }, "Clipboard image upload handled");
2244
+ }
2129
2245
  };
2130
2246
 
2131
2247
  // src/serve/relay-history-handlers.ts
@@ -2372,7 +2488,7 @@ var RelayResourceHandlers = class {
2372
2488
  }
2373
2489
  try {
2374
2490
  const path = validateExecutablePath(rawPath ?? "");
2375
- saveAgentCliPath(provider, path, { envName: this.deps.envName });
2491
+ saveAgentCliPath(provider, path);
2376
2492
  this.deps.setAgentCliPath(provider, path);
2377
2493
  const agentCli = detectAgentCliStatus(this.deps.getProviderEnv(), {
2378
2494
  suggestions: this.deps.getAgentCliSuggestions()
@@ -2442,8 +2558,8 @@ var RelayResourceHandlers = class {
2442
2558
 
2443
2559
  // src/serve/relay-session-create-handler.ts
2444
2560
  import { rmSync, statSync as statSync2 } from "fs";
2445
- import { isAbsolute as isAbsolute3 } from "path";
2446
- import { nanoid as nanoid3 } from "nanoid";
2561
+ import { isAbsolute as isAbsolute4 } from "path";
2562
+ import { nanoid as nanoid4 } from "nanoid";
2447
2563
 
2448
2564
  // src/serve/hosted-pty-registry.ts
2449
2565
  import * as pty from "node-pty";
@@ -2595,6 +2711,7 @@ var HostedPtyRegistry = class {
2595
2711
  hosted.lastOutputTime = Date.now();
2596
2712
  hosted.outputSeq += 1;
2597
2713
  hosted.terminal.write(data);
2714
+ this.deps.touchSessionActivity(sessionId);
2598
2715
  this.sendBinary(sessionId, Buffer.from(data, "utf-8"), hosted.outputSeq);
2599
2716
  const oscSequences = extractOscSequences(data);
2600
2717
  const session = this.deps.sessionManager.getSession(sessionId);
@@ -2731,7 +2848,7 @@ function validateSessionCwd(cwd) {
2731
2848
  return { message: "\u8BF7\u8F93\u5165\u5DE5\u4F5C\u76EE\u5F55", code: ControlErrorCode.INVALID_PATH };
2732
2849
  }
2733
2850
  const trimmed = cwd.trim();
2734
- if (!isAbsolute3(trimmed)) {
2851
+ if (!isAbsolute4(trimmed)) {
2735
2852
  return { message: "\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84", code: ControlErrorCode.INVALID_PATH };
2736
2853
  }
2737
2854
  try {
@@ -2790,7 +2907,7 @@ var RelaySessionCreateHandler = class {
2790
2907
  const resumeSessionId = msg.resumeSessionId;
2791
2908
  const streamDelta = msg.streamDelta === true;
2792
2909
  const name = tildify(sessionCwd);
2793
- const pendingId = nanoid3();
2910
+ const pendingId = nanoid4();
2794
2911
  const hook = this.deps.createHookContext(pendingId, provider);
2795
2912
  const workerPid = this.deps.workerRegistry.spawn(pendingId, {
2796
2913
  cwd: sessionCwd,
@@ -2875,7 +2992,7 @@ var RelaySessionCreateHandler = class {
2875
2992
  return;
2876
2993
  }
2877
2994
  const resumeSessionId = msg.resumeSessionId;
2878
- const pendingId = nanoid3();
2995
+ const pendingId = nanoid4();
2879
2996
  const name = tildify(cwd);
2880
2997
  const hook = this.deps.createHookContext(pendingId, provider);
2881
2998
  try {
@@ -2990,7 +3107,6 @@ var RelayRouter = class {
2990
3107
  relaySend: deps.relaySend,
2991
3108
  controlHandlers: deps.controlHandlers,
2992
3109
  sessionManager: deps.sessionManager,
2993
- envName: deps.envName,
2994
3110
  getProviderEnv: deps.getProviderEnv,
2995
3111
  getAgentCliSuggestions: deps.getAgentCliSuggestions,
2996
3112
  setAgentCliPath: deps.setAgentCliPath
@@ -3042,6 +3158,7 @@ var RelayRouter = class {
3042
3158
  handlers = {
3043
3159
  user_input: (msg) => this.inputHandlers.onUserInput(msg),
3044
3160
  remote_input_raw: (msg) => this.inputHandlers.onRemoteInputRaw(msg),
3161
+ clipboard_image_upload: (msg) => this.inputHandlers.onClipboardImageUpload(msg),
3045
3162
  tool_approve: (msg) => this.permissionHandlers.onToolApprove(msg),
3046
3163
  tool_deny: (msg) => this.permissionHandlers.onToolDeny(msg),
3047
3164
  proxy_info_request: (msg) => this.resourceHandlers.onProxyInfoRequest(msg),
@@ -3232,11 +3349,11 @@ var PermissionBroker = class {
3232
3349
  message: "Duplicate permission request id."
3233
3350
  });
3234
3351
  }
3235
- return new Promise((resolve) => {
3352
+ return new Promise((resolve2) => {
3236
3353
  this.pending.set(request.requestId, {
3237
3354
  ...request,
3238
3355
  source: "hook",
3239
- resolve,
3356
+ resolve: resolve2,
3240
3357
  createdAt: Date.now()
3241
3358
  });
3242
3359
  });
@@ -3460,6 +3577,7 @@ var AgentStatusRegistry = class {
3460
3577
  };
3461
3578
 
3462
3579
  // src/serve/session-broadcast.ts
3580
+ var ACTIVITY_STATUS_PUSH_INTERVAL_MS = 15e3;
3463
3581
  function toSessionListPayload(s) {
3464
3582
  return {
3465
3583
  sessionId: s.id,
@@ -3522,6 +3640,11 @@ function changeSessionState(sessionManager, relay, sessionId, next) {
3522
3640
  if (changed) pushSessionStatus(relay, sessionManager, sessionId);
3523
3641
  return changed;
3524
3642
  }
3643
+ function touchSessionActivity(sessionManager, relay, sessionId, now = Date.now()) {
3644
+ const touched = sessionManager.touchSession(sessionId, now, ACTIVITY_STATUS_PUSH_INTERVAL_MS);
3645
+ if (touched) pushSessionStatus(relay, sessionManager, sessionId);
3646
+ return touched;
3647
+ }
3525
3648
 
3526
3649
  // src/serve/service-files.ts
3527
3650
  import { execSync } from "child_process";
@@ -3529,10 +3652,10 @@ import { existsSync as existsSync5, readFileSync as readFileSync5, unlinkSync as
3529
3652
  import { hostname } from "os";
3530
3653
  import { connect as connect2 } from "net";
3531
3654
  function tryConnectSocket(sockPath) {
3532
- return new Promise((resolve) => {
3655
+ return new Promise((resolve2) => {
3533
3656
  const s = connect2(sockPath);
3534
- s.on("connect", () => resolve(s));
3535
- s.on("error", () => resolve(null));
3657
+ s.on("connect", () => resolve2(s));
3658
+ s.on("error", () => resolve2(null));
3536
3659
  });
3537
3660
  }
3538
3661
  function isProcessAlive(pid) {
@@ -3569,8 +3692,13 @@ async function cleanupStaleResources() {
3569
3692
  serviceLogger.info("Removed stale PID file");
3570
3693
  }
3571
3694
  }
3695
+ function formatProxyNameForProfile(baseName, profileName = PROFILE_NAME) {
3696
+ return profileName === DEFAULT_PROXY_PROFILE ? baseName : `${baseName} (${profileName})`;
3697
+ }
3572
3698
  function getProxyName() {
3573
- return process.env.DEV_ANYWHERE_PROXY_NAME || getComputerName() || hostname();
3699
+ const explicitName = process.env.DEV_ANYWHERE_PROXY_NAME?.trim();
3700
+ if (explicitName) return explicitName;
3701
+ return formatProxyNameForProfile(getComputerName() || hostname());
3574
3702
  }
3575
3703
  function getComputerName() {
3576
3704
  try {
@@ -3876,6 +4004,7 @@ function handleTerminalConnection(socket, deps) {
3876
4004
  },
3877
4005
  (sessionId, data, outputSeq) => {
3878
4006
  if (!sessionManager.getSession(sessionId)) return;
4007
+ touchSessionActivity(sessionManager, relayConnection, sessionId);
3879
4008
  const sessionIdBuf = Buffer.from(sessionId, "utf-8");
3880
4009
  const wsFrame = Buffer.alloc(1 + sessionIdBuf.length + 4 + data.length);
3881
4010
  wsFrame[0] = sessionIdBuf.length;
@@ -3926,7 +4055,7 @@ function handleTerminalConnection(socket, deps) {
3926
4055
 
3927
4056
  // src/serve/hook-registry.ts
3928
4057
  import { createHash, randomBytes } from "crypto";
3929
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
4058
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
3930
4059
  import { dirname as dirname4 } from "path";
3931
4060
  import { z } from "zod";
3932
4061
  var PersistedHookSessionBindingSchema = z.object({
@@ -4006,9 +4135,9 @@ var HookRegistry = class {
4006
4135
  save() {
4007
4136
  if (!this.persistPath) return;
4008
4137
  try {
4009
- mkdirSync4(dirname4(this.persistPath), { recursive: true });
4138
+ mkdirSync5(dirname4(this.persistPath), { recursive: true });
4010
4139
  const tmpPath = `${this.persistPath}.${process.pid}.${Date.now()}.tmp`;
4011
- writeFileSync4(
4140
+ writeFileSync5(
4012
4141
  tmpPath,
4013
4142
  JSON.stringify(
4014
4143
  {
@@ -4060,7 +4189,7 @@ var HookServer = class {
4060
4189
  this.writeJson(res, 500, { error: "internal_error" });
4061
4190
  });
4062
4191
  });
4063
- return new Promise((resolve, reject) => {
4192
+ return new Promise((resolve2, reject) => {
4064
4193
  const onError = (err) => {
4065
4194
  this.server?.off("listening", onListening);
4066
4195
  reject(err);
@@ -4068,7 +4197,7 @@ var HookServer = class {
4068
4197
  const onListening = () => {
4069
4198
  this.server?.off("error", onError);
4070
4199
  serviceLogger.info({ host: this.host, port: this.options.port }, "Hook server listening");
4071
- resolve();
4200
+ resolve2();
4072
4201
  };
4073
4202
  this.server.once("error", onError);
4074
4203
  this.server.once("listening", onListening);
@@ -4079,8 +4208,8 @@ var HookServer = class {
4079
4208
  if (!this.server) return Promise.resolve();
4080
4209
  const server = this.server;
4081
4210
  this.server = null;
4082
- return new Promise((resolve, reject) => {
4083
- server.close((err) => err ? reject(err) : resolve());
4211
+ return new Promise((resolve2, reject) => {
4212
+ server.close((err) => err ? reject(err) : resolve2());
4084
4213
  });
4085
4214
  }
4086
4215
  getListeningPort() {
@@ -4194,7 +4323,7 @@ var HookServer = class {
4194
4323
  this.writeJson(res, 200, payload);
4195
4324
  }
4196
4325
  readBody(req) {
4197
- return new Promise((resolve, reject) => {
4326
+ return new Promise((resolve2, reject) => {
4198
4327
  let body = "";
4199
4328
  let size = 0;
4200
4329
  req.setEncoding("utf8");
@@ -4207,7 +4336,7 @@ var HookServer = class {
4207
4336
  }
4208
4337
  body += chunk;
4209
4338
  });
4210
- req.on("end", () => resolve(body));
4339
+ req.on("end", () => resolve2(body));
4211
4340
  req.on("error", reject);
4212
4341
  });
4213
4342
  }
@@ -4306,24 +4435,25 @@ function parseServiceOptions(argv) {
4306
4435
  const options = {};
4307
4436
  for (let i = 0; i < argv.length; i++) {
4308
4437
  const arg = argv[i];
4309
- if (arg === "--env") {
4310
- const envName = argv[i + 1];
4311
- if (!envName || envName.startsWith("-")) {
4312
- throw new Error("Missing value for --env");
4438
+ if (arg === "--relay") {
4439
+ const relayName = argv[i + 1];
4440
+ if (!relayName || relayName.startsWith("-")) {
4441
+ throw new Error("Missing value for --relay");
4313
4442
  }
4314
- options.envName = envName;
4443
+ options.relayName = relayName;
4315
4444
  i++;
4316
4445
  continue;
4317
4446
  }
4318
- if (arg.startsWith("--env=")) {
4319
- const envName = arg.slice("--env=".length);
4320
- if (!envName) throw new Error("Missing value for --env");
4321
- options.envName = envName;
4447
+ if (arg.startsWith("--relay=")) {
4448
+ const relayName = arg.slice("--relay=".length);
4449
+ if (!relayName) throw new Error("Missing value for --relay");
4450
+ options.relayName = relayName;
4322
4451
  }
4323
4452
  }
4324
4453
  return options;
4325
4454
  }
4326
4455
  async function startService(options) {
4456
+ ensureProfileWorkspace();
4327
4457
  await cleanupStaleResources();
4328
4458
  try {
4329
4459
  unlinkSync3(STOPPED_PATH);
@@ -4351,7 +4481,7 @@ async function startService(options) {
4351
4481
  sessionManager.startReaper();
4352
4482
  const terminalSockets = /* @__PURE__ */ new Map();
4353
4483
  const proxyName = getProxyName();
4354
- let proxyConfig = loadConfig({ envName: options?.envName });
4484
+ let proxyConfig = loadConfig({ relayName: options?.relayName });
4355
4485
  const getProviderEnv = () => buildProviderEnv(proxyConfig, process.env);
4356
4486
  const getAgentCliSuggestions = () => proxyConfig.agentCliSuggestions;
4357
4487
  const setAgentCliPath = (provider, path) => {
@@ -4373,8 +4503,9 @@ async function startService(options) {
4373
4503
  const relayUrl = options?.relayUrl ?? proxyConfig.relayUrl;
4374
4504
  const relayToken = proxyConfig.relayToken;
4375
4505
  const statusConfig = {
4376
- envName: proxyConfig.envName,
4377
- envNameSource: proxyConfig.sources.envName,
4506
+ profile: PROFILE_NAME,
4507
+ relayName: proxyConfig.relayName,
4508
+ relayNameSource: proxyConfig.sources.relayName,
4378
4509
  relayUrl,
4379
4510
  relayUrlSource: proxyConfig.sources.relayUrl,
4380
4511
  relayTokenSource: proxyConfig.sources.relayToken,
@@ -4382,15 +4513,20 @@ async function startService(options) {
4382
4513
  hookPortSource: proxyConfig.sources.hookPort
4383
4514
  };
4384
4515
  if (!relayUrl) {
4385
- const msg = 'Relay URL is required. Set it via RELAY_URL or ~/.dev-anywhere/config.json {"defaultEnv":"local","envs":{"local":{"relayUrl":"ws://..."}}}.';
4516
+ const msg = `Relay URL is required. Set relays.${proxyConfig.relayName}.url in ~/.dev-anywhere/config.json or pass --relay <name>.`;
4386
4517
  serviceLogger.error(msg);
4387
4518
  console.error(msg);
4388
4519
  process.exit(1);
4389
4520
  }
4390
- const relayConnection = new RelayConnection(relayUrl, { name: proxyName, token: relayToken });
4521
+ const relayConnection = new RelayConnection(relayUrl, {
4522
+ name: proxyName,
4523
+ token: relayToken,
4524
+ proxyIdPath: PROXY_ID_PATH
4525
+ });
4391
4526
  const relaySend = (data) => relayConnection.sendRaw(data);
4392
4527
  const controlHandlers = createControlMessageHandlers(relaySend, sessionManager);
4393
4528
  const observerChangeState = (sessionId, next) => changeSessionState(sessionManager, relayConnection, sessionId, next);
4529
+ const observerTouchActivity = (sessionId) => touchSessionActivity(sessionManager, relayConnection, sessionId);
4394
4530
  const emitAgentStatus = (sessionId, phase) => {
4395
4531
  const session = sessionManager.getSession(sessionId);
4396
4532
  if (!session) return;
@@ -4421,6 +4557,7 @@ async function startService(options) {
4421
4557
  permissionBroker,
4422
4558
  relayConnection,
4423
4559
  jsonObserver,
4560
+ touchSessionActivity: observerTouchActivity,
4424
4561
  getProviderEnv
4425
4562
  });
4426
4563
  const hostedPtyRegistry = new HostedPtyRegistry({
@@ -4428,6 +4565,7 @@ async function startService(options) {
4428
4565
  relayConnection,
4429
4566
  getProviderEnv,
4430
4567
  changeSessionState: observerChangeState,
4568
+ touchSessionActivity: observerTouchActivity,
4431
4569
  onTurnComplete: (sessionId) => {
4432
4570
  resolveInterruptedApprovals(
4433
4571
  permissionBroker,
@@ -4446,7 +4584,8 @@ async function startService(options) {
4446
4584
  relayConnection.connect();
4447
4585
  serviceLogger.info(
4448
4586
  {
4449
- envName: proxyConfig.envName ?? "(single)",
4587
+ relayName: proxyConfig.relayName,
4588
+ profile: PROFILE_NAME,
4450
4589
  relayUrl,
4451
4590
  proxyName,
4452
4591
  tokenSet: !!relayToken,
@@ -4470,7 +4609,6 @@ async function startService(options) {
4470
4609
  permissionBroker,
4471
4610
  hookEventRouter: hookRuntime.hookEventRouter,
4472
4611
  agentStatusRegistry,
4473
- envName: proxyConfig.envName,
4474
4612
  getProviderEnv,
4475
4613
  getAgentCliSuggestions,
4476
4614
  setAgentCliPath
@@ -4513,7 +4651,7 @@ async function startService(options) {
4513
4651
  });
4514
4652
  });
4515
4653
  server.listen(SOCK_PATH, () => {
4516
- writeFileSync5(PID_PATH, String(process.pid));
4654
+ writeFileSync6(PID_PATH, String(process.pid));
4517
4655
  chmodSync(SOCK_PATH, 384);
4518
4656
  serviceLogger.info({ pid: process.pid, sock: SOCK_PATH }, "Service started");
4519
4657
  });