@jabezpauls/agentbox 0.1.9 → 0.1.10

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.
Files changed (2) hide show
  1. package/dist/agentbox.mjs +3481 -159
  2. package/package.json +1 -1
package/dist/agentbox.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // agentbox-cli v0.1.9
2
+ // agentbox-cli v0.1.10
3
3
  import { createRequire as __agentboxCreateRequire } from "node:module";
4
4
  const require = __agentboxCreateRequire(import.meta.url);
5
5
  var __create = Object.create;
@@ -1397,7 +1397,7 @@ var require_receiver = __commonJS({
1397
1397
  var require_sender = __commonJS({
1398
1398
  "../node_modules/ws/lib/sender.js"(exports, module) {
1399
1399
  "use strict";
1400
- var { Duplex: Duplex2 } = __require("stream");
1400
+ var { Duplex: Duplex3 } = __require("stream");
1401
1401
  var { randomFillSync } = __require("crypto");
1402
1402
  var {
1403
1403
  types: { isUint8Array }
@@ -2124,7 +2124,7 @@ var require_extension = __commonJS({
2124
2124
  if (dest[name] === void 0) dest[name] = [elem];
2125
2125
  else dest[name].push(elem);
2126
2126
  }
2127
- function parse(header) {
2127
+ function parse2(header) {
2128
2128
  const offers = /* @__PURE__ */ Object.create(null);
2129
2129
  let params = /* @__PURE__ */ Object.create(null);
2130
2130
  let mustUnescape = false;
@@ -2264,7 +2264,7 @@ var require_extension = __commonJS({
2264
2264
  }).join(", ");
2265
2265
  }).join(", ");
2266
2266
  }
2267
- module.exports = { format, parse };
2267
+ module.exports = { format, parse: parse2 };
2268
2268
  }
2269
2269
  });
2270
2270
 
@@ -2272,13 +2272,13 @@ var require_extension = __commonJS({
2272
2272
  var require_websocket = __commonJS({
2273
2273
  "../node_modules/ws/lib/websocket.js"(exports, module) {
2274
2274
  "use strict";
2275
- var EventEmitter2 = __require("events");
2275
+ var EventEmitter4 = __require("events");
2276
2276
  var https2 = __require("https");
2277
- var http3 = __require("http");
2278
- var net3 = __require("net");
2277
+ var http4 = __require("http");
2278
+ var net5 = __require("net");
2279
2279
  var tls = __require("tls");
2280
- var { randomBytes: randomBytes5, createHash: createHash3 } = __require("crypto");
2281
- var { Duplex: Duplex2, Readable: Readable2 } = __require("stream");
2280
+ var { randomBytes: randomBytes10, createHash: createHash5 } = __require("crypto");
2281
+ var { Duplex: Duplex3, Readable: Readable2 } = __require("stream");
2282
2282
  var { URL: URL2 } = __require("url");
2283
2283
  var PerMessageDeflate2 = require_permessage_deflate();
2284
2284
  var Receiver2 = require_receiver();
@@ -2298,13 +2298,13 @@ var require_websocket = __commonJS({
2298
2298
  var {
2299
2299
  EventTarget: { addEventListener, removeEventListener }
2300
2300
  } = require_event_target();
2301
- var { format, parse } = require_extension();
2301
+ var { format, parse: parse2 } = require_extension();
2302
2302
  var { toBuffer } = require_buffer_util();
2303
2303
  var kAborted = /* @__PURE__ */ Symbol("kAborted");
2304
2304
  var protocolVersions = [8, 13];
2305
2305
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
2306
2306
  var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
2307
- var WebSocket2 = class _WebSocket extends EventEmitter2 {
2307
+ var WebSocket2 = class _WebSocket extends EventEmitter4 {
2308
2308
  /**
2309
2309
  * Create a new `WebSocket`.
2310
2310
  *
@@ -2815,8 +2815,8 @@ var require_websocket = __commonJS({
2815
2815
  }
2816
2816
  }
2817
2817
  const defaultPort = isSecure ? 443 : 80;
2818
- const key = randomBytes5(16).toString("base64");
2819
- const request = isSecure ? https2.request : http3.request;
2818
+ const key = randomBytes10(16).toString("base64");
2819
+ const request2 = isSecure ? https2.request : http4.request;
2820
2820
  const protocolSet = /* @__PURE__ */ new Set();
2821
2821
  let perMessageDeflate;
2822
2822
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -2893,12 +2893,12 @@ var require_websocket = __commonJS({
2893
2893
  if (opts.auth && !options.headers.authorization) {
2894
2894
  options.headers.authorization = "Basic " + Buffer.from(opts.auth).toString("base64");
2895
2895
  }
2896
- req = websocket._req = request(opts);
2896
+ req = websocket._req = request2(opts);
2897
2897
  if (websocket._redirects) {
2898
2898
  websocket.emit("redirect", websocket.url, req);
2899
2899
  }
2900
2900
  } else {
2901
- req = websocket._req = request(opts);
2901
+ req = websocket._req = request2(opts);
2902
2902
  }
2903
2903
  if (opts.timeout) {
2904
2904
  req.on("timeout", () => {
@@ -2945,7 +2945,7 @@ var require_websocket = __commonJS({
2945
2945
  abortHandshake(websocket, socket2, "Invalid Upgrade header");
2946
2946
  return;
2947
2947
  }
2948
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
2948
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
2949
2949
  if (res.headers["sec-websocket-accept"] !== digest) {
2950
2950
  abortHandshake(websocket, socket2, "Invalid Sec-WebSocket-Accept header");
2951
2951
  return;
@@ -2975,7 +2975,7 @@ var require_websocket = __commonJS({
2975
2975
  }
2976
2976
  let extensions;
2977
2977
  try {
2978
- extensions = parse(secWebSocketExtensions);
2978
+ extensions = parse2(secWebSocketExtensions);
2979
2979
  } catch (err) {
2980
2980
  const message = "Invalid Sec-WebSocket-Extensions header";
2981
2981
  abortHandshake(websocket, socket2, message);
@@ -3019,12 +3019,12 @@ var require_websocket = __commonJS({
3019
3019
  }
3020
3020
  function netConnect(options) {
3021
3021
  options.path = options.socketPath;
3022
- return net3.connect(options);
3022
+ return net5.connect(options);
3023
3023
  }
3024
3024
  function tlsConnect(options) {
3025
3025
  options.path = void 0;
3026
3026
  if (!options.servername && options.servername !== "") {
3027
- options.servername = net3.isIP(options.host) ? "" : options.host;
3027
+ options.servername = net5.isIP(options.host) ? "" : options.host;
3028
3028
  }
3029
3029
  return tls.connect(options);
3030
3030
  }
@@ -3065,7 +3065,7 @@ var require_websocket = __commonJS({
3065
3065
  websocket._closeCode = code2;
3066
3066
  if (websocket._socket[kWebSocket] === void 0) return;
3067
3067
  websocket._socket.removeListener("data", socketOnData);
3068
- process.nextTick(resume, websocket._socket);
3068
+ process.nextTick(resume2, websocket._socket);
3069
3069
  if (code2 === 1005) websocket.close();
3070
3070
  else websocket.close(code2, reason);
3071
3071
  }
@@ -3077,7 +3077,7 @@ var require_websocket = __commonJS({
3077
3077
  const websocket = this[kWebSocket];
3078
3078
  if (websocket._socket[kWebSocket] !== void 0) {
3079
3079
  websocket._socket.removeListener("data", socketOnData);
3080
- process.nextTick(resume, websocket._socket);
3080
+ process.nextTick(resume2, websocket._socket);
3081
3081
  websocket.close(err[kStatusCode]);
3082
3082
  }
3083
3083
  if (!websocket._errorEmitted) {
@@ -3099,7 +3099,7 @@ var require_websocket = __commonJS({
3099
3099
  function receiverOnPong(data) {
3100
3100
  this[kWebSocket].emit("pong", data);
3101
3101
  }
3102
- function resume(stream) {
3102
+ function resume2(stream) {
3103
3103
  stream.resume();
3104
3104
  }
3105
3105
  function senderOnError(err) {
@@ -3169,7 +3169,7 @@ var require_stream = __commonJS({
3169
3169
  "../node_modules/ws/lib/stream.js"(exports, module) {
3170
3170
  "use strict";
3171
3171
  var WebSocket2 = require_websocket();
3172
- var { Duplex: Duplex2 } = __require("stream");
3172
+ var { Duplex: Duplex3 } = __require("stream");
3173
3173
  function emitClose(stream) {
3174
3174
  stream.emit("close");
3175
3175
  }
@@ -3187,7 +3187,7 @@ var require_stream = __commonJS({
3187
3187
  }
3188
3188
  function createWebSocketStream2(ws, options) {
3189
3189
  let terminateOnDestroy = true;
3190
- const duplex = new Duplex2({
3190
+ const duplex = new Duplex3({
3191
3191
  ...options,
3192
3192
  autoDestroy: false,
3193
3193
  emitClose: false,
@@ -3267,7 +3267,7 @@ var require_subprotocol = __commonJS({
3267
3267
  "../node_modules/ws/lib/subprotocol.js"(exports, module) {
3268
3268
  "use strict";
3269
3269
  var { tokenChars } = require_validation();
3270
- function parse(header) {
3270
+ function parse2(header) {
3271
3271
  const protocols = /* @__PURE__ */ new Set();
3272
3272
  let start = -1;
3273
3273
  let end = -1;
@@ -3303,7 +3303,7 @@ var require_subprotocol = __commonJS({
3303
3303
  protocols.add(protocol);
3304
3304
  return protocols;
3305
3305
  }
3306
- module.exports = { parse };
3306
+ module.exports = { parse: parse2 };
3307
3307
  }
3308
3308
  });
3309
3309
 
@@ -3311,10 +3311,10 @@ var require_subprotocol = __commonJS({
3311
3311
  var require_websocket_server = __commonJS({
3312
3312
  "../node_modules/ws/lib/websocket-server.js"(exports, module) {
3313
3313
  "use strict";
3314
- var EventEmitter2 = __require("events");
3315
- var http3 = __require("http");
3316
- var { Duplex: Duplex2 } = __require("stream");
3317
- var { createHash: createHash3 } = __require("crypto");
3314
+ var EventEmitter4 = __require("events");
3315
+ var http4 = __require("http");
3316
+ var { Duplex: Duplex3 } = __require("stream");
3317
+ var { createHash: createHash5 } = __require("crypto");
3318
3318
  var extension2 = require_extension();
3319
3319
  var PerMessageDeflate2 = require_permessage_deflate();
3320
3320
  var subprotocol2 = require_subprotocol();
@@ -3324,7 +3324,7 @@ var require_websocket_server = __commonJS({
3324
3324
  var RUNNING = 0;
3325
3325
  var CLOSING = 1;
3326
3326
  var CLOSED = 2;
3327
- var WebSocketServer2 = class extends EventEmitter2 {
3327
+ var WebSocketServer2 = class extends EventEmitter4 {
3328
3328
  /**
3329
3329
  * Create a `WebSocketServer` instance.
3330
3330
  *
@@ -3393,8 +3393,8 @@ var require_websocket_server = __commonJS({
3393
3393
  );
3394
3394
  }
3395
3395
  if (options.port != null) {
3396
- this._server = http3.createServer((req, res) => {
3397
- const body = http3.STATUS_CODES[426];
3396
+ this._server = http4.createServer((req, res) => {
3397
+ const body = http4.STATUS_CODES[426];
3398
3398
  res.writeHead(426, {
3399
3399
  "Content-Length": body.length,
3400
3400
  "Content-Type": "text/plain"
@@ -3621,7 +3621,7 @@ var require_websocket_server = __commonJS({
3621
3621
  );
3622
3622
  }
3623
3623
  if (this._state > RUNNING) return abortHandshake(socket2, 503);
3624
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
3624
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
3625
3625
  const headers = [
3626
3626
  "HTTP/1.1 101 Switching Protocols",
3627
3627
  "Upgrade: websocket",
@@ -3683,7 +3683,7 @@ var require_websocket_server = __commonJS({
3683
3683
  this.destroy();
3684
3684
  }
3685
3685
  function abortHandshake(socket2, code2, message, headers) {
3686
- message = message || http3.STATUS_CODES[code2];
3686
+ message = message || http4.STATUS_CODES[code2];
3687
3687
  headers = {
3688
3688
  Connection: "close",
3689
3689
  "Content-Type": "text/html",
@@ -3692,7 +3692,7 @@ var require_websocket_server = __commonJS({
3692
3692
  };
3693
3693
  socket2.once("finish", socket2.destroy);
3694
3694
  socket2.end(
3695
- `HTTP/1.1 ${code2} ${http3.STATUS_CODES[code2]}\r
3695
+ `HTTP/1.1 ${code2} ${http4.STATUS_CODES[code2]}\r
3696
3696
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
3697
3697
  );
3698
3698
  }
@@ -3739,9 +3739,9 @@ var UsageError = class extends CliError {
3739
3739
  }
3740
3740
  };
3741
3741
  var ApiError = class extends CliError {
3742
- constructor(status2, message, code2 = null) {
3743
- super(message, status2 === 401 ? EXIT.AUTH : status2 === 404 ? EXIT.NOT_FOUND : EXIT.FAILURE);
3744
- this.status = status2;
3742
+ constructor(status3, message, code2 = null) {
3743
+ super(message, status3 === 401 ? EXIT.AUTH : status3 === 404 ? EXIT.NOT_FOUND : EXIT.FAILURE);
3744
+ this.status = status3;
3745
3745
  this.code = code2;
3746
3746
  this.name = "ApiError";
3747
3747
  }
@@ -3868,7 +3868,7 @@ function parseSize(raw) {
3868
3868
  }
3869
3869
 
3870
3870
  // src/version.ts
3871
- var VERSION = true ? "v0.1.9" : "0.0.0-dev";
3871
+ var VERSION = true ? "v0.1.10" : "0.0.0-dev";
3872
3872
  var STANDALONE = typeof __AGENTBOX_STANDALONE__ === "boolean" && __AGENTBOX_STANDALONE__;
3873
3873
  var MIN_NODE_MAJOR = 20;
3874
3874
  function nodeMajor(version = process.versions.node) {
@@ -4083,8 +4083,8 @@ var status = command({
4083
4083
  else if (list.length === 0) row("apps", "none");
4084
4084
  else {
4085
4085
  const isShared = (a) => Boolean(a.visibility?.mode && a.visibility.mode !== "private" && (a.visibility.expiresAt == null || a.visibility.expiresAt > ctx.now()));
4086
- const shared = list.filter(isShared).length;
4087
- row("apps", `${list.length}${shared ? ` (${shared} shared)` : ""}`);
4086
+ const shared2 = list.filter(isShared).length;
4087
+ row("apps", `${list.length}${shared2 ? ` (${shared2} shared)` : ""}`);
4088
4088
  const lines = table(
4089
4089
  ["NAME", "PORT", "STATE", "SHARING"],
4090
4090
  list.map((a) => [
@@ -4266,13 +4266,13 @@ var TunnelStream = class extends Duplex {
4266
4266
  this.ws = ws;
4267
4267
  ws.on("message", (data, isBinary) => {
4268
4268
  if (!isBinary) {
4269
- let control = null;
4269
+ let control2 = null;
4270
4270
  try {
4271
- control = JSON.parse(data.toString("utf8"));
4271
+ control2 = JSON.parse(data.toString("utf8"));
4272
4272
  } catch {
4273
4273
  }
4274
- if (control?.type === "error") {
4275
- const err = new TunnelError(control.message ?? "the tunnel failed");
4274
+ if (control2?.type === "error") {
4275
+ const err = new TunnelError(control2.message ?? "the tunnel failed");
4276
4276
  setImmediate(() => this.destroy(err));
4277
4277
  }
4278
4278
  return;
@@ -4317,8 +4317,8 @@ function openTunnel(client, target, opts = {}) {
4317
4317
  const early = (data, isBinary) => {
4318
4318
  if (isBinary) return;
4319
4319
  try {
4320
- const control = JSON.parse(data.toString("utf8"));
4321
- if (control.type === "error") fail(new TunnelError(control.message ?? "the tunnel failed"));
4320
+ const control2 = JSON.parse(data.toString("utf8"));
4321
+ if (control2.type === "error") fail(new TunnelError(control2.message ?? "the tunnel failed"));
4322
4322
  } catch {
4323
4323
  }
4324
4324
  };
@@ -4336,10 +4336,10 @@ function openTunnel(client, target, opts = {}) {
4336
4336
  });
4337
4337
  ws.on("message", early);
4338
4338
  ws.once("unexpected-response", (_req, res) => {
4339
- const status2 = res.statusCode ?? 0;
4339
+ const status3 = res.statusCode ?? 0;
4340
4340
  res.resume();
4341
4341
  fail(
4342
- status2 === 401 ? new CliError("the box did not accept this device's sign-in; run `agentbox login` again", EXIT.AUTH) : status2 === 404 ? new CliError(`${client.origin} has no tunnel endpoint yet; update the box`, EXIT.NOT_FOUND) : new TunnelError(`the box refused the tunnel (HTTP ${status2})`)
4342
+ status3 === 401 ? new CliError("the box did not accept this device's sign-in; run `agentbox login` again", EXIT.AUTH) : status3 === 404 ? new CliError(`${client.origin} has no tunnel endpoint yet; update the box`, EXIT.NOT_FOUND) : new TunnelError(`the box refused the tunnel (HTTP ${status3})`)
4343
4343
  );
4344
4344
  });
4345
4345
  ws.once("error", (err) => fail(new CliError(`could not open a tunnel to ${client.origin}: ${err.message}`, EXIT.UNREACHABLE)));
@@ -4529,20 +4529,20 @@ var share = command({
4529
4529
  const body = { mode: passcode ? "passcode" : "link" };
4530
4530
  if (expires !== void 0) body.expiresIn = parseExpiry(expires);
4531
4531
  if (chosen !== void 0) body.passcode = chosen;
4532
- const shared = await client.json("PUT", `/_gate/apps/${encodeURIComponent(app.id)}/visibility`, {
4532
+ const shared2 = await client.json("PUT", `/_gate/apps/${encodeURIComponent(app.id)}/visibility`, {
4533
4533
  body: { json: body },
4534
4534
  what: `sharing ${app.name}`
4535
4535
  });
4536
4536
  const url = `${box.url}/a/${app.id}/`;
4537
4537
  if (ctx.json) {
4538
- ctx.printJson({ ...shared, url });
4538
+ ctx.printJson({ ...shared2, url });
4539
4539
  return;
4540
4540
  }
4541
4541
  ctx.out(`${url}
4542
4542
  `);
4543
- if (shared.passcode) ctx.out(`passcode ${shared.passcode}
4543
+ if (shared2.passcode) ctx.out(`passcode ${shared2.passcode}
4544
4544
  `);
4545
- ctx.err(`${safeText(app.name)} is ${safeText(sharing(shared, ctx.now()))}${shared.passcode ? "; the passcode is shown only this once" : ""}.
4545
+ ctx.err(`${safeText(app.name)} is ${safeText(sharing(shared2, ctx.now()))}${shared2.passcode ? "; the passcode is shown only this once" : ""}.
4546
4546
  `);
4547
4547
  }
4548
4548
  });
@@ -4677,10 +4677,10 @@ function hasBox(boxes2, name) {
4677
4677
  function emptyConfig() {
4678
4678
  return { version: 1, current: null, boxes: boxTable() };
4679
4679
  }
4680
- function configDir(env = process.env, home = os.homedir(), platform = process.platform) {
4680
+ function configDir(env = process.env, home2 = os.homedir(), platform = process.platform) {
4681
4681
  if (platform === "win32" && env.APPDATA && path3.win32.isAbsolute(env.APPDATA)) return path3.win32.join(env.APPDATA, "agentbox");
4682
4682
  const xdg = env.XDG_CONFIG_HOME;
4683
- const base = xdg && path3.isAbsolute(xdg) ? xdg : path3.join(home, ".config");
4683
+ const base = xdg && path3.isAbsolute(xdg) ? xdg : path3.join(home2, ".config");
4684
4684
  return path3.join(base, "agentbox");
4685
4685
  }
4686
4686
  var BOX_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
@@ -4915,7 +4915,7 @@ async function pollForToken(client, start, deps = {}) {
4915
4915
  await wait(delay, deps.signal);
4916
4916
  delay = interval;
4917
4917
  if (now() > deadline) throw new CliError("the code expired before it was approved; run `agentbox login` again");
4918
- let status2;
4918
+ let status3;
4919
4919
  let body;
4920
4920
  let retryAfter;
4921
4921
  try {
@@ -4925,7 +4925,7 @@ async function pollForToken(client, start, deps = {}) {
4925
4925
  headers: { accept: "application/json" },
4926
4926
  ...deps.signal ? { signal: deps.signal } : {}
4927
4927
  });
4928
- status2 = res.status;
4928
+ status3 = res.status;
4929
4929
  retryAfter = typeof res.headers["retry-after"] === "string" ? res.headers["retry-after"] : void 0;
4930
4930
  const text = await res.text();
4931
4931
  try {
@@ -4940,17 +4940,17 @@ async function pollForToken(client, start, deps = {}) {
4940
4940
  }
4941
4941
  throw err;
4942
4942
  }
4943
- if (status2 >= 500) {
4944
- if (++failures > maxFailures) throw new CliError(`the box kept failing to answer the sign-in (HTTP ${status2}); try again later`);
4943
+ if (status3 >= 500) {
4944
+ if (++failures > maxFailures) throw new CliError(`the box kept failing to answer the sign-in (HTTP ${status3}); try again later`);
4945
4945
  delay = backoff(interval, failures);
4946
4946
  continue;
4947
4947
  }
4948
4948
  failures = 0;
4949
- if (status2 === 200) {
4949
+ if (status3 === 200) {
4950
4950
  if (typeof body.token === "string" && TOKEN_SHAPE.test(body.token)) return body.token;
4951
4951
  throw new CliError("the box answered with something that is not a device token");
4952
4952
  }
4953
- if (status2 === 429) {
4953
+ if (status3 === 429) {
4954
4954
  const s = Number(retryAfter);
4955
4955
  if (Number.isFinite(s) && s > 0) await wait(s * 1e3, deps.signal);
4956
4956
  continue;
@@ -4967,7 +4967,7 @@ async function pollForToken(client, start, deps = {}) {
4967
4967
  case "expired_token":
4968
4968
  throw new CliError("the code expired, or was already used; run `agentbox login` again");
4969
4969
  default:
4970
- throw new CliError(`the box refused the sign-in (HTTP ${status2}${typeof body.error === "string" ? `: ${body.error}` : ""})`);
4970
+ throw new CliError(`the box refused the sign-in (HTTP ${status3}${typeof body.error === "string" ? `: ${body.error}` : ""})`);
4971
4971
  }
4972
4972
  }
4973
4973
  }
@@ -4991,9 +4991,9 @@ import os3 from "node:os";
4991
4991
  import path5 from "node:path";
4992
4992
 
4993
4993
  // ../shared/src/index.ts
4994
- function encodePathParam(path12) {
4994
+ function encodePathParam(path22) {
4995
4995
  let out = "";
4996
- for (const ch of path12) {
4996
+ for (const ch of path22) {
4997
4997
  const cp = ch.codePointAt(0) ?? 0;
4998
4998
  out += cp >= 56448 && cp <= 56575 ? `%${(cp - 56320).toString(16).toUpperCase()}` : encodeURIComponent(ch);
4999
4999
  }
@@ -5065,8 +5065,8 @@ var FilesApi = class {
5065
5065
  copy(from, to, overwrite) {
5066
5066
  return this.client.json("POST", "/api/files/copy", { body: { json: { from, to, overwrite } }, what: `copying ${from}`, idleMs: 6e5 });
5067
5067
  }
5068
- trash(paths) {
5069
- return this.client.json("POST", "/api/files/trash", { body: { json: { paths } }, what: "moving to the trash" });
5068
+ trash(paths2) {
5069
+ return this.client.json("POST", "/api/files/trash", { body: { json: { paths: paths2 } }, what: "moving to the trash" });
5070
5070
  }
5071
5071
  startUpload(p, size, overwrite) {
5072
5072
  return this.client.json("POST", "/api/files/uploads", { body: { json: { path: p, size, overwrite } }, what: `uploading to ${p}` });
@@ -5402,8 +5402,8 @@ function readIfThere(file) {
5402
5402
  }
5403
5403
  }
5404
5404
  function tilde(ctx, p) {
5405
- const home = homeDir(ctx);
5406
- return p.startsWith(home + path5.sep) ? `~${p.slice(home.length)}` : p;
5405
+ const home2 = homeDir(ctx);
5406
+ return p.startsWith(home2 + path5.sep) ? `~${p.slice(home2.length)}` : p;
5407
5407
  }
5408
5408
  function sshConfigArgs(ctx) {
5409
5409
  const mine = path5.join(sshDir(ctx), "config");
@@ -6405,7 +6405,7 @@ var put = command({
6405
6405
  const force = bool(p.options, "force");
6406
6406
  const chunkRaw = typeof p.options["chunk-size"] === "string" ? parseSize(p.options["chunk-size"]) : MAX_CHUNK;
6407
6407
  if (chunkRaw < 1024 || chunkRaw > MAX_CHUNK) throw new UsageError(`--chunk-size must be from 1K to ${MAX_CHUNK / 1024 / 1024}M`);
6408
- const resume = new ResumeStore(ctx.config.dir);
6408
+ const resume2 = new ResumeStore(ctx.config.dir);
6409
6409
  let target = await files.stat(remote);
6410
6410
  if (!target && remote.endsWith("/")) target = await files.mkdir(remote);
6411
6411
  const intoDir = isDir(target);
@@ -6420,7 +6420,7 @@ var put = command({
6420
6420
  remote: dest,
6421
6421
  overwrite: force,
6422
6422
  chunkSize: chunkRaw,
6423
- resume,
6423
+ resume: resume2,
6424
6424
  signal: ctx.abort.signal,
6425
6425
  onProgress: (sent) => bar.update(sent),
6426
6426
  onResume: (received, total) => ctx.err(`${file}: resuming at ${formatBytes(received)} of ${formatBytes(total)}
@@ -6492,7 +6492,7 @@ var rm = command({
6492
6492
  details: "Nothing is deleted outright: restore from Files \u2192 Trash in the app.",
6493
6493
  async run(ctx, p) {
6494
6494
  const files = await filesApi(ctx);
6495
- const paths = [];
6495
+ const paths2 = [];
6496
6496
  for (const target of p.operands) {
6497
6497
  const e = await files.stat(target);
6498
6498
  if (!e) {
@@ -6500,10 +6500,10 @@ var rm = command({
6500
6500
  throw new CliError(`${target}: no such file or folder on the box`, EXIT.NOT_FOUND);
6501
6501
  }
6502
6502
  if (e.type === "dir" && !bool(p.options, "recursive")) throw new CliError(`${e.path} is a folder; add -r to trash it and everything in it`);
6503
- paths.push(e.path);
6503
+ paths2.push(e.path);
6504
6504
  }
6505
- if (paths.length === 0) return;
6506
- const result = await files.trash(paths);
6505
+ if (paths2.length === 0) return;
6506
+ const result = await files.trash(paths2);
6507
6507
  for (const t2 of result.trashed) ctx.err(`trashed ${safeText(t2.originalPath)}
6508
6508
  `);
6509
6509
  }
@@ -6578,8 +6578,8 @@ function editorCommand(editor, file, platform, env = process.env) {
6578
6578
  if (!CMD_SAFE.test(file)) {
6579
6579
  throw new CliError(`the temporary file's path (${file}) has characters cmd.exe would act on; set TEMP to a plain folder, or use an editor that is an .exe`);
6580
6580
  }
6581
- const quote = (w) => `"${w.replace(/"/g, "")}"`;
6582
- return { command: [resolved, ...rest, file].map(quote).join(" "), args: [], shell: true };
6581
+ const quote2 = (w) => `"${w.replace(/"/g, "")}"`;
6582
+ return { command: [resolved, ...rest, file].map(quote2).join(" "), args: [], shell: true };
6583
6583
  }
6584
6584
  function runEditor(editor, file, platform = process.platform, env = process.env) {
6585
6585
  return new Promise((resolve, reject) => {
@@ -6806,14 +6806,3335 @@ var add = command({
6806
6806
  });
6807
6807
  var HERDR_COMMANDS = [add, call, socket];
6808
6808
 
6809
+ // src/commands/node.ts
6810
+ import { spawn as spawn7 } from "node:child_process";
6811
+ import fs20 from "node:fs";
6812
+ import os11 from "node:os";
6813
+ import path18 from "node:path";
6814
+
6815
+ // src/node/control.ts
6816
+ import { randomBytes as randomBytes4, timingSafeEqual } from "node:crypto";
6817
+ import fs9 from "node:fs";
6818
+ import net3 from "node:net";
6819
+
6820
+ // src/node/paths.ts
6821
+ import { randomBytes as randomBytes3 } from "node:crypto";
6822
+ import fs8 from "node:fs";
6823
+ import os7 from "node:os";
6824
+ import path9 from "node:path";
6825
+ function nodeDir(env = process.env, home2 = os7.homedir(), platform = process.platform) {
6826
+ if (platform === "win32") {
6827
+ const local = env.LOCALAPPDATA && path9.win32.isAbsolute(env.LOCALAPPDATA) ? env.LOCALAPPDATA : path9.win32.join(home2, "AppData", "Local");
6828
+ return path9.win32.join(local, "agentbox", "node");
6829
+ }
6830
+ const xdg = env.XDG_DATA_HOME;
6831
+ const base = xdg && path9.isAbsolute(xdg) ? xdg : path9.join(home2, ".local", "share");
6832
+ return path9.join(base, "agentbox", "node");
6833
+ }
6834
+ function nodePaths(dir) {
6835
+ return {
6836
+ dir,
6837
+ policy: path9.join(dir, "policy.json"),
6838
+ credentials: path9.join(dir, "credentials.json"),
6839
+ secrets: path9.join(dir, "secrets"),
6840
+ bin: path9.join(dir, "bin"),
6841
+ state: path9.join(dir, "state"),
6842
+ control: path9.join(dir, "control.sock"),
6843
+ controlSecret: path9.join(dir, "control.secret")
6844
+ };
6845
+ }
6846
+ function ensurePrivateDir(dir) {
6847
+ fs8.mkdirSync(dir, { recursive: true, mode: 448 });
6848
+ if (process.platform !== "win32") fs8.chmodSync(dir, 448);
6849
+ }
6850
+ function writePrivate2(file, contents, mode = 384) {
6851
+ ensurePrivateDir(path9.dirname(file));
6852
+ const tmp = path9.join(path9.dirname(file), `.${path9.basename(file)}.${randomBytes3(6).toString("hex")}.tmp`);
6853
+ const fd = fs8.openSync(tmp, "wx", mode);
6854
+ try {
6855
+ fs8.writeFileSync(fd, contents);
6856
+ if (process.platform !== "win32") fs8.fchmodSync(fd, mode);
6857
+ fs8.fsyncSync(fd);
6858
+ } catch (err) {
6859
+ fs8.closeSync(fd);
6860
+ fs8.rmSync(tmp, { force: true });
6861
+ throw err;
6862
+ }
6863
+ fs8.closeSync(fd);
6864
+ try {
6865
+ fs8.renameSync(tmp, file);
6866
+ } catch (err) {
6867
+ fs8.rmSync(tmp, { force: true });
6868
+ throw err;
6869
+ }
6870
+ }
6871
+
6872
+ // src/node/control.ts
6873
+ var COMMANDS = /* @__PURE__ */ new Set(["status", "pause", "resume", "leave"]);
6874
+ async function startControl(socketPath, secretFile, handle) {
6875
+ const secret = randomBytes4(32).toString("base64url");
6876
+ writePrivate2(secretFile, secret);
6877
+ const want = Buffer.from(secret);
6878
+ fs9.rmSync(socketPath, { force: true });
6879
+ const server = net3.createServer((sock) => {
6880
+ let buf = "";
6881
+ sock.setTimeout(1e4, () => sock.destroy());
6882
+ sock.on("error", () => sock.destroy());
6883
+ sock.on("data", (c) => {
6884
+ buf += c.toString("utf8");
6885
+ if (buf.length > 16 * 1024) return sock.destroy();
6886
+ const nl = buf.indexOf("\n");
6887
+ if (nl < 0) return;
6888
+ const reply = (o) => {
6889
+ sock.end(`${JSON.stringify(o)}
6890
+ `);
6891
+ };
6892
+ let req;
6893
+ try {
6894
+ req = JSON.parse(buf.slice(0, nl));
6895
+ } catch {
6896
+ return reply({ ok: false, error: "bad request" });
6897
+ }
6898
+ const got = Buffer.from(typeof req.secret === "string" ? req.secret : "");
6899
+ if (got.length !== want.length || !timingSafeEqual(got, want)) return reply({ ok: false, error: "refused" });
6900
+ if (typeof req.command !== "string" || !COMMANDS.has(req.command)) return reply({ ok: false, error: "unknown command" });
6901
+ handle(req.command).then(
6902
+ (out) => reply({ ok: true, ...out }),
6903
+ (err) => reply({ ok: false, error: err.message })
6904
+ );
6905
+ });
6906
+ });
6907
+ await new Promise((resolve, reject) => {
6908
+ server.once("error", reject);
6909
+ server.listen(socketPath, () => {
6910
+ server.off("error", reject);
6911
+ resolve();
6912
+ });
6913
+ });
6914
+ if (process.platform !== "win32") fs9.chmodSync(socketPath, 384);
6915
+ return {
6916
+ close: () => {
6917
+ server.close();
6918
+ fs9.rmSync(socketPath, { force: true });
6919
+ }
6920
+ };
6921
+ }
6922
+ function callControl(socketPath, secretFile, command2, timeoutMs = 1e4) {
6923
+ let secret;
6924
+ try {
6925
+ secret = fs9.readFileSync(secretFile, "utf8").trim();
6926
+ } catch {
6927
+ return Promise.resolve(null);
6928
+ }
6929
+ return new Promise((resolve) => {
6930
+ const sock = net3.connect(socketPath);
6931
+ let buf = "";
6932
+ const timer = setTimeout(() => {
6933
+ sock.destroy();
6934
+ resolve(null);
6935
+ }, timeoutMs);
6936
+ sock.on("connect", () => sock.write(`${JSON.stringify({ secret, command: command2 })}
6937
+ `));
6938
+ sock.on("data", (c) => buf += c.toString("utf8"));
6939
+ sock.on("end", () => {
6940
+ clearTimeout(timer);
6941
+ try {
6942
+ resolve(JSON.parse(buf));
6943
+ } catch {
6944
+ resolve(null);
6945
+ }
6946
+ });
6947
+ sock.on("error", () => {
6948
+ clearTimeout(timer);
6949
+ resolve(null);
6950
+ });
6951
+ });
6952
+ }
6953
+
6954
+ // src/node/credentials.ts
6955
+ import fs10 from "node:fs";
6956
+
6957
+ // ../hostcore/src/link/frame.ts
6958
+ var FrameType = {
6959
+ open: 1,
6960
+ data: 2,
6961
+ close: 3,
6962
+ window: 4,
6963
+ control: 5
6964
+ };
6965
+ var TYPES = new Set(Object.values(FrameType));
6966
+ var HEADER_BYTES = 5;
6967
+ var CONTROL_MAX_BYTES = 64 * 1024;
6968
+ var HELLO_MAX_BYTES = 16 * 1024;
6969
+ var MAX_STREAM_ID = 4294967295;
6970
+ var FrameError = class extends Error {
6971
+ };
6972
+ function encodeFrame(type, stream, payload = Buffer.alloc(0)) {
6973
+ if (!TYPES.has(type)) throw new FrameError(`unknown frame type ${type}`);
6974
+ if (!Number.isInteger(stream) || stream < 0 || stream > MAX_STREAM_ID) throw new FrameError(`bad stream id ${stream}`);
6975
+ if (type === FrameType.control && stream !== 0) throw new FrameError("control frames are on stream 0");
6976
+ if (type !== FrameType.control && stream === 0) throw new FrameError("stream 0 is the link's own");
6977
+ const out = Buffer.allocUnsafe(HEADER_BYTES + payload.length);
6978
+ out.writeUInt8(type, 0);
6979
+ out.writeUInt32BE(stream, 1);
6980
+ out.set(payload, HEADER_BYTES);
6981
+ return out;
6982
+ }
6983
+ function decodeFrame(buf) {
6984
+ if (buf.length < HEADER_BYTES) throw new FrameError("frame too short");
6985
+ const type = buf.readUInt8(0);
6986
+ if (!TYPES.has(type)) throw new FrameError(`unknown frame type ${type}`);
6987
+ const stream = buf.readUInt32BE(1);
6988
+ const payload = buf.subarray(HEADER_BYTES);
6989
+ if (type === FrameType.control) {
6990
+ if (stream !== 0) throw new FrameError("control frames are on stream 0");
6991
+ if (payload.length > CONTROL_MAX_BYTES) throw new FrameError("control message too large");
6992
+ } else if (stream === 0) {
6993
+ throw new FrameError("stream 0 is the link's own");
6994
+ }
6995
+ return { type, stream, payload };
6996
+ }
6997
+ function encodeControl(message) {
6998
+ const json2 = Buffer.from(JSON.stringify(message), "utf8");
6999
+ if (json2.length > CONTROL_MAX_BYTES) throw new FrameError("control message too large");
7000
+ return encodeFrame(FrameType.control, 0, json2);
7001
+ }
7002
+ function decodeControl(payload) {
7003
+ let value;
7004
+ try {
7005
+ value = JSON.parse(payload.toString("utf8"));
7006
+ } catch {
7007
+ throw new FrameError("control message is not JSON");
7008
+ }
7009
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new FrameError("control message is not an object");
7010
+ const type = value.type;
7011
+ if (typeof type !== "string" || type === "") throw new FrameError("control message has no type");
7012
+ return value;
7013
+ }
7014
+
7015
+ // ../hostcore/src/link/protocol.ts
7016
+ import { createHash as createHash3, createHmac, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
7017
+ var PROTOCOL_VERSION = 1;
7018
+ var LINK_PATH = "/_gate/node";
7019
+ var DUPLICATE_BACKOFF_MS = 5 * 6e4;
7020
+ var NODE_TOKEN_SHAPE = /^abn_[A-Za-z0-9_-]{43}$/;
7021
+ function tokenDigest(token) {
7022
+ return createHash3("sha256").update(token).digest("hex");
7023
+ }
7024
+ function revokeMac(tokenDigestHex, machineId, nonce) {
7025
+ return createHmac("sha256", tokenDigestHex).update(`agentbox-revoked
7026
+ ${machineId}
7027
+ ${nonce}`).digest("hex");
7028
+ }
7029
+ function verifyRevoke(token, machineId, nonce, mac) {
7030
+ if (typeof mac !== "string" || !/^[0-9a-f]{64}$/.test(mac)) return false;
7031
+ const want = Buffer.from(revokeMac(tokenDigest(token), machineId, nonce), "hex");
7032
+ return timingSafeEqual2(want, Buffer.from(mac, "hex"));
7033
+ }
7034
+
7035
+ // ../hostcore/src/link/stream.ts
7036
+ import { Duplex as Duplex2 } from "node:stream";
7037
+ var STREAM_WINDOW = 256 * 1024;
7038
+ var DATA_CHUNK = 64 * 1024;
7039
+ var STREAM_BUDGET = 64 * 1024 * 1024;
7040
+ function decodeOpen(payload) {
7041
+ let v = {};
7042
+ try {
7043
+ v = JSON.parse(payload.toString("utf8"));
7044
+ } catch {
7045
+ }
7046
+ const id = (s) => typeof s === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(s) ? s : null;
7047
+ return { kind: v.kind === "upgrade" ? "upgrade" : "http", session: id(v.session), grant: id(v.grant) };
7048
+ }
7049
+ function windowPayload(n) {
7050
+ const b = Buffer.allocUnsafe(4);
7051
+ b.writeUInt32BE(n, 0);
7052
+ return b;
7053
+ }
7054
+ var LinkStream = class extends Duplex2 {
7055
+ constructor(id, sendFrame, window = STREAM_WINDOW) {
7056
+ super({ allowHalfOpen: false });
7057
+ this.id = id;
7058
+ this.sendFrame = sendFrame;
7059
+ this.window = window;
7060
+ this.credit = window;
7061
+ this.allowance = window;
7062
+ }
7063
+ id;
7064
+ sendFrame;
7065
+ window;
7066
+ /** Bytes the peer will still take from us. */
7067
+ credit;
7068
+ /** Bytes the peer may still send us before it must wait for a window frame. */
7069
+ allowance;
7070
+ /** Bytes received and pushed to our reader, not yet credited back. */
7071
+ unacked = 0;
7072
+ pending = null;
7073
+ remoteClosed = false;
7074
+ closeSent = false;
7075
+ // What net.Socket has and Node's HTTP server, its client and `ws` call.
7076
+ setTimeout(_ms, cb) {
7077
+ if (cb) this.once("timeout", cb);
7078
+ return this;
7079
+ }
7080
+ setNoDelay() {
7081
+ return this;
7082
+ }
7083
+ setKeepAlive() {
7084
+ return this;
7085
+ }
7086
+ ref() {
7087
+ return this;
7088
+ }
7089
+ unref() {
7090
+ return this;
7091
+ }
7092
+ /** A frame for this stream, from the link. */
7093
+ receive(frame) {
7094
+ if (this.destroyed) return;
7095
+ switch (frame.type) {
7096
+ case FrameType.data: {
7097
+ if (this.remoteClosed) return;
7098
+ const n = frame.payload.length;
7099
+ if (n === 0) return;
7100
+ this.allowance -= n;
7101
+ if (this.allowance < 0) {
7102
+ this.destroy(new Error("the far end sent past its window"));
7103
+ return;
7104
+ }
7105
+ this.unacked += n;
7106
+ if (this.push(Buffer.from(frame.payload)) && this.unacked >= this.window / 4) this.grant();
7107
+ return;
7108
+ }
7109
+ case FrameType.window: {
7110
+ if (frame.payload.length !== 4) {
7111
+ this.destroy(new Error("bad window frame"));
7112
+ return;
7113
+ }
7114
+ this.credit += frame.payload.readUInt32BE(0);
7115
+ if (this.credit > this.window * 4) {
7116
+ this.destroy(new Error("the far end widened the window past any it was given"));
7117
+ return;
7118
+ }
7119
+ this.flush();
7120
+ return;
7121
+ }
7122
+ case FrameType.close:
7123
+ this.remoteEnd();
7124
+ return;
7125
+ default:
7126
+ return;
7127
+ }
7128
+ }
7129
+ /** The far end is gone (its close, or the link's end). */
7130
+ remoteEnd() {
7131
+ if (this.remoteClosed) return;
7132
+ this.remoteClosed = true;
7133
+ this.closeSent = true;
7134
+ const p = this.pending;
7135
+ this.pending = null;
7136
+ p?.cb();
7137
+ this.push(null);
7138
+ if (this.readableLength === 0) setImmediate(() => this.destroy());
7139
+ else this.once("end", () => this.destroy());
7140
+ }
7141
+ grant() {
7142
+ if (this.unacked <= 0 || this.remoteClosed || this.closeSent) return;
7143
+ const n = this.unacked;
7144
+ this.unacked = 0;
7145
+ this.allowance += n;
7146
+ this.sendFrame(encodeFrame(FrameType.window, this.id, windowPayload(n)));
7147
+ }
7148
+ _read() {
7149
+ this.grant();
7150
+ }
7151
+ flush() {
7152
+ const p = this.pending;
7153
+ if (!p) return;
7154
+ this.pending = null;
7155
+ this.sendChunks(p.chunk, p.cb);
7156
+ }
7157
+ sendChunks(chunk, cb) {
7158
+ let off = 0;
7159
+ while (off < chunk.length) {
7160
+ if (this.remoteClosed) return cb();
7161
+ if (this.credit <= 0) {
7162
+ this.pending = { chunk: chunk.subarray(off), cb };
7163
+ return;
7164
+ }
7165
+ const n = Math.min(chunk.length - off, this.credit, DATA_CHUNK);
7166
+ this.sendFrame(encodeFrame(FrameType.data, this.id, chunk.subarray(off, off + n)));
7167
+ this.credit -= n;
7168
+ off += n;
7169
+ }
7170
+ cb();
7171
+ }
7172
+ _write(chunk, _enc, cb) {
7173
+ if (this.remoteClosed) return cb();
7174
+ this.sendChunks(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), cb);
7175
+ }
7176
+ _final(cb) {
7177
+ this.sendClose();
7178
+ cb();
7179
+ if (!this.remoteClosed) {
7180
+ this.remoteClosed = true;
7181
+ this.push(null);
7182
+ if (this.readableLength === 0) setImmediate(() => this.destroy());
7183
+ else this.once("end", () => this.destroy());
7184
+ }
7185
+ }
7186
+ _destroy(err, cb) {
7187
+ this.sendClose();
7188
+ const p = this.pending;
7189
+ this.pending = null;
7190
+ p?.cb(err ?? new Error("stream closed"));
7191
+ cb(err);
7192
+ }
7193
+ sendClose() {
7194
+ if (this.closeSent) return;
7195
+ this.closeSent = true;
7196
+ try {
7197
+ this.sendFrame(encodeFrame(FrameType.close, this.id));
7198
+ } catch {
7199
+ }
7200
+ }
7201
+ };
7202
+ var StreamTable = class {
7203
+ constructor(sendFrame, window = STREAM_WINDOW) {
7204
+ this.sendFrame = sendFrame;
7205
+ this.window = window;
7206
+ }
7207
+ sendFrame;
7208
+ window;
7209
+ streams = /* @__PURE__ */ new Map();
7210
+ get size() {
7211
+ return this.streams.size;
7212
+ }
7213
+ has(id) {
7214
+ return this.streams.has(id);
7215
+ }
7216
+ open(id) {
7217
+ if (this.streams.has(id)) throw new Error(`stream ${id} is open already`);
7218
+ const s = new LinkStream(id, this.sendFrame, this.window);
7219
+ this.streams.set(id, s);
7220
+ s.once("close", () => {
7221
+ if (this.streams.get(id) === s) this.streams.delete(id);
7222
+ });
7223
+ s.on("error", () => {
7224
+ });
7225
+ return s;
7226
+ }
7227
+ /** Route a data, window or close frame; false when no such stream is open. */
7228
+ receive(frame) {
7229
+ const s = this.streams.get(frame.stream);
7230
+ if (!s) return false;
7231
+ s.receive(frame);
7232
+ return true;
7233
+ }
7234
+ /** The link ended: every stream with it. */
7235
+ endAll(reason) {
7236
+ for (const s of [...this.streams.values()]) {
7237
+ s.remoteEnd();
7238
+ s.destroy(new Error(reason));
7239
+ }
7240
+ this.streams.clear();
7241
+ }
7242
+ };
7243
+
7244
+ // src/node/credentials.ts
7245
+ function readCredentials(file) {
7246
+ let raw;
7247
+ try {
7248
+ raw = fs10.readFileSync(file, "utf8");
7249
+ } catch (err) {
7250
+ if (err.code === "ENOENT") return null;
7251
+ throw err;
7252
+ }
7253
+ let c;
7254
+ try {
7255
+ c = JSON.parse(raw);
7256
+ } catch {
7257
+ throw new CliError(`${file} is not valid JSON; this machine cannot link until it is fixed or re-joined`);
7258
+ }
7259
+ const p = c.presence;
7260
+ if (c.version !== 1 || typeof c.box !== "string" || typeof c.machineId !== "string" || typeof c.name !== "string" || typeof c.token !== "string" || !NODE_TOKEN_SHAPE.test(c.token) || !p || typeof p.salt !== "string" || typeof p.key !== "string" || typeof p.iterations !== "number") {
7261
+ throw new CliError(`${file} is not a node's credentials; re-join this machine`);
7262
+ }
7263
+ return c;
7264
+ }
7265
+ function writeCredentials(file, c) {
7266
+ writePrivate2(file, `${JSON.stringify(c, null, 2)}
7267
+ `);
7268
+ }
7269
+
7270
+ // src/node/claude.ts
7271
+ import fs12 from "node:fs";
7272
+ import path11 from "node:path";
7273
+
7274
+ // ../../images/machine/agents.md
7275
+ var agents_default = "# You are working on {{machine}}, through agentbox\n\n{{machine}} is one of the owner's own computers, joined to their agentbox. You\nrun here as the owner, with their files, their tools and their network. The\nowner watches your terminal from their browser, on agentbox's machines page.\n\n- **To show the owner an app** you started (a dev server, say), tell them the\n port it listens on; they open it themselves.\n- **Never make anything public.** Do not use tunnels (ngrok, cloudflared,\n localtunnel), external hosting, claude.ai Artifacts or similar to show a web\n app unless the owner explicitly asks for that.\n- **Docker and ports are this machine's own.** Containers you start and ports\n you listen on are on {{machine}}, not in an agentbox sandbox; clean up what\n you start.\n";
7276
+
7277
+ // src/node/update.ts
7278
+ import { spawnSync as spawnSync2 } from "node:child_process";
7279
+ import crypto2 from "node:crypto";
7280
+ import fs11 from "node:fs";
7281
+ import path10 from "node:path";
7282
+
7283
+ // src/node/release-keys.ts
7284
+ var RELEASE_PUBLIC_KEYS = [
7285
+ // current
7286
+ `-----BEGIN PUBLIC KEY-----
7287
+ MCowBQYDK2VwAyEAQP6eEvWwcx5ptR56iFAJvDISpDYvy2oWOYNTH9KMUzw=
7288
+ -----END PUBLIC KEY-----`,
7289
+ // next (rotation)
7290
+ `-----BEGIN PUBLIC KEY-----
7291
+ MCowBQYDK2VwAyEANhmTV3Z/SVIUjCBjFrK+nZpl3Y2ck7vRex7l5CixSDU=
7292
+ -----END PUBLIC KEY-----`
7293
+ ];
7294
+
7295
+ // src/node/update-verify.ts
7296
+ import crypto from "node:crypto";
7297
+ var MACHINE_PLATFORMS = ["linux-x64", "linux-arm64", "win-x64"];
7298
+ var UpdateRefused = class extends Error {
7299
+ constructor(message) {
7300
+ super(message);
7301
+ this.name = "UpdateRefused";
7302
+ }
7303
+ };
7304
+ function machinePlatform(platform = process.platform, arch = process.arch) {
7305
+ const k = `${platform === "win32" ? "win" : platform}-${arch}`;
7306
+ return MACHINE_PLATFORMS.includes(k) ? k : null;
7307
+ }
7308
+ function parseVersion(v) {
7309
+ const m = /^v?(\d{1,9})\.(\d{1,9})\.(\d{1,9})$/.exec(v);
7310
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
7311
+ }
7312
+ function compareVersions(a, b) {
7313
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
7314
+ }
7315
+ function verifyManifest(bytes, signature, keys) {
7316
+ if (keys.length === 0) {
7317
+ throw new UpdateRefused(
7318
+ "this build of agentbox has no release keys built in, so it cannot check that an update is genuine; it does not update itself (install a release build from https://github.com/jabezpauls/agentbox/releases)"
7319
+ );
7320
+ }
7321
+ const text = (Buffer.isBuffer(signature) ? signature.toString("utf8") : signature ?? "").trim();
7322
+ const sig = /^[A-Za-z0-9+/]{86}==$/.test(text) ? Buffer.from(text, "base64") : null;
7323
+ if (!sig || sig.length !== 64) throw new UpdateRefused("the release manifest is not signed (no valid signature beside it); not updating");
7324
+ const trusted = keys.some((pem) => {
7325
+ try {
7326
+ const key = crypto.createPublicKey(pem);
7327
+ return key.asymmetricKeyType === "ed25519" && crypto.verify(null, bytes, key, sig);
7328
+ } catch {
7329
+ return false;
7330
+ }
7331
+ });
7332
+ if (!trusted) throw new UpdateRefused("the release manifest is not signed by a release key this node trusts; not updating");
7333
+ return parseManifest(bytes);
7334
+ }
7335
+ var HEX64 = /^[0-9a-f]{64}$/;
7336
+ function asset(v, where, versioned) {
7337
+ const a = v;
7338
+ if (!a || typeof a !== "object") throw new UpdateRefused(`the release manifest has no ${where}`);
7339
+ if (typeof a.url !== "string" || !/^https:\/\//.test(a.url)) throw new UpdateRefused(`the release manifest's ${where} has no https URL`);
7340
+ if (typeof a.sha256 !== "string" || !HEX64.test(a.sha256)) throw new UpdateRefused(`the release manifest's ${where} has no SHA-256`);
7341
+ const out = { url: a.url, sha256: a.sha256 };
7342
+ if (typeof a.size === "number" && Number.isSafeInteger(a.size) && a.size > 0) out.size = a.size;
7343
+ if (!versioned) return out;
7344
+ if (typeof a.version !== "string" || !/^[0-9A-Za-z.+-]{1,64}$/.test(a.version)) throw new UpdateRefused(`the release manifest's ${where} has no version`);
7345
+ const entry = { ...out, version: a.version };
7346
+ if (a.files !== void 0) {
7347
+ const f = a.files;
7348
+ if (!f || typeof f !== "object" || !Object.values(f).every((d) => typeof d === "string" && HEX64.test(d))) {
7349
+ throw new UpdateRefused(`the release manifest's ${where} lists files without a SHA-256 each`);
7350
+ }
7351
+ entry.files = f;
7352
+ }
7353
+ return entry;
7354
+ }
7355
+ function parseManifest(bytes) {
7356
+ let raw;
7357
+ try {
7358
+ raw = JSON.parse(bytes.toString("utf8"));
7359
+ } catch {
7360
+ throw new UpdateRefused("the release manifest is not JSON");
7361
+ }
7362
+ if (!raw || typeof raw !== "object" || raw.schema !== 1) throw new UpdateRefused("the release manifest is of a kind this node does not know; update it by hand");
7363
+ if (typeof raw.version !== "string" || !parseVersion(raw.version)) throw new UpdateRefused("the release manifest has no version");
7364
+ const platforms = {};
7365
+ const given = raw.platforms ?? {};
7366
+ for (const p of MACHINE_PLATFORMS) {
7367
+ const e = given[p];
7368
+ if (!e) continue;
7369
+ const entry = {
7370
+ node: asset(e.node, `${p} node`, false),
7371
+ herdr: asset(e.herdr, `${p} herdr`, true),
7372
+ claude: asset(e.claude, `${p} Claude Code`, true)
7373
+ };
7374
+ if (e.nodeGui !== void 0) entry.nodeGui = asset(e.nodeGui, `${p} windowless node`, false);
7375
+ platforms[p] = entry;
7376
+ }
7377
+ return { schema: 1, version: raw.version, platforms };
7378
+ }
7379
+
7380
+ // src/node/update.ts
7381
+ var RELEASES_URL = "https://github.com/jabezpauls/agentbox/releases/latest/download";
7382
+ var MANIFEST_FILE = "machines-manifest.json";
7383
+ var SIGNATURE_FILE = "machines-manifest.json.sig";
7384
+ var MAX_MANIFEST = 256 * 1024;
7385
+ var MAX_SIGNATURE = 4 * 1024;
7386
+ var MAX_ASSET = 1024 * 1024 * 1024;
7387
+ function describeReport(r) {
7388
+ const tools = (t2) => [t2.herdr === "deferred" || t2.claude === "deferred" ? "herdr and Claude Code are updated once no agent is running" : "", t2.herdr === "updated" ? "herdr updated" : "", t2.claude === "updated" ? "Claude Code updated" : ""].filter(Boolean).join("; ");
7389
+ switch (r.state) {
7390
+ case "off":
7391
+ return "updates are off (policy.json)";
7392
+ case "current": {
7393
+ const t2 = tools(r.tools);
7394
+ return `up to date (agentbox ${r.version})${t2 ? `; ${t2}` : ""}`;
7395
+ }
7396
+ case "available":
7397
+ return `agentbox ${r.version} is available (this is ${r.from}); updates are set to notify, so run it by hand or set "updates": "auto"`;
7398
+ case "updated": {
7399
+ const t2 = tools(r.tools);
7400
+ return `updated agentbox ${r.from} \u2192 ${r.version}${r.restart ? ", restarting" : ""}${t2 ? `; ${t2}` : ""}`;
7401
+ }
7402
+ }
7403
+ }
7404
+ async function fetchLimited(fetcher, url, max, what) {
7405
+ const res = await fetcher(url, { redirect: "follow" });
7406
+ if (res.status === 404) return null;
7407
+ if (!res.ok || !res.body) throw new Error(`${what}: HTTP ${res.status} from ${url}`);
7408
+ const chunks = [];
7409
+ let size = 0;
7410
+ for await (const c of res.body) {
7411
+ size += c.length;
7412
+ if (size > max) throw new UpdateRefused(`${what} is larger than any real one; not updating`);
7413
+ chunks.push(Buffer.from(c));
7414
+ }
7415
+ return Buffer.concat(chunks);
7416
+ }
7417
+ async function fetchManifest(o = {}) {
7418
+ const keys = o.keys ?? RELEASE_PUBLIC_KEYS;
7419
+ if (keys.length === 0) verifyManifest(Buffer.alloc(0), null, keys);
7420
+ const fetcher = o.fetch ?? globalThis.fetch;
7421
+ const base = o.releasesUrl ?? RELEASES_URL;
7422
+ const bytes = await fetchLimited(fetcher, `${base}/${MANIFEST_FILE}`, MAX_MANIFEST, "the release manifest");
7423
+ if (!bytes) throw new UpdateRefused("the latest release has no machines manifest");
7424
+ const sig = await fetchLimited(fetcher, `${base}/${SIGNATURE_FILE}`, MAX_SIGNATURE, "the manifest's signature");
7425
+ return verifyManifest(bytes, sig, keys);
7426
+ }
7427
+ async function downloadVerified(fetcher, asset2, dest, what) {
7428
+ const dir = path10.dirname(dest);
7429
+ fs11.mkdirSync(dir, { recursive: true });
7430
+ const tmp = path10.join(dir, `.${path10.basename(dest)}.update-${crypto2.randomBytes(4).toString("hex")}`);
7431
+ const hash = crypto2.createHash("sha256");
7432
+ let fd = null;
7433
+ try {
7434
+ const res = await fetcher(asset2.url, { redirect: "follow" });
7435
+ if (!res.ok || !res.body) throw new Error(`${what}: HTTP ${res.status} from ${asset2.url}`);
7436
+ fd = fs11.openSync(tmp, "wx", 448);
7437
+ let size = 0;
7438
+ for await (const c of res.body) {
7439
+ size += c.length;
7440
+ if (size > (asset2.size ?? MAX_ASSET) || size > MAX_ASSET) throw new UpdateRefused(`${what} is larger than the manifest says; not installing it`);
7441
+ hash.update(c);
7442
+ fs11.writeSync(fd, c);
7443
+ }
7444
+ fs11.closeSync(fd);
7445
+ fd = null;
7446
+ const got = hash.digest("hex");
7447
+ if (got !== asset2.sha256) throw new UpdateRefused(`${what} does not match the signed manifest (SHA-256 ${got}, expected ${asset2.sha256}); not installing it`);
7448
+ fs11.chmodSync(tmp, 493);
7449
+ return tmp;
7450
+ } catch (err) {
7451
+ if (fd !== null) fs11.closeSync(fd);
7452
+ fs11.rmSync(tmp, { force: true });
7453
+ throw err;
7454
+ }
7455
+ }
7456
+ function replaceFile(tmp, dest, platform = process.platform) {
7457
+ try {
7458
+ if (platform === "win32" && fs11.existsSync(dest)) {
7459
+ const old = `${dest}.old`;
7460
+ fs11.rmSync(old, { force: true });
7461
+ fs11.renameSync(dest, old);
7462
+ }
7463
+ fs11.renameSync(tmp, dest);
7464
+ } catch (err) {
7465
+ fs11.rmSync(tmp, { force: true });
7466
+ throw err;
7467
+ }
7468
+ }
7469
+ function defaultCheckRuns(file, version) {
7470
+ const out = spawnSync2(file, ["--version"], { encoding: "utf8", timeout: 3e4, windowsHide: true });
7471
+ const said = (out.stdout ?? "").trim();
7472
+ if (out.status !== 0 || said !== `agentbox ${version}`) {
7473
+ throw new UpdateRefused(`the downloaded agentbox does not run here, or is not ${version} (it said ${JSON.stringify(said.slice(0, 100))}); not installing it`);
7474
+ }
7475
+ }
7476
+ async function updateTool(o, fetcher, platform, name, installed, asset2, idle) {
7477
+ if (!installed) return "none";
7478
+ if (installed.version === asset2.version) return "current";
7479
+ if (asset2.files) return "unsupported";
7480
+ if (!idle) return "deferred";
7481
+ const tmp = await downloadVerified(fetcher, asset2, installed.path, `${name} ${asset2.version} for ${platform}`);
7482
+ replaceFile(tmp, installed.path, o.platform ?? process.platform);
7483
+ if (installed.versionFile) fs11.writeFileSync(installed.versionFile, `${asset2.version}
7484
+ `, { mode: 384 });
7485
+ return "updated";
7486
+ }
7487
+ async function checkForUpdate(o) {
7488
+ if (o.policy === "off") return { state: "off" };
7489
+ const running = parseVersion(o.running);
7490
+ if (!running) throw new UpdateRefused(`this node's version (${o.running}) is not a release's, so it cannot tell what is newer; install a release build`);
7491
+ const platform = machinePlatform(o.platform ?? process.platform, o.arch ?? process.arch);
7492
+ if (!platform) throw new UpdateRefused(`no agentbox release is built for ${o.platform ?? process.platform}-${o.arch ?? process.arch}`);
7493
+ const fetcher = o.fetch ?? globalThis.fetch;
7494
+ const manifest = await fetchManifest(o);
7495
+ const order = compareVersions(parseVersion(manifest.version), running);
7496
+ if (order < 0) throw new UpdateRefused(`the latest release (${manifest.version}) is older than this node (${o.running}); not going back`);
7497
+ const assets = manifest.platforms[platform];
7498
+ if (!assets) throw new UpdateRefused(`the release ${manifest.version} has nothing for ${platform}`);
7499
+ const newer = order > 0;
7500
+ if (newer && o.policy === "notify") return { state: "available", version: manifest.version, from: o.running };
7501
+ const tools = async () => {
7502
+ const needed = [o.herdr && o.herdr.version !== assets.herdr.version, o.claude && o.claude.version !== assets.claude.version].some(Boolean);
7503
+ const idle = needed && o.policy === "auto" && o.idle ? await o.idle() : false;
7504
+ return {
7505
+ herdr: await updateTool(o, fetcher, platform, "herdr", o.herdr, assets.herdr, idle),
7506
+ claude: await updateTool(o, fetcher, platform, "Claude Code", o.claude, assets.claude, idle)
7507
+ };
7508
+ };
7509
+ if (!newer) return { state: "current", version: o.running, tools: o.policy === "auto" ? await tools() : { herdr: "none", claude: "none" } };
7510
+ if (o.nodePath === null) return { state: "available", version: manifest.version, from: o.running };
7511
+ const node = await downloadVerified(fetcher, assets.node, o.nodePath, `agentbox ${manifest.version} for ${platform}`);
7512
+ let gui = null;
7513
+ try {
7514
+ (o.checkRuns ?? defaultCheckRuns)(node, manifest.version);
7515
+ if (o.nodeGuiPath && assets.nodeGui) gui = await downloadVerified(fetcher, assets.nodeGui, o.nodeGuiPath, `agentbox-node.exe ${manifest.version}`);
7516
+ } catch (err) {
7517
+ fs11.rmSync(node, { force: true });
7518
+ throw err;
7519
+ }
7520
+ replaceFile(node, o.nodePath, o.platform ?? process.platform);
7521
+ if (gui && o.nodeGuiPath) replaceFile(gui, o.nodeGuiPath, o.platform ?? process.platform);
7522
+ return { state: "updated", version: manifest.version, from: o.running, restart: true, tools: await tools() };
7523
+ }
7524
+
7525
+ // src/node/claude.ts
7526
+ var CLAUDE_VERSION = "2.1.311";
7527
+ var CLAUDE_PINS = {
7528
+ "linux-x64": { path: "linux-x64/claude", sha256: "86dc1fcc69498159eaf15f446613e847010f9036bfbbdffd4c49350be8489ada", size: 293776752 },
7529
+ "linux-arm64": { path: "linux-arm64/claude", sha256: "2e2496c9a8065df93acb0fca2d341f59a6b8777649b2e025ab8c9bcd5eef61f3", size: 293658808 },
7530
+ "win32-x64": { path: "win32-x64/claude.exe", sha256: "966f4b2ce962298f5dee6a942e8c47c8f97392e69e728e622319a98b259c0680", size: 298870432 }
7531
+ };
7532
+ var CLAUDE_RELEASES = "https://downloads.claude.ai/claude-code-releases";
7533
+ function claudePaths(p, platform = process.platform) {
7534
+ const exe = platform === "win32" ? "claude.exe" : "claude";
7535
+ return {
7536
+ bin: path11.join(p.bin, exe),
7537
+ versionFile: path11.join(p.bin, "claude.version"),
7538
+ config: path11.join(p.dir, "claude"),
7539
+ shims: path11.join(p.dir, "shims"),
7540
+ shim: path11.join(p.dir, "shims", "claude"),
7541
+ agents: path11.join(p.dir, "agents.md"),
7542
+ token: path11.join(p.secrets, "claude-token")
7543
+ };
7544
+ }
7545
+ function writeIfAbsent(file, contents) {
7546
+ if (fs12.existsSync(file)) return false;
7547
+ writePrivate2(file, contents);
7548
+ return true;
7549
+ }
7550
+ function seedClaudeConfig(c, home2) {
7551
+ ensurePrivateDir(c.config);
7552
+ writeIfAbsent(path11.join(c.config, ".claude.json"), `${JSON.stringify({ hasCompletedOnboarding: true, projects: { [home2]: { hasTrustDialogAccepted: true } } }, null, 2)}
7553
+ `);
7554
+ writeIfAbsent(path11.join(c.config, "settings.json"), `${JSON.stringify({ disableDeepLinkRegistration: "disable" }, null, 2)}
7555
+ `);
7556
+ }
7557
+ function agentsText(machine) {
7558
+ return agents_default.replaceAll("{{machine}}", machine.replace(/[^\w.-]/g, ""));
7559
+ }
7560
+ function shQuote(s) {
7561
+ return `'${s.replace(/'/g, "'\\''")}'`;
7562
+ }
7563
+ var PLAIN = ["mcp", "config", "doctor", "update", "install", "setup-token", "migrate-installer", "plugin", "-v", "--version", "-h", "--help"];
7564
+ function shimText(c) {
7565
+ return `#!/bin/sh
7566
+ # agentbox: Claude Code on this machine. Written by the node; rewritten at
7567
+ # every start. Reads the shared sign-in (when this machine was given it) at
7568
+ # each launch, for this process only.
7569
+ CLAUDE_CONFIG_DIR=${shQuote(c.config)}
7570
+ DISABLE_UPDATES=1
7571
+ export CLAUDE_CONFIG_DIR DISABLE_UPDATES
7572
+ if [ -f ${shQuote(c.token)} ]; then
7573
+ CLAUDE_CODE_OAUTH_TOKEN="$(cat ${shQuote(c.token)})"
7574
+ export CLAUDE_CODE_OAUTH_TOKEN
7575
+ fi
7576
+ if [ ! -x ${shQuote(c.bin)} ]; then
7577
+ echo "agentbox: Claude Code is not installed on this machine yet (it is installed by \`agentbox node join\`)." >&2
7578
+ exit 127
7579
+ fi
7580
+ case "\${1:-}" in
7581
+ ${PLAIN.join("|")}) exec ${shQuote(c.bin)} "$@" ;;
7582
+ esac
7583
+ exec ${shQuote(c.bin)} --append-system-prompt-file ${shQuote(c.agents)} "$@"
7584
+ `;
7585
+ }
7586
+ function prepareClaude(c, o) {
7587
+ seedClaudeConfig(c, o.home);
7588
+ writePrivate2(c.agents, agentsText(o.machine));
7589
+ ensurePrivateDir(c.shims);
7590
+ writePrivate2(c.shim, shimText(c), 448);
7591
+ }
7592
+ function holdToken(c, token) {
7593
+ if (token === null) {
7594
+ fs12.rmSync(c.token, { force: true });
7595
+ return;
7596
+ }
7597
+ if (!/^[A-Za-z0-9_\-.~+/=]{20,512}$/.test(token)) return;
7598
+ writePrivate2(c.token, token);
7599
+ }
7600
+ function installedClaude(c) {
7601
+ if (!fs12.existsSync(c.bin)) return null;
7602
+ try {
7603
+ const v = fs12.readFileSync(c.versionFile, "utf8").trim();
7604
+ return parseVersion(v) ? v : null;
7605
+ } catch {
7606
+ return null;
7607
+ }
7608
+ }
7609
+ async function installClaude(c, deps) {
7610
+ const have = installedClaude(c);
7611
+ const want = parseVersion(CLAUDE_VERSION);
7612
+ if (have && compareVersions(parseVersion(have), want) >= 0) return have;
7613
+ const key = `${deps.platform}-${deps.arch}`;
7614
+ const pin = CLAUDE_PINS[key];
7615
+ if (!pin) throw new CliError(`Claude Code ${CLAUDE_VERSION} has no build for ${deps.platform}/${deps.arch} that this node knows`);
7616
+ deps.log?.(`Installing Claude Code ${CLAUDE_VERSION} (about ${Math.round(pin.size / 2 ** 20)} MiB)\u2026
7617
+ `);
7618
+ const tmp = await downloadVerified(
7619
+ deps.fetch ?? globalThis.fetch,
7620
+ { url: `${CLAUDE_RELEASES}/${CLAUDE_VERSION}/${pin.path}`, sha256: pin.sha256, size: pin.size },
7621
+ c.bin,
7622
+ `Claude Code ${CLAUDE_VERSION}`
7623
+ );
7624
+ replaceFile(tmp, c.bin);
7625
+ writePrivate2(c.versionFile, `${CLAUDE_VERSION}
7626
+ `);
7627
+ return CLAUDE_VERSION;
7628
+ }
7629
+
7630
+ // src/node/install.ts
7631
+ import { createHash as createHash4 } from "node:crypto";
7632
+ import fs13 from "node:fs";
7633
+ import path12 from "node:path";
7634
+ var HERDR_VERSION = "0.9.3";
7635
+ var HERDR_PINS = {
7636
+ "linux-x64": { asset: "herdr-linux-x86_64", sha256: "18a8dc65f1c2fa485884344356dea1cfd911c6f06cf46fa78e193f4087f4dba7" },
7637
+ "linux-arm64": { asset: "herdr-linux-aarch64", sha256: "4de7aa3e25678812e92960de64f7c2aaa1bca1f0f80a3c5e559837e231e1f5c0" }
7638
+ };
7639
+ function sha256(bytes) {
7640
+ return createHash4("sha256").update(bytes).digest("hex");
7641
+ }
7642
+ async function fetchBytes(url) {
7643
+ let res;
7644
+ try {
7645
+ res = await fetch(url, { redirect: "follow" });
7646
+ } catch (err) {
7647
+ throw new CliError(`could not download ${url}: ${err.message}`);
7648
+ }
7649
+ if (!res.ok) throw new CliError(`could not download ${url}: HTTP ${res.status}`);
7650
+ return new Uint8Array(await res.arrayBuffer());
7651
+ }
7652
+ async function installHerdr(binDir, deps) {
7653
+ const pin = HERDR_PINS[`${deps.platform}-${deps.arch}`];
7654
+ if (!pin) throw new CliError(`herdr ${HERDR_VERSION} has no build for ${deps.platform}/${deps.arch} that this node knows`);
7655
+ const target = path12.join(binDir, "herdr");
7656
+ try {
7657
+ if (sha256(fs13.readFileSync(target)) === pin.sha256) return target;
7658
+ } catch {
7659
+ }
7660
+ const url = `https://github.com/herdrdev/herdr/releases/download/v${HERDR_VERSION}/${pin.asset}`;
7661
+ deps.log?.(`Installing herdr ${HERDR_VERSION}\u2026
7662
+ `);
7663
+ const bytes = deps.from ? new Uint8Array(fs13.readFileSync(deps.from)) : await (deps.download ?? fetchBytes)(url);
7664
+ const got = sha256(bytes);
7665
+ if (got !== pin.sha256) throw new CliError(`herdr ${HERDR_VERSION} failed verification (SHA-256 ${got}, expected ${pin.sha256}); nothing was installed`);
7666
+ writePrivate2(target, bytes, 448);
7667
+ return target;
7668
+ }
7669
+
7670
+ // src/node/join.ts
7671
+ import fs15 from "node:fs";
7672
+
7673
+ // src/node/policy.ts
7674
+ import fs14 from "node:fs";
7675
+ import path13 from "node:path";
7676
+ var DENY_ALL = { terminals: false, files: [], ports: false, share: false, updates: "auto", presenceHours: 12, paused: false };
7677
+ var KEYS = /* @__PURE__ */ new Set(["terminals", "files", "ports", "share", "updates", "presenceHours", "paused"]);
7678
+ function broken(problem) {
7679
+ return { policy: { ...DENY_ALL, files: [] }, problem };
7680
+ }
7681
+ function expand(p, home2) {
7682
+ if (p === "~") return home2;
7683
+ if (p.startsWith("~/")) return path13.join(home2, p.slice(2));
7684
+ return p;
7685
+ }
7686
+ function parsePolicy(text, home2) {
7687
+ let raw;
7688
+ try {
7689
+ raw = JSON.parse(text);
7690
+ } catch {
7691
+ return broken("policy.json is not valid JSON");
7692
+ }
7693
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return broken("policy.json is not an object");
7694
+ const o = raw;
7695
+ for (const k of Object.keys(o)) if (!KEYS.has(k)) return broken(`policy.json has "${k}", which this node does not know`);
7696
+ const policy = { ...DENY_ALL, files: [] };
7697
+ for (const k of ["terminals", "ports", "share", "paused"]) {
7698
+ if (o[k] === void 0) continue;
7699
+ if (typeof o[k] !== "boolean") return broken(`policy.json: "${k}" must be true or false`);
7700
+ policy[k] = o[k];
7701
+ }
7702
+ if (o.updates !== void 0) {
7703
+ if (o.updates !== "auto" && o.updates !== "notify" && o.updates !== "off") return broken('policy.json: "updates" must be "auto", "notify" or "off"');
7704
+ policy.updates = o.updates;
7705
+ }
7706
+ if (o.presenceHours !== void 0) {
7707
+ const h = o.presenceHours;
7708
+ if (typeof h !== "number" || !Number.isInteger(h) || h < 1 || h > 24) return broken('policy.json: "presenceHours" must be a whole number from 1 to 24');
7709
+ policy.presenceHours = h;
7710
+ }
7711
+ if (o.files !== void 0) {
7712
+ if (!Array.isArray(o.files)) return broken('policy.json: "files" must be a list');
7713
+ for (const f of o.files) {
7714
+ const e = f ?? {};
7715
+ if (typeof e.path !== "string" || e.path === "") return broken('policy.json: every "files" entry needs a "path"');
7716
+ if (e.mode !== "read" && e.mode !== "write") return broken(`policy.json: "${e.path}" needs a "mode" of "read" or "write"`);
7717
+ const full = expand(e.path, home2);
7718
+ if (!path13.isAbsolute(full)) return broken(`policy.json: "${e.path}" is not an absolute folder (or ~/\u2026)`);
7719
+ policy.files.push({ path: path13.normalize(full), mode: e.mode });
7720
+ }
7721
+ }
7722
+ return { policy, problem: null };
7723
+ }
7724
+ function loadPolicy(file, home2) {
7725
+ let text;
7726
+ try {
7727
+ text = fs14.readFileSync(file, "utf8");
7728
+ } catch (err) {
7729
+ if (err.code === "ENOENT") return broken("there is no policy.json");
7730
+ return broken(`policy.json cannot be read (${err.code ?? "error"})`);
7731
+ }
7732
+ return parsePolicy(text, home2);
7733
+ }
7734
+ function summarize(l) {
7735
+ return { ...l.policy, files: l.policy.files.map((f) => ({ ...f })), problem: l.problem };
7736
+ }
7737
+ function watchPolicy(file, home2, onChange, intervalMs = 1e3) {
7738
+ let last = JSON.stringify(loadPolicy(file, home2));
7739
+ const check = () => {
7740
+ const now = loadPolicy(file, home2);
7741
+ const s = JSON.stringify(now);
7742
+ if (s === last) return;
7743
+ last = s;
7744
+ onChange(now);
7745
+ };
7746
+ const timer = setInterval(check, intervalMs);
7747
+ timer.unref();
7748
+ return () => clearInterval(timer);
7749
+ }
7750
+ function writeInitialPolicy(file, folders) {
7751
+ if (fs14.existsSync(file)) return false;
7752
+ const policy = { ...DENY_ALL, terminals: true, files: folders };
7753
+ writePrivate2(file, `${JSON.stringify(policy, null, 2)}
7754
+ `);
7755
+ return true;
7756
+ }
7757
+ function setPaused(file, paused) {
7758
+ let o;
7759
+ try {
7760
+ o = JSON.parse(fs14.readFileSync(file, "utf8"));
7761
+ } catch (err) {
7762
+ if (err.code === "ENOENT") o = { ...DENY_ALL };
7763
+ else throw new CliError(`${file} is not valid JSON; fix it first (everything is denied meanwhile)`);
7764
+ }
7765
+ if (typeof o !== "object" || o === null || Array.isArray(o)) throw new CliError(`${file} is not a policy; fix it first`);
7766
+ o.paused = paused;
7767
+ writePrivate2(file, `${JSON.stringify(o, null, 2)}
7768
+ `);
7769
+ }
7770
+
7771
+ // src/node/join.ts
7772
+ var MIN_FREE_BYTES = 512 * 1024 * 1024;
7773
+ var SUPPORTED_PLATFORMS = /* @__PURE__ */ new Set(["linux"]);
7774
+ async function joinMachine(o, s) {
7775
+ const existing = readCredentials(s.paths.credentials);
7776
+ if (existing && existing.box !== o.box && existing.revokedAt === void 0) {
7777
+ throw new CliError(
7778
+ `this machine is joined to ${existing.box} already; a machine joins one box only (run \`agentbox node leave\` first)`
7779
+ );
7780
+ }
7781
+ const rejoin = existing && existing.box === o.box && existing.revokedAt === void 0 ? existing : null;
7782
+ if (!SUPPORTED_PLATFORMS.has(s.platform)) throw new CliError(`machines on ${s.platform} are not supported yet (Linux only, for now)`);
7783
+ try {
7784
+ ensurePrivateDir(s.paths.dir);
7785
+ fs15.accessSync(s.paths.dir, fs15.constants.W_OK);
7786
+ } catch (err) {
7787
+ throw new CliError(`the node folder ${s.paths.dir} is not writable: ${err.message}`);
7788
+ }
7789
+ const free = s.freeBytes(s.paths.dir);
7790
+ if (free !== null && free < MIN_FREE_BYTES) {
7791
+ throw new CliError(`${s.paths.dir} has ${Math.floor(free / 2 ** 20)} MiB free; the node needs ${MIN_FREE_BYTES / 2 ** 20} MiB`);
7792
+ }
7793
+ await s.install();
7794
+ const presence = await s.enrol(await s.passphrase());
7795
+ let started;
7796
+ try {
7797
+ started = await s.client.json("POST", "/_gate/node/join/start", {
7798
+ auth: false,
7799
+ body: {
7800
+ json: {
7801
+ name: o.name,
7802
+ platform: s.platform,
7803
+ arch: s.arch,
7804
+ hostname: s.hostname,
7805
+ ...rejoin ? { rejoin: { machineId: rejoin.machineId, token: rejoin.token } } : {}
7806
+ }
7807
+ },
7808
+ what: "asking the box to join"
7809
+ });
7810
+ } catch (err) {
7811
+ if (err.status === 404) throw new CliError(`${o.box} does not have machines turned on (or is not an agentbox)`, EXIT.NOT_FOUND);
7812
+ throw err;
7813
+ }
7814
+ s.err(`
7815
+ ${rejoin ? "Re-joining" : "Joining"} as "${started.name}". On the box's machines Admin page${started.approveAt ? ` (${started.approveAt})` : ""},
7816
+ `);
7817
+ s.err(`check the name, platform and address, and type this code:
7818
+
7819
+ ${started.userCode}
7820
+
7821
+ `);
7822
+ s.err(`Waiting for approval (the code lasts ${Math.round(started.expiresIn / 60)} minutes)\u2026
7823
+ `);
7824
+ const deadline = s.now() + started.expiresIn * 1e3;
7825
+ let got = null;
7826
+ while (got === null) {
7827
+ await s.sleep(Math.max(1, started.interval) * 1e3);
7828
+ if (s.now() > deadline) throw new CliError("the code expired before it was approved; run `agentbox node join` again");
7829
+ let p;
7830
+ try {
7831
+ p = await s.client.json("POST", "/_gate/node/join/poll", { auth: false, body: { json: { joinCode: started.joinCode } }, what: "waiting for approval" });
7832
+ } catch (err) {
7833
+ if (err instanceof CliError && (err.exitCode === EXIT.UNREACHABLE || err.status === 429 || (err.status ?? 0) >= 500)) continue;
7834
+ throw err;
7835
+ }
7836
+ if (p.status === "pending") continue;
7837
+ if (p.status === "denied") throw new CliError("the join was denied on the box");
7838
+ if (p.status === "expired") throw new CliError("the code expired, or was already used; run `agentbox node join` again");
7839
+ got = p;
7840
+ }
7841
+ const credentials = {
7842
+ version: 1,
7843
+ box: o.box,
7844
+ machineId: got.machineId,
7845
+ name: got.name,
7846
+ token: got.token,
7847
+ presence,
7848
+ joinedAt: s.now()
7849
+ };
7850
+ writeCredentials(s.paths.credentials, credentials);
7851
+ s.err(`Joined as "${got.name}" (machine ${got.machineId}).
7852
+ `);
7853
+ if (fs15.existsSync(s.paths.policy)) {
7854
+ s.err(`Keeping the existing ${s.paths.policy}:
7855
+ `);
7856
+ } else {
7857
+ const folders = o.folders ?? await s.askFolders();
7858
+ writeInitialPolicy(s.paths.policy, folders);
7859
+ s.err(`Wrote ${s.paths.policy}:
7860
+ `);
7861
+ }
7862
+ s.printPolicy();
7863
+ if (s.service) {
7864
+ await s.service();
7865
+ s.err("Waiting for the machine to show online\u2026\n");
7866
+ if (await s.waitOnline()) s.err("Online.\n");
7867
+ else s.err("It has not come online yet; see `agentbox node status` and `agentbox node logs`.\n");
7868
+ } else {
7869
+ s.err("Start it with `agentbox node run` (no service was registered).\n");
7870
+ }
7871
+ return credentials;
7872
+ }
7873
+
7874
+ // src/node/link.ts
7875
+ import { randomBytes as randomBytes5 } from "node:crypto";
7876
+ import { EventEmitter } from "node:events";
7877
+ function backoffMs(n, min, max, random = Math.random) {
7878
+ const base = Math.min(max, min * 2 ** Math.max(0, n - 1));
7879
+ return Math.round(base / 2 + base / 2 * random());
7880
+ }
7881
+ var NodeLink = class extends EventEmitter {
7882
+ ws = null;
7883
+ stateNow = "stopped";
7884
+ running = false;
7885
+ paused = false;
7886
+ failures = 0;
7887
+ failingSince = null;
7888
+ retryTimer = null;
7889
+ wakeTimer = null;
7890
+ lastTick = 0;
7891
+ nonce = "";
7892
+ nextDelay = null;
7893
+ o;
7894
+ constructor(opts) {
7895
+ super();
7896
+ this.o = {
7897
+ now: Date.now,
7898
+ random: Math.random,
7899
+ pingMs: PING_MS,
7900
+ minBackoffMs: 1e3,
7901
+ maxBackoffMs: 6e4,
7902
+ slowAfterMs: 60 * 6e4,
7903
+ slowRetryMs: 60 * 6e4,
7904
+ duplicateBackoffMs: DUPLICATE_BACKOFF_MS,
7905
+ wakeCheckMs: 5e3,
7906
+ wakeJumpMs: 15e3,
7907
+ ...opts
7908
+ };
7909
+ }
7910
+ get state() {
7911
+ return this.stateNow;
7912
+ }
7913
+ set(state) {
7914
+ if (state === this.stateNow) return;
7915
+ this.stateNow = state;
7916
+ this.emit("state", state);
7917
+ }
7918
+ log(line) {
7919
+ this.emit("log", line);
7920
+ }
7921
+ start() {
7922
+ if (this.running) return;
7923
+ this.running = true;
7924
+ this.lastTick = this.o.now();
7925
+ this.wakeTimer = setInterval(() => this.tick(), this.o.wakeCheckMs);
7926
+ this.wakeTimer.unref();
7927
+ if (!this.paused) this.connect();
7928
+ else this.set("paused");
7929
+ }
7930
+ stop(state = "stopped") {
7931
+ this.running = false;
7932
+ if (this.wakeTimer) clearInterval(this.wakeTimer);
7933
+ this.wakeTimer = null;
7934
+ this.clearRetry();
7935
+ const ws = this.ws;
7936
+ this.ws = null;
7937
+ ws?.close(1e3, "stopping");
7938
+ this.set(state);
7939
+ }
7940
+ /** `agentbox node pause`: say so, close, and stay closed until resume. */
7941
+ pause() {
7942
+ this.paused = true;
7943
+ this.clearRetry();
7944
+ const ws = this.ws;
7945
+ this.ws = null;
7946
+ if (ws && ws.readyState === import_websocket.default.OPEN) {
7947
+ ws.send(encodeControl({ type: "pause" }));
7948
+ ws.close(1e3, "paused");
7949
+ } else {
7950
+ ws?.terminate();
7951
+ }
7952
+ if (this.running) this.set("paused");
7953
+ }
7954
+ resume() {
7955
+ if (!this.paused) return;
7956
+ this.paused = false;
7957
+ this.failures = 0;
7958
+ this.failingSince = null;
7959
+ if (this.running && !this.ws) this.connect();
7960
+ }
7961
+ get isPaused() {
7962
+ return this.paused;
7963
+ }
7964
+ sendPolicy(policy) {
7965
+ if (this.stateNow === "online" && this.ws?.readyState === import_websocket.default.OPEN) this.ws.send(encodeControl({ type: "policy", policy }));
7966
+ }
7967
+ /** Tell the box this machine is leaving; resolves whether it was told. */
7968
+ leave(timeoutMs = 5e3) {
7969
+ const ws = this.ws;
7970
+ if (!ws || this.stateNow !== "online" || ws.readyState !== import_websocket.default.OPEN) return Promise.resolve(false);
7971
+ return new Promise((resolve) => {
7972
+ const timer = setTimeout(() => resolve(false), timeoutMs);
7973
+ ws.once("close", (code2) => {
7974
+ clearTimeout(timer);
7975
+ resolve(code2 === 4005);
7976
+ });
7977
+ ws.send(encodeControl({ type: "leave" }));
7978
+ });
7979
+ }
7980
+ clearRetry() {
7981
+ if (this.retryTimer) clearTimeout(this.retryTimer);
7982
+ this.retryTimer = null;
7983
+ }
7984
+ /** The wall clock jumped: the machine slept. Whatever was waiting, go now. */
7985
+ tick() {
7986
+ const t2 = this.o.now();
7987
+ const late = t2 - this.lastTick - this.o.wakeCheckMs;
7988
+ this.lastTick = t2;
7989
+ if (late < this.o.wakeJumpMs || !this.running || this.paused) return;
7990
+ if (this.stateNow === "revoked" || this.stateNow === "left") return;
7991
+ this.log(`woke after ${Math.round(late / 1e3)} s; reconnecting now`);
7992
+ this.failures = 0;
7993
+ if (this.ws) {
7994
+ const ws = this.ws;
7995
+ this.ws = null;
7996
+ ws.terminate();
7997
+ }
7998
+ this.clearRetry();
7999
+ this.connect();
8000
+ }
8001
+ retry() {
8002
+ if (!this.running || this.paused) return;
8003
+ const t2 = this.o.now();
8004
+ let delay;
8005
+ if (this.nextDelay !== null) {
8006
+ delay = this.nextDelay;
8007
+ this.nextDelay = null;
8008
+ } else {
8009
+ this.failures += 1;
8010
+ this.failingSince ??= t2;
8011
+ delay = t2 - this.failingSince >= this.o.slowAfterMs ? this.o.slowRetryMs : backoffMs(this.failures, this.o.minBackoffMs, this.o.maxBackoffMs, this.o.random);
8012
+ }
8013
+ this.clearRetry();
8014
+ this.retryTimer = setTimeout(() => {
8015
+ this.retryTimer = null;
8016
+ this.connect();
8017
+ }, delay);
8018
+ this.retryTimer.unref();
8019
+ }
8020
+ connect() {
8021
+ if (!this.running || this.paused || this.ws) return;
8022
+ this.set("connecting");
8023
+ const url = `${this.o.box.replace(/^http/, "ws")}${LINK_PATH}`;
8024
+ const ws = new import_websocket.default(url, { headers: { authorization: `Bearer ${this.o.token}` }, perMessageDeflate: false, handshakeTimeout: 2e4, followRedirects: false });
8025
+ this.ws = ws;
8026
+ let welcomed = false;
8027
+ let stopKeepAlive = () => {
8028
+ };
8029
+ const streams = new StreamTable((f) => {
8030
+ if (ws.readyState === import_websocket.default.OPEN) ws.send(f);
8031
+ }, STREAM_WINDOW);
8032
+ ws.on("unexpected-response", (_req, res) => {
8033
+ this.log(`the box answered the link with HTTP ${res.statusCode}; retrying`);
8034
+ res.resume();
8035
+ ws.terminate();
8036
+ });
8037
+ ws.on("error", (err) => this.log(`link error: ${err.message}`));
8038
+ ws.on("open", () => {
8039
+ stopKeepAlive = keepAlive(
8040
+ ws,
8041
+ () => {
8042
+ this.log("the box stopped answering pings; reconnecting");
8043
+ ws.terminate();
8044
+ },
8045
+ this.o.pingMs
8046
+ );
8047
+ this.nonce = randomBytes5(32).toString("base64url");
8048
+ void this.o.describe().then(
8049
+ (d) => {
8050
+ if (ws.readyState !== import_websocket.default.OPEN) return;
8051
+ const hello = { type: "hello", protocol: PROTOCOL_VERSION, version: this.o.version, nonce: this.nonce, ...d };
8052
+ ws.send(encodeControl(hello));
8053
+ },
8054
+ (err) => {
8055
+ this.log(`could not describe this machine: ${err.message}`);
8056
+ ws.terminate();
8057
+ }
8058
+ );
8059
+ });
8060
+ ws.on("message", (data, isBinary) => {
8061
+ if (!isBinary) return;
8062
+ let frame;
8063
+ try {
8064
+ frame = decodeFrame(data);
8065
+ } catch {
8066
+ this.log("the box sent a malformed frame; reconnecting");
8067
+ ws.terminate();
8068
+ return;
8069
+ }
8070
+ if (frame.type === FrameType.control) {
8071
+ let m;
8072
+ try {
8073
+ m = decodeControl(frame.payload);
8074
+ } catch {
8075
+ return;
8076
+ }
8077
+ if (m.type === "ok") {
8078
+ welcomed = true;
8079
+ this.failures = 0;
8080
+ this.failingSince = null;
8081
+ this.set("online");
8082
+ this.emit("welcome", { machineId: m.machineId, name: m.name });
8083
+ } else if (m.type === "revoked") {
8084
+ if (verifyRevoke(this.o.token, this.o.machineId, this.nonce, m.mac)) {
8085
+ this.log("the box revoked this machine");
8086
+ this.emit("revoked");
8087
+ this.stop("revoked");
8088
+ } else {
8089
+ this.log("ignored a revoked message that was not signed for this machine and link");
8090
+ ws.terminate();
8091
+ }
8092
+ } else if (m.type === "duplicate") {
8093
+ this.log("the box refused this link: this machine is already linked from elsewhere; waiting 5 minutes");
8094
+ this.set("duplicate");
8095
+ this.nextDelay = this.o.duplicateBackoffMs;
8096
+ } else if (m.type === "upgrade") {
8097
+ this.log(`the box needs a newer node (protocol ${String(m.minimum)}); waiting an hour`);
8098
+ this.set("upgrade");
8099
+ this.nextDelay = this.o.slowRetryMs;
8100
+ this.emit("upgrade", { minimum: m.minimum });
8101
+ } else if (welcomed) {
8102
+ this.emit("control", m);
8103
+ }
8104
+ return;
8105
+ }
8106
+ if (frame.type === FrameType.open) {
8107
+ if (!welcomed || this.listenerCount("stream") === 0 || streams.has(frame.stream)) {
8108
+ ws.send(encodeFrame(FrameType.close, frame.stream));
8109
+ return;
8110
+ }
8111
+ this.emit("stream", streams.open(frame.stream), decodeOpen(frame.payload));
8112
+ return;
8113
+ }
8114
+ if (!streams.receive(frame) && frame.type !== FrameType.close) ws.send(encodeFrame(FrameType.close, frame.stream));
8115
+ });
8116
+ ws.on("close", (code2) => {
8117
+ stopKeepAlive();
8118
+ streams.endAll("the link closed");
8119
+ if (this.ws !== ws) return;
8120
+ this.ws = null;
8121
+ if (!this.running || this.stateNow === "revoked") return;
8122
+ if (code2 === 4005) {
8123
+ this.stop("left");
8124
+ return;
8125
+ }
8126
+ if (welcomed) {
8127
+ this.failures = 0;
8128
+ this.failingSince = null;
8129
+ }
8130
+ if (this.stateNow !== "duplicate" && this.stateNow !== "upgrade") this.set("offline");
8131
+ this.retry();
8132
+ });
8133
+ }
8134
+ };
8135
+
8136
+ // src/node/presence.ts
8137
+ import { createHmac as createHmac2, pbkdf2, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
8138
+ var PRESENCE_ITERATIONS = 6e5;
8139
+ var MIN_PASSPHRASE_LENGTH = 12;
8140
+ function derive(passphrase2, salt, iterations = PRESENCE_ITERATIONS) {
8141
+ return new Promise((resolve, reject) => pbkdf2(passphrase2, salt, iterations, 32, "sha256", (err, key) => err ? reject(err) : resolve(key)));
8142
+ }
8143
+ async function enrol(passphrase2, now, iterations = PRESENCE_ITERATIONS) {
8144
+ const salt = randomBytes6(16);
8145
+ const key = await derive(passphrase2, salt, iterations);
8146
+ return { algorithm: "PBKDF2-SHA256", iterations, salt: salt.toString("base64url"), key: key.toString("base64url"), createdAt: now };
8147
+ }
8148
+ async function askPassphrase(prompt, err) {
8149
+ err("The machines passphrase (the one set on the box's host with `agentbox machines enable`).\n");
8150
+ err("This machine keeps only a key derived from it; the box never sees it from here.\n");
8151
+ const pass = await prompt.secret("Machines passphrase: ");
8152
+ if (pass.length < MIN_PASSPHRASE_LENGTH) throw new CliError(`the machines passphrase has at least ${MIN_PASSPHRASE_LENGTH} characters`);
8153
+ const again = await prompt.secret("Again: ");
8154
+ if (again !== pass) throw new CliError("the passphrases did not match");
8155
+ return pass;
8156
+ }
8157
+ var CHALLENGE_MS = 6e4;
8158
+ var GRANT_IDLE_MS = 30 * 6e4;
8159
+ var LOCK_FAILURES = 5;
8160
+ var LOCK_WINDOW_MS = 10 * 6e4;
8161
+ var LOCK_MS = 15 * 6e4;
8162
+ var MAX_CHALLENGES = 64;
8163
+ var MAX_GRANTS = 32;
8164
+ function presenceMessage(challenge, sessionId, machineId) {
8165
+ return Buffer.concat([challenge, Buffer.from(sessionId, "utf8"), Buffer.from(machineId, "utf8")]);
8166
+ }
8167
+ function presenceAnswer(key, challenge, sessionId, machineId) {
8168
+ return createHmac2("sha256", key).update(presenceMessage(challenge, sessionId, machineId)).digest("base64url");
8169
+ }
8170
+ var PresenceGate = class {
8171
+ constructor(o) {
8172
+ this.o = o;
8173
+ this.key = Buffer.from(o.presence.key, "base64url");
8174
+ this.now = o.now ?? Date.now;
8175
+ }
8176
+ o;
8177
+ key;
8178
+ challenges = /* @__PURE__ */ new Map();
8179
+ grants = /* @__PURE__ */ new Map();
8180
+ failures = [];
8181
+ lockedUntil = 0;
8182
+ now;
8183
+ /** Locked until this time, or `null`. */
8184
+ locked() {
8185
+ return this.lockedUntil > this.now() ? this.lockedUntil : null;
8186
+ }
8187
+ challenge(session) {
8188
+ if (session === null) return null;
8189
+ const until = this.locked();
8190
+ if (until !== null) return { locked: true, until };
8191
+ const t2 = this.now();
8192
+ for (const [c, v] of this.challenges) if (v.expiresAt <= t2) this.challenges.delete(c);
8193
+ while (this.challenges.size >= MAX_CHALLENGES) this.challenges.delete(this.challenges.keys().next().value);
8194
+ const challenge = randomBytes6(32).toString("base64url");
8195
+ const expiresAt = t2 + CHALLENGE_MS;
8196
+ this.challenges.set(challenge, { session, expiresAt });
8197
+ return { challenge, salt: this.o.presence.salt, iterations: this.o.presence.iterations, expiresAt };
8198
+ }
8199
+ answer(session, challenge, answer) {
8200
+ if (session === null) return { ok: false, error: "no_session" };
8201
+ const until = this.locked();
8202
+ if (until !== null) return { ok: false, error: "locked", until };
8203
+ const t2 = this.now();
8204
+ const c = this.challenges.get(challenge);
8205
+ this.challenges.delete(challenge);
8206
+ if (!c || c.expiresAt <= t2 || c.session !== session) return { ok: false, error: "expired" };
8207
+ const want = Buffer.from(presenceAnswer(this.key, Buffer.from(challenge, "base64url"), session, this.o.machineId), "base64url");
8208
+ const got = Buffer.from(answer, "base64url");
8209
+ if (got.length !== want.length || !timingSafeEqual3(got, want)) {
8210
+ this.failures = this.failures.filter((f) => f > t2 - LOCK_WINDOW_MS);
8211
+ this.failures.push(t2);
8212
+ if (this.failures.length >= LOCK_FAILURES) {
8213
+ this.lockedUntil = t2 + LOCK_MS;
8214
+ this.failures = [];
8215
+ this.challenges.clear();
8216
+ this.o.log?.(`presence: ${LOCK_FAILURES} wrong machines passphrases in ${LOCK_WINDOW_MS / 6e4} minutes; locked for ${LOCK_MS / 6e4} minutes`);
8217
+ return { ok: false, error: "locked", until: this.lockedUntil };
8218
+ }
8219
+ return { ok: false, error: "wrong", failures: this.failures.length };
8220
+ }
8221
+ this.failures = [];
8222
+ while (this.grants.size >= MAX_GRANTS) this.grants.delete(this.grants.keys().next().value);
8223
+ const grant = `pg_${randomBytes6(32).toString("base64url")}`;
8224
+ const g = { session, issuedAt: t2, lastActive: t2, open: 0 };
8225
+ this.grants.set(grant, g);
8226
+ return { ok: true, grant, expiresAt: this.expiry(g) };
8227
+ }
8228
+ expiry(g) {
8229
+ return g.issuedAt + this.o.hours() * 60 * 6e4;
8230
+ }
8231
+ /** Whether `grant` is live for `session` now. */
8232
+ check(grant, session) {
8233
+ if (grant === null || session === null) return false;
8234
+ const g = this.grants.get(grant);
8235
+ if (!g || g.session !== session) return false;
8236
+ const t2 = this.now();
8237
+ if (t2 >= this.expiry(g) || g.open === 0 && t2 - g.lastActive >= GRANT_IDLE_MS) {
8238
+ this.grants.delete(grant);
8239
+ return false;
8240
+ }
8241
+ return true;
8242
+ }
8243
+ /** A channel opened on `grant`: it does not idle out while open. Returns its release. */
8244
+ hold(grant) {
8245
+ const g = this.grants.get(grant);
8246
+ if (!g) return () => {
8247
+ };
8248
+ g.open += 1;
8249
+ g.lastActive = this.now();
8250
+ let done = false;
8251
+ return () => {
8252
+ if (done) return;
8253
+ done = true;
8254
+ g.open -= 1;
8255
+ g.lastActive = this.now();
8256
+ };
8257
+ }
8258
+ };
8259
+
8260
+ // src/node/prompt.ts
8261
+ var Prompter = class {
8262
+ constructor(stdin, out) {
8263
+ this.stdin = stdin;
8264
+ this.out = out;
8265
+ }
8266
+ stdin;
8267
+ out;
8268
+ buffer = "";
8269
+ ended = false;
8270
+ waiting = null;
8271
+ listening = false;
8272
+ listen() {
8273
+ if (this.listening) return;
8274
+ this.listening = true;
8275
+ this.stdin.on("data", (c) => {
8276
+ this.buffer += c.toString();
8277
+ this.waiting?.();
8278
+ });
8279
+ this.stdin.on("end", () => {
8280
+ this.ended = true;
8281
+ this.waiting?.();
8282
+ });
8283
+ this.stdin.resume();
8284
+ }
8285
+ async nextLine() {
8286
+ this.listen();
8287
+ for (; ; ) {
8288
+ const i = this.buffer.search(/\r?\n|\r/);
8289
+ if (i >= 0) {
8290
+ const line = this.buffer.slice(0, i);
8291
+ const nl = /^\r?\n|^\r/.exec(this.buffer.slice(i))?.[0].length ?? 1;
8292
+ this.buffer = this.buffer.slice(i + nl);
8293
+ return line;
8294
+ }
8295
+ if (this.ended) {
8296
+ if (this.buffer === "") throw new CliError("no answer on stdin", EXIT.USAGE);
8297
+ const line = this.buffer;
8298
+ this.buffer = "";
8299
+ return line;
8300
+ }
8301
+ await new Promise((r) => this.waiting = r);
8302
+ this.waiting = null;
8303
+ }
8304
+ }
8305
+ async line(question) {
8306
+ this.out.write(question);
8307
+ return (await this.nextLine()).trim();
8308
+ }
8309
+ /** A secret: on a terminal, read in raw mode and never echoed. */
8310
+ async secret(question) {
8311
+ this.out.write(question);
8312
+ if (!this.stdin.isTTY || !this.stdin.setRawMode) return this.nextLine();
8313
+ this.listen();
8314
+ this.stdin.setRawMode(true);
8315
+ try {
8316
+ let value = "";
8317
+ for (; ; ) {
8318
+ while (this.buffer.length > 0) {
8319
+ const ch = this.buffer[0];
8320
+ this.buffer = this.buffer.slice(1);
8321
+ if (ch === "\r" || ch === "\n") {
8322
+ this.out.write("\n");
8323
+ return value;
8324
+ }
8325
+ if (ch === "") throw new CliError("interrupted", EXIT.INTERRUPTED);
8326
+ if (ch === "\x7F" || ch === "\b") value = value.slice(0, -1);
8327
+ else if (ch >= " ") value += ch;
8328
+ }
8329
+ if (this.ended) throw new CliError("no answer", EXIT.USAGE);
8330
+ await new Promise((r) => this.waiting = r);
8331
+ this.waiting = null;
8332
+ }
8333
+ } finally {
8334
+ this.stdin.setRawMode(false);
8335
+ }
8336
+ }
8337
+ close() {
8338
+ this.stdin.pause();
8339
+ }
8340
+ };
8341
+
8342
+ // src/node/run.ts
8343
+ import fs19 from "node:fs";
8344
+ import path17 from "node:path";
8345
+
8346
+ // ../hostcore/src/herdr/socket.ts
8347
+ import net4 from "node:net";
8348
+ import readline from "node:readline";
8349
+ var HerdrError = class extends Error {
8350
+ code;
8351
+ constructor(code2, message) {
8352
+ super(message);
8353
+ this.code = code2;
8354
+ this.name = "HerdrError";
8355
+ }
8356
+ };
8357
+ var seq = 0;
8358
+ function request(socketPath, method, params = {}, opts = {}) {
8359
+ return new Promise((resolve, reject) => {
8360
+ const id = `wb_${++seq}`;
8361
+ const sock = net4.connect(socketPath);
8362
+ const rl = readline.createInterface({ input: sock });
8363
+ rl.on("error", () => {
8364
+ });
8365
+ const timer = setTimeout(() => {
8366
+ rl.close();
8367
+ sock.destroy();
8368
+ reject(new HerdrError("timeout", `${method} timed out`));
8369
+ }, opts.timeoutMs ?? 1e4);
8370
+ sock.once("error", (err) => {
8371
+ clearTimeout(timer);
8372
+ rl.close();
8373
+ reject(err);
8374
+ });
8375
+ sock.once("connect", () => {
8376
+ sock.write(JSON.stringify({ id, method, params }) + "\n");
8377
+ });
8378
+ rl.once("line", (line) => {
8379
+ clearTimeout(timer);
8380
+ rl.close();
8381
+ sock.end();
8382
+ let msg;
8383
+ try {
8384
+ msg = JSON.parse(line);
8385
+ } catch {
8386
+ reject(new HerdrError("bad_json", line.slice(0, 200)));
8387
+ return;
8388
+ }
8389
+ if (msg.error) {
8390
+ reject(new HerdrError(msg.error.code ?? "error", msg.error.message ?? "herdr error"));
8391
+ return;
8392
+ }
8393
+ resolve(msg.result);
8394
+ });
8395
+ });
8396
+ }
8397
+ function subscribe(socketPath, subscriptions, onEvent, onClose) {
8398
+ return new Promise((resolve, reject) => {
8399
+ const sock = net4.connect(socketPath);
8400
+ let started = false;
8401
+ let closing = false;
8402
+ const rl = readline.createInterface({ input: sock });
8403
+ rl.on("error", () => {
8404
+ });
8405
+ sock.once("connect", () => {
8406
+ sock.write(JSON.stringify({ id: "sub", method: "events.subscribe", params: { subscriptions } }) + "\n");
8407
+ });
8408
+ sock.on("error", (err) => {
8409
+ if (!started) {
8410
+ reject(err);
8411
+ } else if (!closing) {
8412
+ onClose(err);
8413
+ }
8414
+ });
8415
+ sock.on("close", () => {
8416
+ if (started && !closing) onClose();
8417
+ });
8418
+ rl.on("line", (line) => {
8419
+ let msg;
8420
+ try {
8421
+ msg = JSON.parse(line);
8422
+ } catch {
8423
+ return;
8424
+ }
8425
+ if (!started) {
8426
+ if (msg.error) {
8427
+ sock.destroy();
8428
+ reject(new HerdrError(msg.error.code ?? "error", msg.error.message ?? "herdr error"));
8429
+ return;
8430
+ }
8431
+ started = true;
8432
+ resolve({
8433
+ close: () => {
8434
+ closing = true;
8435
+ sock.destroy();
8436
+ }
8437
+ });
8438
+ return;
8439
+ }
8440
+ if (msg.event) onEvent(msg);
8441
+ });
8442
+ });
8443
+ }
8444
+
8445
+ // ../hostcore/src/herdr/session.ts
8446
+ var LIFECYCLE_TYPES = [
8447
+ "workspace.created",
8448
+ "workspace.updated",
8449
+ "workspace.metadata_updated",
8450
+ "workspace.renamed",
8451
+ "workspace.moved",
8452
+ "workspace.reordered",
8453
+ "workspace.closed",
8454
+ "workspace.focused",
8455
+ "worktree.created",
8456
+ "worktree.opened",
8457
+ "worktree.removed",
8458
+ "tab.created",
8459
+ "tab.closed",
8460
+ "tab.focused",
8461
+ "tab.renamed",
8462
+ "tab.moved",
8463
+ "pane.created",
8464
+ "pane.closed",
8465
+ "pane.updated",
8466
+ "pane.focused",
8467
+ "pane.moved",
8468
+ "pane.exited",
8469
+ "pane.agent_detected",
8470
+ "layout.updated"
8471
+ ];
8472
+ var SessionHub = class {
8473
+ socketPath;
8474
+ lifecycleSub = null;
8475
+ agentSub = null;
8476
+ panes = /* @__PURE__ */ new Set();
8477
+ listeners = /* @__PURE__ */ new Set();
8478
+ _connected = false;
8479
+ _version = null;
8480
+ _protocol = null;
8481
+ stopped = false;
8482
+ reconnecting = false;
8483
+ reconnectDelay = 250;
8484
+ reconnectTimer = null;
8485
+ agentDebounce = null;
8486
+ constructor(socketPath) {
8487
+ this.socketPath = socketPath;
8488
+ }
8489
+ get connected() {
8490
+ return this._connected;
8491
+ }
8492
+ get version() {
8493
+ return this._version;
8494
+ }
8495
+ get protocol() {
8496
+ return this._protocol;
8497
+ }
8498
+ paneIds() {
8499
+ return [...this.panes];
8500
+ }
8501
+ on(listener) {
8502
+ this.listeners.add(listener);
8503
+ return () => {
8504
+ this.listeners.delete(listener);
8505
+ };
8506
+ }
8507
+ emit(m) {
8508
+ for (const listener of this.listeners) listener(m);
8509
+ }
8510
+ async start() {
8511
+ await this.connect();
8512
+ }
8513
+ async snapshot() {
8514
+ const res = await request(this.socketPath, "session.snapshot");
8515
+ return res.snapshot;
8516
+ }
8517
+ stop() {
8518
+ this.stopped = true;
8519
+ if (this.reconnectTimer) {
8520
+ clearTimeout(this.reconnectTimer);
8521
+ this.reconnectTimer = null;
8522
+ }
8523
+ if (this.agentDebounce) {
8524
+ clearTimeout(this.agentDebounce);
8525
+ this.agentDebounce = null;
8526
+ }
8527
+ this.closeSubs();
8528
+ this._connected = false;
8529
+ this.listeners.clear();
8530
+ }
8531
+ closeSubs() {
8532
+ if (this.lifecycleSub) {
8533
+ this.lifecycleSub.close();
8534
+ this.lifecycleSub = null;
8535
+ }
8536
+ if (this.agentSub) {
8537
+ this.agentSub.close();
8538
+ this.agentSub = null;
8539
+ }
8540
+ }
8541
+ /** Open lifecycle subscription, load the snapshot, then open agent-status. */
8542
+ async connect() {
8543
+ if (this.agentDebounce) {
8544
+ clearTimeout(this.agentDebounce);
8545
+ this.agentDebounce = null;
8546
+ }
8547
+ this.closeSubs();
8548
+ this.lifecycleSub = await subscribe(
8549
+ this.socketPath,
8550
+ LIFECYCLE_TYPES.map((type) => ({ type })),
8551
+ (e) => this.onLifecycle(e),
8552
+ () => this.onDrop()
8553
+ );
8554
+ const snap = await this.snapshot();
8555
+ this._version = snap.version;
8556
+ this._protocol = snap.protocol;
8557
+ this.panes.clear();
8558
+ for (const p of snap.panes) this.panes.add(p.pane_id);
8559
+ await this.openAgentSub();
8560
+ this._connected = true;
8561
+ }
8562
+ async openAgentSub() {
8563
+ const subs = this.paneIds().map((pane_id) => ({
8564
+ type: "pane.agent_status_changed",
8565
+ pane_id
8566
+ }));
8567
+ const next = await subscribe(
8568
+ this.socketPath,
8569
+ subs,
8570
+ (e) => this.onAgentEvent(e),
8571
+ () => this.onDrop()
8572
+ );
8573
+ const previous = this.agentSub;
8574
+ this.agentSub = next;
8575
+ if (previous) previous.close();
8576
+ }
8577
+ onLifecycle(e) {
8578
+ this.emit({ kind: "event", event: e.event, data: e.data });
8579
+ let changed = false;
8580
+ if (e.event === "pane_created" || e.event === "pane_moved") {
8581
+ const paneId = e.data.pane?.pane_id;
8582
+ if (paneId && !this.panes.has(paneId)) {
8583
+ this.panes.add(paneId);
8584
+ changed = true;
8585
+ }
8586
+ } else if (e.event === "pane_closed") {
8587
+ const paneId = e.data.pane_id;
8588
+ if (paneId && this.panes.delete(paneId)) changed = true;
8589
+ }
8590
+ if (changed) this.scheduleAgentReopen();
8591
+ }
8592
+ onAgentEvent(e) {
8593
+ this.emit({ kind: "event", event: "pane_agent_status_changed", data: e.data });
8594
+ }
8595
+ scheduleAgentReopen() {
8596
+ if (this.agentDebounce) clearTimeout(this.agentDebounce);
8597
+ this.agentDebounce = setTimeout(() => {
8598
+ this.agentDebounce = null;
8599
+ this.openAgentSub().catch(() => this.onDrop());
8600
+ }, 50);
8601
+ }
8602
+ onDrop() {
8603
+ if (this.stopped || this.reconnecting) return;
8604
+ this.reconnecting = true;
8605
+ this._connected = false;
8606
+ this.scheduleReconnect();
8607
+ }
8608
+ scheduleReconnect() {
8609
+ this.reconnectTimer = setTimeout(() => {
8610
+ this.reconnectTimer = null;
8611
+ if (this.stopped) return;
8612
+ this.connect().then(
8613
+ () => {
8614
+ this.reconnectDelay = 250;
8615
+ this.reconnecting = false;
8616
+ this.emit({ kind: "reset", reason: "herdr reconnected" });
8617
+ },
8618
+ () => {
8619
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 5e3);
8620
+ this.scheduleReconnect();
8621
+ }
8622
+ );
8623
+ }, this.reconnectDelay);
8624
+ }
8625
+ };
8626
+
8627
+ // ../hostcore/src/herdr/terminal.ts
8628
+ import { spawn as spawn5 } from "node:child_process";
8629
+ import readline2 from "node:readline";
8630
+ var MAX_REPLAY_BYTES = 1e6;
8631
+ var RELEASE_GRACE_MS = 1e3;
8632
+ var SIGKILL_GRACE_MS = 2e3;
8633
+ function killWithFallback(child, exited, graceMs = SIGKILL_GRACE_MS) {
8634
+ if (exited()) return null;
8635
+ if (!child.killed) child.kill("SIGTERM");
8636
+ const timer = setTimeout(() => {
8637
+ if (!exited()) child.kill("SIGKILL");
8638
+ }, graceMs);
8639
+ timer.unref?.();
8640
+ return timer;
8641
+ }
8642
+ var PaneStream = class {
8643
+ constructor(paneId, env, cols, rows, onDead) {
8644
+ this.paneId = paneId;
8645
+ this.env = env;
8646
+ this.onDead = onDead;
8647
+ this.cols = cols;
8648
+ this.rows = rows;
8649
+ this.watchScroll();
8650
+ this.child = this.spawn();
8651
+ }
8652
+ paneId;
8653
+ env;
8654
+ onDead;
8655
+ child;
8656
+ viewers = /* @__PURE__ */ new Set();
8657
+ /** Last full frame; the baseline replayed to late joiners. */
8658
+ full = null;
8659
+ /** Diffs received since the last full frame, replayed after it. */
8660
+ diffs = [];
8661
+ diffBytes = 0;
8662
+ cols;
8663
+ rows;
8664
+ /** Last size each viewer asked for, so focus can restore that viewer's size. */
8665
+ viewerSize = /* @__PURE__ */ new WeakMap();
8666
+ lastFullSeq = 0;
8667
+ released = false;
8668
+ dead = false;
8669
+ /** True once the child process has actually exited (not merely signalled). */
8670
+ childExited = false;
8671
+ killTimer = null;
8672
+ /** Where herdr's scrollback stands, from its pane.scroll_changed events. */
8673
+ scrolled = null;
8674
+ scrollSub = null;
8675
+ /**
8676
+ * Follow herdr's scrollback position for this pane and pass it to every
8677
+ * viewer, for the scrollbar and the way back to live. herdr reports it on a
8678
+ * scroll, on output that grows the history, and when typing snaps the pane
8679
+ * back to live. Without the socket the pane still scrolls, just unmarked.
8680
+ */
8681
+ watchScroll() {
8682
+ const socketPath = this.env.HERDR_SOCKET_PATH;
8683
+ if (!socketPath) return;
8684
+ subscribe(
8685
+ socketPath,
8686
+ [{ type: "pane.scroll_changed", pane_id: this.paneId }],
8687
+ (e) => {
8688
+ const info = e.data?.scroll;
8689
+ if (!info) return;
8690
+ const m = { type: "scrolled", offset: info.offset_from_bottom, max: info.max_offset_from_bottom, rows: info.viewport_rows };
8691
+ this.scrolled = m;
8692
+ for (const v of this.viewers) v.sendJson(m);
8693
+ },
8694
+ () => {
8695
+ }
8696
+ ).then(
8697
+ (sub) => {
8698
+ if (this.dead || this.released) sub.close();
8699
+ else this.scrollSub = sub;
8700
+ },
8701
+ () => {
8702
+ }
8703
+ );
8704
+ }
8705
+ unwatchScroll() {
8706
+ this.scrollSub?.close();
8707
+ this.scrollSub = null;
8708
+ }
8709
+ /** True once the child has exited/errored or herdr reported the pane closed. */
8710
+ get isDead() {
8711
+ return this.dead;
8712
+ }
8713
+ spawn() {
8714
+ const child = spawn5(
8715
+ "herdr",
8716
+ [
8717
+ "terminal",
8718
+ "session",
8719
+ "control",
8720
+ this.paneId,
8721
+ "--takeover",
8722
+ "--cols",
8723
+ String(this.cols),
8724
+ "--rows",
8725
+ String(this.rows)
8726
+ ],
8727
+ { env: this.env, stdio: ["pipe", "pipe", "pipe"] }
8728
+ );
8729
+ if (child.stdout) {
8730
+ readline2.createInterface({ input: child.stdout }).on("line", (line) => this.onLine(line));
8731
+ }
8732
+ child.stderr?.resume();
8733
+ child.on("error", () => this.markDead("stream error"));
8734
+ child.on("exit", () => {
8735
+ this.childExited = true;
8736
+ if (this.killTimer) {
8737
+ clearTimeout(this.killTimer);
8738
+ this.killTimer = null;
8739
+ }
8740
+ this.markDead("stream ended");
8741
+ });
8742
+ return child;
8743
+ }
8744
+ /**
8745
+ * Send SIGTERM and, if the child is still alive after a bounded grace, follow
8746
+ * it with SIGKILL. A herdr controller that ignores SIGTERM would otherwise
8747
+ * keep the process — and this stream — from ever tearing down.
8748
+ */
8749
+ killWithFallback() {
8750
+ if (this.childExited || this.killTimer) return;
8751
+ this.killTimer = killWithFallback(this.child, () => this.childExited);
8752
+ }
8753
+ /**
8754
+ * Mark the stream dead exactly once (dropping it from the registry) and close
8755
+ * any remaining viewers. Called on child exit/error and on terminal.closed.
8756
+ */
8757
+ markDead(reason) {
8758
+ this.unwatchScroll();
8759
+ if (!this.dead) {
8760
+ this.dead = true;
8761
+ this.onDead(this.paneId, this);
8762
+ }
8763
+ this.shutdownViewers(reason);
8764
+ }
8765
+ shutdownViewers(reason) {
8766
+ for (const v of this.viewers) v.close(reason);
8767
+ this.viewers.clear();
8768
+ }
8769
+ onLine(line) {
8770
+ let msg;
8771
+ try {
8772
+ msg = JSON.parse(line);
8773
+ } catch {
8774
+ return;
8775
+ }
8776
+ if (msg.type === "terminal.frame") {
8777
+ const buf = Buffer.from(msg.bytes, "base64");
8778
+ if (msg.full) {
8779
+ this.full = buf;
8780
+ this.diffs = [];
8781
+ this.diffBytes = 0;
8782
+ this.lastFullSeq = msg.seq;
8783
+ } else {
8784
+ this.diffs.push(buf);
8785
+ this.diffBytes += buf.length;
8786
+ if (this.diffBytes > MAX_REPLAY_BYTES) {
8787
+ this.full = null;
8788
+ this.diffs = [];
8789
+ this.diffBytes = 0;
8790
+ }
8791
+ }
8792
+ for (const v of this.viewers) v.send(buf);
8793
+ } else if (msg.type === "terminal.closed") {
8794
+ this.markDead(msg.reason ?? "closed");
8795
+ }
8796
+ }
8797
+ write(o) {
8798
+ this.child.stdin?.write(JSON.stringify(o) + "\n");
8799
+ }
8800
+ addViewer(v, cols, rows) {
8801
+ this.viewers.add(v);
8802
+ this.viewerSize.set(v, { cols, rows });
8803
+ if (this.scrolled) v.sendJson(this.scrolled);
8804
+ if (this.full && this.diffBytes < MAX_REPLAY_BYTES && cols === this.cols && rows === this.rows) {
8805
+ v.send(this.full);
8806
+ for (const d of this.diffs) v.send(d);
8807
+ } else {
8808
+ this.resize(cols, rows);
8809
+ }
8810
+ }
8811
+ removeViewer(v) {
8812
+ if (!this.viewers.delete(v)) return;
8813
+ if (this.viewers.size === 0) this.release();
8814
+ }
8815
+ release() {
8816
+ if (this.released) return;
8817
+ this.released = true;
8818
+ this.unwatchScroll();
8819
+ this.write({ type: "terminal.release" });
8820
+ this.child.stdin?.end();
8821
+ setTimeout(() => this.killWithFallback(), RELEASE_GRACE_MS).unref();
8822
+ }
8823
+ /** Kill the child immediately; used on shutdown, not on normal release. */
8824
+ destroy() {
8825
+ this.released = true;
8826
+ this.unwatchScroll();
8827
+ this.child.stdin?.end();
8828
+ this.killWithFallback();
8829
+ }
8830
+ resize(cols, rows) {
8831
+ this.cols = cols;
8832
+ this.rows = rows;
8833
+ this.write({ type: "terminal.resize", cols, rows });
8834
+ for (const v of this.viewers) v.sendJson({ type: "size", cols, rows });
8835
+ }
8836
+ /** Record this viewer's size and make it the current pane size. */
8837
+ resizeFor(v, cols, rows) {
8838
+ this.viewerSize.set(v, { cols, rows });
8839
+ this.resize(cols, rows);
8840
+ }
8841
+ /** The focusing viewer becomes the size owner: restore its last known size. */
8842
+ focus(v) {
8843
+ const size = this.viewerSize.get(v) ?? { cols: this.cols, rows: this.rows };
8844
+ this.resize(size.cols, size.rows);
8845
+ }
8846
+ input(text, bytes) {
8847
+ this.write({ type: "terminal.input", ...bytes ? { bytes } : { text: text ?? "" } });
8848
+ }
8849
+ scroll(direction, lines) {
8850
+ this.write({ type: "terminal.scroll", direction, lines: Math.max(1, lines | 0), source: "wheel" });
8851
+ }
8852
+ /** Put herdr's view of the pane `offset` lines above live; 0 is back to live. */
8853
+ scrollTo(offset) {
8854
+ const socketPath = this.env.HERDR_SOCKET_PATH;
8855
+ if (!socketPath || !Number.isFinite(offset)) return;
8856
+ const offset_from_bottom = Math.max(0, Math.floor(offset));
8857
+ request(socketPath, "pane.scroll", { pane_id: this.paneId, offset_from_bottom }).catch(() => {
8858
+ });
8859
+ }
8860
+ size() {
8861
+ return { cols: this.cols, rows: this.rows };
8862
+ }
8863
+ get viewerCount() {
8864
+ return this.viewers.size;
8865
+ }
8866
+ };
8867
+ var TerminalStreams = class {
8868
+ constructor(env) {
8869
+ this.env = env;
8870
+ }
8871
+ env;
8872
+ streams = /* @__PURE__ */ new Map();
8873
+ /** Drop a stream from the registry, but only if it is still the live one. */
8874
+ forget(paneId, stream) {
8875
+ if (this.streams.get(paneId) === stream) this.streams.delete(paneId);
8876
+ }
8877
+ attach(paneId, viewer, cols = 80, rows = 24) {
8878
+ let stream = this.streams.get(paneId);
8879
+ if (!stream || stream.isDead) {
8880
+ stream = new PaneStream(paneId, this.env, cols, rows, (p, s2) => this.forget(p, s2));
8881
+ this.streams.set(paneId, stream);
8882
+ }
8883
+ stream.addViewer(viewer, cols, rows);
8884
+ const s = stream;
8885
+ let detached = false;
8886
+ return {
8887
+ input: (text, bytes) => s.input(text, bytes),
8888
+ resize: (c, r) => s.resizeFor(viewer, c, r),
8889
+ scroll: (direction, lines) => s.scroll(direction, lines),
8890
+ scrollTo: (offset) => s.scrollTo(offset),
8891
+ focus: () => s.focus(viewer),
8892
+ detach: () => {
8893
+ if (detached) return;
8894
+ detached = true;
8895
+ s.removeViewer(viewer);
8896
+ if (s.viewerCount === 0) this.forget(paneId, s);
8897
+ }
8898
+ };
8899
+ }
8900
+ active(paneId) {
8901
+ return this.streams.has(paneId);
8902
+ }
8903
+ size(paneId) {
8904
+ return this.streams.get(paneId)?.size() ?? null;
8905
+ }
8906
+ lastFullSeq(paneId) {
8907
+ return this.streams.get(paneId)?.lastFullSeq ?? 0;
8908
+ }
8909
+ /** Test-only: number of live pane streams in the registry. */
8910
+ streamCount() {
8911
+ return this.streams.size;
8912
+ }
8913
+ /** Tear down every live stream; call on server shutdown. */
8914
+ stop() {
8915
+ for (const s of this.streams.values()) s.destroy();
8916
+ this.streams.clear();
8917
+ }
8918
+ };
8919
+
8920
+ // ../hostcore/src/ws-heartbeat.ts
8921
+ var HEARTBEAT_MS = 3e4;
8922
+ function heartbeat(socket2, intervalMs = HEARTBEAT_MS) {
8923
+ let alive = true;
8924
+ socket2.on("pong", () => {
8925
+ alive = true;
8926
+ });
8927
+ const timer = setInterval(() => {
8928
+ if (socket2.readyState !== socket2.OPEN) return;
8929
+ if (!alive) {
8930
+ socket2.terminate();
8931
+ return;
8932
+ }
8933
+ alive = false;
8934
+ try {
8935
+ socket2.ping();
8936
+ } catch {
8937
+ }
8938
+ }, intervalMs);
8939
+ timer.unref?.();
8940
+ const stop = () => clearInterval(timer);
8941
+ socket2.on("close", stop);
8942
+ return stop;
8943
+ }
8944
+
8945
+ // ../hostcore/src/herdr/terminal-socket.ts
8946
+ var PANE_RE = /^w[0-9A-Za-z]{1,12}:p[0-9A-Za-z]{1,12}$/;
8947
+ var MAX_BUFFERED_BYTES = 8 * 1024 * 1024;
8948
+ var MAX_CHANNELS = 64;
8949
+ function isBackpressured(bufferedAmount, limit = MAX_BUFFERED_BYTES) {
8950
+ return bufferedAmount > limit;
8951
+ }
8952
+ function clamp(value, min, max, fallback) {
8953
+ return Number.isFinite(value) ? Math.min(max, Math.max(min, Math.trunc(value))) : fallback;
8954
+ }
8955
+ function muxFrame(ch, data) {
8956
+ const out = Buffer.allocUnsafe(2 + data.length);
8957
+ out.writeUInt16BE(ch, 0);
8958
+ data.copy(out, 2);
8959
+ return out;
8960
+ }
8961
+ function sendOrShed(socket2, data) {
8962
+ if (socket2.readyState !== socket2.OPEN) return;
8963
+ if (isBackpressured(socket2.bufferedAmount)) {
8964
+ try {
8965
+ socket2.close(1013, "viewer too slow");
8966
+ } catch {
8967
+ }
8968
+ return;
8969
+ }
8970
+ socket2.send(data, { binary: true });
8971
+ }
8972
+ function sendJson(socket2, m) {
8973
+ if (socket2.readyState === socket2.OPEN) socket2.send(JSON.stringify(m));
8974
+ }
8975
+ function control(a, msg, size) {
8976
+ switch (msg.type) {
8977
+ case "input":
8978
+ a.input(msg.text, msg.bytes);
8979
+ break;
8980
+ case "resize":
8981
+ a.resize(clamp(Number(msg.cols), 20, 500, size.cols), clamp(Number(msg.rows), 5, 200, size.rows));
8982
+ break;
8983
+ case "scroll":
8984
+ if (msg.direction === "up" || msg.direction === "down") a.scroll(msg.direction, Number(msg.lines));
8985
+ break;
8986
+ case "scrollTo":
8987
+ a.scrollTo(Number(msg.offset));
8988
+ break;
8989
+ case "focus":
8990
+ a.focus();
8991
+ break;
8992
+ default:
8993
+ break;
8994
+ }
8995
+ }
8996
+ function parse(raw) {
8997
+ try {
8998
+ return JSON.parse(raw.toString());
8999
+ } catch {
9000
+ return null;
9001
+ }
9002
+ }
9003
+ function single(socket2, streams, pane, cols, rows) {
9004
+ const viewer = {
9005
+ send: (data) => sendOrShed(socket2, data),
9006
+ sendJson: (m) => sendJson(socket2, m),
9007
+ close: (reason) => {
9008
+ try {
9009
+ if (socket2.readyState === socket2.OPEN) {
9010
+ socket2.send(JSON.stringify({ type: "closed", reason }));
9011
+ socket2.close(1e3);
9012
+ }
9013
+ } catch {
9014
+ }
9015
+ }
9016
+ };
9017
+ const attachment = streams.attach(pane, viewer, cols, rows);
9018
+ socket2.on("message", (raw) => {
9019
+ const msg = parse(raw);
9020
+ if (msg) control(attachment, msg, { cols, rows });
9021
+ });
9022
+ socket2.on("close", () => attachment.detach());
9023
+ }
9024
+ function shared(socket2, streams) {
9025
+ const channels = /* @__PURE__ */ new Map();
9026
+ const notice = (ch, m) => sendJson(socket2, { ...m, ch });
9027
+ const attach2 = (ch, pane, c, r) => {
9028
+ if (channels.has(ch)) return;
9029
+ if (typeof pane !== "string" || !PANE_RE.test(pane)) return notice(ch, { type: "closed", reason: "invalid pane" });
9030
+ if (channels.size >= MAX_CHANNELS) return notice(ch, { type: "closed", reason: "too many panes on one socket" });
9031
+ const size = { cols: clamp(Number(c), 20, 500, 80), rows: clamp(Number(r), 5, 200, 24) };
9032
+ let live = true;
9033
+ const viewer = {
9034
+ send: (data) => {
9035
+ if (live) sendOrShed(socket2, muxFrame(ch, data));
9036
+ },
9037
+ sendJson: (m) => {
9038
+ if (live) notice(ch, m);
9039
+ },
9040
+ close: (reason) => {
9041
+ if (!live) return;
9042
+ live = false;
9043
+ channels.delete(ch);
9044
+ notice(ch, { type: "closed", reason });
9045
+ }
9046
+ };
9047
+ const attachment = streams.attach(pane, viewer, size.cols, size.rows);
9048
+ if (live) channels.set(ch, { attachment, size });
9049
+ };
9050
+ socket2.on("message", (raw) => {
9051
+ const msg = parse(raw);
9052
+ if (!msg) return;
9053
+ const ch = Number(msg.ch);
9054
+ if (!Number.isInteger(ch) || ch < 0 || ch > 65535) return;
9055
+ if (msg.type === "ping") return sendJson(socket2, { type: "pong", ch, t: Number(msg.t) || 0 });
9056
+ if (msg.type === "attach") return attach2(ch, msg.pane, msg.cols, msg.rows);
9057
+ const c = channels.get(ch);
9058
+ if (!c) return;
9059
+ if (msg.type === "detach") {
9060
+ channels.delete(ch);
9061
+ c.attachment.detach();
9062
+ return;
9063
+ }
9064
+ if (msg.type === "resize") c.size = { cols: clamp(Number(msg.cols), 20, 500, c.size.cols), rows: clamp(Number(msg.rows), 5, 200, c.size.rows) };
9065
+ control(c.attachment, msg, c.size);
9066
+ });
9067
+ socket2.on("close", () => {
9068
+ for (const c of channels.values()) c.attachment.detach();
9069
+ channels.clear();
9070
+ });
9071
+ }
9072
+ function serveTerminalSocket(socket2, streams, query) {
9073
+ const pane = query.pane;
9074
+ if (pane === void 0) {
9075
+ heartbeat(socket2);
9076
+ shared(socket2, streams);
9077
+ return;
9078
+ }
9079
+ if (typeof pane !== "string" || !PANE_RE.test(pane)) {
9080
+ socket2.close(1008, "invalid pane");
9081
+ return;
9082
+ }
9083
+ heartbeat(socket2);
9084
+ single(socket2, streams, pane, clamp(Number(query.cols), 20, 500, 80), clamp(Number(query.rows), 5, 200, 24));
9085
+ }
9086
+
9087
+ // ../hostcore/src/rpc-allowlist.ts
9088
+ var EXACT = /* @__PURE__ */ new Set(["ping", "session.snapshot", "notification.show"]);
9089
+ var PREFIXES = ["workspace.", "worktree.", "tab.", "pane.", "agent.", "layout."];
9090
+ function isGraphics(method) {
9091
+ return method === "pane.graphics" || method.startsWith("pane.graphics.");
9092
+ }
9093
+ function isAllowed(method) {
9094
+ if (EXACT.has(method)) return true;
9095
+ if (isGraphics(method)) return false;
9096
+ return PREFIXES.some(
9097
+ (p) => method.startsWith(p) && method.length > p.length && /^[a-z_.]+$/.test(method)
9098
+ );
9099
+ }
9100
+
9101
+ // src/node/herdr.ts
9102
+ import { spawn as spawn6 } from "node:child_process";
9103
+ import { randomBytes as randomBytes7 } from "node:crypto";
9104
+ import { EventEmitter as EventEmitter2 } from "node:events";
9105
+ import fs16 from "node:fs";
9106
+ import os8 from "node:os";
9107
+ import path14 from "node:path";
9108
+ var HERDR_SESSION = "agentbox-node";
9109
+ var CHECK_MS = 3e4;
9110
+ var FAILED_CHECKS = 2;
9111
+ var READY_MS = 15e3;
9112
+ var MAX_SOCKET_PATH = 96;
9113
+ var HerdrSupervisor = class extends EventEmitter2 {
9114
+ constructor(o) {
9115
+ super();
9116
+ this.o = o;
9117
+ this.stateFile = path14.join(o.dir, "state", "herdr.json");
9118
+ }
9119
+ o;
9120
+ child = null;
9121
+ socket = null;
9122
+ pid = null;
9123
+ stopped = false;
9124
+ failures = 0;
9125
+ checker = null;
9126
+ restarting = null;
9127
+ backoff = 1e3;
9128
+ stateFile;
9129
+ /** The control socket now, or `null` while herdr is starting. */
9130
+ get socketPath() {
9131
+ return this.socket;
9132
+ }
9133
+ log(line) {
9134
+ this.emit("log", line);
9135
+ }
9136
+ async answers(socket2) {
9137
+ try {
9138
+ const pong = await request(socket2, "ping", {}, { timeoutMs: 5e3 });
9139
+ return pong.version === (this.o.version ?? HERDR_VERSION);
9140
+ } catch {
9141
+ return false;
9142
+ }
9143
+ }
9144
+ /** A fresh socket path: in the node folder when it fits, else a private folder of its own. */
9145
+ freshSocket() {
9146
+ const name = `herdr-${randomBytes7(4).toString("hex")}.sock`;
9147
+ const inNode = path14.join(this.o.dir, name);
9148
+ if (Buffer.byteLength(inNode) <= MAX_SOCKET_PATH) return inNode;
9149
+ const dir = fs16.mkdtempSync(path14.join(os8.tmpdir(), "agentbox-node-"));
9150
+ fs16.chmodSync(dir, 448);
9151
+ return path14.join(dir, name);
9152
+ }
9153
+ async start() {
9154
+ this.stopped = false;
9155
+ const saved = this.readSaved();
9156
+ if (saved && await this.answers(saved.socket)) {
9157
+ this.socket = saved.socket;
9158
+ this.pid = saved.pid;
9159
+ this.log(`herdr: kept the running server (pid ${saved.pid})`);
9160
+ this.emit("ready", saved.socket, { restored: false });
9161
+ } else {
9162
+ if (saved) this.kill(saved.pid);
9163
+ await this.spawnServer();
9164
+ }
9165
+ this.watch();
9166
+ }
9167
+ readSaved() {
9168
+ try {
9169
+ const v = JSON.parse(fs16.readFileSync(this.stateFile, "utf8"));
9170
+ return typeof v.socket === "string" && typeof v.pid === "number" ? { socket: v.socket, pid: v.pid } : null;
9171
+ } catch {
9172
+ return null;
9173
+ }
9174
+ }
9175
+ /** Only a herdr of ours: on Linux its command line says so; elsewhere, the pid we recorded. */
9176
+ kill(pid) {
9177
+ try {
9178
+ if (process.platform === "linux") {
9179
+ const cmd = fs16.readFileSync(`/proc/${pid}/cmdline`, "utf8");
9180
+ if (!cmd.includes("herdr")) return;
9181
+ }
9182
+ process.kill(pid, "SIGTERM");
9183
+ } catch {
9184
+ }
9185
+ }
9186
+ removeSocket(socket2) {
9187
+ if (!socket2) return;
9188
+ for (const f of [socket2, socket2.replace(/\.sock$/, "-client.sock")]) fs16.rmSync(f, { force: true });
9189
+ }
9190
+ async spawnServer() {
9191
+ const old = this.socket;
9192
+ this.socket = null;
9193
+ this.removeSocket(old);
9194
+ const socket2 = this.freshSocket();
9195
+ ensurePrivateDir(path14.dirname(this.stateFile));
9196
+ const child = spawn6(this.o.bin, ["server"], {
9197
+ env: { ...this.o.env, HERDR_SOCKET_PATH: socket2, HERDR_SESSION },
9198
+ stdio: ["ignore", "ignore", "pipe"],
9199
+ // Its own session: herdr counts only such a server as a daemon, and it
9200
+ // outlives the node.
9201
+ detached: true,
9202
+ windowsHide: true
9203
+ });
9204
+ this.child = child;
9205
+ child.stderr?.on("data", (c) => this.log(`herdr: ${c.toString("utf8").trim().slice(0, 500)}`));
9206
+ child.once("error", (err) => this.log(`herdr: could not start: ${err.message}`));
9207
+ child.once("exit", (code2, signal) => {
9208
+ if (this.child === child) this.child = null;
9209
+ if (this.stopped || this.socket !== socket2) return;
9210
+ this.log(`herdr exited (code ${code2 ?? "none"}, signal ${signal ?? "none"}); restarting`);
9211
+ this.socket = null;
9212
+ this.emit("down");
9213
+ void this.restart("it exited");
9214
+ });
9215
+ child.unref();
9216
+ const end = Date.now() + READY_MS;
9217
+ while (!await this.answers(socket2)) {
9218
+ if (this.stopped || this.child !== child) return;
9219
+ if (Date.now() > end) {
9220
+ child.kill("SIGTERM");
9221
+ throw new Error(`herdr did not answer on ${socket2} within ${READY_MS / 1e3} s`);
9222
+ }
9223
+ await new Promise((r) => setTimeout(r, 200));
9224
+ }
9225
+ this.socket = socket2;
9226
+ this.pid = child.pid ?? null;
9227
+ if (this.pid !== null) writePrivate2(this.stateFile, `${JSON.stringify({ socket: socket2, pid: this.pid })}
9228
+ `);
9229
+ this.failures = 0;
9230
+ this.backoff = 1e3;
9231
+ this.log(`herdr ${this.o.version ?? HERDR_VERSION} started (pid ${this.pid ?? "?"})`);
9232
+ this.emit("ready", socket2, { restored: true });
9233
+ }
9234
+ watch() {
9235
+ if (this.checker) clearInterval(this.checker);
9236
+ this.checker = setInterval(() => void this.check(), this.o.checkMs ?? CHECK_MS);
9237
+ this.checker.unref();
9238
+ }
9239
+ /** One health check: two failures in a row and herdr is restarted. */
9240
+ async check() {
9241
+ if (this.stopped || this.restarting || !this.socket) return;
9242
+ if (await this.answers(this.socket)) {
9243
+ this.failures = 0;
9244
+ return;
9245
+ }
9246
+ this.failures += 1;
9247
+ this.log(`herdr: its control socket did not answer (${this.failures} of ${FAILED_CHECKS})`);
9248
+ if (this.failures >= FAILED_CHECKS) await this.restart("its control socket stopped answering");
9249
+ }
9250
+ /** Stop the server and start a fresh one (a hang, an exit, an update while idle). */
9251
+ restart(reason) {
9252
+ if (this.restarting) return this.restarting;
9253
+ this.restarting = (async () => {
9254
+ this.log(`herdr: restarting (${reason})`);
9255
+ this.failures = 0;
9256
+ const child = this.child;
9257
+ this.child = null;
9258
+ if (child) child.kill("SIGTERM");
9259
+ else if (this.pid !== null) this.kill(this.pid);
9260
+ this.socket = null;
9261
+ this.emit("down");
9262
+ for (; ; ) {
9263
+ if (this.stopped) return;
9264
+ try {
9265
+ await this.spawnServer();
9266
+ return;
9267
+ } catch (err) {
9268
+ this.log(`herdr: ${err.message}; trying again in ${this.backoff / 1e3} s`);
9269
+ await new Promise((r) => setTimeout(r, this.backoff));
9270
+ this.backoff = Math.min(this.backoff * 2, 3e4);
9271
+ }
9272
+ }
9273
+ })().finally(() => {
9274
+ this.restarting = null;
9275
+ });
9276
+ return this.restarting;
9277
+ }
9278
+ /** No agent is running (the updater replaces herdr and Claude Code only then). */
9279
+ async idle() {
9280
+ if (!this.socket) return true;
9281
+ try {
9282
+ const r = await request(this.socket, "session.snapshot", {}, { timeoutMs: 5e3 });
9283
+ return (r.snapshot.agents ?? []).length === 0;
9284
+ } catch {
9285
+ return false;
9286
+ }
9287
+ }
9288
+ /** Stop supervising. herdr keeps running (its agents with it) unless `kill`. */
9289
+ stop(kill = false) {
9290
+ this.stopped = true;
9291
+ if (this.checker) clearInterval(this.checker);
9292
+ this.checker = null;
9293
+ if (kill) {
9294
+ if (this.child) this.child.kill("SIGTERM");
9295
+ else if (this.pid !== null) this.kill(this.pid);
9296
+ }
9297
+ }
9298
+ };
9299
+
9300
+ // src/node/server.ts
9301
+ import http from "node:http";
9302
+ var MAX_BODY = 64 * 1024;
9303
+ function json(res, status3, body) {
9304
+ const text = JSON.stringify(body);
9305
+ res.writeHead(status3, { "content-type": "application/json", "content-length": Buffer.byteLength(text), "cache-control": "no-store" });
9306
+ res.end(text);
9307
+ }
9308
+ function readJson(req) {
9309
+ return new Promise((resolve) => {
9310
+ const chunks = [];
9311
+ let size = 0;
9312
+ req.on("data", (c) => {
9313
+ size += c.length;
9314
+ if (size > MAX_BODY) {
9315
+ resolve(null);
9316
+ req.destroy();
9317
+ return;
9318
+ }
9319
+ chunks.push(c);
9320
+ });
9321
+ req.on("end", () => {
9322
+ try {
9323
+ const v = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
9324
+ resolve(typeof v === "object" && v !== null && !Array.isArray(v) ? v : null);
9325
+ } catch {
9326
+ resolve(null);
9327
+ }
9328
+ });
9329
+ req.on("error", () => resolve(null));
9330
+ });
9331
+ }
9332
+ function refuseSocket(socket2, status3, body) {
9333
+ const text = JSON.stringify(body);
9334
+ socket2.end(
9335
+ `HTTP/1.1 ${status3} ${http.STATUS_CODES[status3] ?? ""}\r
9336
+ Connection: close\r
9337
+ Content-Type: application/json\r
9338
+ Content-Length: ${Buffer.byteLength(text)}\r
9339
+ \r
9340
+ ${text}`
9341
+ );
9342
+ }
9343
+ var NodeServer = class {
9344
+ constructor(o) {
9345
+ this.o = o;
9346
+ this.server = http.createServer({ requestTimeout: 0, headersTimeout: 3e4, keepAliveTimeout: 6e4 }, (req, res) => {
9347
+ void this.handle(req, res).catch((err) => {
9348
+ o.log?.(`server: ${err.message}`);
9349
+ if (!res.headersSent) json(res, 500, { error: "internal" });
9350
+ else res.destroy();
9351
+ });
9352
+ });
9353
+ this.server.on("upgrade", (req, socket2, head) => this.upgrade(req, socket2, head));
9354
+ this.server.on("clientError", (_err, socket2) => socket2.destroy());
9355
+ }
9356
+ o;
9357
+ server;
9358
+ wss = new import_websocket_server.default({ noServer: true, maxPayload: 1024 * 1024 });
9359
+ opens = /* @__PURE__ */ new WeakMap();
9360
+ channels = /* @__PURE__ */ new Set();
9361
+ /** A stream from the link: a connection to this server. */
9362
+ accept(stream, open3) {
9363
+ this.opens.set(stream, open3);
9364
+ this.server.emit("connection", stream);
9365
+ }
9366
+ openOf(req) {
9367
+ return this.opens.get(req.socket) ?? { kind: "http", session: null, grant: null };
9368
+ }
9369
+ /** Why `need` is not allowed for this stream now, or `null`. */
9370
+ refusal(need, open3) {
9371
+ const l = this.o.policy();
9372
+ if (need === "terminals" && !l.policy.terminals) {
9373
+ return {
9374
+ status: 403,
9375
+ body: { error: "terminals_off", message: l.problem ? `this machine's policy denies everything: ${l.problem}` : "terminals are off in this machine's policy.json" }
9376
+ };
9377
+ }
9378
+ if (!this.o.presence.check(open3.grant, open3.session)) {
9379
+ return { status: 401, body: { error: "presence", message: "this machine wants proof that you are there: the machines passphrase" } };
9380
+ }
9381
+ return null;
9382
+ }
9383
+ /** Close every channel the policy or presence no longer allows. Returns how many. */
9384
+ enforce() {
9385
+ let n = 0;
9386
+ for (const ch of [...this.channels]) {
9387
+ if (this.refusal(ch.need, ch.open) === null) continue;
9388
+ this.channels.delete(ch);
9389
+ ch.close();
9390
+ n += 1;
9391
+ }
9392
+ return n;
9393
+ }
9394
+ /** Track a channel for as long as `until` lasts; holds its grant from idling out. */
9395
+ channel(need, open3, close, onEnd) {
9396
+ const ch = { need, open: open3, close };
9397
+ this.channels.add(ch);
9398
+ const release = open3.grant ? this.o.presence.hold(open3.grant) : () => {
9399
+ };
9400
+ onEnd(() => {
9401
+ this.channels.delete(ch);
9402
+ release();
9403
+ });
9404
+ }
9405
+ get openChannels() {
9406
+ return this.channels.size;
9407
+ }
9408
+ async handle(req, res) {
9409
+ const url = new URL(req.url ?? "/", "http://machine");
9410
+ const open3 = this.openOf(req);
9411
+ const route = `${req.method} ${url.pathname}`;
9412
+ switch (route) {
9413
+ case "GET /api/health": {
9414
+ const hub = this.o.hub();
9415
+ const l = this.o.policy();
9416
+ return json(res, 200, {
9417
+ ok: true,
9418
+ herdr: { connected: hub?.connected ?? false, version: hub?.version ?? null, protocol: hub?.protocol ?? null },
9419
+ workspaceRoot: this.o.home,
9420
+ homeRoot: this.o.home,
9421
+ sharing: false,
9422
+ publicUrl: null,
9423
+ machine: { terminals: l.policy.terminals, problem: l.problem, presenceLocked: this.o.presence.locked() }
9424
+ });
9425
+ }
9426
+ case "POST /presence/challenge": {
9427
+ await readJson(req);
9428
+ const c = this.o.presence.challenge(open3.session);
9429
+ if (c === null) return json(res, 400, { error: "no_session", message: "no machines session" });
9430
+ if ("locked" in c) return json(res, 429, { error: "locked", until: c.until, message: lockedMessage(c.until) });
9431
+ return json(res, 200, c);
9432
+ }
9433
+ case "POST /presence/answer": {
9434
+ const b = await readJson(req);
9435
+ const r = this.o.presence.answer(open3.session, typeof b?.challenge === "string" ? b.challenge : "", typeof b?.answer === "string" ? b.answer : "");
9436
+ if (r.ok) return json(res, 200, { grant: r.grant, expiresAt: r.expiresAt });
9437
+ if (r.error === "locked") return json(res, 429, { error: "locked", until: r.until, message: lockedMessage(r.until) });
9438
+ if (r.error === "wrong") return json(res, 401, { error: "wrong", message: "That is not this machine's passphrase.", failures: r.failures });
9439
+ return json(res, 400, { error: r.error, message: "That challenge has expired; try again." });
9440
+ }
9441
+ case "GET /api/session":
9442
+ case "POST /api/rpc": {
9443
+ const no = this.refusal("terminals", open3);
9444
+ if (no) return json(res, no.status, no.body);
9445
+ const socket2 = this.o.herdrSocket();
9446
+ if (!socket2) return json(res, 503, { error: "herdr", message: "herdr is starting on this machine" });
9447
+ this.channel(
9448
+ "terminals",
9449
+ open3,
9450
+ () => res.destroy(),
9451
+ (cb) => res.once("close", cb)
9452
+ );
9453
+ if (route === "GET /api/session") {
9454
+ const hub = this.o.hub();
9455
+ if (!hub) return json(res, 503, { error: "herdr", message: "herdr is starting on this machine" });
9456
+ try {
9457
+ return json(res, 200, await hub.snapshot());
9458
+ } catch (err) {
9459
+ return json(res, 502, { error: "herdr", message: err.message });
9460
+ }
9461
+ }
9462
+ const b = await readJson(req);
9463
+ const method = b?.method;
9464
+ if (typeof method !== "string") return json(res, 400, { error: "method required" });
9465
+ if (!isAllowed(method)) return json(res, 403, { error: "method not allowed" });
9466
+ const params = typeof b?.params === "object" && b.params !== null ? b.params : {};
9467
+ try {
9468
+ return json(res, 200, { result: await request(socket2, method, params) });
9469
+ } catch (err) {
9470
+ if (err instanceof HerdrError) return json(res, 502, { error: { code: err.code, message: err.message } });
9471
+ return json(res, 502, { error: { code: "unreachable", message: err.message } });
9472
+ }
9473
+ }
9474
+ default:
9475
+ return json(res, 404, { error: "not found" });
9476
+ }
9477
+ }
9478
+ upgrade(req, socket2, head) {
9479
+ socket2.on("error", () => socket2.destroy());
9480
+ const url = new URL(req.url ?? "/", "http://machine");
9481
+ const open3 = this.openOf(req);
9482
+ if (url.pathname !== "/ws/terminal" && url.pathname !== "/ws/events") return refuseSocket(socket2, 404, { error: "not found" });
9483
+ const no = this.refusal("terminals", open3);
9484
+ if (no) return refuseSocket(socket2, no.status, no.body);
9485
+ const streams = this.o.streams();
9486
+ const hub = this.o.hub();
9487
+ if (!streams || !hub) return refuseSocket(socket2, 503, { error: "herdr", message: "herdr is starting on this machine" });
9488
+ this.wss.handleUpgrade(req, socket2, head, (ws) => {
9489
+ this.channel(
9490
+ "terminals",
9491
+ open3,
9492
+ () => ws.terminate(),
9493
+ (cb) => ws.once("close", cb)
9494
+ );
9495
+ if (url.pathname === "/ws/terminal") {
9496
+ serveTerminalSocket(ws, streams, { pane: url.searchParams.get("pane") ?? void 0, cols: url.searchParams.get("cols") ?? void 0, rows: url.searchParams.get("rows") ?? void 0 });
9497
+ return;
9498
+ }
9499
+ this.events(ws, hub);
9500
+ });
9501
+ }
9502
+ /** `/ws/events`: a fresh snapshot, then every hub message as it comes. */
9503
+ events(ws, hub) {
9504
+ heartbeat(ws);
9505
+ let off = null;
9506
+ const send = (m) => {
9507
+ if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(m));
9508
+ };
9509
+ hub.snapshot().then(
9510
+ (snapshot) => {
9511
+ if (ws.readyState !== ws.OPEN) return;
9512
+ send({ kind: "snapshot", snapshot });
9513
+ off = hub.on(send);
9514
+ },
9515
+ () => ws.close()
9516
+ );
9517
+ ws.on("close", () => off?.());
9518
+ }
9519
+ close() {
9520
+ for (const ch of [...this.channels]) ch.close();
9521
+ this.channels.clear();
9522
+ this.wss.close();
9523
+ this.server.close();
9524
+ }
9525
+ };
9526
+ function lockedMessage(until) {
9527
+ const mins = Math.max(1, Math.ceil((until - Date.now()) / 6e4));
9528
+ return `Too many wrong passphrases: this machine has locked presence for ${mins} more minute${mins === 1 ? "" : "s"}.`;
9529
+ }
9530
+
9531
+ // src/node/service-linux.ts
9532
+ import { execFile } from "node:child_process";
9533
+ import fs17 from "node:fs";
9534
+ import os9 from "node:os";
9535
+ import path15 from "node:path";
9536
+ var UNIT_NAME = "agentbox-node.service";
9537
+ var runCommand = (cmd, args) => new Promise((resolve) => {
9538
+ execFile(cmd, args, { timeout: 3e4 }, (err, stdout, stderr) => {
9539
+ const code2 = err ? typeof err.code === "number" ? err.code : 127 : 0;
9540
+ resolve({ code: code2, stdout: String(stdout), stderr: String(stderr) });
9541
+ });
9542
+ });
9543
+ function unitDir(env = process.env, home2 = os9.homedir()) {
9544
+ const xdg = env.XDG_CONFIG_HOME;
9545
+ return path15.join(xdg && path15.isAbsolute(xdg) ? xdg : path15.join(home2, ".config"), "systemd", "user");
9546
+ }
9547
+ function execStart(execPath = process.execPath, script = process.argv[1]) {
9548
+ const isSea = script === void 0 || path15.resolve(script) === path15.resolve(execPath);
9549
+ return isSea ? [execPath, "node", "run"] : [execPath, path15.resolve(script), "node", "run"];
9550
+ }
9551
+ function quote(arg) {
9552
+ return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%")}"`;
9553
+ }
9554
+ function unitText(command2) {
9555
+ return [
9556
+ "[Unit]",
9557
+ "Description=agentbox node: this machine's link to its box",
9558
+ "After=network-online.target",
9559
+ "Wants=network-online.target",
9560
+ "",
9561
+ "[Service]",
9562
+ "Type=simple",
9563
+ `ExecStart=${command2.map(quote).join(" ")}`,
9564
+ "Restart=on-failure",
9565
+ "RestartSec=5",
9566
+ // herdr, and the agents in it, outlive a restart of the node (an
9567
+ // update, a crash); the next node adopts it.
9568
+ "KillMode=process",
9569
+ "",
9570
+ "[Install]",
9571
+ "WantedBy=default.target",
9572
+ ""
9573
+ ].join("\n");
9574
+ }
9575
+ async function installService(opts) {
9576
+ const run4 = opts.run ?? runCommand;
9577
+ const dir = unitDir(opts.env);
9578
+ fs17.mkdirSync(dir, { recursive: true });
9579
+ const unitFile = path15.join(dir, UNIT_NAME);
9580
+ fs17.writeFileSync(unitFile, unitText(opts.command), { mode: 420 });
9581
+ for (const args of [["--user", "daemon-reload"], ["--user", "enable", UNIT_NAME], ["--user", "restart", UNIT_NAME]]) {
9582
+ const r = await run4("systemctl", args);
9583
+ if (r.code !== 0) {
9584
+ throw new Error(`systemctl ${args.join(" ")} failed: ${(r.stderr || r.stdout).trim() || `exit ${r.code}`}`);
9585
+ }
9586
+ }
9587
+ const user = opts.user ?? os9.userInfo().username;
9588
+ const linger = await run4("loginctl", ["enable-linger", user]);
9589
+ if (linger.code === 0) return { unitFile, lingering: true, note: null };
9590
+ const cron = `@reboot ${opts.command.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ")}`;
9591
+ return {
9592
+ unitFile,
9593
+ lingering: false,
9594
+ note: `This machine does not let the node keep running after you log out (loginctl enable-linger was refused), so it runs while you are logged in. To start it at boot instead, add this line with \`crontab -e\`:
9595
+ ${cron}`
9596
+ };
9597
+ }
9598
+ async function removeService(opts = {}) {
9599
+ const run4 = opts.run ?? runCommand;
9600
+ const unitFile = path15.join(unitDir(opts.env), UNIT_NAME);
9601
+ if (!fs17.existsSync(unitFile)) return false;
9602
+ await run4("systemctl", ["--user", "disable", "--now", UNIT_NAME]);
9603
+ fs17.rmSync(unitFile, { force: true });
9604
+ await run4("systemctl", ["--user", "daemon-reload"]);
9605
+ return true;
9606
+ }
9607
+ function serviceInstalled(env = process.env) {
9608
+ return fs17.existsSync(path15.join(unitDir(env), UNIT_NAME));
9609
+ }
9610
+
9611
+ // src/node/specs.ts
9612
+ import { execFile as execFile2 } from "node:child_process";
9613
+ import fs18 from "node:fs";
9614
+ import os10 from "node:os";
9615
+ import path16 from "node:path";
9616
+ function run(cmd, args, timeoutMs = 3e3) {
9617
+ return new Promise((resolve) => {
9618
+ execFile2(cmd, args, { timeout: timeoutMs, windowsHide: true, maxBuffer: 256 * 1024 }, (err, stdout) => resolve(err ? null : stdout));
9619
+ });
9620
+ }
9621
+ function onPath(cmd, env) {
9622
+ const exts = process.platform === "win32" ? [".exe", ".cmd", ""] : [""];
9623
+ for (const dir of (env.PATH ?? "").split(path16.delimiter)) {
9624
+ for (const ext of exts) {
9625
+ try {
9626
+ fs18.accessSync(path16.join(dir, cmd + ext), fs18.constants.X_OK);
9627
+ return true;
9628
+ } catch {
9629
+ }
9630
+ }
9631
+ }
9632
+ return false;
9633
+ }
9634
+ async function gpus(env) {
9635
+ if (onPath("nvidia-smi", env)) {
9636
+ const out = await run("nvidia-smi", ["--query-gpu=name,memory.total", "--format=csv,noheader,nounits"]);
9637
+ if (out) {
9638
+ return out.split("\n").map((l) => l.split(",").map((s) => s.trim())).filter((p) => p[0]).map(([name, mem]) => ({ name, memoryMiB: Number(mem) || null }));
9639
+ }
9640
+ }
9641
+ if (onPath("rocm-smi", env)) {
9642
+ const out = await run("rocm-smi", ["--showproductname", "--csv"]);
9643
+ if (out) {
9644
+ return out.split("\n").slice(1).map((l) => l.split(",")[2]?.trim() ?? "").filter(Boolean).map((name) => ({ name, memoryMiB: null }));
9645
+ }
9646
+ }
9647
+ return [];
9648
+ }
9649
+ function freeBytes(dir) {
9650
+ try {
9651
+ const s = fs18.statfsSync(dir);
9652
+ return s.bavail * s.bsize;
9653
+ } catch {
9654
+ return null;
9655
+ }
9656
+ }
9657
+ async function claudeVersion(binDir, env) {
9658
+ const own = path16.join(binDir, process.platform === "win32" ? "claude.exe" : "claude");
9659
+ const cmd = fs18.existsSync(own) ? own : onPath("claude", env) ? "claude" : null;
9660
+ if (!cmd) return null;
9661
+ const out = await run(cmd, ["--version"], 5e3);
9662
+ return out ? /\d+\.\d+\.\d+/.exec(out)?.[0] ?? null : null;
9663
+ }
9664
+ async function gatherSpecs(opts) {
9665
+ const env = opts.env ?? process.env;
9666
+ const cpus = os10.cpus();
9667
+ const disks = [];
9668
+ for (const f of opts.policy.files) {
9669
+ const free = freeBytes(f.path);
9670
+ if (free !== null) disks.push({ path: f.path, freeBytes: free });
9671
+ }
9672
+ return {
9673
+ platform: process.platform,
9674
+ arch: process.arch,
9675
+ hostname: os10.hostname(),
9676
+ cpu: { model: cpus[0]?.model ?? "", cores: cpus.length },
9677
+ ramBytes: os10.totalmem(),
9678
+ disks,
9679
+ gpus: await gpus(env),
9680
+ docker: onPath("docker", env),
9681
+ claude: await claudeVersion(opts.binDir, env)
9682
+ };
9683
+ }
9684
+
9685
+ // src/node/run.ts
9686
+ var RESTART_EXIT = 75;
9687
+ var HEADLESS = { cols: 200, rows: 50, ms: 3e4 };
9688
+ async function runNode(o) {
9689
+ const now = o.now ?? Date.now;
9690
+ const creds = readCredentials(o.paths.credentials);
9691
+ if (!creds) throw new CliError("this machine has not joined a box; run `agentbox node join <box>`", EXIT.NOT_FOUND);
9692
+ if (creds.revokedAt !== void 0) {
9693
+ o.log(`this machine was revoked by ${creds.box} on ${new Date(creds.revokedAt).toISOString()}; re-join it to use it again`);
9694
+ return EXIT.OK;
9695
+ }
9696
+ if (await callControl(o.paths.control, o.paths.controlSecret, "status", 2e3) !== null) {
9697
+ throw new CliError("a node is already running on this machine (see `agentbox node status`)");
9698
+ }
9699
+ let policy = loadPolicy(o.paths.policy, o.home);
9700
+ const claude = claudePaths(o.paths);
9701
+ prepareClaude(claude, { home: o.home, machine: creds.name });
9702
+ const herdrEnv = { ...process.env, PATH: [claude.shims, o.paths.bin, process.env.PATH ?? ""].join(path17.delimiter) };
9703
+ delete herdrEnv.CLAUDE_CODE_OAUTH_TOKEN;
9704
+ const presence = new PresenceGate({ presence: creds.presence, machineId: creds.machineId, hours: () => policy.policy.presenceHours, now, log: o.log });
9705
+ let hub = null;
9706
+ let streams = null;
9707
+ const herdrBin = path17.join(o.paths.bin, process.platform === "win32" ? "herdr.exe" : "herdr");
9708
+ const herdr = fs19.existsSync(herdrBin) ? new HerdrSupervisor({ bin: herdrBin, dir: o.paths.dir, env: herdrEnv }) : null;
9709
+ if (!herdr) o.log(`herdr is not installed (${herdrBin}); terminals and agents are unavailable until \`agentbox node join\` installs it`);
9710
+ const server = new NodeServer({
9711
+ herdrSocket: () => herdr?.socketPath ?? null,
9712
+ hub: () => hub,
9713
+ streams: () => streams,
9714
+ policy: () => policy,
9715
+ presence,
9716
+ home: o.home,
9717
+ log: o.log
9718
+ });
9719
+ herdr?.on("log", (line) => o.log(line));
9720
+ herdr?.on("down", () => {
9721
+ hub?.stop();
9722
+ hub = null;
9723
+ streams?.stop();
9724
+ streams = null;
9725
+ });
9726
+ herdr?.on("ready", (socket2, info) => {
9727
+ hub?.stop();
9728
+ streams?.stop();
9729
+ const h = new SessionHub(socket2);
9730
+ hub = h;
9731
+ streams = new TerminalStreams({ ...herdrEnv, HERDR_SOCKET_PATH: socket2, HERDR_SESSION });
9732
+ const own = streams;
9733
+ const connect = (tries) => {
9734
+ h.start().then(
9735
+ async () => {
9736
+ if (!info.restored || hub !== h) return;
9737
+ const snap = await h.snapshot().catch(() => null);
9738
+ const quiet = { send: () => {
9739
+ }, sendJson: () => {
9740
+ }, close: () => {
9741
+ } };
9742
+ const held = (snap?.panes ?? []).map((p) => own.attach(p.pane_id, quiet, HEADLESS.cols, HEADLESS.rows));
9743
+ if (held.length) o.log(`herdr: ${held.length} restored pane(s) attached headless at ${HEADLESS.cols}\xD7${HEADLESS.rows}`);
9744
+ setTimeout(() => held.forEach((a) => a.detach()), HEADLESS.ms).unref();
9745
+ },
9746
+ () => {
9747
+ if (hub === h && tries > 0) setTimeout(() => connect(tries - 1), 1e3).unref();
9748
+ }
9749
+ );
9750
+ };
9751
+ connect(10);
9752
+ });
9753
+ const sayPolicy = (l) => {
9754
+ if (l.problem) o.log(`policy: ${l.problem}; everything is denied until it is fixed`);
9755
+ else o.log(`policy: terminals ${l.policy.terminals ? "on" : "off"}, ${l.policy.files.length} folder(s), ports ${l.policy.ports ? "on" : "off"}${l.policy.paused ? ", paused" : ""}`);
9756
+ };
9757
+ sayPolicy(policy);
9758
+ const link = new NodeLink({
9759
+ box: creds.box,
9760
+ token: creds.token,
9761
+ machineId: creds.machineId,
9762
+ version: o.version,
9763
+ describe: async () => {
9764
+ const summary = summarize(policy);
9765
+ return { ...await gatherSpecs({ binDir: o.paths.bin, policy: summary }), policy: summary };
9766
+ },
9767
+ ...o.link ?? {}
9768
+ });
9769
+ link.on("log", (line) => o.log(line));
9770
+ link.on("stream", (stream, open3) => server.accept(stream, open3));
9771
+ link.on("control", (m) => {
9772
+ if (m.type !== "claude-token") return;
9773
+ const token = typeof m.token === "string" ? m.token : null;
9774
+ holdToken(claude, token);
9775
+ o.log(token ? "the box sent the shared Claude sign-in; new Claude Code sessions here use it" : "the box withdrew the shared Claude sign-in from this machine");
9776
+ });
9777
+ link.on("state", (state) => o.log(`link: ${state}`));
9778
+ if (policy.policy.paused) link.pause();
9779
+ const stopWatching = watchPolicy(o.paths.policy, o.home, (l) => {
9780
+ policy = l;
9781
+ sayPolicy(l);
9782
+ const closed = server.enforce();
9783
+ if (closed) o.log(`policy: closed ${closed} channel(s) it no longer allows`);
9784
+ if (l.policy.paused && !link.isPaused) link.pause();
9785
+ else if (!l.policy.paused && link.isPaused) link.resume();
9786
+ else link.sendPolicy(summarize(l));
9787
+ });
9788
+ let finish = () => {
9789
+ };
9790
+ const done = new Promise((r) => finish = r);
9791
+ const control2 = await startControl(o.paths.control, o.paths.controlSecret, async (command2) => {
9792
+ switch (command2) {
9793
+ case "status":
9794
+ return {
9795
+ state: link.state,
9796
+ box: creds.box,
9797
+ machineId: creds.machineId,
9798
+ name: creds.name,
9799
+ paused: link.isPaused,
9800
+ policyProblem: policy.problem,
9801
+ pid: process.pid,
9802
+ herdrSocket: herdr?.socketPath ?? null,
9803
+ presenceLocked: presence.locked(),
9804
+ channels: server.openChannels
9805
+ };
9806
+ case "pause":
9807
+ setPaused(o.paths.policy, true);
9808
+ link.pause();
9809
+ return { state: link.state };
9810
+ case "resume":
9811
+ setPaused(o.paths.policy, false);
9812
+ link.resume();
9813
+ return { state: link.state };
9814
+ case "leave": {
9815
+ const told = await link.leave();
9816
+ setImmediate(() => finish(EXIT.OK));
9817
+ return { told };
9818
+ }
9819
+ }
9820
+ });
9821
+ link.on("revoked", () => {
9822
+ fs19.rmSync(o.paths.secrets, { recursive: true, force: true });
9823
+ writeCredentials(o.paths.credentials, { ...creds, revokedAt: now() });
9824
+ o.log(`${creds.box} revoked this machine. Its shared secrets are wiped; everything else is kept. Re-join it to use it again.`);
9825
+ finish(EXIT.OK);
9826
+ });
9827
+ link.on("state", (state) => {
9828
+ if (state === "left") finish(EXIT.OK);
9829
+ });
9830
+ o.signal.addEventListener("abort", () => finish(EXIT.OK), { once: true });
9831
+ let checking = false;
9832
+ const checkUpdates = async () => {
9833
+ if (checking || o.checkUpdates === false) return;
9834
+ checking = true;
9835
+ try {
9836
+ const herdrPath = herdrBin;
9837
+ const report = await checkForUpdate({
9838
+ policy: policy.policy.updates,
9839
+ running: o.version,
9840
+ // Only the standalone executable replaces itself; agentbox.mjs under Node does not.
9841
+ nodePath: execStart().length === 3 ? process.execPath : null,
9842
+ ...fs19.existsSync(herdrPath) ? { herdr: { path: herdrPath, version: HERDR_VERSION } } : {},
9843
+ ...installedClaude(claude) ? { claude: { path: claude.bin, version: installedClaude(claude), versionFile: claude.versionFile } } : {},
9844
+ // Agents live in herdr: herdr and Claude Code are replaced only while none runs.
9845
+ idle: () => herdr ? herdr.idle() : true
9846
+ });
9847
+ o.log(`update: ${describeReport(report)}`);
9848
+ if ((report.state === "current" || report.state === "updated") && report.tools.herdr === "updated") void herdr?.restart("updated");
9849
+ if (report.state === "updated" && report.restart) finish(RESTART_EXIT);
9850
+ } catch (err) {
9851
+ o.log(`update check failed: ${err.message}`);
9852
+ } finally {
9853
+ checking = false;
9854
+ }
9855
+ };
9856
+ link.on("upgrade", () => void checkUpdates());
9857
+ const firstCheck = setTimeout(() => void checkUpdates(), 10 * 6e4);
9858
+ firstCheck.unref();
9859
+ const daily = setInterval(() => void checkUpdates(), 24 * 60 * 6e4);
9860
+ daily.unref();
9861
+ const enforcer = setInterval(() => server.enforce(), o.enforceMs ?? 5e3);
9862
+ enforcer.unref();
9863
+ if (herdr) {
9864
+ herdr.start().catch((err) => {
9865
+ o.log(`herdr did not start: ${err.message}; trying again`);
9866
+ void herdr.restart("it did not start");
9867
+ });
9868
+ }
9869
+ link.start();
9870
+ const code2 = await done;
9871
+ clearInterval(enforcer);
9872
+ server.close();
9873
+ herdr?.stop();
9874
+ hub?.stop();
9875
+ streams?.stop();
9876
+ clearTimeout(firstCheck);
9877
+ clearInterval(daily);
9878
+ link.stop(link.state === "revoked" ? "revoked" : "stopped");
9879
+ stopWatching();
9880
+ control2.close();
9881
+ return code2;
9882
+ }
9883
+
9884
+ // src/commands/node.ts
9885
+ function paths(ctx) {
9886
+ return nodePaths(nodeDir(ctx.env, ctx.env.HOME || os11.homedir(), ctx.platform));
9887
+ }
9888
+ function home(ctx) {
9889
+ return ctx.env.HOME || os11.homedir();
9890
+ }
9891
+ function parseFolders(raw, homeDir2) {
9892
+ const out = [];
9893
+ for (const part of raw.split(",").map((s) => s.trim()).filter(Boolean)) {
9894
+ const m = /^(.*?)(?::(read|write))?$/.exec(part);
9895
+ let p = m[1];
9896
+ if (p === "~") p = homeDir2;
9897
+ else if (p.startsWith("~/")) p = path18.join(homeDir2, p.slice(2));
9898
+ p = path18.resolve(p);
9899
+ if (p === path18.resolve(homeDir2)) throw new UsageError("not the whole home folder: name the project folders the box may use");
9900
+ out.push({ path: p, mode: m[2] ?? "write" });
9901
+ }
9902
+ return out;
9903
+ }
9904
+ function printPolicy(ctx, p) {
9905
+ try {
9906
+ ctx.err(`${fs20.readFileSync(p.policy, "utf8")}`);
9907
+ } catch {
9908
+ ctx.err("(no policy.json)\n");
9909
+ }
9910
+ }
9911
+ var join = command({
9912
+ path: ["node", "join"],
9913
+ summary: "join this computer to a box as a machine",
9914
+ usage: "<box-url>",
9915
+ operands: { min: 1, max: 1 },
9916
+ details: "Installs what the node runs (verified), asks for the machines passphrase here, shows a code to type\non the box's machines Admin page, writes policy.json if there is none, and starts the node as a\nsystemd user service.",
9917
+ options: [
9918
+ { name: "name", type: "string", value: "name", description: "what the box calls this machine (default: the hostname)" },
9919
+ { name: "folder", type: "string", value: "dirs", description: "folders the box may use, comma-separated, each dir or dir:read (asked when absent)" },
9920
+ { name: "no-service", type: "boolean", description: "do not register a service; run `agentbox node run` yourself" },
9921
+ { name: "no-claude", type: "boolean", description: "do not install Claude Code (about 280 MiB); agents can be started once it is there" }
9922
+ ],
9923
+ async run(ctx, p) {
9924
+ const box = normalizeBoxUrl(p.operands[0]);
9925
+ const np = paths(ctx);
9926
+ const prompt = new Prompter(ctx.io.stdin, ctx.io.stderr);
9927
+ const folderArg = str(p.options, "folder");
9928
+ const noService = bool(p.options, "no-service") || ctx.platform !== "linux";
9929
+ try {
9930
+ await joinMachine(
9931
+ { box, name: str(p.options, "name") ?? os11.hostname(), folders: folderArg !== void 0 ? parseFolders(folderArg, home(ctx)) : null },
9932
+ {
9933
+ paths: np,
9934
+ platform: ctx.platform,
9935
+ arch: process.arch,
9936
+ hostname: os11.hostname(),
9937
+ now: ctx.now,
9938
+ err: (s) => ctx.err(s),
9939
+ client: ctx.client(box, null),
9940
+ freeBytes,
9941
+ install: async () => {
9942
+ await installHerdr(np.bin, { platform: ctx.platform, arch: process.arch, from: ctx.env.AGENTBOX_NODE_HERDR_FROM || void 0, log: (s) => ctx.err(s) });
9943
+ if (!bool(p.options, "no-claude")) await installClaude(claudePaths(np), { platform: ctx.platform, arch: process.arch, log: (s) => ctx.err(s) });
9944
+ },
9945
+ passphrase: () => askPassphrase(prompt, (s) => ctx.err(s)),
9946
+ enrol: (pass) => enrol(pass, ctx.now()),
9947
+ sleep: (ms) => sleep(ms, ctx.abort.signal),
9948
+ askFolders: async () => {
9949
+ ctx.err("Which project folders may the box use? (comma-separated; add :read for read-only; never your whole home)\n");
9950
+ const answer = await prompt.line("Folders: ");
9951
+ return answer ? parseFolders(answer, home(ctx)) : [];
9952
+ },
9953
+ service: noService ? null : async () => {
9954
+ try {
9955
+ const r = await installService({ command: execStart(), env: ctx.env });
9956
+ ctx.err(`Registered ${UNIT_NAME} (${r.unitFile}).
9957
+ `);
9958
+ if (r.note) ctx.err(`${r.note}
9959
+ `);
9960
+ } catch (err) {
9961
+ throw new CliError(`${err.message}
9962
+ The machine is joined; start it with \`agentbox node run\`.`);
9963
+ }
9964
+ },
9965
+ waitOnline: async () => {
9966
+ const deadline = Date.now() + 6e4;
9967
+ while (Date.now() < deadline) {
9968
+ const s = await callControl(np.control, np.controlSecret, "status", 2e3);
9969
+ if (s?.state === "online") return true;
9970
+ await sleep(1e3, ctx.abort.signal);
9971
+ }
9972
+ return false;
9973
+ },
9974
+ printPolicy: () => printPolicy(ctx, np)
9975
+ }
9976
+ );
9977
+ } finally {
9978
+ prompt.close();
9979
+ }
9980
+ }
9981
+ });
9982
+ var run2 = command({
9983
+ path: ["node", "run"],
9984
+ summary: "run the node in the foreground (the service runs this)",
9985
+ usage: "",
9986
+ ownsInterrupt: true,
9987
+ async run(ctx) {
9988
+ const np = paths(ctx);
9989
+ const log = (line) => {
9990
+ ctx.err(`${new Date(ctx.now()).toISOString()} ${line}
9991
+ `);
9992
+ };
9993
+ return runNode({ paths: np, home: home(ctx), version: VERSION, log, signal: ctx.abort.signal });
9994
+ }
9995
+ });
9996
+ var status2 = command({
9997
+ path: ["node", "status"],
9998
+ summary: "whether this machine is joined, linked and paused",
9999
+ usage: "",
10000
+ json: true,
10001
+ async run(ctx) {
10002
+ const np = paths(ctx);
10003
+ const creds = readCredentials(np.credentials);
10004
+ const live = await callControl(np.control, np.controlSecret, "status", 3e3);
10005
+ const policy = loadPolicy(np.policy, home(ctx));
10006
+ const out = {
10007
+ joined: creds !== null,
10008
+ box: creds?.box ?? null,
10009
+ machineId: creds?.machineId ?? null,
10010
+ name: creds?.name ?? null,
10011
+ revoked: creds?.revokedAt !== void 0 && creds !== null,
10012
+ running: live !== null,
10013
+ state: live ? live.state : creds ? creds.revokedAt !== void 0 ? "revoked" : "not running" : "not joined",
10014
+ paused: policy.policy.paused,
10015
+ policyProblem: policy.problem,
10016
+ folder: np.dir,
10017
+ service: ctx.platform === "linux" ? serviceInstalled(ctx.env) : false,
10018
+ herdrSocket: typeof live?.herdrSocket === "string" ? live.herdrSocket : null,
10019
+ presenceLocked: typeof live?.presenceLocked === "number" ? live.presenceLocked : null
10020
+ };
10021
+ if (ctx.json) return ctx.printJson(out);
10022
+ if (!creds) {
10023
+ ctx.out(`Not joined to a box. Run \`agentbox node join <box>\`.
10024
+ `);
10025
+ return EXIT.NOT_FOUND;
10026
+ }
10027
+ ctx.out(`${safeText(out.name ?? "")} (machine ${safeText(out.machineId ?? "")}) at ${safeText(out.box ?? "")}
10028
+ `);
10029
+ ctx.out(`state: ${safeText(String(out.state))}${out.paused ? " (paused: `agentbox node resume`)" : ""}
10030
+ `);
10031
+ if (out.policyProblem) ctx.out(`policy: ${safeText(out.policyProblem)} \u2014 everything is denied
10032
+ `);
10033
+ ctx.out(`folder: ${np.dir}
10034
+ service: ${out.service ? UNIT_NAME : "none"}
10035
+ `);
10036
+ if (out.herdrSocket) ctx.out(`herdr: HERDR_SOCKET_PATH=${out.herdrSocket} herdr (the agents the box sees)
10037
+ `);
10038
+ if (out.presenceLocked) ctx.out(`presence: locked until ${new Date(out.presenceLocked).toISOString()} after wrong passphrases
10039
+ `);
10040
+ }
10041
+ });
10042
+ async function setPause(ctx, paused) {
10043
+ const np = paths(ctx);
10044
+ if (!readCredentials(np.credentials)) throw new CliError("this machine has not joined a box", EXIT.NOT_FOUND);
10045
+ const live = await callControl(np.control, np.controlSecret, paused ? "pause" : "resume");
10046
+ if (!live) setPaused(np.policy, paused);
10047
+ else if (live.ok !== true) throw new CliError(String(live.error ?? "the node refused"));
10048
+ ctx.err(paused ? "Paused: the link is down until `agentbox node resume`.\n" : "Resumed.\n");
10049
+ }
10050
+ var pause = command({ path: ["node", "pause"], summary: "take the link down until resume", usage: "", run: (ctx) => setPause(ctx, true) });
10051
+ var resume = command({ path: ["node", "resume"], summary: "bring the link back up", usage: "", run: (ctx) => setPause(ctx, false) });
10052
+ var leave = command({
10053
+ path: ["node", "leave"],
10054
+ summary: "leave the box: tell it if it can be reached, and forget this machine's token",
10055
+ usage: "",
10056
+ async run(ctx) {
10057
+ const np = paths(ctx);
10058
+ const creds = readCredentials(np.credentials);
10059
+ if (!creds) throw new CliError("this machine has not joined a box", EXIT.NOT_FOUND);
10060
+ let told = false;
10061
+ const live = await callControl(np.control, np.controlSecret, "leave", 1e4);
10062
+ if (live) told = live.told === true;
10063
+ else if (creds.revokedAt === void 0) {
10064
+ const link = new NodeLink({
10065
+ box: creds.box,
10066
+ token: creds.token,
10067
+ machineId: creds.machineId,
10068
+ version: VERSION,
10069
+ describe: async () => {
10070
+ const policy = summarize(loadPolicy(np.policy, home(ctx)));
10071
+ return { ...await gatherSpecs({ binDir: np.bin, policy }), policy };
10072
+ }
10073
+ });
10074
+ link.start();
10075
+ const deadline = Date.now() + 1e4;
10076
+ while (link.state !== "online" && Date.now() < deadline) await sleep(200);
10077
+ told = await link.leave();
10078
+ link.stop();
10079
+ }
10080
+ if (ctx.platform === "linux") await removeService({ env: ctx.env });
10081
+ fs20.rmSync(np.secrets, { recursive: true, force: true });
10082
+ fs20.rmSync(np.credentials, { force: true });
10083
+ fs20.rmSync(np.controlSecret, { force: true });
10084
+ ctx.err(told ? `Left ${creds.box}.
10085
+ ` : `Left here; ${creds.box} could not be told, so it lists this machine until the owner removes it.
10086
+ `);
10087
+ ctx.err(`Kept ${np.policy} and ${np.bin}.
10088
+ `);
10089
+ }
10090
+ });
10091
+ var logs = command({
10092
+ path: ["node", "logs"],
10093
+ summary: "the node's log (from the journal)",
10094
+ usage: "",
10095
+ ownsInterrupt: true,
10096
+ options: [{ name: "follow", short: "f", type: "boolean", description: "keep printing new lines" }],
10097
+ async run(ctx, p) {
10098
+ const args = ["--user", "-u", UNIT_NAME, "-n", "200", "--no-pager", ...bool(p.options, "follow") ? ["-f"] : []];
10099
+ return new Promise((resolve) => {
10100
+ const child = spawn7("journalctl", args, { stdio: "inherit" });
10101
+ child.on("error", () => {
10102
+ ctx.err("journalctl is not available here; run `agentbox node run` in a terminal to see the log.\n");
10103
+ resolve(EXIT.FAILURE);
10104
+ });
10105
+ child.on("exit", (code2) => resolve(code2 ?? EXIT.OK));
10106
+ ctx.abort.signal.addEventListener("abort", () => child.kill("SIGINT"), { once: true });
10107
+ });
10108
+ }
10109
+ });
10110
+ var passphrase = command({
10111
+ path: ["node", "passphrase"],
10112
+ summary: "set this machine's copy of the machines passphrase (after it changed on the box)",
10113
+ usage: "",
10114
+ async run(ctx) {
10115
+ const np = paths(ctx);
10116
+ const creds = readCredentials(np.credentials);
10117
+ if (!creds) throw new CliError("this machine has not joined a box", EXIT.NOT_FOUND);
10118
+ const prompt = new Prompter(ctx.io.stdin, ctx.io.stderr);
10119
+ try {
10120
+ const presence = await enrol(await askPassphrase(prompt, (s) => ctx.err(s)), ctx.now());
10121
+ writeCredentials(np.credentials, { ...creds, presence });
10122
+ } finally {
10123
+ prompt.close();
10124
+ }
10125
+ ctx.err("This machine now proves presence with the new passphrase.\n");
10126
+ }
10127
+ });
10128
+ var NODE_COMMANDS = [join, run2, status2, pause, resume, leave, logs, passphrase];
10129
+
6809
10130
  // src/commands/mount.ts
6810
- import fs8 from "node:fs";
6811
- import os7 from "node:os";
6812
- import path9 from "node:path";
10131
+ import fs21 from "node:fs";
10132
+ import os12 from "node:os";
10133
+ import path19 from "node:path";
6813
10134
 
6814
10135
  // src/dav.ts
6815
- import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
6816
- import http from "node:http";
10136
+ import { randomBytes as randomBytes8, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
10137
+ import http2 from "node:http";
6817
10138
  var REMOTE_PREFIX = "/api/dav";
6818
10139
  var BadPath = class extends Error {
6819
10140
  };
@@ -6859,7 +10180,7 @@ function underPrefix(p, prefix) {
6859
10180
  if (p.length < prefix.length) return false;
6860
10181
  const head = Buffer.from(p.slice(0, prefix.length));
6861
10182
  const want = Buffer.from(prefix);
6862
- if (head.length !== want.length || !timingSafeEqual(head, want)) return false;
10183
+ if (head.length !== want.length || !timingSafeEqual4(head, want)) return false;
6863
10184
  return p.length === prefix.length || p[prefix.length] === "/";
6864
10185
  }
6865
10186
  var LOOPBACK_HOSTS = ["127.0.0.1", "localhost", "[::1]"];
@@ -6921,20 +10242,20 @@ var DROP_REQUEST = /* @__PURE__ */ new Set(["host", "authorization", "proxy-auth
6921
10242
  var DROP_RESPONSE = /* @__PURE__ */ new Set(["set-cookie", "www-authenticate", "strict-transport-security", "alt-svc"]);
6922
10243
  var MAX_REWRITE = 64 * 1024 * 1024;
6923
10244
  function newCredentials() {
6924
- return { user: "agentbox", password: randomBytes3(24).toString("base64url") };
10245
+ return { user: "agentbox", password: randomBytes8(24).toString("base64url") };
6925
10246
  }
6926
10247
  function basicMatches(header, want) {
6927
10248
  const m = /^Basic\s+([A-Za-z0-9+/=]+)\s*$/i.exec(header ?? "");
6928
10249
  if (!m) return false;
6929
10250
  const got = Buffer.from(m[1], "base64");
6930
10251
  const expected = Buffer.from(`${want.user}:${want.password}`, "utf8");
6931
- return got.length === expected.length && timingSafeEqual(got, expected);
10252
+ return got.length === expected.length && timingSafeEqual4(got, expected);
6932
10253
  }
6933
10254
  var DavFront = class {
6934
10255
  constructor(client, opts = {}) {
6935
10256
  this.client = client;
6936
10257
  this.opts = opts;
6937
- this.secret = opts.secret ?? randomBytes3(16).toString("base64url");
10258
+ this.secret = opts.secret ?? randomBytes8(16).toString("base64url");
6938
10259
  this.credentials = opts.credentials === void 0 ? newCredentials() : opts.credentials;
6939
10260
  }
6940
10261
  client;
@@ -6948,7 +10269,7 @@ var DavFront = class {
6948
10269
  credentials;
6949
10270
  /** Start listening; resolves with the URL to give a WebDAV client. */
6950
10271
  async start() {
6951
- const server = http.createServer((req, res) => {
10272
+ const server = http2.createServer((req, res) => {
6952
10273
  this.handle(req, res).catch((err) => {
6953
10274
  this.opts.onError?.(err.message);
6954
10275
  if (!res.headersSent) {
@@ -6991,8 +10312,8 @@ var DavFront = class {
6991
10312
  }
6992
10313
  async handle(req, res) {
6993
10314
  const m = this.map;
6994
- const plain = (status2, text) => {
6995
- res.writeHead(status2, { "content-type": "text/plain; charset=utf-8", "content-length": String(Buffer.byteLength(text) + 1) });
10315
+ const plain = (status3, text) => {
10316
+ res.writeHead(status3, { "content-type": "text/plain; charset=utf-8", "content-length": String(Buffer.byteLength(text) + 1) });
6996
10317
  res.end(`${text}
6997
10318
  `);
6998
10319
  };
@@ -7071,7 +10392,7 @@ var DavFront = class {
7071
10392
  };
7072
10393
 
7073
10394
  // src/http.ts
7074
- import http2 from "node:http";
10395
+ import http3 from "node:http";
7075
10396
  import https from "node:https";
7076
10397
  import { Readable } from "node:stream";
7077
10398
  var USER_AGENT = `agentbox-cli/${VERSION} (node ${process.versions.node}; ${process.platform})`;
@@ -7117,7 +10438,7 @@ function encodeQuery(query, rawQuery) {
7117
10438
  if (rawQuery) parts.push(rawQuery);
7118
10439
  return parts.length ? `?${parts.join("&")}` : "";
7119
10440
  }
7120
- function apiErrorFrom(status2, text, what) {
10441
+ function apiErrorFrom(status3, text, what) {
7121
10442
  let message = "";
7122
10443
  let code2 = null;
7123
10444
  try {
@@ -7130,20 +10451,20 @@ function apiErrorFrom(status2, text, what) {
7130
10451
  message = text.trim().split("\n")[0] ?? "";
7131
10452
  }
7132
10453
  message = safeText(message.slice(0, 300));
7133
- if (status2 === 401) {
10454
+ if (status3 === 401) {
7134
10455
  return new ApiError(401, "the box did not accept this device's sign-in (the token was revoked or has expired); run `agentbox login` again", code2);
7135
10456
  }
7136
- if (status2 === 502 || status2 === 503 || status2 === 504) {
10457
+ if (status3 === 502 || status3 === 503 || status3 === 504) {
7137
10458
  message = message || "the box is up, but the service behind it is not answering";
7138
10459
  }
7139
- return new ApiError(status2, `${what}: ${message || `HTTP ${status2}`}`, code2);
10460
+ return new ApiError(status3, `${what}: ${message || `HTTP ${status3}`}`, code2);
7140
10461
  }
7141
10462
  var BoxClient = class {
7142
10463
  constructor(origin, token) {
7143
10464
  this.token = token;
7144
10465
  this.url = new URL(origin);
7145
10466
  this.origin = this.url.origin;
7146
- const Agent = this.url.protocol === "https:" ? https.Agent : http2.Agent;
10467
+ const Agent = this.url.protocol === "https:" ? https.Agent : http3.Agent;
7147
10468
  this.agent = new Agent({ keepAlive: true, maxSockets: 8 });
7148
10469
  }
7149
10470
  token;
@@ -7166,7 +10487,7 @@ var BoxClient = class {
7166
10487
  get hasToken() {
7167
10488
  return this.token !== null;
7168
10489
  }
7169
- request(method, path12, opts = {}) {
10490
+ request(method, path22, opts = {}) {
7170
10491
  const headers = { ...this.baseHeaders(opts.auth !== false), ...opts.headers ?? {} };
7171
10492
  let payload = null;
7172
10493
  const body = opts.body;
@@ -7181,8 +10502,8 @@ var BoxClient = class {
7181
10502
  }
7182
10503
  if (Buffer.isBuffer(payload)) headers["content-length"] = String(payload.length);
7183
10504
  }
7184
- const target = `${path12}${encodeQuery(opts.query, opts.rawQuery)}`;
7185
- const lib = this.url.protocol === "https:" ? https : http2;
10505
+ const target = `${path22}${encodeQuery(opts.query, opts.rawQuery)}`;
10506
+ const lib = this.url.protocol === "https:" ? https : http3;
7186
10507
  return new Promise((resolve, reject) => {
7187
10508
  const req = lib.request(
7188
10509
  {
@@ -7227,21 +10548,21 @@ var BoxClient = class {
7227
10548
  * A JSON call: the parsed answer on 2xx, else an {@link ApiError} with what
7228
10549
  * the box said. `what` names the action for the message ("listing /x").
7229
10550
  */
7230
- async json(method, path12, opts = {}) {
7231
- const res = await this.request(method, path12, { ...opts, headers: { accept: "application/json", ...opts.headers ?? {} } });
10551
+ async json(method, path22, opts = {}) {
10552
+ const res = await this.request(method, path22, { ...opts, headers: { accept: "application/json", ...opts.headers ?? {} } });
7232
10553
  const text = await res.text();
7233
- if (res.status < 200 || res.status >= 300) throw apiErrorFrom(res.status, text, opts.what ?? `${method} ${path12}`);
10554
+ if (res.status < 200 || res.status >= 300) throw apiErrorFrom(res.status, text, opts.what ?? `${method} ${path22}`);
7234
10555
  if (res.status === 204 || text === "") return void 0;
7235
10556
  try {
7236
10557
  return JSON.parse(text);
7237
10558
  } catch {
7238
- throw new CliError(`${opts.what ?? path12}: the box answered with something that is not JSON (is ${this.origin} an agentbox?)`);
10559
+ throw new CliError(`${opts.what ?? path22}: the box answered with something that is not JSON (is ${this.origin} an agentbox?)`);
7239
10560
  }
7240
10561
  }
7241
10562
  /** A request whose answer must be 2xx; the body is left to the caller. */
7242
- async ok(method, path12, opts = {}) {
7243
- const res = await this.request(method, path12, opts);
7244
- if (res.status < 200 || res.status >= 300) throw apiErrorFrom(res.status, await res.text(), opts.what ?? `${method} ${path12}`);
10563
+ async ok(method, path22, opts = {}) {
10564
+ const res = await this.request(method, path22, opts);
10565
+ if (res.status < 200 || res.status >= 300) throw apiErrorFrom(res.status, await res.text(), opts.what ?? `${method} ${path22}`);
7245
10566
  return res;
7246
10567
  }
7247
10568
  close() {
@@ -7255,11 +10576,11 @@ async function gvfsFolder(root, port, secret, ms) {
7255
10576
  for (; ; ) {
7256
10577
  let names = [];
7257
10578
  try {
7258
- names = fs8.readdirSync(root);
10579
+ names = fs21.readdirSync(root);
7259
10580
  } catch {
7260
10581
  }
7261
10582
  const name = gvfsMountName(names, port, secret);
7262
- if (name) return path9.join(root, name);
10583
+ if (name) return path19.join(root, name);
7263
10584
  if (Date.now() > deadline) return null;
7264
10585
  await new Promise((r) => setTimeout(r, 100));
7265
10586
  }
@@ -7319,7 +10640,7 @@ password ${credentials.password}
7319
10640
  }
7320
10641
  let plan;
7321
10642
  try {
7322
- plan = mountPlan({ platform: ctx.platform, url, dir, boxName: name, home: os7.homedir(), env: ctx.env, credentials });
10643
+ plan = mountPlan({ platform: ctx.platform, url, dir, boxName: name, home: os12.homedir(), env: ctx.env, credentials });
7323
10644
  } catch (err) {
7324
10645
  throw new CliError(err.message, EXIT.USAGE);
7325
10646
  }
@@ -7328,14 +10649,14 @@ password ${credentials.password}
7328
10649
  throw new CliError(`${plan.helper} was not found, so the box cannot be mounted here: ${plan.install}. Or serve it with \`agentbox mount --no-mount\` and use any WebDAV client.`);
7329
10650
  }
7330
10651
  let madeMountpoint = false;
7331
- if (plan.mountpoint && !fs8.existsSync(plan.mountpoint)) {
7332
- fs8.mkdirSync(plan.mountpoint, { recursive: true });
10652
+ if (plan.mountpoint && !fs21.existsSync(plan.mountpoint)) {
10653
+ fs21.mkdirSync(plan.mountpoint, { recursive: true });
7333
10654
  madeMountpoint = true;
7334
10655
  }
7335
- if (plan.link && fs8.existsSync(plan.link)) throw new CliError(`${plan.link} already exists; name a new folder for the link, or leave it out`);
10656
+ if (plan.link && fs21.existsSync(plan.link)) throw new CliError(`${plan.link} already exists; name a new folder for the link, or leave it out`);
7336
10657
  const mounted = await runStep(plan.mount, helper, ctx.env);
7337
10658
  if (mounted.code !== 0) {
7338
- if (madeMountpoint) fs8.rmSync(plan.mountpoint, { recursive: false, force: true });
10659
+ if (madeMountpoint) fs21.rmSync(plan.mountpoint, { recursive: false, force: true });
7339
10660
  throw new CliError(`${plan.helper} could not mount it${mounted.output ? `: ${mounted.output}` : ""}`);
7340
10661
  }
7341
10662
  let where = plan.where;
@@ -7357,7 +10678,7 @@ password ${credentials.password}
7357
10678
  }
7358
10679
  let linked = false;
7359
10680
  if (plan.link && where) {
7360
- fs8.symlinkSync(where, plan.link, "dir");
10681
+ fs21.symlinkSync(where, plan.link, "dir");
7361
10682
  linked = true;
7362
10683
  }
7363
10684
  if (!gioOnly) {
@@ -7372,10 +10693,10 @@ password ${credentials.password}
7372
10693
  const done = await runStep(unmount, findOnPath(unmount.command, ctx.env, ctx.platform) ?? unmount.command, ctx.env);
7373
10694
  if (done.code !== 0) ctx.warn(`${unmount.command} ${unmount.args.join(" ")} failed${done.output ? `: ${done.output}` : ""}; unmount it yourself`);
7374
10695
  }
7375
- if (linked) fs8.rmSync(plan.link, { force: true });
10696
+ if (linked) fs21.rmSync(plan.link, { force: true });
7376
10697
  if (madeMountpoint) {
7377
10698
  try {
7378
- fs8.rmdirSync(plan.mountpoint);
10699
+ fs21.rmdirSync(plan.mountpoint);
7379
10700
  } catch {
7380
10701
  }
7381
10702
  }
@@ -7387,12 +10708,12 @@ password ${credentials.password}
7387
10708
  var MOUNT_COMMANDS = [mount];
7388
10709
 
7389
10710
  // src/commands/terminal.ts
7390
- import fs9 from "node:fs";
7391
- import os8 from "node:os";
7392
- import path10 from "node:path";
10711
+ import fs22 from "node:fs";
10712
+ import os13 from "node:os";
10713
+ import path20 from "node:path";
7393
10714
 
7394
10715
  // src/ttyd.ts
7395
- import { EventEmitter } from "node:events";
10716
+ import { EventEmitter as EventEmitter3 } from "node:events";
7396
10717
  var CLIENT = { INPUT: 48, RESIZE: 49, PAUSE: 50, RESUME: 51 };
7397
10718
  var SERVER = { OUTPUT: 48, TITLE: 49, PREFERENCES: 50 };
7398
10719
  function handshakeFrame(columns, rows, authToken = "") {
@@ -7427,7 +10748,7 @@ function parseServerFrame(frame) {
7427
10748
  return { kind: "unknown", command: command2 ?? -1 };
7428
10749
  }
7429
10750
  }
7430
- var TtydSession = class extends EventEmitter {
10751
+ var TtydSession = class extends EventEmitter3 {
7431
10752
  constructor(client, basePath) {
7432
10753
  super();
7433
10754
  this.client = client;
@@ -7465,9 +10786,9 @@ var TtydSession = class extends EventEmitter {
7465
10786
  else if (parsed.kind === "title") this.emit("title", parsed.title);
7466
10787
  });
7467
10788
  ws.on("unexpected-response", (_req, res) => {
7468
- const status2 = res.statusCode ?? 0;
10789
+ const status3 = res.statusCode ?? 0;
7469
10790
  res.resume();
7470
- const message = status2 === 401 ? "the box did not accept this device's sign-in; run `agentbox login` again" : status2 === 403 ? "the box refused the terminal (origin check); is the box's URL right?" : status2 === 404 ? `this box has no ${this.basePath} terminal` : status2 === 502 || status2 === 503 ? `the ${this.basePath} terminal is not running on the box` : `the box refused the terminal (HTTP ${status2})`;
10791
+ const message = status3 === 401 ? "the box did not accept this device's sign-in; run `agentbox login` again" : status3 === 403 ? "the box refused the terminal (origin check); is the box's URL right?" : status3 === 404 ? `this box has no ${this.basePath} terminal` : status3 === 502 || status3 === 503 ? `the ${this.basePath} terminal is not running on the box` : `the box refused the terminal (HTTP ${status3})`;
7471
10792
  this.finish({ reason: "error", message });
7472
10793
  ws.terminate();
7473
10794
  });
@@ -7865,11 +11186,11 @@ agentbox: ${message}\r
7865
11186
 
7866
11187
  // src/commands/terminal.ts
7867
11188
  function cdCommand(dir) {
7868
- const quote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
11189
+ const quote2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
7869
11190
  let target;
7870
11191
  if (dir === "~") target = "~";
7871
- else if (dir.startsWith("~/")) target = `~/${quote(dir.slice(2))}`;
7872
- else target = quote(dir);
11192
+ else if (dir.startsWith("~/")) target = `~/${quote2(dir.slice(2))}`;
11193
+ else target = quote2(dir);
7873
11194
  return ` cd -- ${target} && clear\r`;
7874
11195
  }
7875
11196
  async function connectTerminal(ctx, basePath, initialInput) {
@@ -7921,8 +11242,8 @@ the network: faster, and no redraw glitches. Install it now with herdr's install
7921
11242
  ctx.warn(`herdr's installer exited ${code2}`);
7922
11243
  return null;
7923
11244
  }
7924
- const home = (win ? ctx.env.USERPROFILE : ctx.env.HOME) || os8.homedir();
7925
- return findOnPath("herdr", ctx.env, ctx.platform) ?? [path10.join(home, ".local", "bin", "herdr")].find((f) => fs9.existsSync(f)) ?? null;
11245
+ const home2 = (win ? ctx.env.USERPROFILE : ctx.env.HOME) || os13.homedir();
11246
+ return findOnPath("herdr", ctx.env, ctx.platform) ?? [path20.join(home2, ".local", "bin", "herdr")].find((f) => fs22.existsSync(f)) ?? null;
7926
11247
  }
7927
11248
  var attach = command({
7928
11249
  path: ["attach"],
@@ -7972,11 +11293,11 @@ var shell = command({
7972
11293
  var TERMINAL_COMMANDS = [attach, shell];
7973
11294
 
7974
11295
  // src/commands/update.ts
7975
- import { spawnSync as spawnSync2 } from "node:child_process";
7976
- import { randomBytes as randomBytes4 } from "node:crypto";
7977
- import fs10 from "node:fs";
7978
- import os9 from "node:os";
7979
- import path11 from "node:path";
11296
+ import { spawnSync as spawnSync3 } from "node:child_process";
11297
+ import { randomBytes as randomBytes9 } from "node:crypto";
11298
+ import fs23 from "node:fs";
11299
+ import os14 from "node:os";
11300
+ import path21 from "node:path";
7980
11301
  var MAX_BUNDLE = 16 * 1024 * 1024;
7981
11302
  function bundleVersion(bundle) {
7982
11303
  const head = (typeof bundle === "string" ? bundle : bundle.subarray(0, 512).toString("utf8")).split("\n", 3);
@@ -7989,17 +11310,17 @@ function selfInstall(argv1) {
7989
11310
  if (!argv1) return null;
7990
11311
  let file;
7991
11312
  try {
7992
- file = fs10.realpathSync(argv1);
11313
+ file = fs23.realpathSync(argv1);
7993
11314
  } catch {
7994
11315
  return null;
7995
11316
  }
7996
11317
  let checkout = false;
7997
11318
  let npm = false;
7998
11319
  try {
7999
- const root = path11.join(path11.dirname(file), "..");
8000
- const pkg = JSON.parse(fs10.readFileSync(path11.join(root, "package.json"), "utf8"));
11320
+ const root = path21.join(path21.dirname(file), "..");
11321
+ const pkg = JSON.parse(fs23.readFileSync(path21.join(root, "package.json"), "utf8"));
8001
11322
  if (pkg.name === NPM_PACKAGE) {
8002
- if (fs10.existsSync(path11.join(root, "src", "cli.ts"))) checkout = true;
11323
+ if (fs23.existsSync(path21.join(root, "src", "cli.ts"))) checkout = true;
8003
11324
  else npm = true;
8004
11325
  }
8005
11326
  } catch {
@@ -8013,13 +11334,13 @@ function npmVersionFor(boxVersion) {
8013
11334
  function nodeCredentials(env, platform = process.platform) {
8014
11335
  const candidates = [];
8015
11336
  if (platform === "win32") {
8016
- if (env.LOCALAPPDATA && path11.win32.isAbsolute(env.LOCALAPPDATA)) candidates.push(path11.join(env.LOCALAPPDATA, "agentbox", "node", "credentials.json"));
11337
+ if (env.LOCALAPPDATA && path21.win32.isAbsolute(env.LOCALAPPDATA)) candidates.push(path21.join(env.LOCALAPPDATA, "agentbox", "node", "credentials.json"));
8017
11338
  } else {
8018
11339
  const xdg = env.XDG_DATA_HOME;
8019
- if (xdg && path11.isAbsolute(xdg)) candidates.push(path11.join(xdg, "agentbox", "node", "credentials.json"));
8020
- candidates.push(path11.join(env.HOME || os9.homedir(), ".local", "share", "agentbox", "node", "credentials.json"));
11340
+ if (xdg && path21.isAbsolute(xdg)) candidates.push(path21.join(xdg, "agentbox", "node", "credentials.json"));
11341
+ candidates.push(path21.join(env.HOME || os14.homedir(), ".local", "share", "agentbox", "node", "credentials.json"));
8021
11342
  }
8022
- return candidates.find((f) => fs10.existsSync(f)) ?? null;
11343
+ return candidates.find((f) => fs23.existsSync(f)) ?? null;
8023
11344
  }
8024
11345
  function signedUpdatesOnly(env, platform = process.platform, standalone = STANDALONE) {
8025
11346
  const creds = nodeCredentials(env, platform);
@@ -8087,7 +11408,7 @@ var update = command({
8087
11408
  }
8088
11409
  return;
8089
11410
  }
8090
- const current = fs10.readFileSync(self.file);
11411
+ const current = fs23.readFileSync(self.file);
8091
11412
  if (bundle.equals(current)) {
8092
11413
  ctx.out(`Already up to date: agentbox ${VERSION}, the same build ${name} serves.
8093
11414
  `);
@@ -8098,24 +11419,24 @@ var update = command({
8098
11419
  `);
8099
11420
  return;
8100
11421
  }
8101
- const dir = path11.dirname(self.file);
8102
- const tmp = path11.join(dir, `.agentbox-update-${randomBytes4(4).toString("hex")}.mjs`);
11422
+ const dir = path21.dirname(self.file);
11423
+ const tmp = path21.join(dir, `.agentbox-update-${randomBytes9(4).toString("hex")}.mjs`);
8103
11424
  try {
8104
- fs10.writeFileSync(tmp, bundle, { mode: 493 });
8105
- fs10.chmodSync(tmp, 493);
11425
+ fs23.writeFileSync(tmp, bundle, { mode: 493 });
11426
+ fs23.chmodSync(tmp, 493);
8106
11427
  } catch (err) {
8107
- fs10.rmSync(tmp, { force: true });
11428
+ fs23.rmSync(tmp, { force: true });
8108
11429
  throw new CliError(`cannot write to ${dir} (${err.message}); reinstall with the box's install script`);
8109
11430
  }
8110
- const check = spawnSync2(process.execPath, [tmp, "--version"], { encoding: "utf8", timeout: 3e4 });
11431
+ const check = spawnSync3(process.execPath, [tmp, "--version"], { encoding: "utf8", timeout: 3e4 });
8111
11432
  if (check.status !== 0) {
8112
- fs10.rmSync(tmp, { force: true });
11433
+ fs23.rmSync(tmp, { force: true });
8113
11434
  throw new CliError(`the downloaded build does not run here: ${(check.stderr || check.stdout || "").trim().slice(0, 300)}`);
8114
11435
  }
8115
11436
  try {
8116
- fs10.renameSync(tmp, self.file);
11437
+ fs23.renameSync(tmp, self.file);
8117
11438
  } catch (err) {
8118
- fs10.rmSync(tmp, { force: true });
11439
+ fs23.rmSync(tmp, { force: true });
8119
11440
  throw new CliError(`cannot replace ${self.file}: ${err.message}`);
8120
11441
  }
8121
11442
  ctx.out(`Updated agentbox ${VERSION} \u2192 ${safeText(version)} from ${name}.
@@ -8125,7 +11446,7 @@ var update = command({
8125
11446
  var UPDATE_COMMANDS = [update];
8126
11447
 
8127
11448
  // src/commands/index.ts
8128
- var COMMANDS = [
11449
+ var COMMANDS2 = [
8129
11450
  ...AUTH_COMMANDS,
8130
11451
  ...STATUS_COMMANDS,
8131
11452
  ...TERMINAL_COMMANDS,
@@ -8136,6 +11457,7 @@ var COMMANDS = [
8136
11457
  ...APPS_COMMANDS,
8137
11458
  ...AGENT_COMMANDS,
8138
11459
  ...HERDR_COMMANDS,
11460
+ ...NODE_COMMANDS,
8139
11461
  ...UPDATE_COMMANDS
8140
11462
  ];
8141
11463
  function optionLine(o) {
@@ -8143,8 +11465,8 @@ function optionLine(o) {
8143
11465
  return ` ${flag.padEnd(26)} ${o.description}`;
8144
11466
  }
8145
11467
  function mainHelp() {
8146
- const width2 = Math.max(...COMMANDS.filter((c) => !c.hidden).map((c) => c.path.join(" ").length));
8147
- const lines = COMMANDS.filter((c) => !c.hidden).map((c) => ` ${c.path.join(" ").padEnd(width2)} ${c.summary}`);
11468
+ const width2 = Math.max(...COMMANDS2.filter((c) => !c.hidden).map((c) => c.path.join(" ").length));
11469
+ const lines = COMMANDS2.filter((c) => !c.hidden).map((c) => ` ${c.path.join(" ").padEnd(width2)} ${c.summary}`);
8148
11470
  return [
8149
11471
  `agentbox ${VERSION} \u2014 your agentbox, from this machine`,
8150
11472
  "",
@@ -8177,7 +11499,7 @@ function commandHelp(c) {
8177
11499
  ].join("\n");
8178
11500
  }
8179
11501
  function groupHelp(prefix) {
8180
- const members = COMMANDS.filter((c) => !c.hidden && prefix.every((w, i) => c.path[i] === w) && c.path.length > prefix.length);
11502
+ const members = COMMANDS2.filter((c) => !c.hidden && prefix.every((w, i) => c.path[i] === w) && c.path.length > prefix.length);
8181
11503
  if (members.length === 0) return null;
8182
11504
  const width2 = Math.max(...members.map((c) => c.path.join(" ").length));
8183
11505
  return [
@@ -8191,7 +11513,7 @@ function groupHelp(prefix) {
8191
11513
  }
8192
11514
 
8193
11515
  // src/context.ts
8194
- import os10 from "node:os";
11516
+ import os15 from "node:os";
8195
11517
  var VERSION_CHECK_MS = 24 * 60 * 6e4;
8196
11518
  var Context = class {
8197
11519
  constructor(io, env, config, globals, platform = process.platform, now = Date.now) {
@@ -8249,7 +11571,7 @@ var Context = class {
8249
11571
  }
8250
11572
  /** Where temporary files go: `$TMPDIR` (or Windows' `%TEMP%`), else the system's. */
8251
11573
  tempRoot() {
8252
- return this.env.TMPDIR || this.env.TEMP || this.env.TMP || os10.tmpdir();
11574
+ return this.env.TMPDIR || this.env.TEMP || this.env.TMP || os15.tmpdir();
8253
11575
  }
8254
11576
  printJson(value) {
8255
11577
  this.out(`${JSON.stringify(value, null, 2)}
@@ -8332,7 +11654,7 @@ function describeLocalError(err) {
8332
11654
  return { message: e.path ? `${e.path}: ${what}` : `${what} (${e.syscall ?? e.code})`, code: e.code === "ENOENT" ? EXIT.NOT_FOUND : EXIT.FAILURE };
8333
11655
  }
8334
11656
  var INTERRUPT_GRACE_MS = 1500;
8335
- async function run(argv, deps = {}) {
11657
+ async function run3(argv, deps = {}) {
8336
11658
  const io = deps.io ?? defaultIo();
8337
11659
  const env = deps.env ?? process.env;
8338
11660
  const fail = (message, code2) => {
@@ -8342,7 +11664,7 @@ async function run(argv, deps = {}) {
8342
11664
  };
8343
11665
  let parsed;
8344
11666
  try {
8345
- parsed = parseArgs(argv, COMMANDS);
11667
+ parsed = parseArgs(argv, COMMANDS2);
8346
11668
  } catch (err) {
8347
11669
  if (err instanceof UsageError) return fail(err.message, EXIT.USAGE);
8348
11670
  throw err;
@@ -8406,6 +11728,6 @@ if (nodeMajor() < MIN_NODE_MAJOR) {
8406
11728
  `);
8407
11729
  process.exit(1);
8408
11730
  }
8409
- var code = await run(process.argv.slice(2), { signals: true });
11731
+ var code = await run3(process.argv.slice(2), { signals: true });
8410
11732
  process.exitCode = code;
8411
11733
  process.stdout.write("", () => process.exit(code));