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/README.md +40 -0
- package/SKILL.md +125 -15
- package/dist/{cli-chunk-DGYNADUR.mjs → cli-chunk-CD47NHZT.mjs} +3 -3
- package/dist/{cli-chunk-M5GGPPAC.mjs → cli-chunk-L6E2YYPQ.mjs} +19 -5
- package/dist/{cli-chunk-WBOYG4KJ.mjs → cli-chunk-MURUADW4.mjs} +1 -1
- package/dist/{cli-chunk-X2JMVOWL.mjs → cli-chunk-UD2YES3P.mjs} +33 -22
- package/dist/cli-chunk-WWORJ2NE.mjs +72 -0
- package/dist/{cli-copy-ZBTICP7N.mjs → cli-copy-2HQU2E7D.mjs} +18 -5
- package/dist/cli-daemon-SD2AS34J.mjs +9872 -0
- package/dist/cli-known-hosts-UU427T4N.mjs +12 -0
- package/dist/{cli-secrets-26D3F6EN.mjs → cli-secrets-BMYFPDB2.mjs} +1 -1
- package/dist/cli-shell-JEC6BEME.mjs +11 -0
- package/dist/{cli-window-WLRFUKSW.mjs → cli-window-G5ESQTF5.mjs} +2 -2
- package/dist/cli.mjs +298 -100
- package/dist/computer.d.ts +176 -0
- package/dist/computer.js +3 -0
- package/dist/index.d.ts +80 -1
- package/dist/index.js +1 -1
- package/dist/protocols.d.ts +108 -12
- package/dist/protocols.js +15 -15
- package/package.json +6 -1
- package/dist/cli-daemon-M3KM2YHW.mjs +0 -8952
- package/dist/cli-shell-22PZYNSJ.mjs +0 -10
package/dist/cli.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
metaPath,
|
|
14
14
|
sessionName,
|
|
15
15
|
socketPath
|
|
16
|
-
} from "./cli-chunk-
|
|
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
|
|
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 =
|
|
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
|
|
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-
|
|
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 (
|
|
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-
|
|
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"
|
|
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-
|
|
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-
|
|
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:
|
|
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-
|
|
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
|
-
|
|
935
|
-
|
|
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, "
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
|
1002
|
-
if (!
|
|
1003
|
-
|
|
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 (!
|
|
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
|
-
|
|
1018
|
-
asked = Boolean(
|
|
1198
|
+
sshPassword = await promptFor(session.host, port, username);
|
|
1199
|
+
asked = Boolean(sshPassword);
|
|
1019
1200
|
}
|
|
1020
|
-
return { username, port, identities, password:
|
|
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-
|
|
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-
|
|
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" :
|
|
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
|
|
1239
|
-
|
|
1240
|
-
|
|
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 === "
|
|
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) => {
|