@agentorg/computer 0.0.8 → 0.0.9

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.
@@ -3753,7 +3753,7 @@ var require_websocket_server = __commonJS({
3753
3753
  // src/main.ts
3754
3754
  import { readFileSync as readFileSync6 } from "node:fs";
3755
3755
  import { homedir as homedir2 } from "node:os";
3756
- import { isAbsolute as isAbsolute2, join as join12, resolve as resolve5 } from "node:path";
3756
+ import { isAbsolute as isAbsolute2, join as join11, resolve as resolve5 } from "node:path";
3757
3757
  import { parseArgs } from "node:util";
3758
3758
  import { isMainThread as isMainThread2 } from "node:worker_threads";
3759
3759
 
@@ -3849,7 +3849,7 @@ async function serveBrowserMcp(options) {
3849
3849
  import { execFile, spawn as spawn3 } from "node:child_process";
3850
3850
  import { readFileSync as readFileSync3 } from "node:fs";
3851
3851
  import { connect } from "node:net";
3852
- import { join as join9 } from "node:path";
3852
+ import { join as join8 } from "node:path";
3853
3853
  import { createInterface as createInterface2 } from "node:readline";
3854
3854
  import { Readable as Readable3 } from "node:stream";
3855
3855
  import { promisify } from "node:util";
@@ -6616,10 +6616,10 @@ var $ZodUnion = /* @__PURE__ */ $constructor("$ZodUnion", (inst, def) => {
6616
6616
  }
6617
6617
  return void 0;
6618
6618
  });
6619
- const first = def.options.length === 1 ? def.options[0]._zod.run : null;
6619
+ const first2 = def.options.length === 1 ? def.options[0]._zod.run : null;
6620
6620
  inst._zod.parse = (payload, ctx) => {
6621
- if (first) {
6622
- return first(payload, ctx);
6621
+ if (first2) {
6622
+ return first2(payload, ctx);
6623
6623
  }
6624
6624
  let async = false;
6625
6625
  const results = [];
@@ -9017,9 +9017,9 @@ var numberProcessor = (schema, ctx, _json, params) => {
9017
9017
  else
9018
9018
  handleUnrepresentable(schema, ctx, json4, params, `A multipleOf divisor of ${divisor} cannot be represented in JSON Schema`);
9019
9019
  }
9020
- const [first, ...rest] = divisors;
9021
- if (first !== void 0)
9022
- json4.multipleOf = first;
9020
+ const [first2, ...rest] = divisors;
9021
+ if (first2 !== void 0)
9022
+ json4.multipleOf = first2;
9023
9023
  if (rest.length)
9024
9024
  json4.allOf = [...json4.allOf ?? [], ...rest.map((m) => ({ multipleOf: m }))];
9025
9025
  }
@@ -19229,8 +19229,8 @@ var require_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
19229
19229
  }
19230
19230
  if (bestLength < 2) return hextets.join(":");
19231
19231
  const head = hextets.slice(0, bestStart).join(":");
19232
- const tail = hextets.slice(bestStart + bestLength).join(":");
19233
- return head + "::" + tail;
19232
+ const tail2 = hextets.slice(bestStart + bestLength).join(":");
19233
+ return head + "::" + tail2;
19234
19234
  }
19235
19235
  function normalizeIPv6Address(input2) {
19236
19236
  const compression = input2.indexOf("::");
@@ -28153,10 +28153,12 @@ var Browser = class {
28153
28153
  // src/inside.ts
28154
28154
  import { createServer as createServer3 } from "node:http";
28155
28155
  import { homedir } from "node:os";
28156
- import { join as join8 } from "node:path";
28156
+ import { join as join7 } from "node:path";
28157
28157
 
28158
28158
  // src/runtime.ts
28159
- import { join as join7 } from "node:path";
28159
+ import { chmodSync as chmodSync2, existsSync, statSync as statSync2 } from "node:fs";
28160
+ import { createRequire as createRequire3 } from "node:module";
28161
+ import { dirname as dirname3, join as join6 } from "node:path";
28160
28162
 
28161
28163
  // node_modules/@agentorg/exec/dist/lib.js
28162
28164
  import { realpathSync } from "node:fs";
@@ -28959,11 +28961,11 @@ var require_ignore = __commonJS2({
28959
28961
  return true;
28960
28962
  };
28961
28963
  var isNotRelative = (path) => {
28962
- const first = path.charCodeAt(0);
28963
- if (first === SLASH_CODE) {
28964
+ const first2 = path.charCodeAt(0);
28965
+ if (first2 === SLASH_CODE) {
28964
28966
  return true;
28965
28967
  }
28966
- if (first !== DOT_CODE) {
28968
+ if (first2 !== DOT_CODE) {
28967
28969
  return false;
28968
28970
  }
28969
28971
  if (path.length === 1) {
@@ -32872,10 +32874,10 @@ var $ZodUnion2 = /* @__PURE__ */ $constructor2("$ZodUnion", (inst, def) => {
32872
32874
  }
32873
32875
  return void 0;
32874
32876
  });
32875
- const first = def.options.length === 1 ? def.options[0]._zod.run : null;
32877
+ const first2 = def.options.length === 1 ? def.options[0]._zod.run : null;
32876
32878
  inst._zod.parse = (payload, ctx) => {
32877
- if (first) {
32878
- return first(payload, ctx);
32879
+ if (first2) {
32880
+ return first2(payload, ctx);
32879
32881
  }
32880
32882
  let async = false;
32881
32883
  const results = [];
@@ -32932,10 +32934,10 @@ function handleExclusiveUnionResults(results, final, inst, ctx) {
32932
32934
  var $ZodXor2 = /* @__PURE__ */ $constructor2("$ZodXor", (inst, def) => {
32933
32935
  $ZodUnion2.init(inst, def);
32934
32936
  def.inclusive = false;
32935
- const first = def.options.length === 1 ? def.options[0]._zod.run : null;
32937
+ const first2 = def.options.length === 1 ? def.options[0]._zod.run : null;
32936
32938
  inst._zod.parse = (payload, ctx) => {
32937
- if (first) {
32938
- return first(payload, ctx);
32939
+ if (first2) {
32940
+ return first2(payload, ctx);
32939
32941
  }
32940
32942
  let async = false;
32941
32943
  const results = [];
@@ -45270,9 +45272,9 @@ var numberProcessor2 = (schema, ctx, _json, params) => {
45270
45272
  else
45271
45273
  handleUnrepresentable2(schema, ctx, json32, params, `A multipleOf divisor of ${divisor} cannot be represented in JSON Schema`);
45272
45274
  }
45273
- const [first, ...rest] = divisors;
45274
- if (first !== void 0)
45275
- json32.multipleOf = first;
45275
+ const [first2, ...rest] = divisors;
45276
+ if (first2 !== void 0)
45277
+ json32.multipleOf = first2;
45276
45278
  if (rest.length)
45277
45279
  json32.allOf = [...json32.allOf ?? [], ...rest.map((m) => ({ multipleOf: m }))];
45278
45280
  }
@@ -47956,14 +47958,14 @@ function checkArrayGuards(arraySchema, guards) {
47956
47958
  const key = canonicalKey(items[i], /* @__PURE__ */ new Set());
47957
47959
  if (key === null)
47958
47960
  continue;
47959
- const first = firstSeen.get(key);
47960
- if (first === void 0) {
47961
+ const first2 = firstSeen.get(key);
47962
+ if (first2 === void 0) {
47961
47963
  firstSeen.set(key, i);
47962
47964
  continue;
47963
47965
  }
47964
47966
  payload.issues.push({
47965
47967
  code: "custom",
47966
- message: `Array items must be unique: element at index ${i} duplicates the one at index ${first}`,
47968
+ message: `Array items must be unique: element at index ${i} duplicates the one at index ${first2}`,
47967
47969
  input: items,
47968
47970
  path: [i],
47969
47971
  continue: true
@@ -56945,8 +56947,8 @@ var require_utils2 = /* @__PURE__ */ __commonJSMin2(((exports, module) => {
56945
56947
  }
56946
56948
  if (bestLength < 2) return hextets.join(":");
56947
56949
  const head = hextets.slice(0, bestStart).join(":");
56948
- const tail = hextets.slice(bestStart + bestLength).join(":");
56949
- return head + "::" + tail;
56950
+ const tail2 = hextets.slice(bestStart + bestLength).join(":");
56951
+ return head + "::" + tail2;
56950
56952
  }
56951
56953
  function normalizeIPv6Address(input2) {
56952
56954
  const compression = input2.indexOf("::");
@@ -63419,9 +63421,9 @@ var WebStandardStreamableHTTPServerTransport = class {
63419
63421
  }
63420
63422
  });
63421
63423
  }
63422
- async _checkScopeChallenge(messages, authInfo) {
63424
+ async _checkScopeChallenge(messages2, authInfo) {
63423
63425
  if (!this._scopeChallengeResolver) return;
63424
- const challenge = await findScopeChallenge(messages.filter((message) => isJSONRPCRequest2(message)), authInfo, this._scopeChallengeResolver);
63426
+ const challenge = await findScopeChallenge(messages2.filter((message) => isJSONRPCRequest2(message)), authInfo, this._scopeChallengeResolver);
63425
63427
  return challenge === void 0 ? void 0 : createScopeChallengeResponse(challenge, scopeChallengeResourceMetadataUrl(authInfo));
63426
63428
  }
63427
63429
  /**
@@ -63719,21 +63721,21 @@ data:
63719
63721
  this.onerror?.(/* @__PURE__ */ new Error(`Invalid Request: Batch must not exceed ${MAX_BATCH_SIZE} messages`));
63720
63722
  return this.createJsonErrorResponse(400, -32600, `Invalid Request: Batch must not exceed ${MAX_BATCH_SIZE} messages`);
63721
63723
  }
63722
- let messages;
63724
+ let messages2;
63723
63725
  try {
63724
- messages = Array.isArray(rawMessage) ? rawMessage.map((msg) => JSONRPCMessageSchema2.parse(msg)) : [JSONRPCMessageSchema2.parse(rawMessage)];
63726
+ messages2 = Array.isArray(rawMessage) ? rawMessage.map((msg) => JSONRPCMessageSchema2.parse(msg)) : [JSONRPCMessageSchema2.parse(rawMessage)];
63725
63727
  } catch (error62) {
63726
63728
  this.onerror?.(error62);
63727
63729
  return this.createJsonErrorResponse(400, -32700, "Parse error: Invalid JSON-RPC message");
63728
63730
  }
63729
63731
  if (this._closed) return this.createJsonErrorResponse(404, -32001, "Session not found");
63730
- const isInitializationRequest = messages.some((element) => isInitializeRequest2(element));
63732
+ const isInitializationRequest = messages2.some((element) => isInitializeRequest2(element));
63731
63733
  if (isInitializationRequest) {
63732
63734
  if (this._initialized && this.sessionId !== void 0) {
63733
63735
  this.onerror?.(/* @__PURE__ */ new Error("Invalid Request: Server already initialized"));
63734
63736
  return this.createJsonErrorResponse(400, -32600, "Invalid Request: Server already initialized");
63735
63737
  }
63736
- if (messages.length > 1) {
63738
+ if (messages2.length > 1) {
63737
63739
  this.onerror?.(/* @__PURE__ */ new Error("Invalid Request: Only one initialization request is allowed"));
63738
63740
  return this.createJsonErrorResponse(400, -32600, "Invalid Request: Only one initialization request is allowed");
63739
63741
  }
@@ -63750,21 +63752,21 @@ data:
63750
63752
  if (this._closed) return this.createJsonErrorResponse(404, -32001, "Session not found");
63751
63753
  let scopeChallengeResponse;
63752
63754
  try {
63753
- scopeChallengeResponse = await this._checkScopeChallenge(messages, options?.authInfo);
63755
+ scopeChallengeResponse = await this._checkScopeChallenge(messages2, options?.authInfo);
63754
63756
  } catch (error62) {
63755
63757
  this.onerror?.(error62);
63756
63758
  return this.createJsonErrorResponse(500, -32603, "Internal server error");
63757
63759
  }
63758
63760
  if (scopeChallengeResponse) return scopeChallengeResponse;
63759
- if (!messages.some((element) => isJSONRPCRequest2(element))) {
63760
- for (const message of messages) this.onmessage?.(message, {
63761
+ if (!messages2.some((element) => isJSONRPCRequest2(element))) {
63762
+ for (const message of messages2) this.onmessage?.(message, {
63761
63763
  authInfo: options?.authInfo,
63762
63764
  request
63763
63765
  });
63764
63766
  return new Response(null, { status: 202 });
63765
63767
  }
63766
63768
  const streamId = crypto.randomUUID();
63767
- const initRequest = messages.find((m) => isInitializeRequest2(m));
63769
+ const initRequest = messages2.find((m) => isInitializeRequest2(m));
63768
63770
  const clientProtocolVersion = initRequest ? initRequest.params.protocolVersion : req.headers.get("mcp-protocol-version") ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION2;
63769
63771
  if (this._enableJsonResponse) return new Promise((resolve42) => {
63770
63772
  this._streamMapping.set(streamId, {
@@ -63773,8 +63775,8 @@ data:
63773
63775
  this._streamMapping.delete(streamId);
63774
63776
  }
63775
63777
  });
63776
- for (const message of messages) if (isJSONRPCRequest2(message)) this._requestToStreamMapping.set(message.id, streamId);
63777
- for (const message of messages) this.onmessage?.(message, {
63778
+ for (const message of messages2) if (isJSONRPCRequest2(message)) this._requestToStreamMapping.set(message.id, streamId);
63779
+ for (const message of messages2) this.onmessage?.(message, {
63778
63780
  authInfo: options?.authInfo,
63779
63781
  request
63780
63782
  });
@@ -63798,7 +63800,7 @@ data:
63798
63800
  "X-Accel-Buffering": "no"
63799
63801
  };
63800
63802
  if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
63801
- for (const message of messages) if (isJSONRPCRequest2(message)) {
63803
+ for (const message of messages2) if (isJSONRPCRequest2(message)) {
63802
63804
  this._streamMapping.set(streamId, {
63803
63805
  controller: streamController,
63804
63806
  encoder,
@@ -63814,7 +63816,7 @@ data:
63814
63816
  this._requestToStreamMapping.set(message.id, streamId);
63815
63817
  }
63816
63818
  await this.writePrimingEvent(streamController, encoder, streamId, clientProtocolVersion);
63817
- for (const message of messages) {
63819
+ for (const message of messages2) {
63818
63820
  let closeSSEStream;
63819
63821
  let closeStandaloneSSEStream;
63820
63822
  if (isJSONRPCRequest2(message) && this._eventStore && this.supportsEmptySSEData(clientProtocolVersion)) {
@@ -65811,10 +65813,10 @@ function cut(text22, max) {
65811
65813
  if (text22.length <= max) return text22;
65812
65814
  const half = Math.max(1, Math.floor(max / 2));
65813
65815
  const head = text22.slice(0, half).replace(/[\uD800-\uDBFF]$/, "");
65814
- const tail = text22.slice(-half).replace(/^[\uDC00-\uDFFF]/, "");
65816
+ const tail2 = text22.slice(-half).replace(/^[\uDC00-\uDFFF]/, "");
65815
65817
  return `${head}
65816
- \u2026 ${text22.length - head.length - tail.length} characters cut \u2026
65817
- ${tail}`;
65818
+ \u2026 ${text22.length - head.length - tail2.length} characters cut \u2026
65819
+ ${tail2}`;
65818
65820
  }
65819
65821
  var Pending = class {
65820
65822
  #head = "";
@@ -65850,6 +65852,10 @@ var DEFAULT_YIELD_MS = 1e4;
65850
65852
  var MIN_POLL_MS = 5e3;
65851
65853
  var WRITE_YIELD_MS = 250;
65852
65854
  var MAX_RUNNING = 64;
65855
+ var MAX_ENDED = 64;
65856
+ var KEPT = 64 * 1024;
65857
+ var CMD_SHOWN = 200;
65858
+ var MAX_SIZE = 1e3;
65853
65859
  var DEFAULT_MAX_TOKENS = 7e3;
65854
65860
  var DRAIN_MS = 200;
65855
65861
  var KILL_AFTER_MS = 1e3;
@@ -65863,48 +65869,183 @@ var SIGNALS = {
65863
65869
  SIGTERM: 15
65864
65870
  };
65865
65871
  var clamp = (value, low, high) => Math.min(Math.max(value, low), high);
65872
+ var tail = (text22) => text22.slice(-KEPT).replace(/^[\uDC00-\uDFFF]/, "");
65866
65873
  var Sessions = class {
65867
65874
  #sessions = /* @__PURE__ */ new Map();
65875
+ /** Those that have ended and are still listed, in the order they ended. */
65876
+ #ended = [];
65868
65877
  #root;
65869
65878
  #emit;
65879
+ #pty;
65870
65880
  constructor(root, emit = () => {
65871
- }) {
65881
+ }, pty2) {
65872
65882
  this.#root = root;
65873
65883
  this.#emit = emit;
65884
+ this.#pty = pty2;
65874
65885
  }
65875
65886
  /** Run `cmd` in a login shell, in `workdir` or the workspace. */
65876
65887
  async exec(input2) {
65877
- const running = [...this.#sessions.values()].filter((s) => s.exitCode === void 0);
65878
- if (running.length >= MAX_RUNNING)
65879
- throw new ExecRefused(`${MAX_RUNNING} commands are running. Wait for one, or stop one.`);
65888
+ this.#room();
65880
65889
  const started = Date.now();
65881
65890
  const session = this.#start(input2.cmd, input2.workdir ?? this.#root);
65882
65891
  const wait = clamp(input2.yield_time_ms ?? DEFAULT_YIELD_MS, MIN_YIELD_MS, MAX_YIELD_MS);
65883
65892
  return this.#after(session, wait, started, input2.max_output_tokens);
65884
65893
  }
65885
65894
  /**
65886
- * Type into a running command, or with no `chars` wait and see what it has written. Ctrl-C
65887
- * (`\u0003`) interrupts it and Ctrl-D (`\u0004`) ends its input.
65895
+ * Type into a running terminal, or with no `chars` wait and see what it has written. In a
65896
+ * command, Ctrl-C (`\u0003`) interrupts it and Ctrl-D (`\u0004`) ends its input; a shell on a
65897
+ * terminal device is typed into as it is.
65888
65898
  */
65889
65899
  async write(input2) {
65890
65900
  const session = this.#sessions.get(input2.session_id);
65891
- if (!session) throw new ExecRefused(`No session ${input2.session_id.slice(0, 64)} is running.`);
65901
+ if (!session || session.reported)
65902
+ throw new ExecRefused(`No session ${input2.session_id.slice(0, 64)} is running.`);
65892
65903
  const started = Date.now();
65893
65904
  const chars = input2.chars ?? "";
65894
- if (session.exitCode === void 0) this.#type(session, chars);
65905
+ if (session.exitCode === void 0) session.type(chars);
65895
65906
  const wait = chars ? clamp(input2.yield_time_ms ?? WRITE_YIELD_MS, MIN_YIELD_MS, MAX_YIELD_MS) : clamp(input2.yield_time_ms ?? MIN_POLL_MS, MIN_POLL_MS, MAX_YIELD_MS);
65896
65907
  return this.#after(session, wait, started, input2.max_output_tokens);
65897
65908
  }
65898
- /** Stop every command: a polite signal, then a certain one. */
65909
+ /** Every terminal, the oldest first: those running, and the last that ended. */
65910
+ list() {
65911
+ return [...this.#sessions.values()].map((session) => ({
65912
+ id: session.id,
65913
+ by: session.by,
65914
+ cmd: session.cmd,
65915
+ workdir: session.workdir,
65916
+ tty: session.tty,
65917
+ at: session.at,
65918
+ output: tail(session.last),
65919
+ ...session.exitCode === void 0 ? {} : { exitCode: session.exitCode, endedAt: session.endedAt }
65920
+ }));
65921
+ }
65922
+ /** Start a person's login shell in the workspace, on a terminal device of this size. */
65923
+ open(columns, rows) {
65924
+ if (!this.#pty)
65925
+ throw new ExecRefused("A shell needs a terminal device, and this host has none to give.");
65926
+ this.#room();
65927
+ const size = sized(columns, rows);
65928
+ const file2 = process.env.SHELL || "/bin/bash";
65929
+ let shell;
65930
+ try {
65931
+ shell = this.#pty(file2, ["-l"], {
65932
+ name: "xterm-256color",
65933
+ ...size,
65934
+ cwd: this.#root,
65935
+ env: process.env
65936
+ });
65937
+ } catch (error62) {
65938
+ const why = error62 instanceof Error ? error62.message : String(error62);
65939
+ throw new ExecRefused(`The shell didn't start: ${why}`);
65940
+ }
65941
+ const { session, wrote, finish } = this.#begin("person", file2, this.#root, true, {
65942
+ type: (chars) => shell.write(chars),
65943
+ signal(signal) {
65944
+ try {
65945
+ shell.kill(signal);
65946
+ } catch {
65947
+ }
65948
+ },
65949
+ resize: (cols, lines) => shell.resize(cols, lines)
65950
+ });
65951
+ shell.onData((text22) => wrote(text22));
65952
+ shell.onExit(({ exitCode, signal }) => finish(signal ? 128 + signal : exitCode));
65953
+ return session.id;
65954
+ }
65955
+ /** Type into a running terminal, as `write` does and with no answer. */
65956
+ type(id, chars) {
65957
+ this.#running(id).type(chars);
65958
+ }
65959
+ /** Give a shell's terminal device a new size. */
65960
+ resize(id, columns, rows) {
65961
+ const session = this.#running(id);
65962
+ if (!session.resize) throw new ExecRefused("Only a shell on a terminal device has a size.");
65963
+ const size = sized(columns, rows);
65964
+ session.resize(size.cols, size.rows);
65965
+ }
65966
+ /**
65967
+ * End one terminal: an interrupt, then a certain signal. It resolves once the terminal has
65968
+ * ended or that signal is sent. One that has ended is left alone.
65969
+ */
65970
+ async stop(id) {
65971
+ const session = this.#sessions.get(id);
65972
+ if (!session) throw new ExecRefused(`No terminal ${id.slice(0, 64)} is here.`);
65973
+ await this.#end([session], "SIGINT");
65974
+ }
65975
+ /** Stop every terminal: a polite signal, then a certain one. */
65899
65976
  async stopAll() {
65900
- const running = [...this.#sessions.values()].filter((s) => s.exitCode === void 0);
65901
- for (const session of running) this.#signal(session, "SIGTERM");
65977
+ await this.#end([...this.#sessions.values()], "SIGTERM");
65978
+ }
65979
+ async #end(sessions, polite) {
65980
+ const running = sessions.filter((s) => s.exitCode === void 0);
65981
+ for (const session of running) session.signal(session.tty ? "SIGHUP" : polite);
65982
+ let timer;
65902
65983
  await Promise.race([
65903
65984
  Promise.all(running.map((s) => s.ended)),
65904
- new Promise((done) => setTimeout(done, KILL_AFTER_MS))
65985
+ new Promise((done) => {
65986
+ timer = setTimeout(done, KILL_AFTER_MS);
65987
+ })
65905
65988
  ]);
65906
- for (const session of running)
65907
- if (session.exitCode === void 0) this.#signal(session, "SIGKILL");
65989
+ clearTimeout(timer);
65990
+ for (const session of running) if (session.exitCode === void 0) session.signal("SIGKILL");
65991
+ }
65992
+ #room() {
65993
+ const running = [...this.#sessions.values()].filter((s) => s.exitCode === void 0);
65994
+ if (running.length >= MAX_RUNNING)
65995
+ throw new ExecRefused(`${MAX_RUNNING} commands are running. Wait for one, or stop one.`);
65996
+ }
65997
+ #running(id) {
65998
+ const session = this.#sessions.get(id);
65999
+ if (!session) throw new ExecRefused(`No terminal ${id.slice(0, 64)} is here.`);
66000
+ if (session.exitCode !== void 0) throw new ExecRefused("That terminal has ended.");
66001
+ return session;
66002
+ }
66003
+ /**
66004
+ * List a terminal that has just started, with the ways to act on its process, and give the two
66005
+ * ways that process reports back.
66006
+ */
66007
+ #begin(by, cmd, workdir, tty, acts) {
66008
+ const id = `s_${randomBytes(9).toString("hex")}`;
66009
+ const at = (/* @__PURE__ */ new Date()).toISOString();
66010
+ let done = () => {
66011
+ };
66012
+ const session = {
66013
+ id,
66014
+ by,
66015
+ cmd,
66016
+ workdir,
66017
+ tty,
66018
+ at,
66019
+ pending: new Pending(),
66020
+ last: "",
66021
+ ended: new Promise((resolve42) => {
66022
+ done = resolve42;
66023
+ }),
66024
+ ...acts
66025
+ };
66026
+ this.#sessions.set(id, session);
66027
+ this.#emit({ type: "start", id, by, cmd, workdir, tty, at });
66028
+ const wrote = (text22, stream) => {
66029
+ if (!text22) return;
66030
+ session.pending.add(text22);
66031
+ session.last += text22;
66032
+ if (session.last.length > 2 * KEPT) session.last = tail(session.last);
66033
+ this.#emit({ type: "output", id, ...stream ? { stream } : {}, text: text22 });
66034
+ };
66035
+ const finish = (exitCode) => {
66036
+ if (session.exitCode !== void 0) return;
66037
+ session.exitCode = exitCode;
66038
+ session.endedAt = (/* @__PURE__ */ new Date()).toISOString();
66039
+ this.#emit({ type: "exit", id, exitCode, at: session.endedAt });
66040
+ this.#ended.push(session);
66041
+ if (this.#ended.length > MAX_ENDED) {
66042
+ const read = this.#ended.findIndex((one) => one.reported);
66043
+ const [old] = this.#ended.splice(Math.max(read, 0), 1);
66044
+ this.#sessions.delete(old.id);
66045
+ }
66046
+ done();
66047
+ };
66048
+ return { session, wrote, finish };
65908
66049
  }
65909
66050
  #start(cmd, cwd) {
65910
66051
  const child = spawn2(process.env.SHELL || "/bin/bash", ["-lc", cmd], {
@@ -65913,62 +66054,44 @@ var Sessions = class {
65913
66054
  // Its own process group, so a signal reaches what it started.
65914
66055
  detached: true
65915
66056
  });
65916
- const pending = new Pending();
65917
- const id = `s_${randomBytes(9).toString("hex")}`;
65918
- const session = { id, child, pending };
65919
- this.#emit({ type: "start", id, cmd, workdir: cwd, at: (/* @__PURE__ */ new Date()).toISOString() });
66057
+ const signal = (name) => {
66058
+ try {
66059
+ if (child.pid) process.kill(-child.pid, name);
66060
+ } catch {
66061
+ }
66062
+ };
66063
+ const type = (chars) => {
66064
+ for (const part of chars.split(new RegExp("(\\p{Cc})", "u"))) {
66065
+ if (part === "") signal("SIGINT");
66066
+ else if (part === "") child.stdin.end();
66067
+ else if (part && child.stdin.writable) child.stdin.write(part);
66068
+ }
66069
+ };
66070
+ const { session, wrote, finish } = this.#begin("agent", cmd, cwd, false, { type, signal });
65920
66071
  for (const [name, stream] of [
65921
66072
  ["stdout", child.stdout],
65922
66073
  ["stderr", child.stderr]
65923
66074
  ]) {
65924
66075
  const decoder = new StringDecoder("utf8");
65925
- stream.on("data", (chunk) => {
65926
- const text22 = decoder.write(chunk);
65927
- pending.add(text22);
65928
- if (text22) this.#emit({ type: "output", id, stream: name, text: text22 });
65929
- });
66076
+ stream.on("data", (chunk) => wrote(decoder.write(chunk), name));
65930
66077
  }
65931
66078
  child.stdin.on("error", () => {
65932
66079
  });
65933
- session.ended = new Promise((done) => {
65934
- const finish = (code) => {
65935
- if (session.exitCode !== void 0) return;
65936
- session.exitCode = code;
65937
- this.#emit({ type: "exit", id, exitCode: code, at: (/* @__PURE__ */ new Date()).toISOString() });
65938
- done();
65939
- };
65940
- child.once("error", (error62) => {
65941
- pending.add(`${error62.message}
65942
- `);
65943
- this.#emit({ type: "output", id, stream: "stderr", text: `${error62.message}
65944
- ` });
65945
- finish(127);
65946
- });
65947
- child.once("exit", (code, signal) => {
65948
- const exitCode = code ?? 128 + (signal ? SIGNALS[signal] ?? 1 : 1);
65949
- const late = setTimeout(() => finish(exitCode), DRAIN_MS);
65950
- child.once("close", () => {
65951
- clearTimeout(late);
65952
- finish(exitCode);
65953
- });
66080
+ child.once("error", (error62) => {
66081
+ wrote(`${error62.message}
66082
+ `, "stderr");
66083
+ finish(127);
66084
+ });
66085
+ child.once("exit", (code, signal2) => {
66086
+ const exitCode = code ?? 128 + (signal2 ? SIGNALS[signal2] ?? 1 : 1);
66087
+ const late = setTimeout(() => finish(exitCode), DRAIN_MS);
66088
+ child.once("close", () => {
66089
+ clearTimeout(late);
66090
+ finish(exitCode);
65954
66091
  });
65955
66092
  });
65956
- this.#sessions.set(session.id, session);
65957
66093
  return session;
65958
66094
  }
65959
- #type(session, chars) {
65960
- for (const part of chars.split(new RegExp("(\\p{Cc})", "u"))) {
65961
- if (part === "") this.#signal(session, "SIGINT");
65962
- else if (part === "") session.child.stdin.end();
65963
- else if (part && session.child.stdin.writable) session.child.stdin.write(part);
65964
- }
65965
- }
65966
- #signal(session, signal) {
65967
- try {
65968
- if (session.child.pid) process.kill(-session.child.pid, signal);
65969
- } catch {
65970
- }
65971
- }
65972
66095
  async #after(session, waitMs, started, maxTokens) {
65973
66096
  let timer;
65974
66097
  await Promise.race([
@@ -65979,7 +66102,7 @@ var Sessions = class {
65979
66102
  ]);
65980
66103
  clearTimeout(timer);
65981
66104
  const exited = session.exitCode !== void 0;
65982
- if (exited) this.#sessions.delete(session.id);
66105
+ if (exited) session.reported = true;
65983
66106
  return {
65984
66107
  exit_code: exited ? session.exitCode : null,
65985
66108
  seconds: Math.round(Date.now() - started) / 1e3,
@@ -65988,6 +66111,12 @@ var Sessions = class {
65988
66111
  };
65989
66112
  }
65990
66113
  };
66114
+ function sized(columns, rows) {
66115
+ for (const value of [columns, rows])
66116
+ if (!Number.isInteger(value) || value < 1 || value > MAX_SIZE)
66117
+ throw new ExecRefused(`A terminal has 1 to ${MAX_SIZE} columns and rows.`);
66118
+ return { cols: columns, rows };
66119
+ }
65991
66120
  function formatAnswer(answer) {
65992
66121
  return [
65993
66122
  `exit_code: ${answer.exit_code ?? "running"}`,
@@ -65997,6 +66126,31 @@ function formatAnswer(answer) {
65997
66126
  answer.text
65998
66127
  ].join("\n");
65999
66128
  }
66129
+ function listing(terminals) {
66130
+ return terminals.map((one) => {
66131
+ const line = one.cmd.split("\n", 1)[0].slice(0, CMD_SHOWN);
66132
+ const shown = line.replace(/[\uD800-\uDBFF]$/, "");
66133
+ return {
66134
+ session_id: one.id,
66135
+ started_by: one.by,
66136
+ since: one.at,
66137
+ exit_code: one.exitCode ?? null,
66138
+ cmd: shown === one.cmd ? shown : `${shown}\u2026`
66139
+ };
66140
+ });
66141
+ }
66142
+ function formatSessions(listed) {
66143
+ if (listed.length === 0) return "No sessions.";
66144
+ return listed.map(
66145
+ (one) => [
66146
+ `session_id: ${one.session_id}`,
66147
+ `started_by: ${one.started_by}`,
66148
+ `since: ${one.since}`,
66149
+ `exit_code: ${one.exit_code ?? "running"}`,
66150
+ `cmd: ${one.cmd}`
66151
+ ].join("\n")
66152
+ ).join("\n\n");
66153
+ }
66000
66154
  var DEFAULT_LINES = 2e3;
66001
66155
  var MAX_READ_CHARS = 28e3;
66002
66156
  var MAX_LINE_CHARS = 2e3;
@@ -66060,10 +66214,10 @@ function createFiles(root) {
66060
66214
  /** Numbered lines of a file. */
66061
66215
  read: (input2) => refusing(async () => {
66062
66216
  const lines = (await text22(await at(input2.path), input2.path)).split("\n");
66063
- const first = Math.max(input2.offset ?? 1, 1);
66064
- const shown = lines.slice(first - 1, first - 1 + (input2.limit ?? DEFAULT_LINES)).map((line, i) => `${first + i} ${cut(line, MAX_LINE_CHARS)}`);
66065
- const last = first + shown.length - 1;
66066
- return `lines ${first}-${last} of ${lines.length}
66217
+ const first2 = Math.max(input2.offset ?? 1, 1);
66218
+ const shown = lines.slice(first2 - 1, first2 - 1 + (input2.limit ?? DEFAULT_LINES)).map((line, i) => `${first2 + i} ${cut(line, MAX_LINE_CHARS)}`);
66219
+ const last = first2 + shown.length - 1;
66220
+ return `lines ${first2}-${last} of ${lines.length}
66067
66221
  ${cut(shown.join("\n"), MAX_READ_CHARS)}`;
66068
66222
  }),
66069
66223
  /** A folder's entries, folders first, each folder ending in `/`. */
@@ -66409,9 +66563,9 @@ function createServer2(root, sessions, version22, level) {
66409
66563
  server.registerTool(
66410
66564
  "write_stdin",
66411
66565
  {
66412
- description: "Types into a running command and returns what it wrote since. With no chars, waits and returns what it has written. A Ctrl-C character (\\u0003) interrupts it; Ctrl-D (\\u0004) ends its input.",
66566
+ description: "Types into a running command and returns what it wrote since. With no chars, waits and returns what it has written. A Ctrl-C character (\\u0003) interrupts it; Ctrl-D (\\u0004) ends its input. It also reaches a shell a person opened, by its id from list_sessions: there these are keys, what you type joins anything already typed, and Ctrl-D at its prompt closes it. Once an answer has given a session's exit code, that session is refused.",
66413
66567
  inputSchema: {
66414
- session_id: external_exports.string().min(1).describe("From exec_command's answer."),
66568
+ session_id: external_exports.string().min(1).describe("From exec_command's answer, or list_sessions."),
66415
66569
  chars: external_exports.string().optional().describe("What to type; include \\n to press Enter."),
66416
66570
  yield_time_ms: YieldTime.describe(
66417
66571
  "How long to wait: with chars 250 to 30000, by default 250; with none 5000 to 30000."
@@ -66423,6 +66577,28 @@ function createServer2(root, sessions, version22, level) {
66423
66577
  },
66424
66578
  (input2) => answering(() => sessions.write(input2))
66425
66579
  );
66580
+ server.registerTool(
66581
+ "list_sessions",
66582
+ {
66583
+ description: "Lists the sessions here, running or lately ended: each command exec_command started, and each shell a person opened. Gives each one's session_id for write_stdin, who started it, since when, its exit code and the start of its command.",
66584
+ outputSchema: {
66585
+ sessions: external_exports.array(
66586
+ external_exports.object({
66587
+ session_id: external_exports.string(),
66588
+ started_by: external_exports.enum(["agent", "person"]).describe("A person's is their own shell."),
66589
+ since: external_exports.string().describe("When it started."),
66590
+ exit_code: external_exports.number().nullable().describe("Null while it is still running."),
66591
+ cmd: external_exports.string().describe("Its first line, or the start of it.")
66592
+ })
66593
+ )
66594
+ },
66595
+ annotations: READ_ONLY
66596
+ },
66597
+ () => {
66598
+ const listed = listing(sessions.list());
66599
+ return { ...text(formatSessions(listed)), structuredContent: { sessions: listed } };
66600
+ }
66601
+ );
66426
66602
  }
66427
66603
  server.registerTool(
66428
66604
  "read",
@@ -66478,18 +66654,22 @@ function createServer2(root, sessions, version22, level) {
66478
66654
  );
66479
66655
  return server;
66480
66656
  }
66481
- var VERSION = "0.0.4";
66657
+ var VERSION = "0.0.5";
66482
66658
  function createAgentExec(options) {
66483
66659
  const root = realpathSync(resolve3(options.workspace));
66484
66660
  const level = options.level ?? "read";
66485
66661
  const listeners = /* @__PURE__ */ new Set();
66486
- const sessions = new Sessions(root, (event) => {
66487
- for (const listener of listeners)
66488
- try {
66489
- listener(event);
66490
- } catch {
66491
- }
66492
- });
66662
+ const sessions = new Sessions(
66663
+ root,
66664
+ (event) => {
66665
+ for (const listener of listeners)
66666
+ try {
66667
+ listener(event);
66668
+ } catch {
66669
+ }
66670
+ },
66671
+ options.pty
66672
+ );
66493
66673
  const host = createHostFiles(root, level);
66494
66674
  return {
66495
66675
  async handle(request, response, body) {
@@ -66507,6 +66687,16 @@ function createAgentExec(options) {
66507
66687
  return () => void listeners.delete(listener);
66508
66688
  },
66509
66689
  files: host.files,
66690
+ terminals: {
66691
+ list: () => sessions.list(),
66692
+ open({ columns, rows }) {
66693
+ if (level !== "terminal") throw new Error("A shell needs the terminal level.");
66694
+ return sessions.open(columns, rows);
66695
+ },
66696
+ write: (id, text22) => sessions.type(id, text22),
66697
+ resize: (id, columns, rows) => sessions.resize(id, columns, rows),
66698
+ stop: (id) => sessions.stop(id)
66699
+ },
66510
66700
  async close() {
66511
66701
  await Promise.all([sessions.stopAll(), host.close()]);
66512
66702
  }
@@ -66927,11 +67117,6 @@ var BrowserLive = class {
66927
67117
  }
66928
67118
  };
66929
67119
 
66930
- // src/live.ts
66931
- import { chmodSync, existsSync, statSync as statSync2 } from "node:fs";
66932
- import { createRequire as createRequire3 } from "node:module";
66933
- import { dirname as dirname3, join as join5 } from "node:path";
66934
-
66935
67120
  // node_modules/ws/wrapper.mjs
66936
67121
  var import_stream2 = __toESM(require_stream(), 1);
66937
67122
  var import_extension = __toESM(require_extension(), 1);
@@ -67109,78 +67294,70 @@ var FilesLive = class {
67109
67294
  }
67110
67295
  };
67111
67296
 
67297
+ // src/text.ts
67298
+ var MAX_TEXT = 1e4;
67299
+ function* pieces(text3, most = MAX_TEXT) {
67300
+ for (let from = 0; from < text3.length; ) {
67301
+ let to = Math.min(from + most, text3.length);
67302
+ const last = text3.charCodeAt(to - 1);
67303
+ if (to < text3.length && last >= 55296 && last <= 56319) to -= 1;
67304
+ yield text3.slice(from, to);
67305
+ from = to;
67306
+ }
67307
+ }
67308
+
67112
67309
  // src/live.ts
67113
67310
  var PROTOCOLS = {
67114
- commands: "commands@1",
67115
- terminal: "pty@1",
67311
+ terminal: "terminal@1",
67116
67312
  files: "files@1",
67117
67313
  browser: "screencast@1"
67118
67314
  };
67119
- var MAX_EVENTS = 2e3;
67120
- var MAX_SCROLLBACK = 64 * 1024;
67121
- var OUTPUT = 48;
67122
- var INPUT = 48;
67123
- var RESIZE = 49;
67124
- var sockets = new import_websocket_server.default({ noServer: true });
67125
- var fileSockets = new import_websocket_server.default({ noServer: true, maxPayload: MAX_PART });
67126
- var pty;
67127
- var loadPty = () => {
67128
- pty ??= (async () => {
67129
- const require3 = createRequire3(import.meta.url);
67130
- let loaded2;
67131
- try {
67132
- loaded2 = require3("node-pty");
67133
- } catch {
67134
- return "The terminal needs node-pty, which isn't installed with this service.";
67135
- }
67136
- const root = dirname3(require3.resolve("node-pty/package.json"));
67137
- for (const helper of [
67138
- join5(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
67139
- join5(root, "build", "Release", "spawn-helper")
67140
- ])
67141
- try {
67142
- if (existsSync(helper) && (statSync2(helper).mode & 73) === 0) chmodSync(helper, 493);
67143
- } catch {
67144
- return `The terminal needs ${helper} to be executable, and it isn't.`;
67145
- }
67146
- return loaded2;
67147
- })();
67148
- return pty;
67315
+ var MAX_MESSAGE = 64 * 1024;
67316
+ var MAX_BUFFERED3 = 4 * 1024 * 1024;
67317
+ var deliver = (ws, message) => {
67318
+ if (ws.bufferedAmount > MAX_BUFFERED3) ws.terminate();
67319
+ else ws.send(JSON.stringify(message));
67320
+ };
67321
+ var MAX_CMD = 8e3;
67322
+ var MAX_WORKDIR = 2e3;
67323
+ var SERVERS = {
67324
+ terminal: new import_websocket_server.default({ noServer: true, maxPayload: MAX_MESSAGE }),
67325
+ files: new import_websocket_server.default({ noServer: true, maxPayload: MAX_PART }),
67326
+ browser: new import_websocket_server.default({ noServer: true })
67327
+ };
67328
+ var MALFORMED = "That isn't a terminal@1 message.";
67329
+ var first = (text3, most) => {
67330
+ const [head = ""] = pieces(text3, most);
67331
+ return head;
67149
67332
  };
67333
+ function* messages(event) {
67334
+ if (event.type === "start")
67335
+ yield { ...event, cmd: first(event.cmd, MAX_CMD), workdir: first(event.workdir, MAX_WORKDIR) };
67336
+ else if (event.type === "exit") yield event;
67337
+ else for (const text3 of pieces(event.text)) yield { ...event, text: text3 };
67338
+ }
67150
67339
  var Live = class {
67151
- #workspace;
67152
- #terminal;
67153
- #events = [];
67154
- #watchers = /* @__PURE__ */ new Set();
67155
- #shells = /* @__PURE__ */ new Map();
67156
- #shell;
67157
- #scrollback = "";
67340
+ #terminals;
67341
+ #viewers = /* @__PURE__ */ new Set();
67158
67342
  #browser;
67159
67343
  #files;
67160
67344
  /** Whether a person may write files here: not where the agent can only read them. */
67161
67345
  #writes;
67162
- constructor(exec, workspace, access, browser) {
67163
- this.#workspace = workspace;
67164
- this.#terminal = access === "terminal" && exec !== void 0;
67346
+ constructor(exec, access, browser) {
67347
+ if (exec && access === "terminal") {
67348
+ this.#terminals = exec.terminals;
67349
+ exec.onCommand((event) => {
67350
+ for (const message of messages(event)) for (const ws of this.#viewers) deliver(ws, message);
67351
+ });
67352
+ }
67165
67353
  this.#files = exec && new FilesLive(exec.files);
67166
67354
  this.#writes = access !== "read";
67167
67355
  this.#browser = browser;
67168
- exec?.onCommand((event) => this.#heard(event));
67169
67356
  }
67170
- async surfaces() {
67357
+ surfaces() {
67171
67358
  const surfaces = [];
67172
- if (this.#terminal) {
67173
- const loaded2 = await loadPty();
67174
- surfaces.push(
67175
- { surface: "commands", protocol: PROTOCOLS.commands, operate: false },
67176
- {
67177
- surface: "terminal",
67178
- protocol: PROTOCOLS.terminal,
67179
- operate: true,
67180
- ...typeof loaded2 === "string" ? { unavailable: loaded2 } : {}
67181
- }
67182
- );
67183
- }
67359
+ if (this.#terminals)
67360
+ surfaces.push({ surface: "terminal", protocol: PROTOCOLS.terminal, operate: true });
67184
67361
  if (this.#files)
67185
67362
  surfaces.push({ surface: "files", protocol: PROTOCOLS.files, operate: this.#writes });
67186
67363
  if (this.#browser)
@@ -67190,102 +67367,75 @@ var Live = class {
67190
67367
  /** Take an upgraded request as a socket on one surface. */
67191
67368
  open(surface, mode, request, socket, head) {
67192
67369
  socket.on("error", () => socket.destroy());
67193
- const server = surface === "files" ? fileSockets : sockets;
67194
- server.handleUpgrade(request, socket, head, (ws) => {
67370
+ SERVERS[surface].handleUpgrade(request, socket, head, (ws) => {
67195
67371
  ws.on("error", () => ws.terminate());
67196
- if (surface === "commands") this.#watch(ws);
67372
+ if (surface === "terminal" && this.#terminals) this.#view(ws, mode, this.#terminals);
67197
67373
  else if (surface === "files" && this.#files) this.#files.open(ws, mode);
67198
67374
  else if (surface === "browser" && this.#browser) this.#browser.open(ws, mode);
67199
- else if (surface === "terminal") this.#attach(ws, mode).catch(() => ws.terminate());
67200
67375
  else ws.close(1011, `There's no ${surface} here.`);
67201
67376
  });
67202
67377
  }
67378
+ /** The terminals themselves end with the AgentExec that runs them. */
67203
67379
  close() {
67204
67380
  void this.#browser?.close();
67205
67381
  this.#files?.close();
67206
- for (const ws of [...this.#watchers, ...this.#shells.keys()]) ws.close(1001, "It stopped.");
67207
- this.#shell?.kill();
67208
- this.#shell = void 0;
67209
- }
67210
- #heard(event) {
67211
- this.#events.push(event);
67212
- if (this.#events.length > MAX_EVENTS) this.#events.splice(0, this.#events.length - MAX_EVENTS);
67213
- const message = JSON.stringify(event);
67214
- for (const ws of this.#watchers) ws.send(message);
67215
- }
67216
- /** commands@1: each event as a JSON message, those before it first. Nothing comes back. */
67217
- #watch(ws) {
67218
- for (const event of this.#events) ws.send(JSON.stringify(event));
67219
- this.#watchers.add(ws);
67220
- ws.on("close", () => this.#watchers.delete(ws));
67221
- }
67222
- /** pty@1: one shell for the computer, shared by every socket on it. */
67223
- async #attach(ws, mode) {
67224
- const loaded2 = await loadPty();
67225
- if (typeof loaded2 === "string") return ws.close(1011, loaded2);
67226
- if (!this.#shell) {
67227
- this.#scrollback = "";
67228
- let shell;
67382
+ for (const ws of this.#viewers) ws.close(1001, "It stopped.");
67383
+ }
67384
+ /**
67385
+ * terminal@1: every terminal on one socket. Those already here come first, each with the end of
67386
+ * what it wrote, and then each event as it happens. Listing and joining are one step, so nothing
67387
+ * falls between them.
67388
+ */
67389
+ #view(ws, mode, terminals) {
67390
+ for (const { id, by, cmd, workdir, tty, at, output: output2, exitCode, endedAt } of terminals.list()) {
67391
+ const events = [{ type: "start", id, by, cmd, workdir, tty, at }];
67392
+ if (output2) events.push({ type: "output", id, text: output2 });
67393
+ if (exitCode !== void 0 && endedAt)
67394
+ events.push({ type: "exit", id, exitCode, at: endedAt });
67395
+ for (const event of events)
67396
+ for (const message of messages(event)) ws.send(JSON.stringify(message));
67397
+ }
67398
+ this.#viewers.add(ws);
67399
+ ws.on("close", () => this.#viewers.delete(ws));
67400
+ if (mode !== "operate") return;
67401
+ const refuse = (why) => deliver(ws, { type: "refused", message: reason2(why) });
67402
+ ws.on("message", (data) => {
67229
67403
  try {
67230
- shell = loaded2.spawn(process.env.SHELL || "/bin/bash", ["-l"], {
67231
- name: "xterm-256color",
67232
- cols: 80,
67233
- rows: 24,
67234
- cwd: this.#workspace,
67235
- env: process.env
67236
- });
67404
+ const sent = JSON.parse(data.toString()) ?? {};
67405
+ const { type, id, text: text3 } = sent;
67406
+ const [columns, rows] = [sent.columns, sent.rows];
67407
+ if (type === "open") terminals.open({ columns, rows });
67408
+ else if (typeof id !== "string") throw new Error(MALFORMED);
67409
+ else if (type === "input" && typeof text3 === "string") terminals.write(id, text3);
67410
+ else if (type === "resize") terminals.resize(id, columns, rows);
67411
+ else if (type === "stop") terminals.stop(id).catch(refuse);
67412
+ else throw new Error(MALFORMED);
67237
67413
  } catch (error62) {
67238
- return ws.close(1011, `The shell didn't start: ${String(error62)}`.slice(0, 120));
67414
+ refuse(error62 instanceof SyntaxError ? MALFORMED : error62);
67239
67415
  }
67240
- shell.onData((data) => {
67241
- this.#scrollback = (this.#scrollback + data).slice(-MAX_SCROLLBACK);
67242
- const frame = Buffer.concat([Buffer.from([OUTPUT]), Buffer.from(data)]);
67243
- for (const client of this.#shells.keys()) client.send(frame);
67244
- });
67245
- shell.onExit(() => {
67246
- if (this.#shell !== shell) return;
67247
- this.#shell = void 0;
67248
- for (const client of this.#shells.keys()) client.close(1e3, "The shell exited.");
67249
- });
67250
- this.#shell = shell;
67251
- }
67252
- if (this.#scrollback)
67253
- ws.send(Buffer.concat([Buffer.from([OUTPUT]), Buffer.from(this.#scrollback)]));
67254
- this.#shells.set(ws, mode);
67255
- ws.on("close", () => this.#shells.delete(ws));
67256
- ws.on("message", (data, binary) => {
67257
- const frame = binary ? data : Buffer.from(data);
67258
- if (mode !== "operate" || frame.length === 0 || !this.#shell) return;
67259
- if (frame[0] === INPUT) this.#shell.write(frame.subarray(1).toString());
67260
- else if (frame[0] === RESIZE)
67261
- try {
67262
- const { columns, rows } = JSON.parse(frame.subarray(1).toString());
67263
- if (columns > 0 && rows > 0) this.#shell.resize(Math.floor(columns), Math.floor(rows));
67264
- } catch {
67265
- }
67266
67416
  });
67267
67417
  }
67268
67418
  };
67269
67419
 
67270
67420
  // src/store.ts
67271
67421
  import { createHash, randomBytes as randomBytes4, timingSafeEqual } from "node:crypto";
67272
- import { chmodSync as chmodSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
67273
- import { join as join6 } from "node:path";
67422
+ import { chmodSync, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
67423
+ import { join as join5 } from "node:path";
67274
67424
  var newToken = () => randomBytes4(32).toString("base64url");
67275
67425
  var digestOf = (token) => createHash("sha256").update(token).digest("hex");
67276
67426
  var sameToken = (token, digest) => timingSafeEqual(Buffer.from(digestOf(token), "hex"), Buffer.from(digest, "hex"));
67277
67427
  function writePrivate(file2, text3) {
67278
67428
  writeFileSync(`${file2}.tmp`, text3, { mode: 384, flush: true });
67279
67429
  renameSync(`${file2}.tmp`, file2);
67280
- chmodSync2(file2, 384);
67430
+ chmodSync(file2, 384);
67281
67431
  }
67282
67432
  var privateFolder = (dir) => {
67283
67433
  mkdirSync(dir, { recursive: true, mode: 448 });
67284
- chmodSync2(dir, 448);
67434
+ chmodSync(dir, 448);
67285
67435
  return dir;
67286
67436
  };
67287
67437
  function controlToken(data) {
67288
- const file2 = join6(data, "control-token");
67438
+ const file2 = join5(data, "control-token");
67289
67439
  try {
67290
67440
  return readFileSync2(file2, "utf8").trim();
67291
67441
  } catch (error62) {
@@ -67305,6 +67455,33 @@ var ApiError = class extends Error {
67305
67455
  this.status = status;
67306
67456
  }
67307
67457
  };
67458
+ var pty;
67459
+ var loadPty = () => {
67460
+ pty ??= (async () => {
67461
+ const fails = (why) => () => {
67462
+ throw new Error(why);
67463
+ };
67464
+ const require3 = createRequire3(import.meta.url);
67465
+ let loaded2;
67466
+ try {
67467
+ loaded2 = require3("node-pty");
67468
+ } catch {
67469
+ return fails("node-pty isn't installed with this service.");
67470
+ }
67471
+ const root = dirname3(require3.resolve("node-pty/package.json"));
67472
+ for (const helper of [
67473
+ join6(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
67474
+ join6(root, "build", "Release", "spawn-helper")
67475
+ ])
67476
+ try {
67477
+ if (existsSync(helper) && (statSync2(helper).mode & 73) === 0) chmodSync2(helper, 493);
67478
+ } catch {
67479
+ return fails(`${helper} has to be executable, and it isn't.`);
67480
+ }
67481
+ return loaded2.spawn;
67482
+ })();
67483
+ return pty;
67484
+ };
67308
67485
  var ProcessRuntime = class {
67309
67486
  isolation = "none";
67310
67487
  #config;
@@ -67320,11 +67497,15 @@ var ProcessRuntime = class {
67320
67497
  async start(spec, folder, events) {
67321
67498
  let exec;
67322
67499
  if (spec.access !== "none")
67323
- exec = createAgentExec({ workspace: spec.workspace, level: spec.access });
67500
+ exec = createAgentExec({
67501
+ workspace: spec.workspace,
67502
+ level: spec.access,
67503
+ pty: await loadPty()
67504
+ });
67324
67505
  let browser;
67325
67506
  let browserDown = false;
67326
67507
  if (spec.browser) {
67327
- const own3 = privateFolder(join7(folder, "browser"));
67508
+ const own3 = privateFolder(join6(folder, "browser"));
67328
67509
  browser = new Browser(
67329
67510
  this.#config,
67330
67511
  own3,
@@ -67348,8 +67529,8 @@ var ProcessRuntime = class {
67348
67529
  browserLive = new BrowserLive(browser.endpoint);
67349
67530
  browser.gate = browserLive;
67350
67531
  }
67351
- const live = new Live(exec, spec.workspace, spec.access, browserLive);
67352
- const surfaces = await live.surfaces();
67532
+ const live = new Live(exec, spec.access, browserLive);
67533
+ const surfaces = live.surfaces();
67353
67534
  return {
67354
67535
  browserTools: browser?.tools ?? [],
67355
67536
  /** As they are now: a browser that has stopped can't be opened, and says so. */
@@ -67390,7 +67571,7 @@ async function serveInside(options) {
67390
67571
  ...options,
67391
67572
  ...options.access === "none" ? {} : { workspace: "/workspace" }
67392
67573
  },
67393
- privateFolder(join8(homedir(), ".agentcomputer")),
67574
+ privateFolder(join7(homedir(), ".agentcomputer")),
67394
67575
  {
67395
67576
  log: (line) => process.stdout.write(`${line}
67396
67577
  `),
@@ -67425,7 +67606,7 @@ async function serveInside(options) {
67425
67606
  socket.on("error", () => socket.destroy());
67426
67607
  const given = /^Bearer (\S+)$/.exec(request.headers.authorization ?? "")?.[1];
67427
67608
  const url3 = new URL(request.url ?? "/", "http://localhost");
67428
- const surface = /^\/live\/(commands|terminal|files|browser)$/.exec(url3.pathname)?.[1];
67609
+ const surface = /^\/live\/(terminal|files|browser)$/.exec(url3.pathname)?.[1];
67429
67610
  const mode = url3.searchParams.get("mode");
67430
67611
  if (!given || !sameToken(given, digest) || !surface || mode !== "watch" && mode !== "operate")
67431
67612
  return void socket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
@@ -67533,7 +67714,7 @@ var ContainerRuntime = class {
67533
67714
  }
67534
67715
  /** The token between the service and this container, which a restart has to find again. */
67535
67716
  #tokenIn(folder) {
67536
- const file2 = join9(folder, "inner-token");
67717
+ const file2 = join8(folder, "inner-token");
67537
67718
  try {
67538
67719
  return readFileSync3(file2, "utf8");
67539
67720
  } catch {
@@ -67672,7 +67853,7 @@ async function forward(url3, authorization, request, response) {
67672
67853
  }
67673
67854
 
67674
67855
  // src/handlers.ts
67675
- var VERSION2 = "0.0.8";
67856
+ var VERSION2 = "0.0.9";
67676
67857
  function createHandlers(computers) {
67677
67858
  return {
67678
67859
  getInfo: async () => ({ version: VERSION2, isolation: computers.isolation }),
@@ -68254,32 +68435,30 @@ async function openLive(session, onMessage) {
68254
68435
  ws.addEventListener("message", (event) => onMessage(event.data));
68255
68436
  return { ws, closed };
68256
68437
  }
68257
- var INPUT2 = 48;
68258
- var RESIZE2 = 49;
68259
- var Pty = class {
68438
+ var Terminals = class {
68260
68439
  #open;
68261
68440
  constructor(open4) {
68262
68441
  this.#open = open4;
68263
68442
  }
68264
- /** Attach to its shell, starting one if none is running. Every attachment shares it. */
68443
+ /**
68444
+ * Each terminal as it starts, writes and exits: those already there first, each with the end of
68445
+ * what it wrote. Operating, the default, can also open a shell and act on any of them.
68446
+ */
68265
68447
  async connect(opts) {
68266
- const mode = opts.mode ?? "operate";
68267
- const decoder = new TextDecoder();
68268
- const { ws, closed } = await openLive(await this.#open("terminal", mode), (data) => {
68269
- if (typeof data === "string") return;
68270
- const frame = new Uint8Array(data);
68271
- if (frame[0] === INPUT2) opts.onData(decoder.decode(frame.subarray(1), { stream: true }));
68272
- });
68273
- const send2 = (type, text3) => {
68274
- const body = new TextEncoder().encode(text3);
68275
- const frame = new Uint8Array(body.length + 1);
68276
- frame[0] = type;
68277
- frame.set(body, 1);
68278
- ws.send(frame);
68279
- };
68448
+ const { ws, closed } = await openLive(
68449
+ await this.#open("terminal", opts.mode ?? "operate"),
68450
+ (data) => {
68451
+ if (typeof data === "string") opts.onEvent(JSON.parse(data));
68452
+ }
68453
+ );
68454
+ const send2 = (input2) => ws.send(JSON.stringify(input2));
68280
68455
  return {
68281
- sendInput: (data) => send2(INPUT2, data),
68282
- resize: (columns, rows) => send2(RESIZE2, JSON.stringify({ columns, rows })),
68456
+ openShell: (columns, rows) => send2({ type: "open", columns, rows }),
68457
+ sendInput: (id, text3) => {
68458
+ for (const piece of pieces(text3)) send2({ type: "input", id, text: piece });
68459
+ },
68460
+ resize: (id, columns, rows) => send2({ type: "resize", id, columns, rows }),
68461
+ stop: (id) => send2({ type: "stop", id }),
68283
68462
  close: () => ws.close(),
68284
68463
  closed
68285
68464
  };
@@ -68287,17 +68466,8 @@ var Pty = class {
68287
68466
  };
68288
68467
  var Commands = class {
68289
68468
  #endpoint;
68290
- #open;
68291
- constructor(endpoint, open4) {
68469
+ constructor(endpoint) {
68292
68470
  this.#endpoint = endpoint;
68293
- this.#open = open4;
68294
- }
68295
- /** Each of the agent's commands as it runs: the ones already heard first, then as they come. */
68296
- async watch(onEvent) {
68297
- const { ws, closed } = await openLive(await this.#open("commands", "watch"), (data) => {
68298
- if (typeof data === "string") onEvent(JSON.parse(data));
68299
- });
68300
- return { close: () => ws.close(), closed };
68301
68471
  }
68302
68472
  async run(cmd, opts = {}) {
68303
68473
  let answer = await this.#endpoint.command("exec_command", {
@@ -68374,7 +68544,7 @@ var Computer = class _Computer {
68374
68544
  computerID;
68375
68545
  commands;
68376
68546
  files;
68377
- pty;
68547
+ terminals;
68378
68548
  #connection;
68379
68549
  #token;
68380
68550
  #computer;
@@ -68385,9 +68555,8 @@ var Computer = class _Computer {
68385
68555
  this.#token = token;
68386
68556
  this.#urls = info.endpoints;
68387
68557
  this.#computer = new Endpoint(info.endpoints.computer, token);
68388
- const open4 = (surface, mode) => this.liveSession(surface, mode);
68389
- this.commands = new Commands(this.#computer, open4);
68390
- this.pty = new Pty(open4);
68558
+ this.commands = new Commands(this.#computer);
68559
+ this.terminals = new Terminals((surface, mode) => this.liveSession(surface, mode));
68391
68560
  this.files = new Files(this.#computer);
68392
68561
  }
68393
68562
  static async create(opts) {
@@ -68508,7 +68677,7 @@ import {
68508
68677
  STATUS_CODES as STATUS_CODES3
68509
68678
  } from "node:http";
68510
68679
  import { createServer as createTlsServer } from "node:https";
68511
- import { join as join11 } from "node:path";
68680
+ import { join as join10 } from "node:path";
68512
68681
 
68513
68682
  // node_modules/@modelcontextprotocol/server/dist/chunk-vYduTEsO.mjs
68514
68683
  var __create4 = Object.create;
@@ -75942,8 +76111,8 @@ var require_utils3 = /* @__PURE__ */ __commonJSMin3(((exports, module) => {
75942
76111
  }
75943
76112
  if (bestLength < 2) return hextets.join(":");
75944
76113
  const head = hextets.slice(0, bestStart).join(":");
75945
- const tail = hextets.slice(bestStart + bestLength).join(":");
75946
- return head + "::" + tail;
76114
+ const tail2 = hextets.slice(bestStart + bestLength).join(":");
76115
+ return head + "::" + tail2;
75947
76116
  }
75948
76117
  function normalizeIPv6Address(input2) {
75949
76118
  const compression = input2.indexOf("::");
@@ -82420,9 +82589,9 @@ var WebStandardStreamableHTTPServerTransport2 = class {
82420
82589
  }
82421
82590
  });
82422
82591
  }
82423
- async _checkScopeChallenge(messages, authInfo) {
82592
+ async _checkScopeChallenge(messages2, authInfo) {
82424
82593
  if (!this._scopeChallengeResolver) return;
82425
- const challenge = await findScopeChallenge2(messages.filter((message) => isJSONRPCRequest3(message)), authInfo, this._scopeChallengeResolver);
82594
+ const challenge = await findScopeChallenge2(messages2.filter((message) => isJSONRPCRequest3(message)), authInfo, this._scopeChallengeResolver);
82426
82595
  return challenge === void 0 ? void 0 : createScopeChallengeResponse2(challenge, scopeChallengeResourceMetadataUrl2(authInfo));
82427
82596
  }
82428
82597
  /**
@@ -82720,21 +82889,21 @@ data:
82720
82889
  this.onerror?.(/* @__PURE__ */ new Error(`Invalid Request: Batch must not exceed ${MAX_BATCH_SIZE2} messages`));
82721
82890
  return this.createJsonErrorResponse(400, -32600, `Invalid Request: Batch must not exceed ${MAX_BATCH_SIZE2} messages`);
82722
82891
  }
82723
- let messages;
82892
+ let messages2;
82724
82893
  try {
82725
- messages = Array.isArray(rawMessage) ? rawMessage.map((msg) => JSONRPCMessageSchema.parse(msg)) : [JSONRPCMessageSchema.parse(rawMessage)];
82894
+ messages2 = Array.isArray(rawMessage) ? rawMessage.map((msg) => JSONRPCMessageSchema.parse(msg)) : [JSONRPCMessageSchema.parse(rawMessage)];
82726
82895
  } catch (error62) {
82727
82896
  this.onerror?.(error62);
82728
82897
  return this.createJsonErrorResponse(400, -32700, "Parse error: Invalid JSON-RPC message");
82729
82898
  }
82730
82899
  if (this._closed) return this.createJsonErrorResponse(404, -32001, "Session not found");
82731
- const isInitializationRequest = messages.some((element) => isInitializeRequest3(element));
82900
+ const isInitializationRequest = messages2.some((element) => isInitializeRequest3(element));
82732
82901
  if (isInitializationRequest) {
82733
82902
  if (this._initialized && this.sessionId !== void 0) {
82734
82903
  this.onerror?.(/* @__PURE__ */ new Error("Invalid Request: Server already initialized"));
82735
82904
  return this.createJsonErrorResponse(400, -32600, "Invalid Request: Server already initialized");
82736
82905
  }
82737
- if (messages.length > 1) {
82906
+ if (messages2.length > 1) {
82738
82907
  this.onerror?.(/* @__PURE__ */ new Error("Invalid Request: Only one initialization request is allowed"));
82739
82908
  return this.createJsonErrorResponse(400, -32600, "Invalid Request: Only one initialization request is allowed");
82740
82909
  }
@@ -82751,21 +82920,21 @@ data:
82751
82920
  if (this._closed) return this.createJsonErrorResponse(404, -32001, "Session not found");
82752
82921
  let scopeChallengeResponse;
82753
82922
  try {
82754
- scopeChallengeResponse = await this._checkScopeChallenge(messages, options?.authInfo);
82923
+ scopeChallengeResponse = await this._checkScopeChallenge(messages2, options?.authInfo);
82755
82924
  } catch (error62) {
82756
82925
  this.onerror?.(error62);
82757
82926
  return this.createJsonErrorResponse(500, -32603, "Internal server error");
82758
82927
  }
82759
82928
  if (scopeChallengeResponse) return scopeChallengeResponse;
82760
- if (!messages.some((element) => isJSONRPCRequest3(element))) {
82761
- for (const message of messages) this.onmessage?.(message, {
82929
+ if (!messages2.some((element) => isJSONRPCRequest3(element))) {
82930
+ for (const message of messages2) this.onmessage?.(message, {
82762
82931
  authInfo: options?.authInfo,
82763
82932
  request
82764
82933
  });
82765
82934
  return new Response(null, { status: 202 });
82766
82935
  }
82767
82936
  const streamId = crypto.randomUUID();
82768
- const initRequest = messages.find((m) => isInitializeRequest3(m));
82937
+ const initRequest = messages2.find((m) => isInitializeRequest3(m));
82769
82938
  const clientProtocolVersion = initRequest ? initRequest.params.protocolVersion : req.headers.get("mcp-protocol-version") ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
82770
82939
  if (this._enableJsonResponse) return new Promise((resolve6) => {
82771
82940
  this._streamMapping.set(streamId, {
@@ -82774,8 +82943,8 @@ data:
82774
82943
  this._streamMapping.delete(streamId);
82775
82944
  }
82776
82945
  });
82777
- for (const message of messages) if (isJSONRPCRequest3(message)) this._requestToStreamMapping.set(message.id, streamId);
82778
- for (const message of messages) this.onmessage?.(message, {
82946
+ for (const message of messages2) if (isJSONRPCRequest3(message)) this._requestToStreamMapping.set(message.id, streamId);
82947
+ for (const message of messages2) this.onmessage?.(message, {
82779
82948
  authInfo: options?.authInfo,
82780
82949
  request
82781
82950
  });
@@ -82799,7 +82968,7 @@ data:
82799
82968
  "X-Accel-Buffering": "no"
82800
82969
  };
82801
82970
  if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
82802
- for (const message of messages) if (isJSONRPCRequest3(message)) {
82971
+ for (const message of messages2) if (isJSONRPCRequest3(message)) {
82803
82972
  this._streamMapping.set(streamId, {
82804
82973
  controller: streamController,
82805
82974
  encoder,
@@ -82815,7 +82984,7 @@ data:
82815
82984
  this._requestToStreamMapping.set(message.id, streamId);
82816
82985
  }
82817
82986
  await this.writePrimingEvent(streamController, encoder, streamId, clientProtocolVersion);
82818
- for (const message of messages) {
82987
+ for (const message of messages2) {
82819
82988
  let closeSSEStream;
82820
82989
  let closeStandaloneSSEStream;
82821
82990
  if (isJSONRPCRequest3(message) && this._eventStore && this.supportsEmptySSEData(clientProtocolVersion)) {
@@ -84324,11 +84493,11 @@ var NodeStreamableHTTPServerTransport2 = class {
84324
84493
  // src/computers.ts
84325
84494
  import { randomBytes as randomBytes5 } from "node:crypto";
84326
84495
  import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
84327
- import { isAbsolute, join as join10, resolve as resolve4 } from "node:path";
84496
+ import { isAbsolute, join as join9, resolve as resolve4 } from "node:path";
84328
84497
  var TOOLS = {
84329
84498
  read: ["read", "list", "search"],
84330
84499
  files: ["read", "list", "search", "edit"],
84331
- terminal: ["read", "list", "search", "edit", "exec_command", "write_stdin"]
84500
+ terminal: ["read", "list", "search", "edit", "exec_command", "write_stdin", "list_sessions"]
84332
84501
  };
84333
84502
  var MAX_LOG = 256 * 1024;
84334
84503
  var MAX_TIMER_S = 2e6;
@@ -84346,7 +84515,7 @@ var Computers = class {
84346
84515
  this.#data = data;
84347
84516
  this.#base = base;
84348
84517
  this.#runtime = runtime;
84349
- this.#file = join10(data, "computers.json");
84518
+ this.#file = join9(data, "computers.json");
84350
84519
  if (existsSync2(this.#file))
84351
84520
  for (const entry of JSON.parse(readFileSync4(this.#file, "utf8")))
84352
84521
  this.#stored.set(entry.id, entry);
@@ -84401,7 +84570,7 @@ var Computers = class {
84401
84570
  ...timeout === void 0 ? {} : { endAt: expiry(timeout) }
84402
84571
  };
84403
84572
  if (!workspace && access !== "none") {
84404
- stored.workspace = join10(this.#folder(id), "workspace");
84573
+ stored.workspace = join9(this.#folder(id), "workspace");
84405
84574
  mkdirSync2(stored.workspace, { recursive: true, mode: 448 });
84406
84575
  }
84407
84576
  this.#stored.set(id, stored);
@@ -84552,10 +84721,10 @@ var Computers = class {
84552
84721
  return stored;
84553
84722
  }
84554
84723
  #folder(id) {
84555
- return privateFolder(join10(this.#data, "computers", id));
84724
+ return privateFolder(join9(this.#data, "computers", id));
84556
84725
  }
84557
84726
  #logFile(id) {
84558
- return join10(this.#folder(id), "log");
84727
+ return join9(this.#folder(id), "log");
84559
84728
  }
84560
84729
  #log(id, line) {
84561
84730
  if (!this.#stored.has(id)) return;
@@ -84647,7 +84816,7 @@ var OPENAPI = {
84647
84816
  "openapi": "3.1.0",
84648
84817
  "info": {
84649
84818
  "title": "AgentComputer",
84650
- "version": "0.0.8",
84819
+ "version": "0.0.9",
84651
84820
  "description": "Computers for agents. This file is the source of truth: the HTTP API, the MCP control tools and the SDK are all generated from it. An operation with `x-mcp-tool` is also an MCP tool of that name.",
84652
84821
  "license": {
84653
84822
  "name": "Apache-2.0",
@@ -84968,7 +85137,7 @@ var OPENAPI = {
84968
85137
  "post": {
84969
85138
  "operationId": "createLiveSession",
84970
85139
  "x-mcp-tool": "computer_live",
84971
- "summary": "A one-time ticket to watch or operate one of its surfaces live: the agent's commands as they run, a shell of your own, its files, or its browser. Open its url as a WebSocket within 60 seconds.",
85140
+ "summary": "A one-time ticket to watch or operate one of its surfaces live: its terminals, its files, or its browser. Open its url as a WebSocket within 60 seconds.",
84972
85141
  "requestBody": {
84973
85142
  "required": true,
84974
85143
  "content": {
@@ -85099,7 +85268,7 @@ var OPENAPI = {
85099
85268
  "files",
85100
85269
  "terminal"
85101
85270
  ],
85102
- "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command and write_stdin. Below terminal, the browser's code-running tool is hidden."
85271
+ "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
85103
85272
  },
85104
85273
  "ComputerState": {
85105
85274
  "type": "string",
@@ -85307,12 +85476,11 @@ var OPENAPI = {
85307
85476
  "Surface": {
85308
85477
  "type": "string",
85309
85478
  "enum": [
85310
- "commands",
85311
85479
  "terminal",
85312
85480
  "files",
85313
85481
  "browser"
85314
85482
  ],
85315
- "description": "commands: the agent's commands as they run, read-only, in commands@1. terminal: a shell of your own in its workspace, as its user, in pty@1. files: its workspace's files, in files@1: text messages are FilesRequest one way and FilesAnswer the other, and a file's bytes travel as binary messages of at most 1,048,576 bytes. Watching lists and reads; operating also writes, which a computer with read access doesn't offer. browser: its browser, in screencast@1: binary messages are JPEG frames of the page shown, text messages are ScreencastEvent one way and ScreencastInput the other. Nobody waits for anybody: the agent's tools and a person's input act on the same browser at once. The page shown is each socket's own. A socket is shown each new tab as it opens, until it sends a tab input to show or open one; from then its page changes only when that page closes."
85483
+ "description": "terminal: every terminal on it, the agent's commands and a person's shells alike, on one socket in terminal@1: text messages are TerminalEvent one way and TerminalInput the other. Watching reads them all; operating also opens a shell, types into any of them, the agent's included, sizes and stops them. files: its workspace's files, in files@1: text messages are FilesRequest one way and FilesAnswer the other, and a file's bytes travel as binary messages of at most 1,048,576 bytes. Watching lists and reads; operating also writes, which a computer with read access doesn't offer. browser: its browser, in screencast@1: binary messages are JPEG frames of the page shown, text messages are ScreencastEvent one way and ScreencastInput the other. Nobody waits for anybody: the agent's tools and a person's input act on the same browser at once. The page shown is each socket's own. A socket is shown each new tab as it opens, until it sends a tab input to show or open one; from then its page changes only when that page closes."
85316
85484
  },
85317
85485
  "LiveMode": {
85318
85486
  "type": "string",
@@ -85755,6 +85923,218 @@ var OPENAPI = {
85755
85923
  }
85756
85924
  ]
85757
85925
  },
85926
+ "TerminalEvent": {
85927
+ "description": "What the service sends on a terminal@1 socket, as JSON text, no message over 65,536 bytes. A terminal is a command the agent started or a shell a person opened; its id is the session id the agent's tools use. A new socket is first sent, for each terminal already there, the oldest first, its start, the end of what it wrote (its last 65,536 characters), and its exit if it has ended; then each event as it happens, with nothing to mark where the first ends. The last 64 that ended are among those already there, and one that leaves that list is not told. A character here is a UTF-16 unit. A socket with more than 4 MiB still to be sent to it is closed, with no close frame (1006): one that has stopped reading, or one that is reading while a terminal writes tens of megabytes at once. It isn't an error. Opened again, it is sent every terminal afresh, with the last 65,536 characters each wrote, in place of whatever it held.",
85928
+ "oneOf": [
85929
+ {
85930
+ "type": "object",
85931
+ "description": "A terminal began.",
85932
+ "required": [
85933
+ "type",
85934
+ "id",
85935
+ "by",
85936
+ "cmd",
85937
+ "workdir",
85938
+ "tty",
85939
+ "at"
85940
+ ],
85941
+ "properties": {
85942
+ "type": {
85943
+ "const": "start"
85944
+ },
85945
+ "id": {
85946
+ "type": "string"
85947
+ },
85948
+ "by": {
85949
+ "type": "string",
85950
+ "enum": [
85951
+ "agent",
85952
+ "person"
85953
+ ],
85954
+ "description": "The way in it came: agent through a tool, person through this surface."
85955
+ },
85956
+ "cmd": {
85957
+ "type": "string",
85958
+ "description": "Its command, the first 8,000 characters of it."
85959
+ },
85960
+ "workdir": {
85961
+ "type": "string",
85962
+ "description": "Its folder, the first 2,000 characters of it."
85963
+ },
85964
+ "tty": {
85965
+ "type": "boolean",
85966
+ "description": "Whether it is on a terminal device, as a person's shell is. One that isn't, as the agent's commands aren't, echoes nothing typed into it and wants a line feed for Enter. Nothing tells the size of one that is."
85967
+ },
85968
+ "at": {
85969
+ "type": "string",
85970
+ "format": "date-time"
85971
+ }
85972
+ }
85973
+ },
85974
+ {
85975
+ "type": "object",
85976
+ "description": "What it wrote, as it writes: 10,000 characters a message at most, a longer piece as several. A command that left something running can still write after its exit.",
85977
+ "required": [
85978
+ "type",
85979
+ "id",
85980
+ "text"
85981
+ ],
85982
+ "properties": {
85983
+ "type": {
85984
+ "const": "output"
85985
+ },
85986
+ "id": {
85987
+ "type": "string"
85988
+ },
85989
+ "stream": {
85990
+ "type": "string",
85991
+ "enum": [
85992
+ "stdout",
85993
+ "stderr"
85994
+ ],
85995
+ "description": "Left out for one on a terminal device, which has one stream, and in what a new socket is first sent, where the two are one text."
85996
+ },
85997
+ "text": {
85998
+ "type": "string"
85999
+ }
86000
+ }
86001
+ },
86002
+ {
86003
+ "type": "object",
86004
+ "description": "It ended.",
86005
+ "required": [
86006
+ "type",
86007
+ "id",
86008
+ "exitCode",
86009
+ "at"
86010
+ ],
86011
+ "properties": {
86012
+ "type": {
86013
+ "const": "exit"
86014
+ },
86015
+ "id": {
86016
+ "type": "string"
86017
+ },
86018
+ "exitCode": {
86019
+ "type": "integer"
86020
+ },
86021
+ "at": {
86022
+ "type": "string",
86023
+ "format": "date-time"
86024
+ }
86025
+ }
86026
+ },
86027
+ {
86028
+ "type": "object",
86029
+ "description": "What this socket asked wasn't done, and why, in words for a person. Sent only to the operate socket that asked. What was done is answered by its event, or by nothing.",
86030
+ "required": [
86031
+ "type",
86032
+ "message"
86033
+ ],
86034
+ "properties": {
86035
+ "type": {
86036
+ "const": "refused"
86037
+ },
86038
+ "message": {
86039
+ "type": "string"
86040
+ }
86041
+ }
86042
+ }
86043
+ ]
86044
+ },
86045
+ "TerminalInput": {
86046
+ "description": "What an operate socket sends on terminal@1, as JSON text. One that can't be done, or that is none of these, is answered with refused. What a watch socket sends is dropped. A message over 65,536 bytes closes the socket, so a long text goes as several inputs.",
86047
+ "oneOf": [
86048
+ {
86049
+ "type": "object",
86050
+ "description": "Start a shell of your own in the workspace, as its user, on a terminal device of this size. It arrives as a start like any other. Refused past the 64 terminals that may run at once, and where the service has no node-pty.",
86051
+ "required": [
86052
+ "type",
86053
+ "columns",
86054
+ "rows"
86055
+ ],
86056
+ "properties": {
86057
+ "type": {
86058
+ "const": "open"
86059
+ },
86060
+ "columns": {
86061
+ "type": "integer",
86062
+ "minimum": 1,
86063
+ "maximum": 1e3
86064
+ },
86065
+ "rows": {
86066
+ "type": "integer",
86067
+ "minimum": 1,
86068
+ "maximum": 1e3
86069
+ }
86070
+ }
86071
+ },
86072
+ {
86073
+ "type": "object",
86074
+ "description": "Type into a running terminal, the agent's included. In a command, Ctrl-C (U+0003) interrupts it and Ctrl-D (U+0004) ends its input. Refused for one that has ended or isn't there.",
86075
+ "required": [
86076
+ "type",
86077
+ "id",
86078
+ "text"
86079
+ ],
86080
+ "properties": {
86081
+ "type": {
86082
+ "const": "input"
86083
+ },
86084
+ "id": {
86085
+ "type": "string"
86086
+ },
86087
+ "text": {
86088
+ "type": "string"
86089
+ }
86090
+ }
86091
+ },
86092
+ {
86093
+ "type": "object",
86094
+ "description": "Give a terminal on a terminal device a new size, which is the size for everyone looking at it. Refused for a command, which has none, and for one that has ended or isn't there.",
86095
+ "required": [
86096
+ "type",
86097
+ "id",
86098
+ "columns",
86099
+ "rows"
86100
+ ],
86101
+ "properties": {
86102
+ "type": {
86103
+ "const": "resize"
86104
+ },
86105
+ "id": {
86106
+ "type": "string"
86107
+ },
86108
+ "columns": {
86109
+ "type": "integer",
86110
+ "minimum": 1,
86111
+ "maximum": 1e3
86112
+ },
86113
+ "rows": {
86114
+ "type": "integer",
86115
+ "minimum": 1,
86116
+ "maximum": 1e3
86117
+ }
86118
+ }
86119
+ },
86120
+ {
86121
+ "type": "object",
86122
+ "description": "End a terminal: an interrupt for a command, a hangup for a shell, then a kill a second later. Its exit is the answer. One that has ended is left as it is, and one that isn't there is refused.",
86123
+ "required": [
86124
+ "type",
86125
+ "id"
86126
+ ],
86127
+ "properties": {
86128
+ "type": {
86129
+ "const": "stop"
86130
+ },
86131
+ "id": {
86132
+ "type": "string"
86133
+ }
86134
+ }
86135
+ }
86136
+ ]
86137
+ },
85758
86138
  "FilesRequest": {
85759
86139
  "description": "What a socket sends on files@1, as JSON text. Each has an id of its own choosing, which the answer repeats; answers come as they are ready, in any order, and ids aren't checked for repeats. A request still unanswered when the socket closes gets no answer. A path is from the workspace, with / between its parts, or absolute inside it, and nothing outside the workspace is reached. A binary message that follows no write closes the socket, and so does any message over 1,048,576 bytes.",
85760
86140
  "oneOf": [
@@ -86224,7 +86604,7 @@ var OPERATIONS = [
86224
86604
  "files",
86225
86605
  "terminal"
86226
86606
  ],
86227
- "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command and write_stdin. Below terminal, the browser's code-running tool is hidden."
86607
+ "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
86228
86608
  },
86229
86609
  "browser": {
86230
86610
  "type": "boolean"
@@ -86313,7 +86693,7 @@ var OPERATIONS = [
86313
86693
  "files",
86314
86694
  "terminal"
86315
86695
  ],
86316
- "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command and write_stdin. Below terminal, the browser's code-running tool is hidden."
86696
+ "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
86317
86697
  },
86318
86698
  "browser": {
86319
86699
  "type": "boolean",
@@ -86383,7 +86763,7 @@ var OPERATIONS = [
86383
86763
  "files",
86384
86764
  "terminal"
86385
86765
  ],
86386
- "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command and write_stdin. Below terminal, the browser's code-running tool is hidden."
86766
+ "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
86387
86767
  },
86388
86768
  "browser": {
86389
86769
  "type": "boolean"
@@ -86523,7 +86903,7 @@ var OPERATIONS = [
86523
86903
  "files",
86524
86904
  "terminal"
86525
86905
  ],
86526
- "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command and write_stdin. Below terminal, the browser's code-running tool is hidden."
86906
+ "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
86527
86907
  },
86528
86908
  "browser": {
86529
86909
  "type": "boolean"
@@ -86607,12 +86987,11 @@ var OPERATIONS = [
86607
86987
  "surface": {
86608
86988
  "type": "string",
86609
86989
  "enum": [
86610
- "commands",
86611
86990
  "terminal",
86612
86991
  "files",
86613
86992
  "browser"
86614
86993
  ],
86615
- "description": "commands: the agent's commands as they run, read-only, in commands@1. terminal: a shell of your own in its workspace, as its user, in pty@1. files: its workspace's files, in files@1: text messages are FilesRequest one way and FilesAnswer the other, and a file's bytes travel as binary messages of at most 1,048,576 bytes. Watching lists and reads; operating also writes, which a computer with read access doesn't offer. browser: its browser, in screencast@1: binary messages are JPEG frames of the page shown, text messages are ScreencastEvent one way and ScreencastInput the other. Nobody waits for anybody: the agent's tools and a person's input act on the same browser at once. The page shown is each socket's own. A socket is shown each new tab as it opens, until it sends a tab input to show or open one; from then its page changes only when that page closes."
86994
+ "description": "terminal: every terminal on it, the agent's commands and a person's shells alike, on one socket in terminal@1: text messages are TerminalEvent one way and TerminalInput the other. Watching reads them all; operating also opens a shell, types into any of them, the agent's included, sizes and stops them. files: its workspace's files, in files@1: text messages are FilesRequest one way and FilesAnswer the other, and a file's bytes travel as binary messages of at most 1,048,576 bytes. Watching lists and reads; operating also writes, which a computer with read access doesn't offer. browser: its browser, in screencast@1: binary messages are JPEG frames of the page shown, text messages are ScreencastEvent one way and ScreencastInput the other. Nobody waits for anybody: the agent's tools and a person's input act on the same browser at once. The page shown is each socket's own. A socket is shown each new tab as it opens, until it sends a tab input to show or open one; from then its page changes only when that page closes."
86616
86995
  },
86617
86996
  "protocol": {
86618
86997
  "type": "string"
@@ -86754,7 +87133,7 @@ var OPERATIONS = [
86754
87133
  "files",
86755
87134
  "terminal"
86756
87135
  ],
86757
- "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command and write_stdin. Below terminal, the browser's code-running tool is hidden."
87136
+ "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
86758
87137
  },
86759
87138
  "browser": {
86760
87139
  "type": "boolean"
@@ -86878,7 +87257,7 @@ var OPERATIONS = [
86878
87257
  "files",
86879
87258
  "terminal"
86880
87259
  ],
86881
- "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command and write_stdin. Below terminal, the browser's code-running tool is hidden."
87260
+ "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
86882
87261
  },
86883
87262
  "browser": {
86884
87263
  "type": "boolean"
@@ -87007,7 +87386,7 @@ var OPERATIONS = [
87007
87386
  "files",
87008
87387
  "terminal"
87009
87388
  ],
87010
- "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command and write_stdin. Below terminal, the browser's code-running tool is hidden."
87389
+ "description": "How far it reaches into files and commands, each level containing the one before: `read` has read, list and search; `files` adds edit; `terminal` adds exec_command, write_stdin and list_sessions. Below terminal, the browser's code-running tool is hidden."
87011
87390
  },
87012
87391
  "browser": {
87013
87392
  "type": "boolean"
@@ -87128,7 +87507,7 @@ var OPERATIONS = [
87128
87507
  "method": "POST",
87129
87508
  "path": "/v1/computers/{computerID}/live",
87130
87509
  "tool": "computer_live",
87131
- "summary": "A one-time ticket to watch or operate one of its surfaces live: the agent's commands as they run, a shell of your own, its files, or its browser. Open its url as a WebSocket within 60 seconds.",
87510
+ "summary": "A one-time ticket to watch or operate one of its surfaces live: its terminals, its files, or its browser. Open its url as a WebSocket within 60 seconds.",
87132
87511
  "status": 201,
87133
87512
  "hasBody": true,
87134
87513
  "pathParameters": [
@@ -87147,12 +87526,11 @@ var OPERATIONS = [
87147
87526
  "surface": {
87148
87527
  "type": "string",
87149
87528
  "enum": [
87150
- "commands",
87151
87529
  "terminal",
87152
87530
  "files",
87153
87531
  "browser"
87154
87532
  ],
87155
- "description": "commands: the agent's commands as they run, read-only, in commands@1. terminal: a shell of your own in its workspace, as its user, in pty@1. files: its workspace's files, in files@1: text messages are FilesRequest one way and FilesAnswer the other, and a file's bytes travel as binary messages of at most 1,048,576 bytes. Watching lists and reads; operating also writes, which a computer with read access doesn't offer. browser: its browser, in screencast@1: binary messages are JPEG frames of the page shown, text messages are ScreencastEvent one way and ScreencastInput the other. Nobody waits for anybody: the agent's tools and a person's input act on the same browser at once. The page shown is each socket's own. A socket is shown each new tab as it opens, until it sends a tab input to show or open one; from then its page changes only when that page closes."
87533
+ "description": "terminal: every terminal on it, the agent's commands and a person's shells alike, on one socket in terminal@1: text messages are TerminalEvent one way and TerminalInput the other. Watching reads them all; operating also opens a shell, types into any of them, the agent's included, sizes and stops them. files: its workspace's files, in files@1: text messages are FilesRequest one way and FilesAnswer the other, and a file's bytes travel as binary messages of at most 1,048,576 bytes. Watching lists and reads; operating also writes, which a computer with read access doesn't offer. browser: its browser, in screencast@1: binary messages are JPEG frames of the page shown, text messages are ScreencastEvent one way and ScreencastInput the other. Nobody waits for anybody: the agent's tools and a person's input act on the same browser at once. The page shown is each socket's own. A socket is shown each new tab as it opens, until it sends a tab input to show or open one; from then its page changes only when that page closes."
87156
87534
  },
87157
87535
  "mode": {
87158
87536
  "allOf": [
@@ -87194,12 +87572,11 @@ var OPERATIONS = [
87194
87572
  "surface": {
87195
87573
  "type": "string",
87196
87574
  "enum": [
87197
- "commands",
87198
87575
  "terminal",
87199
87576
  "files",
87200
87577
  "browser"
87201
87578
  ],
87202
- "description": "commands: the agent's commands as they run, read-only, in commands@1. terminal: a shell of your own in its workspace, as its user, in pty@1. files: its workspace's files, in files@1: text messages are FilesRequest one way and FilesAnswer the other, and a file's bytes travel as binary messages of at most 1,048,576 bytes. Watching lists and reads; operating also writes, which a computer with read access doesn't offer. browser: its browser, in screencast@1: binary messages are JPEG frames of the page shown, text messages are ScreencastEvent one way and ScreencastInput the other. Nobody waits for anybody: the agent's tools and a person's input act on the same browser at once. The page shown is each socket's own. A socket is shown each new tab as it opens, until it sends a tab input to show or open one; from then its page changes only when that page closes."
87579
+ "description": "terminal: every terminal on it, the agent's commands and a person's shells alike, on one socket in terminal@1: text messages are TerminalEvent one way and TerminalInput the other. Watching reads them all; operating also opens a shell, types into any of them, the agent's included, sizes and stops them. files: its workspace's files, in files@1: text messages are FilesRequest one way and FilesAnswer the other, and a file's bytes travel as binary messages of at most 1,048,576 bytes. Watching lists and reads; operating also writes, which a computer with read access doesn't offer. browser: its browser, in screencast@1: binary messages are JPEG frames of the page shown, text messages are ScreencastEvent one way and ScreencastInput the other. Nobody waits for anybody: the agent's tools and a person's input act on the same browser at once. The page shown is each socket's own. A socket is shown each new tab as it opens, until it sends a tab input to show or open one; from then its page changes only when that page closes."
87203
87580
  },
87204
87581
  "mode": {
87205
87582
  "type": "string",
@@ -87419,7 +87796,7 @@ ${JSON.stringify({ code: status, message })}
87419
87796
  const scheme = options.tls ? "https" : "http";
87420
87797
  base = options.publicUrl?.replace(/\/+$/, "") ?? `${scheme}://${bound.family === "IPv6" ? `[${bound.address}]` : bound.address}:${bound.port}`;
87421
87798
  await computers.resume();
87422
- const record3 = join11(data, "server.json");
87799
+ const record3 = join10(data, "server.json");
87423
87800
  writePrivate(
87424
87801
  record3,
87425
87802
  JSON.stringify({ url: base, pid: process.pid, isolation: options.runtime.isolation })
@@ -87459,7 +87836,7 @@ var USAGE = `Usage: agentcomputer <command> [--data DIR]
87459
87836
  stop <id> stop it, keeping its folder and token
87460
87837
  start <id> [--timeout SECONDS] start a stopped computer again
87461
87838
  logs <id> what its servers wrote
87462
- live <id> --surface commands|terminal|files|browser [--mode watch|operate]
87839
+ live <id> --surface terminal|files|browser [--mode watch|operate]
87463
87840
  a one-time ticket to open one live, as a WebSocket
87464
87841
  kill <id> stop it and remove what the service made
87465
87842
 
@@ -87472,13 +87849,13 @@ var UsageError = class extends Error {
87472
87849
  function dataFolder(given) {
87473
87850
  const xdg = process.env.XDG_DATA_HOME;
87474
87851
  return resolve5(
87475
- given ?? process.env.AGENTCOMPUTER_DATA ?? (xdg && isAbsolute2(xdg) ? join12(xdg, "agentcomputer") : join12(homedir2(), ".local", "share", "agentcomputer"))
87852
+ given ?? process.env.AGENTCOMPUTER_DATA ?? (xdg && isAbsolute2(xdg) ? join11(xdg, "agentcomputer") : join11(homedir2(), ".local", "share", "agentcomputer"))
87476
87853
  );
87477
87854
  }
87478
87855
  function connectionTo(data) {
87479
87856
  try {
87480
- const { url: url3 } = JSON.parse(readFileSync6(join12(data, "server.json"), "utf8"));
87481
- return { apiUrl: url3, apiKey: readFileSync6(join12(data, "control-token"), "utf8").trim() };
87857
+ const { url: url3 } = JSON.parse(readFileSync6(join11(data, "server.json"), "utf8"));
87858
+ return { apiUrl: url3, apiKey: readFileSync6(join11(data, "control-token"), "utf8").trim() };
87482
87859
  } catch {
87483
87860
  throw new Error(
87484
87861
  `No service is running with its data in ${data}. Start one: agentcomputer serve`