agentroam 0.2.0-preview.48 → 0.2.0-preview.53

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/README.md CHANGED
@@ -40,7 +40,7 @@ npx agentroam
40
40
 
41
41
  On first run:
42
42
 
43
- 1. AgentRoam tries a bundled **Cloudflare Quick Tunnel**, then a **Pinggy HTTPS tunnel** over SSH port 443.
43
+ 1. AgentRoam tries a bundled **Cloudflare Quick Tunnel** for up to 30 seconds, then uses **LAN access** if that attempt fails or times out.
44
44
  2. A **QR code** prints in the terminal — scan it on your phone.
45
45
  3. Complete **one-time pairing** (5-minute token) and create the admin account.
46
46
  4. Use the web terminal + file tree remotely.
@@ -49,7 +49,7 @@ On first run:
49
49
 
50
50
  ```bash
51
51
  agentroam # start (default)
52
- agentroam doctor # check native modules + platform
52
+ agentroam doctor # check native modules and install Codex if missing
53
53
  agentroam version
54
54
  agent-tui # start the bundled terminal Agent UI
55
55
  ```
@@ -103,7 +103,7 @@ hibernation, shutdown, and low-battery forced sleep remain controlled by the OS.
103
103
  |------|-------------|
104
104
  | `--root <path>` | Workspace root (repeatable). Default: cwd |
105
105
  | `--local-only` | Skip tunnel; LAN/localhost only |
106
- | `--relay auto` | Cloudflare, then Pinggy, then LAN (default) |
106
+ | `--relay auto` | Cloudflare, then LAN on failure or timeout (default) |
107
107
  | `--relay cloudflare` | Cloudflare only, then LAN |
108
108
  | `--relay pinggy` | Pinggy only, then LAN |
109
109
  | `--relay custom --tunnel-command "<cmd>"` | Your own tunnel command |
@@ -111,7 +111,9 @@ hibernation, shutdown, and low-battery forced sleep remain controlled by the OS.
111
111
  | `--port <n>` | Fixed local port |
112
112
  | `--data-dir <path>` | State directory (default: `~/.agentroam`) |
113
113
 
114
- The platform cloudflared binary is installed from npm after size and SHA-256 verification, so startup does not download from GitHub. If Cloudflare cannot connect, automatic mode tries Pinggy over outbound SSH port 443. Pinggy free tunnels last up to 60 minutes and may show a one-time security confirmation in the phone browser. If neither public relay works, the QR code uses a LAN address and the phone must be on the same local network.
114
+ The platform cloudflared binary is installed from npm after size and SHA-256 verification, so startup does not download from GitHub. Automatic mode attempts Cloudflare for at most 30 seconds, including connection and public health checks. Any failure falls back directly to LAN, where the phone must be on the same local network. Pinggy is only used when explicitly selected with `--relay pinggy`; a failed explicit relay also falls back to LAN.
115
+
116
+ Startup reuses a compatible installed Codex CLI. Only when none is found does it install `@openai/codex@latest`; Codex preparation is limited to 180 seconds. Installed OpenCode is reused, and missing OpenCode is skipped without downloading. If Codex installation fails or times out, the local service still starts with an explicit warning that Codex sessions are unavailable. Run `agentroam doctor` to repair Codex, then restart the service. Remote desktop uses the bundled native runtime and still requires system authorization.
115
117
 
116
118
  ## What's included
117
119
 
package/dist/abort.js ADDED
@@ -0,0 +1,25 @@
1
+ /** Bound an asynchronous operation even when its implementation ignores cancellation. */
2
+ export async function withAbortSignal(operation, signal) {
3
+ signal.throwIfAborted();
4
+ return new Promise((resolve, reject) => {
5
+ const abort = () => {
6
+ signal.removeEventListener("abort", abort);
7
+ reject(signal.reason);
8
+ };
9
+ signal.addEventListener("abort", abort, { once: true });
10
+ Promise.resolve().then(() => {
11
+ signal.throwIfAborted();
12
+ return operation();
13
+ }).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
14
+ });
15
+ }
16
+ export async function withTimeout(operation, timeoutMs, message) {
17
+ const controller = new AbortController();
18
+ const timer = setTimeout(() => controller.abort(new Error(message)), timeoutMs);
19
+ try {
20
+ return await withAbortSignal(() => operation(controller.signal), controller.signal);
21
+ }
22
+ finally {
23
+ clearTimeout(timer);
24
+ }
25
+ }
package/dist/cli.js CHANGED
@@ -11,7 +11,8 @@ import { installedDataDir, pairingAdmin, printPairingCode, terminalText } from "
11
11
  import { detectPlatform } from "./platform.js";
12
12
  import { AGENTROAM_VERSION, resolvePlatformRuntime, resolvePlatformTui } from "./platform-packages.js";
13
13
  import { acquireSleepInhibitor } from "./power/sleep-inhibitor.js";
14
- import { RuntimeManager } from "./runtime-manager.js";
14
+ import { CODEX_PREPARATION_TIMEOUT_MS, RuntimeManager } from "./runtime-manager.js";
15
+ import { withTimeout } from "./abort.js";
15
16
  import { ServiceRuntimeReporter } from "./service/runtime-state.js";
16
17
  import { runServiceCommand } from "./service/service-command.js";
17
18
  import { resolveServicePaths } from "./service/service-files.js";
@@ -197,7 +198,7 @@ async function executeCommand(options, argv) {
197
198
  await serviceReporter?.localReady(runtime.localUrl);
198
199
  if (!serviceReporter)
199
200
  printLocalDesktopUrl(runtime.localUrl);
200
- const lanUrl = options.localOnly ? findLanUrl(runtime.port) ?? runtime.localUrl : runtime.localUrl;
201
+ const lanUrl = findLanUrl(runtime.port) ?? runtime.localUrl;
201
202
  const relayRuntime = runtime;
202
203
  relay = await withCliProgress("正在建立手机访问连接", () => selectRelay({
203
204
  cli: options,
@@ -207,9 +208,8 @@ async function executeCommand(options, argv) {
207
208
  port: relayRuntime.port,
208
209
  signal: controller.signal,
209
210
  log: (line) => process.stderr.write(`${line}\n`),
210
- onAttempt: (provider) => console.log(`▲ Trying ${providerDisplayName(provider)} relay...`),
211
- onFailure: (provider, message) => console.error(`⚠ ${providerDisplayName(provider)} unavailable: ${message}`),
212
- allowLanFallback: options.localOnly,
211
+ onAttempt: (provider) => console.log(`▲ 正在连接 ${providerDisplayName(provider)}(最多 30 秒,失败后使用局域网)…`),
212
+ onFailure: (provider, message) => console.error(`⚠ ${providerDisplayName(provider)} 不可用:${message};正在切换到局域网。`),
213
213
  }));
214
214
  if (controller.signal.aborted) {
215
215
  await relay.tunnel?.close();
@@ -220,6 +220,7 @@ async function executeCommand(options, argv) {
220
220
  console.error(` Continuing with local network access: ${relay.publicUrl}/web`);
221
221
  else
222
222
  console.log(`✓ Local network: ${relay.publicUrl}/web`);
223
+ console.log(lanUrl === runtime.localUrl ? "当前没有可用的局域网地址,可先在本机使用;连接 Wi-Fi 或有线网络后重启服务以生成手机访问地址。" : "手机与电脑连接同一局域网后,即可使用以上地址或二维码连接。");
223
224
  }
224
225
  else {
225
226
  console.log(`✓ ${providerDisplayName(relay.provider)} tunnel ready: ${relay.publicUrl}`);
@@ -297,11 +298,12 @@ async function doctor(target, dataDir) {
297
298
  reportDoctorFailure(error, "agent-tui: ");
298
299
  }
299
300
  try {
300
- const codex = await withCliProgress("正在检查和准备 Codex", () => resolveCodexRuntime({
301
+ const codex = await withCliProgress("正在检查和准备 Codex(最多 180 秒)", () => withTimeout(signal => resolveCodexRuntime({
301
302
  dataDir,
302
303
  target,
304
+ signal,
303
305
  onProgress: (message) => console.log(`… ${message}`),
304
- }));
306
+ }), CODEX_PREPARATION_TIMEOUT_MS, "Codex 组件准备超时(180 秒);请检查网络后重试。"));
305
307
  console.log(`✓ Codex ${codex.version} (${codex.source}) ${codex.executable}`);
306
308
  }
307
309
  catch (error) {
@@ -3,22 +3,25 @@ import { constants } from "node:fs";
3
3
  import { access, chmod, mkdir, mkdtemp, open, readdir, rename, rm, stat, } from "node:fs/promises";
4
4
  import { delimiter, dirname, isAbsolute, relative, resolve, win32 } from "node:path";
5
5
  import { promisify } from "node:util";
6
+ import { withAbortSignal } from "./abort.js";
6
7
  const execFileAsync = promisify(execFile);
7
8
  // Fresh installs follow npm's latest stable release; existing installs have an independent floor.
8
9
  export const CODEX_RUNTIME_TAG = "latest";
9
10
  export const CODEX_MINIMUM_VERSION = "0.153.0";
10
11
  const CODEX_PACKAGE = `@openai/codex@${CODEX_RUNTIME_TAG}`;
11
12
  const NPM_REGISTRY = "https://registry.npmjs.org";
12
- export const CODEX_INSTALL_TIMEOUT_MS = 60 * 60_000;
13
+ export const CODEX_INSTALL_TIMEOUT_MS = 180_000;
13
14
  const DEFAULT_LOCK_WAIT_MS = 190_000;
14
15
  export const CODEX_INSTALL_STALE_LOCK_MS = CODEX_INSTALL_TIMEOUT_MS + 5 * 60_000;
15
16
  const DEFAULT_STALE_LOCK_MS = CODEX_INSTALL_STALE_LOCK_MS;
16
17
  const defaultDependencies = {
17
- async run(command, args, timeoutMs) {
18
+ async run(command, args, timeoutMs, signal) {
18
19
  try {
19
20
  const { stdout, stderr } = await execFileAsync(command, args, {
20
21
  encoding: "utf8",
21
22
  timeout: timeoutMs,
23
+ signal,
24
+ killSignal: "SIGKILL",
22
25
  maxBuffer: 4 * 1024 * 1024,
23
26
  windowsHide: true,
24
27
  });
@@ -36,9 +39,21 @@ const defaultDependencies = {
36
39
  now: () => Date.now(),
37
40
  };
38
41
  export async function resolveCodexRuntime(options) {
42
+ return options.signal
43
+ ? withAbortSignal(() => resolveRuntime(options), options.signal)
44
+ : resolveRuntime(options);
45
+ }
46
+ async function resolveRuntime(options) {
39
47
  const environment = options.environment ?? process.env;
40
48
  const platform = options.platform ?? process.platform;
41
49
  const dependencies = { ...defaultDependencies, ...options.dependencies };
50
+ const signal = options.signal;
51
+ if (signal) {
52
+ const run = dependencies.run;
53
+ const sleep = dependencies.sleep;
54
+ dependencies.run = (command, args, timeoutMs) => withAbortSignal(() => run(command, args, timeoutMs, signal), signal);
55
+ dependencies.sleep = (delayMs) => withAbortSignal(() => sleep(delayMs), signal);
56
+ }
42
57
  const explicit = environment.AGENT_CODEX_BIN?.trim();
43
58
  if (explicit) {
44
59
  const absolute = platform === "win32" ? win32.isAbsolute(explicit) : isAbsolute(explicit);
@@ -54,6 +69,9 @@ export async function resolveCodexRuntime(options) {
54
69
  const managed = await findManagedRuntime(managedParent, options.target, platform, dependencies);
55
70
  if (managed)
56
71
  return managed;
72
+ signal?.throwIfAborted();
73
+ if (options.installIfMissing === false)
74
+ throw new Error("Codex is not installed; run agentroam doctor to install it.");
57
75
  options.onProgress?.(`Installing managed Codex from ${CODEX_PACKAGE}...`);
58
76
  return installManagedCodex({
59
77
  ...options,
@@ -199,6 +217,7 @@ async function installManagedCodex(options) {
199
217
  throw new Error(`Codex runtime validation failed after installing ${CODEX_PACKAGE}`);
200
218
  }
201
219
  const managedRoot = resolve(parent, temporary.version);
220
+ options.signal?.throwIfAborted();
202
221
  // An invalid cache for this version must not block a validated replacement.
203
222
  await rm(managedRoot, { recursive: true, force: true });
204
223
  await rename(temporaryRoot, managedRoot);
@@ -209,6 +228,7 @@ async function installManagedCodex(options) {
209
228
  return installed;
210
229
  }
211
230
  catch (error) {
231
+ options.signal?.throwIfAborted();
212
232
  const concurrent = await findManagedRuntime(parent, options.target, options.platform, options.dependencies);
213
233
  if (concurrent)
214
234
  return concurrent;
@@ -47,7 +47,7 @@ export async function configureCodexSharing(options, context = {}) {
47
47
  };
48
48
  const json = `${paths.root}/job.json`;
49
49
  const temporary = `${paths.root}/job.plist`;
50
- const commandOptions = { encoding: "utf8", timeout: 20_000, maxBuffer: 1024 * 1024 };
50
+ const commandOptions = { encoding: "utf8", timeout: 20_000, maxBuffer: 1024 * 1024, signal: context.signal, killSignal: "SIGKILL" };
51
51
  // Identical startup calls must not unload/reload an already healthy job.
52
52
  let unchanged = false;
53
53
  try {
@@ -1,5 +1,22 @@
1
1
  import { chmod } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ const execute = promisify(execFile);
6
+ const WINDOWS_PTY_PROBE = `
7
+ const pty = require(process.argv[1]);
8
+ const marker = "agentroam-runtime-probe";
9
+ const terminal = pty.spawn(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", "echo " + marker], {
10
+ name: "xterm-256color", cols: 80, rows: 24, cwd: process.cwd(), env: process.env,
11
+ });
12
+ let output = "";
13
+ const timer = setTimeout(() => process.exit(1), 5000);
14
+ terminal.onData(chunk => { output = (output + chunk).slice(-4096); });
15
+ terminal.onExit(({ exitCode }) => {
16
+ clearTimeout(timer);
17
+ process.exit(exitCode === 0 && output.includes(marker) ? 0 : 1);
18
+ });
19
+ `;
3
20
  export function probeSQLiteRuntime(runtimeRequire) {
4
21
  const Database = runtimeRequire("better-sqlite3");
5
22
  const database = new Database(":memory:");
@@ -15,12 +32,17 @@ export function probeSQLiteRuntime(runtimeRequire) {
15
32
  }
16
33
  }
17
34
  export async function probeTerminalRuntime(runtimeRequire, platform = process.platform) {
35
+ if (platform === "win32") {
36
+ // ConPTY retains an output worker after shell exit. Keep the probe in an
37
+ // owned child process so terminating it releases all handles together.
38
+ await execute(process.execPath, ["--input-type=commonjs", "-e", WINDOWS_PTY_PROBE, runtimeRequire.resolve("node-pty")], {
39
+ timeout: 10_000, windowsHide: true, maxBuffer: 64 * 1024,
40
+ });
41
+ return;
42
+ }
18
43
  const pty = runtimeRequire("node-pty");
19
- const windows = platform === "win32";
20
44
  const marker = "agentroam-runtime-probe";
21
- const terminal = pty.spawn(windows ? (process.env.ComSpec || "cmd.exe") : "/bin/sh", windows
22
- ? ["/d", "/s", "/c", `echo ${marker}`]
23
- : ["-c", `printf ${marker}`], {
45
+ const terminal = pty.spawn("/bin/sh", ["-c", `printf ${marker}`], {
24
46
  name: "xterm-256color", cols: 80, rows: 24, cwd: process.cwd(), env: process.env,
25
47
  });
26
48
  await new Promise((resolveProbe, rejectProbe) => {
@@ -4,6 +4,7 @@ import { access, chmod, mkdir, mkdtemp, open, rename, rm, stat } from "node:fs/p
4
4
  import { delimiter, isAbsolute, relative, resolve, win32 } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { resolveNpmExecutor } from "./codex-runtime-manager.js";
7
+ import { withAbortSignal } from "./abort.js";
7
8
  const execFileAsync = promisify(execFile);
8
9
  // Default managed installation; compatibility has an independent minimum.
9
10
  export const OPENCODE_RUNTIME_VERSION = "1.18.27";
@@ -11,11 +12,13 @@ export const OPENCODE_MINIMUM_VERSION = "1.18.27";
11
12
  const OPENCODE_PACKAGE = `opencode-ai@${OPENCODE_RUNTIME_VERSION}`;
12
13
  const NPM_REGISTRY = "https://registry.npmjs.org";
13
14
  const defaults = {
14
- async run(command, args, timeoutMs) {
15
+ async run(command, args, timeoutMs, signal) {
15
16
  try {
16
17
  const { stdout, stderr } = await execFileAsync(command, args, {
17
18
  encoding: "utf8",
18
19
  timeout: timeoutMs,
20
+ signal,
21
+ killSignal: "SIGKILL",
19
22
  maxBuffer: 4 * 1024 * 1024,
20
23
  windowsHide: true,
21
24
  });
@@ -30,9 +33,21 @@ const defaults = {
30
33
  now: () => Date.now(),
31
34
  };
32
35
  export async function resolveOpenCodeRuntime(options) {
36
+ return options.signal
37
+ ? withAbortSignal(() => resolveRuntime(options), options.signal)
38
+ : resolveRuntime(options);
39
+ }
40
+ async function resolveRuntime(options) {
33
41
  const environment = options.environment ?? process.env;
34
42
  const platform = options.platform ?? process.platform;
35
43
  const dependencies = { ...defaults, ...options.dependencies };
44
+ const signal = options.signal;
45
+ if (signal) {
46
+ const run = dependencies.run;
47
+ const sleep = dependencies.sleep;
48
+ dependencies.run = (command, args, timeoutMs) => withAbortSignal(() => run(command, args, timeoutMs, signal), signal);
49
+ dependencies.sleep = (delayMs) => withAbortSignal(() => sleep(delayMs), signal);
50
+ }
36
51
  const explicit = environment.AGENT_OPENCODE_BIN?.trim();
37
52
  if (explicit) {
38
53
  const absolute = platform === "win32" ? win32.isAbsolute(explicit) : isAbsolute(explicit);
@@ -47,6 +62,9 @@ export async function resolveOpenCodeRuntime(options) {
47
62
  const managed = await validateManaged(managedRoot, platform, dependencies);
48
63
  if (managed)
49
64
  return managed;
65
+ signal?.throwIfAborted();
66
+ if (options.installIfMissing === false)
67
+ throw new Error("OpenCode is not installed; optional OpenCode sessions are unavailable.");
50
68
  options.onProgress?.(`Installing managed OpenCode ${OPENCODE_RUNTIME_VERSION}...`);
51
69
  return install({ ...options, environment, platform, dependencies, managedRoot });
52
70
  }
@@ -114,6 +132,7 @@ async function install(options) {
114
132
  const validated = await validateManaged(temporary, options.platform, options.dependencies);
115
133
  if (!validated)
116
134
  throw new Error(`validation failed after installing ${OPENCODE_PACKAGE}`);
135
+ options.signal?.throwIfAborted();
117
136
  await rename(temporary, options.managedRoot);
118
137
  temporary = null;
119
138
  const activated = await validateManaged(options.managedRoot, options.platform, options.dependencies);
@@ -122,6 +141,7 @@ async function install(options) {
122
141
  return activated;
123
142
  }
124
143
  catch (error) {
144
+ options.signal?.throwIfAborted();
125
145
  const concurrent = await validateManaged(options.managedRoot, options.platform, options.dependencies);
126
146
  if (concurrent)
127
147
  return concurrent;
@@ -9,6 +9,10 @@ import { findAvailablePort } from "./port.js";
9
9
  import { ProcessSupervisor } from "./process-supervisor.js";
10
10
  import { configureCodexSharing } from "./codex-sharing.js";
11
11
  import { withCliProgress } from "../bin/cli-progress.mjs";
12
+ import { withAbortSignal, withTimeout } from "./abort.js";
13
+ export const CODEX_PREPARATION_TIMEOUT_MS = 180_000;
14
+ const OPTIONAL_RUNTIME_CHECK_TIMEOUT_MS = 60_000;
15
+ const CODEX_SHARING_TIMEOUT_MS = 10_000;
12
16
  export class RuntimeManager {
13
17
  supervisor;
14
18
  codexResolver;
@@ -24,16 +28,15 @@ export class RuntimeManager {
24
28
  const port = await findAvailablePort(options.port);
25
29
  const dataDir = resolve(options.dataDir);
26
30
  await Promise.all(["data", "bin", "cache", "logs"].map((name) => mkdir(resolve(dataDir, name), { recursive: true })));
27
- const codexEnvironment = await withCliProgress("正在准备 Codex 组件", () => prepareCodexRuntimeEnvironment(process.env, { dataDir, target }, this.codexResolver, this.report));
28
- if (process.platform === "darwin" && codexEnvironment.AGENT_CODEX_BIN) {
31
+ const childEnvironment = await withCliProgress("正在检查 Agent 组件;缺少 Codex 时自动安装最新版,最多等待 180 秒", () => prepareAgentRuntimeEnvironment(process.env, { dataDir, target }, this.codexResolver, this.report, this.opencodeResolver));
32
+ if (process.platform === "darwin" && childEnvironment.AGENT_CODEX_BIN) {
29
33
  try {
30
- await configureCodexSharing(options, { environment: codexEnvironment });
34
+ await withTimeout(signal => configureCodexSharing(options, { environment: childEnvironment, signal }), CODEX_SHARING_TIMEOUT_MS, "Codex 共享后台配置超时(10 秒)");
31
35
  }
32
36
  catch (error) {
33
37
  this.report(`Codex 共享启动配置未完成,可在电脑菜单中修复:${error instanceof Error ? error.message : error}`);
34
38
  }
35
39
  }
36
- const childEnvironment = await withCliProgress("正在准备 OpenCode 组件", () => prepareOpenCodeRuntimeEnvironment(codexEnvironment, { dataDir, target }, this.opencodeResolver, this.report));
37
40
  const { runtimeRoot } = resolvePlatformRuntime(target);
38
41
  await repairNativeRuntimePermissions(runtimeRoot);
39
42
  const gateway = resolve(runtimeRoot, "ws-server.mjs");
@@ -44,7 +47,7 @@ export class RuntimeManager {
44
47
  NODE_ENV: "production",
45
48
  NEXT_DIST_DIR: ".next",
46
49
  PORT: String(port),
47
- HOST: options.localOnly ? "0.0.0.0" : "127.0.0.1",
50
+ HOST: "0.0.0.0",
48
51
  AGENT_DATA_DIR: resolve(dataDir, "data"),
49
52
  // 应用日志收口到 <dataDir>/logs,与 launchd 的 service.stdout/stderr.log
50
53
  // 及 CLI 自身的错误日志同一个目录,用户排障时只需打包这一处。
@@ -63,7 +66,14 @@ export class RuntimeManager {
63
66
  child.once("exit", (code, signal) => resolveExit({ code, signal }));
64
67
  });
65
68
  const localUrl = `http://127.0.0.1:${port}`;
66
- const status = await withCliProgress("正在等待本机服务就绪", () => waitForHealth(`${localUrl}/api/web-auth/status`, child, 20_000));
69
+ let status;
70
+ try {
71
+ status = await withCliProgress("正在等待本机服务就绪", () => waitForHealth(`${localUrl}/api/web-auth/status`, child, 20_000));
72
+ }
73
+ catch (error) {
74
+ await this.supervisor.stopAll();
75
+ throw error;
76
+ }
67
77
  return {
68
78
  port,
69
79
  localUrl,
@@ -75,15 +85,29 @@ export class RuntimeManager {
75
85
  };
76
86
  }
77
87
  }
88
+ export async function prepareAgentRuntimeEnvironment(environment, options, codexResolver = resolveCodexRuntime, report = (message) => process.stderr.write(`${message}\n`), opencodeResolver = resolveOpenCodeRuntime) {
89
+ const [codex, opencode] = await Promise.all([
90
+ prepareCodexRuntimeEnvironment(environment, options, codexResolver, report),
91
+ prepareOpenCodeRuntimeEnvironment(environment, options, opencodeResolver, report),
92
+ ]);
93
+ return {
94
+ ...codex,
95
+ AGENT_OPENCODE_BIN: opencode.AGENT_OPENCODE_BIN,
96
+ AGENT_OPENCODE_RUNTIME_ERROR: opencode.AGENT_OPENCODE_RUNTIME_ERROR,
97
+ };
98
+ }
78
99
  export async function prepareCodexRuntimeEnvironment(environment, options, resolver = resolveCodexRuntime, report = (message) => process.stderr.write(`${message}\n`)) {
79
100
  const childEnvironment = { ...environment };
101
+ const timeoutMs = options.timeoutMs ?? CODEX_PREPARATION_TIMEOUT_MS;
80
102
  report(`Checking Codex ${options.target} runtime...`);
81
103
  try {
82
- const resolution = await resolver({
83
- ...options,
104
+ const resolution = await withTimeout(signal => resolver({
105
+ dataDir: options.dataDir,
106
+ target: options.target,
107
+ signal,
84
108
  environment,
85
109
  onProgress: report,
86
- });
110
+ }), timeoutMs, `Codex 组件准备超时(${timeoutMs / 1000} 秒);请检查网络后运行 agentroam doctor 重试。`);
87
111
  childEnvironment.AGENT_CODEX_BIN = resolution.executable;
88
112
  delete childEnvironment.AGENT_CODEX_RUNTIME_ERROR;
89
113
  report(`Codex ${resolution.version} (${resolution.source}): ${resolution.executable}`);
@@ -92,7 +116,7 @@ export async function prepareCodexRuntimeEnvironment(environment, options, resol
92
116
  delete childEnvironment.AGENT_CODEX_BIN;
93
117
  const message = error instanceof Error ? error.message : String(error);
94
118
  childEnvironment.AGENT_CODEX_RUNTIME_ERROR = message;
95
- report(`Codex unavailable; AgentRoam will continue without Codex sessions: ${message}`);
119
+ report(`⚠ Codex 会话功能未就绪,局域网服务将继续启动;运行 agentroam doctor 修复后重启服务:${message}`);
96
120
  }
97
121
  return childEnvironment;
98
122
  }
@@ -100,7 +124,9 @@ export async function prepareOpenCodeRuntimeEnvironment(environment, options, re
100
124
  const childEnvironment = { ...environment };
101
125
  report(`Checking OpenCode ${options.target} runtime...`);
102
126
  try {
103
- const resolution = await resolver({ ...options, environment, onProgress: report });
127
+ const resolution = await withTimeout(signal => resolver({
128
+ dataDir: options.dataDir, target: options.target, environment, signal, installIfMissing: false, onProgress: report,
129
+ }), options.timeoutMs ?? OPTIONAL_RUNTIME_CHECK_TIMEOUT_MS, "OpenCode 组件检查超时;已跳过,不下载安装。");
104
130
  childEnvironment.AGENT_OPENCODE_BIN = resolution.executable;
105
131
  delete childEnvironment.AGENT_OPENCODE_RUNTIME_ERROR;
106
132
  report(`OpenCode ${resolution.version} (${resolution.source}): ${resolution.executable}`);
@@ -109,22 +135,33 @@ export async function prepareOpenCodeRuntimeEnvironment(environment, options, re
109
135
  delete childEnvironment.AGENT_OPENCODE_BIN;
110
136
  const message = error instanceof Error ? error.message : String(error);
111
137
  childEnvironment.AGENT_OPENCODE_RUNTIME_ERROR = message;
112
- report(`OpenCode unavailable; AgentRoam will continue without OpenCode sessions: ${message}`);
138
+ report(`OpenCode 不可用,已跳过(不自动安装):${message}`);
113
139
  }
114
140
  return childEnvironment;
115
141
  }
116
142
  async function waitForHealth(url, child, timeoutMs) {
117
143
  const end = Date.now() + timeoutMs;
118
- while (Date.now() < end) {
119
- if (child.exitCode !== null)
120
- throw new Error(`local server exited with ${child.exitCode}`);
121
- try {
122
- const response = await fetch(url);
123
- if (response.ok)
124
- return response.json();
125
- }
126
- catch { }
127
- await new Promise((resolveWait) => setTimeout(resolveWait, 250));
144
+ const controller = new AbortController();
145
+ const timer = setTimeout(() => controller.abort(new Error("local server health check timed out")), timeoutMs);
146
+ try {
147
+ return await withAbortSignal(async () => {
148
+ while (Date.now() < end) {
149
+ controller.signal.throwIfAborted();
150
+ if (child.exitCode !== null)
151
+ throw new Error(`local server exited with ${child.exitCode}`);
152
+ try {
153
+ const response = await fetch(url, { signal: controller.signal });
154
+ if (response.ok)
155
+ return response.json();
156
+ void response.body?.cancel().catch(() => { });
157
+ }
158
+ catch { }
159
+ await new Promise((resolveWait) => setTimeout(resolveWait, 250));
160
+ }
161
+ throw new Error("local server health check timed out");
162
+ }, controller.signal);
163
+ }
164
+ finally {
165
+ clearTimeout(timer);
128
166
  }
129
- throw new Error("local server health check timed out");
130
167
  }
@@ -19,7 +19,8 @@ export class MacLaunchAgent {
19
19
  this.runner = options.runner ?? runCommand;
20
20
  this.processExists = options.processExists ?? isProcessRunning;
21
21
  this.now = options.now ?? (() => new Date());
22
- this.readyTimeoutMs = options.readyTimeoutMs ?? 120_000;
22
+ // Codex preparation (180s), sharing (10s), local server (20s), relay (30s), plus cleanup.
23
+ this.readyTimeoutMs = options.readyTimeoutMs ?? 270_000;
23
24
  this.stopTimeoutMs = options.stopTimeoutMs ?? 15_000;
24
25
  this.pollIntervalMs = options.pollIntervalMs ?? 250;
25
26
  }
@@ -12,6 +12,7 @@ import { configureCodexSharing } from "../codex-sharing.js";
12
12
  import { setupCodexDesktop } from "../codex-desktop-setup.js";
13
13
  import { authorizeRemoteDesktop } from "../desktop-authorization.js";
14
14
  import { withCliProgress } from "../../bin/cli-progress.mjs";
15
+ import { withTimeout } from "../abort.js";
15
16
  export async function runServiceCommand(options, context) {
16
17
  const platform = context.platform ?? process.platform;
17
18
  const log = context.log ?? console.log;
@@ -33,18 +34,20 @@ export async function runServiceCommand(options, context) {
33
34
  const environmentPath = buildServiceEnvironmentPath(context.nodePath, environment.PATH ?? environment.Path, platform);
34
35
  let codexPath;
35
36
  try {
36
- const codex = await withCliProgress("正在检查和安装 Codex 组件", () => (context.codexResolver ?? resolveCodexRuntime)({
37
+ const codex = await withCliProgress("正在检查已有 Codex;缺失时由后台服务限时安装最新版", () => withTimeout(signal => (context.codexResolver ?? resolveCodexRuntime)({
37
38
  dataDir: options.dataDir,
38
39
  target: detectPlatform(platform, context.arch ?? process.arch, nodeVersion),
39
40
  environment: { ...environment, PATH: environmentPath },
40
41
  platform,
41
42
  nodeExecutable: context.nodePath,
43
+ installIfMissing: false,
44
+ signal,
42
45
  onProgress: (message) => log(`… ${message}`),
43
- }));
46
+ }), 60_000, "检查已有 Codex 超时;后台服务将继续检查。"));
44
47
  codexPath = codex.executable;
45
48
  }
46
49
  catch (error) {
47
- log(`⚠ Codex runtime was not pinned during service install: ${error instanceof Error ? error.message : error}`);
50
+ log(`Codex 将在后台服务启动时检查并限时补齐:${error instanceof Error ? error.message : error}`);
48
51
  }
49
52
  const config = {
50
53
  version: context.version,
@@ -63,17 +66,20 @@ export async function runServiceCommand(options, context) {
63
66
  };
64
67
  if (platform === "darwin" && codexPath) {
65
68
  try {
66
- await (context.configureSharing ?? configureCodexSharing)(options, { homeDir: context.homeDir, environment: { ...environment, PATH: environmentPath, AGENT_CODEX_BIN: codexPath } });
69
+ await withTimeout(signal => (context.configureSharing ?? configureCodexSharing)(options, {
70
+ homeDir: context.homeDir, environment: { ...environment, PATH: environmentPath, AGENT_CODEX_BIN: codexPath }, signal,
71
+ }), 10_000, "Codex 默认共享配置超时(10 秒)");
67
72
  }
68
73
  catch (error) {
69
74
  log(`Codex 默认共享配置未完成,可在电脑菜单中修复:${error instanceof Error ? error.message : error}`);
70
75
  }
71
76
  }
72
- const { definition, state } = await withCliProgress("正在安装后台服务,等待服务和手机访问连接就绪", () => controller.install(config));
77
+ const { definition, state } = await withCliProgress("正在启动后台服务;Codex 准备最多 180 秒,公网连接最多等待 30 秒,失败后使用局域网", () => controller.install(config));
73
78
  log(`✓ AgentRoam service installed: ${definition}`);
74
79
  if (state?.localUrl && state.status !== "stopped")
75
80
  printLocalDesktopUrl(state.localUrl, log, platform);
76
81
  requireReadyService(state);
82
+ printLanFallback(state, log);
77
83
  if (state?.localUrl && state.status !== "stopped" && (platform === "darwin" || platform === "win32")) {
78
84
  if (environment.AGENTROAM_INSTALL_REMOTE_DESKTOP === "skip") {
79
85
  log("已跳过安装时远程桌面授权,可稍后执行 agentroam desktop authorize。");
@@ -100,10 +106,11 @@ export async function runServiceCommand(options, context) {
100
106
  return;
101
107
  }
102
108
  case "start": {
103
- const state = await withCliProgress("正在启动后台服务,等待服务和手机访问连接就绪", () => controller.start());
109
+ const state = await withCliProgress("正在启动后台服务;Codex 准备最多 180 秒,公网连接最多等待 30 秒,失败后使用局域网", () => controller.start());
104
110
  if (state?.localUrl && state.status !== "stopped")
105
111
  printLocalDesktopUrl(state.localUrl, log, platform);
106
112
  requireReadyService(state);
113
+ printLanFallback(state, log);
107
114
  log("✓ AgentRoam service started");
108
115
  await logAccessUrl(state.accessUrl);
109
116
  return;
@@ -126,6 +133,7 @@ export async function runServiceCommand(options, context) {
126
133
  if (status.running && status.state?.localUrl)
127
134
  printLocalDesktopUrl(status.state.localUrl, log, platform);
128
135
  if (status.running && status.state?.status === "ready" && status.state.accessUrl) {
136
+ printLanFallback(status.state, log);
129
137
  await logAccessUrl(status.state.accessUrl);
130
138
  }
131
139
  return;
@@ -144,10 +152,11 @@ export async function runServiceCommand(options, context) {
144
152
  return;
145
153
  }
146
154
  case "restart": {
147
- const state = await withCliProgress("正在重启后台服务,等待服务和手机访问连接就绪", () => controller.restart());
155
+ const state = await withCliProgress("正在重启后台服务;Codex 准备最多 180 秒,公网连接最多等待 30 秒,失败后使用局域网", () => controller.restart());
148
156
  if (state?.localUrl && state.status !== "stopped")
149
157
  printLocalDesktopUrl(state.localUrl, log, platform);
150
158
  requireReadyService(state);
159
+ printLanFallback(state, log);
151
160
  log("✓ AgentRoam service restarted");
152
161
  await logAccessUrl(state.accessUrl);
153
162
  return;
@@ -162,6 +171,15 @@ export async function runServiceCommand(options, context) {
162
171
  throw cliError("service action is required");
163
172
  }
164
173
  }
174
+ function printLanFallback(state, log) {
175
+ if (state.provider !== "lan")
176
+ return;
177
+ if (state.accessUrl && ["127.0.0.1", "localhost", "[::1]"].includes(new URL(state.accessUrl).hostname)) {
178
+ log("✓ 本机服务已启动;当前没有局域网地址,连接 Wi-Fi 或有线网络后重启服务即可生成手机访问地址。");
179
+ return;
180
+ }
181
+ log("✓ 后台服务已通过局域网启动;手机与电脑连接同一局域网后扫码连接。");
182
+ }
165
183
  function requireReadyService(state) {
166
184
  if (state?.status !== "ready" || !state.accessUrl) {
167
185
  throw Object.assign(new Error(`AgentRoam 后台服务未就绪(${state?.status ?? "未取得状态"})。请运行 agentroam service logs 查看原因,修复后运行 agentroam service start,再执行 agentroam pair。`), { exitCode: 1 });
@@ -17,7 +17,8 @@ export class WindowsTaskService {
17
17
  this.runner = options.runner ?? runCommand;
18
18
  this.processExists = options.processExists ?? isProcessRunning;
19
19
  this.now = options.now ?? (() => new Date());
20
- this.readyTimeoutMs = options.readyTimeoutMs ?? 120_000;
20
+ // Leave room for bounded component preparation, local readiness and relay fallback.
21
+ this.readyTimeoutMs = options.readyTimeoutMs ?? 270_000;
21
22
  this.stopTimeoutMs = options.stopTimeoutMs ?? 15_000;
22
23
  this.pollIntervalMs = options.pollIntervalMs ?? 250;
23
24
  this.validate = options.validateConfig ?? validateWindowsConfig;
@@ -219,7 +220,7 @@ $user = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
219
220
  $action = New-ScheduledTaskAction -Execute ${powerShellLiteral(config.nodePath)} -Argument ${powerShellLiteral(argumentsValue)} -WorkingDirectory ${powerShellLiteral(config.roots[0])}
220
221
  $trigger = New-ScheduledTaskTrigger -AtLogOn -User $user
221
222
  $principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited
222
- $settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -RestartCount 999 -RestartInterval (New-TimeSpan -Seconds 5) -ExecutionTimeLimit ([TimeSpan]::Zero) -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
223
+ $settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
223
224
  Register-ScheduledTask -TaskName ${powerShellLiteral(WINDOWS_SERVICE_NAME)} -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
224
225
  `;
225
226
  }
@@ -1,64 +1,83 @@
1
1
  import { fetch as undiciFetch, ProxyAgent } from "undici";
2
2
  import { resolveProxyForUrl } from "./proxy-settings.js";
3
3
  import { AGENTROAM_VERSION } from "../platform-packages.js";
4
+ import { withAbortSignal } from "../abort.js";
4
5
  export async function waitForPublicReadiness(publicUrl, options = {}) {
5
6
  const timeoutMs = options.timeoutMs ?? 30_000;
6
7
  const intervalMs = options.intervalMs ?? 750;
7
8
  const endpoint = new URL("/api/web-auth/status", `${publicUrl.replace(/\/$/, "")}/`);
8
- let proxyFetch = null;
9
+ const deadline = Date.now() + timeoutMs;
10
+ const controller = new AbortController();
11
+ let lastFailure = "no response";
12
+ const timer = setTimeout(() => {
13
+ const detail = lastFailure === "no response" ? "request timed out" : `${lastFailure} (request timed out)`;
14
+ controller.abort(new Error(`public tunnel not ready: ${detail}`));
15
+ }, timeoutMs);
16
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
17
+ const connection = { proxy: null };
9
18
  let fetchImpl = options.fetchImpl;
10
- if (!fetchImpl) {
11
- const proxyResolver = options.proxyResolver ?? resolveProxyForUrl;
12
- const proxyUrl = await proxyResolver(endpoint).catch(() => null);
13
- if (proxyUrl)
14
- proxyFetch = (options.proxyFetchFactory ?? createProxyFetch)(proxyUrl);
15
- fetchImpl = proxyFetch?.fetch ?? fetch;
16
- }
17
19
  try {
18
- const deadline = Date.now() + timeoutMs;
19
- let lastFailure = "no response";
20
- while (Date.now() <= deadline) {
21
- if (options.signal?.aborted)
22
- throw new Error("public tunnel readiness aborted");
23
- try {
24
- const response = await fetchImpl(endpoint, {
25
- cache: "no-store",
26
- headers: { "user-agent": `agentroam-readiness/${AGENTROAM_VERSION}` },
27
- signal: options.signal,
28
- });
29
- if (response.ok) {
30
- let body;
31
- try {
32
- body = (await response.json());
33
- }
34
- catch {
20
+ await withAbortSignal(async () => {
21
+ if (!fetchImpl) {
22
+ const proxyResolver = options.proxyResolver ?? resolveProxyForUrl;
23
+ const proxyUrl = await proxyResolver(endpoint).catch(() => null);
24
+ signal.throwIfAborted();
25
+ if (proxyUrl)
26
+ connection.proxy = (options.proxyFetchFactory ?? createProxyFetch)(proxyUrl);
27
+ fetchImpl = connection.proxy?.fetch ?? fetch;
28
+ }
29
+ while (Date.now() <= deadline) {
30
+ signal.throwIfAborted();
31
+ try {
32
+ const response = await fetchImpl(endpoint, {
33
+ cache: "no-store",
34
+ headers: { "user-agent": `agentroam-readiness/${AGENTROAM_VERSION}` },
35
+ signal,
36
+ });
37
+ if (response.ok) {
38
+ let body;
39
+ try {
40
+ body = (await response.json());
41
+ }
42
+ catch {
43
+ throw new FatalReadinessError("HTTP 200 with invalid auth status JSON");
44
+ }
45
+ signal.throwIfAborted();
46
+ if (typeof body.authenticated === "boolean" && typeof body.needsSetup === "boolean")
47
+ return;
35
48
  throw new FatalReadinessError("HTTP 200 with invalid auth status JSON");
36
49
  }
37
- if (typeof body.authenticated === "boolean" && typeof body.needsSetup === "boolean")
38
- return;
39
- throw new FatalReadinessError("HTTP 200 with invalid auth status JSON");
50
+ else {
51
+ lastFailure = `HTTP ${response.status}`;
52
+ void response.body?.cancel().catch(() => { });
53
+ if (![404, 502, 503, 530].includes(response.status))
54
+ throw new FatalReadinessError(lastFailure);
55
+ }
40
56
  }
41
- else {
42
- lastFailure = `HTTP ${response.status}`;
43
- if (![404, 502, 503, 530].includes(response.status))
44
- throw new FatalReadinessError(lastFailure);
57
+ catch (error) {
58
+ if (options.signal?.aborted)
59
+ throw new Error("public tunnel readiness aborted");
60
+ signal.throwIfAborted();
61
+ if (error instanceof FatalReadinessError)
62
+ throw new Error(`public tunnel not ready: ${error.message}`);
63
+ lastFailure = error instanceof Error ? error.message : String(error);
45
64
  }
65
+ if (Date.now() + intervalMs > deadline)
66
+ break;
67
+ await delay(intervalMs, signal);
46
68
  }
47
- catch (error) {
48
- if (options.signal?.aborted)
49
- throw new Error("public tunnel readiness aborted");
50
- if (error instanceof FatalReadinessError)
51
- throw new Error(`public tunnel not ready: ${error.message}`);
52
- lastFailure = error instanceof Error ? error.message : String(error);
53
- }
54
- if (Date.now() + intervalMs > deadline)
55
- break;
56
- await delay(intervalMs, options.signal);
57
- }
58
- throw new Error(`public tunnel not ready: ${lastFailure}`);
69
+ throw new Error(`public tunnel not ready: ${lastFailure}`);
70
+ }, signal);
71
+ }
72
+ catch (error) {
73
+ if (options.signal?.aborted)
74
+ throw new Error("public tunnel readiness aborted");
75
+ throw error;
59
76
  }
60
77
  finally {
61
- await proxyFetch?.close().catch(() => { });
78
+ clearTimeout(timer);
79
+ if (connection.proxy)
80
+ await withAbortSignal(() => connection.proxy.close(), AbortSignal.timeout(3_000)).catch(() => { });
62
81
  }
63
82
  }
64
83
  class FatalReadinessError extends Error {
@@ -68,16 +87,22 @@ function createProxyFetch(proxyUrl) {
68
87
  return {
69
88
  fetch: async (input, init) => (await undiciFetch(input, { ...init, dispatcher })),
70
89
  close: async () => {
71
- await dispatcher.close();
90
+ await dispatcher.destroy();
72
91
  },
73
92
  };
74
93
  }
75
94
  function delay(milliseconds, signal) {
76
95
  return new Promise((resolvePromise, reject) => {
77
- const timer = setTimeout(resolvePromise, milliseconds);
78
- signal?.addEventListener("abort", () => {
96
+ const abort = () => {
79
97
  clearTimeout(timer);
80
- reject(new Error("public tunnel readiness aborted"));
81
- }, { once: true });
98
+ reject(signal?.reason);
99
+ };
100
+ const timer = setTimeout(() => {
101
+ signal?.removeEventListener("abort", abort);
102
+ resolvePromise();
103
+ }, milliseconds);
104
+ signal?.addEventListener("abort", abort, { once: true });
105
+ if (signal?.aborted)
106
+ abort();
82
107
  });
83
108
  }
@@ -3,33 +3,49 @@ import { CloudflareTunnelProvider } from "./cloudflare-provider.js";
3
3
  import { CustomCommandTunnelProvider } from "./custom-command-provider.js";
4
4
  import { PinggyTunnelProvider } from "./pinggy-provider.js";
5
5
  import { waitForPublicReadiness } from "./public-readiness.js";
6
+ import { withAbortSignal } from "../abort.js";
7
+ export const RELAY_START_TIMEOUT_MS = 30_000;
6
8
  export async function selectRelay(options) {
9
+ options.signal.throwIfAborted();
7
10
  if (options.cli.localOnly)
8
11
  return { tunnel: null, publicUrl: options.lanUrl, provider: "lan", failures: [] };
9
- const providers = options.cli.relay === "auto" ? ["cloudflare", "pinggy"] : [options.cli.relay];
12
+ const providers = options.cli.relay === "auto" ? ["cloudflare"] : [options.cli.relay];
10
13
  const failures = [];
11
14
  const readiness = options.readiness ?? waitForPublicReadiness;
12
15
  for (const providerName of providers) {
13
16
  options.onAttempt?.(providerName);
14
- let tunnel = null;
17
+ const attempt = { tunnel: null };
18
+ const deadline = new AbortController();
19
+ const timeoutMs = options.timeoutMs ?? RELAY_START_TIMEOUT_MS;
20
+ const timer = setTimeout(() => deadline.abort(new Error(`relay connection timed out after ${timeoutMs}ms`)), timeoutMs);
21
+ const signal = AbortSignal.any([options.signal, deadline.signal]);
15
22
  try {
16
- const provider = await createProvider(providerName, options);
17
- tunnel = await provider.start({ localUrl: options.localUrl, signal: options.signal, log: options.log });
18
- await readiness(tunnel.publicUrl, { signal: options.signal });
19
- return { tunnel, publicUrl: tunnel.publicUrl, provider: providerName, failures };
23
+ return await withAbortSignal(async () => {
24
+ const provider = await createProvider(providerName, options);
25
+ signal.throwIfAborted();
26
+ const handle = await provider.start({ localUrl: options.localUrl, signal, log: options.log });
27
+ if (signal.aborted) {
28
+ void handle.close().catch(() => { });
29
+ signal.throwIfAborted();
30
+ }
31
+ attempt.tunnel = handle;
32
+ await readiness(handle.publicUrl, { signal });
33
+ signal.throwIfAborted();
34
+ return { tunnel: handle, publicUrl: handle.publicUrl, provider: providerName, failures };
35
+ }, signal);
20
36
  }
21
37
  catch (error) {
22
- await tunnel?.close().catch(() => { });
38
+ deadline.abort(error);
39
+ if (attempt.tunnel)
40
+ await withAbortSignal(() => attempt.tunnel.close(), AbortSignal.timeout(3_000)).catch(() => { });
41
+ options.signal.throwIfAborted();
23
42
  const message = error instanceof Error ? error.message : String(error);
24
43
  failures.push({ provider: providerName, message });
25
44
  options.onFailure?.(providerName, message);
26
- if (options.signal.aborted)
27
- break;
28
45
  }
29
- }
30
- if (options.allowLanFallback === false) {
31
- const detail = failures.map(({ provider, message }) => `${provider}: ${message}`).join("; ");
32
- throw new Error(`public relay unavailable${detail ? ` (${detail})` : ""}`);
46
+ finally {
47
+ clearTimeout(timer);
48
+ }
33
49
  }
34
50
  return { tunnel: null, publicUrl: options.lanUrl, provider: "lan", failures };
35
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentroam",
3
- "version": "0.2.0-preview.48",
3
+ "version": "0.2.0-preview.53",
4
4
  "description": "Zero-install mobile remote terminal and file console",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,12 +27,12 @@
27
27
  "ws": "^8.21.3"
28
28
  },
29
29
  "optionalDependencies": {
30
- "agentroam-runtime-darwin-arm64": "0.2.0-preview.48",
31
- "agentroam-runtime-win32-x64": "0.2.0-preview.48",
32
- "agentroam-cloudflared-darwin-arm64": "0.2.0-preview.39",
33
- "agentroam-cloudflared-win32-x64": "0.2.0-preview.39",
34
- "agentroam-tui-darwin-arm64": "0.2.0-preview.47",
35
- "@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.47"
30
+ "agentroam-runtime-darwin-arm64": "0.2.0-preview.53",
31
+ "agentroam-runtime-win32-x64": "0.2.0-preview.53",
32
+ "agentroam-cloudflared-darwin-arm64": "0.2.0-preview.53",
33
+ "agentroam-cloudflared-win32-x64": "0.2.0-preview.53",
34
+ "agentroam-tui-darwin-arm64": "0.2.0-preview.53",
35
+ "@caoqu/agentroam-tui-win32-x64": "0.2.0-preview.53"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^22.0.0",