ai-remote 0.5.1 → 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.
@@ -0,0 +1,12 @@
1
+ import {
2
+ checkHostKey,
3
+ forgetHostKey,
4
+ hostAddress,
5
+ knownHosts
6
+ } from "./cli-chunk-WWORJ2NE.mjs";
7
+ export {
8
+ checkHostKey,
9
+ forgetHostKey,
10
+ hostAddress,
11
+ knownHosts
12
+ };
@@ -198,7 +198,7 @@ function savedSecrets() {
198
198
  if (!seen.has(entry.key)) seen.set(entry.key, entry);
199
199
  }
200
200
  }
201
- return [...seen.values()].map(({ key, backend, savedAt }) => ({ key, backend, savedAt })).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
201
+ return [...seen.values()].map(({ key, backend, savedAt }) => ({ key, backend, savedAt })).toSorted((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
202
202
  }
203
203
  async function forgetAll() {
204
204
  const keys = savedSecrets().map((entry) => entry.key);
@@ -0,0 +1,11 @@
1
+ import {
2
+ Shell,
3
+ bareUsername
4
+ } from "./cli-chunk-L6E2YYPQ.mjs";
5
+ import "./cli-chunk-UD2YES3P.mjs";
6
+ import "./cli-chunk-XT2FISR5.mjs";
7
+ import "./cli-chunk-WWORJ2NE.mjs";
8
+ export {
9
+ Shell,
10
+ bareUsername
11
+ };
@@ -4,8 +4,8 @@ import {
4
4
  openWindow,
5
5
  readCurrentDisplaySize,
6
6
  runWindow
7
- } from "./cli-chunk-DGYNADUR.mjs";
8
- import "./cli-chunk-WBOYG4KJ.mjs";
7
+ } from "./cli-chunk-P4645KKB.mjs";
8
+ import "./cli-chunk-TKKENOSJ.mjs";
9
9
  export {
10
10
  detectCurrentDisplaySize,
11
11
  nativeWindowAvailable,
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-TKKENOSJ.mjs";
17
17
 
18
18
  // src/cli/cli.ts
19
19
  import { resolve as resolvePath } from "node:path";
@@ -28,15 +28,93 @@ 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
+ ]);
67
+ var OPTIONAL_VALUE_FLAGS = { shot: /\.(png|jpe?g)$/i };
68
+ function parse(argv) {
69
+ const flags = {};
70
+ const positional = [];
71
+ const identities = [];
72
+ for (let i = 0; i < argv.length; i++) {
73
+ const token = argv[i];
74
+ if (!token.startsWith("-") || token === "-") {
75
+ positional.push(token);
76
+ continue;
77
+ }
78
+ const split = token.indexOf("=");
79
+ const name = (split === -1 ? token : token.slice(0, split)).replace(/^--?/, "");
80
+ const attached = split === -1 ? void 0 : token.slice(split + 1);
81
+ if (name === "i" || name === "identity") {
82
+ identities.push(attached ?? argv[++i] ?? "");
83
+ continue;
84
+ }
85
+ if (attached !== void 0) {
86
+ if (!VALUE_FLAGS.has(name) && !(name in OPTIONAL_VALUE_FLAGS)) {
87
+ throw new CliError(`--${name} takes no value, so --${name}=... is not a thing.`, 12);
88
+ }
89
+ flags[name] = attached;
90
+ } else if (VALUE_FLAGS.has(name)) flags[name] = argv[++i] ?? "";
91
+ else if (name in OPTIONAL_VALUE_FLAGS) {
92
+ const next = argv[i + 1];
93
+ flags[name] = next !== void 0 && OPTIONAL_VALUE_FLAGS[name].test(next) ? argv[++i] : true;
94
+ } else flags[name] = true;
95
+ }
96
+ return { command: positional[0] ?? "", rest: positional.slice(1), flags, identities };
97
+ }
98
+ var flag = (args, ...names) => {
99
+ for (const name of names) if (typeof args.flags[name] === "string") return args.flags[name];
100
+ return void 0;
101
+ };
102
+ var has = (args, ...names) => names.some((name) => args.flags[name] === true);
103
+ var shotFile = (args) => {
104
+ const value = args.flags.shot;
105
+ if (value === void 0) return void 0;
106
+ return typeof value === "string" && value || flag(args, "o", "out") || "screen.png";
107
+ };
108
+
31
109
  // src/cli/prompt.ts
32
110
  function canPrompt() {
33
- return Boolean(process.stdin.isTTY) && !process.env.AI_REMOTE_NO_PROMPT;
111
+ return process.stdin.isTTY && !process.env.AI_REMOTE_NO_PROMPT;
34
112
  }
35
113
  function askPassword(label) {
36
114
  if (!canPrompt()) return Promise.resolve("");
37
115
  return new Promise((resolve, reject) => {
38
116
  const input = process.stdin;
39
- const wasRaw = Boolean(input.isRaw);
117
+ const wasRaw = input.isRaw;
40
118
  const bytes = [];
41
119
  const done = (finish) => {
42
120
  input.off("data", onData);
@@ -180,6 +258,7 @@ The rest drive that session, and take no host:
180
258
  ${NAME} clipboard [on|off] share this machine's clipboard both ways
181
259
  ${NAME} cp <src> <dst> copy files; a remote path starts with ':'
182
260
  ${NAME} do "steps" several actions in one go
261
+ ${NAME} batch actions.json structured actions; - reads JSON from stdin
183
262
  ${NAME} vnc give this session a screen over VNC
184
263
  (--vnc-port N, default 5900)
185
264
  ${NAME} view open the window on the session
@@ -189,6 +268,8 @@ The rest drive that session, and take no host:
189
268
  ${NAME} list every session open, and every password kept
190
269
  ${NAME} saved machines with a password kept
191
270
  ${NAME} forget [<host[:port]>] throw one away; --all throws all
271
+ ${NAME} hosts SSH host keys this machine has accepted
272
+ ${NAME} forget-host <host[:port]> stop trusting one, after a rebuild
192
273
  ${NAME} close close it; --all closes every session
193
274
  ${NAME} probe [<host[:port]>] is anything listening?
194
275
  ${NAME} --version which version this is
@@ -228,6 +309,10 @@ Options for open
228
309
  --side-panel open the SSH terminal docked on the right
229
310
  --audio play the remote computer's sound in the window (RDP)
230
311
  --no-clipboard do not share this machine's clipboard (shared by default)
312
+ --insecure-host-key
313
+ accept an SSH host key that does not match the one on
314
+ file. For a machine you rebuild often, and nothing else:
315
+ a changed key is otherwise refused, which is the point
231
316
  --keys MODE adapt | literal (default: adapt)
232
317
  adapt swaps Control and Command when one end is a Mac,
233
318
  so Command-C copies on Windows and Control-C copies on
@@ -247,7 +332,14 @@ Options for the rest
247
332
  -r, --recursive for cp: copy a directory
248
333
  --reconnect for exec: a new terminal first, then the command
249
334
  -o, --out FILE where a screenshot goes (default: screen.png)
335
+ --shot [FILE.png] for click, type, key, do and batch: screenshot the
336
+ steps produced, in the same command. Waits for the
337
+ frame they caused rather than a fixed delay, and says
338
+ so when nothing on screen changed. Bare --shot writes
339
+ to -o, or to screen.png; a name that is not a .png
340
+ has to be given as --shot=NAME
250
341
  --max-edge N shrink a screenshot to fit N
342
+ --region X,Y,W,H capture a desktop region; --json reports its origin
251
343
  --json machine-readable result on stdout
252
344
 
253
345
  Signing in
@@ -269,58 +361,6 @@ Signing in
269
361
 
270
362
  Exit codes: 0 ok \xB7 10 unreachable \xB7 11 auth rejected \xB7 12 bad usage \xB7 13 timeout
271
363
  `;
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
364
  var PORTS = { rdp: 3389, vnc: 5900, ssh: 22 };
325
365
  function modeFor(args, target) {
326
366
  if (has(args, "ssh", "ssh-only", "terminal")) return "ssh";
@@ -332,19 +372,12 @@ function modeFor(args, target) {
332
372
  return "rdp";
333
373
  }
334
374
  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
375
  var password = () => process.env.AI_REMOTE_PASSWORD ?? "";
343
376
  function splitKey(key) {
344
377
  const at = key.lastIndexOf("@");
345
378
  return at === -1 ? ["", key] : [key.slice(0, at), key.slice(at + 1)];
346
379
  }
347
- var secrets = () => import("./cli-secrets-26D3F6EN.mjs");
380
+ var secrets = () => import("./cli-secrets-BMYFPDB2.mjs");
348
381
  async function passwordFor(host, port, username) {
349
382
  const exported = password();
350
383
  if (exported) return exported;
@@ -388,7 +421,7 @@ function refuseTarget(command, token) {
388
421
  12
389
422
  );
390
423
  }
391
- var DESKTOP_ONLY = /* @__PURE__ */ new Set(["shot", "click", "type", "key", "do"]);
424
+ var DESKTOP_ONLY = /* @__PURE__ */ new Set(["shot", "click", "type", "key", "do", "batch"]);
392
425
  var TAKES_NOTHING = /* @__PURE__ */ new Set(["shot", "view", "status", "shell", "close", "vnc", "reconnect"]);
393
426
  var COORDINATE = /^-?\d+\s*,\s*-?\d+$/;
394
427
  function refuseLegacyTarget(args) {
@@ -419,7 +452,7 @@ async function ask(name, op, args = {}, timeoutMs) {
419
452
  async function tell(session, op, args = {}, timeoutMs) {
420
453
  const result = await ask(session.name, op, args, timeoutMs).catch((error) => {
421
454
  const message = error instanceof Error ? error.message : String(error);
422
- if (/^Unknown operation/.test(message)) {
455
+ if (message.startsWith("Unknown operation")) {
423
456
  throw new CliError(
424
457
  `The session ${session.name} does not support \`${op}\`: it was started by an older version of ${NAME} and is still running it.
425
458
  ${NAME} close --session ${session.name} then open it again`,
@@ -440,7 +473,7 @@ async function startSession(args, name, host, port, secret, account = "") {
440
473
  const explicitHeight = flag(args, "H", "height");
441
474
  let fullscreenSize = null;
442
475
  if (mode === "rdp" && has(args, "fullscreen") && (!explicitWidth || !explicitHeight)) {
443
- const { detectCurrentDisplaySize } = await import("./cli-window-WLRFUKSW.mjs");
476
+ const { detectCurrentDisplaySize } = await import("./cli-window-AWXZ5NJX.mjs");
444
477
  const display = await detectCurrentDisplaySize();
445
478
  if (display) fullscreenSize = fullscreenRdpSize(display, has(args, "side-panel"));
446
479
  }
@@ -485,6 +518,7 @@ async function startSession(args, name, host, port, secret, account = "") {
485
518
  if (has(args, "no-view", "headless")) daemonArgs.push("--no-view");
486
519
  if (has(args, "no-open")) daemonArgs.push("--no-open");
487
520
  if (has(args, "no-clipboard")) daemonArgs.push("--no-clipboard");
521
+ if (has(args, "insecure-host-key")) daemonArgs.push("--insecure-host-key");
488
522
  if (has(args, "watch-only", "observe")) daemonArgs.push("--watch-only");
489
523
  if (has(args, "tab")) daemonArgs.push("--tab");
490
524
  if (has(args, "fullscreen")) daemonArgs.push("--fullscreen");
@@ -563,7 +597,7 @@ async function main() {
563
597
  }
564
598
  const json = has(args, "json");
565
599
  if (args.command === "__display-size") {
566
- const { readCurrentDisplaySize } = await import("./cli-window-WLRFUKSW.mjs");
600
+ const { readCurrentDisplaySize } = await import("./cli-window-AWXZ5NJX.mjs");
567
601
  const size = await readCurrentDisplaySize();
568
602
  if (size) process.stdout.write(`${JSON.stringify(size)}
569
603
  `);
@@ -572,7 +606,7 @@ async function main() {
572
606
  if (args.command === "__session") {
573
607
  const mode = modeFor(args, args.rest[0]);
574
608
  const { host, port } = splitTarget(args.rest[0] ?? "", defaultPort(mode));
575
- const { runDaemon } = await import("./cli-daemon-M3KM2YHW.mjs");
609
+ const { runDaemon } = await import("./cli-daemon-3L36TBIB.mjs");
576
610
  await runDaemon({
577
611
  name: sessionName(host, port, flag(args, "name", "session")),
578
612
  host,
@@ -600,12 +634,13 @@ async function main() {
600
634
  audio: has(args, "audio", "sound") && mode === "rdp" && !has(args, "no-view", "headless"),
601
635
  idleMs: Math.max(0, Number(flag(args, "idle") ?? 0)) * 6e4,
602
636
  keys: flag(args, "keys") === "literal" ? "literal" : "adapt",
603
- clipboard: !has(args, "no-clipboard")
637
+ clipboard: !has(args, "no-clipboard"),
638
+ insecureHostKey: has(args, "insecure-host-key")
604
639
  });
605
640
  return;
606
641
  }
607
642
  if (args.command === "__window") {
608
- const { runWindow } = await import("./cli-window-WLRFUKSW.mjs");
643
+ const { runWindow } = await import("./cli-window-AWXZ5NJX.mjs");
609
644
  await runWindow(process.argv.slice(3));
610
645
  return;
611
646
  }
@@ -764,6 +799,36 @@ Saved, nothing open (\`${NAME} open <host>\` needs no password):
764
799
  }
765
800
  return;
766
801
  }
802
+ if (args.command === "hosts") {
803
+ const { knownHosts } = await import("./cli-known-hosts-UU427T4N.mjs");
804
+ const entries = knownHosts();
805
+ if (json) {
806
+ process.stdout.write(`${JSON.stringify({ hosts: entries })}
807
+ `);
808
+ return;
809
+ }
810
+ if (!entries.length) {
811
+ process.stdout.write("No SSH host keys have been accepted yet.\n");
812
+ return;
813
+ }
814
+ for (const entry of entries) {
815
+ process.stdout.write(` ${entry.address.padEnd(28)} ${entry.fingerprint} ${entry.keyType} first seen ${entry.firstSeen.slice(0, 10)}
816
+ `);
817
+ }
818
+ return;
819
+ }
820
+ if (args.command === "forget-host") {
821
+ const { forgetHostKey } = await import("./cli-known-hosts-UU427T4N.mjs");
822
+ const target = args.rest[0];
823
+ if (!target) throw new CliError(`\`${NAME} forget-host <host[:port]>\` needs a machine to forget.`, 2);
824
+ const where = splitTarget(target, 22);
825
+ const address = `${where.host}:${where.port}`;
826
+ const forgotten = forgetHostKey(address);
827
+ process.stdout.write(forgotten ? `Forgot the host key for ${address}. The next connection records a new one.
828
+ ` : `No host key was on file for ${address}. \`${NAME} hosts\` lists what is.
829
+ `);
830
+ return;
831
+ }
767
832
  if (args.command === "forget") {
768
833
  const { forgetAll, forgetSecret, secretKey } = await secrets();
769
834
  if (has(args, "all")) {
@@ -903,6 +968,22 @@ Open the host again without --ssh for a desktop.`,
903
968
  width: session.width,
904
969
  height: session.height
905
970
  };
971
+ const runSteps = async (script) => {
972
+ const shot = shotFile(args);
973
+ if (shot === void 0) return tell(session, "script", { script });
974
+ return tell(session, "act", {
975
+ script,
976
+ file: resolvePath(shot),
977
+ maxEdge: Number(flag(args, "max-edge") ?? 0),
978
+ region: flag(args, "region")
979
+ });
980
+ };
981
+ const observation = (shot) => shot === void 0 ? { observe: false } : {
982
+ observe: true,
983
+ file: resolvePath(shot),
984
+ region: flag(args, "region"),
985
+ maxEdge: Number(flag(args, "max-edge") ?? 0)
986
+ };
906
987
  switch (args.command) {
907
988
  case "status": {
908
989
  Object.assign(summary, await tell(session, "status", {}, 1e4));
@@ -923,7 +1004,7 @@ Open the host again without --ssh for a desktop.`,
923
1004
  case "shot": {
924
1005
  const file = resolvePath(flag(args, "o", "out") ?? "screen.png");
925
1006
  const maxEdge = Number(flag(args, "max-edge") ?? 0);
926
- Object.assign(summary, await tell(session, "shot", { file, maxEdge }));
1007
+ Object.assign(summary, await tell(session, "shot", { file, maxEdge, region: flag(args, "region") }));
927
1008
  break;
928
1009
  }
929
1010
  case "click": {
@@ -932,7 +1013,7 @@ Open the host again without --ssh for a desktop.`,
932
1013
  const [x, y] = spec.split(",").map(Number);
933
1014
  const button = flag(args, "button") ?? "left";
934
1015
  const step = `click ${x},${y}${button === "left" ? "" : `,${button}`}`;
935
- await tell(session, "script", { script: has(args, "double") ? `${step}; ${step}` : step });
1016
+ Object.assign(summary, await runSteps(has(args, "double") ? `${step}; ${step}` : step));
936
1017
  Object.assign(summary, { clicked: { x, y, button } });
937
1018
  break;
938
1019
  }
@@ -940,13 +1021,16 @@ Open the host again without --ssh for a desktop.`,
940
1021
  refuseSessionHost(args, "type", session);
941
1022
  const text = args.rest.join(" ");
942
1023
  if (!text) throw new CliError("type needs some text", 12);
943
- await tell(session, "script", { script: `type ${text}` });
1024
+ Object.assign(summary, await tell(session, "batch", {
1025
+ actions: [{ type: "type", text }],
1026
+ ...observation(shotFile(args))
1027
+ }));
944
1028
  Object.assign(summary, { typed: text.length });
945
1029
  break;
946
1030
  }
947
1031
  case "key": {
948
1032
  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("; ") });
1033
+ Object.assign(summary, await runSteps(args.rest.map((code) => `key ${code}`).join("; ")));
950
1034
  Object.assign(summary, { keys: args.rest });
951
1035
  break;
952
1036
  }
@@ -954,7 +1038,20 @@ Open the host again without --ssh for a desktop.`,
954
1038
  refuseSessionHost(args, "do", session);
955
1039
  const script = args.rest.join(" ");
956
1040
  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 }));
1041
+ Object.assign(summary, await runSteps(script));
1042
+ break;
1043
+ }
1044
+ case "batch": {
1045
+ const file = args.rest[0];
1046
+ if (!file || args.rest.length !== 1) throw new CliError("batch needs a JSON file containing an action array; use - for stdin.", 12);
1047
+ const source = readFileSync2(file === "-" ? 0 : resolvePath(file), "utf8");
1048
+ let actions;
1049
+ try {
1050
+ actions = JSON.parse(source);
1051
+ } catch (error) {
1052
+ throw new CliError(`${file === "-" ? "stdin" : file} is not valid JSON: ${error.message}`, 12);
1053
+ }
1054
+ Object.assign(summary, await tell(session, "batch", { actions, ...observation(shotFile(args)) }));
958
1055
  break;
959
1056
  }
960
1057
  case "exec": {
@@ -987,7 +1084,7 @@ function report(json, command, summary) {
987
1084
  `);
988
1085
  }
989
1086
  async function sshLogin(args, session, what) {
990
- const { bareUsername } = await import("./cli-shell-22PZYNSJ.mjs");
1087
+ const { bareUsername } = await import("./cli-shell-JEC6BEME.mjs");
991
1088
  const username = bareUsername(flag(args, "ssh-user") ?? session.sshUser ?? "");
992
1089
  if (!username) {
993
1090
  throw new CliError(
@@ -998,12 +1095,12 @@ async function sshLogin(args, session, what) {
998
1095
  const { loadIdentities } = await import("./cli-identities-GBQWYF2O.mjs");
999
1096
  const { identities, skipped } = await loadIdentities(args.identities);
1000
1097
  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);
1098
+ let sshPassword = await passwordFor(session.host, port, username);
1099
+ if (!sshPassword && session.port !== port) {
1100
+ sshPassword = await passwordFor(session.host, session.port, username);
1004
1101
  }
1005
1102
  let asked = false;
1006
- if (!password2 && !identities.length) {
1103
+ if (!sshPassword && !identities.length) {
1007
1104
  if (!prompting(args)) {
1008
1105
  throw new CliError(
1009
1106
  `${what} signs in for itself, and this shell has nothing for it to use.
@@ -1014,10 +1111,10 @@ Keys passed over: ${skipped.join("; ")}` : ""),
1014
1111
  11
1015
1112
  );
1016
1113
  }
1017
- password2 = await promptFor(session.host, port, username);
1018
- asked = Boolean(password2);
1114
+ sshPassword = await promptFor(session.host, port, username);
1115
+ asked = Boolean(sshPassword);
1019
1116
  }
1020
- return { username, port, identities, password: password2, asked };
1117
+ return { username, port, identities, password: sshPassword, asked };
1021
1118
  }
1022
1119
  function resolveEndpoint(args, endpoint) {
1023
1120
  if (!endpoint.remote) return { endpoint, session: null };
@@ -1069,7 +1166,7 @@ async function copyFiles(args, json) {
1069
1166
  upload,
1070
1167
  walkLocal,
1071
1168
  walkRemote
1072
- } = await import("./cli-copy-ZBTICP7N.mjs");
1169
+ } = await import("./cli-copy-2HQU2E7D.mjs");
1073
1170
  const from = resolveEndpoint(args, parseEndpoint(args.rest[0]));
1074
1171
  const to = resolveEndpoint(args, parseEndpoint(args.rest[1]));
1075
1172
  if (!from.endpoint.remote && !to.endpoint.remote) {
@@ -1091,7 +1188,8 @@ async function copyFiles(args, json) {
1091
1188
  port: login.port,
1092
1189
  username: login.username,
1093
1190
  password: login.password,
1094
- identities: login.identities
1191
+ identities: login.identities,
1192
+ insecureHostKey: has(args, "insecure-host-key")
1095
1193
  });
1096
1194
  await connection.connect().catch((error) => {
1097
1195
  throw new CliError(error.message, /password|auth|denied|refused the/i.test(error.message) ? 11 : 10);
@@ -1147,7 +1245,7 @@ async function copyFiles(args, json) {
1147
1245
  }
1148
1246
  }
1149
1247
  async function interactiveShell(args, session) {
1150
- const { Shell } = await import("./cli-shell-22PZYNSJ.mjs");
1248
+ const { Shell } = await import("./cli-shell-JEC6BEME.mjs");
1151
1249
  const login = await sshLogin(args, session, "a terminal");
1152
1250
  const { username, port, identities } = login;
1153
1251
  const prompts = prompting(args);
@@ -1160,6 +1258,7 @@ async function interactiveShell(args, session) {
1160
1258
  username,
1161
1259
  password: secret,
1162
1260
  identities,
1261
+ insecureHostKey: has(args, "insecure-host-key"),
1163
1262
  columns: process.stdout.columns ?? 120,
1164
1263
  rows: process.stdout.rows ?? 30
1165
1264
  });
@@ -1215,11 +1314,11 @@ async function probe(host, port) {
1215
1314
  });
1216
1315
  }
1217
1316
  function describe(command, summary) {
1218
- const size = `${summary.width}x${summary.height}`;
1317
+ const size = `${summary.desktopWidth ?? summary.width}x${summary.desktopHeight ?? summary.height}`;
1219
1318
  const terminalOnly = summary.mode === "ssh";
1220
1319
  if (command === "open") {
1221
1320
  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}`;
1321
+ const shape = terminalOnly ? "a terminal" : size;
1223
1322
  const next = terminalOnly ? `
1224
1323
  Session ${summary.name}. \`${NAME} exec "cmd"\` runs something there, \`${NAME} shell\` opens it, \`${NAME} close\` ends it.` : `
1225
1324
  Session ${summary.name}. Run more commands with no host, then \`${NAME} close\` when you are done.`;
@@ -1235,9 +1334,14 @@ ${summary.viewer ? `Window: ${summary.viewer} (${summary.viewers} watching)` : "
1235
1334
  if (summary.already) {
1236
1335
  return `${summary.name} already has a screen (${summary.mode}), ${size}.`;
1237
1336
  }
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}.` : ""}`;
1337
+ return [
1338
+ `${summary.name} now has a screen over VNC, ${size}.`,
1339
+ summary.viewer ? ` Watch it at ${summary.viewer}` : "",
1340
+ `
1341
+ \`${NAME} shot\`, \`${NAME} click X,Y\` and \`${NAME} type\` now work on it.`,
1342
+ summary.saved ? `
1343
+ The password is saved in ${summary.saved}.` : ""
1344
+ ].join("");
1241
1345
  }
1242
1346
  if (command === "view") {
1243
1347
  if (summary.viewer) return `Window: ${summary.viewer}`;
@@ -1258,13 +1362,15 @@ The password is saved in ${summary.saved}.` : ""}`;
1258
1362
  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
1363
  }
1260
1364
  if (command === "shot") return `Wrote ${summary.file} (${summary.width}x${summary.height}) from ${summary.name}.`;
1365
+ 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
1366
  if (command === "click") {
1262
1367
  const { x, y } = summary.clicked;
1263
- return `Clicked (${x}, ${y}) on a ${size} desktop.`;
1368
+ return `Clicked (${x}, ${y}) on a ${size} desktop.${looked}`;
1264
1369
  }
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 ")}`;
1370
+ if (command === "type") return `Typed ${summary.typed} characters.${looked}`;
1371
+ if (command === "batch") return `Ran ${summary.actions} actions (${summary.clicks} clicks) in ${summary.dispatchMs} ms of dispatch.${looked}`;
1372
+ if (command === "key") return `Pressed ${summary.keys.join(", ")}.${looked}`;
1373
+ if (command === "do") return `Ran ${summary.steps.length} steps: ${summary.steps.join(" \u2192 ")}${looked}`;
1268
1374
  return "Done.";
1269
1375
  }
1270
1376
  main().catch((error) => {
@@ -0,0 +1,77 @@
1
+ export type ComputerPoint = {
2
+ x: number;
3
+ y: number;
4
+ };
5
+ export type ComputerAction = ({
6
+ type: 'click';
7
+ button?: number;
8
+ count?: number;
9
+ intervalMs?: number;
10
+ } & ComputerPoint) | ({
11
+ type: 'move';
12
+ } & ComputerPoint) | ({
13
+ type: 'scroll';
14
+ dx?: number;
15
+ dy?: number;
16
+ } & ComputerPoint) | {
17
+ type: 'type';
18
+ text: string;
19
+ chunkSize?: number;
20
+ intervalMs?: number;
21
+ } | {
22
+ type: 'key';
23
+ chord: string;
24
+ } | {
25
+ type: 'wait';
26
+ ms: number;
27
+ } | {
28
+ type: 'drag';
29
+ path: ComputerPoint[];
30
+ button?: number;
31
+ intervalMs?: number;
32
+ };
33
+ export interface ObservationOptions {
34
+ region?: {
35
+ x: number;
36
+ y: number;
37
+ width: number;
38
+ height: number;
39
+ };
40
+ maxEdge?: number;
41
+ }
42
+ export interface ComputerScreenshot {
43
+ base64: string;
44
+ width: number;
45
+ height: number;
46
+ desktopWidth: number;
47
+ desktopHeight: number;
48
+ viewport?: {
49
+ x: number;
50
+ y: number;
51
+ width: number;
52
+ height: number;
53
+ };
54
+ generation: number;
55
+ changed: boolean;
56
+ quiet: boolean;
57
+ painted: boolean;
58
+ waitedMs: number;
59
+ }
60
+ export interface ComputerBatchResult {
61
+ actions: number;
62
+ clicks: number;
63
+ /** Input dispatch and requested pacing; not proof of application completion. */
64
+ dispatchMs: number;
65
+ }
66
+ /** Map a point in a returned image back to full-desktop coordinates. */
67
+ export declare function desktopPoint(image: ComputerScreenshot, point: ComputerPoint): ComputerPoint;
68
+ export declare class RemoteComputer {
69
+ readonly session: string;
70
+ constructor(session: string);
71
+ private call;
72
+ screenshot(options?: ObservationOptions): Promise<ComputerScreenshot>;
73
+ /** One IPC call, no reconnect or model invocation between actions. */
74
+ act(actions: ComputerAction[], options?: ObservationOptions): Promise<ComputerBatchResult & ComputerScreenshot>;
75
+ /** Explicitly skip observation for a known sequence; inspect when needed. */
76
+ dispatch(actions: ComputerAction[]): Promise<ComputerBatchResult>;
77
+ }
@@ -0,0 +1,3 @@
1
+ import g from"node:net";import{StringDecoder as f}from"node:string_decoder";function w(t,e){let r=new f("utf8"),n="";t.on("data",u=>{for(n+=r.write(u);;){let s=n.indexOf(`
2
+ `);if(s===-1)return;let o=n.slice(0,s);if(n=n.slice(s+1),!!o.trim())try{e(JSON.parse(o))}catch{}}})}var x=(t,e)=>{t.write(`${JSON.stringify(e)}
3
+ `)};function p(t,e,r={},n=12e4){return new Promise((u,s)=>{let o=g.connect(t),a=!1,c=(i,l)=>{a||(a=!0,clearTimeout(b),o.destroy(),i?s(i):u(l))},b=setTimeout(()=>c(new Error(`The session did not answer "${e}" in time.`)),n);o.on("connect",()=>x(o,{id:1,op:e,args:r})),o.on("error",i=>{c(Object.assign(i,{notRunning:i.code==="ENOENT"||i.code==="ECONNREFUSED"}))}),w(o,i=>c(null,i))})}import{homedir as y}from"node:os";import{join as m}from"node:path";var k=m(y(),".ai-remote"),P=m(k,"run");var h=t=>m(P,`${t}.sock`);function T(t,e){if(!Number.isFinite(e.x)||!Number.isFinite(e.y)||e.x<0||e.y<0||e.x>=t.width||e.y>=t.height)throw new Error("Point must be inside the screenshot.");let r=t.viewport??{x:0,y:0,width:t.desktopWidth,height:t.desktopHeight};return{x:Math.min(r.x+r.width-1,Math.floor(r.x+e.x*r.width/t.width)),y:Math.min(r.y+r.height-1,Math.floor(r.y+e.y*r.height/t.height))}}var d=class{session;constructor(e){if(!/^[\w.-]+$/.test(e)||e==="."||e==="..")throw new Error("Use a session name from ai-remote status --json.");this.session=e}async call(e,r){let n=await p(h(this.session),e,r);if(!n.ok)throw new Error(n.error??"Remote operation failed.");return n.result}screenshot(e={}){return this.call("shot",{...e})}act(e,r={}){return this.call("batch",{...r,actions:e})}dispatch(e){return this.call("batch",{actions:e,observe:!1})}};export{d as RemoteComputer,T as desktopPoint};