@rebind.gg/mcp-server 0.3.0 → 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 +2 -2
- package/src/fetch-guard.ts +55 -0
- package/src/index.ts +34 -70
- package/src/rebind.d.luau +217 -25
- package/src/session.ts +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebind.gg/mcp-server",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
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
|
|
653
|
-
// mapping on the session so a subsequent
|
|
654
|
-
//
|
|
655
|
-
const captureScreen = async ()
|
|
656
|
-
|
|
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
|
|
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 -
|
|
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
|
-
|
|
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
|
|
1653
|
-
//
|
|
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
|
-
{
|
|
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
|
|
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"
|
|
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"
|
|
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,
|
|
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
|
-
--
|
|
724
|
+
-- delivered as input only in hardware mode:
|
|
581
725
|
-- function: F13-F24
|
|
582
|
-
-- media: MediaNext, MediaPrev, MediaStop, MediaPlay, VolumeUp, VolumeDown,
|
|
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
|
|
|
@@ -647,21 +801,36 @@ declare rb: {
|
|
|
647
801
|
type: (text: string) -> (),
|
|
648
802
|
typewriter: (text: string, delay_ms: number?) -> (),
|
|
649
803
|
move: (dx: number, dy: number) -> (),
|
|
650
|
-
|
|
804
|
+
-- exact in Windows and macOS software mode; on a Rebind Link it moves,
|
|
805
|
+
-- rereads the cursor and corrects until within 2 px (about 150 ms at
|
|
806
|
+
-- most), waiting with sleep in rb.run and blocking elsewhere. With ms
|
|
807
|
+
-- it glides there over ms on an eased path; a duration needs rb.run or
|
|
808
|
+
-- rb.async.
|
|
809
|
+
move_to: (x: number, y: number, ms: number?) -> (),
|
|
651
810
|
scroll: (amount: number) -> (),
|
|
811
|
+
-- deprecated: the mode is stored but no longer changes move_to
|
|
652
812
|
set_mouse_mode: (mode: string) -> (),
|
|
653
813
|
get_mouse_mode: () -> string,
|
|
654
814
|
-- keyboard HID usage for a key name, or nil (media keys return nil). down,
|
|
655
815
|
-- up, press and combo also accept "usage:N" (decimal or 0x hex, 0x04-0xE7).
|
|
656
816
|
usage: (name: string) -> number?,
|
|
657
817
|
-- sets the whole joypad state (last write wins across scripts). Needs a
|
|
658
|
-
-- 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").
|
|
659
820
|
joypad: (state: JoypadState?) -> (),
|
|
660
|
-
--
|
|
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
|
|
661
826
|
joypad_reset: () -> (),
|
|
662
827
|
},
|
|
663
828
|
input: {
|
|
664
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 },
|
|
665
834
|
duration: (code: string) -> number,
|
|
666
835
|
keys: () -> { string },
|
|
667
836
|
mouse_pos: () -> MousePos,
|
|
@@ -763,8 +932,9 @@ declare rb: {
|
|
|
763
932
|
wait_close: (title: string, timeout: number?) -> boolean,
|
|
764
933
|
},
|
|
765
934
|
-- application control. macOS matches app display names ignoring case;
|
|
766
|
-
-- Windows matches executable names (".exe" optional) and cannot hide
|
|
767
|
-
--
|
|
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).
|
|
768
938
|
app: {
|
|
769
939
|
front: () -> AppInfo?,
|
|
770
940
|
is_front: (name: string) -> boolean,
|
|
@@ -784,13 +954,15 @@ declare rb: {
|
|
|
784
954
|
head: (url: string, options: RbHttpOptions?) -> HttpResponse,
|
|
785
955
|
request: (options: HttpRequestOptions) -> HttpResponse,
|
|
786
956
|
listen: (port: number, handler: (request: HttpRequest) -> HttpResponseSpec | string) -> ServerHandle,
|
|
787
|
-
|
|
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,
|
|
788
959
|
ws_connect: (url: string, handlers: WsConnectHandlers) -> WsConnectHandle,
|
|
789
960
|
-- binds a UDP socket; handler receives each datagram's raw bytes and sender.
|
|
790
961
|
udp_listen: (bind_ip: string, port: number, handler: (data: string, ip: string, port: number) -> ()) -> UdpHandle,
|
|
791
962
|
-- this computer's IPv4 address on its default route, or nil when there is none.
|
|
792
963
|
local_ip: () -> string?,
|
|
793
964
|
},
|
|
965
|
+
-- permission: exec
|
|
794
966
|
pipe: {
|
|
795
967
|
open: (name: string, options: { size: number? }?) -> PipeHandle,
|
|
796
968
|
},
|
|
@@ -805,7 +977,7 @@ declare rb: {
|
|
|
805
977
|
-- nil path lists the script's own directory
|
|
806
978
|
list: (path: string?) -> { string },
|
|
807
979
|
mkdir: (path: string) -> boolean,
|
|
808
|
-
-- recursive;
|
|
980
|
+
-- recursive; raises when the directory does not exist or cannot be removed.
|
|
809
981
|
rmdir: (path: string) -> boolean,
|
|
810
982
|
copy: (src: string, dst: string) -> (),
|
|
811
983
|
move: (src: string, dst: string) -> (),
|
|
@@ -872,6 +1044,7 @@ declare rb: {
|
|
|
872
1044
|
},
|
|
873
1045
|
env: {
|
|
874
1046
|
get: (name: string) -> string?,
|
|
1047
|
+
-- Requires the 'exec' permission.
|
|
875
1048
|
set: (name: string, value: string) -> (),
|
|
876
1049
|
home: () -> string?,
|
|
877
1050
|
config: () -> string?,
|
|
@@ -893,6 +1066,8 @@ declare rb: {
|
|
|
893
1066
|
-- Rebind Pro; require("@rebind/pro") returns rb
|
|
894
1067
|
ai: RbAi,
|
|
895
1068
|
phone: RbPhone,
|
|
1069
|
+
-- drawing on screen over every app, games included (Windows and macOS)
|
|
1070
|
+
overlay: RbOverlay,
|
|
896
1071
|
}
|
|
897
1072
|
|
|
898
1073
|
-- v1 names
|
|
@@ -954,8 +1129,13 @@ declare HID: {
|
|
|
954
1129
|
Type: (text: string) -> (),
|
|
955
1130
|
Typewriter: (text: string, delayMs: number?) -> (),
|
|
956
1131
|
Move: (dx: number, dy: number) -> (),
|
|
957
|
-
|
|
1132
|
+
-- exact in Windows and macOS software mode; on a Rebind Link it moves,
|
|
1133
|
+
-- rereads the cursor and corrects until within 2 px (about 150 ms at
|
|
1134
|
+
-- most), waiting with Sleep in Run and blocking elsewhere. With ms it
|
|
1135
|
+
-- glides there over ms on an eased path; a duration needs Run or Async.
|
|
1136
|
+
MoveTo: (x: number, y: number, ms: number?) -> (),
|
|
958
1137
|
Scroll: (amount: number) -> (),
|
|
1138
|
+
-- deprecated: the mode is stored but no longer changes MoveTo
|
|
959
1139
|
SetMouseMode: (mode: string) -> (),
|
|
960
1140
|
GetMouseMode: () -> string,
|
|
961
1141
|
-- keyboard HID usage for a key name, or nil (media keys return nil). Down,
|
|
@@ -964,8 +1144,13 @@ declare HID: {
|
|
|
964
1144
|
-- set the whole joypad state (last write wins across scripts). Needs a
|
|
965
1145
|
-- Rebind Link whose USB profile has a joypad; raises otherwise. Keyboard
|
|
966
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").
|
|
967
1148
|
Joypad: (state: JoypadState?) -> (),
|
|
968
|
-
--
|
|
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
|
|
969
1154
|
JoypadReset: () -> (),
|
|
970
1155
|
}
|
|
971
1156
|
|
|
@@ -973,6 +1158,10 @@ declare HID: {
|
|
|
973
1158
|
|
|
974
1159
|
declare Input: {
|
|
975
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 },
|
|
976
1165
|
GetDuration: (code: string) -> number,
|
|
977
1166
|
GetActiveKeys: () -> { string },
|
|
978
1167
|
GetMousePos: () -> MousePos,
|
|
@@ -1118,12 +1307,13 @@ declare Window: {
|
|
|
1118
1307
|
}
|
|
1119
1308
|
|
|
1120
1309
|
-- app namespace: application-level control (Keyboard Maestro's Application
|
|
1121
|
-
-- Control).
|
|
1122
|
-
--
|
|
1123
|
-
--
|
|
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.
|
|
1124
1315
|
-- Windows: names are executable names (case-insensitive, ".exe" optional);
|
|
1125
|
-
--
|
|
1126
|
-
-- windows; Hide/IsHidden raise. Linux: every call raises.
|
|
1316
|
+
-- Quit closes its windows; Hide/IsHidden raise.
|
|
1127
1317
|
|
|
1128
1318
|
declare App: {
|
|
1129
1319
|
Front: () -> AppInfo?,
|
|
@@ -1146,7 +1336,7 @@ declare Net: {
|
|
|
1146
1336
|
Head: (url: string, headers: { [string]: string }?, options: HttpOptions?) -> HttpResponse,
|
|
1147
1337
|
Request: (options: HttpRequestOptions) -> HttpResponse,
|
|
1148
1338
|
Listen: (port: number, handler: (request: HttpRequest) -> HttpResponseSpec | string) -> ServerHandle,
|
|
1149
|
-
WSListen: (port: number, handlers: WsListenHandlers) -> WsServerHandle,
|
|
1339
|
+
WSListen: (port: number, handlers: WsListenHandlers, options: { host: string? }?) -> WsServerHandle,
|
|
1150
1340
|
WSConnect: (url: string, handlers: WsConnectHandlers) -> WsConnectHandle,
|
|
1151
1341
|
-- bind a UDP socket; handler receives each datagram's raw bytes and sender.
|
|
1152
1342
|
UDPListen: (bindIp: string, port: number, handler: (data: string, ip: string, port: number) -> ()) -> UdpHandle,
|
|
@@ -1154,7 +1344,7 @@ declare Net: {
|
|
|
1154
1344
|
LocalIP: () -> string?,
|
|
1155
1345
|
}
|
|
1156
1346
|
|
|
1157
|
-
-- pipe namespace
|
|
1347
|
+
-- pipe namespace (permission: exec)
|
|
1158
1348
|
|
|
1159
1349
|
declare Pipe: {
|
|
1160
1350
|
Open: (name: string, options: { size: number? }?) -> PipeHandle,
|
|
@@ -1173,7 +1363,7 @@ declare File: {
|
|
|
1173
1363
|
-- script dir (the File.* jail); absolute paths are rejected.
|
|
1174
1364
|
List: (path: string?) -> { string },
|
|
1175
1365
|
MkDir: (path: string) -> boolean,
|
|
1176
|
-
-- recursive;
|
|
1366
|
+
-- recursive; raises when the directory does not exist or cannot be removed.
|
|
1177
1367
|
RmDir: (path: string) -> boolean,
|
|
1178
1368
|
Copy: (src: string, dst: string) -> (),
|
|
1179
1369
|
Move: (src: string, dst: string) -> (),
|
|
@@ -1266,6 +1456,7 @@ declare Codec: {
|
|
|
1266
1456
|
|
|
1267
1457
|
declare Env: {
|
|
1268
1458
|
Get: (name: string) -> string?,
|
|
1459
|
+
-- Requires the 'exec' permission.
|
|
1269
1460
|
Set: (name: string, value: string) -> (),
|
|
1270
1461
|
Home: () -> string?,
|
|
1271
1462
|
Config: () -> string?,
|
|
@@ -1300,6 +1491,7 @@ declare Registry: {
|
|
|
1300
1491
|
-- declare function OnMove(dx: number, dy: number, device: InputDevice?): boolean? -- return value only used with mouse_block=true (hardware mode only)
|
|
1301
1492
|
-- declare function OnScroll(delta: number, device: InputDevice?): boolean?
|
|
1302
1493
|
-- declare function OnScrollH(delta: number): boolean?
|
|
1494
|
+
-- declare function OnAxis(axis: string, value: number, device: InputDevice?): boolean? -- hardware mode only
|
|
1303
1495
|
-- declare function OnInputDown(key: string, device: InputDevice?): ()
|
|
1304
1496
|
-- declare function OnInputUp(key: string, durationMs: number, device: InputDevice?): ()
|
|
1305
1497
|
-- declare function OnInputMove(dx: number, dy: number, device: InputDevice?): ()
|
package/src/session.ts
CHANGED
|
@@ -87,12 +87,12 @@ export class Session {
|
|
|
87
87
|
/**
|
|
88
88
|
* Move the OS cursor to absolute screen (x, y).
|
|
89
89
|
*
|
|
90
|
-
*
|
|
91
|
-
* cursor
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
90
|
+
* Relays before 3.10.1 derived hid.move_to's move from a stale
|
|
91
|
+
* cached cursor and could fling the pointer to a screen edge (observed:
|
|
92
|
+
* target (800,500) landing at (0,0)), so this reads the REAL cursor, glides
|
|
93
|
+
* the residual in small relative steps (human-like, not an instant jump),
|
|
94
|
+
* then reads back and corrects against the true cursor. Converges to the
|
|
95
|
+
* exact pixel, usually in a single pass.
|
|
96
96
|
*/
|
|
97
97
|
async moveTo(x: number, y: number): Promise<Point & { passes: number }> {
|
|
98
98
|
await this.ensureCalibrated();
|