@opentabs-dev/mcp-server 0.0.19
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/dist/audit-disk.d.ts +23 -0
- package/dist/audit-disk.d.ts.map +1 -0
- package/dist/audit-disk.js +74 -0
- package/dist/audit-disk.js.map +1 -0
- package/dist/browser-tools/analyze-site/detect-apis.d.ts +36 -0
- package/dist/browser-tools/analyze-site/detect-apis.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/detect-apis.js +383 -0
- package/dist/browser-tools/analyze-site/detect-apis.js.map +1 -0
- package/dist/browser-tools/analyze-site/detect-auth.d.ts +72 -0
- package/dist/browser-tools/analyze-site/detect-auth.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/detect-auth.js +384 -0
- package/dist/browser-tools/analyze-site/detect-auth.js.map +1 -0
- package/dist/browser-tools/analyze-site/detect-dom.d.ts +65 -0
- package/dist/browser-tools/analyze-site/detect-dom.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/detect-dom.js +45 -0
- package/dist/browser-tools/analyze-site/detect-dom.js.map +1 -0
- package/dist/browser-tools/analyze-site/detect-framework.d.ts +48 -0
- package/dist/browser-tools/analyze-site/detect-framework.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/detect-framework.js +31 -0
- package/dist/browser-tools/analyze-site/detect-framework.js.map +1 -0
- package/dist/browser-tools/analyze-site/detect-globals.d.ts +41 -0
- package/dist/browser-tools/analyze-site/detect-globals.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/detect-globals.js +42 -0
- package/dist/browser-tools/analyze-site/detect-globals.js.map +1 -0
- package/dist/browser-tools/analyze-site/detect-storage.d.ts +39 -0
- package/dist/browser-tools/analyze-site/detect-storage.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/detect-storage.js +34 -0
- package/dist/browser-tools/analyze-site/detect-storage.js.map +1 -0
- package/dist/browser-tools/analyze-site/index.d.ts +52 -0
- package/dist/browser-tools/analyze-site/index.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site/index.js +827 -0
- package/dist/browser-tools/analyze-site/index.js.map +1 -0
- package/dist/browser-tools/analyze-site.d.ts +17 -0
- package/dist/browser-tools/analyze-site.d.ts.map +1 -0
- package/dist/browser-tools/analyze-site.js +41 -0
- package/dist/browser-tools/analyze-site.js.map +1 -0
- package/dist/browser-tools/clear-console-logs.d.ts +9 -0
- package/dist/browser-tools/clear-console-logs.d.ts.map +1 -0
- package/dist/browser-tools/clear-console-logs.js +16 -0
- package/dist/browser-tools/clear-console-logs.js.map +1 -0
- package/dist/browser-tools/click-element.d.ts +10 -0
- package/dist/browser-tools/click-element.d.ts.map +1 -0
- package/dist/browser-tools/click-element.js +22 -0
- package/dist/browser-tools/click-element.js.map +1 -0
- package/dist/browser-tools/close-tab.d.ts +9 -0
- package/dist/browser-tools/close-tab.d.ts.map +1 -0
- package/dist/browser-tools/close-tab.js +16 -0
- package/dist/browser-tools/close-tab.js.map +1 -0
- package/dist/browser-tools/definition.d.ts +26 -0
- package/dist/browser-tools/definition.d.ts.map +1 -0
- package/dist/browser-tools/definition.js +16 -0
- package/dist/browser-tools/definition.js.map +1 -0
- package/dist/browser-tools/delete-cookies.d.ts +10 -0
- package/dist/browser-tools/delete-cookies.d.ts.map +1 -0
- package/dist/browser-tools/delete-cookies.js +19 -0
- package/dist/browser-tools/delete-cookies.js.map +1 -0
- package/dist/browser-tools/disable-network-capture.d.ts +9 -0
- package/dist/browser-tools/disable-network-capture.d.ts.map +1 -0
- package/dist/browser-tools/disable-network-capture.js +16 -0
- package/dist/browser-tools/disable-network-capture.js.map +1 -0
- package/dist/browser-tools/enable-network-capture.d.ts +12 -0
- package/dist/browser-tools/enable-network-capture.d.ts.map +1 -0
- package/dist/browser-tools/enable-network-capture.js +42 -0
- package/dist/browser-tools/enable-network-capture.js.map +1 -0
- package/dist/browser-tools/execute-script.d.ts +19 -0
- package/dist/browser-tools/execute-script.d.ts.map +1 -0
- package/dist/browser-tools/execute-script.js +51 -0
- package/dist/browser-tools/execute-script.js.map +1 -0
- package/dist/browser-tools/extension-check-adapter.d.ts +11 -0
- package/dist/browser-tools/extension-check-adapter.d.ts.map +1 -0
- package/dist/browser-tools/extension-check-adapter.js +22 -0
- package/dist/browser-tools/extension-check-adapter.js.map +1 -0
- package/dist/browser-tools/extension-force-reconnect.d.ts +9 -0
- package/dist/browser-tools/extension-force-reconnect.d.ts.map +1 -0
- package/dist/browser-tools/extension-force-reconnect.js +19 -0
- package/dist/browser-tools/extension-force-reconnect.js.map +1 -0
- package/dist/browser-tools/extension-get-logs.d.ts +24 -0
- package/dist/browser-tools/extension-get-logs.d.ts.map +1 -0
- package/dist/browser-tools/extension-get-logs.js +34 -0
- package/dist/browser-tools/extension-get-logs.js.map +1 -0
- package/dist/browser-tools/extension-get-side-panel.d.ts +8 -0
- package/dist/browser-tools/extension-get-side-panel.d.ts.map +1 -0
- package/dist/browser-tools/extension-get-side-panel.js +17 -0
- package/dist/browser-tools/extension-get-side-panel.js.map +1 -0
- package/dist/browser-tools/extension-get-state.d.ts +9 -0
- package/dist/browser-tools/extension-get-state.d.ts.map +1 -0
- package/dist/browser-tools/extension-get-state.js +19 -0
- package/dist/browser-tools/extension-get-state.js.map +1 -0
- package/dist/browser-tools/focus-tab.d.ts +9 -0
- package/dist/browser-tools/focus-tab.d.ts.map +1 -0
- package/dist/browser-tools/focus-tab.js +17 -0
- package/dist/browser-tools/focus-tab.js.map +1 -0
- package/dist/browser-tools/get-console-logs.d.ts +18 -0
- package/dist/browser-tools/get-console-logs.d.ts.map +1 -0
- package/dist/browser-tools/get-console-logs.js +30 -0
- package/dist/browser-tools/get-console-logs.js.map +1 -0
- package/dist/browser-tools/get-cookies.d.ts +10 -0
- package/dist/browser-tools/get-cookies.d.ts.map +1 -0
- package/dist/browser-tools/get-cookies.js +23 -0
- package/dist/browser-tools/get-cookies.js.map +1 -0
- package/dist/browser-tools/get-network-requests.d.ts +10 -0
- package/dist/browser-tools/get-network-requests.d.ts.map +1 -0
- package/dist/browser-tools/get-network-requests.js +27 -0
- package/dist/browser-tools/get-network-requests.js.map +1 -0
- package/dist/browser-tools/get-page-html.d.ts +11 -0
- package/dist/browser-tools/get-page-html.d.ts.map +1 -0
- package/dist/browser-tools/get-page-html.js +32 -0
- package/dist/browser-tools/get-page-html.js.map +1 -0
- package/dist/browser-tools/get-resource-content.d.ts +11 -0
- package/dist/browser-tools/get-resource-content.d.ts.map +1 -0
- package/dist/browser-tools/get-resource-content.js +31 -0
- package/dist/browser-tools/get-resource-content.js.map +1 -0
- package/dist/browser-tools/get-storage.d.ts +14 -0
- package/dist/browser-tools/get-storage.d.ts.map +1 -0
- package/dist/browser-tools/get-storage.js +28 -0
- package/dist/browser-tools/get-storage.js.map +1 -0
- package/dist/browser-tools/get-tab-content.d.ts +11 -0
- package/dist/browser-tools/get-tab-content.d.ts.map +1 -0
- package/dist/browser-tools/get-tab-content.js +29 -0
- package/dist/browser-tools/get-tab-content.js.map +1 -0
- package/dist/browser-tools/get-tab-info.d.ts +9 -0
- package/dist/browser-tools/get-tab-info.d.ts.map +1 -0
- package/dist/browser-tools/get-tab-info.js +17 -0
- package/dist/browser-tools/get-tab-info.js.map +1 -0
- package/dist/browser-tools/handle-dialog.d.ts +14 -0
- package/dist/browser-tools/handle-dialog.d.ts.map +1 -0
- package/dist/browser-tools/handle-dialog.js +30 -0
- package/dist/browser-tools/handle-dialog.js.map +1 -0
- package/dist/browser-tools/hover-element.d.ts +11 -0
- package/dist/browser-tools/hover-element.d.ts.map +1 -0
- package/dist/browser-tools/hover-element.js +24 -0
- package/dist/browser-tools/hover-element.js.map +1 -0
- package/dist/browser-tools/index.d.ts +7 -0
- package/dist/browser-tools/index.d.ts.map +1 -0
- package/dist/browser-tools/index.js +81 -0
- package/dist/browser-tools/index.js.map +1 -0
- package/dist/browser-tools/list-resources.d.ts +10 -0
- package/dist/browser-tools/list-resources.d.ts.map +1 -0
- package/dist/browser-tools/list-resources.js +29 -0
- package/dist/browser-tools/list-resources.js.map +1 -0
- package/dist/browser-tools/list-tabs.d.ts +7 -0
- package/dist/browser-tools/list-tabs.d.ts.map +1 -0
- package/dist/browser-tools/list-tabs.js +16 -0
- package/dist/browser-tools/list-tabs.js.map +1 -0
- package/dist/browser-tools/navigate-tab.d.ts +10 -0
- package/dist/browser-tools/navigate-tab.d.ts.map +1 -0
- package/dist/browser-tools/navigate-tab.js +18 -0
- package/dist/browser-tools/navigate-tab.js.map +1 -0
- package/dist/browser-tools/open-tab.d.ts +9 -0
- package/dist/browser-tools/open-tab.d.ts.map +1 -0
- package/dist/browser-tools/open-tab.js +18 -0
- package/dist/browser-tools/open-tab.js.map +1 -0
- package/dist/browser-tools/press-key.d.ts +17 -0
- package/dist/browser-tools/press-key.d.ts.map +1 -0
- package/dist/browser-tools/press-key.js +43 -0
- package/dist/browser-tools/press-key.js.map +1 -0
- package/dist/browser-tools/query-elements.d.ts +12 -0
- package/dist/browser-tools/query-elements.d.ts.map +1 -0
- package/dist/browser-tools/query-elements.js +30 -0
- package/dist/browser-tools/query-elements.js.map +1 -0
- package/dist/browser-tools/reload-extension.d.ts +13 -0
- package/dist/browser-tools/reload-extension.d.ts.map +1 -0
- package/dist/browser-tools/reload-extension.js +35 -0
- package/dist/browser-tools/reload-extension.js.map +1 -0
- package/dist/browser-tools/screenshot-tab.d.ts +9 -0
- package/dist/browser-tools/screenshot-tab.d.ts.map +1 -0
- package/dist/browser-tools/screenshot-tab.js +22 -0
- package/dist/browser-tools/screenshot-tab.js.map +1 -0
- package/dist/browser-tools/scroll.d.ts +23 -0
- package/dist/browser-tools/scroll.d.ts.map +1 -0
- package/dist/browser-tools/scroll.js +56 -0
- package/dist/browser-tools/scroll.js.map +1 -0
- package/dist/browser-tools/select-option.d.ts +12 -0
- package/dist/browser-tools/select-option.d.ts.map +1 -0
- package/dist/browser-tools/select-option.js +25 -0
- package/dist/browser-tools/select-option.js.map +1 -0
- package/dist/browser-tools/set-cookie.d.ts +16 -0
- package/dist/browser-tools/set-cookie.d.ts.map +1 -0
- package/dist/browser-tools/set-cookie.js +42 -0
- package/dist/browser-tools/set-cookie.js.map +1 -0
- package/dist/browser-tools/type-text.d.ts +12 -0
- package/dist/browser-tools/type-text.d.ts.map +1 -0
- package/dist/browser-tools/type-text.js +25 -0
- package/dist/browser-tools/type-text.js.map +1 -0
- package/dist/browser-tools/url-validation.d.ts +13 -0
- package/dist/browser-tools/url-validation.d.ts.map +1 -0
- package/dist/browser-tools/url-validation.js +23 -0
- package/dist/browser-tools/url-validation.js.map +1 -0
- package/dist/browser-tools/wait-for-element.d.ts +12 -0
- package/dist/browser-tools/wait-for-element.d.ts.map +1 -0
- package/dist/browser-tools/wait-for-element.js +29 -0
- package/dist/browser-tools/wait-for-element.js.map +1 -0
- package/dist/config.d.ts +99 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +344 -0
- package/dist/config.js.map +1 -0
- package/dist/dev-mode.d.ts +14 -0
- package/dist/dev-mode.d.ts.map +1 -0
- package/dist/dev-mode.js +15 -0
- package/dist/dev-mode.js.map +1 -0
- package/dist/discovery-legacy.d.ts +32 -0
- package/dist/discovery-legacy.d.ts.map +1 -0
- package/dist/discovery-legacy.js +415 -0
- package/dist/discovery-legacy.js.map +1 -0
- package/dist/discovery.d.ts +28 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +97 -0
- package/dist/discovery.js.map +1 -0
- package/dist/extension-install.d.ts +27 -0
- package/dist/extension-install.d.ts.map +1 -0
- package/dist/extension-install.js +75 -0
- package/dist/extension-install.js.map +1 -0
- package/dist/extension-protocol.d.ts +130 -0
- package/dist/extension-protocol.d.ts.map +1 -0
- package/dist/extension-protocol.js +869 -0
- package/dist/extension-protocol.js.map +1 -0
- package/dist/file-watcher.d.ts +75 -0
- package/dist/file-watcher.d.ts.map +1 -0
- package/dist/file-watcher.js +616 -0
- package/dist/file-watcher.js.map +1 -0
- package/dist/http-routes.d.ts +88 -0
- package/dist/http-routes.d.ts.map +1 -0
- package/dist/http-routes.js +545 -0
- package/dist/http-routes.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +187 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +100 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +402 -0
- package/dist/loader.js.map +1 -0
- package/dist/log-buffer.d.ts +33 -0
- package/dist/log-buffer.d.ts.map +1 -0
- package/dist/log-buffer.js +64 -0
- package/dist/log-buffer.js.map +1 -0
- package/dist/logger.d.ts +34 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +81 -0
- package/dist/logger.js.map +1 -0
- package/dist/manifest-schema.d.ts +14 -0
- package/dist/manifest-schema.d.ts.map +1 -0
- package/dist/manifest-schema.js +51 -0
- package/dist/manifest-schema.js.map +1 -0
- package/dist/mcp-setup.d.ts +131 -0
- package/dist/mcp-setup.d.ts.map +1 -0
- package/dist/mcp-setup.js +673 -0
- package/dist/mcp-setup.js.map +1 -0
- package/dist/permissions.d.ts +59 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +141 -0
- package/dist/permissions.js.map +1 -0
- package/dist/registry.d.ts +78 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +187 -0
- package/dist/registry.js.map +1 -0
- package/dist/reload.d.ts +52 -0
- package/dist/reload.d.ts.map +1 -0
- package/dist/reload.js +326 -0
- package/dist/reload.js.map +1 -0
- package/dist/resolver.d.ts +53 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +272 -0
- package/dist/resolver.js.map +1 -0
- package/dist/sanitize-error.d.ts +8 -0
- package/dist/sanitize-error.d.ts.map +1 -0
- package/dist/sanitize-error.js +25 -0
- package/dist/sanitize-error.js.map +1 -0
- package/dist/sanitize-tool-output.d.ts +20 -0
- package/dist/sanitize-tool-output.d.ts.map +1 -0
- package/dist/sanitize-tool-output.js +52 -0
- package/dist/sanitize-tool-output.js.map +1 -0
- package/dist/sdk-version.d.ts +11 -0
- package/dist/sdk-version.d.ts.map +1 -0
- package/dist/sdk-version.js +23 -0
- package/dist/sdk-version.js.map +1 -0
- package/dist/shutdown.d.ts +28 -0
- package/dist/shutdown.d.ts.map +1 -0
- package/dist/shutdown.js +68 -0
- package/dist/shutdown.js.map +1 -0
- package/dist/skip-confirmation.d.ts +15 -0
- package/dist/skip-confirmation.d.ts.map +1 -0
- package/dist/skip-confirmation.js +16 -0
- package/dist/skip-confirmation.js.map +1 -0
- package/dist/skip-sanitization.d.ts +17 -0
- package/dist/skip-sanitization.d.ts.map +1 -0
- package/dist/skip-sanitization.js +18 -0
- package/dist/skip-sanitization.js.map +1 -0
- package/dist/skip-verification.d.ts +11 -0
- package/dist/skip-verification.d.ts.map +1 -0
- package/dist/skip-verification.js +12 -0
- package/dist/skip-verification.js.map +1 -0
- package/dist/state.d.ts +290 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +111 -0
- package/dist/state.js.map +1 -0
- package/dist/verify-plugin.d.ts +53 -0
- package/dist/verify-plugin.d.ts.map +1 -0
- package/dist/verify-plugin.js +123 -0
- package/dist/verify-plugin.js.map +1 -0
- package/dist/version-check.d.ts +35 -0
- package/dist/version-check.d.ts.map +1 -0
- package/dist/version-check.js +111 -0
- package/dist/version-check.js.map +1 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +22 -0
- package/dist/version.js.map +1 -0
- package/package.json +28 -0
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension WebSocket protocol handler.
|
|
3
|
+
* Handles JSON-RPC messages between the MCP server and Chrome extension.
|
|
4
|
+
*/
|
|
5
|
+
import { getAdaptersDir } from './config.js';
|
|
6
|
+
import { appendLog } from './log-buffer.js';
|
|
7
|
+
import { log } from './logger.js';
|
|
8
|
+
import { prefixedToolName, isToolEnabled, getNextRequestId, DISPATCH_TIMEOUT_MS, MAX_DISPATCH_TIMEOUT_MS, CONFIRMATION_TIMEOUT_MS, } from './state.js';
|
|
9
|
+
import { mkdir, readdir, rename } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
/**
|
|
12
|
+
* Ensure the adapters directory exists, creating it if necessary.
|
|
13
|
+
* Caches the result so mkdir is called at most once per module evaluation
|
|
14
|
+
* (resets on bun --hot reload, which is correct since the config dir could change).
|
|
15
|
+
*/
|
|
16
|
+
let adaptersDirReady = false;
|
|
17
|
+
const ensureAdaptersDir = async () => {
|
|
18
|
+
if (adaptersDirReady)
|
|
19
|
+
return;
|
|
20
|
+
await mkdir(getAdaptersDir(), { recursive: true });
|
|
21
|
+
adaptersDirReady = true;
|
|
22
|
+
};
|
|
23
|
+
/** Maximum incoming WebSocket message size (10MB) */
|
|
24
|
+
const MAX_MESSAGE_SIZE = 10 * 1024 * 1024;
|
|
25
|
+
/** Prefix for dynamically generated exec script files */
|
|
26
|
+
const EXEC_FILE_PREFIX = '__exec-';
|
|
27
|
+
/**
|
|
28
|
+
* Send a JSON-serialized message to the extension WebSocket if connected.
|
|
29
|
+
* Centralizes the null check on state.extensionWs so callers don't repeat it.
|
|
30
|
+
* Returns true if the message was sent, false if the extension is not connected.
|
|
31
|
+
*/
|
|
32
|
+
const sendToExtension = (state, msg) => {
|
|
33
|
+
if (!state.extensionWs)
|
|
34
|
+
return false;
|
|
35
|
+
try {
|
|
36
|
+
state.extensionWs.send(JSON.stringify(msg));
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
log.warn('Failed to send to extension:', err);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Write a plugin's adapter IIFE to the extension's adapters/ directory.
|
|
46
|
+
* The extension injects adapters via chrome.scripting.executeScript({ files: [...] })
|
|
47
|
+
* using these files, bypassing page CSP restrictions.
|
|
48
|
+
*
|
|
49
|
+
* If a source map is provided, writes it alongside the adapter as {pluginName}.js.map
|
|
50
|
+
* and rewrites the sourceMappingURL in the IIFE to reference the per-plugin filename
|
|
51
|
+
* (prevents collisions when multiple plugins are loaded).
|
|
52
|
+
*/
|
|
53
|
+
const writeAdapterFile = async (pluginName, iife, sourceMap) => {
|
|
54
|
+
const adaptersDir = getAdaptersDir();
|
|
55
|
+
const finalPath = join(adaptersDir, `${pluginName}.js`);
|
|
56
|
+
const tmpPath = join(adaptersDir, `${pluginName}.js.tmp`);
|
|
57
|
+
let content = iife;
|
|
58
|
+
if (sourceMap) {
|
|
59
|
+
// Rewrite sourceMappingURL to use the per-plugin filename
|
|
60
|
+
content = iife.replace(/\/\/# sourceMappingURL=adapter\.iife\.js\.map/, `//# sourceMappingURL=${pluginName}.js.map`);
|
|
61
|
+
// Write source map atomically
|
|
62
|
+
const mapFinalPath = join(adaptersDir, `${pluginName}.js.map`);
|
|
63
|
+
const mapTmpPath = join(adaptersDir, `${pluginName}.js.map.tmp`);
|
|
64
|
+
await Bun.write(mapTmpPath, sourceMap);
|
|
65
|
+
await rename(mapTmpPath, mapFinalPath);
|
|
66
|
+
}
|
|
67
|
+
await Bun.write(tmpPath, content);
|
|
68
|
+
await rename(tmpPath, finalPath);
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Remove stale adapter .js files from the adapters directory that do not
|
|
72
|
+
* correspond to any plugin in the current set. Called from sendSyncFull
|
|
73
|
+
* before writing new adapter files.
|
|
74
|
+
*/
|
|
75
|
+
const cleanupStaleAdapterFiles = async (currentPluginNames) => {
|
|
76
|
+
const adaptersDir = getAdaptersDir();
|
|
77
|
+
let entries;
|
|
78
|
+
try {
|
|
79
|
+
entries = await readdir(adaptersDir);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Directory may not exist yet on first run
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const staleFiles = entries.filter(f => {
|
|
86
|
+
if (f.endsWith('.js.tmp') || f.endsWith('.js.map.tmp'))
|
|
87
|
+
return false;
|
|
88
|
+
if (f.startsWith(EXEC_FILE_PREFIX))
|
|
89
|
+
return false; // Managed by cleanupStaleExecFiles
|
|
90
|
+
// Match adapter .js files
|
|
91
|
+
if (f.endsWith('.js')) {
|
|
92
|
+
const pluginName = f.slice(0, -3); // strip '.js'
|
|
93
|
+
return !currentPluginNames.has(pluginName);
|
|
94
|
+
}
|
|
95
|
+
// Match adapter .js.map source map files
|
|
96
|
+
if (f.endsWith('.js.map')) {
|
|
97
|
+
const pluginName = f.slice(0, -7); // strip '.js.map'
|
|
98
|
+
return !currentPluginNames.has(pluginName);
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
});
|
|
102
|
+
if (staleFiles.length === 0)
|
|
103
|
+
return;
|
|
104
|
+
const results = await Promise.allSettled(staleFiles.map(f => Bun.file(join(adaptersDir, f)).delete()));
|
|
105
|
+
let deleted = 0;
|
|
106
|
+
for (const [i, result] of results.entries()) {
|
|
107
|
+
if (result.status === 'rejected') {
|
|
108
|
+
const fileName = staleFiles[i] ?? 'unknown';
|
|
109
|
+
log.warn(`Failed to delete stale adapter file ${fileName}:`, result.reason);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
deleted++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
log.info(`Cleaned up ${deleted} stale adapter file(s)`);
|
|
116
|
+
};
|
|
117
|
+
/** Timeout for batch adapter file writes in sendSyncFull (10 seconds) */
|
|
118
|
+
const ADAPTER_WRITE_TIMEOUT_MS = 10_000;
|
|
119
|
+
/** Create a cancellable timeout promise for use with Promise.race */
|
|
120
|
+
const timeoutRace = (value, ms) => {
|
|
121
|
+
let timerId;
|
|
122
|
+
const promise = new Promise(resolve => {
|
|
123
|
+
timerId = setTimeout(() => resolve(value), ms);
|
|
124
|
+
});
|
|
125
|
+
// timerId is assigned synchronously by the Promise executor
|
|
126
|
+
const cancel = () => clearTimeout(timerId);
|
|
127
|
+
return { promise, cancel };
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Send sync.full notification to extension on connect.
|
|
131
|
+
* Writes all plugin adapter IIFEs to the extension's adapters/ directory,
|
|
132
|
+
* then sends plugin metadata (without IIFE content) to the extension.
|
|
133
|
+
*/
|
|
134
|
+
const sendSyncFull = async (state) => {
|
|
135
|
+
// Write all adapter IIFEs to disk so the extension can inject them as files.
|
|
136
|
+
// Uses allSettled so a single plugin's write failure doesn't block the sync notification.
|
|
137
|
+
// Races against a timeout so stalled writes don't hang hot reload indefinitely.
|
|
138
|
+
const pluginList = Array.from(state.registry.plugins.values());
|
|
139
|
+
await ensureAdaptersDir();
|
|
140
|
+
// Remove stale adapter files from plugins that are no longer in the current set
|
|
141
|
+
const currentPluginNames = new Set(pluginList.map(p => p.name));
|
|
142
|
+
await cleanupStaleAdapterFiles(currentPluginNames);
|
|
143
|
+
const writePromise = Promise.allSettled(pluginList.map(p => writeAdapterFile(p.name, p.iife, p.iifeSourceMap)));
|
|
144
|
+
const timeout = timeoutRace(null, ADAPTER_WRITE_TIMEOUT_MS);
|
|
145
|
+
const writeResults = await Promise.race([writePromise, timeout.promise]);
|
|
146
|
+
timeout.cancel();
|
|
147
|
+
if (writeResults === null) {
|
|
148
|
+
log.warn(`Adapter file writes did not complete within ${ADAPTER_WRITE_TIMEOUT_MS}ms — sending sync.full with available adapters. Pending plugins: ${pluginList.map(p => p.name).join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
for (const [i, result] of writeResults.entries()) {
|
|
152
|
+
if (result.status === 'rejected') {
|
|
153
|
+
const plugin = pluginList[i];
|
|
154
|
+
log.warn(`Failed to write adapter file for ${plugin?.name ?? 'unknown'}:`, result.reason);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const plugins = pluginList.map(p => ({
|
|
159
|
+
name: p.name,
|
|
160
|
+
version: p.version,
|
|
161
|
+
displayName: p.displayName,
|
|
162
|
+
urlPatterns: p.urlPatterns,
|
|
163
|
+
trustTier: p.trustTier,
|
|
164
|
+
sourcePath: p.sourcePath,
|
|
165
|
+
adapterHash: p.adapterHash,
|
|
166
|
+
...(p.iconSvg ? { iconSvg: p.iconSvg } : {}),
|
|
167
|
+
...(p.iconInactiveSvg ? { iconInactiveSvg: p.iconInactiveSvg } : {}),
|
|
168
|
+
tools: p.tools.map(t => ({
|
|
169
|
+
name: t.name,
|
|
170
|
+
displayName: t.displayName,
|
|
171
|
+
description: t.description,
|
|
172
|
+
icon: t.icon,
|
|
173
|
+
...(t.iconSvg ? { iconSvg: t.iconSvg } : {}),
|
|
174
|
+
...(t.iconInactiveSvg ? { iconInactiveSvg: t.iconInactiveSvg } : {}),
|
|
175
|
+
enabled: isToolEnabled(state, prefixedToolName(p.name, t.name)),
|
|
176
|
+
})),
|
|
177
|
+
}));
|
|
178
|
+
const sent = sendToExtension(state, {
|
|
179
|
+
jsonrpc: '2.0',
|
|
180
|
+
method: 'sync.full',
|
|
181
|
+
params: { plugins },
|
|
182
|
+
});
|
|
183
|
+
if (!sent)
|
|
184
|
+
log.warn('Failed to send sync.full — extension not connected');
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Send a JSON-RPC request to the extension and return a promise for the response.
|
|
188
|
+
* Unified dispatch for both browser commands (browser.*, extension.*) and
|
|
189
|
+
* plugin tool dispatches (tool.dispatch).
|
|
190
|
+
*/
|
|
191
|
+
const dispatchToExtension = (state, method, params, options) => {
|
|
192
|
+
// Backward-compatible: options can be a string (label) for existing callers
|
|
193
|
+
const opts = typeof options === 'string' ? { label: options } : (options ?? {});
|
|
194
|
+
const ws = state.extensionWs;
|
|
195
|
+
if (!ws) {
|
|
196
|
+
return Promise.reject(new Error('Extension not connected'));
|
|
197
|
+
}
|
|
198
|
+
const id = getNextRequestId();
|
|
199
|
+
const msg = {
|
|
200
|
+
jsonrpc: '2.0',
|
|
201
|
+
method,
|
|
202
|
+
params: { ...params, dispatchId: id },
|
|
203
|
+
id,
|
|
204
|
+
};
|
|
205
|
+
const dispatchLabel = opts.label ?? method;
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const timerId = setTimeout(() => {
|
|
208
|
+
if (state.pendingDispatches.has(id)) {
|
|
209
|
+
state.pendingDispatches.delete(id);
|
|
210
|
+
reject(new Error(`Dispatch ${dispatchLabel} timed out after ${DISPATCH_TIMEOUT_MS}ms`));
|
|
211
|
+
}
|
|
212
|
+
}, DISPATCH_TIMEOUT_MS);
|
|
213
|
+
const pending = {
|
|
214
|
+
resolve,
|
|
215
|
+
reject,
|
|
216
|
+
label: dispatchLabel,
|
|
217
|
+
startTs: Date.now(),
|
|
218
|
+
timerId,
|
|
219
|
+
progressToken: opts.progressToken,
|
|
220
|
+
onProgress: opts.onProgress,
|
|
221
|
+
};
|
|
222
|
+
state.pendingDispatches.set(id, pending);
|
|
223
|
+
log.debug('dispatch → extension:', method, 'id:', id);
|
|
224
|
+
try {
|
|
225
|
+
ws.send(JSON.stringify(msg));
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
clearTimeout(timerId);
|
|
229
|
+
state.pendingDispatches.delete(id);
|
|
230
|
+
reject(new Error(`WebSocket send failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* Send tool.invocationStart notification to extension (for side panel animation).
|
|
236
|
+
*/
|
|
237
|
+
const sendInvocationStart = (state, plugin, tool) => {
|
|
238
|
+
sendToExtension(state, {
|
|
239
|
+
jsonrpc: '2.0',
|
|
240
|
+
method: 'tool.invocationStart',
|
|
241
|
+
params: { plugin, tool, ts: Date.now() },
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* Send tool.invocationEnd notification to extension (for side panel animation).
|
|
246
|
+
*/
|
|
247
|
+
const sendInvocationEnd = (state, plugin, tool, durationMs, success) => {
|
|
248
|
+
sendToExtension(state, {
|
|
249
|
+
jsonrpc: '2.0',
|
|
250
|
+
method: 'tool.invocationEnd',
|
|
251
|
+
params: { plugin, tool, durationMs, success },
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* Send a confirmation request to the extension and return a promise that resolves
|
|
256
|
+
* with the user's decision. The promise rejects on timeout (30s) or extension disconnect.
|
|
257
|
+
*
|
|
258
|
+
* @param state - Server state
|
|
259
|
+
* @param tool - Browser tool name (e.g., 'browser_execute_script')
|
|
260
|
+
* @param domain - Target domain (e.g., 'mail.google.com'), or null
|
|
261
|
+
* @param tabId - Target tab ID, if applicable
|
|
262
|
+
* @param paramsPreview - Truncated preview of tool parameters
|
|
263
|
+
* @returns The user's decision: 'allow_once', 'allow_always', or 'deny'
|
|
264
|
+
*/
|
|
265
|
+
const sendConfirmationRequest = (state, tool, domain, tabId, paramsPreview) => {
|
|
266
|
+
const id = crypto.randomUUID();
|
|
267
|
+
return new Promise((resolve, reject) => {
|
|
268
|
+
const timerId = setTimeout(() => {
|
|
269
|
+
state.pendingConfirmations.delete(id);
|
|
270
|
+
reject(new Error('CONFIRMATION_TIMEOUT'));
|
|
271
|
+
}, CONFIRMATION_TIMEOUT_MS);
|
|
272
|
+
state.pendingConfirmations.set(id, {
|
|
273
|
+
resolve,
|
|
274
|
+
reject,
|
|
275
|
+
timerId,
|
|
276
|
+
tool,
|
|
277
|
+
domain,
|
|
278
|
+
tabId,
|
|
279
|
+
});
|
|
280
|
+
const sent = sendToExtension(state, {
|
|
281
|
+
jsonrpc: '2.0',
|
|
282
|
+
method: 'confirmation.request',
|
|
283
|
+
params: {
|
|
284
|
+
id,
|
|
285
|
+
tool,
|
|
286
|
+
domain,
|
|
287
|
+
tabId,
|
|
288
|
+
paramsPreview,
|
|
289
|
+
timeoutMs: CONFIRMATION_TIMEOUT_MS,
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
if (!sent) {
|
|
293
|
+
clearTimeout(timerId);
|
|
294
|
+
state.pendingConfirmations.delete(id);
|
|
295
|
+
reject(new Error('Extension not connected — cannot request confirmation'));
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Handle a confirmation.response from the extension.
|
|
301
|
+
* Resolves the pending confirmation promise with the user's decision.
|
|
302
|
+
* For 'allow_always', also adds a session permission rule.
|
|
303
|
+
*/
|
|
304
|
+
const handleConfirmationResponse = (state, params) => {
|
|
305
|
+
if (!params)
|
|
306
|
+
return;
|
|
307
|
+
const id = params.id;
|
|
308
|
+
if (typeof id !== 'string')
|
|
309
|
+
return;
|
|
310
|
+
const decision = params.decision;
|
|
311
|
+
if (decision !== 'allow_once' && decision !== 'allow_always' && decision !== 'deny')
|
|
312
|
+
return;
|
|
313
|
+
const pending = state.pendingConfirmations.get(id);
|
|
314
|
+
if (!pending)
|
|
315
|
+
return;
|
|
316
|
+
clearTimeout(pending.timerId);
|
|
317
|
+
state.pendingConfirmations.delete(id);
|
|
318
|
+
// For allow_always, add a session permission rule based on the scope
|
|
319
|
+
if (decision === 'allow_always') {
|
|
320
|
+
const scope = typeof params.scope === 'string' ? params.scope : 'tool_domain';
|
|
321
|
+
const rule = { tool: pending.tool, domain: pending.domain, scope };
|
|
322
|
+
// Adjust rule fields based on scope
|
|
323
|
+
if (scope === 'tool_all') {
|
|
324
|
+
rule.domain = null;
|
|
325
|
+
}
|
|
326
|
+
else if (scope === 'domain_all') {
|
|
327
|
+
rule.tool = null;
|
|
328
|
+
}
|
|
329
|
+
state.sessionPermissions.push(rule);
|
|
330
|
+
}
|
|
331
|
+
pending.resolve(decision);
|
|
332
|
+
};
|
|
333
|
+
/**
|
|
334
|
+
* Reject all pending confirmations. Called when the extension disconnects
|
|
335
|
+
* to clean up any confirmation promises that can no longer be fulfilled.
|
|
336
|
+
*/
|
|
337
|
+
const rejectAllPendingConfirmations = (state) => {
|
|
338
|
+
for (const [id, pending] of state.pendingConfirmations) {
|
|
339
|
+
clearTimeout(pending.timerId);
|
|
340
|
+
pending.reject(new Error('Extension disconnected — confirmation cancelled'));
|
|
341
|
+
state.pendingConfirmations.delete(id);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Send plugin.update notification to extension with updated plugin metadata.
|
|
346
|
+
* Writes the adapter IIFE to the extension's adapters/ directory first,
|
|
347
|
+
* then sends the notification (without IIFE content) to the extension.
|
|
348
|
+
*
|
|
349
|
+
* Used by file watcher when a local plugin's manifest or IIFE changes on disk,
|
|
350
|
+
* and by the hot reload sequence after re-discovery.
|
|
351
|
+
*
|
|
352
|
+
* Sent as a JSON-RPC notification (no id, no response expected). The extension
|
|
353
|
+
* processes the update and re-injects the adapter into matching tabs.
|
|
354
|
+
*/
|
|
355
|
+
const sendPluginUpdate = async (state, pluginName, iife, sourceMap) => {
|
|
356
|
+
const plugin = state.registry.plugins.get(pluginName);
|
|
357
|
+
if (!plugin)
|
|
358
|
+
return;
|
|
359
|
+
await ensureAdaptersDir();
|
|
360
|
+
await writeAdapterFile(pluginName, iife, sourceMap);
|
|
361
|
+
const sent = sendToExtension(state, {
|
|
362
|
+
jsonrpc: '2.0',
|
|
363
|
+
method: 'plugin.update',
|
|
364
|
+
params: {
|
|
365
|
+
name: plugin.name,
|
|
366
|
+
version: plugin.version,
|
|
367
|
+
displayName: plugin.displayName,
|
|
368
|
+
urlPatterns: plugin.urlPatterns,
|
|
369
|
+
trustTier: plugin.trustTier,
|
|
370
|
+
sourcePath: plugin.sourcePath,
|
|
371
|
+
adapterHash: plugin.adapterHash,
|
|
372
|
+
...(plugin.iconSvg ? { iconSvg: plugin.iconSvg } : {}),
|
|
373
|
+
...(plugin.iconInactiveSvg ? { iconInactiveSvg: plugin.iconInactiveSvg } : {}),
|
|
374
|
+
tools: plugin.tools.map(t => ({
|
|
375
|
+
name: t.name,
|
|
376
|
+
displayName: t.displayName,
|
|
377
|
+
description: t.description,
|
|
378
|
+
icon: t.icon,
|
|
379
|
+
...(t.iconSvg ? { iconSvg: t.iconSvg } : {}),
|
|
380
|
+
...(t.iconInactiveSvg ? { iconInactiveSvg: t.iconInactiveSvg } : {}),
|
|
381
|
+
enabled: isToolEnabled(state, prefixedToolName(plugin.name, t.name)),
|
|
382
|
+
})),
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
if (!sent)
|
|
386
|
+
log.warn('Failed to send plugin.update — extension not connected');
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* Handle an incoming WebSocket message from the extension.
|
|
390
|
+
* Routes to the appropriate handler based on method/id.
|
|
391
|
+
*
|
|
392
|
+
* @param senderWs - The raw WebSocket that sent this message. Used to reply
|
|
393
|
+
* pongs on the exact connection that pinged, preventing race conditions
|
|
394
|
+
* during hot reload when two connections may briefly coexist.
|
|
395
|
+
*/
|
|
396
|
+
const handleExtensionMessage = (state, text, callbacks, senderWs) => {
|
|
397
|
+
if (text.length > MAX_MESSAGE_SIZE) {
|
|
398
|
+
log.warn(`Dropping oversized WebSocket message (${(text.length / 1024 / 1024).toFixed(1)}MB, limit ${MAX_MESSAGE_SIZE / 1024 / 1024}MB)`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
let parsed;
|
|
402
|
+
try {
|
|
403
|
+
parsed = JSON.parse(text);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
log.warn('Dropping malformed WebSocket message (invalid JSON)');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const method = typeof parsed.method === 'string' ? parsed.method : undefined;
|
|
410
|
+
const id = typeof parsed.id === 'string' || typeof parsed.id === 'number' ? parsed.id : undefined;
|
|
411
|
+
// Handle ping keepalive — reply on the SAME ws that sent the ping.
|
|
412
|
+
// This is critical during hot reload: if the old connection sends a ping
|
|
413
|
+
// before it's closed, the pong must go back on that connection (not the
|
|
414
|
+
// new one stored in state.extensionWs).
|
|
415
|
+
if (method === 'ping') {
|
|
416
|
+
const replyWs = senderWs ?? state.extensionWs;
|
|
417
|
+
replyWs?.send(JSON.stringify({ jsonrpc: '2.0', method: 'pong' }));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
// Handle responses to our requests (tool.dispatch responses)
|
|
421
|
+
if (id !== undefined && !method) {
|
|
422
|
+
const pending = state.pendingDispatches.get(id);
|
|
423
|
+
if (!pending)
|
|
424
|
+
return;
|
|
425
|
+
state.pendingDispatches.delete(id);
|
|
426
|
+
clearTimeout(pending.timerId);
|
|
427
|
+
log.debug('dispatch ← extension:', pending.label, 'id:', id, 'in', `${Date.now() - pending.startTs}ms`);
|
|
428
|
+
if ('error' in parsed) {
|
|
429
|
+
const err = parsed.error;
|
|
430
|
+
const error = new DispatchError(err.message, err.code, err.data);
|
|
431
|
+
pending.reject(error);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
pending.resolve(parsed.result);
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
// Validate params: must be a plain object (or undefined/null) per JSON-RPC spec.
|
|
439
|
+
// Reject arrays, primitives, and other non-object types before method handlers.
|
|
440
|
+
const rawParams = parsed.params;
|
|
441
|
+
if (rawParams !== undefined && rawParams !== null && (typeof rawParams !== 'object' || Array.isArray(rawParams))) {
|
|
442
|
+
log.warn(`Dropping message with non-object params: ${method ?? '(no method)'}`);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const params = rawParams;
|
|
446
|
+
// Handle notifications/requests from extension
|
|
447
|
+
if (method === 'tab.syncAll') {
|
|
448
|
+
handleTabSyncAll(state, params);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (method === 'tab.stateChanged') {
|
|
452
|
+
handleTabStateChanged(state, params, id);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// Handle config operations (requests with id from side panel, relayed through extension)
|
|
456
|
+
if (method === 'config.getState' && id !== undefined) {
|
|
457
|
+
handleConfigGetState(state, id);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (method === 'config.setToolEnabled' && id !== undefined) {
|
|
461
|
+
handleConfigSetToolEnabled(state, params, id, callbacks);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (method === 'config.setAllToolsEnabled' && id !== undefined) {
|
|
465
|
+
handleConfigSetAllToolsEnabled(state, params, id, callbacks);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (method === 'tool.progress') {
|
|
469
|
+
handleToolProgress(state, params);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (method === 'plugin.log') {
|
|
473
|
+
handlePluginLog(params, callbacks);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (method === 'confirmation.response') {
|
|
477
|
+
handleConfirmationResponse(state, params);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// Unrecognized method with an id — send JSON-RPC -32601 'Method not found'
|
|
481
|
+
if (id !== undefined && method) {
|
|
482
|
+
sendToExtension(state, {
|
|
483
|
+
jsonrpc: '2.0',
|
|
484
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
485
|
+
id,
|
|
486
|
+
});
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
// Unrecognized notification or malformed message
|
|
490
|
+
if (method) {
|
|
491
|
+
log.warn(`Ignoring unrecognized notification: ${method}`);
|
|
492
|
+
}
|
|
493
|
+
else if (id === undefined) {
|
|
494
|
+
log.warn('Dropping unrecognized WebSocket message (no method, no id)');
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
/**
|
|
498
|
+
* Error class for tool dispatch errors with JSON-RPC error codes.
|
|
499
|
+
*
|
|
500
|
+
* Not exported — consumers must use isDispatchError() for type narrowing.
|
|
501
|
+
* Under bun --hot, each module re-evaluation creates a new class identity,
|
|
502
|
+
* so `instanceof DispatchError` fails across reload boundaries. The duck-typed
|
|
503
|
+
* isDispatchError() guard is co-located here to make the correct approach
|
|
504
|
+
* obvious and the incorrect approach (importing the class) impossible.
|
|
505
|
+
*/
|
|
506
|
+
class DispatchError extends Error {
|
|
507
|
+
code;
|
|
508
|
+
data;
|
|
509
|
+
constructor(message, code, data) {
|
|
510
|
+
super(message);
|
|
511
|
+
this.code = code;
|
|
512
|
+
this.data = data;
|
|
513
|
+
this.name = 'DispatchError';
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Check if an error is a DispatchError by duck-typing rather than instanceof.
|
|
518
|
+
*
|
|
519
|
+
* Under bun --hot, each module re-evaluation creates a new DispatchError class.
|
|
520
|
+
* If a tool dispatch timeout fires after hot reload, the error is an instance of
|
|
521
|
+
* the OLD module's DispatchError class. Using `instanceof` against the NEW
|
|
522
|
+
* module's DispatchError would fail, causing the error to fall through to the
|
|
523
|
+
* generic catch branch and lose the specific error code/message. Duck-typing
|
|
524
|
+
* avoids this cross-reload class identity problem.
|
|
525
|
+
*/
|
|
526
|
+
const isDispatchError = (err) => err !== null &&
|
|
527
|
+
typeof err === 'object' &&
|
|
528
|
+
'code' in err &&
|
|
529
|
+
'name' in err &&
|
|
530
|
+
err.name === 'DispatchError';
|
|
531
|
+
const parseTabMapping = (wire) => ({
|
|
532
|
+
state: wire.state === 'closed' || wire.state === 'unavailable' || wire.state === 'ready' ? wire.state : 'closed',
|
|
533
|
+
tabId: typeof wire.tabId === 'number' ? wire.tabId : null,
|
|
534
|
+
url: typeof wire.url === 'string' ? wire.url : null,
|
|
535
|
+
});
|
|
536
|
+
const handleTabSyncAll = (state, params) => {
|
|
537
|
+
if (!params)
|
|
538
|
+
return;
|
|
539
|
+
const tabSyncParams = params;
|
|
540
|
+
const tabs = tabSyncParams.tabs;
|
|
541
|
+
if (!tabs)
|
|
542
|
+
return;
|
|
543
|
+
state.tabMapping.clear();
|
|
544
|
+
for (const [pluginName, mapping] of Object.entries(tabs)) {
|
|
545
|
+
state.tabMapping.set(pluginName, parseTabMapping(mapping));
|
|
546
|
+
}
|
|
547
|
+
log.info(`tab.syncAll received — ${state.tabMapping.size} plugin(s) mapped`);
|
|
548
|
+
};
|
|
549
|
+
const handleTabStateChanged = (state, params, id) => {
|
|
550
|
+
const sendError = (message) => {
|
|
551
|
+
if (id !== undefined) {
|
|
552
|
+
sendToExtension(state, { jsonrpc: '2.0', error: { code: -32602, message }, id });
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
log.warn(`tab.stateChanged: ${message}`);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
if (!params) {
|
|
559
|
+
sendError('Missing params');
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const plugin = params.plugin;
|
|
563
|
+
if (typeof plugin !== 'string' || plugin.length === 0) {
|
|
564
|
+
sendError('Missing or invalid "plugin" field (expected non-empty string)');
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (!state.registry.plugins.has(plugin)) {
|
|
568
|
+
sendError(`Unknown plugin: ${plugin}`);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (typeof params.state !== 'string') {
|
|
572
|
+
sendError('Missing or invalid "state" field (expected string)');
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (params.state !== 'closed' && params.state !== 'unavailable' && params.state !== 'ready') {
|
|
576
|
+
sendError(`Invalid tab state: ${params.state} (expected closed, unavailable, or ready)`);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const wire = {
|
|
580
|
+
state: params.state,
|
|
581
|
+
tabId: typeof params.tabId === 'number' ? params.tabId : null,
|
|
582
|
+
url: typeof params.url === 'string' ? params.url : null,
|
|
583
|
+
};
|
|
584
|
+
state.tabMapping.set(plugin, parseTabMapping(wire));
|
|
585
|
+
log.info(`tab.stateChanged: ${plugin} → ${wire.state ?? 'unknown'}`);
|
|
586
|
+
};
|
|
587
|
+
const handleConfigGetState = (state, id) => {
|
|
588
|
+
const plugins = Array.from(state.registry.plugins.values())
|
|
589
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
590
|
+
.map(p => {
|
|
591
|
+
const tabInfo = state.tabMapping.get(p.name);
|
|
592
|
+
return {
|
|
593
|
+
name: p.name,
|
|
594
|
+
displayName: p.displayName,
|
|
595
|
+
version: p.version,
|
|
596
|
+
trustTier: p.trustTier,
|
|
597
|
+
source: p.source,
|
|
598
|
+
tabState: tabInfo?.state ?? 'closed',
|
|
599
|
+
urlPatterns: p.urlPatterns,
|
|
600
|
+
...(p.sdkVersion ? { sdkVersion: p.sdkVersion } : {}),
|
|
601
|
+
...(p.iconSvg ? { iconSvg: p.iconSvg } : {}),
|
|
602
|
+
...(p.iconInactiveSvg ? { iconInactiveSvg: p.iconInactiveSvg } : {}),
|
|
603
|
+
tools: p.tools.map(t => ({
|
|
604
|
+
name: t.name,
|
|
605
|
+
displayName: t.displayName,
|
|
606
|
+
description: t.description,
|
|
607
|
+
icon: t.icon,
|
|
608
|
+
...(t.iconSvg ? { iconSvg: t.iconSvg } : {}),
|
|
609
|
+
...(t.iconInactiveSvg ? { iconInactiveSvg: t.iconInactiveSvg } : {}),
|
|
610
|
+
enabled: isToolEnabled(state, prefixedToolName(p.name, t.name)),
|
|
611
|
+
})),
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
sendToExtension(state, {
|
|
615
|
+
jsonrpc: '2.0',
|
|
616
|
+
result: {
|
|
617
|
+
plugins,
|
|
618
|
+
failedPlugins: state.discoveryErrors.map(e => ({ specifier: e.specifier, error: e.error })),
|
|
619
|
+
outdatedPlugins: state.outdatedPlugins,
|
|
620
|
+
},
|
|
621
|
+
id,
|
|
622
|
+
});
|
|
623
|
+
};
|
|
624
|
+
const handleConfigSetToolEnabled = (state, params, id, callbacks) => {
|
|
625
|
+
if (!params) {
|
|
626
|
+
sendToExtension(state, { jsonrpc: '2.0', error: { code: -32602, message: 'Missing params' }, id });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const toolEnabledParams = params;
|
|
630
|
+
const pluginName = toolEnabledParams.plugin;
|
|
631
|
+
const tool = toolEnabledParams.tool;
|
|
632
|
+
const enabled = toolEnabledParams.enabled;
|
|
633
|
+
if (typeof pluginName !== 'string' || typeof tool !== 'string' || typeof enabled !== 'boolean') {
|
|
634
|
+
sendToExtension(state, {
|
|
635
|
+
jsonrpc: '2.0',
|
|
636
|
+
error: { code: -32602, message: 'Invalid params: expected plugin (string), tool (string), enabled (boolean)' },
|
|
637
|
+
id,
|
|
638
|
+
});
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const plugin = state.registry.plugins.get(pluginName);
|
|
642
|
+
if (!plugin) {
|
|
643
|
+
sendToExtension(state, {
|
|
644
|
+
jsonrpc: '2.0',
|
|
645
|
+
error: { code: -32602, message: `Plugin not found: ${pluginName}` },
|
|
646
|
+
id,
|
|
647
|
+
});
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (!plugin.tools.some(t => t.name === tool)) {
|
|
651
|
+
sendToExtension(state, {
|
|
652
|
+
jsonrpc: '2.0',
|
|
653
|
+
error: { code: -32602, message: `Tool not found: ${tool} in plugin ${pluginName}` },
|
|
654
|
+
id,
|
|
655
|
+
});
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const prefixed = prefixedToolName(pluginName, tool);
|
|
659
|
+
state.toolConfig[prefixed] = enabled;
|
|
660
|
+
callbacks.onToolConfigChanged();
|
|
661
|
+
callbacks.onToolConfigPersist();
|
|
662
|
+
sendToExtension(state, {
|
|
663
|
+
jsonrpc: '2.0',
|
|
664
|
+
result: { ok: true },
|
|
665
|
+
id,
|
|
666
|
+
});
|
|
667
|
+
};
|
|
668
|
+
const handleConfigSetAllToolsEnabled = (state, params, id, callbacks) => {
|
|
669
|
+
if (!params) {
|
|
670
|
+
sendToExtension(state, { jsonrpc: '2.0', error: { code: -32602, message: 'Missing params' }, id });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const allToolsEnabledParams = params;
|
|
674
|
+
const pluginName = allToolsEnabledParams.plugin;
|
|
675
|
+
const enabled = allToolsEnabledParams.enabled;
|
|
676
|
+
if (typeof pluginName !== 'string' || typeof enabled !== 'boolean') {
|
|
677
|
+
sendToExtension(state, {
|
|
678
|
+
jsonrpc: '2.0',
|
|
679
|
+
error: { code: -32602, message: 'Invalid params: expected plugin (string), enabled (boolean)' },
|
|
680
|
+
id,
|
|
681
|
+
});
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const plugin = state.registry.plugins.get(pluginName);
|
|
685
|
+
if (!plugin) {
|
|
686
|
+
sendToExtension(state, {
|
|
687
|
+
jsonrpc: '2.0',
|
|
688
|
+
error: { code: -32602, message: `Plugin not found: ${pluginName}` },
|
|
689
|
+
id,
|
|
690
|
+
});
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
for (const tool of plugin.tools) {
|
|
694
|
+
const prefixed = prefixedToolName(pluginName, tool.name);
|
|
695
|
+
state.toolConfig[prefixed] = enabled;
|
|
696
|
+
}
|
|
697
|
+
callbacks.onToolConfigChanged();
|
|
698
|
+
callbacks.onToolConfigPersist();
|
|
699
|
+
sendToExtension(state, {
|
|
700
|
+
jsonrpc: '2.0',
|
|
701
|
+
result: { ok: true },
|
|
702
|
+
id,
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
/**
|
|
706
|
+
* Handle tool.progress notification from the extension.
|
|
707
|
+
* Looks up the pending dispatch by dispatchId, invokes the onProgress callback
|
|
708
|
+
* to emit an MCP ProgressNotification to the client, and resets the dispatch
|
|
709
|
+
* timeout timer (the tool is alive). The timer reset is bounded by
|
|
710
|
+
* MAX_DISPATCH_TIMEOUT_MS — if the dispatch has been running longer than the
|
|
711
|
+
* absolute maximum, it is rejected immediately regardless of progress.
|
|
712
|
+
*/
|
|
713
|
+
const handleToolProgress = (state, params) => {
|
|
714
|
+
if (!params)
|
|
715
|
+
return;
|
|
716
|
+
const dispatchId = params.dispatchId;
|
|
717
|
+
if (typeof dispatchId !== 'string')
|
|
718
|
+
return;
|
|
719
|
+
const progress = params.progress;
|
|
720
|
+
const total = params.total;
|
|
721
|
+
if (typeof progress !== 'number' || typeof total !== 'number')
|
|
722
|
+
return;
|
|
723
|
+
const message = typeof params.message === 'string' ? params.message : undefined;
|
|
724
|
+
const pending = state.pendingDispatches.get(dispatchId);
|
|
725
|
+
if (!pending)
|
|
726
|
+
return;
|
|
727
|
+
pending.lastProgressTs = Date.now();
|
|
728
|
+
// Forward the progress notification to the MCP client
|
|
729
|
+
if (pending.onProgress) {
|
|
730
|
+
try {
|
|
731
|
+
pending.onProgress(progress, total, message);
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
// Fire-and-forget — errors in the progress chain must not affect tool execution
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
// Reset the dispatch timeout — the tool is alive and making progress.
|
|
738
|
+
// Bounded by MAX_DISPATCH_TIMEOUT_MS from the dispatch start time.
|
|
739
|
+
clearTimeout(pending.timerId);
|
|
740
|
+
const elapsed = Date.now() - pending.startTs;
|
|
741
|
+
const remainingMax = MAX_DISPATCH_TIMEOUT_MS - elapsed;
|
|
742
|
+
if (remainingMax <= 0) {
|
|
743
|
+
// Absolute max exceeded — reject immediately
|
|
744
|
+
state.pendingDispatches.delete(dispatchId);
|
|
745
|
+
pending.reject(new Error(`Dispatch ${pending.label} exceeded absolute max timeout of ${MAX_DISPATCH_TIMEOUT_MS}ms`));
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const nextTimeout = Math.min(DISPATCH_TIMEOUT_MS, remainingMax);
|
|
749
|
+
pending.timerId = setTimeout(() => {
|
|
750
|
+
if (state.pendingDispatches.has(dispatchId)) {
|
|
751
|
+
state.pendingDispatches.delete(dispatchId);
|
|
752
|
+
pending.reject(new Error(`Dispatch ${pending.label} timed out after ${DISPATCH_TIMEOUT_MS}ms`));
|
|
753
|
+
}
|
|
754
|
+
}, nextTimeout);
|
|
755
|
+
};
|
|
756
|
+
/** Valid plugin log levels (matches MCP LoggingLevel subset used by the SDK) */
|
|
757
|
+
const VALID_LOG_LEVELS = new Set(['debug', 'info', 'warning', 'error']);
|
|
758
|
+
const handlePluginLog = (params, callbacks) => {
|
|
759
|
+
if (!params)
|
|
760
|
+
return;
|
|
761
|
+
const plugin = params.plugin;
|
|
762
|
+
const level = params.level;
|
|
763
|
+
const message = params.message;
|
|
764
|
+
if (typeof plugin !== 'string' || plugin.length === 0)
|
|
765
|
+
return;
|
|
766
|
+
if (typeof level !== 'string' || !VALID_LOG_LEVELS.has(level))
|
|
767
|
+
return;
|
|
768
|
+
if (typeof message !== 'string')
|
|
769
|
+
return;
|
|
770
|
+
const ts = typeof params.ts === 'string' ? params.ts : new Date().toISOString();
|
|
771
|
+
const entry = {
|
|
772
|
+
level,
|
|
773
|
+
plugin,
|
|
774
|
+
message,
|
|
775
|
+
data: params.data,
|
|
776
|
+
ts,
|
|
777
|
+
};
|
|
778
|
+
appendLog(plugin, entry);
|
|
779
|
+
callbacks.onPluginLog(entry);
|
|
780
|
+
};
|
|
781
|
+
// ---------------------------------------------------------------------------
|
|
782
|
+
// Dynamic exec file helpers — write/delete/cleanup for browser_execute_script
|
|
783
|
+
// ---------------------------------------------------------------------------
|
|
784
|
+
/**
|
|
785
|
+
* Write a dynamic exec script to the adapters/ directory.
|
|
786
|
+
* Wraps the user's code in an IIFE that captures the result (sync or async)
|
|
787
|
+
* into globalThis.__openTabs.__lastExecResult for the extension to read back.
|
|
788
|
+
*
|
|
789
|
+
* Returns the filename (relative to adapters/) for the extension to inject.
|
|
790
|
+
*/
|
|
791
|
+
const writeExecFile = async (execId, code) => {
|
|
792
|
+
await ensureAdaptersDir();
|
|
793
|
+
const filename = `${EXEC_FILE_PREFIX}${execId}.js`;
|
|
794
|
+
const adaptersDir = getAdaptersDir();
|
|
795
|
+
const finalPath = join(adaptersDir, filename);
|
|
796
|
+
const tmpPath = join(adaptersDir, `${filename}.tmp`);
|
|
797
|
+
// Wrap user code to capture sync/async results and errors.
|
|
798
|
+
// The wrapper stores results at globalThis.__openTabs.__lastExecResult.
|
|
799
|
+
// The extension reads this value after injection and cleans it up.
|
|
800
|
+
//
|
|
801
|
+
// User code is passed as a JSON-escaped string literal to new Function(),
|
|
802
|
+
// preventing IIFE wrapper breakout attacks. The Function constructor
|
|
803
|
+
// parses the code in its own context — closing braces/parens in user
|
|
804
|
+
// code cannot break the wrapper syntax.
|
|
805
|
+
const wrapped = [
|
|
806
|
+
'(function() {',
|
|
807
|
+
' var __ot = globalThis.__openTabs = globalThis.__openTabs || {};',
|
|
808
|
+
' try {',
|
|
809
|
+
` var __userFn = new Function(${JSON.stringify(code)});`,
|
|
810
|
+
' var __r = __userFn();',
|
|
811
|
+
' if (__r && typeof __r === "object" && typeof __r.then === "function") {',
|
|
812
|
+
' __ot.__lastExecAsync = true;',
|
|
813
|
+
' __r.then(function(v) { __ot.__lastExecResult = { value: v }; })',
|
|
814
|
+
' .catch(function(e) { __ot.__lastExecResult = { error: e instanceof Error ? e.message : String(e) }; });',
|
|
815
|
+
' } else {',
|
|
816
|
+
' __ot.__lastExecResult = { value: __r };',
|
|
817
|
+
' }',
|
|
818
|
+
' } catch (e) {',
|
|
819
|
+
' __ot.__lastExecResult = { error: e instanceof Error ? e.message : String(e) };',
|
|
820
|
+
' }',
|
|
821
|
+
'})();',
|
|
822
|
+
].join('\n');
|
|
823
|
+
await Bun.write(tmpPath, wrapped);
|
|
824
|
+
await rename(tmpPath, finalPath);
|
|
825
|
+
return filename;
|
|
826
|
+
};
|
|
827
|
+
/** Delete a dynamic exec script file. Fire-and-forget — logs on failure. */
|
|
828
|
+
const deleteExecFile = async (filename) => {
|
|
829
|
+
try {
|
|
830
|
+
await Bun.file(join(getAdaptersDir(), filename)).delete();
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
log.warn(`Failed to delete exec file: ${filename}`);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
/**
|
|
837
|
+
* Remove stale __exec-*.js files from the adapters directory.
|
|
838
|
+
* Called on server startup to clean up leftovers from crashed sessions.
|
|
839
|
+
*/
|
|
840
|
+
const cleanupStaleExecFiles = async () => {
|
|
841
|
+
const adaptersDir = getAdaptersDir();
|
|
842
|
+
let entries;
|
|
843
|
+
try {
|
|
844
|
+
entries = await readdir(adaptersDir);
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const staleExecFiles = entries.filter(f => f.startsWith(EXEC_FILE_PREFIX) && (f.endsWith('.js') || f.endsWith('.js.tmp')));
|
|
850
|
+
if (staleExecFiles.length === 0)
|
|
851
|
+
return;
|
|
852
|
+
await Promise.allSettled(staleExecFiles.map(f => Bun.file(join(adaptersDir, f)).delete()));
|
|
853
|
+
log.info(`Cleaned up ${staleExecFiles.length} stale exec file(s)`);
|
|
854
|
+
};
|
|
855
|
+
/**
|
|
856
|
+
* Send extension.reload JSON-RPC request to trigger chrome.runtime.reload()
|
|
857
|
+
* in the connected extension. Used when the server detects that the managed
|
|
858
|
+
* extension files were updated (version change).
|
|
859
|
+
*/
|
|
860
|
+
const sendExtensionReload = (state) => {
|
|
861
|
+
const id = getNextRequestId();
|
|
862
|
+
return sendToExtension(state, {
|
|
863
|
+
jsonrpc: '2.0',
|
|
864
|
+
method: 'extension.reload',
|
|
865
|
+
id,
|
|
866
|
+
});
|
|
867
|
+
};
|
|
868
|
+
export { sendSyncFull, dispatchToExtension, sendInvocationStart, sendInvocationEnd, sendConfirmationRequest, rejectAllPendingConfirmations, sendPluginUpdate, handleExtensionMessage, isDispatchError, writeAdapterFile, writeExecFile, deleteExecFile, cleanupStaleExecFiles, sendExtensionReload, };
|
|
869
|
+
//# sourceMappingURL=extension-protocol.js.map
|