@rebind.gg/mcp-server 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/package.json +3 -3
- package/src/fetch-guard.ts +55 -0
- package/src/index.ts +222 -70
- package/src/observe.ts +19 -0
- package/src/prompts.ts +5 -1
- package/src/rebind.d.luau +376 -29
- package/src/keys.test.ts +0 -15
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Stdio MCP server that turns [Rebind](https://rebind.gg) into a computer-use tool
|
|
|
7
7
|
- [Bun](https://bun.sh) >= 1.1 (the server runs via `bunx`)
|
|
8
8
|
- The [Rebind app](https://rebind.gg/download) running the free [Remote Control](https://www.rebind.gg/marketplace/script_AAHteeDcYPGOT7GfXZ_OK) package (`rebind install @rebind/remote-control --allow`)
|
|
9
9
|
|
|
10
|
-
The first time the server sends a command, Remote Control asks on that computer whether to allow **Rebind MCP server**. Allow it once; the name is remembered. Tool calls fail with `awaiting_approval` until you answer. Remote Control accepts connections from its own computer only unless **Local only** is turned off in its Options, which an agent on another computer or inside WSL needs. `run_lua`, `focus_window`, `position_window` and `read_page` also need **Allow custom Luau** turned on there; the server reads that setting at startup.
|
|
10
|
+
The first time the server sends a command, Remote Control asks on that computer whether to allow **Rebind MCP server**. Allow it once; the name is remembered. Tool calls fail with `awaiting_approval` until you answer. Remote Control accepts connections from its own computer only unless **Local only** is turned off in its Options, which an agent on another computer or inside WSL needs. `run_lua`, `focus_window`, `position_window` and `read_page` also need **Allow custom Luau** turned on there; the server reads that setting at startup. The `observe_*` tools need **Allow observation** turned on there, Remote Control 0.5.0 and Rebind 3.15.0.
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
@@ -61,6 +61,11 @@ Connects, waits up to a minute for the approval prompt on a first run, takes a c
|
|
|
61
61
|
| `meditate` | Research a problem page until a reasoning model has the data to solve it: fetches the URL directly, pulls the script bundles behind a JS shell so its API endpoints are visible, fetches those endpoints and assets, and reads the live page with vision. The browser steps (rendering a JS shell, same-origin fetches through the page, and the vision pass) need a macOS relay; elsewhere it reads pages over HTTP only. Returns a markdown dossier plus JSON `{error, problem, requirements, plan, submittable, form, live_captcha_hint, confidence, sources, assets, pending_fetch, notes, cost_usd}`. `error` is non-null when no problem could be identified or the page could not be read; partial data is still returned. Gated behind `REBIND_ALLOW_DELEGATION=1` (spends OpenRouter credits). See the pipeline below. |
|
|
62
62
|
| `run_lua` | Run a Luau script on the connected machine and return its `return` value as JSON — the fast path for any deterministic sequence (launch apps, place windows, shell commands, pixel search). |
|
|
63
63
|
| `lua_docs` | Full Luau SDK type definitions for `run_lua`. |
|
|
64
|
+
| `observe_start` / `observe_stop` | Start and stop a recording of the user: every key and button they press (passwords included), mouse movement, the active window, and screenshots, saved in Rebind's observations folder. The server's instructions tell the agent to ask before starting one. It also stops when the server disconnects. |
|
|
65
|
+
| `observe_mark` | Add a note (`mark`, `success`, `failure`, `ai` or `own`, with a label and JSON data) to the recording the server started. |
|
|
66
|
+
| `observe_list` | Every recording in the observations folder, newest first, including ones made in the app or with `rebind observe`. |
|
|
67
|
+
| `observe_events` | A recording's log lines from `from_seq`, 64 KB at a time unless `max_bytes` says otherwise, and `next`, the seq to read on from. |
|
|
68
|
+
| `observe_frame` | One screenshot from a recording, as a `frame` or `crop` line names it, returned as an image. |
|
|
64
69
|
| `list_displays` / `list_windows` / `focus_window` / `position_window` / `active_window` | Display and window enumeration, focus, and placement. |
|
|
65
70
|
| `get_cursor` / `wait` / `calibrate` | Cursor readback, pauses, and the mouse scale probe. |
|
|
66
71
|
| `delegate_task` / `rebind_versus` | Server-side reasoning helpers with no desktop access: one bounded text task, or parallel critics that attack a plan. Registered only with `REBIND_ALLOW_DELEGATION=1` (spends OpenRouter credits). |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebind.gg/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"scripts": {
|
|
42
42
|
"start": "bun run src/index.ts",
|
|
43
43
|
"selftest": "bun run src/index.ts --selftest",
|
|
44
|
-
"test": "bun test
|
|
44
|
+
"test": "bun test tests/",
|
|
45
45
|
"typecheck": "tsc --noEmit",
|
|
46
46
|
"bench": "bun run bench/runner.ts",
|
|
47
47
|
"bench:selftest": "bun run bench/runner.ts --selftest",
|
|
@@ -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,9 +14,11 @@ 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";
|
|
21
|
+
import { observeRefusal } from "./observe.ts";
|
|
20
22
|
import {
|
|
21
23
|
BATCHING_DOCTRINE,
|
|
22
24
|
DETERMINISTIC_WORK_DOCTRINE,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
KEY_DESC,
|
|
25
27
|
LUA_DOCS_DESC,
|
|
26
28
|
modifierNote,
|
|
29
|
+
OBSERVE_DOCTRINE,
|
|
27
30
|
type OS,
|
|
28
31
|
openAppHow,
|
|
29
32
|
PROBE_CLIS_CMD,
|
|
@@ -114,40 +117,11 @@ const MEDITATE_TOTAL_CHARS = 600_000;
|
|
|
114
117
|
const MEDITATE_DL_MAX_BYTES = 26_214_400; // 25 MiB
|
|
115
118
|
const MEDITATE_DL_TIMEOUT_S = 45;
|
|
116
119
|
|
|
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
120
|
/** Front app is a browser we can drive (Safari included — it fails later at the
|
|
122
121
|
* javascript: step, but it is still a browser, not a terminal we'd type into). */
|
|
123
122
|
const BROWSER_RE =
|
|
124
123
|
/chrome|chromium|brave|firefox|edge|opera|vivaldi|arc|safari/i;
|
|
125
124
|
|
|
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
125
|
/** per-asset inlined-content budget fed to the model (text/csv sources) */
|
|
152
126
|
const MEDITATE_ASSET_CHARS = 60_000;
|
|
153
127
|
/** a fetched page shorter than this is a JS shell, not content — go to the browser */
|
|
@@ -338,6 +312,7 @@ async function buildInstructions(
|
|
|
338
312
|
BATCHING_DOCTRINE,
|
|
339
313
|
"When you do need to see the screen, prefer screenshot_window over a full-display screenshot — a window shot costs roughly a quarter of the image tokens. Every screenshot stays in your context for the rest of the session, so each one you take keeps costing you on every later turn: verify with values (clipboard, file contents, run_lua reads) where possible.",
|
|
340
314
|
guiTips(os),
|
|
315
|
+
OBSERVE_DOCTRINE,
|
|
341
316
|
"run_lua chunks execute once and must end with `return <value>`. File.* paths are relative to the script dir (absolute rejected); System.Exec takes real absolute paths.",
|
|
342
317
|
`Full run_lua Luau SDK — every namespace and signature (do not guess APIs):\n${sdk}`,
|
|
343
318
|
]
|
|
@@ -530,6 +505,7 @@ async function main() {
|
|
|
530
505
|
// an app finishing launch) and the payload would be typed into it instead.
|
|
531
506
|
if (!BROWSER_RE.test(await frontApp())) {
|
|
532
507
|
if (!appName) return null;
|
|
508
|
+
// App.Activate focuses a running browser only; a closed one stays closed
|
|
533
509
|
await client.luaExec(
|
|
534
510
|
`App.Activate(${JSON.stringify(appName)}) return true`,
|
|
535
511
|
);
|
|
@@ -649,15 +625,11 @@ async function main() {
|
|
|
649
625
|
await sleep(3000);
|
|
650
626
|
};
|
|
651
627
|
|
|
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({});
|
|
628
|
+
// Capture a display (default: the one under the cursor), render it, and record
|
|
629
|
+
// the image->screen mapping on the session so a subsequent
|
|
630
|
+
// verifiedClick(mapImagePoint(...)) lands correctly.
|
|
631
|
+
const captureScreen = async (display?: number) => {
|
|
632
|
+
const cap = await client.screenCapture({ display });
|
|
661
633
|
const shot = await renderScreenshot({
|
|
662
634
|
png: Buffer.from(cap.image, "base64"),
|
|
663
635
|
logicalWidth: cap.display.width,
|
|
@@ -675,11 +647,7 @@ async function main() {
|
|
|
675
647
|
imageWidth: shot.width,
|
|
676
648
|
imageHeight: shot.height,
|
|
677
649
|
};
|
|
678
|
-
return {
|
|
679
|
-
dataUrl: `data:image/webp;base64,${shot.data.toString("base64")}`,
|
|
680
|
-
width: shot.width,
|
|
681
|
-
height: shot.height,
|
|
682
|
-
};
|
|
650
|
+
return { cap, shot };
|
|
683
651
|
};
|
|
684
652
|
|
|
685
653
|
// Resolve a window (title substring, or the front window when want=""),
|
|
@@ -737,24 +705,7 @@ return { ok = true, proc = proc }
|
|
|
737
705
|
},
|
|
738
706
|
},
|
|
739
707
|
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
|
-
};
|
|
708
|
+
const { cap, shot } = await captureScreen(display);
|
|
758
709
|
const win = await client.systemWindow().catch(() => null);
|
|
759
710
|
const lines = [
|
|
760
711
|
`display ${cap.display.index}: ${cap.display.width}x${cap.display.height} at (${cap.display.x}, ${cap.display.y})${cap.display.primary ? " (primary)" : ""}`,
|
|
@@ -1220,6 +1171,191 @@ return { ok = Window.IsActive(h), name = name }
|
|
|
1220
1171
|
async () => text(await v1Sdk()),
|
|
1221
1172
|
);
|
|
1222
1173
|
|
|
1174
|
+
// --- observation: rb.observe in Remote Control (observe.* commands) ---
|
|
1175
|
+
// A refusal the user fixes (Allow observation off, an older Remote Control
|
|
1176
|
+
// or Rebind) comes back as a plain error result; anything else throws.
|
|
1177
|
+
const observe = async (
|
|
1178
|
+
command: string,
|
|
1179
|
+
args: Record<string, unknown>,
|
|
1180
|
+
answer: (r: any) => any,
|
|
1181
|
+
) => {
|
|
1182
|
+
try {
|
|
1183
|
+
return await answer(await client.call(`observe.${command}`, args));
|
|
1184
|
+
} catch (e) {
|
|
1185
|
+
const refusal = observeRefusal(e);
|
|
1186
|
+
if (refusal === null) throw e;
|
|
1187
|
+
return { ...text(refusal), isError: true };
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
const json = (v: unknown) => text(JSON.stringify(v, null, 2));
|
|
1191
|
+
|
|
1192
|
+
register(
|
|
1193
|
+
"observe_start",
|
|
1194
|
+
{
|
|
1195
|
+
description:
|
|
1196
|
+
"Start recording the USER: every key and button they press, mouse movement, the active window, and screenshots, saved in Rebind's observations folder. " +
|
|
1197
|
+
"Ask the user first and start only after they agree. One recording runs at a time. It ends after `seconds`, at `stop_key`, on observe_stop, or when this server disconnects. " +
|
|
1198
|
+
"Needs Allow observation on in Remote Control's Options. Returns the recording's status, including its id.",
|
|
1199
|
+
inputSchema: {
|
|
1200
|
+
seconds: z
|
|
1201
|
+
.number()
|
|
1202
|
+
.int()
|
|
1203
|
+
.min(1)
|
|
1204
|
+
.max(14400)
|
|
1205
|
+
.optional()
|
|
1206
|
+
.describe("how long to record (default 120)"),
|
|
1207
|
+
stop_key: z
|
|
1208
|
+
.string()
|
|
1209
|
+
.optional()
|
|
1210
|
+
.describe(
|
|
1211
|
+
'a keyboard key that ends the recording early, such as "F8"',
|
|
1212
|
+
),
|
|
1213
|
+
goal: z
|
|
1214
|
+
.string()
|
|
1215
|
+
.max(500)
|
|
1216
|
+
.optional()
|
|
1217
|
+
.describe("what the recording is for, saved with it"),
|
|
1218
|
+
frame_ms: z
|
|
1219
|
+
.number()
|
|
1220
|
+
.int()
|
|
1221
|
+
.min(0)
|
|
1222
|
+
.max(30000)
|
|
1223
|
+
.optional()
|
|
1224
|
+
.describe(
|
|
1225
|
+
"ms between screenshots while nothing happens (default 1000; 0 turns them off)",
|
|
1226
|
+
),
|
|
1227
|
+
frame_edge: z
|
|
1228
|
+
.number()
|
|
1229
|
+
.int()
|
|
1230
|
+
.min(256)
|
|
1231
|
+
.max(2048)
|
|
1232
|
+
.optional()
|
|
1233
|
+
.describe("long edge of the screenshots in pixels (default 1024)"),
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
async (args) => observe("start", args, (r) => json(r.session)),
|
|
1237
|
+
);
|
|
1238
|
+
|
|
1239
|
+
register(
|
|
1240
|
+
"observe_mark",
|
|
1241
|
+
{
|
|
1242
|
+
description:
|
|
1243
|
+
"Add a note to the recording this server started, among the inputs around it: a moment worth finding later, a success, or a failure. Returns whether it was queued (false when no recording of this server's is running).",
|
|
1244
|
+
inputSchema: {
|
|
1245
|
+
kind: z
|
|
1246
|
+
.enum(["mark", "success", "failure", "ai", "own"])
|
|
1247
|
+
.default("mark"),
|
|
1248
|
+
label: z.string().max(200).optional(),
|
|
1249
|
+
data: z.unknown().optional().describe("any JSON value, up to 8 KB"),
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
async (args) =>
|
|
1253
|
+
observe("mark", args, (r) =>
|
|
1254
|
+
text(
|
|
1255
|
+
r.queued
|
|
1256
|
+
? "marked"
|
|
1257
|
+
: "not marked: no recording of this server's is running",
|
|
1258
|
+
),
|
|
1259
|
+
),
|
|
1260
|
+
);
|
|
1261
|
+
|
|
1262
|
+
register(
|
|
1263
|
+
"observe_stop",
|
|
1264
|
+
{
|
|
1265
|
+
description:
|
|
1266
|
+
"Stop the recording this server started. Returns its final counts and the reason it ended.",
|
|
1267
|
+
},
|
|
1268
|
+
async () => observe("stop", {}, (r) => json(r.session)),
|
|
1269
|
+
);
|
|
1270
|
+
|
|
1271
|
+
register(
|
|
1272
|
+
"observe_list",
|
|
1273
|
+
{
|
|
1274
|
+
description:
|
|
1275
|
+
"List every recording in Rebind's observations folder, newest first, including ones the user made in the app or with `rebind observe`: id, name, goal, owner, times, and counts.",
|
|
1276
|
+
},
|
|
1277
|
+
async () => observe("list", {}, (r) => json(r.recordings)),
|
|
1278
|
+
);
|
|
1279
|
+
|
|
1280
|
+
register(
|
|
1281
|
+
"observe_events",
|
|
1282
|
+
{
|
|
1283
|
+
description:
|
|
1284
|
+
"Read a recording's log lines (events.jsonl) in seq order: key and button presses with hold times, mouse movement, scrolls, window changes, frame and crop lines naming screenshots, and marks. " +
|
|
1285
|
+
"Returns `lines` and `next`, the seq to pass as from_seq to read on.",
|
|
1286
|
+
inputSchema: {
|
|
1287
|
+
id: z
|
|
1288
|
+
.string()
|
|
1289
|
+
.describe("the recording id, from observe_list or observe_start"),
|
|
1290
|
+
from_seq: z
|
|
1291
|
+
.number()
|
|
1292
|
+
.int()
|
|
1293
|
+
.min(0)
|
|
1294
|
+
.optional()
|
|
1295
|
+
.describe("first seq (default 0)"),
|
|
1296
|
+
to_seq: z.number().int().min(0).optional().describe("last seq"),
|
|
1297
|
+
max_bytes: z
|
|
1298
|
+
.number()
|
|
1299
|
+
.int()
|
|
1300
|
+
.min(1)
|
|
1301
|
+
.max(4194304)
|
|
1302
|
+
.default(65536)
|
|
1303
|
+
.describe(
|
|
1304
|
+
"stop before this many bytes of lines; at least one line is returned",
|
|
1305
|
+
),
|
|
1306
|
+
},
|
|
1307
|
+
},
|
|
1308
|
+
async ({ id, ...rest }) =>
|
|
1309
|
+
observe("events", { session: id, ...rest }, (r) =>
|
|
1310
|
+
json({ lines: r.lines, next: r.next }),
|
|
1311
|
+
),
|
|
1312
|
+
);
|
|
1313
|
+
|
|
1314
|
+
register(
|
|
1315
|
+
"observe_frame",
|
|
1316
|
+
{
|
|
1317
|
+
description:
|
|
1318
|
+
"Show one screenshot from a recording, as a frame or crop line names it (frames/NNNNNN.png, frames/NNNNNN.win.png or crops/NNNNNN.png). " +
|
|
1319
|
+
"It shows the screen as it was then: do not click by its coordinates.",
|
|
1320
|
+
inputSchema: {
|
|
1321
|
+
id: z.string().describe("the recording id"),
|
|
1322
|
+
file: z.string().describe("the `file` of a frame or crop line"),
|
|
1323
|
+
max_edge: z
|
|
1324
|
+
.number()
|
|
1325
|
+
.int()
|
|
1326
|
+
.min(1)
|
|
1327
|
+
.default(1280)
|
|
1328
|
+
.describe("scale the image down so its long edge fits"),
|
|
1329
|
+
},
|
|
1330
|
+
},
|
|
1331
|
+
async ({ id, file, max_edge }) =>
|
|
1332
|
+
observe(
|
|
1333
|
+
"frame",
|
|
1334
|
+
{ session: id, file, max_edge },
|
|
1335
|
+
async (r: { data: string; width: number; height: number }) => {
|
|
1336
|
+
const shot = await renderScreenshot({
|
|
1337
|
+
png: Buffer.from(r.data, "base64"),
|
|
1338
|
+
logicalWidth: r.width,
|
|
1339
|
+
logicalHeight: r.height,
|
|
1340
|
+
maxEdge: max_edge,
|
|
1341
|
+
});
|
|
1342
|
+
return {
|
|
1343
|
+
content: [
|
|
1344
|
+
{
|
|
1345
|
+
type: "text" as const,
|
|
1346
|
+
text: `${file} of recording ${id}: ${shot.width}x${shot.height}`,
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
type: "image" as const,
|
|
1350
|
+
data: shot.data.toString("base64"),
|
|
1351
|
+
mimeType: "image/webp",
|
|
1352
|
+
},
|
|
1353
|
+
],
|
|
1354
|
+
};
|
|
1355
|
+
},
|
|
1356
|
+
),
|
|
1357
|
+
);
|
|
1358
|
+
|
|
1223
1359
|
// Server-side subagents. Gated: a stock install ships without them. They make
|
|
1224
1360
|
// OpenRouter calls (money) and touch no HID, so they register DIRECTLY on the
|
|
1225
1361
|
// server — NOT through `register()`, whose serial `chain` would let one
|
|
@@ -1446,7 +1582,8 @@ return { ok = Window.IsActive(h), name = name }
|
|
|
1446
1582
|
// Fetch a URL's text relay-side. This is the primary way content is
|
|
1447
1583
|
// gathered: a GET returns exactly what the server has, where driving the
|
|
1448
1584
|
// browser to the same URL costs HID, focus, and a fragile extraction.
|
|
1449
|
-
// Same SSRF guard and size/time limits as the download path
|
|
1585
|
+
// Same SSRF guard and size/time limits as the download path, and no -L:
|
|
1586
|
+
// a redirect target is a host the guard never checked.
|
|
1450
1587
|
const fetchText = async (
|
|
1451
1588
|
u: string,
|
|
1452
1589
|
maxChars: number,
|
|
@@ -1454,7 +1591,7 @@ return { ok = Window.IsActive(h), name = name }
|
|
|
1454
1591
|
if (!isSafeFetchUrl(u)) return null;
|
|
1455
1592
|
try {
|
|
1456
1593
|
const out = await client.luaExec(
|
|
1457
|
-
`local r = System.Exec([[curl -
|
|
1594
|
+
`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
1595
|
);
|
|
1459
1596
|
const s = String(out ?? "");
|
|
1460
1597
|
return s.trim() ? s.slice(0, maxChars) : null;
|
|
@@ -1567,7 +1704,12 @@ return { ok = Window.IsActive(h), name = name }
|
|
|
1567
1704
|
if (haveBrowser)
|
|
1568
1705
|
await client.luaExec(positionLua("", 0, 0, 1200, 900));
|
|
1569
1706
|
if (isShell) {
|
|
1570
|
-
|
|
1707
|
+
const d = haveBrowser ? await extractPage(browser, pageUrl) : null;
|
|
1708
|
+
if (d && isSafeFetchUrl(d.split("\n")[1] ?? "")) first = d;
|
|
1709
|
+
else if (d)
|
|
1710
|
+
note(
|
|
1711
|
+
"rendered page refused: the browser landed on a host the fetch guard refuses",
|
|
1712
|
+
);
|
|
1571
1713
|
if (!first) first = raw; // no browser — the shell is all we have
|
|
1572
1714
|
}
|
|
1573
1715
|
// PREPEND, never append: the per-page budget keeps only the first
|
|
@@ -1649,11 +1791,12 @@ return { ok = Window.IsActive(h), name = name }
|
|
|
1649
1791
|
.replace(/[^\w.-]/g, "_")
|
|
1650
1792
|
.slice(0, 80) || "asset";
|
|
1651
1793
|
const path = `${dlDir}/${assets.length}-${name}`;
|
|
1652
|
-
// isSafeFetchUrl bans quotes
|
|
1653
|
-
//
|
|
1794
|
+
// isSafeFetchUrl bans quotes, brackets and braces and private
|
|
1795
|
+
// hosts, and -g turns curl's URL globbing off, so this splice is
|
|
1796
|
+
// injection-safe; --proto and no -L block redirect-SSRF,
|
|
1654
1797
|
// --max-filesize/--max-time bound a hostile response.
|
|
1655
1798
|
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`,
|
|
1799
|
+
`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
1800
|
);
|
|
1658
1801
|
const ok = Number(exit) === 0;
|
|
1659
1802
|
// inline text/csv content so the model can READ the source, not
|
|
@@ -1729,7 +1872,11 @@ return { ok = Window.IsActive(h), name = name }
|
|
|
1729
1872
|
if (haveBrowser) {
|
|
1730
1873
|
await gotoUrl(pageUrl);
|
|
1731
1874
|
try {
|
|
1732
|
-
const shot = await captureScreen();
|
|
1875
|
+
const { shot } = await captureScreen();
|
|
1876
|
+
if (!isSafeFetchUrl(await tabUrl(browser || front)))
|
|
1877
|
+
throw new Error(
|
|
1878
|
+
"the browser landed on a URL the fetch guard refuses",
|
|
1879
|
+
);
|
|
1733
1880
|
const vstep = await llmStep(
|
|
1734
1881
|
[
|
|
1735
1882
|
{ role: "system", content: MEDITATE_VISION_SYS },
|
|
@@ -1737,7 +1884,12 @@ return { ok = Window.IsActive(h), name = name }
|
|
|
1737
1884
|
role: "user",
|
|
1738
1885
|
content: [
|
|
1739
1886
|
{ type: "text", text: "Read this page." },
|
|
1740
|
-
{
|
|
1887
|
+
{
|
|
1888
|
+
type: "image_url",
|
|
1889
|
+
image_url: {
|
|
1890
|
+
url: `data:image/webp;base64,${shot.data.toString("base64")}`,
|
|
1891
|
+
},
|
|
1892
|
+
},
|
|
1741
1893
|
],
|
|
1742
1894
|
},
|
|
1743
1895
|
],
|
|
@@ -1756,7 +1908,7 @@ return { ok = Window.IsActive(h), name = name }
|
|
|
1756
1908
|
}
|
|
1757
1909
|
} catch (e) {
|
|
1758
1910
|
note(
|
|
1759
|
-
`vision pass skipped
|
|
1911
|
+
`vision pass skipped: ${e instanceof Error ? e.message : e}`,
|
|
1760
1912
|
);
|
|
1761
1913
|
}
|
|
1762
1914
|
}
|
package/src/observe.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// the observe_* tools' plain answers when Remote Control refuses an observe.*
|
|
2
|
+
// command for a reason the user fixes, not the agent.
|
|
3
|
+
|
|
4
|
+
import { ServerError } from "@rebind.gg/client-ts";
|
|
5
|
+
|
|
6
|
+
/** the text to show for a refusal the user must fix, or null for any other error */
|
|
7
|
+
export function observeRefusal(e: unknown): string | null {
|
|
8
|
+
if (!(e instanceof ServerError)) return null;
|
|
9
|
+
switch (e.code) {
|
|
10
|
+
case "disabled":
|
|
11
|
+
return "Allow observation is off in Remote Control's Options on the computer running Rebind. Ask the user to turn it on if they want to be recorded; while it is on, a connected program can record every key they press and their screen.";
|
|
12
|
+
case "unknown_command":
|
|
13
|
+
return "This Remote Control has no observe commands: it is older than 0.5.0. Ask the user to update the Remote Control package.";
|
|
14
|
+
case "unavailable":
|
|
15
|
+
return "This Rebind cannot record observations: it is older than 3.15.0. Ask the user to update Rebind.";
|
|
16
|
+
default:
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/prompts.ts
CHANGED
|
@@ -73,6 +73,10 @@ export const DETERMINISTIC_WORK_DOCTRINE =
|
|
|
73
73
|
export const SAVE_WORK_DOCTRINE =
|
|
74
74
|
"When a sequence works and the user wants to keep it, save it as a Rebind script so it runs again with no agent and no tokens. On the computer running Rebind: `rebind init <dir>` scaffolds rebind.toml and main.luau; put the sequence in main.luau bound to a key (Bind or OnDown; hooks never fire inside run_lua, they do in an installed script); add `[runtime] permissions = [...]` to rebind.toml listing only what it uses; then run `rebind check <dir>`, `rebind pack <dir>` (writes <name>.rbp in the current directory), `rebind install <name>.rbp --allow` (it prints what the package asks for; add --force to replace an earlier install) and `rebind script start packages/<name>/main.luau`. Tell the user which key runs it, and that it runs until Rebind restarts and can be started again from Rebind's scripts list.";
|
|
75
75
|
|
|
76
|
+
/** MCP server only: the observe_* tools record the user, so they ask first. */
|
|
77
|
+
export const OBSERVE_DOCTRINE =
|
|
78
|
+
"observe_start records the user, not you: every key and button they press and release (passwords included, by name), mouse movement, the active window, and screenshots of their screen, saved in Rebind's observations folder until they delete it. Use it to watch the user do a task before you script it. Never start one on your own: ask first, saying what it records, for how long, and the stop key if you set one, and start only after the user agrees. Tell them when recording has started and not to type secrets. When they say they are done, call observe_stop, then read the recording with observe_events (log lines in seq order) and observe_frame (the screenshots its frame and crop lines name). observe_list shows every recording, including ones the user made in the app.";
|
|
79
|
+
|
|
76
80
|
// --- tool descriptions (shared only where the underlying behavior is identical
|
|
77
81
|
// across both surfaces: run_lua, lua_docs, and key all hit the same relay) ---
|
|
78
82
|
|
|
@@ -90,7 +94,7 @@ export const RUN_LUA_DESC =
|
|
|
90
94
|
'(Sleep, HID.Press, Dialog file pickers, Screen.Capture, Screen.FindImage, Screen.SearchForColor over 200k pixels) error — wait with System.Exec("sleep 0.5") instead, and take screenshots with the screenshot tool, not from a chunk. There is no Lua stdlib `io`/`os`/`require`. ' +
|
|
91
95
|
"Use it to launch apps (System.ExecDetached), place windows (Window.Wait / Window.Move — on macOS the window handle is advisory and Move targets the FRONTMOST window, so bring the app forward first), run shell commands " +
|
|
92
96
|
'(System.Exec(cmd) -> {exit,stdout,stderr}, absolute paths fine; inside a chunk it runs asynchronously on a background thread and does not block, so a slow command is safe), find UI by color in a small region (Screen.SearchForColor({x1,y1,x2,y2}, "rrggbb", tolerance), region under 200k pixels), type, click, read pixels (Screen.GetPixelColor), manage processes. ' +
|
|
93
|
-
"File.* (Read/Write/Append/ReadJSON/WriteJSON/Exists/Delete/List/MkDir/RmDir/Copy/Move/GetSize/GetTime/GetScriptDir) is jailed to the script dir: paths RELATIVE, absolute rejected. " +
|
|
97
|
+
"File.* (Read/Write/Append/ReadJSON/WriteJSON/Exists/Delete/List/MkDir/RmDir/Copy/Move/GetSize/GetTime/GetScriptDir/GetDataDir) is jailed to the script dir: paths RELATIVE, absolute rejected. Paths starting \"@data/\" reach the script's data folder (3.13.0), which a chunk does not have. " +
|
|
94
98
|
'A folder described as on the Desktop or in home is reachable at its BARE name from the jail root (e.g. File.List("myfolder")) — never prefix "Desktop/" or "~". ' +
|
|
95
99
|
"JSON is JSON.Parse(s) and JSON.Stringify(v) — there is no JSON.Decode/Encode; for JSON files File.ReadJSON/File.WriteJSON is one call. " +
|
|
96
100
|
"System.Exec commands run in an UNRELATED working directory — a relative shell path does NOT land in the File jail, so do file work with File.* (File.Write always works in the jail), and use absolute paths inside shell commands. " +
|
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
|
|
|
@@ -407,6 +407,9 @@ export type SliderOpts = WidgetOpts & {
|
|
|
407
407
|
suffix: string?,
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
-- a keybind setting holds a key name, or "<key>@<device id>" once the user
|
|
411
|
+
-- restricts it to the device that pressed it; Bind, Bind.Toggle, Bind.Remap
|
|
412
|
+
-- and rb.bind read the suffix as their device filter. A default is a plain key.
|
|
410
413
|
export type KeybindOpts = WidgetOpts
|
|
411
414
|
|
|
412
415
|
export type SelectOpts = WidgetOpts
|
|
@@ -419,6 +422,12 @@ export type TextOpts = WidgetOpts & {
|
|
|
419
422
|
|
|
420
423
|
export type ColorOpts = WidgetOpts
|
|
421
424
|
|
|
425
|
+
export type ListOpts = WidgetOpts & {
|
|
426
|
+
placeholder: string?,
|
|
427
|
+
maxLength: number?,
|
|
428
|
+
max_length: number?,
|
|
429
|
+
}
|
|
430
|
+
|
|
422
431
|
export type RecordOpts = {
|
|
423
432
|
ignore_mouse: boolean?,
|
|
424
433
|
ignore_keyboard: boolean?,
|
|
@@ -432,7 +441,7 @@ export type UiProxy = { [string]: any }
|
|
|
432
441
|
|
|
433
442
|
-- bind config for table-style binding
|
|
434
443
|
export type BindConfig = {
|
|
435
|
-
device: string?, -- nickname or id: the bind answers only to that device
|
|
444
|
+
device: string?, -- nickname or id: the bind answers only to that device; replaces a pin in the key
|
|
436
445
|
when: ((device: InputDevice?) -> boolean)?,
|
|
437
446
|
action: ((device: InputDevice?) -> boolean?)?,
|
|
438
447
|
release: ((device: InputDevice?) -> ())?,
|
|
@@ -445,9 +454,8 @@ export type RebindInfo = {
|
|
|
445
454
|
version: string,
|
|
446
455
|
name: string,
|
|
447
456
|
runtime_uuid: string,
|
|
448
|
-
id: string?,
|
|
449
457
|
instance: string,
|
|
450
|
-
-- host operating system: "windows" | "macos"
|
|
458
|
+
-- host operating system: "windows" | "macos"
|
|
451
459
|
platform: string,
|
|
452
460
|
}
|
|
453
461
|
|
|
@@ -486,7 +494,8 @@ export type RbBindOptions = {
|
|
|
486
494
|
when: ((device: InputDevice?) -> boolean)?,
|
|
487
495
|
-- runs when the key is released
|
|
488
496
|
release: ((device: InputDevice?) -> ())?,
|
|
489
|
-
-- nickname or id: the bind answers only to that device
|
|
497
|
+
-- nickname or id: the bind answers only to that device; replaces a pin in
|
|
498
|
+
-- the key ("<key>@<device id>", saved by a keybind setting)
|
|
490
499
|
device: string?,
|
|
491
500
|
}
|
|
492
501
|
|
|
@@ -541,9 +550,298 @@ export type RbPhone = {
|
|
|
541
550
|
notify: (text: string, title: string?) -> PhoneResult,
|
|
542
551
|
}
|
|
543
552
|
|
|
553
|
+
-- a colour is "#rrggbb" or "#rrggbbaa"; in widget text it may also be a theme
|
|
554
|
+
-- role: "text", "dim", "faint", "accent", "positive", "warning" or "danger"
|
|
555
|
+
export type OverlaySpan = string | { any }
|
|
556
|
+
|
|
557
|
+
-- a string (a newline starts a line) or one line of spans
|
|
558
|
+
export type OverlayText = string | { OverlaySpan }
|
|
559
|
+
|
|
560
|
+
-- one draw operation in a surface, in logical units from its top-left corner
|
|
561
|
+
export type OverlayOp = {
|
|
562
|
+
kind: "rect" | "text" | "image",
|
|
563
|
+
x: number?,
|
|
564
|
+
y: number?,
|
|
565
|
+
-- rect and image; an image defaults to its size in pixels
|
|
566
|
+
w: number?,
|
|
567
|
+
h: number?,
|
|
568
|
+
fill: string?,
|
|
569
|
+
border: string?,
|
|
570
|
+
border_width: number?,
|
|
571
|
+
radius: number?,
|
|
572
|
+
-- true (50% black), false, or the shadow's opacity from 0 to 1
|
|
573
|
+
shadow: (boolean | number)?,
|
|
574
|
+
-- text: one line, as text plus color and bold, or as spans
|
|
575
|
+
text: string?,
|
|
576
|
+
spans: { OverlaySpan }?,
|
|
577
|
+
color: string?,
|
|
578
|
+
bold: boolean?,
|
|
579
|
+
size: number?,
|
|
580
|
+
-- image (3.14.0): the bytes of a PNG file, such as
|
|
581
|
+
-- rb.file.read_bytes("icon.png")
|
|
582
|
+
png: string?,
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
-- where a surface goes: x and y in rb.screen coordinates, or an anchor
|
|
586
|
+
-- ("top-left", "top", "top-right", "left", "center", "right", "bottom-left",
|
|
587
|
+
-- "bottom", "bottom-right") on a display ("active": the foreground window's
|
|
588
|
+
-- monitor, "primary", "mouse"), moved offset_x and offset_y inward
|
|
589
|
+
export type OverlayPlacement = {
|
|
590
|
+
x: number?,
|
|
591
|
+
y: number?,
|
|
592
|
+
anchor: string?,
|
|
593
|
+
display: ("active" | "primary" | "mouse")?,
|
|
594
|
+
offset_x: number?,
|
|
595
|
+
offset_y: number?,
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export type OverlaySurfaceOptions = OverlayPlacement & {
|
|
599
|
+
width: number,
|
|
600
|
+
height: number,
|
|
601
|
+
opacity: number?,
|
|
602
|
+
-- true shows the surface in screenshots and recordings (3.14.0)
|
|
603
|
+
capturable: boolean?,
|
|
604
|
+
ops: { OverlayOp }?,
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export type OverlaySurface = {
|
|
608
|
+
-- merges fields into the surface; x/y and anchor/display replace each other
|
|
609
|
+
set: (self: OverlaySurface, fields: { [string]: any }) -> OverlaySurface,
|
|
610
|
+
draw: (self: OverlaySurface, ops: { OverlayOp }) -> OverlaySurface,
|
|
611
|
+
remove: (self: OverlaySurface) -> (),
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
-- shared by the widgets; theme is a name from rb.overlay.themes or a table
|
|
615
|
+
-- of the same roles
|
|
616
|
+
export type OverlayWidgetOptions = OverlayPlacement & {
|
|
617
|
+
theme: (string | { [string]: any })?,
|
|
618
|
+
size: number?,
|
|
619
|
+
-- true shows the widget in screenshots and recordings (3.14.0)
|
|
620
|
+
capturable: boolean?,
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export type OverlayToastOptions = OverlayWidgetOptions & {
|
|
624
|
+
title: string?,
|
|
625
|
+
meta: string?,
|
|
626
|
+
-- ms on screen (default 4000)
|
|
627
|
+
duration: number?,
|
|
628
|
+
-- ms to wait for hidden overlays to show before giving up (default 60000)
|
|
629
|
+
expires: number?,
|
|
630
|
+
max_width: number?,
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export type OverlayToast = {
|
|
634
|
+
close: () -> (),
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export type OverlayMenuItem = string | {
|
|
638
|
+
label: string,
|
|
639
|
+
hint: string?,
|
|
640
|
+
action: ((index: number, item: any) -> ())?,
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export type OverlayMenuOptions = OverlayWidgetOptions & {
|
|
644
|
+
title: string?,
|
|
645
|
+
subtitle: string?,
|
|
646
|
+
width: number?,
|
|
647
|
+
keep_open: boolean?,
|
|
648
|
+
on_select: ((index: number, item: any) -> ())?,
|
|
649
|
+
on_close: (() -> ())?,
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export type OverlayMenu = {
|
|
653
|
+
selected: number,
|
|
654
|
+
select: (index: number) -> (),
|
|
655
|
+
close: () -> (),
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export type OverlayPromptOptions = OverlayWidgetOptions & {
|
|
659
|
+
title: string?,
|
|
660
|
+
-- the text to start with
|
|
661
|
+
value: string?,
|
|
662
|
+
-- shown faint while the field is empty
|
|
663
|
+
placeholder: string?,
|
|
664
|
+
-- characters, 1 to 256 (default 128)
|
|
665
|
+
max_length: number?,
|
|
666
|
+
width: number?,
|
|
667
|
+
on_submit: ((text: string) -> ())?,
|
|
668
|
+
on_cancel: (() -> ())?,
|
|
669
|
+
on_close: (() -> ())?,
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export type OverlayPrompt = {
|
|
673
|
+
text: string,
|
|
674
|
+
set: (text: string) -> (),
|
|
675
|
+
close: () -> (),
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export type OverlayStatus = {
|
|
679
|
+
set: (text: OverlayText) -> OverlayStatus,
|
|
680
|
+
remove: () -> (),
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export type OverlayProgressOptions = OverlayWidgetOptions & {
|
|
684
|
+
-- logical units (default 320)
|
|
685
|
+
width: number?,
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export type OverlayProgress = {
|
|
689
|
+
set: (fraction: number?, label: string?) -> OverlayProgress,
|
|
690
|
+
remove: () -> (),
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export type RbOverlay = {
|
|
694
|
+
-- a surface you draw yourself; at most 16 per script covering at most
|
|
695
|
+
-- 2,000,000 square units together, removed when the script stops
|
|
696
|
+
surface: (options: OverlaySurfaceOptions) -> OverlaySurface,
|
|
697
|
+
-- width and height of one line of text in the bundled monospace face
|
|
698
|
+
measure: (text: string, size: number?, bold: boolean?) -> (number, number),
|
|
699
|
+
-- false only when the OS is known to hide overlays (Windows: an exclusive
|
|
700
|
+
-- full-screen Direct3D game; macOS: the overlays on screen covered for 2
|
|
701
|
+
-- seconds, so with none on screen it is true)
|
|
702
|
+
visible: () -> boolean,
|
|
703
|
+
-- removes every surface of this script
|
|
704
|
+
clear: () -> (),
|
|
705
|
+
-- "rebind", "rebind-light", "catppuccin", "tokyo-night", "contrast"
|
|
706
|
+
themes: { [string]: { [string]: any } },
|
|
707
|
+
-- a timed notification; toasts at one anchor stack, and one asked for while
|
|
708
|
+
-- overlays are hidden waits until they show or it expires. A waiting toast
|
|
709
|
+
-- that then cannot show is dropped with a warning in the script's log.
|
|
710
|
+
toast: (text: OverlayText, options: OverlayToastOptions?) -> OverlayToast,
|
|
711
|
+
-- a keyboard menu: while open it takes Up, Down, Enter, Escape and 1 to 9 and
|
|
712
|
+
-- swallows them. A pick closes it (unless keep_open), then runs the item's
|
|
713
|
+
-- action and on_select. Losing focus or the OS hiding overlays also closes
|
|
714
|
+
-- it; on_close runs for every close except rb.overlay.clear() and stopping.
|
|
715
|
+
-- nil and a reason when overlays are hidden, the script is unfocused, or a
|
|
716
|
+
-- menu or prompt is already open, in this script or another.
|
|
717
|
+
menu: (items: { OverlayMenuItem }, options: OverlayMenuOptions?) -> (OverlayMenu?, string?),
|
|
718
|
+
-- (3.15.0) a one-line text field typed through swallowing binds, so it needs no focus.
|
|
719
|
+
-- Enter closes it and runs on_submit(text); Escape, close(), losing focus or
|
|
720
|
+
-- the OS hiding overlays close it and run on_cancel; on_close then runs for
|
|
721
|
+
-- either. Each key types its US-layout character, Shift picks the shifted
|
|
722
|
+
-- one, Backspace deletes, Ctrl+V (Cmd+V on macOS) pastes. Ctrl, Alt and Win
|
|
723
|
+
-- chords reach the app. Holding a key acts once. It holds the same keyboard
|
|
724
|
+
-- as menu, so it returns the same reasons as menu.
|
|
725
|
+
prompt: (label: string, options: OverlayPromptOptions?) -> (OverlayPrompt?, string?),
|
|
726
|
+
-- a one-line strip that stays until removed
|
|
727
|
+
status: (text: OverlayText, options: OverlayWidgetOptions?) -> OverlayStatus,
|
|
728
|
+
-- a labelled bar, fraction 0 to 1
|
|
729
|
+
progress: (label: string, fraction: number?, options: OverlayProgressOptions?) -> OverlayProgress,
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
-- rb.observe (3.15.0) records what the user does, as `rebind observe` does:
|
|
733
|
+
-- every key and button with its hold time, mouse movement, the active window
|
|
734
|
+
-- and screenshots, in a session folder the script reads back. It needs the
|
|
735
|
+
-- observe permission, which a script holds only when it declares it, with
|
|
736
|
+
-- min_sdk=3.15.0 or later. A script marks and stops only the session it
|
|
737
|
+
-- started, and renames or deletes only recordings a script made under its
|
|
738
|
+
-- name (owner_kind "script"); it reads every recording.
|
|
739
|
+
-- start, stop, list, events, frame, rename and delete wait for an answer, so
|
|
740
|
+
-- call them inside rb.run; mark and status answer at once.
|
|
741
|
+
|
|
742
|
+
export type ObserveStartOptions = {
|
|
743
|
+
-- 1 to 14400 (default 120)
|
|
744
|
+
seconds: number?,
|
|
745
|
+
-- a keyboard key that ends the session early, such as "F8"
|
|
746
|
+
stop_key: string?,
|
|
747
|
+
-- what the recording is for, up to 500 characters
|
|
748
|
+
goal: string?,
|
|
749
|
+
-- ms between idle screenshots, 0 to 30000; 0 turns them off (default 1000)
|
|
750
|
+
frame_ms: number?,
|
|
751
|
+
-- the long edge of screenshots, 256 to 2048 pixels (default 1024)
|
|
752
|
+
frame_edge: number?,
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export type ObserveStatus = {
|
|
756
|
+
id: string,
|
|
757
|
+
-- the session folder, holding events.jsonl, frames/ and crops/
|
|
758
|
+
dir: string,
|
|
759
|
+
elapsed_ms: number,
|
|
760
|
+
seconds: number,
|
|
761
|
+
events: number,
|
|
762
|
+
frames: number,
|
|
763
|
+
crops: number,
|
|
764
|
+
marks: number,
|
|
765
|
+
drops: number,
|
|
766
|
+
bytes: number,
|
|
767
|
+
-- the script's name, or the app or CLI that started it
|
|
768
|
+
owner: string,
|
|
769
|
+
-- "script" for a session rb.observe started, "client" for the app or CLI
|
|
770
|
+
owner_kind: string,
|
|
771
|
+
goal: string?,
|
|
772
|
+
-- why no screenshots are taken, when they are not
|
|
773
|
+
frames_off: string?,
|
|
774
|
+
-- why it ended, once it has
|
|
775
|
+
reason: string?,
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export type ObserveSummary = {
|
|
779
|
+
id: string,
|
|
780
|
+
name: string?,
|
|
781
|
+
goal: string?,
|
|
782
|
+
owner: string,
|
|
783
|
+
-- "script" or "client", as on ObserveStatus
|
|
784
|
+
owner_kind: string,
|
|
785
|
+
-- epoch ms
|
|
786
|
+
started_at: number,
|
|
787
|
+
ended_at: number?,
|
|
788
|
+
seconds: number,
|
|
789
|
+
events: number,
|
|
790
|
+
frames: number,
|
|
791
|
+
crops: number,
|
|
792
|
+
marks: number,
|
|
793
|
+
drops: number,
|
|
794
|
+
dropped_marks: number,
|
|
795
|
+
bytes: number,
|
|
796
|
+
frames_off: string?,
|
|
797
|
+
reason: string?,
|
|
798
|
+
-- true while it records
|
|
799
|
+
running: boolean?,
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export type ObserveEvents = {
|
|
803
|
+
-- events.jsonl lines, each a table with seq, t and kind
|
|
804
|
+
lines: { { [string]: any } },
|
|
805
|
+
-- the seq to ask for next
|
|
806
|
+
next: number,
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
export type ObserveImage = {
|
|
810
|
+
mime: string,
|
|
811
|
+
width: number,
|
|
812
|
+
height: number,
|
|
813
|
+
-- the PNG, base64
|
|
814
|
+
data: string,
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export type RbObserve = {
|
|
818
|
+
-- starts a session this script owns; one session runs at a time
|
|
819
|
+
start: (options: ObserveStartOptions?) -> ObserveStatus,
|
|
820
|
+
-- ends the session this script started; its final counts and reason
|
|
821
|
+
stop: () -> ObserveStatus,
|
|
822
|
+
-- a note in this script's running session: label up to 200 characters,
|
|
823
|
+
-- data up to 8 KB as JSON and 32 tables deep. false when this script has
|
|
824
|
+
-- no session running or the note could not be queued
|
|
825
|
+
mark: (kind: "mark" | "success" | "failure" | "ai" | "own", label: string?, data: any?) -> boolean,
|
|
826
|
+
-- the running session, whoever started it, or nil
|
|
827
|
+
status: () -> ObserveStatus?,
|
|
828
|
+
-- every recording, newest first
|
|
829
|
+
list: () -> { ObserveSummary },
|
|
830
|
+
-- lines from from_seq through to_seq, stopping before max_bytes of them
|
|
831
|
+
-- (default 262144, at most 4194304) but returning at least one
|
|
832
|
+
events: (id: string, from_seq: number?, to_seq: number?, max_bytes: number?) -> ObserveEvents,
|
|
833
|
+
-- "frames/NNNNNN.png", "frames/NNNNNN.win.png" or "crops/NNNNNN.png",
|
|
834
|
+
-- scaled down to max_edge
|
|
835
|
+
frame: (id: string, file: string, max_edge: number?) -> ObserveImage,
|
|
836
|
+
-- names a finished recording; "" clears the name
|
|
837
|
+
rename: (id: string, name: string) -> ObserveSummary,
|
|
838
|
+
-- removes a finished recording's folder; returns its id
|
|
839
|
+
delete: (id: string) -> string,
|
|
840
|
+
}
|
|
841
|
+
|
|
544
842
|
-- 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.
|
|
843
|
+
-- handler receives. For down, up, scroll and axis, a handler that returns
|
|
844
|
+
-- false swallows the input; for move only with mouse_block=true.
|
|
547
845
|
export type Event =
|
|
548
846
|
| "start" -- ()
|
|
549
847
|
| "stop" -- ()
|
|
@@ -557,6 +855,7 @@ export type Event =
|
|
|
557
855
|
| "move" -- (dx: number, dy: number, device: InputDevice?) hardware mode only
|
|
558
856
|
| "scroll" -- (delta: number, device: InputDevice?)
|
|
559
857
|
| "scroll_h" -- (delta: number)
|
|
858
|
+
| "axis" -- (axis: string, value: number, device: InputDevice?) hardware mode only
|
|
560
859
|
| "input_down" -- (key: string, device: InputDevice?)
|
|
561
860
|
| "input_up" -- (key: string, duration_ms: number, device: InputDevice?)
|
|
562
861
|
| "input_move" -- (dx: number, dy: number, device: InputDevice?)
|
|
@@ -577,9 +876,19 @@ export type Event =
|
|
|
577
876
|
-- system: PrintScreen, Pause, Menu
|
|
578
877
|
-- numpad: Kp0-Kp9, KpPlus, KpMinus, KpMultiply, KpDivide, KpEnter, KpDot
|
|
579
878
|
--
|
|
580
|
-
--
|
|
879
|
+
-- delivered as input only in hardware mode:
|
|
581
880
|
-- function: F13-F24
|
|
582
|
-
-- media: MediaNext, MediaPrev, MediaStop, MediaPlay, VolumeUp, VolumeDown,
|
|
881
|
+
-- media: MediaNext, MediaPrev, MediaStop, MediaPlay, VolumeUp, VolumeDown, VolumeMute
|
|
882
|
+
--
|
|
883
|
+
-- game controllers, hardware mode only (named by position, Xbox letters):
|
|
884
|
+
-- buttons: PadA (bottom), PadB (right), PadX (left), PadY (top), PadLB,
|
|
885
|
+
-- PadRB, PadLT, PadRT, PadView, PadMenu, PadHome, PadLS, PadRS,
|
|
886
|
+
-- PadUp, PadDown, PadLeft, PadRight, PadP1-PadP4 (paddles)
|
|
887
|
+
-- other pads: Joy1-Joy32, in the order the controller reports them
|
|
888
|
+
-- axes: LeftX, LeftY, RightX, RightY (-1..1, up and left negative),
|
|
889
|
+
-- LT, RT (0..1); the "axis" event and input.axis use these
|
|
890
|
+
--
|
|
891
|
+
-- HID output and bind chord modifiers only:
|
|
583
892
|
-- aliases: Numpad0-9, NP0-9, NPAdd, NPSubtract, NPMultiply, NPDivide, NPEnter,
|
|
584
893
|
-- NPDecimal, Ctrl, Shift, Alt, Win, Meta, Del, PgUp, PgDn, etc.
|
|
585
894
|
|
|
@@ -652,7 +961,6 @@ declare rb: {
|
|
|
652
961
|
-- most), waiting with sleep in rb.run and blocking elsewhere. With ms
|
|
653
962
|
-- it glides there over ms on an eased path; a duration needs rb.run or
|
|
654
963
|
-- rb.async.
|
|
655
|
-
-- Not supported on Linux (no cursor source).
|
|
656
964
|
move_to: (x: number, y: number, ms: number?) -> (),
|
|
657
965
|
scroll: (amount: number) -> (),
|
|
658
966
|
-- deprecated: the mode is stored but no longer changes move_to
|
|
@@ -662,13 +970,22 @@ declare rb: {
|
|
|
662
970
|
-- up, press and combo also accept "usage:N" (decimal or 0x hex, 0x04-0xE7).
|
|
663
971
|
usage: (name: string) -> number?,
|
|
664
972
|
-- sets the whole joypad state (last write wins across scripts). Needs a
|
|
665
|
-
-- Rebind Link whose USB profile has a joypad; raises otherwise.
|
|
973
|
+
-- Rebind Link whose USB profile has a joypad; raises otherwise. down, up,
|
|
974
|
+
-- press and combo also press controller buttons ("PadA").
|
|
666
975
|
joypad: (state: JoypadState?) -> (),
|
|
667
|
-
--
|
|
976
|
+
-- holds one controller axis (LeftX..RightY -1..1, LT/RT 0..1) until set
|
|
977
|
+
-- again or reset; it wins over a captured controller's value
|
|
978
|
+
axis: (name: string, value: number) -> (),
|
|
979
|
+
-- drops what scripts set, keeping what a captured controller holds; the
|
|
980
|
+
-- relay also does this when a script stops or errors
|
|
668
981
|
joypad_reset: () -> (),
|
|
669
982
|
},
|
|
670
983
|
input: {
|
|
671
984
|
is_down: (code: string) -> boolean,
|
|
985
|
+
-- a controller axis by name, 0 at rest
|
|
986
|
+
axis: (name: string) -> number,
|
|
987
|
+
-- the devices Rebind has identified (3.12.0); empty in software mode
|
|
988
|
+
devices: () -> { InputDevice },
|
|
672
989
|
duration: (code: string) -> number,
|
|
673
990
|
keys: () -> { string },
|
|
674
991
|
mouse_pos: () -> MousePos,
|
|
@@ -682,6 +999,7 @@ declare rb: {
|
|
|
682
999
|
select: (default: string, choices: { string }, opts: SelectOpts?) -> any,
|
|
683
1000
|
text: (default: string, opts: TextOpts?) -> any,
|
|
684
1001
|
color: (default: string, opts: ColorOpts?) -> any,
|
|
1002
|
+
list: (default: { string }, opts: ListOpts?) -> any,
|
|
685
1003
|
get: (id: string) -> any,
|
|
686
1004
|
set: (id: string, value: any) -> (),
|
|
687
1005
|
get_all: () -> { [string]: any },
|
|
@@ -770,8 +1088,9 @@ declare rb: {
|
|
|
770
1088
|
wait_close: (title: string, timeout: number?) -> boolean,
|
|
771
1089
|
},
|
|
772
1090
|
-- application control. macOS matches app display names ignoring case;
|
|
773
|
-
-- Windows matches executable names (".exe" optional) and cannot hide
|
|
774
|
-
--
|
|
1091
|
+
-- Windows matches executable names (".exe" optional) and cannot hide.
|
|
1092
|
+
-- activate focuses a running app only (it needs Accessibility on macOS);
|
|
1093
|
+
-- start one with rb.system.exec_detached (permission: exec).
|
|
775
1094
|
app: {
|
|
776
1095
|
front: () -> AppInfo?,
|
|
777
1096
|
is_front: (name: string) -> boolean,
|
|
@@ -791,17 +1110,21 @@ declare rb: {
|
|
|
791
1110
|
head: (url: string, options: RbHttpOptions?) -> HttpResponse,
|
|
792
1111
|
request: (options: HttpRequestOptions) -> HttpResponse,
|
|
793
1112
|
listen: (port: number, handler: (request: HttpRequest) -> HttpResponseSpec | string) -> ServerHandle,
|
|
794
|
-
|
|
1113
|
+
-- options.host binds one IP ("127.0.0.1" keeps it on this computer); default all interfaces
|
|
1114
|
+
ws_listen: (port: number, handlers: WsListenHandlers, options: { host: string? }?) -> WsServerHandle,
|
|
795
1115
|
ws_connect: (url: string, handlers: WsConnectHandlers) -> WsConnectHandle,
|
|
796
1116
|
-- binds a UDP socket; handler receives each datagram's raw bytes and sender.
|
|
797
1117
|
udp_listen: (bind_ip: string, port: number, handler: (data: string, ip: string, port: number) -> ()) -> UdpHandle,
|
|
798
1118
|
-- this computer's IPv4 address on its default route, or nil when there is none.
|
|
799
1119
|
local_ip: () -> string?,
|
|
800
1120
|
},
|
|
1121
|
+
-- permission: exec
|
|
801
1122
|
pipe: {
|
|
802
1123
|
open: (name: string, options: { size: number? }?) -> PipeHandle,
|
|
803
1124
|
},
|
|
804
|
-
-- paths are relative to the script directory; absolute paths are rejected
|
|
1125
|
+
-- paths are relative to the script directory; absolute paths are rejected.
|
|
1126
|
+
-- "@data/..." paths (3.13.0) reach the script's data folder instead, kept
|
|
1127
|
+
-- outside the scripts tree across updates and deleted with the script.
|
|
805
1128
|
file: {
|
|
806
1129
|
read: (path: string) -> string,
|
|
807
1130
|
read_bytes: (path: string) -> string,
|
|
@@ -812,13 +1135,16 @@ declare rb: {
|
|
|
812
1135
|
-- nil path lists the script's own directory
|
|
813
1136
|
list: (path: string?) -> { string },
|
|
814
1137
|
mkdir: (path: string) -> boolean,
|
|
815
|
-
-- recursive;
|
|
1138
|
+
-- recursive; raises when the directory does not exist or cannot be removed.
|
|
816
1139
|
rmdir: (path: string) -> boolean,
|
|
817
1140
|
copy: (src: string, dst: string) -> (),
|
|
818
1141
|
move: (src: string, dst: string) -> (),
|
|
819
1142
|
read_json: (path: string) -> any,
|
|
820
1143
|
write_json: (path: string, value: any) -> (),
|
|
821
1144
|
script_dir: () -> string,
|
|
1145
|
+
-- absolute path of the script's data folder (3.13.0); raises for a
|
|
1146
|
+
-- script with no file
|
|
1147
|
+
data_dir: () -> string,
|
|
822
1148
|
size: (path: string) -> number,
|
|
823
1149
|
time: (path: string) -> number,
|
|
824
1150
|
is_dir: (path: string) -> boolean,
|
|
@@ -879,6 +1205,7 @@ declare rb: {
|
|
|
879
1205
|
},
|
|
880
1206
|
env: {
|
|
881
1207
|
get: (name: string) -> string?,
|
|
1208
|
+
-- Requires the 'exec' permission.
|
|
882
1209
|
set: (name: string, value: string) -> (),
|
|
883
1210
|
home: () -> string?,
|
|
884
1211
|
config: () -> string?,
|
|
@@ -900,6 +1227,10 @@ declare rb: {
|
|
|
900
1227
|
-- Rebind Pro; require("@rebind/pro") returns rb
|
|
901
1228
|
ai: RbAi,
|
|
902
1229
|
phone: RbPhone,
|
|
1230
|
+
-- drawing on screen over every app, games included (Windows and macOS)
|
|
1231
|
+
overlay: RbOverlay,
|
|
1232
|
+
-- recording input, windows and screenshots (3.15.0); permission: observe
|
|
1233
|
+
observe: RbObserve,
|
|
903
1234
|
}
|
|
904
1235
|
|
|
905
1236
|
-- v1 names
|
|
@@ -965,7 +1296,6 @@ declare HID: {
|
|
|
965
1296
|
-- rereads the cursor and corrects until within 2 px (about 150 ms at
|
|
966
1297
|
-- most), waiting with Sleep in Run and blocking elsewhere. With ms it
|
|
967
1298
|
-- glides there over ms on an eased path; a duration needs Run or Async.
|
|
968
|
-
-- Not supported on Linux (no cursor source).
|
|
969
1299
|
MoveTo: (x: number, y: number, ms: number?) -> (),
|
|
970
1300
|
Scroll: (amount: number) -> (),
|
|
971
1301
|
-- deprecated: the mode is stored but no longer changes MoveTo
|
|
@@ -977,8 +1307,13 @@ declare HID: {
|
|
|
977
1307
|
-- set the whole joypad state (last write wins across scripts). Needs a
|
|
978
1308
|
-- Rebind Link whose USB profile has a joypad; raises otherwise. Keyboard
|
|
979
1309
|
-- and mouse calls raise the same way when the profile leaves them out.
|
|
1310
|
+
-- Down, Up, Press and Combo also press controller buttons ("PadA").
|
|
980
1311
|
Joypad: (state: JoypadState?) -> (),
|
|
981
|
-
--
|
|
1312
|
+
-- hold one controller axis (LeftX..RightY -1..1, LT/RT 0..1) until set
|
|
1313
|
+
-- again or reset; it wins over a captured controller's value
|
|
1314
|
+
Axis: (name: string, value: number) -> (),
|
|
1315
|
+
-- drop what scripts set, keeping what a captured controller holds; the
|
|
1316
|
+
-- relay also does this when a script stops or errors
|
|
982
1317
|
JoypadReset: () -> (),
|
|
983
1318
|
}
|
|
984
1319
|
|
|
@@ -986,6 +1321,10 @@ declare HID: {
|
|
|
986
1321
|
|
|
987
1322
|
declare Input: {
|
|
988
1323
|
IsDown: (code: string) -> boolean,
|
|
1324
|
+
-- a controller axis by name, 0 at rest
|
|
1325
|
+
GetAxis: (name: string) -> number,
|
|
1326
|
+
-- the devices Rebind has identified (3.12.0); empty in software mode
|
|
1327
|
+
Devices: () -> { InputDevice },
|
|
989
1328
|
GetDuration: (code: string) -> number,
|
|
990
1329
|
GetActiveKeys: () -> { string },
|
|
991
1330
|
GetMousePos: () -> MousePos,
|
|
@@ -1002,6 +1341,7 @@ declare UI: {
|
|
|
1002
1341
|
Select: (default: string, choices: { string }, opts: SelectOpts?) -> any,
|
|
1003
1342
|
Text: (default: string, opts: TextOpts?) -> any,
|
|
1004
1343
|
Color: (default: string, opts: ColorOpts?) -> any,
|
|
1344
|
+
List: (default: { string }, opts: ListOpts?) -> any,
|
|
1005
1345
|
Get: (id: string) -> any,
|
|
1006
1346
|
Set: (id: string, value: any) -> (),
|
|
1007
1347
|
GetAll: () -> { [string]: any },
|
|
@@ -1131,12 +1471,13 @@ declare Window: {
|
|
|
1131
1471
|
}
|
|
1132
1472
|
|
|
1133
1473
|
-- app namespace: application-level control (Keyboard Maestro's Application
|
|
1134
|
-
-- Control).
|
|
1135
|
-
--
|
|
1136
|
-
--
|
|
1474
|
+
-- Control). Activate focuses a running app and never launches one; start an
|
|
1475
|
+
-- app with System.ExecDetached (permission: exec). macOS: names match
|
|
1476
|
+
-- case-insensitively by app display name; Activate needs Accessibility;
|
|
1477
|
+
-- Quit/IsHidden drive an AppleEvent and prompt for macOS Automation
|
|
1478
|
+
-- permission on first use; Quit leaves an app that is not running alone.
|
|
1137
1479
|
-- Windows: names are executable names (case-insensitive, ".exe" optional);
|
|
1138
|
-
--
|
|
1139
|
-
-- windows; Hide/IsHidden raise. Linux: every call raises.
|
|
1480
|
+
-- Quit closes its windows; Hide/IsHidden raise.
|
|
1140
1481
|
|
|
1141
1482
|
declare App: {
|
|
1142
1483
|
Front: () -> AppInfo?,
|
|
@@ -1159,7 +1500,7 @@ declare Net: {
|
|
|
1159
1500
|
Head: (url: string, headers: { [string]: string }?, options: HttpOptions?) -> HttpResponse,
|
|
1160
1501
|
Request: (options: HttpRequestOptions) -> HttpResponse,
|
|
1161
1502
|
Listen: (port: number, handler: (request: HttpRequest) -> HttpResponseSpec | string) -> ServerHandle,
|
|
1162
|
-
WSListen: (port: number, handlers: WsListenHandlers) -> WsServerHandle,
|
|
1503
|
+
WSListen: (port: number, handlers: WsListenHandlers, options: { host: string? }?) -> WsServerHandle,
|
|
1163
1504
|
WSConnect: (url: string, handlers: WsConnectHandlers) -> WsConnectHandle,
|
|
1164
1505
|
-- bind a UDP socket; handler receives each datagram's raw bytes and sender.
|
|
1165
1506
|
UDPListen: (bindIp: string, port: number, handler: (data: string, ip: string, port: number) -> ()) -> UdpHandle,
|
|
@@ -1167,7 +1508,7 @@ declare Net: {
|
|
|
1167
1508
|
LocalIP: () -> string?,
|
|
1168
1509
|
}
|
|
1169
1510
|
|
|
1170
|
-
-- pipe namespace
|
|
1511
|
+
-- pipe namespace (permission: exec)
|
|
1171
1512
|
|
|
1172
1513
|
declare Pipe: {
|
|
1173
1514
|
Open: (name: string, options: { size: number? }?) -> PipeHandle,
|
|
@@ -1183,16 +1524,20 @@ declare File: {
|
|
|
1183
1524
|
Exists: (path: string) -> boolean,
|
|
1184
1525
|
Delete: (path: string) -> boolean,
|
|
1185
1526
|
-- nil path lists the script's own directory. Paths are relative to the
|
|
1186
|
-
-- script dir (the File.* jail); absolute paths are rejected.
|
|
1527
|
+
-- script dir (the File.* jail); absolute paths are rejected. "@data/..."
|
|
1528
|
+
-- paths (3.13.0) reach the script's data folder instead.
|
|
1187
1529
|
List: (path: string?) -> { string },
|
|
1188
1530
|
MkDir: (path: string) -> boolean,
|
|
1189
|
-
-- recursive;
|
|
1531
|
+
-- recursive; raises when the directory does not exist or cannot be removed.
|
|
1190
1532
|
RmDir: (path: string) -> boolean,
|
|
1191
1533
|
Copy: (src: string, dst: string) -> (),
|
|
1192
1534
|
Move: (src: string, dst: string) -> (),
|
|
1193
1535
|
ReadJSON: (path: string) -> any,
|
|
1194
1536
|
WriteJSON: (path: string, value: any) -> (),
|
|
1195
1537
|
GetScriptDir: () -> string,
|
|
1538
|
+
-- absolute path of the script's data folder (3.13.0); raises for a
|
|
1539
|
+
-- script with no file
|
|
1540
|
+
GetDataDir: () -> string,
|
|
1196
1541
|
GetSize: (path: string) -> number,
|
|
1197
1542
|
GetTime: (path: string) -> number,
|
|
1198
1543
|
IsDir: (path: string) -> boolean,
|
|
@@ -1279,6 +1624,7 @@ declare Codec: {
|
|
|
1279
1624
|
|
|
1280
1625
|
declare Env: {
|
|
1281
1626
|
Get: (name: string) -> string?,
|
|
1627
|
+
-- Requires the 'exec' permission.
|
|
1282
1628
|
Set: (name: string, value: string) -> (),
|
|
1283
1629
|
Home: () -> string?,
|
|
1284
1630
|
Config: () -> string?,
|
|
@@ -1313,6 +1659,7 @@ declare Registry: {
|
|
|
1313
1659
|
-- declare function OnMove(dx: number, dy: number, device: InputDevice?): boolean? -- return value only used with mouse_block=true (hardware mode only)
|
|
1314
1660
|
-- declare function OnScroll(delta: number, device: InputDevice?): boolean?
|
|
1315
1661
|
-- declare function OnScrollH(delta: number): boolean?
|
|
1662
|
+
-- declare function OnAxis(axis: string, value: number, device: InputDevice?): boolean? -- hardware mode only
|
|
1316
1663
|
-- declare function OnInputDown(key: string, device: InputDevice?): ()
|
|
1317
1664
|
-- declare function OnInputUp(key: string, durationMs: number, device: InputDevice?): ()
|
|
1318
1665
|
-- declare function OnInputMove(dx: number, dy: number, device: InputDevice?): ()
|
package/src/keys.test.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
import { normalizeCombo } from "./keys";
|
|
3
|
-
|
|
4
|
-
test("normalizes every Command spelling models emit", () => {
|
|
5
|
-
for (const alias of ["LCmd", "Cmd", "Command", "⌘", "Super", "LWin", "Win"]) {
|
|
6
|
-
expect(normalizeCombo(`${alias}+L`)).toBe("LMeta+L");
|
|
7
|
-
}
|
|
8
|
-
expect(normalizeCombo("RCmd+Tab")).toBe("RMeta+Tab");
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test("passes through already-valid combos and bare keys", () => {
|
|
12
|
-
expect(normalizeCombo("LCtrl+Shift+P")).toBe("LCtrl+Shift+P");
|
|
13
|
-
expect(normalizeCombo("Enter")).toBe("Enter");
|
|
14
|
-
expect(normalizeCombo(" LMeta + A ")).toBe("LMeta+A");
|
|
15
|
-
});
|