@rebind.gg/mcp-server 0.3.1 → 0.3.2

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebind.gg/mcp-server",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "MCP server exposing Rebind as computer-use tools: screenshot, verified click, type, key, scroll, and window control over the Remote Control protocol.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@modelcontextprotocol/sdk": "^1.12.0",
53
- "@rebind.gg/client-ts": "0.3.0",
53
+ "@rebind.gg/client-ts": "0.3.3",
54
54
  "sharp": "^0.34.2",
55
55
  "zod": "^3.25.0"
56
56
  },
@@ -0,0 +1,55 @@
1
+ // The meditate tool's check on model- and page-chosen URLs before they reach
2
+ // relay-side curl (spliced into a shell command inside Lua long-bracket
3
+ // quoting) or the user's browser.
4
+
5
+ import { isIP } from "node:net";
6
+
7
+ /** plain http(s), with nothing that could leave the shell or Lua quoting or
8
+ * that curl reads as a glob ([] and {}); bracketed IPv6 literals stop here */
9
+ const SAFE_URL = /^https?:\/\/[^\s"'`\\\][{}]+$/;
10
+
11
+ // IPv4 ranges that are not public, as [base address, prefix length]
12
+ const RESERVED_V4: [string, number][] = [
13
+ ["0.0.0.0", 8],
14
+ ["10.0.0.0", 8],
15
+ ["100.64.0.0", 10],
16
+ ["127.0.0.0", 8],
17
+ ["169.254.0.0", 16],
18
+ ["172.16.0.0", 12],
19
+ ["192.0.0.0", 24],
20
+ ["192.0.2.0", 24],
21
+ ["192.168.0.0", 16],
22
+ ["198.18.0.0", 15],
23
+ ["198.51.100.0", 24],
24
+ ["203.0.113.0", 24],
25
+ ["224.0.0.0", 3], // multicast and reserved, through 255.255.255.255
26
+ ];
27
+
28
+ const toInt = (ip: string) =>
29
+ ip.split(".").reduce((n, octet) => n * 256 + Number(octet), 0);
30
+ const inRange = (ip: string, [base, bits]: [string, number]) =>
31
+ Math.floor(toInt(ip) / 2 ** (32 - bits)) ===
32
+ Math.floor(toInt(base) / 2 ** (32 - bits));
33
+
34
+ // names that resolve to this machine, the LAN, or a cloud metadata service
35
+ const PRIVATE_NAME = /(^|\.)(localhost|local|internal|home\.arpa)$/;
36
+
37
+ /** Whether a URL may be fetched or opened for the user. Names are checked as
38
+ * written, not resolved, so a public name that points at a private address
39
+ * (127.0.0.1.nip.io, DNS rebinding) still passes, and the browser follows
40
+ * redirects before the landed URL is checked. Relay-side curl runs without
41
+ * -L, so its redirects are not followed. */
42
+ export function isSafeFetchUrl(u: string): boolean {
43
+ if (!SAFE_URL.test(u)) return false;
44
+ let host: string;
45
+ try {
46
+ host = new URL(u).hostname.toLowerCase();
47
+ } catch {
48
+ return false;
49
+ }
50
+ if (host.endsWith(".")) host = host.slice(0, -1);
51
+ // the URL parser has already turned 127.1, 0x7f.0.0.1 and 2130706433
52
+ // into dotted decimal
53
+ if (isIP(host) === 4) return !RESERVED_V4.some((r) => inRange(host, r));
54
+ return host.includes(".") && !PRIVATE_NAME.test(host);
55
+ }
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import { RebindRemote, ServerError } from "@rebind.gg/client-ts";
14
14
  import { z } from "zod";
15
15
  import pkg from "../package.json";
16
16
  import { mapToImage } from "./coords.ts";
17
+ import { isSafeFetchUrl } from "./fetch-guard.ts";
17
18
  import { renderScreenshot } from "./image.ts";
18
19
  import { normalizeCombo } from "./keys.ts";
19
20
  import { llmStep, type Msg } from "./llm.ts";
@@ -114,40 +115,11 @@ const MEDITATE_TOTAL_CHARS = 600_000;
114
115
  const MEDITATE_DL_MAX_BYTES = 26_214_400; // 25 MiB
115
116
  const MEDITATE_DL_TIMEOUT_S = 45;
116
117
 
117
- /** URLs the model may visit/download: plain http(s), no quotes/brackets that
118
- * could escape the shell or Lua long-bracket quoting they are spliced into. */
119
- const SAFE_URL = /^https?:\/\/[^\s"'`\\\][]+$/;
120
-
121
118
  /** Front app is a browser we can drive (Safari included — it fails later at the
122
119
  * javascript: step, but it is still a browser, not a terminal we'd type into). */
123
120
  const BROWSER_RE =
124
121
  /chrome|chromium|brave|firefox|edge|opera|vivaldi|arc|safari/i;
125
122
 
126
- /** SSRF guard: block loopback, RFC1918, link-local (incl. cloud metadata), and
127
- * *.local before a model-chosen URL reaches the real browser or relay curl.
128
- * Hostname-based — does not defeat DNS rebinding, but stops the direct hits. */
129
- function isSafeFetchUrl(u: string): boolean {
130
- if (!SAFE_URL.test(u)) return false;
131
- let host: string;
132
- try {
133
- host = new URL(u).hostname.toLowerCase();
134
- } catch {
135
- return false;
136
- }
137
- if (host === "localhost" || host === "0.0.0.0" || host.endsWith(".local"))
138
- return false;
139
- if (
140
- /^(127\.|10\.|192\.168\.|169\.254\.|::1$|\[?::1\]?$|fe80:|fc|fd)/i.test(
141
- host,
142
- )
143
- )
144
- return false;
145
- const m = /^(\d+)\.(\d+)\./.exec(host);
146
- if (m && Number(m[1]) === 172 && Number(m[2]) >= 16 && Number(m[2]) <= 31)
147
- return false;
148
- return true;
149
- }
150
-
151
123
  /** per-asset inlined-content budget fed to the model (text/csv sources) */
152
124
  const MEDITATE_ASSET_CHARS = 60_000;
153
125
  /** a fetched page shorter than this is a JS shell, not content — go to the browser */
@@ -530,6 +502,7 @@ async function main() {
530
502
  // an app finishing launch) and the payload would be typed into it instead.
531
503
  if (!BROWSER_RE.test(await frontApp())) {
532
504
  if (!appName) return null;
505
+ // App.Activate focuses a running browser only; a closed one stays closed
533
506
  await client.luaExec(
534
507
  `App.Activate(${JSON.stringify(appName)}) return true`,
535
508
  );
@@ -649,15 +622,11 @@ async function main() {
649
622
  await sleep(3000);
650
623
  };
651
624
 
652
- // Capture the display under the cursor, render it, and record the image->screen
653
- // mapping on the session so a subsequent verifiedClick(mapImagePoint(...)) lands
654
- // correctly. Returns the webp bytes as a base64 data URL for vision models.
655
- const captureScreen = async (): Promise<{
656
- dataUrl: string;
657
- width: number;
658
- height: number;
659
- }> => {
660
- const cap = await client.screenCapture({});
625
+ // Capture a display (default: the one under the cursor), render it, and record
626
+ // the image->screen mapping on the session so a subsequent
627
+ // verifiedClick(mapImagePoint(...)) lands correctly.
628
+ const captureScreen = async (display?: number) => {
629
+ const cap = await client.screenCapture({ display });
661
630
  const shot = await renderScreenshot({
662
631
  png: Buffer.from(cap.image, "base64"),
663
632
  logicalWidth: cap.display.width,
@@ -675,11 +644,7 @@ async function main() {
675
644
  imageWidth: shot.width,
676
645
  imageHeight: shot.height,
677
646
  };
678
- return {
679
- dataUrl: `data:image/webp;base64,${shot.data.toString("base64")}`,
680
- width: shot.width,
681
- height: shot.height,
682
- };
647
+ return { cap, shot };
683
648
  };
684
649
 
685
650
  // Resolve a window (title substring, or the front window when want=""),
@@ -737,24 +702,7 @@ return { ok = true, proc = proc }
737
702
  },
738
703
  },
739
704
  async ({ display }) => {
740
- const cap = await client.screenCapture({ display });
741
- const shot = await renderScreenshot({
742
- png: Buffer.from(cap.image, "base64"),
743
- logicalWidth: cap.display.width,
744
- logicalHeight: cap.display.height,
745
- cursor: {
746
- x: cap.cursor.x - cap.display.x,
747
- y: cap.cursor.y - cap.display.y,
748
- },
749
- });
750
- session.lastShot = {
751
- displayIndex: cap.display.index,
752
- originX: cap.display.x,
753
- originY: cap.display.y,
754
- scale: shot.scale,
755
- imageWidth: shot.width,
756
- imageHeight: shot.height,
757
- };
705
+ const { cap, shot } = await captureScreen(display);
758
706
  const win = await client.systemWindow().catch(() => null);
759
707
  const lines = [
760
708
  `display ${cap.display.index}: ${cap.display.width}x${cap.display.height} at (${cap.display.x}, ${cap.display.y})${cap.display.primary ? " (primary)" : ""}`,
@@ -1446,7 +1394,8 @@ return { ok = Window.IsActive(h), name = name }
1446
1394
  // Fetch a URL's text relay-side. This is the primary way content is
1447
1395
  // gathered: a GET returns exactly what the server has, where driving the
1448
1396
  // browser to the same URL costs HID, focus, and a fragile extraction.
1449
- // Same SSRF guard and size/time limits as the download path.
1397
+ // Same SSRF guard and size/time limits as the download path, and no -L:
1398
+ // a redirect target is a host the guard never checked.
1450
1399
  const fetchText = async (
1451
1400
  u: string,
1452
1401
  maxChars: number,
@@ -1454,7 +1403,7 @@ return { ok = Window.IsActive(h), name = name }
1454
1403
  if (!isSafeFetchUrl(u)) return null;
1455
1404
  try {
1456
1405
  const out = await client.luaExec(
1457
- `local r = System.Exec([[curl -fsSL --proto '=http,https' --max-filesize ${MEDITATE_DL_MAX_BYTES} --max-time ${MEDITATE_DL_TIMEOUT_S} '${u}']], { timeout = ${(MEDITATE_DL_TIMEOUT_S + 10) * 1000} }) return r.stdout`,
1406
+ `local r = System.Exec([[curl -fsS -g --proto '=http,https' --max-filesize ${MEDITATE_DL_MAX_BYTES} --max-time ${MEDITATE_DL_TIMEOUT_S} '${u}']], { timeout = ${(MEDITATE_DL_TIMEOUT_S + 10) * 1000} }) return r.stdout`,
1458
1407
  );
1459
1408
  const s = String(out ?? "");
1460
1409
  return s.trim() ? s.slice(0, maxChars) : null;
@@ -1567,7 +1516,12 @@ return { ok = Window.IsActive(h), name = name }
1567
1516
  if (haveBrowser)
1568
1517
  await client.luaExec(positionLua("", 0, 0, 1200, 900));
1569
1518
  if (isShell) {
1570
- if (haveBrowser) first = await extractPage(browser, pageUrl);
1519
+ const d = haveBrowser ? await extractPage(browser, pageUrl) : null;
1520
+ if (d && isSafeFetchUrl(d.split("\n")[1] ?? "")) first = d;
1521
+ else if (d)
1522
+ note(
1523
+ "rendered page refused: the browser landed on a host the fetch guard refuses",
1524
+ );
1571
1525
  if (!first) first = raw; // no browser — the shell is all we have
1572
1526
  }
1573
1527
  // PREPEND, never append: the per-page budget keeps only the first
@@ -1649,11 +1603,12 @@ return { ok = Window.IsActive(h), name = name }
1649
1603
  .replace(/[^\w.-]/g, "_")
1650
1604
  .slice(0, 80) || "asset";
1651
1605
  const path = `${dlDir}/${assets.length}-${name}`;
1652
- // isSafeFetchUrl bans quotes/brackets AND private hosts, so this
1653
- // splice is injection-safe; --proto and no -L block redirect-SSRF,
1606
+ // isSafeFetchUrl bans quotes, brackets and braces and private
1607
+ // hosts, and -g turns curl's URL globbing off, so this splice is
1608
+ // injection-safe; --proto and no -L block redirect-SSRF,
1654
1609
  // --max-filesize/--max-time bound a hostile response.
1655
1610
  const exit = await client.luaExec(
1656
- `local r = System.Exec([[curl -fsS --proto '=http,https' --max-filesize ${MEDITATE_DL_MAX_BYTES} --max-time ${MEDITATE_DL_TIMEOUT_S} -o '${path}' '${u}']], { timeout = ${(MEDITATE_DL_TIMEOUT_S + 10) * 1000} }) return r.exit`,
1611
+ `local r = System.Exec([[curl -fsS -g --proto '=http,https' --max-filesize ${MEDITATE_DL_MAX_BYTES} --max-time ${MEDITATE_DL_TIMEOUT_S} -o '${path}' '${u}']], { timeout = ${(MEDITATE_DL_TIMEOUT_S + 10) * 1000} }) return r.exit`,
1657
1612
  );
1658
1613
  const ok = Number(exit) === 0;
1659
1614
  // inline text/csv content so the model can READ the source, not
@@ -1729,7 +1684,11 @@ return { ok = Window.IsActive(h), name = name }
1729
1684
  if (haveBrowser) {
1730
1685
  await gotoUrl(pageUrl);
1731
1686
  try {
1732
- const shot = await captureScreen();
1687
+ const { shot } = await captureScreen();
1688
+ if (!isSafeFetchUrl(await tabUrl(browser || front)))
1689
+ throw new Error(
1690
+ "the browser landed on a URL the fetch guard refuses",
1691
+ );
1733
1692
  const vstep = await llmStep(
1734
1693
  [
1735
1694
  { role: "system", content: MEDITATE_VISION_SYS },
@@ -1737,7 +1696,12 @@ return { ok = Window.IsActive(h), name = name }
1737
1696
  role: "user",
1738
1697
  content: [
1739
1698
  { type: "text", text: "Read this page." },
1740
- { type: "image_url", image_url: { url: shot.dataUrl } },
1699
+ {
1700
+ type: "image_url",
1701
+ image_url: {
1702
+ url: `data:image/webp;base64,${shot.data.toString("base64")}`,
1703
+ },
1704
+ },
1741
1705
  ],
1742
1706
  },
1743
1707
  ],
@@ -1756,7 +1720,7 @@ return { ok = Window.IsActive(h), name = name }
1756
1720
  }
1757
1721
  } catch (e) {
1758
1722
  note(
1759
- `vision pass skipped (model may lack image input): ${e instanceof Error ? e.message : e}`,
1723
+ `vision pass skipped: ${e instanceof Error ? e.message : e}`,
1760
1724
  );
1761
1725
  }
1762
1726
  }
package/src/rebind.d.luau CHANGED
@@ -190,10 +190,10 @@ export type InputDevice = {
190
190
  id: string,
191
191
  name: string,
192
192
  nickname: string?,
193
- kind: "keyboard" | "mouse" | "other",
193
+ kind: "keyboard" | "mouse" | "gamepad" | "joystick" | "other",
194
194
  vid: string,
195
195
  pid: string,
196
- source: "vm" | "host",
196
+ source: "vm",
197
197
  exact: boolean,
198
198
  }
199
199
 
@@ -445,9 +445,8 @@ export type RebindInfo = {
445
445
  version: string,
446
446
  name: string,
447
447
  runtime_uuid: string,
448
- id: string?,
449
448
  instance: string,
450
- -- host operating system: "windows" | "macos" | "linux"
449
+ -- host operating system: "windows" | "macos"
451
450
  platform: string,
452
451
  }
453
452
 
@@ -541,9 +540,153 @@ export type RbPhone = {
541
540
  notify: (text: string, title: string?) -> PhoneResult,
542
541
  }
543
542
 
543
+ -- a colour is "#rrggbb" or "#rrggbbaa"; in widget text it may also be a theme
544
+ -- role: "text", "dim", "faint", "accent", "positive", "warning" or "danger"
545
+ export type OverlaySpan = string | { any }
546
+
547
+ -- a string (a newline starts a line) or one line of spans
548
+ export type OverlayText = string | { OverlaySpan }
549
+
550
+ -- one draw operation in a surface, in logical units from its top-left corner
551
+ export type OverlayOp = {
552
+ kind: "rect" | "text",
553
+ x: number?,
554
+ y: number?,
555
+ -- rect
556
+ w: number?,
557
+ h: number?,
558
+ fill: string?,
559
+ border: string?,
560
+ border_width: number?,
561
+ radius: number?,
562
+ -- true (50% black), false, or the shadow's opacity from 0 to 1
563
+ shadow: (boolean | number)?,
564
+ -- text: one line, as text plus color and bold, or as spans
565
+ text: string?,
566
+ spans: { OverlaySpan }?,
567
+ color: string?,
568
+ bold: boolean?,
569
+ size: number?,
570
+ }
571
+
572
+ -- where a surface goes: x and y in rb.screen coordinates, or an anchor
573
+ -- ("top-left", "top", "top-right", "left", "center", "right", "bottom-left",
574
+ -- "bottom", "bottom-right") on a display ("active": the foreground window's
575
+ -- monitor, "primary", "mouse"), moved offset_x and offset_y inward
576
+ export type OverlayPlacement = {
577
+ x: number?,
578
+ y: number?,
579
+ anchor: string?,
580
+ display: ("active" | "primary" | "mouse")?,
581
+ offset_x: number?,
582
+ offset_y: number?,
583
+ }
584
+
585
+ export type OverlaySurfaceOptions = OverlayPlacement & {
586
+ width: number,
587
+ height: number,
588
+ opacity: number?,
589
+ ops: { OverlayOp }?,
590
+ }
591
+
592
+ export type OverlaySurface = {
593
+ -- merges fields into the surface; x/y and anchor/display replace each other
594
+ set: (self: OverlaySurface, fields: { [string]: any }) -> OverlaySurface,
595
+ draw: (self: OverlaySurface, ops: { OverlayOp }) -> OverlaySurface,
596
+ remove: (self: OverlaySurface) -> (),
597
+ }
598
+
599
+ -- shared by the widgets; theme is a name from rb.overlay.themes or a table
600
+ -- of the same roles
601
+ export type OverlayWidgetOptions = OverlayPlacement & {
602
+ theme: (string | { [string]: any })?,
603
+ size: number?,
604
+ }
605
+
606
+ export type OverlayToastOptions = OverlayWidgetOptions & {
607
+ title: string?,
608
+ meta: string?,
609
+ -- ms on screen (default 4000)
610
+ duration: number?,
611
+ -- ms to wait for hidden overlays to show before giving up (default 60000)
612
+ expires: number?,
613
+ max_width: number?,
614
+ }
615
+
616
+ export type OverlayToast = {
617
+ close: () -> (),
618
+ }
619
+
620
+ export type OverlayMenuItem = string | {
621
+ label: string,
622
+ hint: string?,
623
+ action: ((index: number, item: any) -> ())?,
624
+ }
625
+
626
+ export type OverlayMenuOptions = OverlayWidgetOptions & {
627
+ title: string?,
628
+ subtitle: string?,
629
+ width: number?,
630
+ keep_open: boolean?,
631
+ on_select: ((index: number, item: any) -> ())?,
632
+ on_close: (() -> ())?,
633
+ }
634
+
635
+ export type OverlayMenu = {
636
+ selected: number,
637
+ select: (index: number) -> (),
638
+ close: () -> (),
639
+ }
640
+
641
+ export type OverlayStatus = {
642
+ set: (text: OverlayText) -> OverlayStatus,
643
+ remove: () -> (),
644
+ }
645
+
646
+ export type OverlayProgressOptions = OverlayWidgetOptions & {
647
+ -- logical units (default 320)
648
+ width: number?,
649
+ }
650
+
651
+ export type OverlayProgress = {
652
+ set: (fraction: number?, label: string?) -> OverlayProgress,
653
+ remove: () -> (),
654
+ }
655
+
656
+ export type RbOverlay = {
657
+ -- a surface you draw yourself; at most 16 per script covering at most
658
+ -- 2,000,000 square units together, removed when the script stops
659
+ surface: (options: OverlaySurfaceOptions) -> OverlaySurface,
660
+ -- width and height of one line of text in the bundled monospace face
661
+ measure: (text: string, size: number?, bold: boolean?) -> (number, number),
662
+ -- false only when the OS is known to hide overlays (Windows: an exclusive
663
+ -- full-screen Direct3D game; macOS: the overlays on screen covered for 2
664
+ -- seconds, so with none on screen it is true)
665
+ visible: () -> boolean,
666
+ -- removes every surface of this script
667
+ clear: () -> (),
668
+ -- "rebind", "rebind-light", "catppuccin", "tokyo-night", "contrast"
669
+ themes: { [string]: { [string]: any } },
670
+ -- a timed notification; toasts at one anchor stack, and one asked for while
671
+ -- overlays are hidden waits until they show or it expires. A waiting toast
672
+ -- that then cannot show is dropped with a warning in the script's log.
673
+ toast: (text: OverlayText, options: OverlayToastOptions?) -> OverlayToast,
674
+ -- a keyboard menu: while open it takes Up, Down, Enter, Escape and 1 to 9 and
675
+ -- swallows them. A pick closes it (unless keep_open), then runs the item's
676
+ -- action and on_select. Losing focus or the OS hiding overlays also closes
677
+ -- it; on_close runs for every close except rb.overlay.clear() and stopping.
678
+ -- nil and a reason when overlays are hidden, the script is unfocused, or a
679
+ -- menu is already open, in this script or another.
680
+ menu: (items: { OverlayMenuItem }, options: OverlayMenuOptions?) -> (OverlayMenu?, string?),
681
+ -- a one-line strip that stays until removed
682
+ status: (text: OverlayText, options: OverlayWidgetOptions?) -> OverlayStatus,
683
+ -- a labelled bar, fraction 0 to 1
684
+ progress: (label: string, fraction: number?, options: OverlayProgressOptions?) -> OverlayProgress,
685
+ }
686
+
544
687
  -- events for rb.on (Rebind 3.9.0 and later), with the arguments each
545
- -- handler receives. For down, up, move and scroll, a handler that returns
546
- -- false swallows the input.
688
+ -- handler receives. For down, up, scroll and axis, a handler that returns
689
+ -- false swallows the input; for move only with mouse_block=true.
547
690
  export type Event =
548
691
  | "start" -- ()
549
692
  | "stop" -- ()
@@ -557,6 +700,7 @@ export type Event =
557
700
  | "move" -- (dx: number, dy: number, device: InputDevice?) hardware mode only
558
701
  | "scroll" -- (delta: number, device: InputDevice?)
559
702
  | "scroll_h" -- (delta: number)
703
+ | "axis" -- (axis: string, value: number, device: InputDevice?) hardware mode only
560
704
  | "input_down" -- (key: string, device: InputDevice?)
561
705
  | "input_up" -- (key: string, duration_ms: number, device: InputDevice?)
562
706
  | "input_move" -- (dx: number, dy: number, device: InputDevice?)
@@ -577,9 +721,19 @@ export type Event =
577
721
  -- system: PrintScreen, Pause, Menu
578
722
  -- numpad: Kp0-Kp9, KpPlus, KpMinus, KpMultiply, KpDivide, KpEnter, KpDot
579
723
  --
580
- -- HID output only:
724
+ -- delivered as input only in hardware mode:
581
725
  -- function: F13-F24
582
- -- media: MediaNext, MediaPrev, MediaStop, MediaPlay, VolumeUp, VolumeDown, Mute
726
+ -- media: MediaNext, MediaPrev, MediaStop, MediaPlay, VolumeUp, VolumeDown, VolumeMute
727
+ --
728
+ -- game controllers, hardware mode only (named by position, Xbox letters):
729
+ -- buttons: PadA (bottom), PadB (right), PadX (left), PadY (top), PadLB,
730
+ -- PadRB, PadLT, PadRT, PadView, PadMenu, PadHome, PadLS, PadRS,
731
+ -- PadUp, PadDown, PadLeft, PadRight, PadP1-PadP4 (paddles)
732
+ -- other pads: Joy1-Joy32, in the order the controller reports them
733
+ -- axes: LeftX, LeftY, RightX, RightY (-1..1, up and left negative),
734
+ -- LT, RT (0..1); the "axis" event and input.axis use these
735
+ --
736
+ -- HID output and bind chord modifiers only:
583
737
  -- aliases: Numpad0-9, NP0-9, NPAdd, NPSubtract, NPMultiply, NPDivide, NPEnter,
584
738
  -- NPDecimal, Ctrl, Shift, Alt, Win, Meta, Del, PgUp, PgDn, etc.
585
739
 
@@ -652,7 +806,6 @@ declare rb: {
652
806
  -- most), waiting with sleep in rb.run and blocking elsewhere. With ms
653
807
  -- it glides there over ms on an eased path; a duration needs rb.run or
654
808
  -- rb.async.
655
- -- Not supported on Linux (no cursor source).
656
809
  move_to: (x: number, y: number, ms: number?) -> (),
657
810
  scroll: (amount: number) -> (),
658
811
  -- deprecated: the mode is stored but no longer changes move_to
@@ -662,13 +815,22 @@ declare rb: {
662
815
  -- up, press and combo also accept "usage:N" (decimal or 0x hex, 0x04-0xE7).
663
816
  usage: (name: string) -> number?,
664
817
  -- sets the whole joypad state (last write wins across scripts). Needs a
665
- -- Rebind Link whose USB profile has a joypad; raises otherwise.
818
+ -- Rebind Link whose USB profile has a joypad; raises otherwise. down, up,
819
+ -- press and combo also press controller buttons ("PadA").
666
820
  joypad: (state: JoypadState?) -> (),
667
- -- back to neutral; the relay also does this when a script stops or errors
821
+ -- holds one controller axis (LeftX..RightY -1..1, LT/RT 0..1) until set
822
+ -- again or reset; it wins over a captured controller's value
823
+ axis: (name: string, value: number) -> (),
824
+ -- drops what scripts set, keeping what a captured controller holds; the
825
+ -- relay also does this when a script stops or errors
668
826
  joypad_reset: () -> (),
669
827
  },
670
828
  input: {
671
829
  is_down: (code: string) -> boolean,
830
+ -- a controller axis by name, 0 at rest
831
+ axis: (name: string) -> number,
832
+ -- the devices Rebind has identified (3.12.0); empty in software mode
833
+ devices: () -> { InputDevice },
672
834
  duration: (code: string) -> number,
673
835
  keys: () -> { string },
674
836
  mouse_pos: () -> MousePos,
@@ -770,8 +932,9 @@ declare rb: {
770
932
  wait_close: (title: string, timeout: number?) -> boolean,
771
933
  },
772
934
  -- application control. macOS matches app display names ignoring case;
773
- -- Windows matches executable names (".exe" optional) and cannot hide;
774
- -- Linux raises on every call.
935
+ -- Windows matches executable names (".exe" optional) and cannot hide.
936
+ -- activate focuses a running app only (it needs Accessibility on macOS);
937
+ -- start one with rb.system.exec_detached (permission: exec).
775
938
  app: {
776
939
  front: () -> AppInfo?,
777
940
  is_front: (name: string) -> boolean,
@@ -791,13 +954,15 @@ declare rb: {
791
954
  head: (url: string, options: RbHttpOptions?) -> HttpResponse,
792
955
  request: (options: HttpRequestOptions) -> HttpResponse,
793
956
  listen: (port: number, handler: (request: HttpRequest) -> HttpResponseSpec | string) -> ServerHandle,
794
- ws_listen: (port: number, handlers: WsListenHandlers) -> WsServerHandle,
957
+ -- options.host binds one IP ("127.0.0.1" keeps it on this computer); default all interfaces
958
+ ws_listen: (port: number, handlers: WsListenHandlers, options: { host: string? }?) -> WsServerHandle,
795
959
  ws_connect: (url: string, handlers: WsConnectHandlers) -> WsConnectHandle,
796
960
  -- binds a UDP socket; handler receives each datagram's raw bytes and sender.
797
961
  udp_listen: (bind_ip: string, port: number, handler: (data: string, ip: string, port: number) -> ()) -> UdpHandle,
798
962
  -- this computer's IPv4 address on its default route, or nil when there is none.
799
963
  local_ip: () -> string?,
800
964
  },
965
+ -- permission: exec
801
966
  pipe: {
802
967
  open: (name: string, options: { size: number? }?) -> PipeHandle,
803
968
  },
@@ -812,7 +977,7 @@ declare rb: {
812
977
  -- nil path lists the script's own directory
813
978
  list: (path: string?) -> { string },
814
979
  mkdir: (path: string) -> boolean,
815
- -- recursive; returns true even when the directory did not exist.
980
+ -- recursive; raises when the directory does not exist or cannot be removed.
816
981
  rmdir: (path: string) -> boolean,
817
982
  copy: (src: string, dst: string) -> (),
818
983
  move: (src: string, dst: string) -> (),
@@ -879,6 +1044,7 @@ declare rb: {
879
1044
  },
880
1045
  env: {
881
1046
  get: (name: string) -> string?,
1047
+ -- Requires the 'exec' permission.
882
1048
  set: (name: string, value: string) -> (),
883
1049
  home: () -> string?,
884
1050
  config: () -> string?,
@@ -900,6 +1066,8 @@ declare rb: {
900
1066
  -- Rebind Pro; require("@rebind/pro") returns rb
901
1067
  ai: RbAi,
902
1068
  phone: RbPhone,
1069
+ -- drawing on screen over every app, games included (Windows and macOS)
1070
+ overlay: RbOverlay,
903
1071
  }
904
1072
 
905
1073
  -- v1 names
@@ -965,7 +1133,6 @@ declare HID: {
965
1133
  -- rereads the cursor and corrects until within 2 px (about 150 ms at
966
1134
  -- most), waiting with Sleep in Run and blocking elsewhere. With ms it
967
1135
  -- glides there over ms on an eased path; a duration needs Run or Async.
968
- -- Not supported on Linux (no cursor source).
969
1136
  MoveTo: (x: number, y: number, ms: number?) -> (),
970
1137
  Scroll: (amount: number) -> (),
971
1138
  -- deprecated: the mode is stored but no longer changes MoveTo
@@ -977,8 +1144,13 @@ declare HID: {
977
1144
  -- set the whole joypad state (last write wins across scripts). Needs a
978
1145
  -- Rebind Link whose USB profile has a joypad; raises otherwise. Keyboard
979
1146
  -- and mouse calls raise the same way when the profile leaves them out.
1147
+ -- Down, Up, Press and Combo also press controller buttons ("PadA").
980
1148
  Joypad: (state: JoypadState?) -> (),
981
- -- back to neutral; the relay also does this when a script stops or errors
1149
+ -- hold one controller axis (LeftX..RightY -1..1, LT/RT 0..1) until set
1150
+ -- again or reset; it wins over a captured controller's value
1151
+ Axis: (name: string, value: number) -> (),
1152
+ -- drop what scripts set, keeping what a captured controller holds; the
1153
+ -- relay also does this when a script stops or errors
982
1154
  JoypadReset: () -> (),
983
1155
  }
984
1156
 
@@ -986,6 +1158,10 @@ declare HID: {
986
1158
 
987
1159
  declare Input: {
988
1160
  IsDown: (code: string) -> boolean,
1161
+ -- a controller axis by name, 0 at rest
1162
+ GetAxis: (name: string) -> number,
1163
+ -- the devices Rebind has identified (3.12.0); empty in software mode
1164
+ Devices: () -> { InputDevice },
989
1165
  GetDuration: (code: string) -> number,
990
1166
  GetActiveKeys: () -> { string },
991
1167
  GetMousePos: () -> MousePos,
@@ -1131,12 +1307,13 @@ declare Window: {
1131
1307
  }
1132
1308
 
1133
1309
  -- app namespace: application-level control (Keyboard Maestro's Application
1134
- -- Control). macOS: names match case-insensitively by app display name;
1135
- -- Front/IsRunning/Activate are permission-free; Hide/Quit/IsHidden drive an
1136
- -- AppleEvent and prompt for macOS Automation permission on first use.
1310
+ -- Control). Activate focuses a running app and never launches one; start an
1311
+ -- app with System.ExecDetached (permission: exec). macOS: names match
1312
+ -- case-insensitively by app display name; Activate needs Accessibility;
1313
+ -- Quit/IsHidden drive an AppleEvent and prompt for macOS Automation
1314
+ -- permission on first use; Quit leaves an app that is not running alone.
1137
1315
  -- Windows: names are executable names (case-insensitive, ".exe" optional);
1138
- -- Activate focuses a running app and does not launch one; Quit closes its
1139
- -- windows; Hide/IsHidden raise. Linux: every call raises.
1316
+ -- Quit closes its windows; Hide/IsHidden raise.
1140
1317
 
1141
1318
  declare App: {
1142
1319
  Front: () -> AppInfo?,
@@ -1159,7 +1336,7 @@ declare Net: {
1159
1336
  Head: (url: string, headers: { [string]: string }?, options: HttpOptions?) -> HttpResponse,
1160
1337
  Request: (options: HttpRequestOptions) -> HttpResponse,
1161
1338
  Listen: (port: number, handler: (request: HttpRequest) -> HttpResponseSpec | string) -> ServerHandle,
1162
- WSListen: (port: number, handlers: WsListenHandlers) -> WsServerHandle,
1339
+ WSListen: (port: number, handlers: WsListenHandlers, options: { host: string? }?) -> WsServerHandle,
1163
1340
  WSConnect: (url: string, handlers: WsConnectHandlers) -> WsConnectHandle,
1164
1341
  -- bind a UDP socket; handler receives each datagram's raw bytes and sender.
1165
1342
  UDPListen: (bindIp: string, port: number, handler: (data: string, ip: string, port: number) -> ()) -> UdpHandle,
@@ -1167,7 +1344,7 @@ declare Net: {
1167
1344
  LocalIP: () -> string?,
1168
1345
  }
1169
1346
 
1170
- -- pipe namespace
1347
+ -- pipe namespace (permission: exec)
1171
1348
 
1172
1349
  declare Pipe: {
1173
1350
  Open: (name: string, options: { size: number? }?) -> PipeHandle,
@@ -1186,7 +1363,7 @@ declare File: {
1186
1363
  -- script dir (the File.* jail); absolute paths are rejected.
1187
1364
  List: (path: string?) -> { string },
1188
1365
  MkDir: (path: string) -> boolean,
1189
- -- recursive; returns true even when the directory did not exist.
1366
+ -- recursive; raises when the directory does not exist or cannot be removed.
1190
1367
  RmDir: (path: string) -> boolean,
1191
1368
  Copy: (src: string, dst: string) -> (),
1192
1369
  Move: (src: string, dst: string) -> (),
@@ -1279,6 +1456,7 @@ declare Codec: {
1279
1456
 
1280
1457
  declare Env: {
1281
1458
  Get: (name: string) -> string?,
1459
+ -- Requires the 'exec' permission.
1282
1460
  Set: (name: string, value: string) -> (),
1283
1461
  Home: () -> string?,
1284
1462
  Config: () -> string?,
@@ -1313,6 +1491,7 @@ declare Registry: {
1313
1491
  -- declare function OnMove(dx: number, dy: number, device: InputDevice?): boolean? -- return value only used with mouse_block=true (hardware mode only)
1314
1492
  -- declare function OnScroll(delta: number, device: InputDevice?): boolean?
1315
1493
  -- declare function OnScrollH(delta: number): boolean?
1494
+ -- declare function OnAxis(axis: string, value: number, device: InputDevice?): boolean? -- hardware mode only
1316
1495
  -- declare function OnInputDown(key: string, device: InputDevice?): ()
1317
1496
  -- declare function OnInputUp(key: string, durationMs: number, device: InputDevice?): ()
1318
1497
  -- declare function OnInputMove(dx: number, dy: number, device: InputDevice?): ()