ai-remote 0.6.0 → 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.
@@ -4,8 +4,8 @@ import {
4
4
  openWindow,
5
5
  readCurrentDisplaySize,
6
6
  runWindow
7
- } from "./cli-chunk-P4645KKB.mjs";
8
- import "./cli-chunk-TKKENOSJ.mjs";
7
+ } from "./cli-chunk-CD47NHZT.mjs";
8
+ import "./cli-chunk-MURUADW4.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-TKKENOSJ.mjs";
16
+ } from "./cli-chunk-MURUADW4.mjs";
17
17
 
18
18
  // src/cli/cli.ts
19
19
  import { resolve as resolvePath } from "node:path";
@@ -62,7 +62,14 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
62
62
  "vnc-port",
63
63
  "port",
64
64
  "keys",
65
- "region"
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"
66
73
  ]);
67
74
  var OPTIONAL_VALUE_FLAGS = { shot: /\.(png|jpe?g)$/i };
68
75
  function parse(argv) {
@@ -105,6 +112,22 @@ var shotFile = (args) => {
105
112
  if (value === void 0) return void 0;
106
113
  return typeof value === "string" && value || flag(args, "o", "out") || "screen.png";
107
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");
108
131
 
109
132
  // src/cli/prompt.ts
110
133
  function canPrompt() {
@@ -250,6 +273,7 @@ The rest drive that session, and take no host:
250
273
 
251
274
  ${NAME} shot screenshot the desktop
252
275
  ${NAME} click X,Y click, --button right|middle, --double
276
+ --hold MS, --count N, --with Shift
253
277
  ${NAME} type "text" type into whatever has focus
254
278
  ${NAME} key <Code> press keys, e.g. MetaLeft, ControlLeft+KeyA
255
279
  ${NAME} exec "cmd" run a command in the terminal (SSH)
@@ -261,6 +285,7 @@ The rest drive that session, and take no host:
261
285
  ${NAME} batch actions.json structured actions; - reads JSON from stdin
262
286
  ${NAME} vnc give this session a screen over VNC
263
287
  (--vnc-port N, default 5900)
288
+ ${NAME} input off take the pointer from the window (on gives it back)
264
289
  ${NAME} view open the window on the session
265
290
  ${NAME} view --watch-only a window that watches rather than drives
266
291
  ${NAME} view --close close the window, session keeps running
@@ -307,7 +332,7 @@ Options for open
307
332
  -H, --height N desktop height (default: 800)
308
333
  --fullscreen fill the display; RDP uses its current resolution
309
334
  --side-panel open the SSH terminal docked on the right
310
- --audio play the remote computer's sound in the window (RDP)
335
+ --no-audio disable remote sound (enabled by default for windowed RDP)
311
336
  --no-clipboard do not share this machine's clipboard (shared by default)
312
337
  --insecure-host-key
313
338
  accept an SSH host key that does not match the one on
@@ -342,6 +367,29 @@ Options for the rest
342
367
  --region X,Y,W,H capture a desktop region; --json reports its origin
343
368
  --json machine-readable result on stdout
344
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
+
345
393
  Signing in
346
394
  A terminal signs in with a key where it can: with no --identity, ssh-agent is
347
395
  asked for what it is holding and ~/.ssh/id_ed25519, id_ecdsa and id_rsa are
@@ -473,7 +521,7 @@ async function startSession(args, name, host, port, secret, account = "") {
473
521
  const explicitHeight = flag(args, "H", "height");
474
522
  let fullscreenSize = null;
475
523
  if (mode === "rdp" && has(args, "fullscreen") && (!explicitWidth || !explicitHeight)) {
476
- const { detectCurrentDisplaySize } = await import("./cli-window-AWXZ5NJX.mjs");
524
+ const { detectCurrentDisplaySize } = await import("./cli-window-G5ESQTF5.mjs");
477
525
  const display = await detectCurrentDisplaySize();
478
526
  if (display) fullscreenSize = fullscreenRdpSize(display, has(args, "side-panel"));
479
527
  }
@@ -523,13 +571,14 @@ async function startSession(args, name, host, port, secret, account = "") {
523
571
  if (has(args, "tab")) daemonArgs.push("--tab");
524
572
  if (has(args, "fullscreen")) daemonArgs.push("--fullscreen");
525
573
  if (has(args, "side-panel") && mode !== "ssh") daemonArgs.push("--side-panel");
526
- if (has(args, "audio", "sound")) {
574
+ if (has(args, "no-audio")) daemonArgs.push("--no-audio");
575
+ if (audioEnabled(args, mode) || has(args, "audio", "sound")) {
527
576
  if (mode !== "rdp") {
528
577
  process.stderr.write(`--audio is RDP only; a ${mode.toUpperCase()} session has no audio channel.
529
578
  `);
530
579
  } else if (has(args, "no-view", "headless")) {
531
580
  process.stderr.write("--audio needs a window to play into; ignoring it for a headless session.\n");
532
- } else {
581
+ } else if (audioEnabled(args, mode)) {
533
582
  daemonArgs.push("--audio");
534
583
  }
535
584
  }
@@ -597,7 +646,7 @@ async function main() {
597
646
  }
598
647
  const json = has(args, "json");
599
648
  if (args.command === "__display-size") {
600
- const { readCurrentDisplaySize } = await import("./cli-window-AWXZ5NJX.mjs");
649
+ const { readCurrentDisplaySize } = await import("./cli-window-G5ESQTF5.mjs");
601
650
  const size = await readCurrentDisplaySize();
602
651
  if (size) process.stdout.write(`${JSON.stringify(size)}
603
652
  `);
@@ -606,7 +655,7 @@ async function main() {
606
655
  if (args.command === "__session") {
607
656
  const mode = modeFor(args, args.rest[0]);
608
657
  const { host, port } = splitTarget(args.rest[0] ?? "", defaultPort(mode));
609
- const { runDaemon } = await import("./cli-daemon-3L36TBIB.mjs");
658
+ const { runDaemon } = await import("./cli-daemon-SD2AS34J.mjs");
610
659
  await runDaemon({
611
660
  name: sessionName(host, port, flag(args, "name", "session")),
612
661
  host,
@@ -631,7 +680,7 @@ async function main() {
631
680
  watchOnly: has(args, "watch-only", "observe"),
632
681
  // RDP is the only protocol here with an audio channel, and a session with
633
682
  // no window has nowhere to play what the channel would carry.
634
- audio: has(args, "audio", "sound") && mode === "rdp" && !has(args, "no-view", "headless"),
683
+ audio: audioEnabled(args, mode),
635
684
  idleMs: Math.max(0, Number(flag(args, "idle") ?? 0)) * 6e4,
636
685
  keys: flag(args, "keys") === "literal" ? "literal" : "adapt",
637
686
  clipboard: !has(args, "no-clipboard"),
@@ -640,7 +689,7 @@ async function main() {
640
689
  return;
641
690
  }
642
691
  if (args.command === "__window") {
643
- const { runWindow } = await import("./cli-window-AWXZ5NJX.mjs");
692
+ const { runWindow } = await import("./cli-window-G5ESQTF5.mjs");
644
693
  await runWindow(process.argv.slice(3));
645
694
  return;
646
695
  }
@@ -984,6 +1033,12 @@ Open the host again without --ssh for a desktop.`,
984
1033
  region: flag(args, "region"),
985
1034
  maxEdge: Number(flag(args, "max-edge") ?? 0)
986
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
+ });
987
1042
  switch (args.command) {
988
1043
  case "status": {
989
1044
  Object.assign(summary, await tell(session, "status", {}, 1e4));
@@ -1012,8 +1067,11 @@ Open the host again without --ssh for a desktop.`,
1012
1067
  if (!spec) throw new CliError(`click needs a position, e.g. \`${NAME} click 640,400\``, 12);
1013
1068
  const [x, y] = spec.split(",").map(Number);
1014
1069
  const button = flag(args, "button") ?? "left";
1015
- const step = `click ${x},${y}${button === "left" ? "" : `,${button}`}`;
1016
- Object.assign(summary, await runSteps(has(args, "double") ? `${step}; ${step}` : step));
1070
+ Object.assign(summary, await tell(session, "batch", {
1071
+ actions: [clickAction(args)],
1072
+ ...pacing(),
1073
+ ...observation(shotFile(args))
1074
+ }));
1017
1075
  Object.assign(summary, { clicked: { x, y, button } });
1018
1076
  break;
1019
1077
  }
@@ -1023,6 +1081,7 @@ Open the host again without --ssh for a desktop.`,
1023
1081
  if (!text) throw new CliError("type needs some text", 12);
1024
1082
  Object.assign(summary, await tell(session, "batch", {
1025
1083
  actions: [{ type: "type", text }],
1084
+ ...pacing(),
1026
1085
  ...observation(shotFile(args))
1027
1086
  }));
1028
1087
  Object.assign(summary, { typed: text.length });
@@ -1030,7 +1089,12 @@ Open the host again without --ssh for a desktop.`,
1030
1089
  }
1031
1090
  case "key": {
1032
1091
  if (!args.rest.length) throw new CliError("key needs at least one code, e.g. MetaLeft", 12);
1033
- Object.assign(summary, await runSteps(args.rest.map((code) => `key ${code}`).join("; ")));
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
+ }));
1034
1098
  Object.assign(summary, { keys: args.rest });
1035
1099
  break;
1036
1100
  }
@@ -1051,7 +1115,27 @@ Open the host again without --ssh for a desktop.`,
1051
1115
  } catch (error) {
1052
1116
  throw new CliError(`${file === "-" ? "stdin" : file} is not valid JSON: ${error.message}`, 12);
1053
1117
  }
1054
- Object.assign(summary, await tell(session, "batch", { actions, ...observation(shotFile(args)) }));
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" }));
1055
1139
  break;
1056
1140
  }
1057
1141
  case "exec": {
@@ -1370,6 +1454,14 @@ The password is saved in ${summary.saved}.` : ""
1370
1454
  if (command === "type") return `Typed ${summary.typed} characters.${looked}`;
1371
1455
  if (command === "batch") return `Ran ${summary.actions} actions (${summary.clicks} clicks) in ${summary.dispatchMs} ms of dispatch.${looked}`;
1372
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.";
1464
+ }
1373
1465
  if (command === "do") return `Ran ${summary.steps.length} steps: ${summary.steps.join(" \u2192 ")}${looked}`;
1374
1466
  return "Done.";
1375
1467
  }
@@ -2,34 +2,102 @@ export type ComputerPoint = {
2
2
  x: number;
3
3
  y: number;
4
4
  };
5
+ /** The wire's 0/1/2, or the name for it. */
6
+ export type ComputerButton = 0 | 1 | 2 | 'left' | 'middle' | 'right' | 'l' | 'm' | 'r';
7
+ /** A modifier chord held for the length of one action, e.g. `ShiftLeft`. */
8
+ type Modified = {
9
+ key?: string;
10
+ };
5
11
  export type ComputerAction = ({
6
12
  type: 'click';
7
- button?: number;
13
+ button?: ComputerButton;
14
+ /** ChatGPT's spelling of `button`. */
15
+ mouse_button?: ComputerButton;
8
16
  count?: number;
17
+ /** ChatGPT's spelling of `count`. */
18
+ click_count?: number;
9
19
  intervalMs?: number;
10
- } & ComputerPoint) | ({
20
+ /** Milliseconds to hold the button down for each click. */
21
+ duration?: number;
22
+ } & ComputerPoint & Modified) | ({
11
23
  type: 'move';
12
- } & ComputerPoint) | ({
24
+ } & ComputerPoint & Modified) | ({
13
25
  type: 'scroll';
26
+ /** Wheel notches, positive right and down. */
14
27
  dx?: number;
15
28
  dy?: number;
16
- } & ComputerPoint) | {
29
+ /** ChatGPT's spelling: a direction and a distance in pixels. */
30
+ direction?: 'up' | 'down' | 'left' | 'right' | 'u' | 'd' | 'l' | 'r';
31
+ pixels?: number;
32
+ } & ComputerPoint & Modified) | {
17
33
  type: 'type';
18
34
  text: string;
19
35
  chunkSize?: number;
20
36
  intervalMs?: number;
21
37
  } | {
22
38
  type: 'key';
23
- chord: string;
39
+ chord?: string;
40
+ /** ChatGPT's spelling of `chord`. */
41
+ key?: string;
42
+ /** Milliseconds to hold the chord before releasing it. */
43
+ duration?: number;
24
44
  } | {
25
45
  type: 'wait';
26
46
  ms: number;
27
- } | {
47
+ } | ({
28
48
  type: 'drag';
29
49
  path: ComputerPoint[];
30
- button?: number;
50
+ button?: ComputerButton;
31
51
  intervalMs?: number;
52
+ } & Modified)
53
+ /**
54
+ * A drag held open across calls, which is ChatGPT's `drag_handle()`.
55
+ *
56
+ * `drag` is one gesture with a known shape. These three are for the drag
57
+ * whose ending depends on what the drag itself reveals: press, take a
58
+ * screenshot, decide, and only then release. The button stays down on the
59
+ * host between calls, so something has to end it -- the session lets go if it
60
+ * closes first, but nothing else will.
61
+ */
62
+ | ({
63
+ type: 'drag-start';
64
+ button?: ComputerButton;
65
+ } & ComputerPoint) | ({
66
+ type: 'drag-move';
67
+ } & ComputerPoint) | {
68
+ type: 'drag-end';
32
69
  };
70
+ export interface BatchOptions {
71
+ /**
72
+ * Wait for the drawn cursor to reach each target before the input goes out.
73
+ *
74
+ * Only means anything while a viewer is attached; there is no cursor to watch
75
+ * otherwise and nothing to wait for. It makes a batch slower on purpose, in
76
+ * exchange for a click that a person watching sees land.
77
+ */
78
+ animate?: boolean;
79
+ /**
80
+ * A floor between consecutive actions, ChatGPT's `post_action_sleep_ms`.
81
+ *
82
+ * Theirs is 100ms and always on. This defaults to none: a batch exists to
83
+ * spend one model turn on many inputs, and `dispatchMs` is a published
84
+ * measurement. Ask for it when the far side needs the breathing room.
85
+ */
86
+ postActionSleepMs?: number;
87
+ /**
88
+ * Take the keyboard and pointer from the viewer for the length of this batch.
89
+ *
90
+ * A remote desktop has one pointer. An agent does not get its own -- neither
91
+ * protocol has a second one to give it -- so a hand resting on a trackpad is
92
+ * moving the same cursor the agent is aiming, and a click that lands where
93
+ * the hand left it is worse than one that does not land at all. This holds
94
+ * the input for the batch and gives it back afterwards.
95
+ *
96
+ * Not a lock. The control button in the window takes it back at once, and
97
+ * `interference` in the next result says that it happened.
98
+ */
99
+ exclusive?: boolean;
100
+ }
33
101
  export interface ObservationOptions {
34
102
  region?: {
35
103
  x: number;
@@ -62,6 +130,21 @@ export interface ComputerBatchResult {
62
130
  clicks: number;
63
131
  /** Input dispatch and requested pacing; not proof of application completion. */
64
132
  dispatchMs: number;
133
+ /** A `drag-start` is still holding a button down on the host. */
134
+ dragging: boolean;
135
+ /**
136
+ * Input events that arrived from a viewer while this batch ran.
137
+ *
138
+ * Zero is the ordinary answer. Anything else is somebody using the machine at
139
+ * the same time, and their pointer is the same pointer: treat the actions in
140
+ * this batch as having possibly landed somewhere else, look before carrying
141
+ * on, and consider `exclusive` or `takeInput()` if it keeps happening.
142
+ * Refused input counts, so this stays a useful signal after the input has
143
+ * been taken away.
144
+ */
145
+ interference: number;
146
+ /** Whether the people watching may drive right now. */
147
+ humanInput: boolean;
65
148
  }
66
149
  /** Map a point in a returned image back to full-desktop coordinates. */
67
150
  export declare function desktopPoint(image: ComputerScreenshot, point: ComputerPoint): ComputerPoint;
@@ -71,7 +154,23 @@ export declare class RemoteComputer {
71
154
  private call;
72
155
  screenshot(options?: ObservationOptions): Promise<ComputerScreenshot>;
73
156
  /** One IPC call, no reconnect or model invocation between actions. */
74
- act(actions: ComputerAction[], options?: ObservationOptions): Promise<ComputerBatchResult & ComputerScreenshot>;
157
+ act(actions: ComputerAction[], options?: ObservationOptions & BatchOptions): Promise<ComputerBatchResult & ComputerScreenshot>;
158
+ /**
159
+ * Take the keyboard and pointer from every window, until given back.
160
+ *
161
+ * For an agent that has seen `interference` and decided to work alone.
162
+ * `releaseInput()` undoes it, and so does the control button in the window --
163
+ * which is the point: the person at the machine is never locked out of it.
164
+ */
165
+ takeInput(): Promise<{
166
+ humanInput: boolean;
167
+ viewers: number;
168
+ }>;
169
+ /** Give the keyboard and pointer back to whoever is watching. */
170
+ releaseInput(): Promise<{
171
+ humanInput: boolean;
172
+ viewers: number;
173
+ }>;
75
174
  /** Explicitly skip observation for a known sequence; inspect when needed. */
76
- dispatch(actions: ComputerAction[]): Promise<ComputerBatchResult>;
175
+ dispatch(actions: ComputerAction[], options?: BatchOptions): Promise<ComputerBatchResult>;
77
176
  }
package/dist/computer.js CHANGED
@@ -1,3 +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};
1
+ import g from"node:net";import{StringDecoder as f}from"node:string_decoder";function w(r,e){let t=new f("utf8"),n="";r.on("data",u=>{for(n+=t.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=(r,e)=>{r.write(`${JSON.stringify(e)}
3
+ `)};function p(r,e,t={},n=12e4){return new Promise((u,s)=>{let o=g.connect(r),a=!1,m=(i,b)=>{a||(a=!0,clearTimeout(l),o.destroy(),i?s(i):u(b))},l=setTimeout(()=>m(new Error(`The session did not answer "${e}" in time.`)),n);o.on("connect",()=>x(o,{id:1,op:e,args:t})),o.on("error",i=>{m(Object.assign(i,{notRunning:i.code==="ENOENT"||i.code==="ECONNREFUSED"}))}),w(o,i=>m(null,i))})}import{homedir as y}from"node:os";import{join as c}from"node:path";var k=c(y(),".ai-remote"),C=c(k,"run");var h=r=>c(C,`${r}.sock`);function N(r,e){if(!Number.isFinite(e.x)||!Number.isFinite(e.y)||e.x<0||e.y<0||e.x>=r.width||e.y>=r.height)throw new Error("Point must be inside the screenshot.");let t=r.viewport??{x:0,y:0,width:r.desktopWidth,height:r.desktopHeight};return{x:Math.min(t.x+t.width-1,Math.floor(t.x+e.x*t.width/r.width)),y:Math.min(t.y+t.height-1,Math.floor(t.y+e.y*t.height/r.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,t){let n=await p(h(this.session),e,t);if(!n.ok)throw new Error(n.error??"Remote operation failed.");return n.result}screenshot(e={}){return this.call("shot",{...e})}act(e,t={}){return this.call("batch",{...t,actions:e})}takeInput(){return this.call("input",{on:!1})}releaseInput(){return this.call("input",{on:!0})}dispatch(e,t={}){return this.call("batch",{...t,actions:e,observe:!1})}};export{d as RemoteComputer,N as desktopPoint};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-remote",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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": {