@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 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.2",
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 src/ tests/",
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
- });