ai-remote 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  metaPath,
14
14
  sessionName,
15
15
  socketPath
16
- } from "./cli-chunk-WBOYG4KJ.mjs";
16
+ } from "./cli-chunk-MURUADW4.mjs";
17
17
 
18
18
  // src/cli/cli.ts
19
19
  import { resolve as resolvePath } from "node:path";
@@ -28,15 +28,116 @@ function splitTarget(target, fallbackPort) {
28
28
  return { account, host, port: port ? Number(port) : fallbackPort };
29
29
  }
30
30
 
31
+ // src/cli/args.ts
32
+ var CliError = class extends Error {
33
+ constructor(message, code) {
34
+ super(message);
35
+ this.code = code;
36
+ }
37
+ code;
38
+ };
39
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
40
+ "u",
41
+ "user",
42
+ "d",
43
+ "domain",
44
+ "s",
45
+ "security",
46
+ "W",
47
+ "width",
48
+ "H",
49
+ "height",
50
+ "o",
51
+ "out",
52
+ "at",
53
+ "button",
54
+ "max-edge",
55
+ "view-port",
56
+ "idle",
57
+ "name",
58
+ "session",
59
+ "ssh-user",
60
+ "ssh-port",
61
+ "settle",
62
+ "vnc-port",
63
+ "port",
64
+ "keys",
65
+ "region",
66
+ // How an input is performed rather than where: how long a button or chord is
67
+ // held, how many times, what is held down with it, and the floor between one
68
+ // action and the next.
69
+ "hold",
70
+ "count",
71
+ "with",
72
+ "pace"
73
+ ]);
74
+ var OPTIONAL_VALUE_FLAGS = { shot: /\.(png|jpe?g)$/i };
75
+ function parse(argv) {
76
+ const flags = {};
77
+ const positional = [];
78
+ const identities = [];
79
+ for (let i = 0; i < argv.length; i++) {
80
+ const token = argv[i];
81
+ if (!token.startsWith("-") || token === "-") {
82
+ positional.push(token);
83
+ continue;
84
+ }
85
+ const split = token.indexOf("=");
86
+ const name = (split === -1 ? token : token.slice(0, split)).replace(/^--?/, "");
87
+ const attached = split === -1 ? void 0 : token.slice(split + 1);
88
+ if (name === "i" || name === "identity") {
89
+ identities.push(attached ?? argv[++i] ?? "");
90
+ continue;
91
+ }
92
+ if (attached !== void 0) {
93
+ if (!VALUE_FLAGS.has(name) && !(name in OPTIONAL_VALUE_FLAGS)) {
94
+ throw new CliError(`--${name} takes no value, so --${name}=... is not a thing.`, 12);
95
+ }
96
+ flags[name] = attached;
97
+ } else if (VALUE_FLAGS.has(name)) flags[name] = argv[++i] ?? "";
98
+ else if (name in OPTIONAL_VALUE_FLAGS) {
99
+ const next = argv[i + 1];
100
+ flags[name] = next !== void 0 && OPTIONAL_VALUE_FLAGS[name].test(next) ? argv[++i] : true;
101
+ } else flags[name] = true;
102
+ }
103
+ return { command: positional[0] ?? "", rest: positional.slice(1), flags, identities };
104
+ }
105
+ var flag = (args, ...names) => {
106
+ for (const name of names) if (typeof args.flags[name] === "string") return args.flags[name];
107
+ return void 0;
108
+ };
109
+ var has = (args, ...names) => names.some((name) => args.flags[name] === true);
110
+ var shotFile = (args) => {
111
+ const value = args.flags.shot;
112
+ if (value === void 0) return void 0;
113
+ return typeof value === "string" && value || flag(args, "o", "out") || "screen.png";
114
+ };
115
+ function clickAction(args) {
116
+ const spec = args.rest[0];
117
+ if (!spec) throw new CliError("click needs a position, e.g. click 640,400", 12);
118
+ const [x, y] = spec.split(",").map(Number);
119
+ return {
120
+ type: "click",
121
+ x,
122
+ y,
123
+ mouse_button: flag(args, "button") ?? "left",
124
+ count: has(args, "double") ? 2 : Number(flag(args, "count") ?? 1),
125
+ intervalMs: Number(flag(args, "pace") ?? 0),
126
+ duration: Number(flag(args, "hold") ?? 0),
127
+ ...flag(args, "with") ? { key: flag(args, "with") } : {}
128
+ };
129
+ }
130
+ var audioEnabled = (args, mode) => mode === "rdp" && !has(args, "no-view", "headless", "no-audio");
131
+
31
132
  // src/cli/prompt.ts
32
133
  function canPrompt() {
33
- return Boolean(process.stdin.isTTY) && !process.env.AI_REMOTE_NO_PROMPT;
134
+ return process.stdin.isTTY && !process.env.AI_REMOTE_NO_PROMPT;
34
135
  }
35
136
  function askPassword(label) {
36
137
  if (!canPrompt()) return Promise.resolve("");
37
138
  return new Promise((resolve, reject) => {
38
139
  const input = process.stdin;
39
- const wasRaw = Boolean(input.isRaw);
140
+ const wasRaw = input.isRaw;
40
141
  const bytes = [];
41
142
  const done = (finish) => {
42
143
  input.off("data", onData);
@@ -172,6 +273,7 @@ The rest drive that session, and take no host:
172
273
 
173
274
  ${NAME} shot screenshot the desktop
174
275
  ${NAME} click X,Y click, --button right|middle, --double
276
+ --hold MS, --count N, --with Shift
175
277
  ${NAME} type "text" type into whatever has focus
176
278
  ${NAME} key <Code> press keys, e.g. MetaLeft, ControlLeft+KeyA
177
279
  ${NAME} exec "cmd" run a command in the terminal (SSH)
@@ -180,8 +282,10 @@ The rest drive that session, and take no host:
180
282
  ${NAME} clipboard [on|off] share this machine's clipboard both ways
181
283
  ${NAME} cp <src> <dst> copy files; a remote path starts with ':'
182
284
  ${NAME} do "steps" several actions in one go
285
+ ${NAME} batch actions.json structured actions; - reads JSON from stdin
183
286
  ${NAME} vnc give this session a screen over VNC
184
287
  (--vnc-port N, default 5900)
288
+ ${NAME} input off take the pointer from the window (on gives it back)
185
289
  ${NAME} view open the window on the session
186
290
  ${NAME} view --watch-only a window that watches rather than drives
187
291
  ${NAME} view --close close the window, session keeps running
@@ -189,6 +293,8 @@ The rest drive that session, and take no host:
189
293
  ${NAME} list every session open, and every password kept
190
294
  ${NAME} saved machines with a password kept
191
295
  ${NAME} forget [<host[:port]>] throw one away; --all throws all
296
+ ${NAME} hosts SSH host keys this machine has accepted
297
+ ${NAME} forget-host <host[:port]> stop trusting one, after a rebuild
192
298
  ${NAME} close close it; --all closes every session
193
299
  ${NAME} probe [<host[:port]>] is anything listening?
194
300
  ${NAME} --version which version this is
@@ -226,8 +332,12 @@ Options for open
226
332
  -H, --height N desktop height (default: 800)
227
333
  --fullscreen fill the display; RDP uses its current resolution
228
334
  --side-panel open the SSH terminal docked on the right
229
- --audio play the remote computer's sound in the window (RDP)
335
+ --no-audio disable remote sound (enabled by default for windowed RDP)
230
336
  --no-clipboard do not share this machine's clipboard (shared by default)
337
+ --insecure-host-key
338
+ accept an SSH host key that does not match the one on
339
+ file. For a machine you rebuild often, and nothing else:
340
+ a changed key is otherwise refused, which is the point
231
341
  --keys MODE adapt | literal (default: adapt)
232
342
  adapt swaps Control and Command when one end is a Mac,
233
343
  so Command-C copies on Windows and Control-C copies on
@@ -247,9 +357,39 @@ Options for the rest
247
357
  -r, --recursive for cp: copy a directory
248
358
  --reconnect for exec: a new terminal first, then the command
249
359
  -o, --out FILE where a screenshot goes (default: screen.png)
360
+ --shot [FILE.png] for click, type, key, do and batch: screenshot the
361
+ steps produced, in the same command. Waits for the
362
+ frame they caused rather than a fixed delay, and says
363
+ so when nothing on screen changed. Bare --shot writes
364
+ to -o, or to screen.png; a name that is not a .png
365
+ has to be given as --shot=NAME
250
366
  --max-edge N shrink a screenshot to fit N
367
+ --region X,Y,W,H capture a desktop region; --json reports its origin
251
368
  --json machine-readable result on stdout
252
369
 
370
+ Performing an input rather than merely sending it
371
+ --hold MS for click and key: how long the button or chord stays
372
+ down. A press and a release with nothing between is
373
+ not a held key, and a control that measures how long
374
+ it was held cannot be worked without this
375
+ --count N for click: repeat it, --pace between repeats
376
+ --with CHORD for click: a modifier held for that one action,
377
+ e.g. --with ShiftLeft
378
+ --animate for click, key and batch: draw the pointer travelling
379
+ to each target in the window, and hold the input until
380
+ it lands. Slower on purpose, and does nothing at all
381
+ with no window open
382
+ --pace MS a floor between one action and the next. ChatGPT's
383
+ computer use waits 100ms after every action; this
384
+ waits none unless asked, because a batch exists to
385
+ spend one turn on many inputs
386
+ --exclusive take the pointer from the window for the length of
387
+ this command and give it back after. A desktop has one
388
+ pointer: a hand that moves it mid-batch moves what the
389
+ agent is aiming at. \`${NAME} input off\` holds it
390
+ open instead, and either way the window's control
391
+ button takes it back
392
+
253
393
  Signing in
254
394
  A terminal signs in with a key where it can: with no --identity, ssh-agent is
255
395
  asked for what it is holding and ~/.ssh/id_ed25519, id_ecdsa and id_rsa are
@@ -269,58 +409,6 @@ Signing in
269
409
 
270
410
  Exit codes: 0 ok \xB7 10 unreachable \xB7 11 auth rejected \xB7 12 bad usage \xB7 13 timeout
271
411
  `;
272
- var VALUE_FLAGS = /* @__PURE__ */ new Set([
273
- "u",
274
- "user",
275
- "d",
276
- "domain",
277
- "s",
278
- "security",
279
- "W",
280
- "width",
281
- "H",
282
- "height",
283
- "o",
284
- "out",
285
- "at",
286
- "button",
287
- "max-edge",
288
- "view-port",
289
- "idle",
290
- "name",
291
- "session",
292
- "ssh-user",
293
- "ssh-port",
294
- "settle",
295
- "vnc-port",
296
- "port",
297
- "keys"
298
- ]);
299
- function parse(argv) {
300
- const flags = {};
301
- const positional = [];
302
- const identities = [];
303
- for (let i = 0; i < argv.length; i++) {
304
- const token = argv[i];
305
- if (!token.startsWith("-") || token === "-") {
306
- positional.push(token);
307
- continue;
308
- }
309
- const name = token.replace(/^--?/, "");
310
- if (name === "i" || name === "identity") {
311
- identities.push(argv[++i] ?? "");
312
- continue;
313
- }
314
- if (VALUE_FLAGS.has(name)) flags[name] = argv[++i] ?? "";
315
- else flags[name] = true;
316
- }
317
- return { command: positional[0] ?? "", rest: positional.slice(1), flags, identities };
318
- }
319
- var flag = (args, ...names) => {
320
- for (const name of names) if (typeof args.flags[name] === "string") return args.flags[name];
321
- return void 0;
322
- };
323
- var has = (args, ...names) => names.some((name) => args.flags[name] === true);
324
412
  var PORTS = { rdp: 3389, vnc: 5900, ssh: 22 };
325
413
  function modeFor(args, target) {
326
414
  if (has(args, "ssh", "ssh-only", "terminal")) return "ssh";
@@ -332,19 +420,12 @@ function modeFor(args, target) {
332
420
  return "rdp";
333
421
  }
334
422
  var defaultPort = (mode) => PORTS[mode];
335
- var CliError = class extends Error {
336
- constructor(message, code) {
337
- super(message);
338
- this.code = code;
339
- }
340
- code;
341
- };
342
423
  var password = () => process.env.AI_REMOTE_PASSWORD ?? "";
343
424
  function splitKey(key) {
344
425
  const at = key.lastIndexOf("@");
345
426
  return at === -1 ? ["", key] : [key.slice(0, at), key.slice(at + 1)];
346
427
  }
347
- var secrets = () => import("./cli-secrets-26D3F6EN.mjs");
428
+ var secrets = () => import("./cli-secrets-BMYFPDB2.mjs");
348
429
  async function passwordFor(host, port, username) {
349
430
  const exported = password();
350
431
  if (exported) return exported;
@@ -388,7 +469,7 @@ function refuseTarget(command, token) {
388
469
  12
389
470
  );
390
471
  }
391
- var DESKTOP_ONLY = /* @__PURE__ */ new Set(["shot", "click", "type", "key", "do"]);
472
+ var DESKTOP_ONLY = /* @__PURE__ */ new Set(["shot", "click", "type", "key", "do", "batch"]);
392
473
  var TAKES_NOTHING = /* @__PURE__ */ new Set(["shot", "view", "status", "shell", "close", "vnc", "reconnect"]);
393
474
  var COORDINATE = /^-?\d+\s*,\s*-?\d+$/;
394
475
  function refuseLegacyTarget(args) {
@@ -419,7 +500,7 @@ async function ask(name, op, args = {}, timeoutMs) {
419
500
  async function tell(session, op, args = {}, timeoutMs) {
420
501
  const result = await ask(session.name, op, args, timeoutMs).catch((error) => {
421
502
  const message = error instanceof Error ? error.message : String(error);
422
- if (/^Unknown operation/.test(message)) {
503
+ if (message.startsWith("Unknown operation")) {
423
504
  throw new CliError(
424
505
  `The session ${session.name} does not support \`${op}\`: it was started by an older version of ${NAME} and is still running it.
425
506
  ${NAME} close --session ${session.name} then open it again`,
@@ -440,7 +521,7 @@ async function startSession(args, name, host, port, secret, account = "") {
440
521
  const explicitHeight = flag(args, "H", "height");
441
522
  let fullscreenSize = null;
442
523
  if (mode === "rdp" && has(args, "fullscreen") && (!explicitWidth || !explicitHeight)) {
443
- const { detectCurrentDisplaySize } = await import("./cli-window-WLRFUKSW.mjs");
524
+ const { detectCurrentDisplaySize } = await import("./cli-window-G5ESQTF5.mjs");
444
525
  const display = await detectCurrentDisplaySize();
445
526
  if (display) fullscreenSize = fullscreenRdpSize(display, has(args, "side-panel"));
446
527
  }
@@ -485,17 +566,19 @@ async function startSession(args, name, host, port, secret, account = "") {
485
566
  if (has(args, "no-view", "headless")) daemonArgs.push("--no-view");
486
567
  if (has(args, "no-open")) daemonArgs.push("--no-open");
487
568
  if (has(args, "no-clipboard")) daemonArgs.push("--no-clipboard");
569
+ if (has(args, "insecure-host-key")) daemonArgs.push("--insecure-host-key");
488
570
  if (has(args, "watch-only", "observe")) daemonArgs.push("--watch-only");
489
571
  if (has(args, "tab")) daemonArgs.push("--tab");
490
572
  if (has(args, "fullscreen")) daemonArgs.push("--fullscreen");
491
573
  if (has(args, "side-panel") && mode !== "ssh") daemonArgs.push("--side-panel");
492
- if (has(args, "audio", "sound")) {
574
+ if (has(args, "no-audio")) daemonArgs.push("--no-audio");
575
+ if (audioEnabled(args, mode) || has(args, "audio", "sound")) {
493
576
  if (mode !== "rdp") {
494
577
  process.stderr.write(`--audio is RDP only; a ${mode.toUpperCase()} session has no audio channel.
495
578
  `);
496
579
  } else if (has(args, "no-view", "headless")) {
497
580
  process.stderr.write("--audio needs a window to play into; ignoring it for a headless session.\n");
498
- } else {
581
+ } else if (audioEnabled(args, mode)) {
499
582
  daemonArgs.push("--audio");
500
583
  }
501
584
  }
@@ -563,7 +646,7 @@ async function main() {
563
646
  }
564
647
  const json = has(args, "json");
565
648
  if (args.command === "__display-size") {
566
- const { readCurrentDisplaySize } = await import("./cli-window-WLRFUKSW.mjs");
649
+ const { readCurrentDisplaySize } = await import("./cli-window-G5ESQTF5.mjs");
567
650
  const size = await readCurrentDisplaySize();
568
651
  if (size) process.stdout.write(`${JSON.stringify(size)}
569
652
  `);
@@ -572,7 +655,7 @@ async function main() {
572
655
  if (args.command === "__session") {
573
656
  const mode = modeFor(args, args.rest[0]);
574
657
  const { host, port } = splitTarget(args.rest[0] ?? "", defaultPort(mode));
575
- const { runDaemon } = await import("./cli-daemon-M3KM2YHW.mjs");
658
+ const { runDaemon } = await import("./cli-daemon-SD2AS34J.mjs");
576
659
  await runDaemon({
577
660
  name: sessionName(host, port, flag(args, "name", "session")),
578
661
  host,
@@ -597,15 +680,16 @@ async function main() {
597
680
  watchOnly: has(args, "watch-only", "observe"),
598
681
  // RDP is the only protocol here with an audio channel, and a session with
599
682
  // no window has nowhere to play what the channel would carry.
600
- audio: has(args, "audio", "sound") && mode === "rdp" && !has(args, "no-view", "headless"),
683
+ audio: audioEnabled(args, mode),
601
684
  idleMs: Math.max(0, Number(flag(args, "idle") ?? 0)) * 6e4,
602
685
  keys: flag(args, "keys") === "literal" ? "literal" : "adapt",
603
- clipboard: !has(args, "no-clipboard")
686
+ clipboard: !has(args, "no-clipboard"),
687
+ insecureHostKey: has(args, "insecure-host-key")
604
688
  });
605
689
  return;
606
690
  }
607
691
  if (args.command === "__window") {
608
- const { runWindow } = await import("./cli-window-WLRFUKSW.mjs");
692
+ const { runWindow } = await import("./cli-window-G5ESQTF5.mjs");
609
693
  await runWindow(process.argv.slice(3));
610
694
  return;
611
695
  }
@@ -764,6 +848,36 @@ Saved, nothing open (\`${NAME} open <host>\` needs no password):
764
848
  }
765
849
  return;
766
850
  }
851
+ if (args.command === "hosts") {
852
+ const { knownHosts } = await import("./cli-known-hosts-UU427T4N.mjs");
853
+ const entries = knownHosts();
854
+ if (json) {
855
+ process.stdout.write(`${JSON.stringify({ hosts: entries })}
856
+ `);
857
+ return;
858
+ }
859
+ if (!entries.length) {
860
+ process.stdout.write("No SSH host keys have been accepted yet.\n");
861
+ return;
862
+ }
863
+ for (const entry of entries) {
864
+ process.stdout.write(` ${entry.address.padEnd(28)} ${entry.fingerprint} ${entry.keyType} first seen ${entry.firstSeen.slice(0, 10)}
865
+ `);
866
+ }
867
+ return;
868
+ }
869
+ if (args.command === "forget-host") {
870
+ const { forgetHostKey } = await import("./cli-known-hosts-UU427T4N.mjs");
871
+ const target = args.rest[0];
872
+ if (!target) throw new CliError(`\`${NAME} forget-host <host[:port]>\` needs a machine to forget.`, 2);
873
+ const where = splitTarget(target, 22);
874
+ const address = `${where.host}:${where.port}`;
875
+ const forgotten = forgetHostKey(address);
876
+ process.stdout.write(forgotten ? `Forgot the host key for ${address}. The next connection records a new one.
877
+ ` : `No host key was on file for ${address}. \`${NAME} hosts\` lists what is.
878
+ `);
879
+ return;
880
+ }
767
881
  if (args.command === "forget") {
768
882
  const { forgetAll, forgetSecret, secretKey } = await secrets();
769
883
  if (has(args, "all")) {
@@ -903,6 +1017,28 @@ Open the host again without --ssh for a desktop.`,
903
1017
  width: session.width,
904
1018
  height: session.height
905
1019
  };
1020
+ const runSteps = async (script) => {
1021
+ const shot = shotFile(args);
1022
+ if (shot === void 0) return tell(session, "script", { script });
1023
+ return tell(session, "act", {
1024
+ script,
1025
+ file: resolvePath(shot),
1026
+ maxEdge: Number(flag(args, "max-edge") ?? 0),
1027
+ region: flag(args, "region")
1028
+ });
1029
+ };
1030
+ const observation = (shot) => shot === void 0 ? { observe: false } : {
1031
+ observe: true,
1032
+ file: resolvePath(shot),
1033
+ region: flag(args, "region"),
1034
+ maxEdge: Number(flag(args, "max-edge") ?? 0)
1035
+ };
1036
+ const pacing = () => ({
1037
+ animate: has(args, "animate"),
1038
+ postActionSleepMs: Number(flag(args, "pace") ?? 0),
1039
+ // Hold the pointer for the length of this command and give it back after.
1040
+ exclusive: has(args, "exclusive")
1041
+ });
906
1042
  switch (args.command) {
907
1043
  case "status": {
908
1044
  Object.assign(summary, await tell(session, "status", {}, 1e4));
@@ -923,7 +1059,7 @@ Open the host again without --ssh for a desktop.`,
923
1059
  case "shot": {
924
1060
  const file = resolvePath(flag(args, "o", "out") ?? "screen.png");
925
1061
  const maxEdge = Number(flag(args, "max-edge") ?? 0);
926
- Object.assign(summary, await tell(session, "shot", { file, maxEdge }));
1062
+ Object.assign(summary, await tell(session, "shot", { file, maxEdge, region: flag(args, "region") }));
927
1063
  break;
928
1064
  }
929
1065
  case "click": {
@@ -931,8 +1067,11 @@ Open the host again without --ssh for a desktop.`,
931
1067
  if (!spec) throw new CliError(`click needs a position, e.g. \`${NAME} click 640,400\``, 12);
932
1068
  const [x, y] = spec.split(",").map(Number);
933
1069
  const button = flag(args, "button") ?? "left";
934
- const step = `click ${x},${y}${button === "left" ? "" : `,${button}`}`;
935
- await tell(session, "script", { script: has(args, "double") ? `${step}; ${step}` : step });
1070
+ Object.assign(summary, await tell(session, "batch", {
1071
+ actions: [clickAction(args)],
1072
+ ...pacing(),
1073
+ ...observation(shotFile(args))
1074
+ }));
936
1075
  Object.assign(summary, { clicked: { x, y, button } });
937
1076
  break;
938
1077
  }
@@ -940,13 +1079,22 @@ Open the host again without --ssh for a desktop.`,
940
1079
  refuseSessionHost(args, "type", session);
941
1080
  const text = args.rest.join(" ");
942
1081
  if (!text) throw new CliError("type needs some text", 12);
943
- await tell(session, "script", { script: `type ${text}` });
1082
+ Object.assign(summary, await tell(session, "batch", {
1083
+ actions: [{ type: "type", text }],
1084
+ ...pacing(),
1085
+ ...observation(shotFile(args))
1086
+ }));
944
1087
  Object.assign(summary, { typed: text.length });
945
1088
  break;
946
1089
  }
947
1090
  case "key": {
948
1091
  if (!args.rest.length) throw new CliError("key needs at least one code, e.g. MetaLeft", 12);
949
- await tell(session, "script", { script: args.rest.map((code) => `key ${code}`).join("; ") });
1092
+ const duration = Number(flag(args, "hold") ?? 0);
1093
+ Object.assign(summary, await tell(session, "batch", {
1094
+ actions: args.rest.map((chord) => ({ type: "key", chord, duration })),
1095
+ ...pacing(),
1096
+ ...observation(shotFile(args))
1097
+ }));
950
1098
  Object.assign(summary, { keys: args.rest });
951
1099
  break;
952
1100
  }
@@ -954,7 +1102,40 @@ Open the host again without --ssh for a desktop.`,
954
1102
  refuseSessionHost(args, "do", session);
955
1103
  const script = args.rest.join(" ");
956
1104
  if (!script) throw new CliError('do needs a script, e.g. "key MetaLeft; wait 800; shot s.png"', 12);
957
- Object.assign(summary, await tell(session, "script", { script }));
1105
+ Object.assign(summary, await runSteps(script));
1106
+ break;
1107
+ }
1108
+ case "batch": {
1109
+ const file = args.rest[0];
1110
+ if (!file || args.rest.length !== 1) throw new CliError("batch needs a JSON file containing an action array; use - for stdin.", 12);
1111
+ const source = readFileSync2(file === "-" ? 0 : resolvePath(file), "utf8");
1112
+ let actions;
1113
+ try {
1114
+ actions = JSON.parse(source);
1115
+ } catch (error) {
1116
+ throw new CliError(`${file === "-" ? "stdin" : file} is not valid JSON: ${error.message}`, 12);
1117
+ }
1118
+ Object.assign(summary, await tell(session, "batch", {
1119
+ actions,
1120
+ ...pacing(),
1121
+ ...observation(shotFile(args))
1122
+ }));
1123
+ break;
1124
+ }
1125
+ /**
1126
+ * Who may drive: the people watching, or nobody but the agent.
1127
+ *
1128
+ * A remote desktop has one pointer, so an agent working while a hand rests
1129
+ * on the trackpad is two things pulling at the same cursor. `off` takes it
1130
+ * away from the windows; the button in the window still takes it back, and
1131
+ * the next batch is told that it happened.
1132
+ */
1133
+ case "input": {
1134
+ const word = args.rest[0];
1135
+ if (word !== void 0 && word !== "on" && word !== "off") {
1136
+ throw new CliError(`input takes on or off, e.g. \`${NAME} input off\``, 12);
1137
+ }
1138
+ Object.assign(summary, await tell(session, "input", word === void 0 ? {} : { on: word === "on" }));
958
1139
  break;
959
1140
  }
960
1141
  case "exec": {
@@ -987,7 +1168,7 @@ function report(json, command, summary) {
987
1168
  `);
988
1169
  }
989
1170
  async function sshLogin(args, session, what) {
990
- const { bareUsername } = await import("./cli-shell-22PZYNSJ.mjs");
1171
+ const { bareUsername } = await import("./cli-shell-JEC6BEME.mjs");
991
1172
  const username = bareUsername(flag(args, "ssh-user") ?? session.sshUser ?? "");
992
1173
  if (!username) {
993
1174
  throw new CliError(
@@ -998,12 +1179,12 @@ async function sshLogin(args, session, what) {
998
1179
  const { loadIdentities } = await import("./cli-identities-GBQWYF2O.mjs");
999
1180
  const { identities, skipped } = await loadIdentities(args.identities);
1000
1181
  const port = Number(flag(args, "ssh-port") ?? session.sshPort ?? 22);
1001
- let password2 = await passwordFor(session.host, port, username);
1002
- if (!password2 && session.port !== port) {
1003
- password2 = await passwordFor(session.host, session.port, username);
1182
+ let sshPassword = await passwordFor(session.host, port, username);
1183
+ if (!sshPassword && session.port !== port) {
1184
+ sshPassword = await passwordFor(session.host, session.port, username);
1004
1185
  }
1005
1186
  let asked = false;
1006
- if (!password2 && !identities.length) {
1187
+ if (!sshPassword && !identities.length) {
1007
1188
  if (!prompting(args)) {
1008
1189
  throw new CliError(
1009
1190
  `${what} signs in for itself, and this shell has nothing for it to use.
@@ -1014,10 +1195,10 @@ Keys passed over: ${skipped.join("; ")}` : ""),
1014
1195
  11
1015
1196
  );
1016
1197
  }
1017
- password2 = await promptFor(session.host, port, username);
1018
- asked = Boolean(password2);
1198
+ sshPassword = await promptFor(session.host, port, username);
1199
+ asked = Boolean(sshPassword);
1019
1200
  }
1020
- return { username, port, identities, password: password2, asked };
1201
+ return { username, port, identities, password: sshPassword, asked };
1021
1202
  }
1022
1203
  function resolveEndpoint(args, endpoint) {
1023
1204
  if (!endpoint.remote) return { endpoint, session: null };
@@ -1069,7 +1250,7 @@ async function copyFiles(args, json) {
1069
1250
  upload,
1070
1251
  walkLocal,
1071
1252
  walkRemote
1072
- } = await import("./cli-copy-ZBTICP7N.mjs");
1253
+ } = await import("./cli-copy-2HQU2E7D.mjs");
1073
1254
  const from = resolveEndpoint(args, parseEndpoint(args.rest[0]));
1074
1255
  const to = resolveEndpoint(args, parseEndpoint(args.rest[1]));
1075
1256
  if (!from.endpoint.remote && !to.endpoint.remote) {
@@ -1091,7 +1272,8 @@ async function copyFiles(args, json) {
1091
1272
  port: login.port,
1092
1273
  username: login.username,
1093
1274
  password: login.password,
1094
- identities: login.identities
1275
+ identities: login.identities,
1276
+ insecureHostKey: has(args, "insecure-host-key")
1095
1277
  });
1096
1278
  await connection.connect().catch((error) => {
1097
1279
  throw new CliError(error.message, /password|auth|denied|refused the/i.test(error.message) ? 11 : 10);
@@ -1147,7 +1329,7 @@ async function copyFiles(args, json) {
1147
1329
  }
1148
1330
  }
1149
1331
  async function interactiveShell(args, session) {
1150
- const { Shell } = await import("./cli-shell-22PZYNSJ.mjs");
1332
+ const { Shell } = await import("./cli-shell-JEC6BEME.mjs");
1151
1333
  const login = await sshLogin(args, session, "a terminal");
1152
1334
  const { username, port, identities } = login;
1153
1335
  const prompts = prompting(args);
@@ -1160,6 +1342,7 @@ async function interactiveShell(args, session) {
1160
1342
  username,
1161
1343
  password: secret,
1162
1344
  identities,
1345
+ insecureHostKey: has(args, "insecure-host-key"),
1163
1346
  columns: process.stdout.columns ?? 120,
1164
1347
  rows: process.stdout.rows ?? 30
1165
1348
  });
@@ -1215,11 +1398,11 @@ async function probe(host, port) {
1215
1398
  });
1216
1399
  }
1217
1400
  function describe(command, summary) {
1218
- const size = `${summary.width}x${summary.height}`;
1401
+ const size = `${summary.desktopWidth ?? summary.width}x${summary.desktopHeight ?? summary.height}`;
1219
1402
  const terminalOnly = summary.mode === "ssh";
1220
1403
  if (command === "open") {
1221
1404
  const window = summary.window === "window" ? "A window is open on it." : summary.window === "tab" ? "It opened in your browser." : summary.viewer ? `Open it at ${summary.viewer}` : "No window (headless).";
1222
- const shape = terminalOnly ? "a terminal" : `${size}`;
1405
+ const shape = terminalOnly ? "a terminal" : size;
1223
1406
  const next = terminalOnly ? `
1224
1407
  Session ${summary.name}. \`${NAME} exec "cmd"\` runs something there, \`${NAME} shell\` opens it, \`${NAME} close\` ends it.` : `
1225
1408
  Session ${summary.name}. Run more commands with no host, then \`${NAME} close\` when you are done.`;
@@ -1235,9 +1418,14 @@ ${summary.viewer ? `Window: ${summary.viewer} (${summary.viewers} watching)` : "
1235
1418
  if (summary.already) {
1236
1419
  return `${summary.name} already has a screen (${summary.mode}), ${size}.`;
1237
1420
  }
1238
- return `${summary.name} now has a screen over VNC, ${size}.${summary.viewer ? ` Watch it at ${summary.viewer}` : ""}
1239
- \`${NAME} shot\`, \`${NAME} click X,Y\` and \`${NAME} type\` now work on it.${summary.saved ? `
1240
- The password is saved in ${summary.saved}.` : ""}`;
1421
+ return [
1422
+ `${summary.name} now has a screen over VNC, ${size}.`,
1423
+ summary.viewer ? ` Watch it at ${summary.viewer}` : "",
1424
+ `
1425
+ \`${NAME} shot\`, \`${NAME} click X,Y\` and \`${NAME} type\` now work on it.`,
1426
+ summary.saved ? `
1427
+ The password is saved in ${summary.saved}.` : ""
1428
+ ].join("");
1241
1429
  }
1242
1430
  if (command === "view") {
1243
1431
  if (summary.viewer) return `Window: ${summary.viewer}`;
@@ -1258,13 +1446,23 @@ The password is saved in ${summary.saved}.` : ""}`;
1258
1446
  return `${summary.replaced ? "Replaced" : "Opened"} the terminal on ${summary.name}. It is a new login, so it sees whatever the PATH says now -- and has forgotten the working directory and any exported variables.`;
1259
1447
  }
1260
1448
  if (command === "shot") return `Wrote ${summary.file} (${summary.width}x${summary.height}) from ${summary.name}.`;
1449
+ const looked = summary.file ? ` Wrote ${summary.file} (${summary.width}x${summary.height})` + (summary.changed === false ? ", though nothing on screen changed" : "") + (summary.quiet === false ? "; the screen was still moving" : "") + "." : "";
1261
1450
  if (command === "click") {
1262
1451
  const { x, y } = summary.clicked;
1263
- return `Clicked (${x}, ${y}) on a ${size} desktop.`;
1452
+ return `Clicked (${x}, ${y}) on a ${size} desktop.${looked}`;
1453
+ }
1454
+ if (command === "type") return `Typed ${summary.typed} characters.${looked}`;
1455
+ if (command === "batch") return `Ran ${summary.actions} actions (${summary.clicks} clicks) in ${summary.dispatchMs} ms of dispatch.${looked}`;
1456
+ if (command === "key") return `Pressed ${summary.keys.join(", ")}.${looked}`;
1457
+ if (command === "input") {
1458
+ const count = Number(summary.viewers ?? 0);
1459
+ const windows = `${count} window${count === 1 ? "" : "s"}`;
1460
+ if (summary.humanInput) {
1461
+ return count ? `The keyboard and pointer are shared: ${windows} may drive.` : "The keyboard and pointer are shared with whoever is watching. No window is open.";
1462
+ }
1463
+ return count ? `The agent has the keyboard and pointer; ${windows} can only watch. The control button in the window takes them back.` : "The agent has the keyboard and pointer. No window is open, and one that opens will start watching.";
1264
1464
  }
1265
- if (command === "type") return `Typed ${summary.typed} characters.`;
1266
- if (command === "key") return `Pressed ${summary.keys.join(", ")}.`;
1267
- if (command === "do") return `Ran ${summary.steps.length} steps: ${summary.steps.join(" \u2192 ")}`;
1465
+ if (command === "do") return `Ran ${summary.steps.length} steps: ${summary.steps.join(" \u2192 ")}${looked}`;
1268
1466
  return "Done.";
1269
1467
  }
1270
1468
  main().catch((error) => {