@agentconnect.md/computer 2.6.0-rc.95 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -138,35 +138,45 @@ enough: the switch must be on. Without it, clicks and keys are silently dropped,
138
138
  moves still work. Cmd shortcuts are allowed on macOS; only lock screen, log out, and force quit are
139
139
  refused. ffmpeg is a pinned static 6.0 build (eugeneware/ffmpeg-static b6.1.1).
140
140
 
141
- ### macOS app mode (prototype): the app holds the permissions, not Terminal
141
+ ### macOS: the app holds the permissions, not Terminal
142
142
 
143
- `pnpm bundle:macos` builds `AgentConnect Computer.app` (`md.agentconnect.computer`): the daemon as a
144
- Node single executable app on the official, pinned Node.js, ad-hoc signed. Install it and run
145
- `setup` from the app's binary. This works from Terminal or over SSH:
143
+ `agentconnect computer install` puts `AgentConnect Computer.app` (`md.agentconnect.computer`) in
144
+ `~/Applications` next to the daemon. The app is a signed copy of the official, pinned Node.js; the
145
+ daemon itself stays in the version store and runs inside it. Run the install from Terminal on the
146
+ Mac, or over SSH while someone is logged in to its desktop:
146
147
 
147
148
  ```sh
148
- ditto -x -k AgentConnect-Computer-macos-arm64.zip ~/Applications/
149
- ~/Applications/"AgentConnect Computer.app"/Contents/MacOS/agentconnect-computer setup
149
+ npx -y @agentconnect.md/cli@latest computer install --register <token> --cp-url <exchange URL>
150
150
  ```
151
151
 
152
152
  - `setup` installs a per-user LaunchAgent (`~/Library/LaunchAgents/md.agentconnect.computer.plist`,
153
- Aqua session, starts at login, restarts on crash) and starts it. launchd starts the app itself,
154
- so macOS attributes Accessibility and Screen Recording to **AgentConnect Computer**. Cua Driver
155
- and ffmpeg run as its children and inherit the grants. The app raises the dialogs itself.
156
- `setup` opens the System Settings panes, waits, and restarts the service until a new Screen
157
- Recording grant takes effect. Then it runs `doctor` inside the service.
153
+ Aqua session, starts at login, restarts on crash) that runs
154
+ `<app>/Contents/MacOS/agentconnect-computer ~/.agentconnect/computer/current/dist/agentconnect-computer.mjs run`.
155
+ launchd starts the app itself, so macOS attributes Accessibility and Screen Recording to
156
+ **AgentConnect Computer**. Cua Driver and ffmpeg run as its children and inherit the grants. The
157
+ app raises the dialogs on the Mac's screen; `setup` opens the System Settings panes, waits, and
158
+ restarts the service until a new Screen Recording grant takes effect. Then it runs `doctor`
159
+ inside the service.
160
+ - `agentconnect computer upgrade` only moves `current`, so the grants stay. The app is replaced
161
+ only when a version pins a different one (a new app build, such as a Node update).
158
162
  - `doctor` asks the running service to run the checks as its own child (`/control/doctor`, loopback
159
- only), so the result reflects the app's grants. `--here` checks the calling process instead.
160
- - `service start | stop | restart | status | uninstall` manage the LaunchAgent. The log is
161
- `~/.agentconnect/computer/daemon.log`.
162
- - Ad-hoc signing ties the grants to this exact build: each rebuild must be granted again.
163
- Distribution needs a Developer ID signature plus notarization (`CODESIGN_IDENTITY=...`; the
164
- hardened runtime then needs the JIT entitlements for Node). Unsigned zips downloaded through a
165
- browser are quarantined and blocked by Gatekeeper.
163
+ only), so the result reflects the app's grants. It tells a grant that is listed but switched off
164
+ from one never asked for, and from one the app lost (fix:
165
+ `tccutil reset Accessibility md.agentconnect.computer`, then `setup`).
166
+ - `agentconnect computer uninstall` removes the LaunchAgent and the app; `--purge` also resets the
167
+ app's grants.
168
+ - Until a release publishes the app packages, `install` says that this version pins no app, and
169
+ the grants go to the terminal that runs the daemon.
170
+ - `pnpm bundle:macos` builds the app package into `bundle/computer-darwin-<arch>/` (on a Mac;
171
+ `TARGET_ARCH=x64` cross-builds). It is ad-hoc signed by default, which ties grants to that build;
172
+ `CODESIGN_IDENTITY` (Developer ID) and `NOTARY_KEYCHAIN_PROFILE` sign and notarize it for release.
173
+ - The release builds the app on a macOS runner once the repository variable
174
+ `PUBLISH_COMPUTER_MACOS_APP` is `true`. It republishes an arch's app only when
175
+ `build-macos-app.mjs` changed, and the computer package pins each app exactly.
166
176
 
167
177
  ## Run on Linux
168
178
 
169
- **Install** (what the console's **Computers → Add Computer** shows for Linux): run it in a terminal
179
+ **Install** (what the console's **Computers → Add Computer** shows for every desktop OS): run it in a terminal
170
180
  inside the desktop session, as the desktop user, not root. It needs Node.js ≥ 24.12 (the official
171
181
  tarball or your distribution's package).
172
182
 
@@ -251,8 +261,8 @@ off; `BROWSER_URL` opens a page). `docker stop` stops the daemon, closes the bro
251
261
  code 0. `/home/computer` is a volume: the Computer key, config, audit log, and the browser profile survive a new
252
262
  container. A `HEALTHCHECK` runs `doctor --json --daemon` inside the running daemon.
253
263
 
254
- Register it as a Computer: create a token in the console under **Computers → Add Computer** (pick **Docker** for
255
- this command), then
264
+ Register it as a Computer: create a token in the console under **Computers → Add Computer** (its **Docker** tab
265
+ shows this command), then
256
266
 
257
267
  ```sh
258
268
  docker run -d --name computer --hostname computer-1 --shm-size=1g --restart unless-stopped \
@@ -31457,9 +31457,9 @@ var require_websocket_server = __commonJS({
31457
31457
  });
31458
31458
 
31459
31459
  // src/cli.ts
31460
- import { existsSync as existsSync6, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
31460
+ import { existsSync as existsSync6, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "node:fs";
31461
31461
  import { hostname as hostname5, networkInterfaces as networkInterfaces2 } from "node:os";
31462
- import { basename as basename2, join as join11 } from "node:path";
31462
+ import { basename as basename2, join as join12 } from "node:path";
31463
31463
  import { createInterface as createInterface2 } from "node:readline";
31464
31464
  import { parseArgs } from "node:util";
31465
31465
 
@@ -84773,9 +84773,8 @@ async function startLocalControls(options) {
84773
84773
  // src/self.ts
84774
84774
  import { execFile } from "node:child_process";
84775
84775
  import { resolve } from "node:path";
84776
- import { isSea } from "node:sea";
84777
84776
  function runSelf(args, timeoutMs) {
84778
- const argv = isSea() ? args : [process.argv[1], ...args];
84777
+ const argv = [process.argv[1], ...args];
84779
84778
  return new Promise((resolve2, reject) => {
84780
84779
  execFile(
84781
84780
  process.execPath,
@@ -84795,7 +84794,7 @@ var managedEntry = () => process.env[SERVICE_ENTRY_ENV]?.trim() || void 0;
84795
84794
  var serviceEntry = () => managedEntry() ?? resolve(process.argv[1]);
84796
84795
 
84797
84796
  // src/version.ts
84798
- var COMPUTER_DAEMON_VERSION = true ? "2.6.0-rc.95" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
84797
+ var COMPUTER_DAEMON_VERSION = true ? "2.6.0" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
84799
84798
 
84800
84799
  // src/http-server.ts
84801
84800
  var CONTROL_HEADER = "x-agentconnect-computer";
@@ -108628,6 +108627,39 @@ function displaySleepMinutes() {
108628
108627
  return match ? Number(match[1]) : void 0;
108629
108628
  }
108630
108629
 
108630
+ // src/macos-grants.ts
108631
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "node:fs";
108632
+ import { join as join7 } from "node:path";
108633
+ var APP_ID = "md.agentconnect.computer";
108634
+ var TCC_SERVICE = { accessibility: "Accessibility", screenRecording: "ScreenCapture" };
108635
+ var grantRecordPath = (dir) => join7(dir, "macos-grants.json");
108636
+ function readGrantRecord(dir) {
108637
+ try {
108638
+ const record2 = JSON.parse(readFileSync3(grantRecordPath(dir), "utf8"));
108639
+ return { asked: record2.asked ?? {}, held: record2.held ?? {} };
108640
+ } catch {
108641
+ return { asked: {}, held: {} };
108642
+ }
108643
+ }
108644
+ function recordGrants(dir, state, asked, now = /* @__PURE__ */ new Date()) {
108645
+ const record2 = readGrantRecord(dir);
108646
+ const at = now.toISOString();
108647
+ for (const grant of ["accessibility", "screenRecording"]) {
108648
+ if (state[grant]) record2.held[grant] ??= at;
108649
+ else if (asked) record2.asked[grant] ??= at;
108650
+ }
108651
+ try {
108652
+ writeFileSync6(grantRecordPath(dir), `${JSON.stringify(record2, null, 2)}
108653
+ `, { mode: 384 });
108654
+ } catch {
108655
+ }
108656
+ return record2;
108657
+ }
108658
+ function grantLoss(record2, grant) {
108659
+ if (record2.held[grant]) return "lost";
108660
+ return record2.asked[grant] ? "listed-off" : "never-asked";
108661
+ }
108662
+
108631
108663
  // src/cp/authority.ts
108632
108664
  var RELAY_TOKEN_TTL_MS = 6e4;
108633
108665
  var ChannelAuthority = class {
@@ -109526,8 +109558,8 @@ function createRelayedMcpHandler(deps) {
109526
109558
  }
109527
109559
 
109528
109560
  // src/cp/registration.ts
109529
- import { chmodSync, closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync3, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
109530
- import { join as join7 } from "node:path";
109561
+ import { chmodSync, closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync4, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "node:fs";
109562
+ import { join as join8 } from "node:path";
109531
109563
  var Registration = external_exports.object({
109532
109564
  cpUrl: external_exports.string().url(),
109533
109565
  channelUrl: external_exports.string().url(),
@@ -109543,11 +109575,11 @@ var Registration = external_exports.object({
109543
109575
  revokedAt: external_exports.string().datetime().optional(),
109544
109576
  revokedReason: external_exports.string().optional()
109545
109577
  });
109546
- var registrationPath = (dir = dataDir()) => join7(dir, "computer-key.json");
109578
+ var registrationPath = (dir = dataDir()) => join8(dir, "computer-key.json");
109547
109579
  function loadRegistration(dir = dataDir()) {
109548
109580
  let raw;
109549
109581
  try {
109550
- raw = readFileSync3(registrationPath(dir), "utf8");
109582
+ raw = readFileSync4(registrationPath(dir), "utf8");
109551
109583
  } catch (error62) {
109552
109584
  if (error62.code === "ENOENT") return void 0;
109553
109585
  throw error62;
@@ -109564,7 +109596,7 @@ function saveRegistration(registration, dir = dataDir()) {
109564
109596
  rmSync4(tmp, { force: true });
109565
109597
  const fd = openSync(tmp, "wx", 384);
109566
109598
  try {
109567
- writeFileSync6(fd, body);
109599
+ writeFileSync7(fd, body);
109568
109600
  } finally {
109569
109601
  closeSync(fd);
109570
109602
  }
@@ -109629,7 +109661,7 @@ async function registerOrKeep(dir, cpUrl, register3) {
109629
109661
 
109630
109662
  // src/cp/report.ts
109631
109663
  import { spawnSync as spawnSync5 } from "node:child_process";
109632
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4 } from "node:fs";
109664
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync5 } from "node:fs";
109633
109665
  import { release as release2 } from "node:os";
109634
109666
  var MAX_CHECKS = 64;
109635
109667
  var MAX_TEXT = 1e3;
@@ -109637,7 +109669,7 @@ var computerOs = (platform = process.platform) => platform === "darwin" ? "macos
109637
109669
  function osVersion(platform = process.platform) {
109638
109670
  if (platform === "linux") {
109639
109671
  try {
109640
- const pretty = /^PRETTY_NAME="?([^"\n]+)"?$/m.exec(readFileSync4("/etc/os-release", "utf8"))?.[1];
109672
+ const pretty = /^PRETTY_NAME="?([^"\n]+)"?$/m.exec(readFileSync5("/etc/os-release", "utf8"))?.[1];
109641
109673
  if (pretty) return pretty.slice(0, 120);
109642
109674
  } catch {
109643
109675
  }
@@ -109654,7 +109686,7 @@ function xvfbServes(display) {
109654
109686
  try {
109655
109687
  for (const pid of readdirSync("/proc")) {
109656
109688
  if (!/^\d+$/.test(pid)) continue;
109657
- const argv = readFileSync4(`/proc/${pid}/cmdline`, "utf8").split("\0");
109689
+ const argv = readFileSync5(`/proc/${pid}/cmdline`, "utf8").split("\0");
109658
109690
  if (/(^|\/)Xvfb$/.test(argv[0] ?? "") && argv.includes(`:${number4}`)) return true;
109659
109691
  }
109660
109692
  } catch {
@@ -109726,12 +109758,12 @@ function capabilitiesFrom(checks, display, options = {}) {
109726
109758
 
109727
109759
  // src/cp/env-registration.ts
109728
109760
  import { createHash as createHash6 } from "node:crypto";
109729
- import { readFileSync as readFileSync5 } from "node:fs";
109761
+ import { readFileSync as readFileSync6 } from "node:fs";
109730
109762
  var REGISTER_TOKEN_ENV = "AGENTCONNECT_REGISTER_TOKEN";
109731
109763
  var REGISTER_TOKEN_FILE_ENV = "AGENTCONNECT_REGISTER_TOKEN_FILE";
109732
109764
  var CP_URL_ENV = "AGENTCONNECT_CP_URL";
109733
109765
  var COMPUTER_NAME_ENV = "AGENTCONNECT_COMPUTER_NAME";
109734
- function registrationFromEnv(env = process.env, read = (path) => readFileSync5(path, "utf8")) {
109766
+ function registrationFromEnv(env = process.env, read = (path) => readFileSync6(path, "utf8")) {
109735
109767
  let token = env[REGISTER_TOKEN_ENV]?.trim() ?? "";
109736
109768
  const file2 = env[REGISTER_TOKEN_FILE_ENV]?.trim();
109737
109769
  if (!token && file2) {
@@ -110104,11 +110136,11 @@ function scheduleDoctor(options) {
110104
110136
 
110105
110137
  // src/service-linux.ts
110106
110138
  import { execFile as execFile2, spawnSync as spawnSync6 } from "node:child_process";
110107
- import { copyFileSync, mkdirSync as mkdirSync7, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
110139
+ import { copyFileSync, mkdirSync as mkdirSync7, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
110108
110140
  import { homedir as homedir3 } from "node:os";
110109
- import { join as join8 } from "node:path";
110141
+ import { join as join9 } from "node:path";
110110
110142
  var UNIT = "agentconnect-computer.service";
110111
- var unitPath = () => join8(process.env.XDG_CONFIG_HOME ?? join8(homedir3(), ".config"), "systemd", "user", UNIT);
110143
+ var unitPath = () => join9(process.env.XDG_CONFIG_HOME ?? join9(homedir3(), ".config"), "systemd", "user", UNIT);
110112
110144
  var SESSION_VARS = [
110113
110145
  "DISPLAY",
110114
110146
  "XAUTHORITY",
@@ -110121,7 +110153,7 @@ var SESSION_VARS = [
110121
110153
  var SESSION_ENV = "AGENTCONNECT_COMPUTER_SESSION";
110122
110154
  var SESSION_DISPLAY_VARS = ["DISPLAY", "XAUTHORITY", "WAYLAND_DISPLAY", SESSION_ENV];
110123
110155
  var EXIT_NEW_SESSION = 75;
110124
- var autostartPath = () => join8(process.env.XDG_CONFIG_HOME ?? join8(homedir3(), ".config"), "autostart", "agentconnect-computer.desktop");
110156
+ var autostartPath = () => join9(process.env.XDG_CONFIG_HOME ?? join9(homedir3(), ".config"), "autostart", "agentconnect-computer.desktop");
110125
110157
  function systemctl(...args) {
110126
110158
  const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
110127
110159
  return { ok: result.status === 0, output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() };
@@ -110164,15 +110196,15 @@ X-GNOME-Autostart-enabled=true
110164
110196
  function installService(dataDir2, script, managed = false) {
110165
110197
  let target = script;
110166
110198
  if (!managed) {
110167
- const app = join8(dataDir2, "app");
110199
+ const app = join9(dataDir2, "app");
110168
110200
  mkdirSync7(app, { recursive: true });
110169
- target = join8(app, "agentconnect-computer.mjs");
110201
+ target = join9(app, "agentconnect-computer.mjs");
110170
110202
  if (script !== target) copyFileSync(script, target);
110171
110203
  }
110172
- mkdirSync7(join8(unitPath(), ".."), { recursive: true });
110173
- writeFileSync7(unitPath(), unitFile(process.execPath, target));
110174
- mkdirSync7(join8(autostartPath(), ".."), { recursive: true });
110175
- writeFileSync7(autostartPath(), autostartEntry());
110204
+ mkdirSync7(join9(unitPath(), ".."), { recursive: true });
110205
+ writeFileSync8(unitPath(), unitFile(process.execPath, target));
110206
+ mkdirSync7(join9(autostartPath(), ".."), { recursive: true });
110207
+ writeFileSync8(autostartPath(), autostartEntry());
110176
110208
  systemctl("daemon-reload");
110177
110209
  const enabled = systemctl("enable", UNIT);
110178
110210
  if (!enabled.ok) throw new Error(`systemctl --user enable failed: ${enabled.output}`);
@@ -110309,25 +110341,25 @@ function watchSession(options) {
110309
110341
 
110310
110342
  // src/service-macos.ts
110311
110343
  import { spawnSync as spawnSync7 } from "node:child_process";
110312
- import { existsSync as existsSync4, mkdirSync as mkdirSync8, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
110344
+ import { existsSync as existsSync4, mkdirSync as mkdirSync8, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
110313
110345
  import { homedir as homedir4 } from "node:os";
110314
- import { join as join9 } from "node:path";
110346
+ import { join as join10 } from "node:path";
110315
110347
  var SERVICE_LABEL = "md.agentconnect.computer";
110316
- var plistPath = () => join9(homedir4(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
110348
+ var plistPath = () => join10(homedir4(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
110317
110349
  var domain2 = () => `gui/${process.getuid()}`;
110318
110350
  var xml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
110319
110351
  function launchctl(...args) {
110320
110352
  const result = spawnSync7("launchctl", args, { encoding: "utf8" });
110321
110353
  return { ok: result.status === 0, output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() };
110322
110354
  }
110323
- function installService2(executable, logPath) {
110324
- mkdirSync8(join9(homedir4(), "Library", "LaunchAgents"), { recursive: true });
110325
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
110355
+ var guiSession = () => launchctl("print", domain2()).ok;
110356
+ function servicePlist(program, logPath) {
110357
+ return `<?xml version="1.0" encoding="UTF-8"?>
110326
110358
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
110327
110359
  <plist version="1.0">
110328
110360
  <dict>
110329
110361
  <key>Label</key><string>${SERVICE_LABEL}</string>
110330
- <key>ProgramArguments</key><array><string>${xml(executable)}</string><string>run</string></array>
110362
+ <key>ProgramArguments</key><array>${[...program, "run"].map((arg) => `<string>${xml(arg)}</string>`).join("")}</array>
110331
110363
  <key>AssociatedBundleIdentifiers</key><array><string>${SERVICE_LABEL}</string></array>
110332
110364
  <key>LimitLoadToSessionType</key><string>Aqua</string>
110333
110365
  <key>ProcessType</key><string>Interactive</string>
@@ -110335,15 +110367,22 @@ function installService2(executable, logPath) {
110335
110367
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
110336
110368
  <key>StandardOutPath</key><string>${xml(logPath)}</string>
110337
110369
  <key>StandardErrorPath</key><string>${xml(logPath)}</string>
110370
+ <key>EnvironmentVariables</key><dict><key>NODE_OPTIONS</key><string></string></dict>
110338
110371
  </dict>
110339
110372
  </plist>
110340
110373
  `;
110374
+ }
110375
+ function installService2(program, logPath) {
110376
+ mkdirSync8(join10(homedir4(), "Library", "LaunchAgents"), { recursive: true });
110341
110377
  stopService2();
110342
- writeFileSync8(plistPath(), plist);
110378
+ writeFileSync9(plistPath(), servicePlist(program, logPath));
110379
+ if (!guiSession()) return false;
110343
110380
  startService2();
110381
+ return true;
110344
110382
  }
110345
110383
  function startService2() {
110346
110384
  if (serviceStatus2().loaded) return;
110385
+ if (!guiSession()) throw new Error("nobody is logged in to this Mac's desktop: the service starts at the next login");
110347
110386
  const result = launchctl("bootstrap", domain2(), plistPath());
110348
110387
  if (!result.ok) throw new Error(`launchctl bootstrap failed: ${result.output}`);
110349
110388
  }
@@ -110373,7 +110412,7 @@ function serviceStatus2() {
110373
110412
  // src/setup.ts
110374
110413
  import { spawn as spawn5, spawnSync as spawnSync8 } from "node:child_process";
110375
110414
  import { existsSync as existsSync5 } from "node:fs";
110376
- import { basename, dirname as dirname2, isAbsolute, join as join10 } from "node:path";
110415
+ import { basename, dirname as dirname2, isAbsolute, join as join11 } from "node:path";
110377
110416
  import { createInterface } from "node:readline";
110378
110417
  var localizeChecks = (checks, command = selfCommand()) => {
110379
110418
  const rename = (text) => text.replace(/(?<![\w/.-])agentconnect-computer(?= |$)/g, command);
@@ -110479,7 +110518,10 @@ async function doctor(ctx, captureTimeoutMs = 15e3) {
110479
110518
  status: "ok",
110480
110519
  detail: `${driver.version ?? CUA_DRIVER_VERSION}: ${size.content.find((block) => block.text)?.text ?? "started"}`
110481
110520
  });
110482
- if (process.platform === "darwin") checks.push(...macPermissionChecks(await macPermissions(driver)));
110521
+ if (process.platform === "darwin")
110522
+ checks.push(
110523
+ ...macPermissionChecks(await macPermissions(driver), appBundlePath() ? readGrantRecord(ctx.dir) : void 0)
110524
+ );
110483
110525
  const shot = await driver.call("get_desktop_state", { max_image_dimension: 320 }).then(() => void 0).catch((error62) => error62.message);
110484
110526
  checks.push(
110485
110527
  shot ? { name: "screenshot", status: "fail", detail: shot.slice(0, 160), fix: permissionFix() } : { name: "screenshot", status: "ok", detail: "desktop capture works" }
@@ -110572,22 +110614,40 @@ function sessionCheck() {
110572
110614
  };
110573
110615
  }
110574
110616
  var permissionFix = () => process.platform === "darwin" ? `grant ${hostApp()} the macOS permissions: ${selfCommand()} setup` : void 0;
110575
- function macPermissionChecks(state) {
110576
- const app = hostApp();
110577
- const reopen = appBundlePath() ? `then: ${selfCommand()} service restart` : "then Cmd+Q and reopen";
110578
- return [
110579
- state.accessibility ? { name: "accessibility", status: "ok", detail: `granted to ${app}` } : {
110580
- name: "accessibility",
110581
- status: "fail",
110582
- detail: `not granted to ${app}: clicks and keys are silently dropped`,
110583
- fix: `Privacy & Security > Accessibility (macOS 27: Device Control and Data Access): switch it on, ${reopen}`
110584
- },
110585
- state.screenRecording ? { name: "screen recording", status: "ok", detail: `granted to ${app}` } : {
110586
- name: "screen recording",
110617
+ var PANE = {
110618
+ accessibility: "Privacy & Security > Accessibility (macOS 27: Device Control and Data Access)",
110619
+ screenRecording: "Privacy & Security > Screen & System Audio Recording"
110620
+ };
110621
+ function macPermissionChecks(state, record2, app = hostApp()) {
110622
+ const inApp = Boolean(record2);
110623
+ const reopen = inApp ? `then: ${selfCommand()} service restart` : "then Cmd+Q and reopen";
110624
+ const check2 = (grant, name, effect) => {
110625
+ if (state[grant]) return { name, status: "ok", detail: `granted to ${app}` };
110626
+ const loss = record2 ? grantLoss(record2, grant) : "never-asked";
110627
+ if (loss === "lost")
110628
+ return {
110629
+ name,
110630
+ status: "fail",
110631
+ detail: `was granted to ${app} but is not in effect: switched off, or held by an earlier build of the app${effect}`,
110632
+ fix: `run: tccutil reset ${TCC_SERVICE[grant]} ${APP_ID}, then: ${selfCommand()} setup`
110633
+ };
110634
+ if (loss === "listed-off")
110635
+ return {
110636
+ name,
110637
+ status: "fail",
110638
+ detail: `${app} is listed in ${PANE[grant]} but switched off${effect}`,
110639
+ fix: `switch ${app} on there (listed is not enough), ${reopen}`
110640
+ };
110641
+ return {
110642
+ name,
110587
110643
  status: "fail",
110588
- detail: `not granted to ${app}`,
110589
- fix: `Privacy & Security > Screen & System Audio Recording: switch it on, ${reopen}`
110590
- }
110644
+ detail: `not granted to ${app}${effect}`,
110645
+ fix: inApp ? `run: ${selfCommand()} setup` : `${PANE[grant]}: switch it on, ${reopen}`
110646
+ };
110647
+ };
110648
+ return [
110649
+ check2("accessibility", "accessibility", ": clicks and keys are silently dropped while pointer moves still work"),
110650
+ check2("screenRecording", "screen recording", "")
110591
110651
  ];
110592
110652
  }
110593
110653
  function displayCheck(config2) {
@@ -110751,7 +110811,7 @@ async function setupLinux(ctx) {
110751
110811
  if (gnomeWayland()) {
110752
110812
  const state = gnomeHelperState();
110753
110813
  if (state === "missing") {
110754
- const installer = join10(dirname2(ctx.driverCommand), "wayland-helper", "install.sh");
110814
+ const installer = join11(dirname2(ctx.driverCommand), "wayland-helper", "install.sh");
110755
110815
  const result = spawnSync8(installer, [], { encoding: "utf8" });
110756
110816
  ctx.log(
110757
110817
  result.status === 0 ? `Installed the ${GNOME_HELPER} GNOME extension.` : `Extension install failed: ${result.stderr}`
@@ -110783,6 +110843,9 @@ async function setupMac(ctx) {
110783
110843
  return;
110784
110844
  }
110785
110845
  const app = hostApp();
110846
+ ctx.log(
110847
+ `macOS grants these permissions to ${app}, which runs this command; \`agentconnect computer install\` installs the AgentConnect Computer app to hold them instead.`
110848
+ );
110786
110849
  const driver = new CuaDriver({ command: ctx.driverCommand, callTimeoutMs: 18e4 });
110787
110850
  try {
110788
110851
  let state = await macPermissions(driver);
@@ -111024,7 +111087,9 @@ async function main() {
111024
111087
  callTimeoutMs: values.prompt ? 18e4 : 2e4
111025
111088
  });
111026
111089
  try {
111027
- console.log(JSON.stringify(await macPermissions(driver, Boolean(values.prompt))));
111090
+ const state = await macPermissions(driver, Boolean(values.prompt));
111091
+ if (appBundlePath()) recordGrants(dir, state, Boolean(values.prompt));
111092
+ console.log(JSON.stringify(state));
111028
111093
  } finally {
111029
111094
  await driver.close();
111030
111095
  }
@@ -111053,7 +111118,7 @@ function printClientSetup(config2, client, token, tokenFile) {
111053
111118
  let shown = token;
111054
111119
  if (tokenFile) {
111055
111120
  rmSync7(tokenFile, { force: true });
111056
- writeFileSync9(tokenFile, `${token}
111121
+ writeFileSync10(tokenFile, `${token}
111057
111122
  `, { mode: 384, flag: "wx" });
111058
111123
  shown = "<token>";
111059
111124
  console.log(`
@@ -111111,7 +111176,7 @@ function buildComputer(dir, config2, onStatus, refusal, report, screenshots, nam
111111
111176
  const computer = new Computer({
111112
111177
  driver,
111113
111178
  config: config2,
111114
- audit: auditSinks(fileAuditSink(join11(dir, "audit.log")), ...report ? [report] : []),
111179
+ audit: auditSinks(fileAuditSink(join12(dir, "audit.log")), ...report ? [report] : []),
111115
111180
  log: log39,
111116
111181
  ...onStatus ? { onStatus } : {},
111117
111182
  ...refusal ? { refusal } : {},
@@ -111335,7 +111400,7 @@ async function run2(dir, initial, clientName) {
111335
111400
  const issued = issueClientToken(config2, clientName);
111336
111401
  config2 = issued.config;
111337
111402
  saveConfig(config2, dir);
111338
- printClientSetup(config2, clientName, issued.token, process.stdout.isTTY ? void 0 : join11(dir, "client-token"));
111403
+ printClientSetup(config2, clientName, issued.token, process.stdout.isTTY ? void 0 : join12(dir, "client-token"));
111339
111404
  }
111340
111405
  if (!config2.driverPath && !existsSync6(installedDriverPath(dir))) await installDriver(dir);
111341
111406
  const setTitle = (status) => {
@@ -111391,6 +111456,7 @@ async function run2(dir, initial, clientName) {
111391
111456
  if (process.platform === "darwin") await warnMissingMacPermissions(driver);
111392
111457
  if (process.platform === "darwin" && appBundlePath() && !process.stdin.isTTY) {
111393
111458
  const state = await macPermissions(driver).catch(() => void 0);
111459
+ if (state) recordGrants(dir, state, false);
111394
111460
  if (state && (!state.accessibility || !state.screenRecording)) {
111395
111461
  void runSelf(["permissions", "--json", "--prompt"], 2e5).catch(() => void 0);
111396
111462
  }
@@ -111528,17 +111594,18 @@ async function waitForDaemon(config2, timeoutMs) {
111528
111594
  }
111529
111595
  return false;
111530
111596
  }
111597
+ var macServiceProgram = () => [process.execPath, serviceEntry()];
111531
111598
  async function setupMacApp(dir, config2) {
111532
111599
  const app = basename2(appBundlePath());
111533
- if (!appBundlePath().startsWith("/Applications/") && !appBundlePath().includes("/Applications/")) {
111534
- console.log(
111535
- `Note: the service runs ${process.execPath}; move ${app} to /Applications first if you plan to keep it.`
111600
+ if (!installService2(macServiceProgram(), join12(dir, "daemon.log"))) {
111601
+ throw new Error(
111602
+ `installed LaunchAgent ${plistPath()}, but nobody is logged in to this Mac's desktop: log in (in person or through Screen Sharing) to answer the permission prompts, then run: ${selfCommand()} setup`
111536
111603
  );
111537
111604
  }
111538
- installService2(process.execPath, join11(dir, "daemon.log"));
111539
111605
  console.log(`Installed LaunchAgent ${plistPath()} and started ${app}.`);
111606
+ if (overSsh()) console.log(`The permission prompts appear on the Mac's own screen; answer them there.`);
111540
111607
  if (!await waitForDaemon(config2, 6e4))
111541
- throw new Error(`the service did not come up; see ${join11(dir, "daemon.log")}`);
111608
+ throw new Error(`the service did not come up; see ${join12(dir, "daemon.log")}`);
111542
111609
  const grants = async (prompt) => await (await controlFetch(config2, `/control/permissions${prompt ? "?prompt=1" : ""}`, 21e4)).json();
111543
111610
  let state = await grants(false);
111544
111611
  const initiallyMissingScreen = !state.screenRecording;
@@ -111594,7 +111661,7 @@ function serviceCommand(dir, config2, action) {
111594
111661
  if (!appBundlePath())
111595
111662
  throw new Error("run this from the AgentConnect Computer.app binary (\u2026/Contents/MacOS/agentconnect-computer)");
111596
111663
  }
111597
- if (action === "install") installService2(process.execPath, join11(dir, "daemon.log"));
111664
+ if (action === "install") installService2(macServiceProgram(), join12(dir, "daemon.log"));
111598
111665
  else if (action === "uninstall") uninstallService2();
111599
111666
  else if (action === "start") startService2();
111600
111667
  else if (action === "stop") stopService2();
@@ -111603,7 +111670,7 @@ function serviceCommand(dir, config2, action) {
111603
111670
  const status = serviceStatus2();
111604
111671
  console.log(
111605
111672
  JSON.stringify(
111606
- { ...status, plist: plistPath(), log: join11(dir, "daemon.log"), port: config2.port, app: hostApp() },
111673
+ { ...status, plist: plistPath(), log: join12(dir, "daemon.log"), port: config2.port, app: hostApp() },
111607
111674
  null,
111608
111675
  2
111609
111676
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentconnect.md/computer",
3
- "version": "2.6.0-rc.95",
3
+ "version": "2.6.0",
4
4
  "description": "AgentConnect computer daemon: drives a desktop through Cua Driver and serves the computer_* tools over MCP; installed and upgraded by agentconnect computer",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -22,7 +22,6 @@
22
22
  "@types/ws": "^8.18.2",
23
23
  "esbuild": "0.28.2",
24
24
  "json": "^11.0.0",
25
- "postject": "1.0.0-alpha.6",
26
25
  "tsx": "^4.23.15",
27
26
  "typescript": "^6.0.3",
28
27
  "vitest": "^5.0.5"
@@ -39,6 +38,7 @@
39
38
  "publishConfig": {
40
39
  "access": "public"
41
40
  },
41
+ "optionalDependencies": {},
42
42
  "scripts": {
43
43
  "build": "pnpm --filter \"{.}...\" build:package",
44
44
  "build:package": "node scripts/bundle.mjs --dist",