terminal-bridge-setup 2.9.0 → 3.0.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 CHANGED
@@ -46,6 +46,10 @@ Agent (命令)
46
46
  ## 特性
47
47
 
48
48
  - **双终端支持**:JumpServer 堡垒机 Web 终端 + Arthas Web Console
49
+ - **Yearning SQL 自动化**:Agent 通过 yr-run 注入 SQL → 自动点查询 → MessagePack 结果帧解码返回;手动查询同样捕获
50
+ - **CSV 导出**:每次查询结果自动进 popup 的「CSV 导出记录」,点击即下载(BOM + RFC4180,Excel 直开)
51
+ - **多 Yearning 页面**:popup 列表展示数据源/数据库,选择目标页面,SQL 注入与结果按 tab 隔离
52
+ - **注入前新建 SQL 窗口**:不污染用户正在使用的编辑器;数据库未选择时提前报错
49
53
  - **结构化输出**:ANSI 已清理,命令回显已去除,返回纯净文本
50
54
  - **sudo 自动重试**:检测 sudo 别名劫持,询问用户后切 root 重试
51
55
  - **Arthas 安全基线**:中风险命令(trace/watch)自动补 `-n`,高风险命令(retransform/profiler/stop)无条件禁用
@@ -16,6 +16,13 @@ let bridgeConnected = false;
16
16
  const termReadyTabs = new Set(); // 哪些 tab 的 content script 上报了 term-ready
17
17
  const termReadyFrames = new Map(); // tabId -> frameId(终端所在的 frame,注入时直接用)
18
18
  let activeTabId = null; // 当前激活的终端 tab(命令只发它,WS 帧只收它的)
19
+ // WS 监听(tap)模式:非终端页面(如 Yearning)没有 xterm,无法走 term-ready 激活。
20
+ // 用户在 popup 点「监听当前页 WS」把 tab 加进 tapTabs,CDP 帧照抓照转发,
21
+ // 代理侧按 URL 过滤喂给 tap 客户端(不经终端命令的 prompt 配对)。
22
+ const tapTabs = new Set();
23
+ const tapTabMeta = new Map(); // tabId -> { title, url, host, database, dataSource, label }
24
+ let activeTapTabId = null; // Yearning SQL 自动化使用的目标 tab
25
+ const wsUrls = new Map(); // requestId -> url(帧转发时带上来源 URL,供 tap 过滤)
19
26
 
20
27
  // ============== 消息处理(来自 popup / content script)==============
21
28
  // 注意:listener 不能是 async——async 函数返回 Promise 而非 true,
@@ -82,6 +89,90 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
82
89
  return true;
83
90
  }
84
91
 
92
+ // --- Yearning 监听:显式绑定 tab,并维护 active 页面 ---
93
+ if (msg.type === "WS_TAP_ATTACH") {
94
+ const tabId = msg.tabId;
95
+ if (tabId == null) { sendResponse({ ok: false, msg: "missing tabId" }); return false; }
96
+ tapTabs.add(tabId);
97
+ if (activeTapTabId == null) activeTapTabId = tabId;
98
+ attachDebugger(tabId);
99
+ refreshTapTabMeta(tabId).then(() => {
100
+ sendToBridge({ type: "yr-active-tab", tabId: activeTapTabId });
101
+ sendResponse({ ok: true, tabId, activeTapTabId, tabs: [...tapTabMeta.values()] });
102
+ });
103
+ return true;
104
+ }
105
+ if (msg.type === "WS_TAP_DETACH" || msg.type === "YR_TAP_DETACH") {
106
+ const tabId = msg.tabId;
107
+ tapTabs.delete(tabId);
108
+ tapTabMeta.delete(tabId);
109
+ if (activeTapTabId === tabId) activeTapTabId = [...tapTabs][0] ?? null;
110
+ sendResponse({ ok: true, tabId, activeTapTabId, tabs: [...tapTabMeta.values()] });
111
+ return false;
112
+ }
113
+ if (msg.type === "YR_TAP_SELECT") {
114
+ if (!tapTabs.has(msg.tabId)) {
115
+ sendResponse({ ok: false, msg: "该 Yearning 页面尚未监听" });
116
+ } else {
117
+ activeTapTabId = msg.tabId;
118
+ sendToBridge({ type: "yr-active-tab", tabId: activeTapTabId });
119
+ sendResponse({ ok: true, activeTapTabId });
120
+ }
121
+ return false;
122
+ }
123
+ if (msg.type === "YR_TAP_STATUS" || msg.type === "WS_TAP_STATUS") {
124
+ buildTapStatus().then(sendResponse);
125
+ return true;
126
+ }
127
+
128
+ // --- CSV 导出记录(popup 列表,存 session storage 防 SW 休眠丢失)---
129
+ if (msg.type === "CSV_LIST") {
130
+ getCsvExports().then(list => {
131
+ sendResponse({
132
+ ok: true,
133
+ exports: list.map(e => ({ id: e.id, name: e.name, rows: e.rows, sql: e.sql, time: e.time })),
134
+ });
135
+ });
136
+ return true; // 异步
137
+ }
138
+ if (msg.type === "CSV_DOWNLOAD") {
139
+ getCsvExports().then(list => {
140
+ const record = list.find(e => e.id === msg.id);
141
+ if (!record) {
142
+ sendResponse({ ok: false, msg: "导出记录不存在" });
143
+ return;
144
+ }
145
+ chrome.downloads.download(
146
+ { url: record.dataUrl, filename: record.name, saveAs: true },
147
+ (downloadId) => {
148
+ if (chrome.runtime.lastError) {
149
+ sendResponse({ ok: false, msg: chrome.runtime.lastError.message });
150
+ return;
151
+ }
152
+ sendResponse({ ok: true, downloadId });
153
+ }
154
+ );
155
+ });
156
+ return true; // 异步
157
+ }
158
+
159
+ // --- Yearning 自动化(探测/注入 SQL/点查询)---
160
+ if (msg.type === "YR_PING" || msg.type === "YR_SQL_SET" || msg.type === "YR_QUERY_CLICK") {
161
+ const tabId = msg.tabId != null ? msg.tabId : activeTapTabId;
162
+ if (tabId == null) {
163
+ sendResponse({ ok: false, msg: "没有选中的 Yearning 页面,请先监听并选择 tab" });
164
+ return false;
165
+ }
166
+ chrome.tabs.sendMessage(tabId, msg, { frameId: 0 }, (res) => {
167
+ if (chrome.runtime.lastError) {
168
+ sendResponse({ ok: false, msg: "content script 无响应: " + chrome.runtime.lastError.message });
169
+ return;
170
+ }
171
+ sendResponse(res || { ok: false });
172
+ });
173
+ return true;
174
+ }
175
+
85
176
  return false;
86
177
  });
87
178
 
@@ -131,6 +222,47 @@ function hostFromUrl(url) {
131
222
  }
132
223
  }
133
224
 
225
+ async function refreshTapTabMeta(tabId) {
226
+ let tab = null;
227
+ try { tab = await chrome.tabs.get(tabId); } catch { return null; }
228
+ const meta = {
229
+ tabId,
230
+ title: (tab.title || "Yearning").slice(0, 80),
231
+ url: tab.url || "",
232
+ host: hostFromUrl(tab.url || ""),
233
+ database: "",
234
+ dataSource: "",
235
+ label: tab.title || "Yearning",
236
+ isCurrent: false,
237
+ active: tabId === activeTapTabId,
238
+ };
239
+ const details = await sendFrameMessage(tabId, { type: "yr-meta" }, 0);
240
+ if (details?.ok) {
241
+ meta.database = details.database || "";
242
+ meta.dataSource = details.dataSource || "";
243
+ meta.label = details.label || meta.label;
244
+ }
245
+ tapTabMeta.set(tabId, meta);
246
+ return meta;
247
+ }
248
+
249
+ async function buildTapStatus() {
250
+ let currentTabId = null;
251
+ try {
252
+ const [current] = await chrome.tabs.query({ active: true, currentWindow: true });
253
+ currentTabId = current?.id ?? null;
254
+ } catch {}
255
+ for (const tabId of tapTabs) {
256
+ if (!tapTabMeta.has(tabId)) await refreshTapTabMeta(tabId);
257
+ const meta = tapTabMeta.get(tabId);
258
+ if (meta) {
259
+ meta.active = tabId === activeTapTabId;
260
+ meta.isCurrent = tabId === currentTabId;
261
+ }
262
+ }
263
+ return { ok: true, tapTabs: [...tapTabs], activeTapTabId, currentTabId, tabs: [...tapTabMeta.values()] };
264
+ }
265
+
134
266
  // ============== Native Messaging(启动/停止代理)==============
135
267
  const NATIVE_HOST = "com.wssniffer.host";
136
268
 
@@ -202,25 +334,29 @@ function attachDebugger(tabId) {
202
334
  chrome.debugger.onEvent.addListener((source, method, params) => {
203
335
  const tabId = source.tabId;
204
336
  const isActive = tabId === activeTabId;
337
+ const isTap = tapTabs.has(tabId);
338
+ if (!isActive && !isTap) return; // 终端激活 tab 和 tap 监听 tab 之外不上送
205
339
 
206
340
  if (method === 'Network.webSocketCreated') {
341
+ // 记录 requestId -> url,帧转发时带上(tap 客户端按 URL 过滤)
342
+ wsUrls.set(params.requestId, params.url);
207
343
  if (isActive) {
208
344
  console.log('[WS] 新连接:', params.url, '(active tab', tabId, ')');
209
345
  sendToBridge({ type: 'ws-open', payload: { url: params.url, requestId: params.requestId } });
210
346
  }
211
347
  }
212
348
  else if (method === 'Network.webSocketFrameReceived') {
213
- // recv 帧是命令输出,只有 activeTabId 的才上送给代理配对
214
- if (isActive) {
215
- sendToBridge({
216
- type: 'ws-recv',
217
- payload: {
218
- data: extractPayloadData(params.response),
219
- opcode: params.response && params.response.opcode,
220
- t: Date.now()
221
- }
222
- });
223
- }
349
+ // recv 帧:终端激活 tab 的进代理配对;tap tab 的进代理 tap 通道(都带 url)
350
+ sendToBridge({
351
+ type: 'ws-recv',
352
+ payload: {
353
+ data: extractPayloadData(params.response),
354
+ opcode: params.response && params.response.opcode,
355
+ url: wsUrls.get(params.requestId) || "",
356
+ tabId,
357
+ t: Date.now()
358
+ }
359
+ });
224
360
  }
225
361
  else if (method === 'Network.webSocketFrameSent') {
226
362
  if (isActive) {
@@ -229,12 +365,15 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
229
365
  payload: {
230
366
  data: extractPayloadData(params.response),
231
367
  opcode: params.response && params.response.opcode,
368
+ url: wsUrls.get(params.requestId) || "",
369
+ tabId,
232
370
  t: Date.now()
233
371
  }
234
372
  });
235
373
  }
236
374
  }
237
375
  else if (method === 'Network.webSocketClosed') {
376
+ wsUrls.delete(params.requestId);
238
377
  if (isActive) sendToBridge({ type: 'ws-close', payload: { requestId: params.requestId } });
239
378
  }
240
379
  else if (method === 'Network.webSocketFrameError') {
@@ -390,6 +529,39 @@ function handleBridgeCommand(frame) {
390
529
  return;
391
530
  }
392
531
 
532
+ // { type: "yr-cmd", sub: "ping"|"sql-set"|"query-click", sql, reqId }
533
+ // Yearning 自动化:代理编排(yr-run),本插件转发到 tap tab 的 content script。
534
+ // sql-set 走 CDP Input.insertText(浏览器信任层级,monaco 等任何编辑器都接受);
535
+ // 合成 paste/execCommand 对 monaco 无效(isTrusted=false 被忽略,实测)。
536
+ if (frame.type === "yr-cmd") {
537
+ const tabId = frame.tabId != null ? frame.tabId : activeTapTabId;
538
+ if (tabId == null) {
539
+ sendToBridge({ type: "yr-result", reqId: frame.reqId, tabId: null, ok: false, error: "no tap tab; 先在 Yearning 页面点「监听当前页 WS」" });
540
+ return;
541
+ }
542
+ if (frame.sub === "sql-set") {
543
+ yrSqlSetViaCDP(tabId, frame.sql || "", frame.reqId);
544
+ return;
545
+ }
546
+ const payload = frame.sub === "ping" ? { type: "yr-ping" }
547
+ : { type: "yr-query-click" };
548
+ chrome.tabs.sendMessage(tabId, payload, { frameId: 0 }, (res) => {
549
+ if (chrome.runtime.lastError) {
550
+ sendToBridge({ type: "yr-result", reqId: frame.reqId, tabId, ok: false, error: "content script 无响应: " + chrome.runtime.lastError.message });
551
+ return;
552
+ }
553
+ sendToBridge({ type: "yr-result", reqId: frame.reqId, tabId, ...(res || { ok: false }) });
554
+ });
555
+ return;
556
+ }
557
+
558
+ // { type: "yr-export-csv", payload(结果JSON), sql, rows }
559
+ // 代理在 yr-run 查询成功后发来:浏览器侧生成 CSV 并落下载,popup 展示列表
560
+ if (frame.type === "yr-export-csv") {
561
+ handleYrExportCsv(frame);
562
+ return;
563
+ }
564
+
393
565
  // { type: "ping" }
394
566
  if (frame.type === "ping") {
395
567
  sendToBridge({ type: "pong", payload: { readyTabs: [...termReadyTabs] } });
@@ -397,6 +569,146 @@ function handleBridgeCommand(frame) {
397
569
  }
398
570
  }
399
571
 
572
+ // Yearning SQL 通过 CDP Input 注入(monaco 等编辑器接受浏览器信任层级事件)
573
+ async function yrSqlSetViaCDP(tabId, sql, reqId) {
574
+ try {
575
+ // 未 attach 的 tab 上 Input.* 命令会静默无效 → 注入校验失败,先确保 attach
576
+ if (!attached[tabId]) {
577
+ await new Promise(resolve => attachDebugger(tabId) ?? resolve());
578
+ await new Promise(r => setTimeout(r, 400)); // 等 Network.enable 完成
579
+ }
580
+ // 新建 SQL 窗口:避免把 SQL 注入用户正在使用的已有编辑器
581
+ const newWin = await sendFrameMessage(tabId, { type: "yr-new-sql" }, 0);
582
+ if (!newWin || !newWin.ok) {
583
+ console.warn("[bg] 新建 SQL 窗口失败(继续在当前编辑器注入):", newWin?.error);
584
+ }
585
+ // 前置校验:数据库未选择时 Yearning 查询必报错,提前失败给明确提示
586
+ const meta = await sendFrameMessage(tabId, { type: "yr-meta" }, 0);
587
+ if (meta?.ok && !meta.database) {
588
+ sendToBridge({
589
+ type: "yr-result", reqId, tabId, ok: false,
590
+ error: "database-not-selected",
591
+ message: "该 Yearning 页面未选择数据库(查询会报错)。请先在页面上选择数据库后重试。",
592
+ });
593
+ return;
594
+ }
595
+ const focused = await sendFrameMessage(tabId, { type: "yr-focus-editor" }, 0);
596
+ if (!focused || !focused.ok) {
597
+ sendToBridge({ type: "yr-result", reqId, tabId, ok: false, error: focused?.error || "cannot focus Yearning editor" });
598
+ return;
599
+ }
600
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchKeyEvent", {
601
+ type: "keyDown", key: "a", code: "KeyA", windowsVirtualKeyCode: 65,
602
+ nativeVirtualKeyCode: 65, modifiers: 2,
603
+ });
604
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchKeyEvent", {
605
+ type: "keyUp", key: "a", code: "KeyA", windowsVirtualKeyCode: 65,
606
+ nativeVirtualKeyCode: 65, modifiers: 2,
607
+ });
608
+ await chrome.debugger.sendCommand({ tabId }, "Input.insertText", { text: sql });
609
+ await new Promise(r => setTimeout(r, 400));
610
+ let verified = await sendFrameMessage(tabId, { type: "yr-verify-sql", sql }, 0);
611
+ // 校验失败重试一次(编辑器渲染慢/焦点竞争时首轮 insertText 可能丢)
612
+ if (!verified?.ok) {
613
+ const refocus = await sendFrameMessage(tabId, { type: "yr-focus-editor" }, 0);
614
+ if (refocus?.ok) {
615
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchKeyEvent", {
616
+ type: "keyDown", key: "a", code: "KeyA", windowsVirtualKeyCode: 65,
617
+ nativeVirtualKeyCode: 65, modifiers: 2,
618
+ });
619
+ await chrome.debugger.sendCommand({ tabId }, "Input.dispatchKeyEvent", {
620
+ type: "keyUp", key: "a", code: "KeyA", windowsVirtualKeyCode: 65,
621
+ nativeVirtualKeyCode: 65, modifiers: 2,
622
+ });
623
+ await chrome.debugger.sendCommand({ tabId }, "Input.insertText", { text: sql });
624
+ await new Promise(r => setTimeout(r, 500));
625
+ verified = await sendFrameMessage(tabId, { type: "yr-verify-sql", sql }, 0);
626
+ }
627
+ }
628
+ sendToBridge({
629
+ type: "yr-result", reqId, tabId, ok: !!verified?.ok,
630
+ via: "cdp-input", error: verified?.ok ? undefined : "CDP 注入后编辑器读回校验失败",
631
+ info: verified?.editorText,
632
+ });
633
+ } catch (err) {
634
+ sendToBridge({ type: "yr-result", reqId, tabId, ok: false, error: "CDP SQL 注入失败: " + err.message });
635
+ }
636
+ }
637
+
638
+ function sendFrameMessage(tabId, msg, frameId) {
639
+ return new Promise((resolve) => {
640
+ chrome.tabs.sendMessage(tabId, msg, { frameId }, (res) => {
641
+ if (chrome.runtime.lastError) { resolve(null); return; }
642
+ resolve(res || null);
643
+ });
644
+ });
645
+ }
646
+
647
+ // ===================== Yearning CSV 导出 =====================
648
+ // 导出记录存 chrome.storage.session(MV3 service worker 会休眠,
649
+ // 内存数组 30s 就丢;session 存储跨 SW 重启保留、浏览器关闭即清空,
650
+ // 正好匹配"最近导出"的语义)。dataUrl 存在记录里,重新下载不依赖原始文件。
651
+ const CSV_STORAGE_KEY = "yrCsvExports";
652
+ const CSV_MAX_RECORDS = 20;
653
+
654
+ async function getCsvExports() {
655
+ try {
656
+ const r = await chrome.storage.session.get(CSV_STORAGE_KEY);
657
+ return r[CSV_STORAGE_KEY] || [];
658
+ } catch { return []; }
659
+ }
660
+
661
+ async function addCsvExport(record) {
662
+ const list = await getCsvExports();
663
+ list.unshift(record);
664
+ if (list.length > CSV_MAX_RECORDS) list.length = CSV_MAX_RECORDS;
665
+ try { await chrome.storage.session.set({ [CSV_STORAGE_KEY]: list }); } catch {}
666
+ }
667
+
668
+ function csvCell(value) {
669
+ if (value == null) return "";
670
+ const s = String(value);
671
+ if (/[",\r\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
672
+ return s;
673
+ }
674
+
675
+ // Yearning 结果 JSON → CSV 文本。多结果集时只取第一个非空表
676
+ // (Yearning 对 SHOW INDEX 会推两份相同结果,取一即可)
677
+ function resultJsonToCsv(jsonText) {
678
+ let obj;
679
+ try { obj = JSON.parse(jsonText); } catch { return null; }
680
+ if (!Array.isArray(obj.results)) return null;
681
+ const table = obj.results.find(t => t && Array.isArray(t.field) && t.field.length > 0)
682
+ || obj.results[0];
683
+ if (!table || !Array.isArray(table.field)) return null;
684
+ const headers = table.field.map(f => csvCell(f.title || f.dataIndex || ""));
685
+ const rows = (table.data || []).map(row =>
686
+ table.field.map(f => csvCell(row[f.dataIndex])).join(",")
687
+ );
688
+ return "\uFEFF" + [headers.join(","), ...rows].join("\r\n") + "\r\n";
689
+ }
690
+
691
+ async function handleYrExportCsv(frame) {
692
+ const csv = resultJsonToCsv(frame.payload || "");
693
+ if (!csv) {
694
+ console.warn("[bg] yr-export-csv: 结果 JSON 解析失败或无表结构");
695
+ return;
696
+ }
697
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
698
+ const sqlHead = (frame.sql || "query").slice(0, 30).replace(/[^\w-]+/g, "_");
699
+ const name = `yearning-${sqlHead}-${stamp}.csv`;
700
+ const dataUrl = "data:text/csv;charset=utf-8," + encodeURIComponent(csv);
701
+ await addCsvExport({
702
+ id: Date.now(),
703
+ name,
704
+ rows: frame.rows || 0,
705
+ sql: frame.sql || "",
706
+ time: Date.now(),
707
+ dataUrl,
708
+ });
709
+ console.log("[bg] CSV 导出已记录:", name, `(${frame.rows} 行),可在 popup 点击下载`);
710
+ }
711
+
400
712
  // 把命令注入到 activeTabId 的终端 frame。
401
713
  // 多终端场景下只注入用户选中的 tab,避免串扰。
402
714
  //
@@ -533,6 +845,7 @@ chrome.tabs.onRemoved.addListener((tabId) => {
533
845
  delete attached[tabId];
534
846
  termReadyTabs.delete(tabId);
535
847
  termReadyFrames.delete(tabId);
848
+ tapTabs.delete(tabId);
536
849
  // 关闭的是 activeTabId 就清空,让下次自动选或用户重选
537
850
  if (activeTabId === tabId) {
538
851
  activeTabId = null;
@@ -0,0 +1,56 @@
1
+ // Terminal Bridge - Yearning MAIN world 桥接
2
+ //
3
+ // CodeMirror(el.CodeMirror)/ monaco(window.monaco)等编辑器的 JS API
4
+ // 挂在页面 JS 上下文上,ISOLATED world 的 content script 访问不到。
5
+ // 本脚本运行在 MAIN world,通过 window.postMessage 与 ISOLATED 侧通信。
6
+ //
7
+ // 协议(source 字段区分方向):
8
+ // ISOLATED → MAIN: {source:"tb-yr-iso", id, kind:"detect"|"set-sql", sql}
9
+ // MAIN → ISOLATED: {source:"tb-yr-main", id, ok, via, info}
10
+
11
+ (function () {
12
+ if (window.__terminalBridgeYearningMain) return;
13
+ window.__terminalBridgeYearningMain = true;
14
+
15
+ function detect() {
16
+ const editors = [];
17
+ // CodeMirror:DOM 元素上挂 .CodeMirror 属性(v5)
18
+ document.querySelectorAll(".CodeMirror").forEach((el) => {
19
+ if (el.CodeMirror) editors.push({ type: "codemirror" });
20
+ });
21
+ // monaco
22
+ if (window.monaco && window.monaco.editor && typeof window.monaco.editor.getEditors === "function") {
23
+ window.monaco.editor.getEditors().forEach(() => editors.push({ type: "monaco" }));
24
+ }
25
+ return { editors };
26
+ }
27
+
28
+ function setSql(sql) {
29
+ const cmEl = document.querySelector(".CodeMirror");
30
+ if (cmEl && cmEl.CodeMirror) {
31
+ cmEl.CodeMirror.setValue(sql);
32
+ return { ok: true, via: "codemirror" };
33
+ }
34
+ if (window.monaco && window.monaco.editor && typeof window.monaco.editor.getEditors === "function") {
35
+ const eds = window.monaco.editor.getEditors();
36
+ if (eds.length > 0) {
37
+ eds[0].setValue(sql);
38
+ return { ok: true, via: "monaco" };
39
+ }
40
+ }
41
+ return { ok: false, error: "no cm/monaco editor in MAIN world" };
42
+ }
43
+
44
+ window.addEventListener("message", (event) => {
45
+ if (event.source !== window) return;
46
+ const msg = event.data;
47
+ if (!msg || msg.source !== "tb-yr-iso") return;
48
+ let reply;
49
+ if (msg.kind === "detect") reply = detect();
50
+ else if (msg.kind === "set-sql") reply = setSql(msg.sql || "");
51
+ else return;
52
+ window.postMessage({ source: "tb-yr-main", id: msg.id, ...reply }, "*");
53
+ });
54
+
55
+ console.log("[terminal-yr-main] MAIN world bridge ready");
56
+ })();