@rebind.gg/mcp-server 0.1.6 → 0.2.0
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 +24 -8
- package/package.json +9 -6
- package/src/bookmarklet.js +182 -0
- package/src/index.ts +1237 -31
- package/src/keys.test.ts +15 -0
- package/src/keys.ts +31 -0
- package/src/llm.ts +157 -0
- package/src/prompts.ts +101 -0
- package/src/rebind.d.luau +95 -30
- package/src/session.ts +2 -1
package/src/keys.test.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
});
|
package/src/keys.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Shared key-combo normalization for every surface that sends model-authored
|
|
2
|
+
// chords to the relay (the MCP server's `key` tool and the bench executor).
|
|
3
|
+
//
|
|
4
|
+
// Models emit "LCmd"/"Cmd"/"Command"/"⌘" for the macOS Command key, but the SDK
|
|
5
|
+
// key parser only knows LMeta/Meta/Cmd (not the L-prefixed "LCmd"). Without
|
|
6
|
+
// normalization the modifier is silently dropped and the bare letter types.
|
|
7
|
+
|
|
8
|
+
const META = new Set([
|
|
9
|
+
"LCMD",
|
|
10
|
+
"CMD",
|
|
11
|
+
"COMMAND",
|
|
12
|
+
"⌘",
|
|
13
|
+
"LMETA",
|
|
14
|
+
"META",
|
|
15
|
+
"SUPER",
|
|
16
|
+
"LWIN",
|
|
17
|
+
"WIN",
|
|
18
|
+
]);
|
|
19
|
+
const RMETA = new Set(["RCMD", "RCOMMAND", "RMETA", "RWIN"]);
|
|
20
|
+
|
|
21
|
+
export function normalizeCombo(combo: string): string {
|
|
22
|
+
return combo
|
|
23
|
+
.split("+")
|
|
24
|
+
.map((t) => {
|
|
25
|
+
const u = t.trim().toUpperCase();
|
|
26
|
+
if (META.has(u)) return "LMeta";
|
|
27
|
+
if (RMETA.has(u)) return "RMeta";
|
|
28
|
+
return t.trim();
|
|
29
|
+
})
|
|
30
|
+
.join("+");
|
|
31
|
+
}
|
package/src/llm.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Minimal OpenRouter tool-calling step. LEAF module: node fs/fetch only, ZERO
|
|
2
|
+
// imports from the MCP SDK / image pipeline / session — the bench re-exports
|
|
3
|
+
// llmStep from here and must never transitively pull those in (see the covenant
|
|
4
|
+
// in ./prompts.ts). Both the bench loop and the server's delegate_task /
|
|
5
|
+
// rebind_versus tools call it.
|
|
6
|
+
//
|
|
7
|
+
// Deliberately NOT @rebind/llm: that transport force-adds a web_search tool and
|
|
8
|
+
// drops usage. Here we want a clean, controlled step — tools in, the raw
|
|
9
|
+
// assistant message + parsed tool calls + exact usage out. Temperature 0.
|
|
10
|
+
|
|
11
|
+
const URL =
|
|
12
|
+
process.env.OPENROUTER_URL || "https://openrouter.ai/api/v1/chat/completions";
|
|
13
|
+
// hard ceiling on one model round-trip. fetch has no built-in timeout, so a
|
|
14
|
+
// wedged connection would hang forever; abort so the caller fails and moves on.
|
|
15
|
+
const LLM_TIMEOUT_MS = Number(process.env.BENCH_LLM_TIMEOUT || 60000);
|
|
16
|
+
const MAX_RETRIES = Number(process.env.OPENROUTER_MAX_RETRIES || 3);
|
|
17
|
+
|
|
18
|
+
// key resolution: env var first, then the repo root's decrypted .env.production
|
|
19
|
+
// so `just bench-demo` (and a bare server) work from a fresh shell without
|
|
20
|
+
// exporting anything. Resolved LAZILY — importing this module must have no fs
|
|
21
|
+
// side effect (the production server imports it just to register two tools).
|
|
22
|
+
let KEY: string | undefined;
|
|
23
|
+
function key(): string {
|
|
24
|
+
if (KEY !== undefined) return KEY;
|
|
25
|
+
KEY = process.env.OPENROUTER_API_KEY || keyFromRootEnv();
|
|
26
|
+
return KEY;
|
|
27
|
+
}
|
|
28
|
+
function keyFromRootEnv(): string {
|
|
29
|
+
try {
|
|
30
|
+
const txt = require("node:fs").readFileSync(
|
|
31
|
+
new globalThis.URL("../../../.env.production", import.meta.url),
|
|
32
|
+
"utf8",
|
|
33
|
+
) as string;
|
|
34
|
+
return /^OPENROUTER_API_KEY=(.*)$/m.exec(txt)?.[1]?.trim() ?? "";
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Tool {
|
|
41
|
+
type: "function";
|
|
42
|
+
function: {
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
parameters: Record<string, unknown>;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// OpenAI/OpenRouter message shape. `content` is a string or multimodal parts.
|
|
50
|
+
export type Msg = Record<string, unknown>;
|
|
51
|
+
|
|
52
|
+
export interface ParsedCall {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
args: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Step {
|
|
59
|
+
/** raw assistant message, pushed back verbatim so tool_call_ids line up */
|
|
60
|
+
message: Msg;
|
|
61
|
+
toolCalls: ParsedCall[];
|
|
62
|
+
/** OpenRouter-reported usage for THIS request; cost is exact USD credits. */
|
|
63
|
+
usage?: { prompt: number; completion: number; cost: number };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const safeParse = (s: string): Record<string, unknown> => {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(s || "{}");
|
|
69
|
+
} catch {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
75
|
+
|
|
76
|
+
export async function llmStep(
|
|
77
|
+
messages: Msg[],
|
|
78
|
+
tools: Tool[],
|
|
79
|
+
model: string,
|
|
80
|
+
): Promise<Step> {
|
|
81
|
+
if (!key()) throw new Error("set OPENROUTER_API_KEY (Bun auto-loads .env)");
|
|
82
|
+
|
|
83
|
+
// usage.include makes OpenRouter return exact token counts + USD cost per
|
|
84
|
+
// request — no client-side pricing table to drift. Omit `tools` entirely when
|
|
85
|
+
// empty: strict providers 400 on an empty tools array.
|
|
86
|
+
const body = JSON.stringify({
|
|
87
|
+
model,
|
|
88
|
+
messages,
|
|
89
|
+
...(tools.length ? { tools } : {}),
|
|
90
|
+
max_tokens: 4096,
|
|
91
|
+
temperature: 0,
|
|
92
|
+
usage: { include: true },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
let res: Response;
|
|
96
|
+
for (let attempt = 0; ; attempt++) {
|
|
97
|
+
try {
|
|
98
|
+
res = await fetch(URL, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
Authorization: `Bearer ${key()}`,
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
"HTTP-Referer": "https://rebind.gg",
|
|
104
|
+
"X-Title": "Rebind Agent Bench",
|
|
105
|
+
},
|
|
106
|
+
body,
|
|
107
|
+
signal: AbortSignal.timeout(LLM_TIMEOUT_MS),
|
|
108
|
+
});
|
|
109
|
+
} catch (e) {
|
|
110
|
+
if (e instanceof Error && e.name === "TimeoutError") {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`openrouter request timed out after ${LLM_TIMEOUT_MS}ms`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
if (res.ok) break;
|
|
118
|
+
// 429 (rate limit) and 5xx are transient — parallel fan-out reliably trips
|
|
119
|
+
// 429 on the flash tiers. Back off and retry; surface anything else at once.
|
|
120
|
+
if ((res.status === 429 || res.status >= 500) && attempt < MAX_RETRIES) {
|
|
121
|
+
await sleep(400 * 2 ** attempt);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`openrouter ${res.status}: ${await res.text()}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const j = (await res.json()) as {
|
|
128
|
+
choices?: {
|
|
129
|
+
message?: {
|
|
130
|
+
content?: string | null;
|
|
131
|
+
tool_calls?: {
|
|
132
|
+
id: string;
|
|
133
|
+
function: { name: string; arguments: string };
|
|
134
|
+
}[];
|
|
135
|
+
};
|
|
136
|
+
}[];
|
|
137
|
+
usage?: {
|
|
138
|
+
prompt_tokens?: number;
|
|
139
|
+
completion_tokens?: number;
|
|
140
|
+
cost?: number;
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
const m = j.choices?.[0]?.message ?? { content: null };
|
|
144
|
+
const toolCalls: ParsedCall[] = (m.tool_calls ?? []).map((t) => ({
|
|
145
|
+
id: t.id,
|
|
146
|
+
name: t.function.name,
|
|
147
|
+
args: safeParse(t.function.arguments),
|
|
148
|
+
}));
|
|
149
|
+
const usage = j.usage
|
|
150
|
+
? {
|
|
151
|
+
prompt: j.usage.prompt_tokens ?? 0,
|
|
152
|
+
completion: j.usage.completion_tokens ?? 0,
|
|
153
|
+
cost: j.usage.cost ?? 0,
|
|
154
|
+
}
|
|
155
|
+
: undefined;
|
|
156
|
+
return { message: m as Msg, toolCalls, usage };
|
|
157
|
+
}
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Canonical agent-facing copy, shared by the Rebind MCP server (this package)
|
|
2
|
+
// and the agent bench (packages/mcp-server/bench). ONE source of truth for the tool
|
|
3
|
+
// descriptions, efficiency doctrine, and machine-probe commands that both
|
|
4
|
+
// surfaces show the model, so they can never drift.
|
|
5
|
+
//
|
|
6
|
+
// PURE strings + string builders — ZERO runtime imports. That is deliberate:
|
|
7
|
+
// the colocated bench (../bench) imports this by relative path and must NOT pull
|
|
8
|
+
// in the MCP SDK, image pipeline, or calibration that live elsewhere in src/.
|
|
9
|
+
//
|
|
10
|
+
// What is NOT here, by design: the screenshot/click descriptions. The server's
|
|
11
|
+
// click is verified + calibrated; the bench's is a primitive hidMoveTo+press.
|
|
12
|
+
// A shared description would make one of them lie, so each keeps its own.
|
|
13
|
+
|
|
14
|
+
export type OS = "macOS" | "Linux" | "Windows";
|
|
15
|
+
|
|
16
|
+
// --- machine probes (run relay-side through System.Exec) ---
|
|
17
|
+
|
|
18
|
+
/** CLIs worth surfacing to the model; prints the ones present, space-separated. */
|
|
19
|
+
export const PROBE_CLIS_CMD = `for c in python3 osascript jq curl sed awk sort uniq grep wc open unzip tar; do command -v $c >/dev/null 2>&1 && printf "%s " $c; done`;
|
|
20
|
+
|
|
21
|
+
/** List up to 40 installed GUI apps, comma-separated, for the given OS. */
|
|
22
|
+
export function probeAppsCmd(os: OS): string {
|
|
23
|
+
return os === "macOS"
|
|
24
|
+
? `ls /Applications /System/Applications 2>/dev/null | grep '\\.app$' | sed 's/\\.app$//' | sort -u | head -40 | tr '\\n' ','`
|
|
25
|
+
: `ls /usr/share/applications 2>/dev/null | sed 's/\\.desktop$//' | sort -u | head -40 | tr '\\n' ','`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** How to launch a GUI app by name via run_lua, per OS. */
|
|
29
|
+
export function openAppHow(os: OS): string {
|
|
30
|
+
if (os === "macOS") return 'System.ExecDetached("open", {"-a", "<Name>"})';
|
|
31
|
+
if (os === "Linux") return 'System.ExecDetached("<name>", {})';
|
|
32
|
+
return 'System.ExecDetached("cmd", {"/c","start","","<name>"})';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The shortcut modifier per OS — the #1 fix for wrong-key shortcut failures.
|
|
36
|
+
* Returned as a fragment the caller wraps ("...shortcuts use <this>."). */
|
|
37
|
+
export function modifierNote(os: OS): string {
|
|
38
|
+
return os === "macOS"
|
|
39
|
+
? "LMeta (Command): LMeta+A select all, LMeta+C copy, LMeta+V paste, LMeta+L focus the browser address bar"
|
|
40
|
+
: "LCtrl: LCtrl+A select all, LCtrl+C copy, LCtrl+V paste, LCtrl+L focus the address bar";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- efficiency doctrine ---
|
|
44
|
+
|
|
45
|
+
/** Tool-preference ladder. run_lua only appears when the surface/arm has it. */
|
|
46
|
+
export function toolPriority(hasLua: boolean): string {
|
|
47
|
+
return `Tool priority, most to least preferred: ${hasLua ? "script (run_lua) > " : ""}keyboard > mouse > screenshot. If an application accepts keyboard input for the job — typed digits and operators, shortcuts, menu accelerators, type-to-select — use the keyboard: it is deterministic and needs no coordinates. Use the mouse only when no keyboard path exists, and screenshots only when you genuinely need to see state.`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const BATCHING_DOCTRINE =
|
|
51
|
+
"When you must drive the GUI: do not verify after every action. Batch predictable action sequences — keystrokes especially — and verify once at the end via values (file contents, clipboard, command output) rather than screenshots.";
|
|
52
|
+
|
|
53
|
+
/** GUI traps that reliably strand an agent, per OS. Returned as one line or "". */
|
|
54
|
+
export function guiTips(os: OS): string {
|
|
55
|
+
if (os === "macOS")
|
|
56
|
+
return (
|
|
57
|
+
"GUI traps to avoid on macOS: (1) In Finder, Return/Enter RENAMES the selected item — it does NOT open it; open it with a DOUBLE-CLICK, or LMeta+O, or LMeta+Down. " +
|
|
58
|
+
'(2) To open a URL, file, or app, prefer run_lua `System.ExecDetached("open", {"<url-or-path>"})` (or `{"-a","<App>"}`) over clicking the app\'s chrome — it is one call and cannot get lost. ' +
|
|
59
|
+
"(3) If an unexpected window or dialog blocks you, handle it before continuing — never loop on it: dismiss a dialog with Escape, cycle between windows of the front app with LMeta+Grave (backtick), and close an unwanted window or tab with LMeta+W. " +
|
|
60
|
+
'(4) To READ the current web page without a screenshot, script the browser: `local app = App.Front().name` (App.Front returns a table — use .name), then run_lua `System.Exec([[osascript -e \'tell application "<Name>" to get title of active tab of front window\']])` for the page title (swap `title` for `URL` to read the address). The tab title IS the page\'s heading for reporting purposes — report it as-is; do NOT try AppleScript `execute javascript` to scrape the DOM, it is disabled by default in every browser and the quoting will eat your remaining steps. For full page CONTENT (lists, articles, multiple pages), fetch the URL directly instead: `System.Exec("curl -s <url>")` and parse the HTML. ' +
|
|
61
|
+
"(5) To DRIVE a web form (fill fields, click submit) you must work by SIGHT — call position_window on the browser FIRST so the whole form is visible and click coordinates are stable, then: screenshot, click a field, type its value, and after the last field click the submit button; screenshot to confirm each step landed. Do NOT use osascript `execute javascript` to fill or submit forms — it is disabled by default in browsers and the nested shell quoting fails."
|
|
62
|
+
);
|
|
63
|
+
return (
|
|
64
|
+
"GUI traps: to open a URL/file/app, prefer run_lua `System.ExecDetached` over clicking the app's UI. Open a file by double-clicking it. " +
|
|
65
|
+
"If an unexpected window/dialog blocks you, dismiss it (Escape) or close it (LCtrl+W) before continuing — never loop on it."
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const DETERMINISTIC_WORK_DOCTRINE =
|
|
70
|
+
"run_lua is your PRIMARY tool — reach for it FIRST and use it aggressively. It exposes the ENTIRE machine SDK (every namespace below), so almost anything a task needs — file operations, text processing, arithmetic, clipboard, launching and positioning apps, shell commands, reading the screen — is ONE run_lua call, never a click loop. Do not operate calculators, browsers, or file managers through the GUI when a script can do the work. Fall back to screenshot/click ONLY for state that exists purely on screen and cannot be scripted.";
|
|
71
|
+
|
|
72
|
+
// --- tool descriptions (shared only where the underlying behavior is identical
|
|
73
|
+
// across both surfaces: run_lua, lua_docs, and key all hit the same relay) ---
|
|
74
|
+
|
|
75
|
+
export const KEY_DESC =
|
|
76
|
+
"Press a key or combo. Modifiers: LCtrl, LShift, LAlt, LMeta (Command on macOS — use LMeta for macOS shortcuts, LCtrl on Windows/Linux). " +
|
|
77
|
+
'Non-modifier keys are NAMES, never glyphs: arrows are "Up"/"Down"/"Left"/"Right" (not ↑↓←→); also "Enter", "Tab", "Escape", "Space", "Backspace", "Delete", "Home", "End", "PageUp", "PageDown", "F1".."F12". ' +
|
|
78
|
+
'Punctuation is spelled out: "Equal", "Minus", "Comma", "Period", "Slash", "Semicolon", "LeftBracket", "RightBracket", "Grave", "Backslash". ' +
|
|
79
|
+
'Combos join with "+", e.g. "LCtrl+L" (focus address bar), "LMeta+A" (select all), "LAlt+Tab". ' +
|
|
80
|
+
'Zoom in is "LMeta+Equal" / "LCtrl+Equal" (never "++" — the extra + splits into empty keys); zoom out uses "Minus". Escape closes dialogs; Space / PageDown scroll.';
|
|
81
|
+
|
|
82
|
+
export const RUN_LUA_DESC =
|
|
83
|
+
"Run a Luau script on the connected machine through Rebind's engine and return its `return` value as JSON. " +
|
|
84
|
+
"This is your PRIMARY tool and FIRST choice — it exposes the ENTIRE machine SDK (every namespace listed below; call lua_docs for the full function list) and is far faster, cheaper, and more reliable than the screenshot-and-click loop, which it sidesteps entirely. Default to it for ANYTHING that can be scripted. " +
|
|
85
|
+
"The chunk runs ONCE and must end with `return <value>`: script hooks (OnDown/OnTick) never fire here, and coroutine-only calls " +
|
|
86
|
+
'(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`. ' +
|
|
87
|
+
"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 " +
|
|
88
|
+
"(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. " +
|
|
89
|
+
"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. " +
|
|
90
|
+
'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 "~". ' +
|
|
91
|
+
"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. " +
|
|
92
|
+
"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. " +
|
|
93
|
+
'System.Exec takes ONE shell-command STRING — System.Exec("ls /tmp") — and its optional second parameter is {timeout, cwd}, NOT an argument array (an args table there is silently ignored and the bare command runs); only System.ExecDetached takes an args array. ' +
|
|
94
|
+
'There is NO io.* or os.* — file work is File.*, shell work is System.Exec. App has Front/IsFront/IsRunning/Activate/Hide/Quit only — there is no App.List (installed apps are listed in your instructions). HID.Press takes ONE key; fire chords with HID.Combo("LMeta+C"). ' +
|
|
95
|
+
"All namespaces: System, HID, Input, Screen, Window, App, Process, File, Clipboard, Net, Env, Log, UI, Bind, Timer, Math, JSON, Pipe, Audio, Dialog, Script, Regex, Config, Hash, Codec, Macro, Registry. " +
|
|
96
|
+
'Example (move all .log files into d/logs and report the count): `File.MkDir("d/logs"); local n=0; for _,f in ipairs(File.List("d")) do if f:match("%.log$") then File.Move("d/"..f, "d/logs/"..f); n=n+1 end end; return n`. ' +
|
|
97
|
+
"Call `lua_docs` for exact signatures BEFORE writing nontrivial scripts — do not guess APIs.";
|
|
98
|
+
|
|
99
|
+
export const LUA_DOCS_DESC =
|
|
100
|
+
"Return the full Luau SDK type definitions for run_lua: every namespace, function signature, and type (~5k tokens, one call is enough). " +
|
|
101
|
+
"Read this BEFORE writing any nontrivial run_lua script instead of guessing APIs.";
|
package/src/rebind.d.luau
CHANGED
|
@@ -183,51 +183,38 @@ export type MacroAction = MacroMoveAction | MacroExplicitAction
|
|
|
183
183
|
|
|
184
184
|
-- ui widget option types
|
|
185
185
|
|
|
186
|
-
export type
|
|
186
|
+
export type WidgetOpts = {
|
|
187
187
|
label: string?,
|
|
188
|
+
tooltip: string?,
|
|
189
|
+
group: string?,
|
|
190
|
+
tab: string?,
|
|
191
|
+
showIf: string?,
|
|
188
192
|
}
|
|
189
193
|
|
|
190
|
-
export type
|
|
191
|
-
|
|
194
|
+
export type ToggleOpts = WidgetOpts
|
|
195
|
+
|
|
196
|
+
export type SliderOpts = WidgetOpts & {
|
|
192
197
|
min: number?,
|
|
193
198
|
max: number?,
|
|
194
199
|
step: number?,
|
|
195
200
|
suffix: string?,
|
|
196
|
-
tooltip: string?,
|
|
197
|
-
group: string?,
|
|
198
|
-
tab: string?,
|
|
199
|
-
showIf: string?,
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
export type KeybindOpts =
|
|
203
|
-
label: string?,
|
|
204
|
-
}
|
|
203
|
+
export type KeybindOpts = WidgetOpts
|
|
205
204
|
|
|
206
|
-
export type SelectOpts =
|
|
207
|
-
label: string?,
|
|
208
|
-
}
|
|
205
|
+
export type SelectOpts = WidgetOpts
|
|
209
206
|
|
|
210
|
-
export type TextOpts = {
|
|
211
|
-
label: string?,
|
|
207
|
+
export type TextOpts = WidgetOpts & {
|
|
212
208
|
placeholder: string?,
|
|
213
209
|
maxLength: number?,
|
|
214
|
-
tooltip: string?,
|
|
215
|
-
group: string?,
|
|
216
|
-
tab: string?,
|
|
217
|
-
showIf: string?,
|
|
218
210
|
}
|
|
219
211
|
|
|
220
|
-
export type ColorOpts =
|
|
221
|
-
label: string?,
|
|
222
|
-
tooltip: string?,
|
|
223
|
-
group: string?,
|
|
224
|
-
tab: string?,
|
|
225
|
-
showIf: string?,
|
|
226
|
-
}
|
|
212
|
+
export type ColorOpts = WidgetOpts
|
|
227
213
|
|
|
228
214
|
export type RecordOpts = {
|
|
229
215
|
ignore_mouse: boolean?,
|
|
230
216
|
ignore_keyboard: boolean?,
|
|
217
|
+
-- default "normal"
|
|
231
218
|
precision: string?,
|
|
232
219
|
}
|
|
233
220
|
|
|
@@ -251,14 +238,20 @@ export type RebindInfo = {
|
|
|
251
238
|
runtime_uuid: string,
|
|
252
239
|
id: string?,
|
|
253
240
|
instance: string,
|
|
241
|
+
-- host operating system: "windows" | "macos" | "linux"
|
|
242
|
+
platform: string,
|
|
254
243
|
}
|
|
255
244
|
|
|
256
245
|
-- global functions
|
|
257
246
|
|
|
258
247
|
declare function Run(fn: () -> ()): TaskHandle
|
|
259
248
|
-- RunAsync was removed. Use Run() directly.
|
|
249
|
+
-- coroutine-only: Sleep must be called inside Run()/a scheduled coroutine.
|
|
250
|
+
-- Calling it from a plain hook body or a one-shot exec chunk errors at runtime.
|
|
260
251
|
declare function Sleep(ms: number): ()
|
|
261
252
|
declare function After(ms: number, fn: () -> ()): TaskHandle
|
|
253
|
+
-- wraps fn so it runs on the coroutine scheduler; when auto-spawned the call
|
|
254
|
+
-- returns false instead of fn's results.
|
|
262
255
|
declare function Async(fn: (...any) -> ...any): (...any) -> ...any
|
|
263
256
|
declare function print(...: any): ()
|
|
264
257
|
declare function require(module: string): any
|
|
@@ -270,7 +263,9 @@ declare _REBIND: RebindInfo
|
|
|
270
263
|
-- system namespace
|
|
271
264
|
|
|
272
265
|
export type ExecOptions = {
|
|
266
|
+
-- milliseconds; default 5000. The process is killed on expiry and Exec errors.
|
|
273
267
|
timeout: number?,
|
|
268
|
+
-- working directory for the command; default: the host process's cwd.
|
|
274
269
|
cwd: string?,
|
|
275
270
|
}
|
|
276
271
|
|
|
@@ -289,8 +284,18 @@ declare System: {
|
|
|
289
284
|
Mouse: () -> (number, number),
|
|
290
285
|
Screen: () -> (number, number),
|
|
291
286
|
Window: () -> WindowInfo,
|
|
287
|
+
-- runs cmd through the shell (sh -c / cmd.exe), output capped at 64KB per
|
|
288
|
+
-- stream. Requires the 'exec' permission. Return shape {exit,stdout,stderr}
|
|
289
|
+
-- unchanged. Inside Run()/a coroutine it runs async off the tick thread
|
|
290
|
+
-- (yields, resumes with the result on a later tick). From a plain hook body
|
|
291
|
+
-- or one-shot chunk it falls back to bounded blocking (deprecated; warns
|
|
292
|
+
-- once, blocks the tick thread — wrap it in Run()).
|
|
292
293
|
Exec: (cmd: string, options: ExecOptions?) -> ExecResult,
|
|
294
|
+
-- fire-and-forget spawn (no shell, argv array); returns the pid.
|
|
293
295
|
ExecDetached: (cmd: string, args: { string }?, options: ExecDetachedOptions?) -> number,
|
|
296
|
+
-- types text through the host OS (Unicode aware), never through the HID
|
|
297
|
+
-- transport. raises when the OS refuses the injection.
|
|
298
|
+
TypeText: (text: string) -> (),
|
|
294
299
|
}
|
|
295
300
|
|
|
296
301
|
-- hid namespace
|
|
@@ -298,6 +303,8 @@ declare System: {
|
|
|
298
303
|
declare HID: {
|
|
299
304
|
Down: (code: string) -> (),
|
|
300
305
|
Up: (code: string) -> (),
|
|
306
|
+
-- coroutine-only (sleeps for holdMs): call inside Run(). From a plain
|
|
307
|
+
-- chunk use Down+Up or Combo instead.
|
|
301
308
|
Press: (code: string, holdMs: number?) -> (),
|
|
302
309
|
-- fire a chord in one synchronous call (press L→R, release R→L). no sleep,
|
|
303
310
|
-- so callable directly from a Bind action / OnDown. modifier tokens accept
|
|
@@ -361,6 +368,8 @@ declare Timer: {
|
|
|
361
368
|
-- macro namespace
|
|
362
369
|
|
|
363
370
|
declare Macro: {
|
|
371
|
+
-- mode: "parallel" (default, plays alongside running macros) | "replace"
|
|
372
|
+
-- (stops all running macros first).
|
|
364
373
|
Play: (macro: { MacroAction }, speed: number?, mode: string?) -> MacroHandle,
|
|
365
374
|
StopAll: () -> (),
|
|
366
375
|
Record: (options: RecordOpts?) -> (),
|
|
@@ -378,6 +387,7 @@ declare Math: {
|
|
|
378
387
|
Spline: (macro: { MacroAction }, tension: number) -> { MacroAction },
|
|
379
388
|
Resample: (macro: { MacroAction }, intervalMs: number) -> { MacroAction },
|
|
380
389
|
TimeComp: (macro: { MacroAction }, targetMs: number) -> { MacroAction },
|
|
390
|
+
-- mode: "linear" | anything else = positional curve (default).
|
|
381
391
|
Interpolate: (macro: { MacroAction }, intervalMs: number, mode: string?) -> { MacroAction },
|
|
382
392
|
}
|
|
383
393
|
|
|
@@ -426,10 +436,52 @@ export type MonitorEntry = {
|
|
|
426
436
|
primary: boolean,
|
|
427
437
|
}
|
|
428
438
|
|
|
439
|
+
export type CaptureOptions = {
|
|
440
|
+
-- 1-based display index; default: the display under the cursor.
|
|
441
|
+
display: number?,
|
|
442
|
+
-- display-local logical coords; default: the whole display.
|
|
443
|
+
region: { x: number, y: number, w: number, h: number }?,
|
|
444
|
+
-- downscale so the long edge is at most this; default and 0: native.
|
|
445
|
+
max_edge: number?,
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export type CaptureResult = {
|
|
449
|
+
image: string, -- base64 PNG
|
|
450
|
+
width: number,
|
|
451
|
+
height: number,
|
|
452
|
+
display: MonitorEntry,
|
|
453
|
+
cursor: { x: number, y: number },
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export type FindImageOptions = {
|
|
457
|
+
display: number?,
|
|
458
|
+
region: { x: number, y: number, w: number, h: number }?,
|
|
459
|
+
-- 0.0 exact .. 1.0, per-channel fraction of 255 (same scale as SearchForColor).
|
|
460
|
+
tolerance: number?,
|
|
461
|
+
-- ratios to resample the needle by before scanning, tried in order; default { 1 }.
|
|
462
|
+
scales: { number }?,
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export type ImageMatch = {
|
|
466
|
+
x: number,
|
|
467
|
+
y: number,
|
|
468
|
+
width: number,
|
|
469
|
+
height: number,
|
|
470
|
+
}
|
|
471
|
+
|
|
429
472
|
declare Screen: {
|
|
430
473
|
GetPixelColor: (x: number?, y: number?) -> string,
|
|
474
|
+
-- reads the published frame. regions up to 200k physical pixels answer on the
|
|
475
|
+
-- calling thread; larger ones yield inside Run() and are refused outside it.
|
|
431
476
|
SearchForColor: (region: {number}, color: string, tolerance: number?) -> PixelMatch?,
|
|
432
477
|
List: () -> { MonitorEntry },
|
|
478
|
+
-- yields inside Run(); refused outside a coroutine.
|
|
479
|
+
Capture: (options: CaptureOptions?) -> CaptureResult,
|
|
480
|
+
-- needle is a PNG path relative to the script directory. yields inside Run(); refused outside a coroutine.
|
|
481
|
+
FindImage: (needle: string, options: FindImageOptions?) -> ImageMatch?,
|
|
482
|
+
-- usable desktop of the display containing the point (cursor when omitted).
|
|
483
|
+
-- Windows only; raises elsewhere.
|
|
484
|
+
WorkArea: (x: number?, y: number?) -> WindowPos,
|
|
433
485
|
}
|
|
434
486
|
|
|
435
487
|
-- window namespace
|
|
@@ -455,6 +507,10 @@ declare Window: {
|
|
|
455
507
|
Kill: (handle: number?) -> (),
|
|
456
508
|
IsVisible: (handle: number) -> boolean,
|
|
457
509
|
IsActive: (handle: number?) -> boolean,
|
|
510
|
+
GetForeground: () -> number?,
|
|
511
|
+
FromPoint: (x: number, y: number) -> number?,
|
|
512
|
+
-- "minimized" | "maximized" | "normal". Windows only; raises elsewhere.
|
|
513
|
+
GetState: (handle: number?) -> string,
|
|
458
514
|
Wait: (title: string, timeout: number?) -> number?,
|
|
459
515
|
WaitActive: (title: string, timeout: number?) -> boolean,
|
|
460
516
|
WaitClose: (title: string, timeout: number?) -> boolean,
|
|
@@ -510,16 +566,19 @@ declare File: {
|
|
|
510
566
|
Append: (path: string, content: string) -> (),
|
|
511
567
|
Exists: (path: string) -> boolean,
|
|
512
568
|
Delete: (path: string) -> boolean,
|
|
569
|
+
-- nil path lists the script's own directory. Paths are relative to the
|
|
570
|
+
-- script dir (the File.* jail); absolute paths are rejected.
|
|
513
571
|
List: (path: string?) -> { string },
|
|
514
572
|
MkDir: (path: string) -> boolean,
|
|
515
|
-
|
|
573
|
+
-- recursive; returns true even when the directory did not exist.
|
|
574
|
+
RmDir: (path: string) -> boolean,
|
|
575
|
+
Copy: (src: string, dst: string) -> (),
|
|
576
|
+
Move: (src: string, dst: string) -> (),
|
|
516
577
|
ReadJSON: (path: string) -> any,
|
|
517
578
|
WriteJSON: (path: string, value: any) -> (),
|
|
518
579
|
GetScriptDir: () -> string,
|
|
519
580
|
GetSize: (path: string) -> number,
|
|
520
581
|
GetTime: (path: string) -> number,
|
|
521
|
-
Copy: (src: string, dst: string) -> (),
|
|
522
|
-
Move: (src: string, dst: string) -> (),
|
|
523
582
|
IsDir: (path: string) -> boolean,
|
|
524
583
|
IsFile: (path: string) -> boolean,
|
|
525
584
|
}
|
|
@@ -566,6 +625,8 @@ export type DialogFileOptions = {
|
|
|
566
625
|
filters: { DialogFileFilter }?,
|
|
567
626
|
}
|
|
568
627
|
|
|
628
|
+
-- every Dialog call is coroutine-only: it yields until the dialog closes and
|
|
629
|
+
-- errors outside Run(). the file and folder pickers return nil on cancel.
|
|
569
630
|
declare Dialog: {
|
|
570
631
|
Message: (text: string, options: DialogMessageOptions?) -> (),
|
|
571
632
|
Confirm: (text: string, options: DialogMessageOptions?) -> boolean,
|
|
@@ -694,5 +755,9 @@ declare Registry: {
|
|
|
694
755
|
-- declare function OnMove(dx: number, dy: number): boolean? -- return value only used with mouse_block=true (hardware mode only)
|
|
695
756
|
-- declare function OnScroll(delta: number): boolean?
|
|
696
757
|
-- declare function OnScrollH(delta: number): boolean?
|
|
697
|
-
-- declare function
|
|
758
|
+
-- declare function OnInputDown(key: string): ()
|
|
759
|
+
-- declare function OnInputUp(key: string, durationMs: number): ()
|
|
760
|
+
-- declare function OnInputMove(dx: number, dy: number): ()
|
|
761
|
+
-- declare function OnInputScroll(delta: number): ()
|
|
762
|
+
-- declare function OnError(message: string): ()
|
|
698
763
|
-- declare function OnReload(): ()
|
package/src/session.ts
CHANGED
|
@@ -77,7 +77,8 @@ export class Session {
|
|
|
77
77
|
}
|
|
78
78
|
if (!inImageBounds(this.lastShot, ix, iy)) {
|
|
79
79
|
throw new Error(
|
|
80
|
-
`(${ix}, ${iy}) is outside the last screenshot
|
|
80
|
+
`(${ix}, ${iy}) is outside the last screenshot, which is ${this.lastShot.imageWidth}x${this.lastShot.imageHeight} pixels, 0-indexed from the top-left. ` +
|
|
81
|
+
`Emit pixel coordinates within it — if you emitted 0-1000-normalized coordinates, multiply by the image dimensions instead — or take a fresh screenshot.`,
|
|
81
82
|
);
|
|
82
83
|
}
|
|
83
84
|
return mapToScreen(this.lastShot, ix, iy);
|