@tianshu-ai/local-bridge 0.1.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 ADDED
@@ -0,0 +1,91 @@
1
+ # local-bridge
2
+
3
+ `@tianshu-ai/local-bridge` — a lightweight local client that connects
4
+ your machine's tools (browser, and more) into a
5
+ [tianshu](https://github.com/tianshu-ai/tianshu) server via
6
+ **reverse-MCP**.
7
+
8
+ tianshu runs on a server. This bridge runs on **your** computer, dials
9
+ out to the server over a WebSocket, and registers itself as an MCP
10
+ server. The server-side agent can then drive your local tools — as if
11
+ they were built in — but scoped to your own sessions only.
12
+
13
+ ```
14
+ your machine tianshu server
15
+ ┌──────────────┐ outbound WSS ┌──────────────────┐
16
+ │ local-bridge │ ────────────────▶ │ /ws (Local Bridge │
17
+ │ • browser │ register tools │ plugin) │
18
+ │ • echo │ ◀──── tools/call ─│ → agent uses │
19
+ └──────────────┘ ──── result ─────▶ │ your tools │
20
+ └──────────────────┘
21
+ ```
22
+
23
+ ## Install & run
24
+
25
+ Open the **Local Bridge** panel in tianshu, copy the command it shows,
26
+ and run it on your machine:
27
+
28
+ ```bash
29
+ npx @tianshu-ai/local-bridge --server wss://your-tianshu.example.com/ws --token ***
30
+ ```
31
+
32
+ For a local dev server with auth off, the token is optional:
33
+
34
+ ```bash
35
+ npx @tianshu-ai/local-bridge --server ws://localhost:3110/ws
36
+ ```
37
+
38
+ ### Flags
39
+
40
+ | flag | meaning |
41
+ |---|---|
42
+ | `--server <url>` | tianshu chat WS endpoint (required), e.g. `wss://host/ws` |
43
+ | `--token <token>` | connection token from the Local Bridge panel |
44
+ | `--device <id>` | stable device id (default: hostname) |
45
+ | `--label <name>` | human label shown in the panel |
46
+ | `--no-browser` | expose only the connectivity `echo` tool |
47
+ | `--headless` | run the browser without a window (default: headful) |
48
+ | `--cdp <url>` | connect to an already-running Chrome's CDP endpoint (default probe `http://127.0.0.1:9222`; `off` to skip) |
49
+ | `--chrome-channel <ch>` | which installed browser to launch: `chrome` (default), `msedge`, … |
50
+ | `--user-data-dir <dir>` | reuse a Chrome profile dir so logins carry over |
51
+
52
+ **No Chromium download.** The bridge uses *your* Chrome:
53
+
54
+ 1. If a Chrome is already running with `--remote-debugging-port=9222`,
55
+ it connects to that (your real browser — real cookies + session).
56
+ 2. Otherwise it launches your system-installed Google Chrome via
57
+ Playwright's `channel: "chrome"`. Pass `--user-data-dir` to reuse
58
+ your logged-in profile.
59
+
60
+ To expose your everyday browser, start Chrome with a debugging port:
61
+
62
+ ```bash
63
+ # macOS
64
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222
65
+ ```
66
+
67
+ ## Tools
68
+
69
+ - `echo` — connectivity check.
70
+ - `browser_navigate` / `browser_get_text` / `browser_click` /
71
+ `browser_screenshot` — drive your real local Chrome (via CDP connect
72
+ or system-Chrome launch; automation-detection softened).
73
+
74
+ ## Protocol
75
+
76
+ The bridge speaks the reverse-MCP protocol defined authoritatively in the
77
+ server repo: `plugins/reverse-mcp/PROTOCOL.md`. It's a thin envelope
78
+ around standard MCP JSON-RPC over the authenticated chat WebSocket. This
79
+ client imports no tianshu package.
80
+
81
+ ## Develop
82
+
83
+ ```bash
84
+ npm install
85
+ npm run build
86
+ node dist/index.js --server ws://localhost:3110/ws --no-browser
87
+ ```
88
+
89
+ Repo: <https://github.com/tianshu-ai/local-bridge>
90
+
91
+ Apache-2.0.
@@ -0,0 +1,160 @@
1
+ // Local browser tools, driven by Playwright against the user's OWN
2
+ // Chrome — never a bundled/downloaded Chromium.
3
+ //
4
+ // Acquisition strategy (best → fallback), so we reuse the real browser
5
+ // with its real fingerprint + logged-in cookies and avoid a heavy
6
+ // download:
7
+ //
8
+ // 1. CONNECT over CDP to a Chrome the user already has running with
9
+ // `--remote-debugging-port` (default probe: http://127.0.0.1:9222).
10
+ // This is the user's actual browser — real profile, real session.
11
+ // 2. LAUNCH the system-installed Google Chrome via Playwright's
12
+ // `channel: "chrome"` (no Chromium download). Optionally reuse the
13
+ // user's profile via `userDataDir` so logins carry over. Launched
14
+ // with automation-detection softened.
15
+ //
16
+ // Nothing here downloads a browser; if neither path works the tool
17
+ // returns a clear, actionable error.
18
+ import { textResult } from "./protocol.js";
19
+ // Args that soften automation detection (navigator.webdriver etc.).
20
+ const STEALTH_ARGS = [
21
+ "--disable-blink-features=AutomationControlled",
22
+ "--no-default-browser-check",
23
+ "--no-first-run",
24
+ ];
25
+ async function probeCdp(cdpUrl) {
26
+ if (!cdpUrl)
27
+ return false;
28
+ try {
29
+ const base = cdpUrl.replace(/\/$/, "");
30
+ const res = await fetch(`${base}/json/version`, {
31
+ signal: AbortSignal.timeout(2500),
32
+ });
33
+ return res.ok;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ /** Acquire a Page, connecting to the user's Chrome or launching the
40
+ * system Chrome. Cached after first success. */
41
+ async function ensurePage(state, cfg) {
42
+ if (state.page && !state.page.isClosed())
43
+ return state.page;
44
+ const { chromium } = await import("playwright");
45
+ // 1) Connect to an already-running Chrome over CDP.
46
+ if (!state.browser && !state.context && (await probeCdp(cfg.cdpUrl))) {
47
+ try {
48
+ const browser = await chromium.connectOverCDP(cfg.cdpUrl);
49
+ state.browser = browser;
50
+ const ctx = browser.contexts()[0] ?? (await browser.newContext());
51
+ state.context = ctx;
52
+ state.page = ctx.pages()[0] ?? (await ctx.newPage());
53
+ state.how = `connected to your running Chrome at ${cfg.cdpUrl}`;
54
+ cfg.log(state.how);
55
+ return state.page;
56
+ }
57
+ catch (err) {
58
+ cfg.log(`CDP connect failed (${err instanceof Error ? err.message : String(err)}); falling back to launch`);
59
+ }
60
+ }
61
+ // 2) Launch the system-installed Chrome (no Chromium download).
62
+ if (cfg.userDataDir) {
63
+ // Persistent context reuses the user's profile → logged-in sessions.
64
+ const ctx = await chromium.launchPersistentContext(cfg.userDataDir, {
65
+ channel: cfg.channel,
66
+ headless: !cfg.headful,
67
+ args: STEALTH_ARGS,
68
+ });
69
+ state.context = ctx;
70
+ state.page = ctx.pages()[0] ?? (await ctx.newPage());
71
+ state.how = `launched system ${cfg.channel} with your profile (${cfg.userDataDir})`;
72
+ }
73
+ else {
74
+ const browser = await chromium.launch({
75
+ channel: cfg.channel,
76
+ headless: !cfg.headful,
77
+ args: STEALTH_ARGS,
78
+ });
79
+ state.browser = browser;
80
+ state.context = await browser.newContext();
81
+ state.page = await state.context.newPage();
82
+ state.how = `launched system ${cfg.channel} (ephemeral profile)`;
83
+ }
84
+ cfg.log(state.how);
85
+ return state.page;
86
+ }
87
+ export function makeBrowserTools(cfg) {
88
+ const state = { browser: null, context: null, page: null, how: "" };
89
+ const navigate = {
90
+ descriptor: {
91
+ name: "browser_navigate",
92
+ description: "Open a URL in the user's local browser and return the page title. Uses their real Chrome (real cookies/session).",
93
+ inputSchema: {
94
+ type: "object",
95
+ properties: { url: { type: "string", description: "Absolute http(s) URL to open." } },
96
+ required: ["url"],
97
+ },
98
+ },
99
+ async run(args) {
100
+ const url = String(args.url ?? "");
101
+ if (!/^https?:\/\//i.test(url))
102
+ return textResult("url must be an absolute http(s) URL", true);
103
+ const page = await ensurePage(state, cfg);
104
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45_000 });
105
+ const title = await page.title();
106
+ return textResult(`Navigated to ${page.url()}\nTitle: ${title}\n(${state.how})`);
107
+ },
108
+ };
109
+ const getText = {
110
+ descriptor: {
111
+ name: "browser_get_text",
112
+ description: "Return the visible text of the current page (optionally a CSS selector).",
113
+ inputSchema: {
114
+ type: "object",
115
+ properties: { selector: { type: "string", description: "Optional CSS selector; defaults to <body>." } },
116
+ },
117
+ },
118
+ async run(args) {
119
+ if (!state.page || state.page.isClosed())
120
+ return textResult("no page open — call browser_navigate first", true);
121
+ const selector = typeof args.selector === "string" && args.selector ? args.selector : "body";
122
+ const text = await state.page.locator(selector).first().innerText({ timeout: 15_000 }).catch(() => "");
123
+ return textResult(text.slice(0, 8000) || "(no text found)");
124
+ },
125
+ };
126
+ const click = {
127
+ descriptor: {
128
+ name: "browser_click",
129
+ description: "Click the first element matching a CSS selector on the current page.",
130
+ inputSchema: {
131
+ type: "object",
132
+ properties: { selector: { type: "string" } },
133
+ required: ["selector"],
134
+ },
135
+ },
136
+ async run(args) {
137
+ if (!state.page || state.page.isClosed())
138
+ return textResult("no page open — call browser_navigate first", true);
139
+ const selector = String(args.selector ?? "");
140
+ await state.page.locator(selector).first().click({ timeout: 15_000 });
141
+ return textResult(`clicked ${selector}\nNow at: ${state.page.url()}`);
142
+ },
143
+ };
144
+ const screenshot = {
145
+ descriptor: {
146
+ name: "browser_screenshot",
147
+ description: "Capture a screenshot of the current page (saved on the bridge host).",
148
+ inputSchema: { type: "object", properties: {} },
149
+ },
150
+ async run() {
151
+ if (!state.page || state.page.isClosed())
152
+ return textResult("no page open — call browser_navigate first", true);
153
+ const path = `/tmp/tianshu-local-bridge-shot-${Date.now()}.png`;
154
+ await state.page.screenshot({ path, fullPage: false });
155
+ return textResult(`screenshot saved on the bridge host at ${path}`);
156
+ },
157
+ };
158
+ return [navigate, getText, click, screenshot];
159
+ }
160
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ // WebSocket dial-in to a tianshu server + reverse-MCP request handling.
2
+ //
3
+ // The bridge connects OUTBOUND (works behind NAT/firewall), authenticates
4
+ // with a Bearer token, registers its tools, then serves tools/call
5
+ // requests the server sends. Auto-reconnects with backoff.
6
+ import WebSocket from "ws";
7
+ import { MSG, textResult } from "./protocol.js";
8
+ const RECONNECT_BASE_MS = 1000;
9
+ const RECONNECT_MAX_MS = 30_000;
10
+ export class BridgeConnection {
11
+ opts;
12
+ ws = null;
13
+ closed = false;
14
+ attempt = 0;
15
+ toolsByName = new Map();
16
+ log;
17
+ constructor(opts) {
18
+ this.opts = opts;
19
+ this.log = opts.log ?? ((m) => console.log(m));
20
+ for (const t of opts.tools)
21
+ this.toolsByName.set(t.descriptor.name, t);
22
+ }
23
+ start() {
24
+ this.closed = false;
25
+ this.connect();
26
+ }
27
+ stop() {
28
+ this.closed = true;
29
+ try {
30
+ this.ws?.send(JSON.stringify({ type: MSG.unregister }));
31
+ }
32
+ catch {
33
+ /* ignore */
34
+ }
35
+ this.ws?.close();
36
+ this.ws = null;
37
+ }
38
+ connect() {
39
+ const headers = {};
40
+ if (this.opts.token)
41
+ headers.Authorization = `Bearer ${this.opts.token}`;
42
+ const ws = new WebSocket(this.opts.server, { headers });
43
+ this.ws = ws;
44
+ ws.on("open", () => {
45
+ this.attempt = 0;
46
+ this.log(`connected to ${this.opts.server}`);
47
+ const reg = {
48
+ type: MSG.register,
49
+ deviceId: this.opts.deviceId,
50
+ label: this.opts.label,
51
+ tools: this.opts.tools.map((t) => t.descriptor),
52
+ };
53
+ ws.send(JSON.stringify(reg));
54
+ });
55
+ ws.on("message", (raw) => {
56
+ let msg;
57
+ try {
58
+ msg = JSON.parse(raw.toString());
59
+ }
60
+ catch {
61
+ return;
62
+ }
63
+ switch (msg.type) {
64
+ case MSG.registered:
65
+ if (msg.ok) {
66
+ this.log(`registered device "${this.opts.deviceId}" (${this.toolsByName.size} tools)`);
67
+ }
68
+ else {
69
+ this.log(`register rejected: ${msg.error ?? "unknown"}`);
70
+ }
71
+ break;
72
+ case MSG.request:
73
+ void this.handleRequest(msg);
74
+ break;
75
+ default:
76
+ // Ignore other chat-channel traffic on the shared /ws.
77
+ break;
78
+ }
79
+ });
80
+ ws.on("close", () => {
81
+ this.ws = null;
82
+ if (this.closed)
83
+ return;
84
+ const delay = Math.min(RECONNECT_BASE_MS * 2 ** this.attempt, RECONNECT_MAX_MS);
85
+ this.attempt++;
86
+ this.log(`disconnected; reconnecting in ${Math.round(delay / 1000)}s`);
87
+ setTimeout(() => {
88
+ if (!this.closed)
89
+ this.connect();
90
+ }, delay);
91
+ });
92
+ ws.on("error", (err) => {
93
+ this.log(`socket error: ${err instanceof Error ? err.message : String(err)}`);
94
+ });
95
+ }
96
+ send(msg) {
97
+ try {
98
+ this.ws?.send(JSON.stringify(msg));
99
+ }
100
+ catch (err) {
101
+ this.log(`failed to send response: ${err instanceof Error ? err.message : String(err)}`);
102
+ }
103
+ }
104
+ async handleRequest(req) {
105
+ if (req.method !== "tools/call") {
106
+ this.send({ type: MSG.response, id: req.id, error: { code: -32601, message: `unsupported method: ${req.method}` } });
107
+ return;
108
+ }
109
+ const params = (req.params ?? {});
110
+ const tool = params.name ? this.toolsByName.get(params.name) : undefined;
111
+ if (!tool) {
112
+ this.send({ type: MSG.response, id: req.id, error: { code: -32602, message: `unknown tool: ${params.name}` } });
113
+ return;
114
+ }
115
+ try {
116
+ const result = await tool.run(params.arguments ?? {});
117
+ this.send({ type: MSG.response, id: req.id, result });
118
+ }
119
+ catch (err) {
120
+ this.send({
121
+ type: MSG.response,
122
+ id: req.id,
123
+ result: textResult(`tool "${params.name}" failed: ${err instanceof Error ? err.message : String(err)}`, true),
124
+ });
125
+ }
126
+ }
127
+ }
128
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ // @tianshu-ai/local-bridge — dial into a tianshu server and expose local
3
+ // tools (browser, …) to the agent via reverse-MCP.
4
+ //
5
+ // Usage:
6
+ // npx @tianshu-ai/local-bridge --server wss://tianshu.example.com/ws --token ***
7
+ // npx @tianshu-ai/local-bridge --server ws://localhost:3110/ws (dev, no auth)
8
+ //
9
+ // Flags:
10
+ // --server <url> tianshu chat WS endpoint (required)
11
+ // --token <token> connection token from the Local Bridge panel
12
+ // --device <id> stable device id (default: hostname)
13
+ // --label <name> human label shown in the panel (default: device id)
14
+ // --no-browser don't expose browser tools (echo only)
15
+ // --headless run the browser without a window (default: headful)
16
+ // --cdp <url> connect to an already-running Chrome's CDP endpoint
17
+ // (default probe: http://127.0.0.1:9222). Set to "off"
18
+ // to skip and always launch.
19
+ // --chrome-channel which installed browser to launch: chrome (default),
20
+ // msedge, chrome-beta, … (never downloads Chromium)
21
+ // --user-data-dir reuse a Chrome profile dir so logins carry over
22
+ import os from "node:os";
23
+ import { BridgeConnection } from "./connection.js";
24
+ import { makeBrowserTools } from "./browser.js";
25
+ import { textResult } from "./protocol.js";
26
+ function parseArgs(argv) {
27
+ const out = {};
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const a = argv[i];
30
+ if (!a.startsWith("--"))
31
+ continue;
32
+ const key = a.slice(2);
33
+ const next = argv[i + 1];
34
+ if (next && !next.startsWith("--")) {
35
+ out[key] = next;
36
+ i++;
37
+ }
38
+ else {
39
+ out[key] = true;
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+ function main() {
45
+ const args = parseArgs(process.argv.slice(2));
46
+ const server = typeof args.server === "string" ? args.server : "";
47
+ if (!server) {
48
+ console.error("error: --server <wss://host/ws> is required");
49
+ console.error("example: npx @tianshu-ai/local-bridge --server wss://tianshu.example.com/ws --token ***");
50
+ process.exit(2);
51
+ }
52
+ const token = typeof args.token === "string" ? args.token : undefined;
53
+ const deviceId = (typeof args.device === "string" && args.device) || os.hostname() || "bridge";
54
+ const label = typeof args.label === "string" ? args.label : deviceId;
55
+ // Default headful: a real browser window the user can watch. --headless to hide.
56
+ const headful = args.headless !== true;
57
+ const cdpArg = typeof args.cdp === "string" ? args.cdp : "http://127.0.0.1:9222";
58
+ const cdpUrl = cdpArg === "off" ? "" : cdpArg;
59
+ const channel = typeof args["chrome-channel"] === "string" ? args["chrome-channel"] : "chrome";
60
+ const userDataDir = typeof args["user-data-dir"] === "string" ? args["user-data-dir"] : "";
61
+ // Always ship a trivial echo tool so the round-trip can be verified
62
+ // without a browser install.
63
+ const echo = {
64
+ descriptor: {
65
+ name: "echo",
66
+ description: "Echo the given text back (bridge connectivity check).",
67
+ inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
68
+ },
69
+ async run(a) {
70
+ return textResult(String(a.text ?? ""));
71
+ },
72
+ };
73
+ const browserOn = args["no-browser"] !== true;
74
+ const tools = [echo];
75
+ if (browserOn) {
76
+ tools.push(...makeBrowserTools({
77
+ cdpUrl,
78
+ channel,
79
+ userDataDir,
80
+ headful,
81
+ log: (m) => console.log(`[local-bridge] ${m}`),
82
+ }));
83
+ }
84
+ const conn = new BridgeConnection({
85
+ server,
86
+ token,
87
+ deviceId,
88
+ label,
89
+ tools,
90
+ log: (m) => console.log(`[local-bridge] ${m}`),
91
+ });
92
+ console.log(`[local-bridge] starting — device="${deviceId}", ${tools.length} tools, ` +
93
+ `browser=${browserOn ? (cdpUrl ? `connect(${cdpUrl}) or ${channel}` : channel) : "off"}`);
94
+ conn.start();
95
+ const shutdown = () => {
96
+ console.log("\n[local-bridge] shutting down…");
97
+ conn.stop();
98
+ process.exit(0);
99
+ };
100
+ process.on("SIGINT", shutdown);
101
+ process.on("SIGTERM", shutdown);
102
+ }
103
+ main();
104
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ // Reverse-MCP wire protocol (client side). Mirrors the authoritative
2
+ // contract in the tianshu server's plugins/reverse-mcp/PROTOCOL.md. We
3
+ // deliberately do NOT import any tianshu package — the bridge stays a
4
+ // standalone client that speaks the protocol.
5
+ export const MSG = {
6
+ register: "reverse_mcp_register",
7
+ unregister: "reverse_mcp_unregister",
8
+ response: "reverse_mcp_response",
9
+ request: "reverse_mcp_request",
10
+ registered: "reverse_mcp_registered",
11
+ };
12
+ export function textResult(text, isError = false) {
13
+ return { content: [{ type: "text", text }], isError };
14
+ }
15
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,uEAAuE;AACvE,sEAAsE;AACtE,8CAA8C;AAE9C,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,QAAQ,EAAE,sBAAsB;IAChC,UAAU,EAAE,wBAAwB;IACpC,QAAQ,EAAE,sBAAsB;IAChC,OAAO,EAAE,qBAAqB;IAC9B,UAAU,EAAE,wBAAwB;CAC5B,CAAC;AAqBX,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAO,GAAG,KAAK;IACtD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC;AACxD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@tianshu-ai/local-bridge",
3
+ "version": "0.1.0",
4
+ "description": "Local bridge for tianshu — dial into a tianshu server over WebSocket and expose your machine's tools (browser, files, shell) to the agent via reverse-MCP.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "tianshu-local-bridge": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsc --watch --preserveWatchOutput",
17
+ "start": "node dist/index.js",
18
+ "typecheck": "tsc --noEmit",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "dependencies": {
22
+ "playwright": "^1.56.0",
23
+ "ws": "^8.18.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^24.0.0",
27
+ "@types/ws": "^8.5.12",
28
+ "typescript": "^5.6.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }