@slayzone/computer 0.36.3 → 0.36.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.cjs CHANGED
@@ -1453,7 +1453,7 @@ var require_receiver = __commonJS({
1453
1453
  var require_sender = __commonJS({
1454
1454
  "../../../node_modules/ws/lib/sender.js"(exports2, module2) {
1455
1455
  "use strict";
1456
- var { Duplex } = require("stream");
1456
+ var { Duplex: Duplex2 } = require("stream");
1457
1457
  var { randomFillSync } = require("crypto");
1458
1458
  var {
1459
1459
  types: { isUint8Array }
@@ -2330,11 +2330,11 @@ var require_websocket = __commonJS({
2330
2330
  "use strict";
2331
2331
  var EventEmitter = require("events");
2332
2332
  var https = require("https");
2333
- var http2 = require("http");
2333
+ var http3 = require("http");
2334
2334
  var net = require("net");
2335
2335
  var tls = require("tls");
2336
- var { randomBytes, createHash } = require("crypto");
2337
- var { Duplex, Readable } = require("stream");
2336
+ var { randomBytes: randomBytes2, createHash } = require("crypto");
2337
+ var { Duplex: Duplex2, Readable } = require("stream");
2338
2338
  var { URL: URL2 } = require("url");
2339
2339
  var PerMessageDeflate2 = require_permessage_deflate();
2340
2340
  var Receiver2 = require_receiver();
@@ -2884,8 +2884,8 @@ var require_websocket = __commonJS({
2884
2884
  }
2885
2885
  }
2886
2886
  const defaultPort = isSecure ? 443 : 80;
2887
- const key = randomBytes(16).toString("base64");
2888
- const request = isSecure ? https.request : http2.request;
2887
+ const key = randomBytes2(16).toString("base64");
2888
+ const request = isSecure ? https.request : http3.request;
2889
2889
  const protocolSet = /* @__PURE__ */ new Set();
2890
2890
  let perMessageDeflate;
2891
2891
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -3238,7 +3238,7 @@ var require_stream = __commonJS({
3238
3238
  "../../../node_modules/ws/lib/stream.js"(exports2, module2) {
3239
3239
  "use strict";
3240
3240
  var WebSocket2 = require_websocket();
3241
- var { Duplex } = require("stream");
3241
+ var { Duplex: Duplex2 } = require("stream");
3242
3242
  function emitClose(stream) {
3243
3243
  stream.emit("close");
3244
3244
  }
@@ -3256,7 +3256,7 @@ var require_stream = __commonJS({
3256
3256
  }
3257
3257
  function createWebSocketStream2(ws, options) {
3258
3258
  let terminateOnDestroy = true;
3259
- const duplex = new Duplex({
3259
+ const duplex = new Duplex2({
3260
3260
  ...options,
3261
3261
  autoDestroy: false,
3262
3262
  emitClose: false,
@@ -3381,8 +3381,8 @@ var require_websocket_server = __commonJS({
3381
3381
  "../../../node_modules/ws/lib/websocket-server.js"(exports2, module2) {
3382
3382
  "use strict";
3383
3383
  var EventEmitter = require("events");
3384
- var http2 = require("http");
3385
- var { Duplex } = require("stream");
3384
+ var http3 = require("http");
3385
+ var { Duplex: Duplex2 } = require("stream");
3386
3386
  var { createHash } = require("crypto");
3387
3387
  var extension2 = require_extension();
3388
3388
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -3462,8 +3462,8 @@ var require_websocket_server = __commonJS({
3462
3462
  );
3463
3463
  }
3464
3464
  if (options.port != null) {
3465
- this._server = http2.createServer((req, res) => {
3466
- const body = http2.STATUS_CODES[426];
3465
+ this._server = http3.createServer((req, res) => {
3466
+ const body = http3.STATUS_CODES[426];
3467
3467
  res.writeHead(426, {
3468
3468
  "Content-Length": body.length,
3469
3469
  "Content-Type": "text/plain"
@@ -3752,7 +3752,7 @@ var require_websocket_server = __commonJS({
3752
3752
  this.destroy();
3753
3753
  }
3754
3754
  function abortHandshake(socket, code, message, headers) {
3755
- message = message || http2.STATUS_CODES[code];
3755
+ message = message || http3.STATUS_CODES[code];
3756
3756
  headers = {
3757
3757
  Connection: "close",
3758
3758
  "Content-Type": "text/html",
@@ -3761,7 +3761,7 @@ var require_websocket_server = __commonJS({
3761
3761
  };
3762
3762
  socket.once("finish", socket.destroy);
3763
3763
  socket.end(
3764
- `HTTP/1.1 ${code} ${http2.STATUS_CODES[code]}\r
3764
+ `HTTP/1.1 ${code} ${http3.STATUS_CODES[code]}\r
3765
3765
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
3766
3766
  );
3767
3767
  }
@@ -5220,10 +5220,10 @@ var init_agent_hooks = __esm({
5220
5220
  }
5221
5221
  });
5222
5222
 
5223
- // raw-import:/tmp/slz-publish-wt.GgutIu/packages/shared/hooks/src/notify.sh
5223
+ // raw-import:/tmp/slz-publish-wt.a2Pyg4/packages/shared/hooks/src/notify.sh
5224
5224
  var notify_default;
5225
5225
  var init_notify = __esm({
5226
- "raw-import:/tmp/slz-publish-wt.GgutIu/packages/shared/hooks/src/notify.sh"() {
5226
+ "raw-import:/tmp/slz-publish-wt.a2Pyg4/packages/shared/hooks/src/notify.sh"() {
5227
5227
  notify_default = `#!/bin/sh
5228
5228
  # SlayZone agent lifecycle hook \u2014 BENIGN DUMB FORWARDER.
5229
5229
  #
@@ -5385,10 +5385,10 @@ exit 0
5385
5385
  }
5386
5386
  });
5387
5387
 
5388
- // raw-import:/tmp/slz-publish-wt.GgutIu/packages/shared/hooks/src/opencode-plugin.js
5388
+ // raw-import:/tmp/slz-publish-wt.a2Pyg4/packages/shared/hooks/src/opencode-plugin.js
5389
5389
  var opencode_plugin_default;
5390
5390
  var init_opencode_plugin = __esm({
5391
- "raw-import:/tmp/slz-publish-wt.GgutIu/packages/shared/hooks/src/opencode-plugin.js"() {
5391
+ "raw-import:/tmp/slz-publish-wt.a2Pyg4/packages/shared/hooks/src/opencode-plugin.js"() {
5392
5392
  opencode_plugin_default = "// SlayZone opencode plugin v1\n//\n// Forwards OpenCode SDK session lifecycle events to the SlayZone agent-hook\n// notify script over a local shell exec. Plugin is loaded by OpenCode from\n// ${XDG_CONFIG_HOME:-~/.config}/opencode/plugin/slayzone-notify.js\n//\n// The {{NOTIFY_PATH}} placeholder is substituted by the SlayZone installer\n// (packages/apps/app/src/main/agent-hooks/opencode-plugin-installer.ts) at\n// install time with the absolute path to `~/.slayzone/hooks/notify.sh`.\n//\n// Behavior:\n// - No-op when SLAYZONE_TASK_ID is unset (plugin only fires inside SlayZone PTYs).\n// - Subagent (child session) events are suppressed \u2014 oh-my-opencode + similar\n// tools spawn many child sessions and would otherwise flap Start/Stop.\n// - On client.session.list() error, sessions are assumed CHILD (safer to\n// suppress than emit a false-positive Start/Stop).\n// - Handles both modern (session.status w/ nested type) and legacy\n// (session.busy / session.idle) event shapes for forward compat.\n// - Singleton guard prevents double-load if OpenCode re-imports the plugin.\n\nexport const SlayzoneNotifyPlugin = async ({ $, client }) => {\n if (globalThis.__slayzoneOpencodePluginV1) return {}\n globalThis.__slayzoneOpencodePluginV1 = true\n if (!process?.env?.SLAYZONE_TASK_ID) return {}\n\n const notifyPath = '{{NOTIFY_PATH}}'\n let currentState = 'idle' // 'idle' | 'busy'\n let rootSessionID = null // first non-child session we see\n let stopSent = false // dedup Stop emits\n const childSessionCache = new Map() // sessionID -> isChild bool\n\n const notify = async (eventName) => {\n const payload = JSON.stringify({ hook_event_name: eventName })\n try {\n await $`bash ${notifyPath} ${payload}`\n } catch {\n // Hook failures must never bubble into the OpenCode TUI.\n }\n }\n\n const isChildSession = async (sessionID) => {\n if (!sessionID) return true\n if (!client?.session?.list) return true\n if (childSessionCache.has(sessionID)) return childSessionCache.get(sessionID)\n try {\n const sessions = await client.session.list()\n const session = sessions.data?.find((s) => s.id === sessionID)\n const isChild = !!session?.parentID\n childSessionCache.set(sessionID, isChild)\n return isChild\n } catch {\n // On error, assume CHILD \u2014 safer than emitting a false-positive Start/Stop.\n return true\n }\n }\n\n const handleBusy = async (sessionID) => {\n if (!rootSessionID) rootSessionID = sessionID\n if (sessionID !== rootSessionID) return\n if (currentState === 'idle') {\n currentState = 'busy'\n stopSent = false\n await notify('Start')\n }\n }\n\n const handleStop = async (sessionID) => {\n if (rootSessionID && sessionID !== rootSessionID) return\n if (currentState === 'busy' && !stopSent) {\n currentState = 'idle'\n stopSent = true\n await notify('Stop')\n rootSessionID = null // reset for next session\n }\n }\n\n return {\n event: async ({ event }) => {\n const sessionID = event.properties?.sessionID ?? event.properties?.info?.id ?? null\n\n if (event.type === 'session.created') {\n const isChild = Boolean(event.properties?.info?.parentID)\n if (sessionID) childSessionCache.set(sessionID, isChild)\n if (!isChild) await notify('SessionStart')\n return\n }\n\n if (event.type === 'session.deleted') {\n const cached = sessionID ? childSessionCache.get(sessionID) : undefined\n const isChild = cached !== undefined ? cached : await isChildSession(sessionID)\n if (!isChild) await notify('SessionEnd')\n if (sessionID) childSessionCache.delete(sessionID)\n return\n }\n\n if (await isChildSession(sessionID)) return\n\n if (event.type === 'session.status') {\n const status = event.properties?.status\n if (status?.type === 'busy') await handleBusy(sessionID)\n else if (status?.type === 'idle') await handleStop(sessionID)\n }\n // Backwards-compat: some OpenCode versions emit busy/idle as top-level events.\n if (event.type === 'session.busy') await handleBusy(sessionID)\n if (event.type === 'session.idle') await handleStop(sessionID)\n if (event.type === 'session.error') await handleStop(sessionID)\n },\n 'permission.ask': async (_permission, output) => {\n if (output.status === 'ask') await notify('PermissionRequest')\n }\n }\n}\n";
5393
5393
  }
5394
5394
  });
@@ -5431,7 +5431,7 @@ var init_agent_hooks2 = __esm({
5431
5431
  });
5432
5432
 
5433
5433
  // src/bin.ts
5434
- var import_node_os10 = require("node:os");
5434
+ var import_node_os11 = require("node:os");
5435
5435
  var import_node_path18 = require("node:path");
5436
5436
 
5437
5437
  // ../../shared/platform/src/config-prompt.ts
@@ -25569,7 +25569,7 @@ function date4(params) {
25569
25569
 
25570
25570
  // ../../domains/computer-transport/src/shared/frames.ts
25571
25571
  var COMPUTER_EXIT_NEEDS_RE_ENROLLMENT = 78;
25572
- var COMPUTER_PROTOCOL_VERSION = 3;
25572
+ var COMPUTER_PROTOCOL_VERSION = 4;
25573
25573
  var ComputerTransportErrorCodes = {
25574
25574
  /** Command is part of the contract but not implemented by this peer. */
25575
25575
  unimplemented: -32001,
@@ -25671,12 +25671,14 @@ var ComputerNotificationMethods = {
25671
25671
  };
25672
25672
  var ptyDataParamsSchema = external_exports.object({
25673
25673
  sessionId: external_exports.string().min(1),
25674
+ spawnId: external_exports.string().min(1).optional(),
25674
25675
  /** Monotonic per-session sequence number, preserved end-to-end. */
25675
25676
  seq: external_exports.number().int().nonnegative(),
25676
25677
  data: external_exports.string()
25677
25678
  });
25678
25679
  var ptyExitParamsSchema = external_exports.object({
25679
25680
  sessionId: external_exports.string().min(1),
25681
+ spawnId: external_exports.string().min(1).optional(),
25680
25682
  exitCode: external_exports.number().int().nullable(),
25681
25683
  signal: external_exports.string().nullable().optional(),
25682
25684
  /**
@@ -25828,6 +25830,9 @@ var ptySpawnParamsSchema = external_exports.object({
25828
25830
  startupTimeoutMs: external_exports.number().int().positive().optional(),
25829
25831
  command: external_exports.string().min(1),
25830
25832
  args: external_exports.array(external_exports.string()).optional(),
25833
+ /** New computers resolve their own login shell; command/args remain for older peers. */
25834
+ computerShell: external_exports.boolean().optional(),
25835
+ computerShellCommand: external_exports.string().optional(),
25831
25836
  /**
25832
25837
  * Absolute cwd. Optional now: when absent the COMPUTER resolves it from
25833
25838
  * `projectId` against its own `project.*` mapping, because the hub cannot —
@@ -25846,6 +25851,19 @@ var ptySpawnParamsSchema = external_exports.object({
25846
25851
  }).refine((p) => p.cwd !== void 0 || p.projectId !== void 0, {
25847
25852
  message: "pty.spawn needs either cwd or projectId"
25848
25853
  });
25854
+ var HUB_PTY_ENV_KEYS = [
25855
+ "SLAYZONE_TASK_ID",
25856
+ "SLAYZONE_PROJECT_ID",
25857
+ "SLAYZONE_AGENT_ID",
25858
+ "SLAYZONE_AGENT_HOOK_CONTEXT"
25859
+ ];
25860
+ function pickHubPtyEnv(env) {
25861
+ const picked = {};
25862
+ for (const key of HUB_PTY_ENV_KEYS) {
25863
+ if (env?.[key] !== void 0) picked[key] = env[key];
25864
+ }
25865
+ return picked;
25866
+ }
25849
25867
  var ptySpawnResultSchema = external_exports.object({
25850
25868
  pid: external_exports.number().int(),
25851
25869
  /** The computer armed the requested `startupTimeoutMs`. Absent from a computer
@@ -25854,10 +25872,12 @@ var ptySpawnResultSchema = external_exports.object({
25854
25872
  });
25855
25873
  var ptyKillParamsSchema = external_exports.object({
25856
25874
  sessionId: external_exports.string().min(1),
25875
+ spawnId: spawnIdField,
25857
25876
  signal: external_exports.string().optional()
25858
25877
  });
25859
25878
  var ptyPrioritizeParamsSchema = external_exports.object({
25860
- sessionId: external_exports.string().min(1)
25879
+ sessionId: external_exports.string().min(1),
25880
+ spawnId: spawnIdField
25861
25881
  });
25862
25882
  var ptyPrioritizeResultSchema = external_exports.object({
25863
25883
  ok: external_exports.literal(true),
@@ -25865,6 +25885,7 @@ var ptyPrioritizeResultSchema = external_exports.object({
25865
25885
  });
25866
25886
  var ptyBackgroundJobsParamsSchema = external_exports.object({
25867
25887
  sessionId: external_exports.string().min(1),
25888
+ spawnId: spawnIdField,
25868
25889
  jobs: external_exports.array(
25869
25890
  external_exports.object({
25870
25891
  id: external_exports.string().min(1),
@@ -25893,15 +25914,18 @@ var ptyDoctorResultSchema = external_exports.object({
25893
25914
  });
25894
25915
  var ptyResizeParamsSchema = external_exports.object({
25895
25916
  sessionId: external_exports.string().min(1),
25917
+ spawnId: spawnIdField,
25896
25918
  cols: external_exports.number().int().positive(),
25897
25919
  rows: external_exports.number().int().positive()
25898
25920
  });
25899
25921
  var ptyWriteParamsSchema = external_exports.object({
25900
25922
  sessionId: external_exports.string().min(1),
25923
+ spawnId: spawnIdField,
25901
25924
  data: external_exports.string()
25902
25925
  });
25903
25926
  var ptyGetBufferSinceParamsSchema = external_exports.object({
25904
25927
  sessionId: external_exports.string().min(1),
25928
+ spawnId: spawnIdField,
25905
25929
  /**
25906
25930
  * Exclusive lower bound — frames with `seq > this` are replayed.
25907
25931
  *
@@ -25943,6 +25967,7 @@ var ptyWarmSpawnParamsSchema = external_exports.object({
25943
25967
  warmId: external_exports.string().min(1),
25944
25968
  command: external_exports.string().min(1),
25945
25969
  args: external_exports.array(external_exports.string()).optional(),
25970
+ computerShell: external_exports.boolean().optional(),
25946
25971
  cwd: external_exports.string().min(1),
25947
25972
  env: external_exports.record(external_exports.string(), external_exports.string()).optional(),
25948
25973
  cols: external_exports.number().int().positive().optional(),
@@ -26432,6 +26457,20 @@ function wsDataToText(data) {
26432
26457
  return data.toString("utf8");
26433
26458
  }
26434
26459
 
26460
+ // ../../domains/computer-transport/src/shared/http-tunnel.ts
26461
+ var HTTP_TUNNEL_PREFIX = "cli.v1.";
26462
+ var HTTP_TUNNEL_CHUNK = 32 * 1024;
26463
+ var httpTunnelParamsSchema = external_exports.object({
26464
+ id: external_exports.string().min(1).max(100),
26465
+ data: external_exports.string().max(Math.ceil(HTTP_TUNNEL_CHUNK / 3) * 4).optional(),
26466
+ end: external_exports.boolean().optional()
26467
+ }).strict();
26468
+ var httpTunnelOpenResultSchema = external_exports.object({ id: external_exports.string().min(1).max(100) });
26469
+ var httpTunnelReadResultSchema = external_exports.object({
26470
+ data: external_exports.string().max(Math.ceil(HTTP_TUNNEL_CHUNK / 3) * 4),
26471
+ end: external_exports.boolean()
26472
+ });
26473
+
26435
26474
  // ../../domains/computer-transport/src/client/backoff.ts
26436
26475
  var DEFAULT_BACKOFF = {
26437
26476
  initialDelayMs: 100,
@@ -26925,6 +26964,64 @@ var HubDialer = class {
26925
26964
  }
26926
26965
  };
26927
26966
 
26967
+ // ../../domains/computer-transport/src/client/http-tunnel.ts
26968
+ var import_node_stream = require("node:stream");
26969
+ async function openHttpTunnel(requester) {
26970
+ const { id } = httpTunnelOpenResultSchema.parse(
26971
+ await requester.request(`${HTTP_TUNNEL_PREFIX}open`)
26972
+ );
26973
+ let reading = false;
26974
+ const socket = new import_node_stream.Duplex({
26975
+ read() {
26976
+ if (reading) return;
26977
+ reading = true;
26978
+ void (async () => {
26979
+ try {
26980
+ while (!socket.destroyed) {
26981
+ const result = httpTunnelReadResultSchema.parse(
26982
+ await requester.request(`${HTTP_TUNNEL_PREFIX}read`, { id })
26983
+ );
26984
+ const more = result.data ? socket.push(Buffer.from(result.data, "base64")) : true;
26985
+ if (result.end) {
26986
+ socket.push(null);
26987
+ break;
26988
+ }
26989
+ if (!more) break;
26990
+ }
26991
+ } catch (error62) {
26992
+ socket.destroy(error62 instanceof Error ? error62 : new Error(String(error62)));
26993
+ } finally {
26994
+ reading = false;
26995
+ }
26996
+ })();
26997
+ },
26998
+ write(chunk, _encoding, done) {
26999
+ void (async () => {
27000
+ for (let offset = 0; offset < chunk.length; offset += HTTP_TUNNEL_CHUNK) {
27001
+ await requester.request(`${HTTP_TUNNEL_PREFIX}write`, {
27002
+ id,
27003
+ data: chunk.subarray(offset, offset + HTTP_TUNNEL_CHUNK).toString("base64")
27004
+ });
27005
+ }
27006
+ })().then(() => done(), done);
27007
+ },
27008
+ final(done) {
27009
+ void requester.request(`${HTTP_TUNNEL_PREFIX}write`, { id, end: true }).then(() => done(), done);
27010
+ },
27011
+ destroy(error62, done) {
27012
+ void requester.request(`${HTTP_TUNNEL_PREFIX}close`, { id }).catch(() => {
27013
+ });
27014
+ done(error62);
27015
+ }
27016
+ });
27017
+ Object.assign(socket, {
27018
+ setTimeout: () => socket,
27019
+ setNoDelay: () => socket,
27020
+ setKeepAlive: () => socket
27021
+ });
27022
+ return socket;
27023
+ }
27024
+
26928
27025
  // src/config.ts
26929
27026
  var import_node_fs2 = require("node:fs");
26930
27027
  var import_node_os2 = require("node:os");
@@ -27446,8 +27543,10 @@ function assertHubLocalExecEnabled(refusal) {
27446
27543
 
27447
27544
  // ../../shared/platform/src/env-manifest.ts
27448
27545
  var ENV_MANIFEST = {
27449
- // --- secret: credentials/tokens, never in a terminal ---
27546
+ // --- secret: credentials/tokens, never inherited into a terminal ---
27450
27547
  SLAYZONE_HUB_TOKEN: "secret",
27548
+ SLAYZONE_CLI_RELAY_TOKEN: "secret",
27549
+ // computer-issued capability, re-added per session
27451
27550
  // HMAC signing key for ALL of hub-auth (better-auth's session/cookie signer AND
27452
27551
  // the computer enroll/api-key credentials) — hence `HUB_AUTH`, not the former
27453
27552
  // `HUB_COMPUTER_TRANSPORT_SECRET`, which named just one of its consumers. Rule 2
@@ -27482,6 +27581,8 @@ var ENV_MANIFEST = {
27482
27581
  // name refuses to boot. Stripping both here keeps a terminal's dial address from
27483
27582
  // ever being mistaken for another process's listen address.
27484
27583
  SLAYZONE_HUB_ADDRESS: "infra",
27584
+ SLAYZONE_CLI_RELAY_ADDRESS: "infra",
27585
+ // computer loopback; re-added per session
27485
27586
  SLAYZONE_HUB_LISTEN_ADDRESS: "infra",
27486
27587
  // hub-only; where the hub listens
27487
27588
  SLAYZONE_HUB_PUBLIC_ADDRESS: "infra",
@@ -28089,6 +28190,155 @@ function createAgentHookServer(ctx) {
28089
28190
  });
28090
28191
  }
28091
28192
 
28193
+ // src/cli-relay.ts
28194
+ var import_node_http2 = __toESM(require("node:http"), 1);
28195
+ var import_node_crypto3 = require("node:crypto");
28196
+ function responseHeaders(headers) {
28197
+ const excluded = /* @__PURE__ */ new Set([
28198
+ "connection",
28199
+ "keep-alive",
28200
+ "proxy-authenticate",
28201
+ "proxy-authorization",
28202
+ "proxy-connection",
28203
+ "te",
28204
+ "trailer",
28205
+ "transfer-encoding",
28206
+ "upgrade",
28207
+ ...(headers.connection ?? "").split(",").map((name) => name.trim().toLowerCase())
28208
+ ]);
28209
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => !excluded.has(name)));
28210
+ }
28211
+ async function createComputerCliRelay(requester) {
28212
+ const tokens = /* @__PURE__ */ new Map();
28213
+ const responses = /* @__PURE__ */ new Set();
28214
+ const active = /* @__PURE__ */ new Set();
28215
+ const server = import_node_http2.default.createServer((req, res) => {
28216
+ const token = req.headers.authorization?.replace(/^Bearer /, "") ?? "";
28217
+ const requests = tokens.get(token);
28218
+ if (!requests) {
28219
+ res.writeHead(401, { "content-type": "application/json" }).end(
28220
+ JSON.stringify({
28221
+ error: "CLI relay capability is missing or expired. Open a new task terminal."
28222
+ })
28223
+ );
28224
+ return;
28225
+ }
28226
+ if (responses.size >= 32) {
28227
+ res.writeHead(429).end("Too many CLI relay requests");
28228
+ return;
28229
+ }
28230
+ let upstream;
28231
+ let receivedResponse = false;
28232
+ let cancelled = false;
28233
+ responses.add(res);
28234
+ requests.add(res);
28235
+ res.on("close", () => {
28236
+ responses.delete(res);
28237
+ requests.delete(res);
28238
+ cancelled = true;
28239
+ upstream?.destroy();
28240
+ });
28241
+ void openHttpTunnel(requester).then((socket) => {
28242
+ if (cancelled || !tokens.has(token)) {
28243
+ socket.destroy();
28244
+ return;
28245
+ }
28246
+ const headers = { connection: "close" };
28247
+ for (const name of ["content-type", "content-length", "accept", "last-event-id"]) {
28248
+ if (req.headers[name] !== void 0) headers[name] = req.headers[name];
28249
+ }
28250
+ const agent = new import_node_http2.default.Agent({ keepAlive: false });
28251
+ agent.createConnection = () => socket;
28252
+ upstream = import_node_http2.default.request(
28253
+ {
28254
+ host: "relay",
28255
+ path: req.url,
28256
+ method: req.method,
28257
+ headers,
28258
+ agent
28259
+ },
28260
+ (response) => {
28261
+ receivedResponse = true;
28262
+ const stopUpload = () => {
28263
+ req.unpipe(upstream);
28264
+ req.resume();
28265
+ };
28266
+ if ((response.statusCode ?? 502) >= 400) stopUpload();
28267
+ response.on("end", stopUpload);
28268
+ res.writeHead(response.statusCode ?? 502, responseHeaders(response.headers));
28269
+ response.on("error", () => res.destroy());
28270
+ response.pipe(res);
28271
+ }
28272
+ );
28273
+ active.add(upstream);
28274
+ const current = upstream;
28275
+ upstream.on("close", () => {
28276
+ active.delete(current);
28277
+ agent.destroy();
28278
+ });
28279
+ upstream.on("error", () => {
28280
+ if (receivedResponse) return;
28281
+ if (res.headersSent) res.destroy();
28282
+ else
28283
+ res.writeHead(503, { "content-type": "application/json" }).end(
28284
+ JSON.stringify({
28285
+ error: "CLI relay lost its hub connection. Check the computer connection, then retry."
28286
+ })
28287
+ );
28288
+ });
28289
+ req.on("aborted", () => upstream?.destroy());
28290
+ req.pipe(upstream);
28291
+ }).catch(() => {
28292
+ if (!res.headersSent)
28293
+ res.writeHead(503, { "content-type": "application/json" }).end(
28294
+ JSON.stringify({
28295
+ error: "CLI relay unavailable. Check the computer connection and update hub and computer together."
28296
+ })
28297
+ );
28298
+ });
28299
+ });
28300
+ await new Promise((resolve9, reject) => {
28301
+ server.once("error", reject);
28302
+ server.listen(0, "127.0.0.1", resolve9);
28303
+ });
28304
+ const address = server.address();
28305
+ if (!address || typeof address === "string") throw new Error("CLI relay failed to bind");
28306
+ const disconnect = () => {
28307
+ for (const response of responses) {
28308
+ if (response.headersSent) response.destroy();
28309
+ else
28310
+ response.writeHead(503, { "content-type": "application/json" }).end(
28311
+ JSON.stringify({
28312
+ error: "CLI relay lost its hub connection. Check the computer connection, then retry."
28313
+ })
28314
+ );
28315
+ }
28316
+ for (const request of active) request.destroy(new Error("hub disconnected"));
28317
+ };
28318
+ return {
28319
+ url: `http://127.0.0.1:${address.port}`,
28320
+ issue() {
28321
+ const token = (0, import_node_crypto3.randomBytes)(32).toString("hex");
28322
+ const requests = /* @__PURE__ */ new Set();
28323
+ tokens.set(token, requests);
28324
+ return {
28325
+ token,
28326
+ revoke() {
28327
+ tokens.delete(token);
28328
+ for (const response of requests) response.destroy();
28329
+ }
28330
+ };
28331
+ },
28332
+ disconnect,
28333
+ async close() {
28334
+ tokens.clear();
28335
+ disconnect();
28336
+ server.closeAllConnections();
28337
+ await new Promise((resolve9) => server.close(() => resolve9()));
28338
+ }
28339
+ };
28340
+ }
28341
+
28092
28342
  // src/handlers/fs.ts
28093
28343
  var import_node_fs7 = require("node:fs");
28094
28344
  var import_promises7 = require("node:fs/promises");
@@ -31613,18 +31863,37 @@ function resolveCommand(command, env, cwd) {
31613
31863
  }
31614
31864
 
31615
31865
  // src/handlers/spawn-env.ts
31616
- function buildSpawnEnv(ctx, overrides) {
31866
+ var import_node_os9 = require("node:os");
31867
+ function buildSpawnEnv(ctx, overrides, capability) {
31617
31868
  const merged = overrides ? { ...sanitizeSpawnEnv(process.env), ...overrides } : sanitizeSpawnEnv(process.env);
31618
31869
  if (ctx.agentHookUrl) {
31619
31870
  merged.SLAYZONE_AGENT_HOOK_URL = ctx.agentHookUrl;
31620
31871
  delete merged.SLAYZONE_HUB_TOKEN;
31621
31872
  }
31622
31873
  if (ctx.hubAddress) merged.SLAYZONE_HUB_ADDRESS = ctx.hubAddress;
31874
+ delete merged.SLAYZONE_CLI_RELAY_ADDRESS;
31875
+ delete merged.SLAYZONE_CLI_RELAY_TOKEN;
31876
+ if (capability && ctx.cliRelay) {
31877
+ delete merged.SLAYZONE_HUB_TOKEN;
31878
+ merged.SLAYZONE_CLI_RELAY_ADDRESS = new URL(ctx.cliRelay.url).host;
31879
+ merged.SLAYZONE_CLI_RELAY_TOKEN = capability.token;
31880
+ }
31623
31881
  if (ctx.config.binOverrideDir) {
31624
31882
  merged.PATH = mergePathLists(ctx.config.binOverrideDir, merged.PATH);
31625
31883
  }
31626
31884
  return merged;
31627
31885
  }
31886
+ function buildPtySpawnEnv(ctx, hubEnv, capability) {
31887
+ const env = buildSpawnEnv(ctx, pickHubPtyEnv(hubEnv), capability);
31888
+ const account = (0, import_node_os9.userInfo)();
31889
+ env.HOME = account.homedir;
31890
+ env.USER = account.username;
31891
+ env.LOGNAME = account.username;
31892
+ env.SHELL = resolveUserShell();
31893
+ env.TERM = "xterm-256color";
31894
+ env.COLORTERM = "truecolor";
31895
+ return env;
31896
+ }
31628
31897
 
31629
31898
  // src/handlers/proc.ts
31630
31899
  var MAX_BUFFER_SIZE = 750 * 1024;
@@ -31717,11 +31986,14 @@ function createProcHandlers(ctx, getProjectPaths, options = {}) {
31717
31986
  const cwd = spawnCwd(resolved, ctx.config.allowedRoots);
31718
31987
  const existing = procs.get(params.sessionId);
31719
31988
  if (existing) {
31989
+ existing.revokeCli?.();
31720
31990
  killTree(existing.child);
31721
31991
  procs.delete(params.sessionId);
31722
31992
  }
31723
- const env = buildSpawnEnv(ctx, params.env);
31993
+ const capability = ctx.cliRelay?.issue();
31994
+ const env = buildSpawnEnv(ctx, params.env, capability);
31724
31995
  if (!params.shell && resolveCommand(params.command, env, cwd) === null) {
31996
+ capability?.revoke();
31725
31997
  throw refuse2(
31726
31998
  ProcRefusalReasons.binaryNotFound,
31727
31999
  `${params.command} was not found on this computer's PATH`,
@@ -31729,17 +32001,24 @@ function createProcHandlers(ctx, getProjectPaths, options = {}) {
31729
32001
  );
31730
32002
  }
31731
32003
  const invocation = params.shell ? buildShellInvocation(params.command) : { file: params.command, args: params.args ?? [] };
31732
- const child = (0, import_node_child_process8.spawn)(invocation.file, invocation.args, {
31733
- ...cwd ? { cwd } : {},
31734
- env,
31735
- // stdin PIPED (was 'ignore') so `proc.write` can reach the child.
31736
- stdio: ["pipe", "pipe", "pipe"],
31737
- // Its own process group, so a kill reaches everything it started (`pnpm dev`
31738
- // → node → …), not just the shell — see `killTree`.
31739
- detached: PROCESS_TREE_DETACHED
31740
- });
32004
+ let child;
32005
+ try {
32006
+ child = (0, import_node_child_process8.spawn)(invocation.file, invocation.args, {
32007
+ ...cwd ? { cwd } : {},
32008
+ env,
32009
+ // stdin PIPED (was 'ignore') so `proc.write` can reach the child.
32010
+ stdio: ["pipe", "pipe", "pipe"],
32011
+ // Its own process group, so a kill reaches everything it started (`pnpm dev`
32012
+ // → node → …), not just the shell — see `killTree`.
32013
+ detached: PROCESS_TREE_DETACHED
32014
+ });
32015
+ } catch (error62) {
32016
+ capability?.revoke();
32017
+ throw error62;
32018
+ }
31741
32019
  const buffer = new RingBuffer(MAX_BUFFER_SIZE);
31742
32020
  const session = {
32021
+ revokeCli: capability?.revoke,
31743
32022
  child,
31744
32023
  buffer,
31745
32024
  ...params.spawnId !== void 0 ? { spawnId: params.spawnId } : {},
@@ -31754,6 +32033,7 @@ function createProcHandlers(ctx, getProjectPaths, options = {}) {
31754
32033
  ctx.log("proc stdin error", { sessionId: params.sessionId, error: err.message });
31755
32034
  });
31756
32035
  const settle2 = (payload) => {
32036
+ capability?.revoke();
31757
32037
  releaseSlot(session);
31758
32038
  if (procs.get(params.sessionId) !== session) return;
31759
32039
  procs.delete(params.sessionId);
@@ -31914,6 +32194,7 @@ function createProcHandlers(ctx, getProjectPaths, options = {}) {
31914
32194
  }
31915
32195
  queued.clear();
31916
32196
  for (const [sessionId, session] of procs) {
32197
+ session.revokeCli?.();
31917
32198
  releaseSlot(session);
31918
32199
  killTree(session.child);
31919
32200
  ctx.log("proc disposed on shutdown", { sessionId });
@@ -32179,6 +32460,72 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32179
32460
  const spawnPty = options.spawnPty ?? pty.spawn;
32180
32461
  const installProblems2 = options.nodePtyInstallProblems ?? installProblemsOnDisk;
32181
32462
  const queued = /* @__PURE__ */ new Map();
32463
+ const starts = /* @__PURE__ */ new Map();
32464
+ function serializeStart(id, spawnId, run) {
32465
+ const prior = starts.get(id)?.promise ?? Promise.resolve();
32466
+ const record2 = { spawnId, promise: void 0, cancelled: false };
32467
+ const assertActive = () => {
32468
+ if (record2.cancelled) throw new Error(`pty ${id}: spawn killed before start`);
32469
+ };
32470
+ const current = prior.catch(() => {
32471
+ }).then(() => {
32472
+ assertActive();
32473
+ return run(assertActive);
32474
+ });
32475
+ record2.promise = current;
32476
+ starts.set(id, record2);
32477
+ void current.finally(() => {
32478
+ if (starts.get(id)?.promise === current) starts.delete(id);
32479
+ }).catch(() => {
32480
+ });
32481
+ return current;
32482
+ }
32483
+ async function waitForExit(session, timeoutMs) {
32484
+ let timer;
32485
+ try {
32486
+ return await Promise.race([
32487
+ session.exited.then(() => true),
32488
+ new Promise((resolve9) => {
32489
+ timer = setTimeout(() => resolve9(false), timeoutMs);
32490
+ timer.unref?.();
32491
+ })
32492
+ ]);
32493
+ } finally {
32494
+ if (timer) clearTimeout(timer);
32495
+ }
32496
+ }
32497
+ function stopPty(session, signal) {
32498
+ if (session.stopping) return session.stopping;
32499
+ session.stopping = (async () => {
32500
+ try {
32501
+ session.proc.kill(signal);
32502
+ } catch {
32503
+ }
32504
+ if (await waitForExit(session, 2e3)) return;
32505
+ try {
32506
+ session.proc.kill("SIGKILL");
32507
+ } catch {
32508
+ }
32509
+ if (!await waitForExit(session, 2e3)) {
32510
+ throw new Error(`pty ${session.id}: process ${session.proc.pid} did not exit`);
32511
+ }
32512
+ })();
32513
+ void session.stopping.catch(() => {
32514
+ session.stopping = void 0;
32515
+ });
32516
+ return session.stopping;
32517
+ }
32518
+ async function stopBeforeReplacement(id) {
32519
+ const old = sessions.get(id);
32520
+ if (!old) return;
32521
+ old.replacing = true;
32522
+ try {
32523
+ await stopPty(old);
32524
+ } catch (err) {
32525
+ old.replacing = false;
32526
+ throw err;
32527
+ }
32528
+ }
32182
32529
  function resolveCwd(opts) {
32183
32530
  if (opts.cwd) return spawnCwd(opts.cwd, ctx.config.allowedRoots);
32184
32531
  if (!opts.projectId) throw new Error("pty.spawn needs either cwd or projectId");
@@ -32188,33 +32535,31 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32188
32535
  }
32189
32536
  function spawnSession(id, opts) {
32190
32537
  const cwd = resolveCwd(opts);
32191
- const existing = sessions.get(id);
32192
- if (existing) {
32193
- existing.deadline?.cancel();
32194
- releaseAdmission(existing);
32195
- try {
32196
- existing.proc.kill();
32197
- } catch {
32198
- }
32199
- sessions.delete(id);
32200
- }
32538
+ if (sessions.has(id)) throw new Error(`pty ${id}: old process still live`);
32539
+ const capability = ctx.cliRelay?.issue();
32201
32540
  let proc;
32202
32541
  try {
32203
32542
  proc = spawnPty(opts.command, opts.args ?? [], {
32204
32543
  name: "xterm-color",
32205
32544
  cwd,
32206
- env: buildSpawnEnv(ctx, opts.env),
32545
+ env: buildPtySpawnEnv(ctx, opts.env, capability),
32207
32546
  cols: opts.cols ?? 80,
32208
32547
  rows: opts.rows ?? 24
32209
32548
  });
32210
32549
  } catch (err) {
32550
+ capability?.revoke();
32211
32551
  throw explainSpawnFailure(err);
32212
32552
  }
32213
32553
  const buffer = new RingBuffer(MAX_BUFFER_SIZE2);
32554
+ let resolveExit;
32555
+ const exited = new Promise((resolve9) => resolveExit = resolve9);
32214
32556
  const session = {
32557
+ revokeCli: capability?.revoke,
32215
32558
  proc,
32216
32559
  buffer,
32217
32560
  id,
32561
+ exited,
32562
+ resolveExit,
32218
32563
  ...opts.warm ? { warm: opts.warm } : {},
32219
32564
  ...opts.spawnId !== void 0 ? { spawnId: opts.spawnId } : {}
32220
32565
  };
@@ -32233,11 +32578,14 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32233
32578
  if (session.warm) return;
32234
32579
  ctx.dialer.notify(ComputerNotificationMethods.ptyData, {
32235
32580
  sessionId: session.id,
32581
+ ...session.spawnId ? { spawnId: session.spawnId } : {},
32236
32582
  seq,
32237
32583
  data
32238
32584
  });
32239
32585
  });
32240
32586
  proc.onExit(({ exitCode, signal }) => {
32587
+ capability?.revoke();
32588
+ session.resolveExit();
32241
32589
  reap(
32242
32590
  session,
32243
32591
  typeof exitCode === "number" ? exitCode : null,
@@ -32263,6 +32611,16 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32263
32611
  sessions.delete(session.id);
32264
32612
  session.deadline?.cancel();
32265
32613
  releaseAdmission(session);
32614
+ if (session.replacing) {
32615
+ tombstones.add(session.id, {
32616
+ spawnId: session.spawnId,
32617
+ pid: session.proc.pid,
32618
+ exitCode,
32619
+ signal,
32620
+ reason: "replaced"
32621
+ });
32622
+ return;
32623
+ }
32266
32624
  if (session.warm) {
32267
32625
  ctx.log("warm pty exited before adoption", { warmId: session.id });
32268
32626
  return;
@@ -32270,6 +32628,7 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32270
32628
  const reason = session.exitReason;
32271
32629
  const delivered = ctx.dialer.notify(ComputerNotificationMethods.ptyExit, {
32272
32630
  sessionId: session.id,
32631
+ ...session.spawnId ? { spawnId: session.spawnId } : {},
32273
32632
  exitCode,
32274
32633
  signal,
32275
32634
  ...reason ? { reason } : {}
@@ -32305,11 +32664,20 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32305
32664
  slot.release();
32306
32665
  }
32307
32666
  function startSession(params) {
32308
- const session = spawnSession(params.sessionId, params);
32667
+ const shell = params.computerShell ? resolveUserShell() : params.command;
32668
+ const args = params.computerShell ? [
32669
+ ...getShellStartupArgs(shell),
32670
+ ...params.computerShellCommand ? ["-c", params.computerShellCommand] : []
32671
+ ] : params.args;
32672
+ const session = spawnSession(params.sessionId, {
32673
+ ...params,
32674
+ command: shell,
32675
+ args
32676
+ });
32309
32677
  ctx.log("pty spawned", {
32310
32678
  sessionId: params.sessionId,
32311
32679
  pid: session.proc.pid,
32312
- command: params.command
32680
+ command: shell
32313
32681
  });
32314
32682
  return {
32315
32683
  pid: session.proc.pid,
@@ -32361,16 +32729,26 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32361
32729
  }
32362
32730
  const waitedMs = Date.now() - enqueuedAt;
32363
32731
  if (waitedMs >= 1e3) ctx.log("agent start admitted after queueing", { id, lane, waitedMs });
32364
- item.resolve(reply);
32732
+ Promise.resolve(reply).then(item.resolve, (err) => {
32733
+ slot.release();
32734
+ item.reject(err);
32735
+ });
32365
32736
  });
32366
32737
  return promise2;
32367
32738
  }
32368
32739
  function spawn6(rawParams) {
32369
32740
  const params = ptySpawnParamsSchema.parse(rawParams);
32741
+ const starting = starts.get(params.sessionId);
32742
+ if (starting && params.spawnId !== void 0 && starting.spawnId === params.spawnId) {
32743
+ return starting.promise;
32744
+ }
32370
32745
  if (params.spawnId !== void 0) {
32371
32746
  const live = sessions.get(params.sessionId);
32372
32747
  if (live && !live.warm && live.spawnId === params.spawnId) {
32373
- return { pid: live.proc.pid, ...live.deadline ? { startupDeadline: true } : {} };
32748
+ return {
32749
+ pid: live.proc.pid,
32750
+ ...live.deadline ? { startupDeadline: true } : {}
32751
+ };
32374
32752
  }
32375
32753
  if (tombstones.ran(params.sessionId, params.spawnId)) {
32376
32754
  throw new Error(
@@ -32380,7 +32758,11 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32380
32758
  }
32381
32759
  if (params.admission === void 0) {
32382
32760
  const prior = queued.get(params.sessionId);
32383
- const reply = startSession(params);
32761
+ const reply = sessions.has(params.sessionId) || starts.has(params.sessionId) ? serializeStart(params.sessionId, params.spawnId, async (assertActive) => {
32762
+ await stopBeforeReplacement(params.sessionId);
32763
+ assertActive();
32764
+ return startSession(params);
32765
+ }) : startSession(params);
32384
32766
  if (prior) {
32385
32767
  queued.delete(params.sessionId);
32386
32768
  admission.cancel(prior.ticket);
@@ -32388,11 +32770,15 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32388
32770
  }
32389
32771
  return reply;
32390
32772
  }
32391
- return enqueueSpawn(params.sessionId, params.admission, params, (slot, item) => {
32392
- const reply = startSession({
32393
- ...params,
32394
- ...item.cols !== void 0 ? { cols: item.cols } : {},
32395
- ...item.rows !== void 0 ? { rows: item.rows } : {}
32773
+ return enqueueSpawn(params.sessionId, params.admission, params, async (slot, item) => {
32774
+ const reply = await serializeStart(params.sessionId, params.spawnId, async (assertActive) => {
32775
+ await stopBeforeReplacement(params.sessionId);
32776
+ assertActive();
32777
+ return startSession({
32778
+ ...params,
32779
+ ...item.cols !== void 0 ? { cols: item.cols } : {},
32780
+ ...item.rows !== void 0 ? { rows: item.rows } : {}
32781
+ });
32396
32782
  });
32397
32783
  const session = sessions.get(params.sessionId);
32398
32784
  session.admissionSlot = slot;
@@ -32401,18 +32787,28 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32401
32787
  });
32402
32788
  }
32403
32789
  function startWarm(params, cols, rows) {
32790
+ const shell = params.computerShell ? resolveUserShell() : params.command;
32404
32791
  const session = spawnSession(params.warmId, {
32405
32792
  ...params,
32793
+ command: shell,
32794
+ args: params.computerShell ? getShellStartupArgs(shell) : params.args,
32406
32795
  ...cols !== void 0 ? { cols } : {},
32407
32796
  ...rows !== void 0 ? { rows } : {},
32408
32797
  warm: { cwd: params.cwd, startedAt: Date.now() }
32409
32798
  });
32410
32799
  if (params.postSpawnCommand) session.proc.write(`${params.postSpawnCommand}\r`);
32411
- ctx.log("warm pty spawned", { warmId: params.warmId, pid: session.proc.pid });
32800
+ ctx.log("warm pty spawned", {
32801
+ warmId: params.warmId,
32802
+ pid: session.proc.pid
32803
+ });
32412
32804
  return session;
32413
32805
  }
32414
32806
  function warmSpawn(rawParams) {
32415
32807
  const params = ptyWarmSpawnParamsSchema.parse(rawParams);
32808
+ const live = sessions.get(params.warmId);
32809
+ if (live?.warm) return Promise.resolve({ pid: live.proc.pid });
32810
+ const waiting = queued.get(params.warmId);
32811
+ if (waiting) return waiting.promise;
32416
32812
  return enqueueSpawn(params.warmId, "warm", params, (slot, item) => {
32417
32813
  const session = startWarm(params, item.cols, item.rows);
32418
32814
  session.admissionSlot = slot;
@@ -32431,11 +32827,15 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32431
32827
  function prioritize(rawParams) {
32432
32828
  const params = ptyPrioritizeParamsSchema.parse(rawParams);
32433
32829
  const item = queued.get(params.sessionId);
32434
- return { ok: true, queued: item ? admission.prioritize(item.ticket) : false };
32830
+ return {
32831
+ ok: true,
32832
+ queued: item && (params.spawnId === void 0 || item.spawnId === params.spawnId) ? admission.prioritize(item.ticket) : false
32833
+ };
32435
32834
  }
32436
32835
  async function backgroundJobs(rawParams) {
32437
32836
  const params = ptyBackgroundJobsParamsSchema.parse(rawParams);
32438
- const pid = sessions.get(params.sessionId)?.proc.pid;
32837
+ const session = sessions.get(params.sessionId);
32838
+ const pid = session && (params.spawnId === void 0 || session.spawnId === params.spawnId) ? session.proc.pid : void 0;
32439
32839
  if (pid === void 0) return { ok: true, supported: true, alive: [] };
32440
32840
  const alive = await probeBackgroundJobs(pid, params.jobs);
32441
32841
  return alive === null ? { ok: true, supported: false, alive: [] } : { ok: true, supported: true, alive };
@@ -32443,14 +32843,17 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32443
32843
  async function doctor(rawParams) {
32444
32844
  const params = ptyDoctorParamsSchema.parse(rawParams);
32445
32845
  await options.pathReady;
32446
- const env = buildSpawnEnv(ctx);
32846
+ const env = buildPtySpawnEnv(ctx);
32447
32847
  const binaries = await Promise.all(
32448
- params.binaries.map(async (binary) => ({ binary, path: await findOnPath(binary, env) }))
32848
+ params.binaries.map(async (binary) => ({
32849
+ binary,
32850
+ path: await findOnPath(binary, env)
32851
+ }))
32449
32852
  );
32450
32853
  return {
32451
32854
  ok: true,
32452
32855
  problems: installProblems2(),
32453
- shellExists: shellExists(params.shell),
32856
+ shellExists: shellExists(resolveUserShell()),
32454
32857
  binaries
32455
32858
  };
32456
32859
  }
@@ -32459,6 +32862,14 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32459
32862
  }
32460
32863
  function warmAdopt(rawParams) {
32461
32864
  const params = ptyWarmAdoptParamsSchema.parse(rawParams);
32865
+ const adopted2 = sessions.get(params.sessionId);
32866
+ if (adopted2?.spawnId === params.warmId) {
32867
+ return {
32868
+ pid: adopted2.proc.pid,
32869
+ data: adopted2.buffer.toString(),
32870
+ seq: adopted2.buffer.getCurrentSeq()
32871
+ };
32872
+ }
32462
32873
  const session = sessions.get(params.warmId);
32463
32874
  if (!session || !session.warm) {
32464
32875
  throw new RpcError(
@@ -32468,16 +32879,15 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32468
32879
  }
32469
32880
  const displaced = sessions.get(params.sessionId);
32470
32881
  if (displaced) {
32471
- ctx.log("warm adopt displaced a live session", {
32472
- warmId: params.warmId,
32473
- sessionId: params.sessionId,
32474
- displacedPid: displaced.proc.pid,
32475
- displacedWarm: !!displaced.warm
32476
- });
32882
+ throw new RpcError(
32883
+ ComputerTransportErrorCodes.unknownComputer,
32884
+ `session ${params.sessionId} already has live process ${displaced.proc.pid}`
32885
+ );
32477
32886
  }
32478
32887
  sessions.delete(params.warmId);
32479
32888
  session.id = params.sessionId;
32480
32889
  delete session.warm;
32890
+ session.spawnId = params.warmId;
32481
32891
  sessions.set(params.sessionId, session);
32482
32892
  releaseAdmission(session);
32483
32893
  const data = session.buffer.toString();
@@ -32489,16 +32899,12 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32489
32899
  });
32490
32900
  return { pid: session.proc.pid, data, seq };
32491
32901
  }
32492
- function warmKill(rawParams) {
32902
+ async function warmKill(rawParams) {
32493
32903
  const params = ptyWarmKillParamsSchema.parse(rawParams);
32494
32904
  cancelQueued(params.warmId, "killed");
32495
32905
  const session = sessions.get(params.warmId);
32496
32906
  if (!session) return { ok: true };
32497
- sessions.delete(params.warmId);
32498
- try {
32499
- session.proc.kill();
32500
- } catch {
32501
- }
32907
+ await stopPty(session);
32502
32908
  return { ok: true };
32503
32909
  }
32504
32910
  function warmList() {
@@ -32514,58 +32920,55 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32514
32920
  }
32515
32921
  return { warms };
32516
32922
  }
32517
- function kill(rawParams) {
32923
+ async function kill(rawParams) {
32518
32924
  const params = ptyKillParamsSchema.parse(rawParams);
32519
- const waiting = queued.get(params.sessionId);
32520
- if (waiting) {
32925
+ const starting = starts.get(params.sessionId);
32926
+ if (starting && (params.spawnId === void 0 || starting.spawnId === params.spawnId)) {
32927
+ starting.cancelled = true;
32928
+ }
32929
+ const live = sessions.get(params.sessionId);
32930
+ const pending = queued.get(params.sessionId);
32931
+ if (pending && (params.spawnId === void 0 || pending.spawnId === params.spawnId)) {
32521
32932
  const delivered = ctx.dialer.notify(ComputerNotificationMethods.ptyExit, {
32522
32933
  sessionId: params.sessionId,
32934
+ ...pending.spawnId ? { spawnId: pending.spawnId } : {},
32523
32935
  exitCode: null,
32524
32936
  signal: params.signal ?? "SIGHUP",
32525
32937
  reason: "killed-before-start"
32526
32938
  });
32527
32939
  tombstones.add(params.sessionId, {
32528
- spawnId: waiting.spawnId,
32940
+ spawnId: pending.spawnId,
32529
32941
  exitCode: null,
32530
32942
  signal: params.signal ?? "SIGHUP",
32531
32943
  reason: "killed-before-start"
32532
32944
  });
32533
32945
  if (!delivered)
32534
- ctx.log("kill of a queued start not delivered", { sessionId: params.sessionId });
32946
+ ctx.log("kill of a queued start not delivered", {
32947
+ sessionId: params.sessionId
32948
+ });
32535
32949
  cancelQueued(params.sessionId, "killed");
32536
- const superseded = sessions.get(params.sessionId);
32537
- if (superseded) {
32538
- sessions.delete(params.sessionId);
32539
- superseded.deadline?.cancel();
32540
- releaseAdmission(superseded);
32541
- try {
32542
- superseded.proc.kill(params.signal);
32543
- } catch {
32544
- }
32545
- }
32950
+ if (live && params.spawnId === void 0) await stopPty(live, params.signal);
32546
32951
  return { ok: true };
32547
32952
  }
32548
- const session = sessions.get(params.sessionId);
32549
- if (!session) {
32953
+ if (!live || params.spawnId !== void 0 && live.spawnId !== params.spawnId) {
32550
32954
  ctx.log("pty kill on unknown session", { sessionId: params.sessionId });
32551
32955
  return { ok: true };
32552
32956
  }
32553
- try {
32554
- session.proc.kill(params.signal);
32555
- } catch (err) {
32556
- ctx.log("pty kill failed", { sessionId: params.sessionId, error: String(err) });
32557
- }
32957
+ await stopPty(live, params.signal);
32558
32958
  return { ok: true };
32559
32959
  }
32560
32960
  function resize(rawParams) {
32561
32961
  const params = ptyResizeParamsSchema.parse(rawParams);
32562
32962
  const waiting = queued.get(params.sessionId);
32563
32963
  if (waiting) {
32964
+ if (params.spawnId !== void 0 && waiting.spawnId !== params.spawnId) return { ok: true };
32564
32965
  waiting.cols = params.cols;
32565
32966
  waiting.rows = params.rows;
32566
32967
  return { ok: true };
32567
32968
  }
32568
32969
  const session = sessions.get(params.sessionId);
32970
+ if (session && params.spawnId !== void 0 && session.spawnId !== params.spawnId)
32971
+ return { ok: true };
32569
32972
  if (!session) {
32570
32973
  ctx.log("pty resize on unknown session", { sessionId: params.sessionId });
32571
32974
  return { ok: true };
@@ -32577,10 +32980,13 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32577
32980
  const params = ptyWriteParamsSchema.parse(rawParams);
32578
32981
  const waiting = queued.get(params.sessionId);
32579
32982
  if (waiting) {
32983
+ if (params.spawnId !== void 0 && waiting.spawnId !== params.spawnId) return { ok: true };
32580
32984
  waiting.writes.push(params.data);
32581
32985
  return { ok: true };
32582
32986
  }
32583
32987
  const session = sessions.get(params.sessionId);
32988
+ if (session && params.spawnId !== void 0 && session.spawnId !== params.spawnId)
32989
+ return { ok: true };
32584
32990
  if (!session) {
32585
32991
  ctx.log("pty write on unknown session", { sessionId: params.sessionId });
32586
32992
  return { ok: true };
@@ -32606,7 +33012,9 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32606
33012
  }
32607
33013
  function getBufferSince(rawParams) {
32608
33014
  const params = ptyGetBufferSinceParamsSchema.parse(rawParams);
32609
- const buffer = sessions.get(params.sessionId)?.buffer ?? tombstones.get(params.sessionId)?.buffer;
33015
+ const live = sessions.get(params.sessionId);
33016
+ const exited = tombstones.get(params.sessionId);
33017
+ const buffer = live && (params.spawnId === void 0 || live.spawnId === params.spawnId) ? live.buffer : exited && (params.spawnId === void 0 || exited.spawnId === params.spawnId) ? exited.buffer : void 0;
32610
33018
  if (!buffer) return { frames: [] };
32611
33019
  const frames = buffer.getChunksSince(params.seq).map((c) => ({ seq: c.seq, data: c.data }));
32612
33020
  return { frames };
@@ -32614,6 +33022,7 @@ function createPtyHandlers(ctx, getProjectPaths, options = {}) {
32614
33022
  function disposeAll() {
32615
33023
  dropQueued("computer shutting down");
32616
33024
  for (const [sessionId, session] of sessions) {
33025
+ session.revokeCli?.();
32617
33026
  session.deadline?.cancel();
32618
33027
  releaseAdmission(session);
32619
33028
  try {
@@ -32700,12 +33109,12 @@ function createComputerConfigHandlers(ctx) {
32700
33109
 
32701
33110
  // src/project-paths.ts
32702
33111
  var import_promises10 = require("node:fs/promises");
32703
- var import_node_crypto3 = require("node:crypto");
32704
- var import_node_os9 = require("node:os");
33112
+ var import_node_crypto4 = require("node:crypto");
33113
+ var import_node_os10 = require("node:os");
32705
33114
  var import_node_path16 = require("node:path");
32706
33115
  function slayzoneRootDir3() {
32707
33116
  if (process.env.SLAYZONE_ROOT) return process.env.SLAYZONE_ROOT;
32708
- const home = process.env.HOME ?? process.env.USERPROFILE ?? (0, import_node_os9.homedir)();
33117
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? (0, import_node_os10.homedir)();
32709
33118
  return (0, import_node_path16.join)(home, ".slayzone");
32710
33119
  }
32711
33120
  var projectPathEntrySchema = external_exports.object({
@@ -32747,7 +33156,7 @@ async function readFileMap(filePath) {
32747
33156
  }
32748
33157
  async function writeFileMap(filePath, map2) {
32749
33158
  await (0, import_promises10.mkdir)((0, import_node_path16.dirname)(filePath), { recursive: true, mode: 448 });
32750
- const tmpPath = `${filePath}.tmp-${process.pid}-${(0, import_node_crypto3.randomUUID)()}`;
33159
+ const tmpPath = `${filePath}.tmp-${process.pid}-${(0, import_node_crypto4.randomUUID)()}`;
32751
33160
  await (0, import_promises10.writeFile)(tmpPath, `${JSON.stringify(map2, null, 2)}
32752
33161
  `, { mode: 384 });
32753
33162
  try {
@@ -32814,6 +33223,10 @@ function createHubRequestHandler(deps2) {
32814
33223
  ...deps2.agentHookUrl ? { agentHookUrl: deps2.agentHookUrl } : {},
32815
33224
  ...deps2.hubAddress ? { hubAddress: deps2.hubAddress } : {}
32816
33225
  };
33226
+ const cliReady = deps2.cliRelayReady?.then((relay) => {
33227
+ ctx.cliRelay = relay;
33228
+ });
33229
+ void cliReady?.catch(() => log("CLI relay is unavailable"));
32817
33230
  const pty2 = createPtyHandlers(ctx, deps2.getProjectPaths, {
32818
33231
  ...deps2.admission ? { admission: deps2.admission } : {},
32819
33232
  ...deps2.pathReady ? { pathReady: deps2.pathReady } : {}
@@ -32873,6 +33286,8 @@ function createHubRequestHandler(deps2) {
32873
33286
  throw err;
32874
33287
  }
32875
33288
  try {
33289
+ if (method === HubToComputerMethods.ptySpawn || method === HubToComputerMethods.ptyWarmSpawn || method === HubToComputerMethods.procSpawn)
33290
+ await cliReady;
32876
33291
  if (deps2.pathReady && !ptyMethods.has(method)) await deps2.pathReady;
32877
33292
  const result = await entry(params);
32878
33293
  record2("ok");
@@ -32977,7 +33392,10 @@ function startComputer(config2, deps2 = {}) {
32977
33392
  count: store.list().length
32978
33393
  });
32979
33394
  }).catch((err) => log("project path map failed to load", { hubUrl, error: String(err) }));
33395
+ const cliRelayReady = createComputerCliRelay(dialer);
33396
+ void cliRelayReady.catch(() => log("CLI relay failed to start", { hubUrl }));
32980
33397
  dispatch = createHubRequestHandler({
33398
+ cliRelayReady,
32981
33399
  shutdown: (reason) => {
32982
33400
  log("computer shutdown requested by hub", { hubUrl, reason });
32983
33401
  void handle.stop().then(() => deps2.onShutdown?.(reason));
@@ -32998,6 +33416,11 @@ function startComputer(config2, deps2 = {}) {
32998
33416
  ({ computerId, mode }) => log("connected to hub", { hubUrl, computerId, mode })
32999
33417
  );
33000
33418
  dialer.events.on("disconnected", ({ reason }) => {
33419
+ void cliRelayReady.then(
33420
+ (relay) => relay.disconnect(),
33421
+ () => {
33422
+ }
33423
+ );
33001
33424
  log("disconnected from hub", { hubUrl, reason });
33002
33425
  dispatch.hubDisconnected(String(reason));
33003
33426
  });
@@ -33026,6 +33449,11 @@ function startComputer(config2, deps2 = {}) {
33026
33449
  dialer,
33027
33450
  dispose: async () => {
33028
33451
  dispatch.dispose();
33452
+ await cliRelayReady.then(
33453
+ (relay) => relay.close(),
33454
+ () => {
33455
+ }
33456
+ );
33029
33457
  if (agentHookServer) await agentHookServer.close();
33030
33458
  await dialer.stop();
33031
33459
  }
@@ -33095,7 +33523,7 @@ async function maybeInteractiveSetup() {
33095
33523
  configKey: "computerName",
33096
33524
  envKey: "SLAYZONE_COMPUTER_NAME",
33097
33525
  label: "Computer display name",
33098
- hint: `default: ${(0, import_node_os10.hostname)()}`
33526
+ hint: `default: ${(0, import_node_os11.hostname)()}`
33099
33527
  }
33100
33528
  ]
33101
33529
  });