ai-remote 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,6 +35,46 @@ npx ai-remote exec "systemctl status nginx"
35
35
  npx ai-remote close
36
36
  ```
37
37
 
38
+ ### Agent computer use over VNC and RDP
39
+
40
+ Open a session once, then reuse it from a persistent Node runtime:
41
+
42
+ ```js
43
+ import { RemoteComputer, desktopPoint } from 'ai-remote/computer';
44
+
45
+ const desktop = new RemoteComputer('my-session');
46
+ const image = await desktop.screenshot({ maxEdge: 1000 });
47
+ // Inspect the PNG in image.base64, then choose a point in screenshot pixels.
48
+ const point = desktopPoint(image, { x: 100, y: 150 });
49
+ const after = await desktop.act([
50
+ { type: 'click', ...point },
51
+ { type: 'type', text: 'literal text; including semicolons' },
52
+ ]);
53
+ // after.base64 is the new PNG, ready to return to a vision-capable agent.
54
+ ```
55
+
56
+ The same API drives either protocol. `act` batches input and returns one
57
+ in-memory screenshot; `dispatch` skips capture for a known sequence. Keep the
58
+ JavaScript handle and your own variables across calls. No CLI subprocess or
59
+ new remote connection is needed for each action.
60
+
61
+ For CLI callers, put an action array in a JSON file:
62
+
63
+ ```bash
64
+ ai-remote batch actions.json --session my-session --shot after.png --json
65
+ ```
66
+
67
+ Actions include repeated clicks (`count`, optional `intervalMs`), pointer moves,
68
+ scrolling, literal text, keyboard chords, waits, and drag paths. See
69
+ [the agent skill](SKILL.md#structured-batches-and-a-reusable-javascript-client)
70
+ for schemas, pacing, and coordinate rules. JSON input is validated before
71
+ execution, and desktop IPC batches do not interleave with each other.
72
+
73
+ This supplies remote pixels and input; it does not add browser DOM access,
74
+ native accessibility trees, or a model. `changed` and `quiet` describe observed
75
+ pixels, not completion of the application's work. Verify results after bursts
76
+ and observe before choosing actions that depend on a new screen.
77
+
38
78
  ### What a command costs, and where the time actually goes
39
79
 
40
80
  Measured on an M4, `list` with two sessions open:
package/SKILL.md CHANGED
@@ -53,25 +53,104 @@ prints are npm's own and harmless — ignore them, or add `--silent`.
53
53
 
54
54
  ## Working loop
55
55
 
56
- Act, then look. The session holds the framebuffer, so a screenshot taken after a
57
- click shows what the click did.
56
+ ### Structured batches and a reusable JavaScript client
57
+
58
+ Prefer `batch` for generated actions and literal text. Unlike `do`, JSON text
59
+ is never split on semicolons, and the entire batch schema and coordinates are
60
+ validated before input starts. Both VNC and RDP use the same action API.
61
+
62
+ ```json
63
+ [
64
+ { "type": "click", "x": 168, "y": 185, "count": 50 },
65
+ { "type": "wait", "ms": 500 }
66
+ ]
67
+ ```
68
+
69
+ Save this as `actions.json`, then run:
58
70
 
59
71
  ```bash
60
- npx ai-remote key MetaLeft # open the Start menu
61
- npx ai-remote shot -o after.png # see what happened
72
+ ai-remote batch actions.json --session test --shot result.png --region 8,95,320,180 --json
73
+ ```
74
+
75
+ `batch -` reads JSON from stdin. Without `--shot`, dispatch finishes without
76
+ waiting for a screenshot. A click's optional `intervalMs` controls pacing;
77
+ its default is zero. Fifty inputs dispatched is not proof of fifty inputs
78
+ processed: verify the application's resulting count. A burst may still be
79
+ drawing when the first screenshot arrives. Add a workload-specific `wait`
80
+ inside the batch or take another observation when that happens.
81
+
82
+ Supported actions: `click` (`x`, `y`, optional `button`, `count`, `intervalMs`),
83
+ `move` (`x`, `y`), `scroll` (`x`, `y`, `dx`, `dy`), `type` (`text`, optional
84
+ `chunkSize`, `intervalMs`), `key` (`chord`), `wait` (`ms`), and `drag` (`path`
85
+ of `{x,y}` points, optional `button`, `intervalMs`). Buttons are left=0,
86
+ middle=1, right=2. Drag releases its button even if a step fails. Text defaults
87
+ to eight Unicode code points per chunk with 20 ms between chunks; zero pacing
88
+ can be selected explicitly. This avoids the observed unpaced long-input issue
89
+ in the RDP Chrome omnibox without slowing clicks.
90
+
91
+ For a persistent Node/JavaScript execution environment:
92
+
93
+ ```js
94
+ import { RemoteComputer, desktopPoint } from 'ai-remote/computer';
95
+ const desktop = new RemoteComputer('test'); // session already opened by CLI
96
+ const image = await desktop.screenshot({ maxEdge: 800 });
97
+ // Inspect image.base64 as PNG before choosing a point in that image.
98
+ const point = desktopPoint(image, { x: 100, y: 100 });
99
+ const after = await desktop.act([{ type: 'click', ...point }]);
100
+ // Return after.base64 directly to the vision-capable caller.
101
+ ```
102
+
103
+ Keep `desktop` and variables in the caller's JavaScript runtime. `act` returns
104
+ PNG base64 and geometry directly, with no screenshot file or CLI subprocess.
105
+ `dispatch` takes the same actions and skips observation. The remote connection
106
+ stays in the existing daemon; individual API calls use local IPC. Desktop IPC
107
+ batches and screenshots are serialized; a live human/viewer can still interact.
108
+ This is a pixel/input API, not a browser DOM or native accessibility tree.
109
+
110
+ Act, then look, in one command. `--shot` runs the steps and captures the frame
111
+ they produced, so the two halves of one observation cost one round trip:
112
+
113
+ ```bash
114
+ npx ai-remote key MetaLeft --shot after.png # open the Start menu, and see it
115
+ npx ai-remote click 640,400 --shot after.png
116
+ npx ai-remote do "type hello; key Enter" --shot after.png
117
+ ```
118
+
119
+ The capture waits for the frame the steps caused, not for a fixed delay, and it
120
+ tells you what it found:
121
+
122
+ ```
123
+ Pressed MetaLeft. Wrote after.png (1280x800).
124
+ Pressed MetaLeft. Wrote after.png (1280x800), though nothing on screen changed.
62
125
  ```
63
126
 
64
- `shot` waits for the screen to stop changing before it captures, so there is
65
- normally no need to sleep first. When an action starts something slow (an app
66
- launching, a page loading), put an explicit wait in a `do` script instead of
67
- guessing from outside.
127
+ **Read that second line.** "Nothing on screen changed" usually means the click
128
+ landed somewhere that does nothing the wrong coordinates, a window that was
129
+ not focused rather than that the screenshot is stale. Look again before
130
+ repeating the click.
131
+
132
+ It can also mean the action's only mark was too small to tell apart from what
133
+ the desktop does on its own: clicking into a text field paints one caret, and a
134
+ caret is smaller than a clock ticking. The picture is still the current one.
135
+
136
+ "The screen was still moving" is not a failure: the picture is current, it was
137
+ just taken while something was animating. Take another if it matters.
138
+
139
+ A bare `npx ai-remote shot -o after.png` still works and is the right thing when
140
+ you have not just done something. When an action starts something slow — an app
141
+ launching, a page loading — put an explicit wait in a `do` script rather than
142
+ guessing from outside:
143
+
144
+ ```bash
145
+ npx ai-remote do "key MetaLeft; wait 800; type notepad; key Enter; wait 1500" --shot s.png
146
+ ```
68
147
 
69
148
  Prefer `do` when the next few steps are already known — one round trip instead
70
149
  of four, and the timing between steps stays inside the session rather than
71
150
  depending on how fast the shell gets round to the next command:
72
151
 
73
152
  ```bash
74
- npx ai-remote do "key MetaLeft; wait 800; type notepad; wait 1200; key Enter; wait 1500; shot s.png"
153
+ npx ai-remote do "key MetaLeft; wait 800; type notepad; wait 1200; key Enter; wait 1500" --shot s.png
75
154
  ```
76
155
 
77
156
  Steps, separated by `;`:
@@ -114,6 +193,34 @@ screenshot full size and click the coordinates you read off it.
114
193
  image tokens and a 800px one is a fraction of that, usually with no loss of
115
194
  anything you needed to see.
116
195
 
196
+ ### Focus on one application
197
+
198
+ For a known window that stays in place, `--region X,Y,W,H` captures only that
199
+ rectangle. It works on `shot` and on commands with `--shot`. Use `--json` to
200
+ retain its coordinate origin:
201
+
202
+ ```bash
203
+ ai-remote click 544,705 --shot result.png --region 520,538,208,351 --json
204
+ ```
205
+
206
+ Coordinates sent to input remain full-desktop pixels. For a returned cropped
207
+ image, map using `desktop_x = viewport.x + image_x * viewport.width / width`
208
+ and the equivalent formula for y. `width` and `height` are the returned image
209
+ size; `desktopWidth` and `desktopHeight` are the whole desktop. The full-screen
210
+ scaling formula above does not apply to cropped images.
211
+
212
+ Invalid or out-of-bounds regions are refused before an atomic action runs.
213
+ Re-observe the full desktop if the window moves or a dialog can appear outside
214
+ the crop. A crop is explicit geometry, not automatic window tracking.
215
+
216
+ A region also says which part of the screen you care about, so the wait for the
217
+ screen to settle watches that rectangle and ignores the rest. On a machine with
218
+ a build log scrolling in one corner, that is the difference between a capture
219
+ that returns promptly and one that waits for a screen that is never still. The
220
+ trade is the other way round too: a change *outside* the region no longer holds
221
+ the capture back, so re-observe the whole desktop when something may have
222
+ happened elsewhere.
223
+
117
224
  ## Modifier keys between a Mac and a PC
118
225
 
119
226
  When one end is a Mac and the other is not, **Control and Command swap** so the
@@ -433,10 +540,13 @@ npx ai-remote open HOST --ssh -u USER [-i ~/.ssh/id_ed25519] # a terminal,
433
540
  npx ai-remote open HOST --vnc -u USER # a screen over VNC
434
541
  npx ai-remote vnc [--vnc-port N] # ...or add one to this session
435
542
  npx ai-remote shot [-o FILE] [--max-edge N] [--json]
436
- npx ai-remote click X,Y [--button right|middle] [--double]
437
- npx ai-remote type "text"
438
- npx ai-remote key ControlLeft+KeyA
439
- npx ai-remote do "key MetaLeft; wait 800; type notepad; shot s.png"
543
+ npx ai-remote click X,Y [--button right|middle] [--double] [--shot FILE]
544
+ npx ai-remote type "text" [--shot FILE]
545
+ npx ai-remote key ControlLeft+KeyA [--shot FILE]
546
+ npx ai-remote do "key MetaLeft; wait 800; type notepad" [--shot FILE]
547
+ # --shot: act and look in one round trip
548
+ # bare --shot writes screen.png; a name that is
549
+ # not a .png goes as --shot=NAME
440
550
  npx ai-remote exec "command" [--reconnect]
441
551
  npx ai-remote reconnect # a new login: fresh PATH
442
552
  npx ai-remote clipboard [on|off] # share text both ways
@@ -1,7 +1,11 @@
1
1
  import {
2
2
  SshSession,
3
3
  TcpTransport
4
- } from "./cli-chunk-S2TWL5LF.mjs";
4
+ } from "./cli-chunk-UD2YES3P.mjs";
5
+ import {
6
+ checkHostKey,
7
+ hostAddress
8
+ } from "./cli-chunk-WWORJ2NE.mjs";
5
9
 
6
10
  // src/cli/shell.ts
7
11
  function bareUsername(username) {
@@ -27,10 +31,20 @@ var Shell = class {
27
31
  identities: options.identities ?? [],
28
32
  columns: options.columns ?? 120,
29
33
  rows: options.rows ?? 30,
30
- // Every host this reaches was named by the person running the command,
31
- // on their own network. There is no stored known_hosts to compare with
32
- // yet, so nothing useful can be decided here.
33
- verifyHost: async () => true,
34
+ // Compared against `~/.config/ai-remote/known_hosts.json`, on the same
35
+ // accept-new terms OpenSSH uses. A key that changed stops the connection.
36
+ verifyHost: (info) => {
37
+ const verdict = checkHostKey(
38
+ hostAddress(options.host, options.port),
39
+ info,
40
+ { insecure: options.insecureHostKey }
41
+ );
42
+ if (verdict.message) {
43
+ (options.onNotice ?? ((line) => console.error(line)))(verdict.message);
44
+ }
45
+ if (!verdict.accepted) this.lastError = verdict.message;
46
+ return verdict.accepted;
47
+ },
34
48
  openTransport: () => new TcpTransport(options.host, options.port)
35
49
  });
36
50
  this.session.addEventListener("data", (event) => {
@@ -3,7 +3,7 @@ import {
3
3
  VERSION,
4
4
  isTitlebarDragPoint,
5
5
  logicalDisplaySize
6
- } from "./cli-chunk-OZG2CODR.mjs";
6
+ } from "./cli-chunk-TKKENOSJ.mjs";
7
7
 
8
8
  // src/cli/window.ts
9
9
  import { spawn } from "node:child_process";
@@ -38,7 +38,7 @@ function chunk(type, body) {
38
38
  view.setUint32(8 + body.length, crc32(out.subarray(4, 8 + body.length)));
39
39
  return out;
40
40
  }
41
- function encodePng(rgba, width, height) {
41
+ function encodePng(rgba, width, height, level = 6) {
42
42
  const stride = width * 4;
43
43
  const raw = new Uint8Array((stride + 1) * height);
44
44
  for (let y = 0; y < height; y++) {
@@ -57,7 +57,7 @@ function encodePng(rgba, width, height) {
57
57
  const parts = [
58
58
  new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
59
59
  chunk("IHDR", ihdr),
60
- chunk("IDAT", new Uint8Array(deflateSync(raw, { level: 6 }))),
60
+ chunk("IDAT", new Uint8Array(deflateSync(raw, { level }))),
61
61
  chunk("IEND", new Uint8Array(0))
62
62
  ];
63
63
  const total = parts.reduce((sum, part) => sum + part.length, 0);
@@ -1,5 +1,5 @@
1
1
  // src/cli/generated/version.ts
2
- var VERSION = "0.5.0";
2
+ var VERSION = "0.6.0";
3
3
 
4
4
  // src/cli/viewer-layout.ts
5
5
  var TERMINAL_COLUMNS = 80;
@@ -175,7 +175,9 @@ async function createKexClient(algorithmName) {
175
175
  }
176
176
  };
177
177
  }
178
- const pair = await subtle.generateKey({ name: "ECDH", namedCurve: algorithm.curve }, false, ["deriveBits"]);
178
+ if (!algorithm.curve) throw new Error(`SSH: ${algorithmName} names no curve to exchange over`);
179
+ const curve = algorithm.curve;
180
+ const pair = await subtle.generateKey({ name: "ECDH", namedCurve: curve }, false, ["deriveBits"]);
179
181
  const publicKey = new Uint8Array(await subtle.exportKey("raw", pair.publicKey));
180
182
  return {
181
183
  hash: algorithm.hash,
@@ -184,7 +186,7 @@ async function createKexClient(algorithmName) {
184
186
  const peer = await subtle.importKey(
185
187
  "raw",
186
188
  serverPublicKey,
187
- { name: "ECDH", namedCurve: algorithm.curve },
189
+ { name: "ECDH", namedCurve: curve },
188
190
  false,
189
191
  []
190
192
  );
@@ -243,12 +245,14 @@ function signatureBytes(algorithmName, signatureBlob) {
243
245
  const s = parts.mpint();
244
246
  return concatBytes(padStart(r, 32), padStart(s, 32));
245
247
  }
246
- async function verifyHostKeySignature({ algorithm, keyBlob, signatureBlob, exchangeHash: exchangeHash2 }) {
247
- const { kind, hash } = HOST_KEY_ALGORITHMS[algorithm];
248
+ async function verifyHostKeySignature({ algorithm, keyBlob, signatureBlob, exchangeHash: signedHash }) {
249
+ const named = HOST_KEY_ALGORITHMS[algorithm];
250
+ if (!named) throw new Error(`SSH: unsupported host key algorithm ${algorithm}`);
251
+ const { kind, hash } = named;
248
252
  const key = await importHostKey(algorithm, keyBlob);
249
253
  const signature = signatureBytes(algorithm, signatureBlob);
250
254
  const parameters = kind === "ecdsa" ? { name: "ECDSA", hash } : kind === "rsa" ? { name: "RSASSA-PKCS1-v1_5" } : { name: "Ed25519" };
251
- return subtle.verify(parameters, key, signature, exchangeHash2);
255
+ return subtle.verify(parameters, key, signature, signedHash);
252
256
  }
253
257
  async function exchangeHash({
254
258
  hash,
@@ -343,9 +347,11 @@ var SshTransport = class extends EventTarget {
343
347
  greeting;
344
348
  sendSequence;
345
349
  receiveSequence;
350
+ /** The AES-GCM state for each direction, once NEWKEYS has been exchanged. */
346
351
  outgoing;
347
352
  incoming;
348
353
  sessionId;
354
+ /** The key exchange in flight, or null between exchanges. */
349
355
  kex;
350
356
  kexQueue;
351
357
  keepaliveTimer;
@@ -356,6 +362,7 @@ var SshTransport = class extends EventTarget {
356
362
  releaseNewKeys;
357
363
  pendingServerKexInit;
358
364
  established;
365
+ drainChain;
359
366
  /**
360
367
  * @param {string} url gateway WebSocket URL
361
368
  * @param {object} options
@@ -381,16 +388,17 @@ var SshTransport = class extends EventTarget {
381
388
  this.sessionId = null;
382
389
  this.kex = null;
383
390
  this.kexQueue = [];
384
- this.keepaliveTimer = null;
391
+ this.keepaliveTimer = void 0;
385
392
  this.bytesSent = 0;
386
393
  this.bytesReceived = 0;
387
394
  }
388
395
  // --- connection ----------------------------------------------------------
389
396
  connect() {
390
- this.socket = this.openTransport ? this.openTransport(this.url) : new WebSocket(this.url);
391
- this.socket.binaryType = "arraybuffer";
392
- this.socket.addEventListener("open", () => {
393
- this.socket.send(GATEWAY_READY_SIGNAL);
397
+ const socket = this.openTransport ? this.openTransport(this.url) : new WebSocket(this.url);
398
+ this.socket = socket;
399
+ socket.binaryType = "arraybuffer";
400
+ socket.addEventListener("open", () => {
401
+ socket.send(GATEWAY_READY_SIGNAL);
394
402
  this.#write(encodeUtf8(`${CLIENT_VERSION}\r
395
403
  `));
396
404
  this.keepaliveTimer = setInterval(() => {
@@ -404,18 +412,20 @@ var SshTransport = class extends EventTarget {
404
412
  }
405
413
  }, KEEPALIVE_INTERVAL_MS);
406
414
  });
407
- this.socket.addEventListener("message", (event) => {
408
- if (typeof event.data === "string") return;
409
- this.#receive(new Uint8Array(event.data));
415
+ socket.addEventListener("message", (event) => {
416
+ const { data } = event;
417
+ if (typeof data === "string") return;
418
+ this.#receive(new Uint8Array(data));
410
419
  });
411
- this.socket.addEventListener("close", (event) => {
420
+ socket.addEventListener("close", (rawEvent) => {
421
+ const event = rawEvent;
412
422
  this.#finish({
413
423
  clean: event.code === 1e3,
414
424
  code: event.code,
415
425
  message: event.reason || (event.code === 1006 ? "The gateway rejected or could not reach this SSH target. Confirm that SSH is published for this host." : "")
416
426
  });
417
427
  });
418
- this.socket.addEventListener("error", () => {
428
+ socket.addEventListener("error", () => {
419
429
  this.log("The SSH gateway WebSocket reported a transport error; waiting for its close reason.");
420
430
  });
421
431
  }
@@ -439,7 +449,7 @@ var SshTransport = class extends EventTarget {
439
449
  if (this.closed) return;
440
450
  this.closed = true;
441
451
  clearInterval(this.keepaliveTimer);
442
- this.keepaliveTimer = null;
452
+ this.keepaliveTimer = void 0;
443
453
  this.dispatchEvent(new CustomEvent("close", { detail }));
444
454
  }
445
455
  fail(error) {
@@ -550,16 +560,18 @@ var SshTransport = class extends EventTarget {
550
560
  return frame;
551
561
  }
552
562
  async #decrypt(frame) {
563
+ const incoming = this.incoming;
564
+ if (!incoming) throw new Error("SSH: a packet arrived before the cipher was installed.");
553
565
  let plain;
554
566
  try {
555
567
  plain = new Uint8Array(await subtle2.decrypt(
556
568
  {
557
569
  name: "AES-GCM",
558
- iv: this.incoming.nextIv(),
570
+ iv: incoming.nextIv(),
559
571
  additionalData: frame.subarray(0, 4),
560
572
  tagLength: GCM_TAG_BYTES * 8
561
573
  },
562
- this.incoming.key,
574
+ incoming.key,
563
575
  frame.subarray(4)
564
576
  ));
565
577
  } catch {
@@ -711,6 +723,8 @@ var SshTransport = class extends EventTarget {
711
723
  this.kex.inbox.push(payload);
712
724
  }
713
725
  #nextKexPacket(expectedType) {
726
+ const kex = this.kex;
727
+ if (!kex) throw new Error("SSH: a key-exchange packet was awaited outside a key exchange.");
714
728
  return new Promise((resolve, reject) => {
715
729
  const check = (payload) => {
716
730
  if (expectedType !== void 0 && payload[0] !== expectedType) {
@@ -719,13 +733,13 @@ var SshTransport = class extends EventTarget {
719
733
  }
720
734
  resolve(payload);
721
735
  };
722
- const queued = this.kex.inbox.shift();
736
+ const queued = kex.inbox.shift();
723
737
  if (queued) {
724
738
  check(queued);
725
739
  return;
726
740
  }
727
- this.kex.pending = (payload) => {
728
- this.kex.pending = null;
741
+ kex.pending = (payload) => {
742
+ kex.pending = null;
729
743
  check(payload);
730
744
  };
731
745
  });
@@ -792,15 +806,20 @@ var SshTransport = class extends EventTarget {
792
806
  exchangeHash: hash,
793
807
  sessionId: this.sessionId
794
808
  };
809
+ const toServerCipher = CIPHERS[cipherToServer];
810
+ const toClientCipher = CIPHERS[cipherToClient];
811
+ if (!toServerCipher || !toClientCipher) {
812
+ throw new Error(`SSH: negotiated a cipher this client does not implement (${cipherToServer}, ${cipherToClient})`);
813
+ }
795
814
  const [ivToServer, ivToClient, keyToServer, keyToClient] = await Promise.all([
796
815
  deriveKey({ ...parameters, letter: "A", length: GCM_IV_BYTES }),
797
816
  deriveKey({ ...parameters, letter: "B", length: GCM_IV_BYTES }),
798
- deriveKey({ ...parameters, letter: "C", length: CIPHERS[cipherToServer].keyLength }),
799
- deriveKey({ ...parameters, letter: "D", length: CIPHERS[cipherToClient].keyLength })
817
+ deriveKey({ ...parameters, letter: "C", length: toServerCipher.keyLength }),
818
+ deriveKey({ ...parameters, letter: "D", length: toClientCipher.keyLength })
800
819
  ]);
801
820
  this.outgoing = await GcmCipher.create(keyToServer, ivToServer);
802
821
  this.incoming = await GcmCipher.create(keyToClient, ivToClient);
803
- this.kex.running = false;
822
+ if (this.kex) this.kex.running = false;
804
823
  this.releaseNewKeys?.();
805
824
  this.releaseNewKeys = null;
806
825
  const queued = this.kexQueue;
@@ -956,6 +975,55 @@ function releaseDisplayFilter(session) {
956
975
  }));
957
976
  }
958
977
  var SshSession = class extends EventTarget {
978
+ options;
979
+ username;
980
+ password;
981
+ /**
982
+ * Keys to sign in with, if any. A function rather than a list is allowed
983
+ * because reading keys off a disk -- or asking an agent for them -- is work
984
+ * a session that never needs them should not do.
985
+ */
986
+ /** Replaced with the resolved list the first time it is read. */
987
+ identities;
988
+ requestInput;
989
+ /** Whether there is anybody to ask; see the constructor. */
990
+ canAsk;
991
+ columns;
992
+ rows;
993
+ /** A subsystem to run instead of a shell, e.g. 'sftp'. */
994
+ subsystem;
995
+ channelId;
996
+ remoteChannelId;
997
+ remoteWindow;
998
+ remoteMaxPacket;
999
+ localWindow;
1000
+ pendingWrites;
1001
+ /** True while the interactive shell channel is usable. */
1002
+ shellOpen;
1003
+ commandInFlight;
1004
+ /** Set while a tool command runs, to keep its framing off the screen. */
1005
+ displayFilter;
1006
+ /** True only after the requested interactive shell is ready for commands. */
1007
+ /** True once the interactive shell is actually usable, not merely authenticated. */
1008
+ readySignaled = false;
1009
+ powerShellAttempted;
1010
+ powerShellTimer;
1011
+ /** '', 'posix', 'cmd' or 'powershell'; learned from the prompt. */
1012
+ /** 'powershell' when the host answered with one; '' until the shell is known. */
1013
+ shellFamily = "";
1014
+ promptTail;
1015
+ exitStatus;
1016
+ exitSignal;
1017
+ authMethods;
1018
+ /** Which keys were offered and refused, for the message if nothing works. */
1019
+ triedKeys;
1020
+ /** Set once the host has said no to the stored password. */
1021
+ passwordRejected;
1022
+ closed;
1023
+ waiters;
1024
+ transport;
1025
+ fingerprint;
1026
+ lastMessage;
959
1027
  /**
960
1028
  * @param {string} url gateway WebSocket URL
961
1029
  * @param {object} options
@@ -1033,7 +1101,7 @@ var SshSession = class extends EventTarget {
1033
1101
  this.transport.addEventListener("close", (event) => {
1034
1102
  if (this.closed) return;
1035
1103
  this.closed = true;
1036
- clearTimeout(this.powerShellTimer);
1104
+ clearTimeout(this.powerShellTimer ?? void 0);
1037
1105
  this.#abandonWaiters(event.detail.message || "The SSH connection closed.");
1038
1106
  this.dispatchEvent(new CustomEvent("close", {
1039
1107
  detail: {
@@ -1050,7 +1118,7 @@ var SshSession = class extends EventTarget {
1050
1118
  }
1051
1119
  disconnect() {
1052
1120
  if (this.closed) return;
1053
- clearTimeout(this.powerShellTimer);
1121
+ clearTimeout(this.powerShellTimer ?? void 0);
1054
1122
  if (this.shellOpen && this.remoteChannelId !== null) {
1055
1123
  this.transport.send(new SshWriter(8).u8(MSG.CHANNEL_CLOSE).u32(this.remoteChannelId).take());
1056
1124
  }
@@ -1086,7 +1154,7 @@ var SshSession = class extends EventTarget {
1086
1154
  idleMs = COMMAND_IDLE_MS,
1087
1155
  maxMs = COMMAND_MAX_MS
1088
1156
  } = {}) {
1089
- const text = String(command || "").trim();
1157
+ const text = (command || "").trim();
1090
1158
  if (!text) return Promise.reject(new Error("No command was given."));
1091
1159
  if (!this.shellOpen || this.closed) {
1092
1160
  return Promise.reject(new Error("The SSH shell is not open."));
@@ -1111,9 +1179,9 @@ var SshSession = class extends EventTarget {
1111
1179
  return new Promise((resolve, reject) => {
1112
1180
  let transcript = "";
1113
1181
  let started = false;
1114
- let startTimer = null;
1115
- let idleTimer = null;
1116
- let maxTimer = null;
1182
+ let startTimer;
1183
+ let idleTimer;
1184
+ let maxTimer;
1117
1185
  const finish = (settle) => {
1118
1186
  clearTimeout(startTimer);
1119
1187
  clearTimeout(idleTimer);
@@ -1149,7 +1217,7 @@ var SshSession = class extends EventTarget {
1149
1217
  timedOut: false,
1150
1218
  started,
1151
1219
  durationMs: Date.now() - startedAt,
1152
- error: error.message
1220
+ error: error instanceof Error ? error.message : String(error)
1153
1221
  }
1154
1222
  }));
1155
1223
  reject(error);
@@ -1174,7 +1242,7 @@ var SshSession = class extends EventTarget {
1174
1242
  if (!started && transcript.indexOf(begin) !== -1) {
1175
1243
  started = true;
1176
1244
  clearTimeout(startTimer);
1177
- startTimer = null;
1245
+ startTimer = void 0;
1178
1246
  }
1179
1247
  if (started) {
1180
1248
  clearTimeout(idleTimer);
@@ -1333,7 +1401,7 @@ var SshSession = class extends EventTarget {
1333
1401
  }, 2e3);
1334
1402
  }
1335
1403
  #markReady(message = "Connected.") {
1336
- clearTimeout(this.powerShellTimer);
1404
+ clearTimeout(this.powerShellTimer ?? void 0);
1337
1405
  this.powerShellTimer = null;
1338
1406
  this.readySignaled = true;
1339
1407
  this.#status("ready", message);
@@ -1373,21 +1441,21 @@ var SshSession = class extends EventTarget {
1373
1441
  this.#status("authenticating", `Authenticating as ${this.username}\u2026`);
1374
1442
  this.transport.send(new SshWriter(32).u8(MSG.SERVICE_REQUEST).string("ssh-userauth").take());
1375
1443
  await this.#expect(MSG.SERVICE_ACCEPT);
1376
- if (await this.#tryNone()) return void await this.#openShell();
1444
+ if (await this.#tryNone()) return await this.#openShell();
1377
1445
  if (this.authMethods.includes("publickey")) {
1378
1446
  for (const identity of await this.#identities()) {
1379
- if (await this.#tryPublicKey(identity)) return void await this.#openShell();
1447
+ if (await this.#tryPublicKey(identity)) return await this.#openShell();
1380
1448
  }
1381
1449
  }
1382
1450
  if (this.authMethods.includes("password") && this.password) {
1383
- if (await this.#tryPassword(this.password)) return void await this.#openShell();
1451
+ if (await this.#tryPassword(this.password)) return await this.#openShell();
1384
1452
  this.passwordRejected = true;
1385
1453
  }
1386
1454
  if (this.authMethods.includes("keyboard-interactive")) {
1387
- if (await this.#tryKeyboardInteractive()) return void await this.#openShell();
1455
+ if (await this.#tryKeyboardInteractive()) return await this.#openShell();
1388
1456
  if (this.canAsk && this.authMethods.includes("keyboard-interactive")) {
1389
1457
  if (await this.#tryKeyboardInteractive({ usePassword: false })) {
1390
- return void await this.#openShell();
1458
+ return await this.#openShell();
1391
1459
  }
1392
1460
  }
1393
1461
  }
@@ -1396,7 +1464,7 @@ var SshSession = class extends EventTarget {
1396
1464
  prompt: `${this.username}@${this.options.hostLabel || "host"}'s password: `,
1397
1465
  echo: false
1398
1466
  });
1399
- if (typed && await this.#tryPassword(typed)) return void await this.#openShell();
1467
+ if (typed && await this.#tryPassword(typed)) return await this.#openShell();
1400
1468
  }
1401
1469
  throw new Error(this.authMethods.length ? `SSH: authentication failed${this.triedKeys.length ? ` (the host refused ${this.triedKeys.join(", ")})` : ""}. The host accepts ${this.authMethods.join(", ")}.` : "SSH: the host rejected the connection before any authentication method was offered.");
1402
1470
  } catch (error) {