@rebind.gg/mcp-server 0.2.4 → 0.3.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 +10 -4
- package/package.json +1 -1
- package/src/index.ts +83 -21
- package/src/prompts.ts +4 -0
- package/src/rebind.d.luau +56 -2
package/README.md
CHANGED
|
@@ -5,7 +5,9 @@ Stdio MCP server that turns [Rebind](https://rebind.gg) into a computer-use tool
|
|
|
5
5
|
**Requirements**
|
|
6
6
|
|
|
7
7
|
- [Bun](https://bun.sh) >= 1.1 (the server runs via `bunx`)
|
|
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`)
|
|
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
|
+
|
|
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.
|
|
9
11
|
|
|
10
12
|
## Install
|
|
11
13
|
|
|
@@ -41,7 +43,7 @@ Any MCP client takes the stdio block directly:
|
|
|
41
43
|
bunx @rebind.gg/mcp-server --selftest
|
|
42
44
|
```
|
|
43
45
|
|
|
44
|
-
Connects, takes a capture, and reports the display
|
|
46
|
+
Connects, waits up to a minute for the approval prompt on a first run, takes a capture, and reports the display. It is the fastest way to catch the #1 first-run failure (relay not loaded).
|
|
45
47
|
|
|
46
48
|
## Tools
|
|
47
49
|
|
|
@@ -55,7 +57,7 @@ Connects, takes a capture, and reports the display — the fastest way to catch
|
|
|
55
57
|
| `type` | Type text into the focused element. |
|
|
56
58
|
| `key` | Press a key or combo, e.g. `Enter`, `LCtrl+L`, `LAlt+Tab`. |
|
|
57
59
|
| `scroll` | Scroll at the cursor. Positive scrolls down, negative up. `unit` is `wheel` (default, fine wheel clicks) or `page` (one PageDown or PageUp per unit, capped at 10). |
|
|
58
|
-
| `read_page` | Extract the focused browser's current page as structured text (URL, forms, interactive elements, plain text) via an address-bar bookmarklet; the full dump, including cleaned HTML, lands in the clipboard. The result is read back through AppleScript (`URL of active tab of front window`), so the tool is registered only when the relay host is macOS, and the front browser must answer that query. Safari is detected but blocks the bookmarklet path. |
|
|
60
|
+
| `read_page` | Extract the focused browser's current page as structured text (URL, forms, interactive elements, plain text) via an address-bar bookmarklet; the full dump, including cleaned HTML, lands in the clipboard. The result is read back through AppleScript (`URL of active tab of front window`), so the tool is registered only when the relay host is macOS and **Allow custom Luau** is on, and the front browser must answer that query. Safari is detected but blocks the bookmarklet path. |
|
|
59
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. |
|
|
60
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). |
|
|
61
63
|
| `lua_docs` | Full Luau SDK type definitions for `run_lua`. |
|
|
@@ -84,7 +86,11 @@ Connects, takes a capture, and reports the display — the fastest way to catch
|
|
|
84
86
|
|
|
85
87
|
**One coordinate space.** All results are reported in the pixel space of the last screenshot — the same space the model emits — so no rescaling math ever falls on the model.
|
|
86
88
|
|
|
87
|
-
**Machine-aware orientation.** At startup the server probes the connected machine through the relay (
|
|
89
|
+
**Machine-aware orientation.** At startup the server reads the OS from Remote Control's `hello` banner and, when custom Luau is allowed, probes the connected machine through the relay (available shell tools, installed apps) and serves it as the MCP `instructions` block, along with a policy: prefer `run_lua` for deterministic work, keyboard over mouse, and screenshots last. The full Luau SDK type definitions are embedded inline so the model never has to guess `run_lua` APIs (including the coroutine-safe HID path: `HID.Down`/`Up`/`MoveTo`/`Combo` work in a one-shot chunk, `HID.Press` does not). The OS is read from the relay, not `process.platform`, so the guidance is correct even when the server and the relay run on different machines.
|
|
90
|
+
|
|
91
|
+
## Save what worked
|
|
92
|
+
|
|
93
|
+
When the user wants a working sequence kept, the server's instructions tell the agent to save it as a Rebind script that runs with no agent: `rebind init <dir>`, the sequence in `main.luau` bound to a key, `[runtime] permissions` in `rebind.toml`, then `rebind check <dir>`, `rebind pack <dir>`, `rebind install <name>.rbp --allow` (`--force` to replace) and `rebind script start packages/<name>/main.luau`, on the computer running Rebind.
|
|
88
94
|
|
|
89
95
|
## Solve pipeline
|
|
90
96
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebind.gg/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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": {
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { appendFileSync } from "node:fs";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
-
import { RebindRemote } from "@rebind.gg/client-ts";
|
|
13
|
+
import { RebindRemote, ServerError } from "@rebind.gg/client-ts";
|
|
14
14
|
import { z } from "zod";
|
|
15
15
|
import pkg from "../package.json";
|
|
16
16
|
import { mapToImage } from "./coords.ts";
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
PROBE_CLIS_CMD,
|
|
30
30
|
probeAppsCmd,
|
|
31
31
|
RUN_LUA_DESC,
|
|
32
|
+
SAVE_WORK_DOCTRINE,
|
|
32
33
|
toolPriority,
|
|
33
34
|
} from "./prompts.ts";
|
|
34
35
|
import { CLICK_EPS_PX, Session } from "./session.ts";
|
|
@@ -282,11 +283,16 @@ async function v1Sdk(): Promise<string> {
|
|
|
282
283
|
// signal because this process may run on a different machine than the relay.
|
|
283
284
|
// Probes are best-effort: a locked-down remote (no exec permission) just
|
|
284
285
|
// yields a shorter card. Machine facts and general technique only.
|
|
285
|
-
// The
|
|
286
|
+
// The OS comes from the hello banner when the server reports it (Remote
|
|
287
|
+
// Control 0.3.0+), so it is right even with lua.exec off; older servers fall
|
|
288
|
+
// back to a uname probe. When the banner says lua.exec is off, the probes and
|
|
289
|
+
// read_page (both built on lua.exec) are skipped.
|
|
290
|
+
// The OS is returned alongside: tools that pick a shortcut modifier
|
|
286
291
|
// (read_page) need it, and re-probing would be a second relay round-trip.
|
|
287
292
|
async function buildInstructions(
|
|
288
293
|
client: RebindRemote,
|
|
289
|
-
|
|
294
|
+
hello: Hello,
|
|
295
|
+
): Promise<{ instructions: string; os: OS; hasLua: boolean }> {
|
|
290
296
|
const sh = async (cmd: string): Promise<string> => {
|
|
291
297
|
try {
|
|
292
298
|
const r = await client.luaExec(
|
|
@@ -297,14 +303,20 @@ async function buildInstructions(
|
|
|
297
303
|
return "";
|
|
298
304
|
}
|
|
299
305
|
};
|
|
300
|
-
const
|
|
301
|
-
const os: OS =
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
? "
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
const hasLua = hello.gates?.lua !== false;
|
|
307
|
+
const os: OS =
|
|
308
|
+
HELLO_OS[hello.platform ?? ""] ??
|
|
309
|
+
(await (async () => {
|
|
310
|
+
const uname = hasLua ? await sh("uname") : "";
|
|
311
|
+
return /darwin/i.test(uname)
|
|
312
|
+
? "macOS"
|
|
313
|
+
: /linux/i.test(uname)
|
|
314
|
+
? "Linux"
|
|
315
|
+
: "Windows";
|
|
316
|
+
})());
|
|
317
|
+
const probe = hasLua && os !== "Windows";
|
|
318
|
+
const clis = probe ? await sh(PROBE_CLIS_CMD) : "";
|
|
319
|
+
const apps = probe ? await sh(probeAppsCmd(os)) : "";
|
|
308
320
|
// Embed the full Luau SDK inline so the model never has to guess run_lua APIs
|
|
309
321
|
// or make a lua_docs round-trip. It carries the coroutine-safety notes that
|
|
310
322
|
// matter for HID: HID.Press sleeps and is coroutine-only, but HID.Down/Up/
|
|
@@ -320,8 +332,9 @@ async function buildInstructions(
|
|
|
320
332
|
apps
|
|
321
333
|
? `GUI applications installed (open via ${openAppHow(os)}): ${apps}`
|
|
322
334
|
: "",
|
|
323
|
-
`Work in as few steps as possible
|
|
324
|
-
|
|
335
|
+
`Work in as few steps as possible.${hasLua ? ` ${DETERMINISTIC_WORK_DOCTRINE}` : ""}`,
|
|
336
|
+
SAVE_WORK_DOCTRINE,
|
|
337
|
+
toolPriority(hasLua),
|
|
325
338
|
BATCHING_DOCTRINE,
|
|
326
339
|
"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.",
|
|
327
340
|
guiTips(os),
|
|
@@ -330,14 +343,34 @@ async function buildInstructions(
|
|
|
330
343
|
]
|
|
331
344
|
.filter(Boolean)
|
|
332
345
|
.join("\n");
|
|
333
|
-
return { instructions, os };
|
|
346
|
+
return { instructions, os, hasLua };
|
|
334
347
|
}
|
|
335
348
|
|
|
349
|
+
/** the fields of the Remote Control hello banner this server reads */
|
|
350
|
+
type Hello = { platform?: string; gates?: { lua?: boolean } };
|
|
351
|
+
|
|
352
|
+
const HELLO_OS: Record<string, OS> = {
|
|
353
|
+
macos: "macOS",
|
|
354
|
+
linux: "Linux",
|
|
355
|
+
windows: "Windows",
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/** the name Remote Control shows in its approval prompt and remembers */
|
|
359
|
+
const APP_NAME = "Rebind MCP server";
|
|
360
|
+
|
|
336
361
|
async function main() {
|
|
337
362
|
const url = process.env.REBIND_URL ?? "ws://127.0.0.1:19561";
|
|
338
|
-
|
|
363
|
+
// name this client on every connect, reconnects included: Remote Control
|
|
364
|
+
// approves programs by the name they declare in hello. setState fires before
|
|
365
|
+
// connect() resolves, so the hello goes out ahead of any tool call.
|
|
366
|
+
let hello: Promise<Hello> = Promise.resolve({});
|
|
367
|
+
const client: RebindRemote = new RebindRemote(url, {
|
|
339
368
|
token: process.env.REBIND_TOKEN ?? "",
|
|
340
369
|
timeoutMs: 15000,
|
|
370
|
+
onStateChange: (s) => {
|
|
371
|
+
if (s !== "connected") return;
|
|
372
|
+
hello = client.call<Hello>("hello", { app: APP_NAME }).catch(() => ({}));
|
|
373
|
+
},
|
|
341
374
|
});
|
|
342
375
|
|
|
343
376
|
// --selftest: verify the relay is reachable and capturing, then exit. Used by
|
|
@@ -346,15 +379,41 @@ async function main() {
|
|
|
346
379
|
if (process.argv.includes("--selftest")) {
|
|
347
380
|
try {
|
|
348
381
|
await client.connect();
|
|
349
|
-
|
|
382
|
+
await hello;
|
|
383
|
+
// a first connection waits on the approval prompt in Remote Control:
|
|
384
|
+
// retry for up to a minute while the person decides.
|
|
385
|
+
const deadline = Date.now() + 60_000;
|
|
386
|
+
let cap: Awaited<ReturnType<RebindRemote["screenCapture"]>>;
|
|
387
|
+
for (;;) {
|
|
388
|
+
try {
|
|
389
|
+
cap = await client.screenCapture({});
|
|
390
|
+
break;
|
|
391
|
+
} catch (e) {
|
|
392
|
+
const waiting =
|
|
393
|
+
e instanceof ServerError && e.code === "awaiting_approval";
|
|
394
|
+
if (!waiting || Date.now() + 2000 > deadline) throw e;
|
|
395
|
+
console.error(
|
|
396
|
+
`selftest: allow "${APP_NAME}" in the Remote Control prompt on that computer...`,
|
|
397
|
+
);
|
|
398
|
+
await sleep(2000);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
350
401
|
console.error(
|
|
351
402
|
`selftest OK: relay ${url}, display ${cap.display.index} ${cap.display.width}x${cap.display.height}`,
|
|
352
403
|
);
|
|
353
404
|
process.exit(0);
|
|
354
405
|
} catch (e) {
|
|
406
|
+
if (e instanceof ServerError) {
|
|
407
|
+
console.error(
|
|
408
|
+
e.code === "awaiting_approval" || e.code === "not_approved"
|
|
409
|
+
? `selftest FAILED: "${APP_NAME}" was not allowed in Remote Control's prompt on ${url}. (${e.message})`
|
|
410
|
+
: `selftest FAILED: Remote Control on ${url} refused the capture. (${e.message})`,
|
|
411
|
+
);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
355
414
|
console.error(
|
|
356
415
|
`selftest FAILED: Rebind relay not reachable on ${url}.\n` +
|
|
357
|
-
`Start Rebind and the Remote Control package (rebind install @rebind/remote-control).\n(${e})`,
|
|
416
|
+
`Start Rebind and the Remote Control package (rebind install @rebind/remote-control --allow).\n(${e})`,
|
|
358
417
|
);
|
|
359
418
|
process.exit(1);
|
|
360
419
|
}
|
|
@@ -364,7 +423,10 @@ async function main() {
|
|
|
364
423
|
|
|
365
424
|
const session = new Session(client);
|
|
366
425
|
|
|
367
|
-
const { instructions, os } = await buildInstructions(
|
|
426
|
+
const { instructions, os, hasLua } = await buildInstructions(
|
|
427
|
+
client,
|
|
428
|
+
await hello,
|
|
429
|
+
);
|
|
368
430
|
const server = new McpServer(
|
|
369
431
|
{ name: "rebind", version: pkg.version },
|
|
370
432
|
{ instructions },
|
|
@@ -1080,9 +1142,9 @@ return { ok = Window.IsActive(h), name = name }
|
|
|
1080
1142
|
);
|
|
1081
1143
|
|
|
1082
1144
|
// read_page finds the front app through App.Front and reads its result back
|
|
1083
|
-
// through AppleScript (tabUrl)
|
|
1084
|
-
//
|
|
1085
|
-
if (os === "macOS") {
|
|
1145
|
+
// through AppleScript (tabUrl), both over lua.exec: offered only on a macOS
|
|
1146
|
+
// relay with lua.exec on, so never where it fails.
|
|
1147
|
+
if (os === "macOS" && hasLua) {
|
|
1086
1148
|
register(
|
|
1087
1149
|
"read_page",
|
|
1088
1150
|
{
|
package/src/prompts.ts
CHANGED
|
@@ -69,6 +69,10 @@ export function guiTips(os: OS): string {
|
|
|
69
69
|
export const DETERMINISTIC_WORK_DOCTRINE =
|
|
70
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
71
|
|
|
72
|
+
/** MCP server only (the bench keeps its own prompt): keep what worked. */
|
|
73
|
+
export const SAVE_WORK_DOCTRINE =
|
|
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
|
+
|
|
72
76
|
// --- tool descriptions (shared only where the underlying behavior is identical
|
|
73
77
|
// across both surfaces: run_lua, lua_docs, and key all hit the same relay) ---
|
|
74
78
|
|
package/src/rebind.d.luau
CHANGED
|
@@ -490,6 +490,57 @@ export type RbBindOptions = {
|
|
|
490
490
|
device: string?,
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
+
-- rb.ai and rb.phone are Rebind Pro (a 7-day trial starts on first use).
|
|
494
|
+
-- Each rb.ai call waits for the model, so call it inside rb.run; its cost
|
|
495
|
+
-- counts against the monthly AI allowance. A script that declares
|
|
496
|
+
-- permissions needs ai, or ai:decide, ai:find or ai:read. A window option
|
|
497
|
+
-- names a window by title (or app name on macOS); the call reads the screen
|
|
498
|
+
-- pixels inside that window's rectangle, through the screen: permission.
|
|
499
|
+
|
|
500
|
+
export type AiDecideOptions = {
|
|
501
|
+
choices: { string },
|
|
502
|
+
-- send the display under the cursor (the target window under screen:target)
|
|
503
|
+
screen: boolean?,
|
|
504
|
+
-- send this window instead
|
|
505
|
+
window: string?,
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export type AiFindOptions = {
|
|
509
|
+
-- look in this window; default: the display under the cursor
|
|
510
|
+
window: string?,
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export type AiReadOptions = {
|
|
514
|
+
window: string?,
|
|
515
|
+
-- read only around this point (screen coordinates, such as rb.ai.find's)
|
|
516
|
+
at: AiPoint?,
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
-- screen coordinates, for rb.hid.move_to
|
|
520
|
+
export type AiPoint = {
|
|
521
|
+
x: number,
|
|
522
|
+
y: number,
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export type RbAi = {
|
|
526
|
+
-- picks one of opts.choices; returns it and the model's confidence (0 to 1)
|
|
527
|
+
decide: (question: string, opts: AiDecideOptions) -> (string, number),
|
|
528
|
+
-- the centre of what is described, or nil when it is not visible
|
|
529
|
+
find: (what: string, opts: AiFindOptions?) -> AiPoint?,
|
|
530
|
+
-- the text described, as it appears on screen
|
|
531
|
+
read: (what: string, opts: AiReadOptions?) -> string,
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export type PhoneResult = {
|
|
535
|
+
delivered: boolean,
|
|
536
|
+
reason: string?,
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export type RbPhone = {
|
|
540
|
+
-- no phone app yet: returns { delivered = false, reason = "no phone connected" }
|
|
541
|
+
notify: (text: string, title: string?) -> PhoneResult,
|
|
542
|
+
}
|
|
543
|
+
|
|
493
544
|
-- events for rb.on (Rebind 3.9.0 and later), with the arguments each
|
|
494
545
|
-- handler receives. For down, up, move and scroll, a handler that returns
|
|
495
546
|
-- false swallows the input.
|
|
@@ -690,7 +741,7 @@ declare rb: {
|
|
|
690
741
|
get_pos: (handle: number?) -> WindowPos,
|
|
691
742
|
get_pid: (handle: number?) -> number,
|
|
692
743
|
activate: (handle: number) -> (),
|
|
693
|
-
move: (handle: number
|
|
744
|
+
move: (handle: number?, rect: RbWindowRect?) -> (),
|
|
694
745
|
set_always_on_top: (handle: number, enabled: boolean) -> (),
|
|
695
746
|
set_transparency: (handle: number, alpha: number) -> (),
|
|
696
747
|
set_title: (handle: number, title: string) -> (),
|
|
@@ -839,6 +890,9 @@ declare rb: {
|
|
|
839
890
|
delete_key: (key_path: string) -> (),
|
|
840
891
|
create_key: (key_path: string) -> (),
|
|
841
892
|
},
|
|
893
|
+
-- Rebind Pro; require("@rebind/pro") returns rb
|
|
894
|
+
ai: RbAi,
|
|
895
|
+
phone: RbPhone,
|
|
842
896
|
}
|
|
843
897
|
|
|
844
898
|
-- v1 names
|
|
@@ -1041,7 +1095,7 @@ declare Window: {
|
|
|
1041
1095
|
GetPos: (handle: number?) -> WindowPos,
|
|
1042
1096
|
GetPID: (handle: number?) -> number,
|
|
1043
1097
|
Activate: (handle: number) -> (),
|
|
1044
|
-
Move: (handle: number
|
|
1098
|
+
Move: (handle: number?, x: number?, y: number?, width: number?, height: number?) -> (),
|
|
1045
1099
|
SetAlwaysOnTop: (handle: number, enabled: boolean) -> (),
|
|
1046
1100
|
SetTransparency: (handle: number, alpha: number) -> (),
|
|
1047
1101
|
SetTitle: (handle: number, title: string) -> (),
|