ai-remote 0.3.0 → 0.4.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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "ai-remote",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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",
8
+ "remotectl": "dist/cli.mjs"
9
9
  },
10
10
  "license": "MIT",
11
11
  "author": {
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "https://github.com/guocity/vnc-node-worker.git"
18
+ "url": "git+https://github.com/guocity/vnc-node-worker.git"
19
19
  },
20
20
  "keywords": [
21
21
  "remote-desktop",
@@ -88,5 +88,8 @@
88
88
  },
89
89
  "engines": {
90
90
  "node": ">=22"
91
+ },
92
+ "optionalDependencies": {
93
+ "@webviewjs/webview": "^0.4.3"
91
94
  }
92
95
  }
package/src/cli/cli.ts CHANGED
@@ -10,8 +10,9 @@
10
10
  import { basename, resolve as resolvePath } from 'node:path';
11
11
  import { spawn } from 'node:child_process';
12
12
  import { readFileSync, readdirSync, existsSync, openSync, unlinkSync } from 'node:fs';
13
- import { Shell } from './shell';
13
+ import { bareUsername, Shell } from './shell';
14
14
  import { runDaemon } from './daemon';
15
+ import { runWindow } from './window';
15
16
  import { request } from './ipc';
16
17
  import { ensureRunDir, logPath, metaPath, RUN_DIR, sessionName, socketPath } from './paths';
17
18
 
@@ -59,7 +60,8 @@ Steps for "do", separated by semicolons:
59
60
  Options
60
61
  -u, --user NAME account on the host
61
62
  -d, --domain NAME Windows domain or computer name
62
- --ssh-user NAME account for the terminal (default: --user)
63
+ --ssh-user NAME only if the terminal signs in as someone else;
64
+ by default it reuses --user and the same password
63
65
  --ssh-port N SSH port (default: 22)
64
66
  -s, --security MODE auto | nla | tls | rdp (default: auto)
65
67
  -W, --width N desktop width (default: 1280)
@@ -186,6 +188,8 @@ async function startSession(args: Args, name: string, host: string, port: number
186
188
  ];
187
189
  if (has(args, 'no-view', 'headless')) daemonArgs.push('--no-view');
188
190
  if (has(args, 'no-open')) daemonArgs.push('--no-open');
191
+ if (has(args, 'tab')) daemonArgs.push('--tab');
192
+ if (has(args, 'fullscreen')) daemonArgs.push('--fullscreen');
189
193
 
190
194
  // Its output goes to a file rather than nowhere: a session that fails to
191
195
  // start is the one moment its log is worth having.
@@ -273,6 +277,8 @@ async function main(): Promise<void> {
273
277
  viewPort: Number(flag(args, 'view-port') ?? 7373),
274
278
  sshUsername: flag(args, 'ssh-user') ?? flag(args, 'u', 'user') ?? '',
275
279
  sshPort: Number(flag(args, 'ssh-port') ?? 22),
280
+ tab: has(args, 'tab'),
281
+ fullscreen: has(args, 'fullscreen'),
276
282
  // Zero by default: a session stays open until it is closed. An agent
277
283
  // that pauses to think is not an agent that has finished.
278
284
  idleMs: Math.max(0, Number(flag(args, 'idle') ?? 0)) * 60_000,
@@ -280,6 +286,9 @@ async function main(): Promise<void> {
280
286
  return;
281
287
  }
282
288
 
289
+ // The hidden command the native window runs as.
290
+ if (args.command === '__window') { await runWindow(process.argv.slice(3)); return; }
291
+
283
292
  if (args.command === 'shell') { await interactiveShell(args, host); return; }
284
293
 
285
294
  // Everything else runs against a session: the one already open, or a new one.
@@ -296,7 +305,7 @@ async function main(): Promise<void> {
296
305
  if (!has(args, 'no-view', 'headless') && !status.viewer) {
297
306
  Object.assign(status, await ask(name, 'view-open', { launch: !has(args, 'no-open') }));
298
307
  }
299
- Object.assign(report, { viewer: status.viewer });
308
+ Object.assign(report, { viewer: status.viewer, window: status.window, viewers: status.viewers });
300
309
  break;
301
310
 
302
311
  case 'view': {
@@ -376,7 +385,7 @@ async function interactiveShell(args: Args, host: string): Promise<void> {
376
385
  const shell = new Shell({
377
386
  host,
378
387
  port: Number(flag(args, 'ssh-port') ?? 22),
379
- username: flag(args, 'ssh-user') ?? flag(args, 'u', 'user') ?? '',
388
+ username: flag(args, 'ssh-user') ?? bareUsername(flag(args, 'u', 'user') ?? ''),
380
389
  password: password(),
381
390
  columns: process.stdout.columns ?? 120,
382
391
  rows: process.stdout.rows ?? 30,
@@ -423,8 +432,10 @@ function describe(command: string, report: Record<string, unknown>): string {
423
432
  const where = report.reused ? 'the open session' : 'a new session';
424
433
 
425
434
  if (command === 'open') {
426
- return `${report.reused ? 'Already connected' : 'Connected'} to ${report.host}:${report.port} at ${size}.`
427
- + `${report.viewer ? `\nWindow: ${report.viewer}` : ''}`
435
+ const window = report.window === 'window' ? 'A window is open on it.'
436
+ : report.window === 'tab' ? 'It opened in your browser.'
437
+ : report.viewer ? `Open it at ${report.viewer}` : 'No window (headless).';
438
+ return `${report.reused ? 'Already connected' : 'Connected'} to ${report.host}:${report.port} at ${size}. ${window}`
428
439
  + `\nRun more commands against it, then \`${NAME} close ${report.host}\` when you are done.`;
429
440
  }
430
441
  if (command === 'view') {
package/src/cli/daemon.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  import net from 'node:net';
14
14
  import { unlinkSync, writeFileSync, existsSync } from 'node:fs';
15
15
  import { RdpSession, type SessionOptions } from './session';
16
- import { Shell } from './shell';
16
+ import { bareUsername, Shell } from './shell';
17
17
  import { downscale, encodePng } from './png';
18
18
  import { startViewer, type Viewer } from './viewer';
19
19
  import { readMessages, writeMessage, type Request } from './ipc';
@@ -26,6 +26,10 @@ export interface DaemonOptions extends SessionOptions {
26
26
  viewPort: number;
27
27
  /** Account for the terminal, when one is wanted. Blank leaves SSH unopened. */
28
28
  sshUsername: string;
29
+ /** Open in the default browser rather than a window of its own. */
30
+ tab: boolean;
31
+ /** Start the window already filling the display. */
32
+ fullscreen: boolean;
29
33
  sshPort: number;
30
34
  idleMs: number;
31
35
  }
@@ -43,6 +47,10 @@ export async function runDaemon(options: DaemonOptions): Promise<void> {
43
47
  * The terminal is opened on first use rather than at connect time. Most
44
48
  * sessions never ask for one, and a second sign-in that nobody wanted is a
45
49
  * second chance to lock an account out.
50
+ *
51
+ * It signs in as the desktop's own account and password by default. Needing a
52
+ * separate flag to get a terminal on the machine you are already looking at
53
+ * is a question with an obvious answer, so it is not asked.
46
54
  */
47
55
  function openShell(): Promise<Shell> {
48
56
  if (shell) return Promise.resolve(shell);
@@ -52,7 +60,7 @@ export async function runDaemon(options: DaemonOptions): Promise<void> {
52
60
  const started = new Shell({
53
61
  host: options.host,
54
62
  port: options.sshPort,
55
- username: options.sshUsername || options.username,
63
+ username: options.sshUsername || bareUsername(options.username),
56
64
  password: options.password,
57
65
  });
58
66
  await started.connect();
@@ -100,7 +108,12 @@ export async function runDaemon(options: DaemonOptions): Promise<void> {
100
108
  if (launch) viewer.launch();
101
109
  return viewer;
102
110
  }
103
- viewer = await startViewer(session, options.viewPort, { launch, openShell });
111
+ viewer = await startViewer(session, options.viewPort, {
112
+ launch,
113
+ openShell,
114
+ tab: options.tab,
115
+ fullscreen: options.fullscreen,
116
+ });
104
117
  writeMeta();
105
118
  return viewer;
106
119
  }
@@ -143,7 +156,7 @@ export async function runDaemon(options: DaemonOptions): Promise<void> {
143
156
  switch (message.op) {
144
157
  case 'view-open': {
145
158
  const opened = await openViewer(args.launch !== false);
146
- return { viewer: opened.url };
159
+ return { viewer: opened.url, window: opened.window?.kind ?? null };
147
160
  }
148
161
 
149
162
  case 'view-close':
@@ -158,6 +171,8 @@ export async function runDaemon(options: DaemonOptions): Promise<void> {
158
171
  height: session.framebuffer.height,
159
172
  connected: session.connected,
160
173
  viewer: viewer?.url ?? null,
174
+ window: viewer?.window?.kind ?? null,
175
+ viewers: viewer?.viewers ?? 0,
161
176
  shell: shell ? 'open' : shellStarting ? 'opening' : 'closed',
162
177
  pid: process.pid,
163
178
  };