@rebind.gg/mcp-server 0.3.2 → 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 +2 -2
- package/src/index.ts +188 -0
- package/src/observe.ts +19 -0
- package/src/prompts.ts +5 -1
- package/src/rebind.d.luau +175 -7
- 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",
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { isSafeFetchUrl } from "./fetch-guard.ts";
|
|
|
18
18
|
import { renderScreenshot } from "./image.ts";
|
|
19
19
|
import { normalizeCombo } from "./keys.ts";
|
|
20
20
|
import { llmStep, type Msg } from "./llm.ts";
|
|
21
|
+
import { observeRefusal } from "./observe.ts";
|
|
21
22
|
import {
|
|
22
23
|
BATCHING_DOCTRINE,
|
|
23
24
|
DETERMINISTIC_WORK_DOCTRINE,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
KEY_DESC,
|
|
26
27
|
LUA_DOCS_DESC,
|
|
27
28
|
modifierNote,
|
|
29
|
+
OBSERVE_DOCTRINE,
|
|
28
30
|
type OS,
|
|
29
31
|
openAppHow,
|
|
30
32
|
PROBE_CLIS_CMD,
|
|
@@ -310,6 +312,7 @@ async function buildInstructions(
|
|
|
310
312
|
BATCHING_DOCTRINE,
|
|
311
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.",
|
|
312
314
|
guiTips(os),
|
|
315
|
+
OBSERVE_DOCTRINE,
|
|
313
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.",
|
|
314
317
|
`Full run_lua Luau SDK — every namespace and signature (do not guess APIs):\n${sdk}`,
|
|
315
318
|
]
|
|
@@ -1168,6 +1171,191 @@ return { ok = Window.IsActive(h), name = name }
|
|
|
1168
1171
|
async () => text(await v1Sdk()),
|
|
1169
1172
|
);
|
|
1170
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
|
+
|
|
1171
1359
|
// Server-side subagents. Gated: a stock install ships without them. They make
|
|
1172
1360
|
// OpenRouter calls (money) and touch no HID, so they register DIRECTLY on the
|
|
1173
1361
|
// server — NOT through `register()`, whose serial `chain` would let one
|
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
|
@@ -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?) -> ())?,
|
|
@@ -485,7 +494,8 @@ export type RbBindOptions = {
|
|
|
485
494
|
when: ((device: InputDevice?) -> boolean)?,
|
|
486
495
|
-- runs when the key is released
|
|
487
496
|
release: ((device: InputDevice?) -> ())?,
|
|
488
|
-
-- 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)
|
|
489
499
|
device: string?,
|
|
490
500
|
}
|
|
491
501
|
|
|
@@ -549,10 +559,10 @@ export type OverlayText = string | { OverlaySpan }
|
|
|
549
559
|
|
|
550
560
|
-- one draw operation in a surface, in logical units from its top-left corner
|
|
551
561
|
export type OverlayOp = {
|
|
552
|
-
kind: "rect" | "text",
|
|
562
|
+
kind: "rect" | "text" | "image",
|
|
553
563
|
x: number?,
|
|
554
564
|
y: number?,
|
|
555
|
-
-- rect
|
|
565
|
+
-- rect and image; an image defaults to its size in pixels
|
|
556
566
|
w: number?,
|
|
557
567
|
h: number?,
|
|
558
568
|
fill: string?,
|
|
@@ -567,6 +577,9 @@ export type OverlayOp = {
|
|
|
567
577
|
color: string?,
|
|
568
578
|
bold: boolean?,
|
|
569
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?,
|
|
570
583
|
}
|
|
571
584
|
|
|
572
585
|
-- where a surface goes: x and y in rb.screen coordinates, or an anchor
|
|
@@ -586,6 +599,8 @@ export type OverlaySurfaceOptions = OverlayPlacement & {
|
|
|
586
599
|
width: number,
|
|
587
600
|
height: number,
|
|
588
601
|
opacity: number?,
|
|
602
|
+
-- true shows the surface in screenshots and recordings (3.14.0)
|
|
603
|
+
capturable: boolean?,
|
|
589
604
|
ops: { OverlayOp }?,
|
|
590
605
|
}
|
|
591
606
|
|
|
@@ -601,6 +616,8 @@ export type OverlaySurface = {
|
|
|
601
616
|
export type OverlayWidgetOptions = OverlayPlacement & {
|
|
602
617
|
theme: (string | { [string]: any })?,
|
|
603
618
|
size: number?,
|
|
619
|
+
-- true shows the widget in screenshots and recordings (3.14.0)
|
|
620
|
+
capturable: boolean?,
|
|
604
621
|
}
|
|
605
622
|
|
|
606
623
|
export type OverlayToastOptions = OverlayWidgetOptions & {
|
|
@@ -638,6 +655,26 @@ export type OverlayMenu = {
|
|
|
638
655
|
close: () -> (),
|
|
639
656
|
}
|
|
640
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
|
+
|
|
641
678
|
export type OverlayStatus = {
|
|
642
679
|
set: (text: OverlayText) -> OverlayStatus,
|
|
643
680
|
remove: () -> (),
|
|
@@ -676,14 +713,132 @@ export type RbOverlay = {
|
|
|
676
713
|
-- action and on_select. Losing focus or the OS hiding overlays also closes
|
|
677
714
|
-- it; on_close runs for every close except rb.overlay.clear() and stopping.
|
|
678
715
|
-- nil and a reason when overlays are hidden, the script is unfocused, or a
|
|
679
|
-
-- menu is already open, in this script or another.
|
|
716
|
+
-- menu or prompt is already open, in this script or another.
|
|
680
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?),
|
|
681
726
|
-- a one-line strip that stays until removed
|
|
682
727
|
status: (text: OverlayText, options: OverlayWidgetOptions?) -> OverlayStatus,
|
|
683
728
|
-- a labelled bar, fraction 0 to 1
|
|
684
729
|
progress: (label: string, fraction: number?, options: OverlayProgressOptions?) -> OverlayProgress,
|
|
685
730
|
}
|
|
686
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
|
+
|
|
687
842
|
-- events for rb.on (Rebind 3.9.0 and later), with the arguments each
|
|
688
843
|
-- handler receives. For down, up, scroll and axis, a handler that returns
|
|
689
844
|
-- false swallows the input; for move only with mouse_block=true.
|
|
@@ -844,6 +999,7 @@ declare rb: {
|
|
|
844
999
|
select: (default: string, choices: { string }, opts: SelectOpts?) -> any,
|
|
845
1000
|
text: (default: string, opts: TextOpts?) -> any,
|
|
846
1001
|
color: (default: string, opts: ColorOpts?) -> any,
|
|
1002
|
+
list: (default: { string }, opts: ListOpts?) -> any,
|
|
847
1003
|
get: (id: string) -> any,
|
|
848
1004
|
set: (id: string, value: any) -> (),
|
|
849
1005
|
get_all: () -> { [string]: any },
|
|
@@ -966,7 +1122,9 @@ declare rb: {
|
|
|
966
1122
|
pipe: {
|
|
967
1123
|
open: (name: string, options: { size: number? }?) -> PipeHandle,
|
|
968
1124
|
},
|
|
969
|
-
-- 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.
|
|
970
1128
|
file: {
|
|
971
1129
|
read: (path: string) -> string,
|
|
972
1130
|
read_bytes: (path: string) -> string,
|
|
@@ -984,6 +1142,9 @@ declare rb: {
|
|
|
984
1142
|
read_json: (path: string) -> any,
|
|
985
1143
|
write_json: (path: string, value: any) -> (),
|
|
986
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,
|
|
987
1148
|
size: (path: string) -> number,
|
|
988
1149
|
time: (path: string) -> number,
|
|
989
1150
|
is_dir: (path: string) -> boolean,
|
|
@@ -1068,6 +1229,8 @@ declare rb: {
|
|
|
1068
1229
|
phone: RbPhone,
|
|
1069
1230
|
-- drawing on screen over every app, games included (Windows and macOS)
|
|
1070
1231
|
overlay: RbOverlay,
|
|
1232
|
+
-- recording input, windows and screenshots (3.15.0); permission: observe
|
|
1233
|
+
observe: RbObserve,
|
|
1071
1234
|
}
|
|
1072
1235
|
|
|
1073
1236
|
-- v1 names
|
|
@@ -1178,6 +1341,7 @@ declare UI: {
|
|
|
1178
1341
|
Select: (default: string, choices: { string }, opts: SelectOpts?) -> any,
|
|
1179
1342
|
Text: (default: string, opts: TextOpts?) -> any,
|
|
1180
1343
|
Color: (default: string, opts: ColorOpts?) -> any,
|
|
1344
|
+
List: (default: { string }, opts: ListOpts?) -> any,
|
|
1181
1345
|
Get: (id: string) -> any,
|
|
1182
1346
|
Set: (id: string, value: any) -> (),
|
|
1183
1347
|
GetAll: () -> { [string]: any },
|
|
@@ -1360,7 +1524,8 @@ declare File: {
|
|
|
1360
1524
|
Exists: (path: string) -> boolean,
|
|
1361
1525
|
Delete: (path: string) -> boolean,
|
|
1362
1526
|
-- nil path lists the script's own directory. Paths are relative to the
|
|
1363
|
-
-- 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.
|
|
1364
1529
|
List: (path: string?) -> { string },
|
|
1365
1530
|
MkDir: (path: string) -> boolean,
|
|
1366
1531
|
-- recursive; raises when the directory does not exist or cannot be removed.
|
|
@@ -1370,6 +1535,9 @@ declare File: {
|
|
|
1370
1535
|
ReadJSON: (path: string) -> any,
|
|
1371
1536
|
WriteJSON: (path: string, value: any) -> (),
|
|
1372
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,
|
|
1373
1541
|
GetSize: (path: string) -> number,
|
|
1374
1542
|
GetTime: (path: string) -> number,
|
|
1375
1543
|
IsDir: (path: string) -> boolean,
|
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
|
-
});
|