ai-remote 0.4.16 → 0.4.18
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 +83 -13
- package/SKILL.md +48 -25
- package/dist/{cli-chunk-K2DYJXC5.mjs → cli-chunk-R2ZEHRBG.mjs} +4 -0
- package/dist/{cli-chunk-7UHHVT7Q.mjs → cli-chunk-SGEG6DMQ.mjs} +7 -5
- package/dist/{cli-daemon-4VU67MLE.mjs → cli-daemon-6XQDFK3P.mjs} +80 -8
- package/dist/{cli-window-LN32ONC4.mjs → cli-window-B6ICRJVI.mjs} +2 -2
- package/dist/cli.mjs +46 -22
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -91,10 +91,10 @@ Silencing them is a choice made where the command is run:
|
|
|
91
91
|
npx --silent ai-remote list # this invocation only
|
|
92
92
|
NPM_CONFIG_LOGLEVEL=error npx ... # this shell only
|
|
93
93
|
npm config set loglevel error # this machine, all npm commands
|
|
94
|
-
npm install -g ai-remote # no npm in the path at all; use `
|
|
94
|
+
npm install -g ai-remote # no npm in the path at all; use `ai-remote`
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
The last one also skips the 300 ms: `
|
|
97
|
+
The last one also skips the 300 ms: `ai-remote list` answers in about 27 ms. It
|
|
98
98
|
is a per-machine decision rather than something the package can carry, which is
|
|
99
99
|
why `npx` is still what the examples here use.
|
|
100
100
|
|
|
@@ -216,21 +216,89 @@ npx ai-remote exec "uname -a"
|
|
|
216
216
|
npx ai-remote shell # Ctrl-] to leave
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
+
### Modifier keys between a Mac and a PC
|
|
220
|
+
|
|
221
|
+
A Mac's shortcut modifier is Command; Windows and Linux use Control. Send the
|
|
222
|
+
physical key through unchanged and Command-C on a Windows desktop opens the
|
|
223
|
+
Start menu and types a C, because Command is physically the same key as the
|
|
224
|
+
Windows logo key.
|
|
225
|
+
|
|
226
|
+
Microsoft's own Mac client sends it through unchanged and documents the
|
|
227
|
+
consequence: *"The Command key on the Mac keyboard equals the Windows key. To
|
|
228
|
+
perform actions that use the Command button on the Mac, you will need to use the
|
|
229
|
+
control button in Windows (for example Copy = Ctrl+C)."* That is consistent, and
|
|
230
|
+
it is also the thing people complain about most, because it asks the fingers to
|
|
231
|
+
learn a second shortcut modifier for one application.
|
|
232
|
+
|
|
233
|
+
So by default, when one end is a Mac and the other is not, **Control and Command
|
|
234
|
+
swap**:
|
|
235
|
+
|
|
236
|
+
| pressed on the Mac | arrives on the Windows/Linux desktop |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| Command | Control — `Command-C` copies |
|
|
239
|
+
| Control | the Windows key — `Control-D` shows the desktop |
|
|
240
|
+
| Option | Alt, unchanged |
|
|
241
|
+
|
|
242
|
+
and the mirror when a PC keyboard drives a Mac desktop (the VNC case):
|
|
243
|
+
|
|
244
|
+
| pressed on the PC | arrives on the Mac |
|
|
245
|
+
| --- | --- |
|
|
246
|
+
| Control | Command — `Control-C` copies |
|
|
247
|
+
| Windows key | Control |
|
|
248
|
+
| Alt | Option, unchanged |
|
|
249
|
+
|
|
250
|
+
A swap rather than a redirect, deliberately: it is a bijection, so the Windows
|
|
251
|
+
key is still reachable and nothing becomes impossible to press. Redirecting
|
|
252
|
+
Command to Control and leaving Control alone would give a Mac two Controls and
|
|
253
|
+
no Windows key at all.
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
npx ai-remote open 192.168.1.50 -u owner # adapt, the default
|
|
257
|
+
npx ai-remote open 192.168.1.50 -u owner --keys literal # Microsoft's behaviour
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The far end is worked out rather than assumed: RDP is Windows, and for VNC only
|
|
261
|
+
macOS Screen Sharing offers Apple's Diffie-Hellman security type, which is
|
|
262
|
+
already what decides how far a scroll notch goes. Ends that agree -- Mac to Mac,
|
|
263
|
+
PC to PC, Linux to Windows -- are left alone entirely, and `status` reports what
|
|
264
|
+
the mapping is doing under `keys`.
|
|
265
|
+
|
|
266
|
+
**Only the window's live keyboard goes through this.** `key` and `do` name the
|
|
267
|
+
key they want *on the host*, so they are never remapped -- `key MetaLeft` opens
|
|
268
|
+
the Start menu on Windows whatever keyboard is in front of you, and
|
|
269
|
+
`key ControlLeft+KeyC` means Control-C there. Ctrl+Alt+Del is likewise a
|
|
270
|
+
sequence the session synthesizes from real Windows codes, not three keys somebody
|
|
271
|
+
pressed, so the swap does not turn it into Windows+Alt+Del.
|
|
272
|
+
|
|
219
273
|
### The clipboard
|
|
220
274
|
|
|
221
|
-
|
|
222
|
-
|
|
275
|
+
**On by default.** Text copied on this machine reaches the remote clipboard and
|
|
276
|
+
text copied there reaches this one. The **clipboard button in the window's
|
|
277
|
+
toolbar** is the switch, and so is the command line:
|
|
223
278
|
|
|
224
279
|
```bash
|
|
225
|
-
npx ai-remote clipboard on # both directions, until turned off
|
|
226
280
|
npx ai-remote clipboard # what it is doing now
|
|
227
|
-
npx ai-remote
|
|
281
|
+
npx ai-remote clipboard off # stop it for this session
|
|
282
|
+
npx ai-remote open 192.168.1.50 -u owner --no-clipboard
|
|
228
283
|
```
|
|
229
284
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
285
|
+
Only a desktop carries a clipboard -- RDP negotiates a `cliprdr` channel and VNC
|
|
286
|
+
has cut-text in the protocol -- so a terminal-only session has none, the toolbar
|
|
287
|
+
button does not appear, and nothing is started.
|
|
288
|
+
|
|
289
|
+
Two things follow from it being a default rather than a choice, and both are
|
|
290
|
+
worth knowing:
|
|
291
|
+
|
|
292
|
+
- **Everything you copy locally is sent** while a session is open. That is the
|
|
293
|
+
point, and it is also the reason `open` prints one line saying so. If you are
|
|
294
|
+
about to copy something the remote machine should not have, `clipboard off`
|
|
295
|
+
first.
|
|
296
|
+
- **It polls, so it costs something.** No platform gives Node a
|
|
297
|
+
clipboard-change event, so the local side is read twice a second for as long
|
|
298
|
+
as the session is open. Measured on an M4 that is 5.7 ms per read, about 1% of
|
|
299
|
+
one core per session. The rate stays at twice a second rather than being
|
|
300
|
+
slowed to save it, because the failure it prevents -- pasting what was on the
|
|
301
|
+
clipboard *before* the thing you just copied -- is worse than the cost.
|
|
234
302
|
|
|
235
303
|
**The bridge is the session's, not the page's.** The obvious place to put it
|
|
236
304
|
would be the viewer, using the browser's `navigator.clipboard` -- but a browser
|
|
@@ -355,7 +423,9 @@ where a decrypted key belongs, and it is offered from there.
|
|
|
355
423
|
- `-s, --security MODE`: `auto` | `nla` | `tls` | `rdp` (default: `auto`).
|
|
356
424
|
- `-W, --width N` / `-H, --height N`: Desktop resolution (default: 1280x800).
|
|
357
425
|
- `--idle MINUTES`: Close an unused session (default: 0, never).
|
|
358
|
-
- `--clipboard`:
|
|
426
|
+
- `--no-clipboard`: Do not share this machine's clipboard. Shared by default.
|
|
427
|
+
- `--keys MODE`: `adapt` (default) swaps Control and Command when one end is a
|
|
428
|
+
Mac and the other is not; `literal` sends every key through as itself.
|
|
359
429
|
- `--headless` / `--no-view`: Run headless without opening the viewer window.
|
|
360
430
|
- `--no-reuse`: A second session beside one that is already open.
|
|
361
431
|
- `--save`: Keep the password for next time, once it has worked.
|
|
@@ -369,8 +439,8 @@ where a decrypted key belongs, and it is offered from there.
|
|
|
369
439
|
- `--max-edge N`: Shrink a screenshot to fit N.
|
|
370
440
|
- `--json`: Output machine-readable JSON results on stdout.
|
|
371
441
|
|
|
372
|
-
The password, where one is wanted, comes from `AI_REMOTE_PASSWORD
|
|
373
|
-
|
|
442
|
+
The password, where one is wanted, comes from `AI_REMOTE_PASSWORD`. There is no
|
|
443
|
+
`--password` flag: a command line is visible
|
|
374
444
|
to every process on the machine.
|
|
375
445
|
|
|
376
446
|
---
|
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ai-remote
|
|
3
|
-
description: Drive a remote Windows, Linux or macOS desktop from the command line with `npx ai-remote`
|
|
3
|
+
description: Drive a remote Windows, Linux or macOS desktop from the command line with `npx ai-remote` — open one session, then screenshot it, click, type, press keys, run shell commands over SSH, and watch it in a window. Use this skill whenever the user wants to control, inspect, automate or screenshot a machine over RDP/VNC/SSH — including when they never name the tool and just say "click the button on that Windows box", "what's on the screen of 192.168.x.x", "log into the VM and run this", or "take a screenshot of the remote desktop".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Driving a remote desktop with ai-remote
|
|
@@ -114,6 +114,29 @@ screenshot full size and click the coordinates you read off it.
|
|
|
114
114
|
image tokens and a 800px one is a fraction of that, usually with no loss of
|
|
115
115
|
anything you needed to see.
|
|
116
116
|
|
|
117
|
+
## Modifier keys between a Mac and a PC
|
|
118
|
+
|
|
119
|
+
When one end is a Mac and the other is not, **Control and Command swap** so the
|
|
120
|
+
shortcut key the fingers know keeps working:
|
|
121
|
+
|
|
122
|
+
| pressed on a Mac | arrives on Windows/Linux |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| Command | Control — so Command-C copies |
|
|
125
|
+
| Control | the Windows key |
|
|
126
|
+
| Option | Alt, unchanged |
|
|
127
|
+
|
|
128
|
+
and the mirror when a PC keyboard drives a Mac desktop: Control arrives as
|
|
129
|
+
Command, the Windows key as Control, Alt as Option.
|
|
130
|
+
|
|
131
|
+
`open --keys literal` turns it off and sends every key through as itself, which
|
|
132
|
+
is what Microsoft's own Mac client does — there, Command *is* the Windows key and
|
|
133
|
+
Windows shortcuts want Control.
|
|
134
|
+
|
|
135
|
+
**This applies only to the window's live keyboard.** `key` and `do` name the key
|
|
136
|
+
they want *on the host*, so they are never remapped: `key MetaLeft` opens the
|
|
137
|
+
Start menu on Windows whatever the local keyboard is, and `key ControlLeft+KeyC`
|
|
138
|
+
means Control-C there.
|
|
139
|
+
|
|
117
140
|
## Keys
|
|
118
141
|
|
|
119
142
|
Key codes are DOM-style physical codes: `MetaLeft`, `Enter`, `Escape`, `Tab`,
|
|
@@ -175,23 +198,24 @@ when the second command should only run if the first worked.
|
|
|
175
198
|
|
|
176
199
|
## The clipboard
|
|
177
200
|
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
**On by default**, both ways: text copied on this machine reaches the remote
|
|
202
|
+
clipboard and text copied there reaches this one, so a password, a path or a
|
|
203
|
+
stack trace moves without being typed.
|
|
180
204
|
|
|
181
205
|
```bash
|
|
182
|
-
npx ai-remote clipboard on # both directions, until turned off
|
|
183
206
|
npx ai-remote clipboard # what it is doing now
|
|
184
|
-
npx ai-remote
|
|
207
|
+
npx ai-remote clipboard off # stop it for this session
|
|
208
|
+
npx ai-remote open HOST -u USER --no-clipboard # never start it
|
|
185
209
|
```
|
|
186
210
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
and VNC has it in the protocol, while an `--ssh` session has none and the button
|
|
191
|
-
does not appear.
|
|
211
|
+
The toolbar's clipboard button is the same switch. Only a desktop has a
|
|
212
|
+
clipboard — RDP negotiates one and VNC has it in the protocol — so an `--ssh`
|
|
213
|
+
session has none and the button does not appear.
|
|
192
214
|
|
|
193
|
-
|
|
194
|
-
|
|
215
|
+
**Tell the user it is on** if they are about to copy something on their own
|
|
216
|
+
machine that the remote end should not see: while sharing is on, everything
|
|
217
|
+
copied locally is sent. `clipboard off` is a real off — nothing is read and
|
|
218
|
+
nothing is written.
|
|
195
219
|
|
|
196
220
|
## Copying files
|
|
197
221
|
|
|
@@ -262,8 +286,7 @@ agent.
|
|
|
262
286
|
A desktop always wants one; a terminal usually does not, because of the keys
|
|
263
287
|
above. Three places are tried, in this order:
|
|
264
288
|
|
|
265
|
-
1. `AI_REMOTE_PASSWORD` in this shell
|
|
266
|
-
name.
|
|
289
|
+
1. `AI_REMOTE_PASSWORD` in this shell.
|
|
267
290
|
2. Whatever `--save` kept for that machine.
|
|
268
291
|
3. You, at the terminal — only when one is attached, and only after the first
|
|
269
292
|
two have come up empty.
|
|
@@ -417,6 +440,7 @@ npx ai-remote do "key MetaLeft; wait 800; type notepad; shot s.png"
|
|
|
417
440
|
npx ai-remote exec "command" [--reconnect]
|
|
418
441
|
npx ai-remote reconnect # a new login: fresh PATH
|
|
419
442
|
npx ai-remote clipboard [on|off] # share text both ways
|
|
443
|
+
# open --keys adapt|literal modifier mapping
|
|
420
444
|
npx ai-remote cp [-r] SRC DST # ':path' is on the session
|
|
421
445
|
npx ai-remote view [--watch-only] [--close]
|
|
422
446
|
npx ai-remote status | list | close [--all]
|
|
@@ -428,19 +452,18 @@ Every command takes `--session NAME` to pick between open sessions, and `--json`
|
|
|
428
452
|
for machine-readable output. `ai-remote --help` is the authority; this file is
|
|
429
453
|
the map.
|
|
430
454
|
|
|
431
|
-
## Working
|
|
455
|
+
## Working with the installed package
|
|
432
456
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
457
|
+
The published package does not include its TypeScript source tree. The CLI and
|
|
458
|
+
its internal modules are bundled into `dist`, so paths such as
|
|
459
|
+
`packages/core/src/cli/cli.ts` will not exist in an installed copy. Use the
|
|
460
|
+
public command when driving a machine:
|
|
436
461
|
|
|
437
462
|
```bash
|
|
438
|
-
|
|
439
|
-
node packages/core/dist/cli.mjs open HOST -u USER --headless
|
|
463
|
+
npx ai-remote open HOST -u USER --headless
|
|
440
464
|
```
|
|
441
465
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
`viewer.ts` plus `window.ts` are the window. `pnpm test` covers them.
|
|
466
|
+
Do not edit or import files under `dist`; they are generated implementation
|
|
467
|
+
artifacts, not public entry points. To change ai-remote itself, work from a
|
|
468
|
+
source checkout, rebuild the package there, and test the generated CLI before
|
|
469
|
+
publishing a new version.
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/cli/generated/version.ts
|
|
2
|
+
var VERSION = "0.4.18";
|
|
3
|
+
|
|
1
4
|
// src/cli/viewer-layout.ts
|
|
2
5
|
var TERMINAL_COLUMNS = 80;
|
|
3
6
|
var TERMINAL_CELL_WIDTH = 7.25;
|
|
@@ -48,6 +51,7 @@ var logPath = (name) => join(RUN_DIR, `${name}.log`);
|
|
|
48
51
|
var currentPath = () => join(ROOT, "current");
|
|
49
52
|
|
|
50
53
|
export {
|
|
54
|
+
VERSION,
|
|
51
55
|
SIDE_PANEL_WIDTH,
|
|
52
56
|
isTitlebarDragPoint,
|
|
53
57
|
logicalDisplaySize,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ROOT,
|
|
3
|
+
VERSION,
|
|
3
4
|
isTitlebarDragPoint,
|
|
4
5
|
logicalDisplaySize
|
|
5
|
-
} from "./cli-chunk-
|
|
6
|
+
} from "./cli-chunk-R2ZEHRBG.mjs";
|
|
6
7
|
|
|
7
8
|
// src/cli/window.ts
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
@@ -208,7 +209,8 @@ var ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><def
|
|
|
208
209
|
// src/cli/bundle.ts
|
|
209
210
|
var APP_NAME = "ai-remote";
|
|
210
211
|
var windowsDir = () => join(ROOT, "window");
|
|
211
|
-
var
|
|
212
|
+
var bundleName = () => `v${VERSION}-${ICON_VERSION}`;
|
|
213
|
+
var bundleDir = () => join(windowsDir(), bundleName(), `${APP_NAME}.app`);
|
|
212
214
|
var PLIST = `<?xml version="1.0" encoding="UTF-8"?>
|
|
213
215
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
214
216
|
<plist version="1.0">
|
|
@@ -220,8 +222,8 @@ var PLIST = `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
220
222
|
<key>CFBundleIconFile</key><string>${APP_NAME}</string>
|
|
221
223
|
<key>CFBundlePackageType</key><string>APPL</string>
|
|
222
224
|
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
|
223
|
-
<key>CFBundleShortVersionString</key><string
|
|
224
|
-
<key>CFBundleVersion</key><string>${
|
|
225
|
+
<key>CFBundleShortVersionString</key><string>${VERSION}</string>
|
|
226
|
+
<key>CFBundleVersion</key><string>${VERSION}</string>
|
|
225
227
|
<key>NSHighResolutionCapable</key><true/>
|
|
226
228
|
<key>LSApplicationCategoryType</key><string>public.app-category.utilities</string>
|
|
227
229
|
</dict>
|
|
@@ -248,7 +250,7 @@ function build(app, executable) {
|
|
|
248
250
|
writeFileSync(join(contents, "Resources", `${APP_NAME}.icns`), iconIcns());
|
|
249
251
|
symlinkSync(process.execPath, executable);
|
|
250
252
|
for (const old of readdirSync(windowsDir())) {
|
|
251
|
-
if (old !==
|
|
253
|
+
if (old !== bundleName()) rmSync(join(windowsDir(), old), { recursive: true, force: true });
|
|
252
254
|
}
|
|
253
255
|
}
|
|
254
256
|
|
|
@@ -7,13 +7,13 @@ import {
|
|
|
7
7
|
downscale,
|
|
8
8
|
encodePng,
|
|
9
9
|
openWindow
|
|
10
|
-
} from "./cli-chunk-
|
|
10
|
+
} from "./cli-chunk-SGEG6DMQ.mjs";
|
|
11
11
|
import {
|
|
12
12
|
SIDE_PANEL_WIDTH,
|
|
13
13
|
ensureRunDir,
|
|
14
14
|
metaPath,
|
|
15
15
|
socketPath
|
|
16
|
-
} from "./cli-chunk-
|
|
16
|
+
} from "./cli-chunk-R2ZEHRBG.mjs";
|
|
17
17
|
import {
|
|
18
18
|
Shell,
|
|
19
19
|
bareUsername
|
|
@@ -7555,7 +7555,7 @@ var WebSocketPeer = class extends EventTarget {
|
|
|
7555
7555
|
this.dispatchEvent(new Event("close"));
|
|
7556
7556
|
}
|
|
7557
7557
|
#onData(chunk) {
|
|
7558
|
-
if (process.env.
|
|
7558
|
+
if (process.env.AI_REMOTE_DEBUG) console.log("[ws] data", chunk.length, "bytes");
|
|
7559
7559
|
this.#buffer = Buffer.concat([this.#buffer, chunk]);
|
|
7560
7560
|
for (; ; ) {
|
|
7561
7561
|
const parsed = readFrame(this.#buffer);
|
|
@@ -7861,7 +7861,8 @@ async function startViewer(session, port, {
|
|
|
7861
7861
|
watchOnly = false,
|
|
7862
7862
|
title,
|
|
7863
7863
|
host,
|
|
7864
|
-
clipboard
|
|
7864
|
+
clipboard,
|
|
7865
|
+
mapKey = (code) => code
|
|
7865
7866
|
} = {}) {
|
|
7866
7867
|
const token = randomBytes2(16).toString("hex");
|
|
7867
7868
|
const peers = /* @__PURE__ */ new Set();
|
|
@@ -7974,7 +7975,7 @@ async function startViewer(session, port, {
|
|
|
7974
7975
|
if (clipboard) void clipboard.state().then((state) => sendClipboard(state, peer));
|
|
7975
7976
|
peer.addEventListener("message", (event) => {
|
|
7976
7977
|
const raw = event.data;
|
|
7977
|
-
if (process.env.
|
|
7978
|
+
if (process.env.AI_REMOTE_DEBUG) console.log("[viewer] <-", raw);
|
|
7978
7979
|
try {
|
|
7979
7980
|
const message = JSON.parse(raw);
|
|
7980
7981
|
if (message.type === "control") {
|
|
@@ -8006,7 +8007,7 @@ async function startViewer(session, port, {
|
|
|
8006
8007
|
shellFor.get(peer)?.resize(size.columns, size.rows);
|
|
8007
8008
|
return;
|
|
8008
8009
|
}
|
|
8009
|
-
if (session) applyInput(session, message);
|
|
8010
|
+
if (session) applyInput(session, message, mapKey);
|
|
8010
8011
|
} catch (error) {
|
|
8011
8012
|
console.warn("[viewer] dropped an unreadable message", error);
|
|
8012
8013
|
}
|
|
@@ -8121,7 +8122,7 @@ async function startViewer(session, port, {
|
|
|
8121
8122
|
}
|
|
8122
8123
|
};
|
|
8123
8124
|
}
|
|
8124
|
-
function applyInput(session, message) {
|
|
8125
|
+
function applyInput(session, message, mapKey) {
|
|
8125
8126
|
switch (message.type) {
|
|
8126
8127
|
case "move":
|
|
8127
8128
|
session.movePointer(message.x, message.y);
|
|
@@ -8133,7 +8134,7 @@ function applyInput(session, message) {
|
|
|
8133
8134
|
session.scroll(message.x, message.y, message.dx ?? 0, message.dy ?? 0);
|
|
8134
8135
|
break;
|
|
8135
8136
|
case "key":
|
|
8136
|
-
session.sendKeyCode(message.code, Boolean(message.pressed));
|
|
8137
|
+
session.sendKeyCode(mapKey(String(message.code)), Boolean(message.pressed));
|
|
8137
8138
|
break;
|
|
8138
8139
|
case "text":
|
|
8139
8140
|
session.typeText(String(message.text ?? ""));
|
|
@@ -8325,6 +8326,43 @@ var ClipboardBridge = class {
|
|
|
8325
8326
|
}
|
|
8326
8327
|
};
|
|
8327
8328
|
|
|
8329
|
+
// src/cli/keys.ts
|
|
8330
|
+
function platformOf(nodePlatform) {
|
|
8331
|
+
if (nodePlatform === "darwin") return "macos";
|
|
8332
|
+
if (nodePlatform === "win32") return "windows";
|
|
8333
|
+
return "other";
|
|
8334
|
+
}
|
|
8335
|
+
var SWAP = {
|
|
8336
|
+
MetaLeft: "ControlLeft",
|
|
8337
|
+
MetaRight: "ControlRight",
|
|
8338
|
+
ControlLeft: "MetaLeft",
|
|
8339
|
+
ControlRight: "MetaRight"
|
|
8340
|
+
};
|
|
8341
|
+
function needsSwap(local, remote) {
|
|
8342
|
+
if (remote === "none" || local === "none") return false;
|
|
8343
|
+
return local === "macos" !== (remote === "macos");
|
|
8344
|
+
}
|
|
8345
|
+
function keyMapper(options) {
|
|
8346
|
+
if ((options.style ?? "adapt") === "literal" || !needsSwap(options.local, options.remote)) {
|
|
8347
|
+
return (code) => code;
|
|
8348
|
+
}
|
|
8349
|
+
return (code) => SWAP[code] ?? code;
|
|
8350
|
+
}
|
|
8351
|
+
function describeKeys(options) {
|
|
8352
|
+
const style = options.style ?? "adapt";
|
|
8353
|
+
const { local, remote } = options;
|
|
8354
|
+
if (remote === "none") {
|
|
8355
|
+
return "No desktop, so no keyboard to map: a terminal session types into a shell.";
|
|
8356
|
+
}
|
|
8357
|
+
if (!needsSwap(local, remote)) {
|
|
8358
|
+
return "Keyboard sent through as it is: both ends use the same shortcut modifier.";
|
|
8359
|
+
}
|
|
8360
|
+
if (style === "literal") {
|
|
8361
|
+
return local === "macos" ? "Keyboard sent through unchanged: Command is the Windows key, so shortcuts want Control (Copy is Control-C). `--keys adapt` swaps them." : "Keyboard sent through unchanged: Control is Control, and the Windows key is Command. `--keys adapt` swaps them.";
|
|
8362
|
+
}
|
|
8363
|
+
return local === "macos" ? "Mac keyboard on a non-Mac desktop: Command acts as Control (so Command-C copies) and Control acts as the Windows key. `--keys literal` sends them through as they are." : "Non-Mac keyboard on a Mac desktop: Control acts as Command (so Control-C copies) and the Windows key acts as Control. `--keys literal` sends them through as they are.";
|
|
8364
|
+
}
|
|
8365
|
+
|
|
8328
8366
|
// src/cli/daemon.ts
|
|
8329
8367
|
async function runDaemon(options) {
|
|
8330
8368
|
ensureRunDir();
|
|
@@ -8337,6 +8375,17 @@ async function runDaemon(options) {
|
|
|
8337
8375
|
log: (message) => console.log(message)
|
|
8338
8376
|
});
|
|
8339
8377
|
let unwatchClipboard = null;
|
|
8378
|
+
function remotePlatform() {
|
|
8379
|
+
if (protocol === "rdp") return "windows";
|
|
8380
|
+
if (protocol === "vnc") return session?.client?.appleServer ? "macos" : "other";
|
|
8381
|
+
return "none";
|
|
8382
|
+
}
|
|
8383
|
+
const keyStyle = options.keys === "literal" ? "literal" : "adapt";
|
|
8384
|
+
const mapKey = (code) => keyMapper({
|
|
8385
|
+
local: platformOf(process.platform),
|
|
8386
|
+
remote: remotePlatform(),
|
|
8387
|
+
style: keyStyle
|
|
8388
|
+
})(code);
|
|
8340
8389
|
function openDesktop(kind, password = options.password, port = desktopPort) {
|
|
8341
8390
|
if (kind === "rdp") return new RdpSession(options);
|
|
8342
8391
|
if (kind === "vnc") {
|
|
@@ -8445,6 +8494,7 @@ async function runDaemon(options) {
|
|
|
8445
8494
|
sidePanel,
|
|
8446
8495
|
watchOnly,
|
|
8447
8496
|
host: options.host,
|
|
8497
|
+
mapKey,
|
|
8448
8498
|
clipboard: {
|
|
8449
8499
|
state: () => clipboard.state(),
|
|
8450
8500
|
// The switch belongs to the session, so a viewer that moves it tells
|
|
@@ -8556,6 +8606,18 @@ async function runDaemon(options) {
|
|
|
8556
8606
|
viewers: viewer?.viewers ?? 0,
|
|
8557
8607
|
shell: shell ? "open" : shellStarting ? "opening" : "closed",
|
|
8558
8608
|
clipboard: await clipboard.state(),
|
|
8609
|
+
keys: {
|
|
8610
|
+
style: keyStyle,
|
|
8611
|
+
local: platformOf(process.platform),
|
|
8612
|
+
remote: remotePlatform(),
|
|
8613
|
+
/** True only when a key is actually being rewritten. */
|
|
8614
|
+
swapped: keyStyle === "adapt" && needsSwap(platformOf(process.platform), remotePlatform()),
|
|
8615
|
+
describe: describeKeys({
|
|
8616
|
+
local: platformOf(process.platform),
|
|
8617
|
+
remote: remotePlatform(),
|
|
8618
|
+
style: keyStyle
|
|
8619
|
+
})
|
|
8620
|
+
},
|
|
8559
8621
|
watchOnly: options.watchOnly,
|
|
8560
8622
|
sshUser: options.sshUsername || bareUsername(options.username),
|
|
8561
8623
|
sshPort: options.sshPort,
|
|
@@ -8635,6 +8697,16 @@ async function runDaemon(options) {
|
|
|
8635
8697
|
await new Promise((resolve) => server.listen(path, resolve));
|
|
8636
8698
|
writeMeta();
|
|
8637
8699
|
if (session) watchDesktop(session);
|
|
8700
|
+
if (session) console.log(`[keys] ${describeKeys({
|
|
8701
|
+
local: platformOf(process.platform),
|
|
8702
|
+
remote: remotePlatform(),
|
|
8703
|
+
style: keyStyle
|
|
8704
|
+
})}`);
|
|
8705
|
+
if (session && options.clipboard !== false) {
|
|
8706
|
+
await clipboard.set(true).catch((error) => {
|
|
8707
|
+
console.log(`[clipboard] not shared (${error instanceof Error ? error.message : String(error)})`);
|
|
8708
|
+
});
|
|
8709
|
+
}
|
|
8638
8710
|
const idleTimer = options.idleMs > 0 ? setInterval(() => {
|
|
8639
8711
|
if (Date.now() - lastUsed > options.idleMs) shutdown("idle");
|
|
8640
8712
|
}, 3e4) : null;
|
|
@@ -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-SGEG6DMQ.mjs";
|
|
8
|
+
import "./cli-chunk-R2ZEHRBG.mjs";
|
|
9
9
|
export {
|
|
10
10
|
detectCurrentDisplaySize,
|
|
11
11
|
nativeWindowAvailable,
|
package/dist/cli.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
RUN_DIR,
|
|
7
7
|
SIDE_PANEL_WIDTH,
|
|
8
|
+
VERSION,
|
|
8
9
|
currentPath,
|
|
9
10
|
ensureRunDir,
|
|
10
11
|
fullscreenRdpSize,
|
|
@@ -12,10 +13,10 @@ import {
|
|
|
12
13
|
metaPath,
|
|
13
14
|
sessionName,
|
|
14
15
|
socketPath
|
|
15
|
-
} from "./cli-chunk-
|
|
16
|
+
} from "./cli-chunk-R2ZEHRBG.mjs";
|
|
16
17
|
|
|
17
18
|
// src/cli/cli.ts
|
|
18
|
-
import {
|
|
19
|
+
import { resolve as resolvePath } from "node:path";
|
|
19
20
|
import { readFileSync as readFileSync2, openSync } from "node:fs";
|
|
20
21
|
|
|
21
22
|
// src/cli/prompt.ts
|
|
@@ -149,10 +150,7 @@ function readLog(name) {
|
|
|
149
150
|
return "";
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
|
-
var NAME =
|
|
153
|
-
const invoked = basename(process.argv[1] ?? "").replace(/\.mjs$/, "");
|
|
154
|
-
return invoked === "ai-remote" || invoked === "remotectl" ? invoked : "ai-remote";
|
|
155
|
-
})();
|
|
153
|
+
var NAME = "ai-remote";
|
|
156
154
|
var USAGE = `${NAME} -- drive a remote desktop, with a window you can watch
|
|
157
155
|
|
|
158
156
|
One command names the machine:
|
|
@@ -184,6 +182,7 @@ The rest drive that session, and take no host:
|
|
|
184
182
|
${NAME} forget [<host[:port]>] throw one away; --all throws all
|
|
185
183
|
${NAME} close close it; --all closes every session
|
|
186
184
|
${NAME} probe [<host[:port]>] is anything listening?
|
|
185
|
+
${NAME} --version which version this is
|
|
187
186
|
|
|
188
187
|
Copying files, like scp but with the session standing in for the host:
|
|
189
188
|
${NAME} cp ./setup.exe :C:/Users/owner/Desktop/ to the session in hand
|
|
@@ -219,7 +218,11 @@ Options for open
|
|
|
219
218
|
--fullscreen fill the display; RDP uses its current resolution
|
|
220
219
|
--side-panel open the SSH terminal docked on the right
|
|
221
220
|
--audio play the remote computer's sound in the window (RDP)
|
|
222
|
-
--clipboard
|
|
221
|
+
--no-clipboard do not share this machine's clipboard (shared by default)
|
|
222
|
+
--keys MODE adapt | literal (default: adapt)
|
|
223
|
+
adapt swaps Control and Command when one end is a Mac,
|
|
224
|
+
so Command-C copies on Windows and Control-C copies on
|
|
225
|
+
a Mac; literal sends every key through as itself
|
|
223
226
|
--view-port N port for the window (default: 7373)
|
|
224
227
|
--idle MINUTES close an unused session (default: 0, never)
|
|
225
228
|
--headless no window (same as --no-view)
|
|
@@ -244,7 +247,7 @@ Signing in
|
|
|
244
247
|
read if they have no passphrase. A desktop -- RDP or VNC -- has no such thing
|
|
245
248
|
and always wants a password.
|
|
246
249
|
|
|
247
|
-
The password comes from AI_REMOTE_PASSWORD
|
|
250
|
+
The password comes from AI_REMOTE_PASSWORD, then from
|
|
248
251
|
whatever --save kept for that machine, and last from asking you -- once the
|
|
249
252
|
ways that need no asking have been tried, and only when a terminal is there to
|
|
250
253
|
be asked. There is no --password flag: a command line is visible to every
|
|
@@ -281,7 +284,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
281
284
|
"ssh-port",
|
|
282
285
|
"settle",
|
|
283
286
|
"vnc-port",
|
|
284
|
-
"port"
|
|
287
|
+
"port",
|
|
288
|
+
"keys"
|
|
285
289
|
]);
|
|
286
290
|
function parse(argv) {
|
|
287
291
|
const flags = {};
|
|
@@ -331,7 +335,7 @@ var CliError = class extends Error {
|
|
|
331
335
|
}
|
|
332
336
|
code;
|
|
333
337
|
};
|
|
334
|
-
var password = () => process.env.AI_REMOTE_PASSWORD ??
|
|
338
|
+
var password = () => process.env.AI_REMOTE_PASSWORD ?? "";
|
|
335
339
|
function splitKey(key) {
|
|
336
340
|
const at = key.lastIndexOf("@");
|
|
337
341
|
return at === -1 ? ["", key] : [key.slice(0, at), key.slice(at + 1)];
|
|
@@ -432,7 +436,7 @@ async function startSession(args, name, host, port, secret) {
|
|
|
432
436
|
const explicitHeight = flag(args, "H", "height");
|
|
433
437
|
let fullscreenSize = null;
|
|
434
438
|
if (mode === "rdp" && has(args, "fullscreen") && (!explicitWidth || !explicitHeight)) {
|
|
435
|
-
const { detectCurrentDisplaySize } = await import("./cli-window-
|
|
439
|
+
const { detectCurrentDisplaySize } = await import("./cli-window-B6ICRJVI.mjs");
|
|
436
440
|
const display = await detectCurrentDisplaySize();
|
|
437
441
|
if (display) fullscreenSize = fullscreenRdpSize(display, has(args, "side-panel"));
|
|
438
442
|
}
|
|
@@ -468,12 +472,15 @@ async function startSession(args, name, host, port, secret) {
|
|
|
468
472
|
// Zero unless asked for: a session stays open until it is closed. An agent
|
|
469
473
|
// that pauses to think is not an agent that has finished.
|
|
470
474
|
"--idle",
|
|
471
|
-
flag(args, "idle") ?? "0"
|
|
475
|
+
flag(args, "idle") ?? "0",
|
|
476
|
+
"--keys",
|
|
477
|
+
flag(args, "keys") ?? "adapt"
|
|
472
478
|
];
|
|
473
479
|
if (mode !== "rdp") daemonArgs.push(`--${mode}`);
|
|
474
480
|
for (const identity of args.identities) daemonArgs.push("-i", identity);
|
|
475
481
|
if (has(args, "no-view", "headless")) daemonArgs.push("--no-view");
|
|
476
482
|
if (has(args, "no-open")) daemonArgs.push("--no-open");
|
|
483
|
+
if (has(args, "no-clipboard")) daemonArgs.push("--no-clipboard");
|
|
477
484
|
if (has(args, "watch-only", "observe")) daemonArgs.push("--watch-only");
|
|
478
485
|
if (has(args, "tab")) daemonArgs.push("--tab");
|
|
479
486
|
if (has(args, "fullscreen")) daemonArgs.push("--fullscreen");
|
|
@@ -541,13 +548,18 @@ function sessionFailure(name, died, secret, mode) {
|
|
|
541
548
|
}
|
|
542
549
|
async function main() {
|
|
543
550
|
const args = parse(process.argv.slice(2));
|
|
551
|
+
if (has(args, "v", "version") || args.command === "version") {
|
|
552
|
+
process.stdout.write(`${NAME} ${VERSION}
|
|
553
|
+
`);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
544
556
|
if (!args.command || has(args, "h", "help")) {
|
|
545
557
|
process.stdout.write(USAGE);
|
|
546
558
|
return;
|
|
547
559
|
}
|
|
548
560
|
const json = has(args, "json");
|
|
549
561
|
if (args.command === "__display-size") {
|
|
550
|
-
const { readCurrentDisplaySize } = await import("./cli-window-
|
|
562
|
+
const { readCurrentDisplaySize } = await import("./cli-window-B6ICRJVI.mjs");
|
|
551
563
|
const size = await readCurrentDisplaySize();
|
|
552
564
|
if (size) process.stdout.write(`${JSON.stringify(size)}
|
|
553
565
|
`);
|
|
@@ -556,7 +568,7 @@ async function main() {
|
|
|
556
568
|
if (args.command === "__session") {
|
|
557
569
|
const mode = modeFor(args, args.rest[0]);
|
|
558
570
|
const { host, port } = splitTarget(args.rest[0] ?? "", defaultPort(mode));
|
|
559
|
-
const { runDaemon } = await import("./cli-daemon-
|
|
571
|
+
const { runDaemon } = await import("./cli-daemon-6XQDFK3P.mjs");
|
|
560
572
|
await runDaemon({
|
|
561
573
|
name: sessionName(host, port, flag(args, "name", "session")),
|
|
562
574
|
host,
|
|
@@ -582,12 +594,14 @@ async function main() {
|
|
|
582
594
|
// RDP is the only protocol here with an audio channel, and a session with
|
|
583
595
|
// no window has nowhere to play what the channel would carry.
|
|
584
596
|
audio: has(args, "audio", "sound") && mode === "rdp" && !has(args, "no-view", "headless"),
|
|
585
|
-
idleMs: Math.max(0, Number(flag(args, "idle") ?? 0)) * 6e4
|
|
597
|
+
idleMs: Math.max(0, Number(flag(args, "idle") ?? 0)) * 6e4,
|
|
598
|
+
keys: flag(args, "keys") === "literal" ? "literal" : "adapt",
|
|
599
|
+
clipboard: !has(args, "no-clipboard")
|
|
586
600
|
});
|
|
587
601
|
return;
|
|
588
602
|
}
|
|
589
603
|
if (args.command === "__window") {
|
|
590
|
-
const { runWindow } = await import("./cli-window-
|
|
604
|
+
const { runWindow } = await import("./cli-window-B6ICRJVI.mjs");
|
|
591
605
|
await runWindow(process.argv.slice(3));
|
|
592
606
|
return;
|
|
593
607
|
}
|
|
@@ -634,6 +648,10 @@ Saved, nothing open (\`${NAME} open <host>\` needs no password):
|
|
|
634
648
|
if (args.command === "open") {
|
|
635
649
|
const target = args.rest[0];
|
|
636
650
|
if (!target) throw new CliError(`Name the machine to connect to: \`${NAME} open <host[:port]>\``, 12);
|
|
651
|
+
const keys = flag(args, "keys");
|
|
652
|
+
if (keys && keys !== "adapt" && keys !== "literal") {
|
|
653
|
+
throw new CliError(`--keys takes "adapt" or "literal", not "${keys}".`, 12);
|
|
654
|
+
}
|
|
637
655
|
const mode = modeFor(args, target);
|
|
638
656
|
const { host, port } = splitTarget(target, defaultPort(mode));
|
|
639
657
|
const asked = flag(args, "session", "name");
|
|
@@ -679,12 +697,10 @@ Saved, nothing open (\`${NAME} open <host>\` needs no password):
|
|
|
679
697
|
sidePanel: has(args, "side-panel")
|
|
680
698
|
}));
|
|
681
699
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
Object.assign(status, { clipboard: shared });
|
|
700
|
+
const shared = await ask(name2, "clipboard", {});
|
|
701
|
+
Object.assign(status, { clipboard: shared });
|
|
702
|
+
if (has(args, "clipboard") && !shared?.available) {
|
|
703
|
+
process.stderr.write("The clipboard cannot be shared with this session (it needs a desktop at the far end and a clipboard tool here).\n");
|
|
688
704
|
}
|
|
689
705
|
setCurrent(name2);
|
|
690
706
|
report(json, "open", {
|
|
@@ -703,6 +719,14 @@ Saved, nothing open (\`${NAME} open <host>\` needs no password):
|
|
|
703
719
|
if (prompted && !saved) {
|
|
704
720
|
process.stderr.write("Add --save next time to keep that password.\n");
|
|
705
721
|
}
|
|
722
|
+
if (status.keys?.swapped && status.keys?.describe) {
|
|
723
|
+
process.stderr.write(`${status.keys.describe}
|
|
724
|
+
`);
|
|
725
|
+
}
|
|
726
|
+
if (status.clipboard?.on) {
|
|
727
|
+
process.stderr.write(`Clipboard shared both ways (${status.clipboard.tool}). \`${NAME} clipboard off\` stops it; --no-clipboard opens without it.
|
|
728
|
+
`);
|
|
729
|
+
}
|
|
706
730
|
return;
|
|
707
731
|
}
|
|
708
732
|
if (args.command === "probe") {
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-remote",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.18",
|
|
4
4
|
"description": "RDP, VNC and SSH protocol engines that run in Node, a Worker or a browser -- plus `npx ai-remote`, a CLI that drives a machine and shows you what it is doing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ai-remote": "dist/cli.mjs"
|
|
8
|
-
"remotectl": "dist/cli.mjs"
|
|
7
|
+
"ai-remote": "dist/cli.mjs"
|
|
9
8
|
},
|
|
10
9
|
"license": "MIT",
|
|
11
10
|
"author": {
|