@beeuni/agent-tunnel 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,7 +68,10 @@ It starts at login, and the supervisor restarts it after every exit (launchd
68
68
  `KeepAlive` with a 10 s throttle, systemd `Restart=always`, a 5 s Windows loop).
69
69
  On Linux, install enables `loginctl enable-linger` so the user service keeps
70
70
  running after logout and starts at boot; when that is not allowed, install
71
- prints a warning. The Windows task runs hidden, on battery, with no time limit.
71
+ prints a warning. The Windows task runs PowerShell under `conhost --headless`
72
+ (no console window, also where Windows Terminal is the default terminal), on
73
+ battery, with no time limit. Every process the agent starts on Windows sets
74
+ `windowsHide`, so no console window opens.
72
75
  The agent itself never stops on its own:
73
76
 
74
77
  - It reconnects with exponential backoff from 1 s to 30 s, ±20 % jitter, forever.
@@ -181,9 +184,10 @@ change to the agent as `tunnel.access.set`:
181
184
  A pairing made from the Beeuni desktop app starts in `ask`: the app shows the
182
185
  prompt. A pairing made with this CLI alone starts in `always`, because nothing
183
186
  on the machine could answer a prompt. This is a deliberate deviation from
184
- "new pairings default to ask". Every new pairing resets `access.json`, so a
185
- grant or `always` from an earlier pairing never carries over, and `logout`
186
- deletes it. The agent home (`config.json`, `access.json`, `desktop-app.json`)
187
+ "new pairings default to ask". The mode and keep awake are the owner's
188
+ machine settings: pairing again (`--reauth`, a rejected or stale credential)
189
+ keeps them, and only a grant or a denial from the earlier pairing is dropped.
190
+ `logout` deletes `access.json`, so the next pairing starts from the default. The agent home (`config.json`, `access.json`, `desktop-app.json`)
187
191
  is always a blocked path: no file or shell call can change these files.
188
192
 
189
193
  The agent reports its state to Beeuni as the signed notification
@@ -233,9 +237,16 @@ you trust with this computer.
233
237
 
234
238
  ## Computer Use driver
235
239
 
236
- Agent Tunnel never downloads or executes a desktop driver. Install `cua-driver`
237
- locally before enabling Computer Use. The tunnel uses an existing binary from
238
- `CUA_DRIVER_BIN`, `~/.local/bin`, `/usr/local/bin`, or `/opt/homebrew/bin`.
240
+ Agent Tunnel never downloads or executes a desktop driver. The Beeuni desktop
241
+ app bundles `cua-driver` on macOS and Windows (x64), and the agent it runs uses
242
+ that copy. A CLI install uses an existing binary from `CUA_DRIVER_BIN`,
243
+ `~/.local/bin`, `/usr/local/bin`, or `/opt/homebrew/bin`.
244
+
245
+ An approved `desktop` registers whenever the driver is present. Missing macOS
246
+ grants do not unregister it: the agent reports `desktopPermissions` and the
247
+ setup asks for them. When the driver is missing or rejected (wrong owner,
248
+ group-writable), `desktop` does not register and the reason is reported as
249
+ `unavailableCapabilities.desktop` in `status --json` and in `machineInfo`.
239
250
  Treat that binary as trusted local code. Agent Tunnel does not verify or update
240
251
  it.
241
252
 
package/dist/agent-cli.js CHANGED
@@ -3516,6 +3516,15 @@ function writePrivateJson(file, home, value) {
3516
3516
  function writeAccess(state, home = agentTunnelHome()) {
3517
3517
  writePrivateJson(accessFilePath(home), home, state);
3518
3518
  }
3519
+ function accessAfterPairing(home, desktopApp) {
3520
+ const kept = existsSync2(accessFilePath(home)) ? readAccess(home) : null;
3521
+ return {
3522
+ mode: kept?.mode ?? (desktopApp ? "ask" : "always"),
3523
+ grantedUntil: null,
3524
+ deniedUntil: null,
3525
+ keepAwake: kept?.keepAwake ?? false
3526
+ };
3527
+ }
3519
3528
  function decideAccess(state, now = Date.now()) {
3520
3529
  if (state.mode === "always")
3521
3530
  return "run";
@@ -3803,7 +3812,7 @@ function machineDisplayName({
3803
3812
  os = platform2(),
3804
3813
  host = hostname(),
3805
3814
  env = process.env,
3806
- run = (command, args) => execFileSync(command, args, { encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"] })
3815
+ run = (command, args) => execFileSync(command, args, { encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"], windowsHide: true })
3807
3816
  } = {}) {
3808
3817
  const fallback = host.replace(/\.local$/i, "") || host;
3809
3818
  try {
@@ -3815,7 +3824,7 @@ function machineDisplayName({
3815
3824
  }
3816
3825
  function machineId({
3817
3826
  os = platform2(),
3818
- run = (command, args) => execFileSync(command, args, { encoding: "utf8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"] }),
3827
+ run = (command, args) => execFileSync(command, args, { encoding: "utf8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"], windowsHide: true }),
3819
3828
  read = (path) => readFileSync4(path, "utf8")
3820
3829
  } = {}) {
3821
3830
  const attempt = (source) => {
@@ -4036,6 +4045,10 @@ function verifyMessageSignature(signingKey, payload, nonce, signature) {
4036
4045
 
4037
4046
  // src/agent/agent.ts
4038
4047
  var AGENT_VERSION = agentTunnelVersion();
4048
+ function openRelaySocket(url) {
4049
+ const RelaySocket = WebSocket;
4050
+ return new RelaySocket(url, { headers: { "User-Agent": `beeuni-agent-tunnel/${AGENT_VERSION}` } });
4051
+ }
4039
4052
  var AUTH_REJECTED_CLOSE_CODES = [4001, 4003];
4040
4053
  var RELAY_FAULT_4001_REASONS = new Set(["auth timeout", "authentication error", "authentication response failed"]);
4041
4054
  function isCredentialRejection(code, reason = "") {
@@ -4137,7 +4150,7 @@ class TunnelAgent {
4137
4150
  this.connectStartedAt = this.now();
4138
4151
  this.setStatus("connecting");
4139
4152
  try {
4140
- this.ws = new WebSocket(new URL(wsUrl));
4153
+ this.ws = openRelaySocket(new URL(wsUrl).href);
4141
4154
  this.setupWsHandlers();
4142
4155
  } catch (err) {
4143
4156
  log(`${c.red}✗${c.reset}`, `Connection failed`);
@@ -4456,7 +4469,8 @@ class TunnelAgent {
4456
4469
  osVersion: release(),
4457
4470
  agentVersion: AGENT_VERSION,
4458
4471
  homeDir: this.config.workingDir,
4459
- allowedPaths: this.config.allowedPaths
4472
+ allowedPaths: this.config.allowedPaths,
4473
+ unavailableCapabilities: Object.fromEntries(this.registry.unavailable)
4460
4474
  }
4461
4475
  }
4462
4476
  });
@@ -4581,7 +4595,7 @@ class TunnelAgent {
4581
4595
  const blocker = keepAwakeCommand(platform3(), process.pid);
4582
4596
  if (!blocker)
4583
4597
  return;
4584
- const child = spawn3(blocker.command, blocker.args, { stdio: "ignore" });
4598
+ const child = spawn3(blocker.command, blocker.args, { stdio: "ignore", windowsHide: true });
4585
4599
  child.on("error", () => {
4586
4600
  this.keepAwakeFailed = true;
4587
4601
  log(`${c.yellow}!${c.reset}`, `Keep awake is unavailable: ${blocker.command} could not start`);
@@ -4749,6 +4763,7 @@ async function printStartupBanner({
4749
4763
  // src/agent/capabilities/index.ts
4750
4764
  class CapabilityRegistry {
4751
4765
  capabilities = new Map;
4766
+ unavailable = new Map;
4752
4767
  register(capability) {
4753
4768
  this.capabilities.set(capability.name, capability);
4754
4769
  }
@@ -4774,7 +4789,7 @@ class CapabilityRegistry {
4774
4789
  // src/agent/capabilities/desktop/cua-driver.ts
4775
4790
  import { spawn as spawn4 } from "child_process";
4776
4791
  import { existsSync as existsSync3, readFileSync as readFileSync6, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
4777
- import { homedir as homedir3, platform as platform5, tmpdir } from "os";
4792
+ import { homedir as homedir3, platform as platform5, tmpdir, userInfo } from "os";
4778
4793
  import { basename as basename2, dirname as dirname3, join as join5 } from "path";
4779
4794
  var MAX_DRIVER_OUTPUT_BYTES = 5 * 1024 * 1024;
4780
4795
  var DRIVER_ENV_KEYS = [
@@ -4789,7 +4804,19 @@ var DRIVER_ENV_KEYS = [
4789
4804
  "WAYLAND_DISPLAY",
4790
4805
  "XDG_RUNTIME_DIR",
4791
4806
  "XAUTHORITY",
4792
- "DBUS_SESSION_BUS_ADDRESS"
4807
+ "DBUS_SESSION_BUS_ADDRESS",
4808
+ "SystemRoot",
4809
+ "windir",
4810
+ "USERPROFILE",
4811
+ "USERNAME",
4812
+ "USERDOMAIN",
4813
+ "APPDATA",
4814
+ "LOCALAPPDATA",
4815
+ "ProgramData",
4816
+ "TEMP",
4817
+ "TMP",
4818
+ "ComSpec",
4819
+ "PATHEXT"
4793
4820
  ];
4794
4821
  function driverEnvironment(extra = {}) {
4795
4822
  const env = { CUA_DRIVER_RS_TELEMETRY_ENABLED: "0" };
@@ -4852,7 +4879,8 @@ function execFile(cmd, args, timeoutMs = 30000, env = driverEnvironment()) {
4852
4879
  return new Promise((resolve2, reject) => {
4853
4880
  const proc = spawn4(cmd, args, {
4854
4881
  stdio: ["ignore", "pipe", "pipe"],
4855
- env
4882
+ env,
4883
+ windowsHide: true
4856
4884
  });
4857
4885
  let stdout = "";
4858
4886
  let stderr = "";
@@ -4939,9 +4967,8 @@ class CuaDriver {
4939
4967
  constructor(options = {}) {
4940
4968
  this.binary = options.binary ?? null;
4941
4969
  this.embedded = options.embedded;
4942
- const uid = typeof process.getuid === "function" ? process.getuid() : "user";
4943
4970
  const home = serviceLabelFor(agentTunnelHome()).slice(SERVICE_LABEL.length);
4944
- this.socketPath = options.socketPath ?? join5(tmpdir(), `beeuni-cua-${uid}${home}.sock`);
4971
+ this.socketPath = options.socketPath ?? (process.platform === "win32" ? `\\\\.\\pipe\\beeuni-cua-${userInfo().username}${home}` : join5(tmpdir(), `beeuni-cua-${typeof process.getuid === "function" ? process.getuid() : "user"}${home}.sock`));
4945
4972
  this.hostBundleId = options.hostBundleId;
4946
4973
  }
4947
4974
  isEmbedded(binary) {
@@ -4950,10 +4977,11 @@ class CuaDriver {
4950
4977
  mode(binary) {
4951
4978
  if (!this.isEmbedded(binary))
4952
4979
  return { env: driverEnvironment(), socket: [] };
4980
+ const bundleId = this.hostBundleId ?? hostBundleId();
4953
4981
  return {
4954
4982
  env: driverEnvironment({
4955
4983
  CUA_DRIVER_EMBEDDED: "1",
4956
- CUA_DRIVER_HOST_BUNDLE_ID: this.hostBundleId ?? hostBundleId()
4984
+ ...bundleId ? { CUA_DRIVER_HOST_BUNDLE_ID: bundleId } : {}
4957
4985
  }),
4958
4986
  socket: ["--socket", this.socketPath]
4959
4987
  };
@@ -5027,7 +5055,8 @@ class CuaDriver {
5027
5055
  const child = spawn4("open", ["-n", "-g", "-a", "CuaDriver", "--args", "serve"], {
5028
5056
  detached: true,
5029
5057
  stdio: "ignore",
5030
- env: driverEnvironment()
5058
+ env: driverEnvironment(),
5059
+ windowsHide: true
5031
5060
  });
5032
5061
  child.unref();
5033
5062
  await sleep2(750);
@@ -5035,7 +5064,8 @@ class CuaDriver {
5035
5064
  const child = spawn4(bin, ["serve"], {
5036
5065
  detached: true,
5037
5066
  stdio: "ignore",
5038
- env: driverEnvironment()
5067
+ env: driverEnvironment(),
5068
+ windowsHide: true
5039
5069
  });
5040
5070
  child.unref();
5041
5071
  await sleep2(750);
@@ -5052,7 +5082,11 @@ class CuaDriver {
5052
5082
  if (this.daemon && this.daemon.exitCode === null)
5053
5083
  return;
5054
5084
  const { env, socket } = this.mode(bin);
5055
- if (existsSync3(this.socketPath)) {
5085
+ if (process.platform === "win32") {
5086
+ await execFile(bin, ["stop", ...socket], 1e4, env).catch(() => {
5087
+ return;
5088
+ });
5089
+ } else if (existsSync3(this.socketPath)) {
5056
5090
  await execFile(bin, ["stop", ...socket], 1e4, env).catch(() => {
5057
5091
  return;
5058
5092
  });
@@ -5060,7 +5094,8 @@ class CuaDriver {
5060
5094
  }
5061
5095
  const child = spawn4(bin, ["serve", "--embedded", "--no-permissions-gate", ...socket], {
5062
5096
  stdio: "ignore",
5063
- env
5097
+ env,
5098
+ windowsHide: true
5064
5099
  });
5065
5100
  child.unref();
5066
5101
  child.on("exit", () => {
@@ -5071,16 +5106,23 @@ class CuaDriver {
5071
5106
  });
5072
5107
  this.daemon = child;
5073
5108
  process.once("exit", () => child.kill());
5074
- for (let waited = 0;waited < 1e4 && !existsSync3(this.socketPath); waited += 50) {
5109
+ const started = Date.now();
5110
+ while (Date.now() - started < 1e4 && !await this.endpointUp(bin)) {
5075
5111
  if (child.exitCode !== null)
5076
5112
  break;
5077
5113
  await sleep2(50);
5078
5114
  }
5079
- if (!existsSync3(this.socketPath)) {
5115
+ if (!await this.endpointUp(bin)) {
5080
5116
  child.kill();
5081
5117
  throw new Error("cua-driver did not start (no socket after 10 s)");
5082
5118
  }
5083
5119
  }
5120
+ async endpointUp(bin) {
5121
+ if (process.platform !== "win32")
5122
+ return existsSync3(this.socketPath);
5123
+ const { env, socket } = this.mode(bin);
5124
+ return execFile(bin, ["status", ...socket], 5000, env).then(({ stdout }) => !/not\s+running|unhealthy/i.test(stdout), () => false);
5125
+ }
5084
5126
  async permissionOwner() {
5085
5127
  return this.isEmbedded(await this.ensureInstalled()) ? "Beeuni" : "CuaDriver";
5086
5128
  }
@@ -5101,7 +5143,7 @@ class CuaDriver {
5101
5143
  async ensureDaemonReady() {
5102
5144
  const bin = await this.ensureInstalled();
5103
5145
  if (this.isEmbedded(bin)) {
5104
- if (this.daemon?.exitCode === null && existsSync3(this.socketPath))
5146
+ if (this.daemon?.exitCode === null && (process.platform === "win32" ? this.daemonReady : existsSync3(this.socketPath)))
5105
5147
  return;
5106
5148
  this.stop();
5107
5149
  await this.startDaemon();
@@ -5547,7 +5589,8 @@ function createShellCapability(config) {
5547
5589
  cwd,
5548
5590
  shell: false,
5549
5591
  timeout,
5550
- env: safeEnv
5592
+ env: safeEnv,
5593
+ windowsHide: true
5551
5594
  });
5552
5595
  let stdout = "";
5553
5596
  let stderr = "";
@@ -5603,6 +5646,14 @@ function createShellCapability(config) {
5603
5646
  }
5604
5647
 
5605
5648
  // src/agent/capabilities/enabled-registry.ts
5649
+ var DRIVER_NOT_FOUND = "cua-driver was not found. The Beeuni desktop app bundles it on macOS and Windows; a CLI install needs it in CUA_DRIVER_BIN, ~/.local/bin, /usr/local/bin or /opt/homebrew/bin.";
5650
+ function desktopUnavailableReason(findDesktopDriver = findCuaDriverBinary) {
5651
+ try {
5652
+ return findDesktopDriver() ? null : DRIVER_NOT_FOUND;
5653
+ } catch (error) {
5654
+ return error instanceof Error ? error.message : String(error);
5655
+ }
5656
+ }
5606
5657
  function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriverBinary, desktop = {}) {
5607
5658
  const registry = new CapabilityRegistry;
5608
5659
  const enabled = new Set(config.enabledCapabilities ?? ["filesystem", "shell", "desktop"]);
@@ -5610,8 +5661,12 @@ function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriv
5610
5661
  registry.register(createFilesystemCapability(config));
5611
5662
  if (enabled.has("shell"))
5612
5663
  registry.register(createShellCapability(config));
5613
- if (enabled.has("desktop") && findDesktopDriver()) {
5614
- registry.register(createDesktopCapability(desktop));
5664
+ if (enabled.has("desktop")) {
5665
+ const reason = desktopUnavailableReason(findDesktopDriver);
5666
+ if (reason)
5667
+ registry.unavailable.set("desktop", reason);
5668
+ else
5669
+ registry.register(createDesktopCapability(desktop));
5615
5670
  }
5616
5671
  return registry;
5617
5672
  }
@@ -5677,17 +5732,17 @@ function probeCredentials(config, options = {}) {
5677
5732
  return new Promise((resolve3) => {
5678
5733
  let socket;
5679
5734
  try {
5680
- socket = new WebSocket(new URL(buildTunnelWsUrl(config)));
5735
+ socket = openRelaySocket(new URL(buildTunnelWsUrl(config)).href);
5681
5736
  } catch {
5682
5737
  resolve3("unreachable");
5683
5738
  return;
5684
5739
  }
5685
5740
  const settle = (result) => {
5686
5741
  clearTimeout(timer);
5742
+ resolve3(result);
5687
5743
  try {
5688
5744
  socket.close(1000, "probe complete");
5689
5745
  } catch {}
5690
- resolve3(result);
5691
5746
  };
5692
5747
  const timer = setTimeout(() => settle("unreachable"), options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS);
5693
5748
  socket.addEventListener("open", () => {
@@ -5760,7 +5815,7 @@ import { join as join10 } from "path";
5760
5815
  // src/agent/service-drivers.ts
5761
5816
  import { spawnSync } from "child_process";
5762
5817
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
5763
- import { homedir as homedir4, platform as platform6, userInfo } from "os";
5818
+ import { homedir as homedir4, platform as platform6, userInfo as userInfo2 } from "os";
5764
5819
  import { dirname as dirname6, join as join9 } from "path";
5765
5820
 
5766
5821
  // src/agent/service-quoting.ts
@@ -5783,7 +5838,7 @@ function posixShellCommand(runner) {
5783
5838
  }
5784
5839
  var supervisor = {
5785
5840
  run(command, args) {
5786
- const result = spawnSync(command, args, { encoding: "utf8" });
5841
+ const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
5787
5842
  return {
5788
5843
  ok: result.status === 0,
5789
5844
  detail: [result.stdout, result.stderr].filter(Boolean).join(`
@@ -5801,7 +5856,7 @@ function joinDetails(...results) {
5801
5856
  `);
5802
5857
  }
5803
5858
  function launchdTarget() {
5804
- const uid = typeof process.getuid === "function" ? process.getuid() : userInfo().uid;
5859
+ const uid = typeof process.getuid === "function" ? process.getuid() : userInfo2().uid;
5805
5860
  return `gui/${uid}`;
5806
5861
  }
5807
5862
  function logPaths(paths) {
@@ -5874,7 +5929,8 @@ function renderWindowsPowerShellScript(runner) {
5874
5929
  const args = runner.args.map(powershellQuote).join(" ");
5875
5930
  const env = Object.entries(runner.env ?? {}).map(([key, value]) => `$env:${key} = ${powershellQuote(value)}
5876
5931
  `).join("");
5877
- return `$ErrorActionPreference = 'Continue'
5932
+ return `# Run by the Beeuni task under conhost --headless: no console window.
5933
+ $ErrorActionPreference = 'Continue'
5878
5934
  ${env}while ($true) {
5879
5935
  # The pipe makes PowerShell wait for a GUI-subsystem binary (the desktop
5880
5936
  # app's Beeuni.exe) and set $LASTEXITCODE; a bare call returns at once.
@@ -5934,7 +5990,7 @@ var launchd = {
5934
5990
  },
5935
5991
  status(paths, installed) {
5936
5992
  const disabled = run("launchctl", ["print-disabled", launchdTarget()]);
5937
- const enabled = !new RegExp(`"${paths.label.replace(/\./g, "\\.")}" => (disabled|true)`).test(disabled.detail);
5993
+ const enabled = ![`"${paths.label}" => disabled`, `"${paths.label}" => true`].some((line) => disabled.detail.includes(line));
5938
5994
  const status = run("launchctl", ["print", `${launchdTarget()}/${paths.label}`]);
5939
5995
  return {
5940
5996
  active: status.ok,
@@ -5989,7 +6045,7 @@ var systemd = {
5989
6045
  }
5990
6046
  };
5991
6047
  function ensureLinger() {
5992
- const user = userInfo().username;
6048
+ const user = userInfo2().username;
5993
6049
  const linger = run("loginctl", ["show-user", user, "-p", "Linger"]);
5994
6050
  if (/Linger=yes/.test(linger.detail))
5995
6051
  return { ok: true, detail: "" };
@@ -6014,11 +6070,11 @@ function renderWindowsTaskXml(paths, user) {
6014
6070
  <StartWhenAvailable>true</StartWhenAvailable>
6015
6071
  <Enabled>true</Enabled>
6016
6072
  </Settings>
6017
- <Actions Context="Author"><Exec><Command>powershell.exe</Command><Arguments>${xmlEscape(`-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "${paths.windowsScript}"`)}</Arguments></Exec></Actions>
6073
+ <Actions Context="Author"><Exec><Command>conhost.exe</Command><Arguments>${xmlEscape(`--headless powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "${paths.windowsScript}"`)}</Arguments></Exec></Actions>
6018
6074
  </Task>
6019
6075
  `;
6020
6076
  }
6021
- var windowsUser = () => process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${userInfo().username}` : userInfo().username;
6077
+ var windowsUser = () => process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${userInfo2().username}` : userInfo2().username;
6022
6078
  var windowsTaskXmlPath = (paths) => paths.windowsScript.replace(/\.ps1$/, ".xml");
6023
6079
  var scheduledTask = {
6024
6080
  unitPath: (paths) => paths.windowsScript,
@@ -6382,9 +6438,9 @@ function startAgent(config, options = {}) {
6382
6438
  const registry = createEnabledCapabilityRegistry(config, undefined, {
6383
6439
  onPermissionMissing: restart?.permissionMissing
6384
6440
  });
6385
- if (config.enabledCapabilities?.includes("desktop") && !registry.has("desktop")) {
6386
- console.error("[agent-tunnel] Computer Use is approved but unavailable: install the trusted cua-driver locally, then restart Agent Tunnel.");
6387
- }
6441
+ const desktopReason = registry.unavailable.get("desktop");
6442
+ if (desktopReason)
6443
+ console.error(`[agent-tunnel] Computer Use is approved but unavailable: ${desktopReason}`);
6388
6444
  if (options.service) {
6389
6445
  process.stdout.write(`[agent-tunnel] service starting: ${config.tunnelId} -> ${config.apiUrl}
6390
6446
  `);
@@ -6401,7 +6457,6 @@ function startAgent(config, options = {}) {
6401
6457
  persistent: options.service === true,
6402
6458
  reloadConfig: () => loadConfig({ apiUrl: config.apiUrl })
6403
6459
  });
6404
- agent.connect();
6405
6460
  const shutdown = () => {
6406
6461
  if (!options.service)
6407
6462
  console.log(`
@@ -6411,6 +6466,7 @@ ${c.dim} Shutting down…${c.reset}`);
6411
6466
  };
6412
6467
  process.on("SIGTERM", shutdown);
6413
6468
  process.on("SIGINT", shutdown);
6469
+ agent.connect();
6414
6470
  }
6415
6471
  function chooseBackgroundMode(flags) {
6416
6472
  if (anyFlag(flags, BACKGROUND_FLAGS))
@@ -6521,12 +6577,7 @@ async function pairThisMachine(apiUrl, flags, lease) {
6521
6577
  process.exit(1);
6522
6578
  }
6523
6579
  saveCredentials(outcome.tunnelId, outcome.token, apiUrl, outcome.capabilities);
6524
- writeAccess({
6525
- mode: existsSync7(desktopAppPath()) ? "ask" : "always",
6526
- grantedUntil: null,
6527
- deniedUntil: null,
6528
- keepAwake: readAccess().keepAwake
6529
- });
6580
+ writeAccess(accessAfterPairing(agentTunnelHome(), existsSync7(desktopAppPath())));
6530
6581
  if (jsonMode) {
6531
6582
  emit("approved", { tunnelId: outcome.tunnelId, capabilities: outcome.capabilities });
6532
6583
  } else {
@@ -6633,12 +6684,14 @@ function commandStatus(flags) {
6633
6684
  const service = getServiceStatus();
6634
6685
  const paired = Boolean(config.token && config.tunnelId);
6635
6686
  const approved = new Set(config.enabledCapabilities ?? []);
6687
+ const desktopReason = approved.has("desktop") ? desktopUnavailableReason() : null;
6636
6688
  if (isTruthyFlag(flags.json)) {
6637
6689
  console.log(JSON.stringify({
6638
6690
  paired,
6639
6691
  tunnelId: paired ? config.tunnelId : null,
6640
6692
  apiUrl: config.apiUrl,
6641
6693
  capabilities: [...approved],
6694
+ unavailableCapabilities: desktopReason ? { desktop: desktopReason } : {},
6642
6695
  version: agentTunnelVersion(),
6643
6696
  home: agentTunnelHome(),
6644
6697
  serviceLabel: getServicePaths().label,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beeuni/agent-tunnel",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Tunnel relay between cloud AI agents and local machines — server relay, local agent, client SDK, JSON-RPC, HMAC signing",
@@ -12,6 +12,12 @@
12
12
  "dist/",
13
13
  "README.md"
14
14
  ],
15
+ "scripts": {
16
+ "build": "bun build src/agent/cli.ts --target=node --outfile=dist/agent-cli.js --banner='#!/usr/bin/env node' && bun build src/client/cli.ts --target=node --outfile=dist/client-cli.js --banner='#!/usr/bin/env node' && node -e \"const fs=require('node:fs'); fs.chmodSync('dist/agent-cli.js', 0o755); fs.chmodSync('dist/client-cli.js', 0o755);\"",
17
+ "prepack": "bun run build",
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "bun test --timeout ${BEEUNI_TEST_TIMEOUT_MS:-15000}"
20
+ },
15
21
  "dependencies": {
16
22
  "hono": "^4.13.7",
17
23
  "ws": "^8.18.0"