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.
- package/README.md +40 -0
- package/SKILL.md +123 -13
- package/dist/{cli-chunk-M5GGPPAC.mjs → cli-chunk-L6E2YYPQ.mjs} +19 -5
- package/dist/{cli-chunk-DGYNADUR.mjs → cli-chunk-P4645KKB.mjs} +3 -3
- package/dist/{cli-chunk-WBOYG4KJ.mjs → cli-chunk-TKKENOSJ.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-M3KM2YHW.mjs → cli-daemon-3L36TBIB.mjs} +636 -211
- 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-AWXZ5NJX.mjs} +2 -2
- package/dist/cli.mjs +201 -95
- package/dist/computer.d.ts +77 -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-shell-22PZYNSJ.mjs +0 -10
|
@@ -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 })).
|
|
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);
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
openWindow,
|
|
5
5
|
readCurrentDisplaySize,
|
|
6
6
|
runWindow
|
|
7
|
-
} from "./cli-chunk-
|
|
8
|
-
import "./cli-chunk-
|
|
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-
|
|
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
|
|
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 =
|
|
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-
|
|
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 (
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
|
1002
|
-
if (!
|
|
1003
|
-
|
|
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 (!
|
|
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
|
-
|
|
1018
|
-
asked = Boolean(
|
|
1114
|
+
sshPassword = await promptFor(session.host, port, username);
|
|
1115
|
+
asked = Boolean(sshPassword);
|
|
1019
1116
|
}
|
|
1020
|
-
return { username, port, identities, password:
|
|
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-
|
|
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-
|
|
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" :
|
|
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
|
|
1239
|
-
|
|
1240
|
-
|
|
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 === "
|
|
1267
|
-
if (command === "
|
|
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
|
+
}
|
package/dist/computer.js
ADDED
|
@@ -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};
|