@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 +2 -2
- package/src/fetch-guard.ts +55 -0
- package/src/index.ts +34 -70
- package/src/rebind.d.luau +204 -25
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
|
|
|
@@ -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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
|
|
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;
|
|
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
|
-
--
|
|
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).
|
|
1135
|
-
--
|
|
1136
|
-
--
|
|
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
|
-
--
|
|
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;
|
|
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?): ()
|