This is page 4 of 10. Use http://codebase.md/hangwin/mcp-chrome?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .gitattributes
├── .github
│ └── workflows
│ └── build-release.yml
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── .vscode
│ └── extensions.json
├── app
│ ├── chrome-extension
│ │ ├── _locales
│ │ │ ├── de
│ │ │ │ └── messages.json
│ │ │ ├── en
│ │ │ │ └── messages.json
│ │ │ ├── ja
│ │ │ │ └── messages.json
│ │ │ ├── ko
│ │ │ │ └── messages.json
│ │ │ ├── zh_CN
│ │ │ │ └── messages.json
│ │ │ └── zh_TW
│ │ │ └── messages.json
│ │ ├── .env.example
│ │ ├── assets
│ │ │ └── vue.svg
│ │ ├── common
│ │ │ ├── constants.ts
│ │ │ ├── message-types.ts
│ │ │ └── tool-handler.ts
│ │ ├── entrypoints
│ │ │ ├── background
│ │ │ │ ├── index.ts
│ │ │ │ ├── native-host.ts
│ │ │ │ ├── semantic-similarity.ts
│ │ │ │ ├── storage-manager.ts
│ │ │ │ └── tools
│ │ │ │ ├── base-browser.ts
│ │ │ │ ├── browser
│ │ │ │ │ ├── bookmark.ts
│ │ │ │ │ ├── common.ts
│ │ │ │ │ ├── console.ts
│ │ │ │ │ ├── file-upload.ts
│ │ │ │ │ ├── history.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── inject-script.ts
│ │ │ │ │ ├── interaction.ts
│ │ │ │ │ ├── keyboard.ts
│ │ │ │ │ ├── network-capture-debugger.ts
│ │ │ │ │ ├── network-capture-web-request.ts
│ │ │ │ │ ├── network-request.ts
│ │ │ │ │ ├── screenshot.ts
│ │ │ │ │ ├── vector-search.ts
│ │ │ │ │ ├── web-fetcher.ts
│ │ │ │ │ └── window.ts
│ │ │ │ └── index.ts
│ │ │ ├── content.ts
│ │ │ ├── offscreen
│ │ │ │ ├── index.html
│ │ │ │ └── main.ts
│ │ │ └── popup
│ │ │ ├── App.vue
│ │ │ ├── components
│ │ │ │ ├── ConfirmDialog.vue
│ │ │ │ ├── icons
│ │ │ │ │ ├── BoltIcon.vue
│ │ │ │ │ ├── CheckIcon.vue
│ │ │ │ │ ├── DatabaseIcon.vue
│ │ │ │ │ ├── DocumentIcon.vue
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── TabIcon.vue
│ │ │ │ │ ├── TrashIcon.vue
│ │ │ │ │ └── VectorIcon.vue
│ │ │ │ ├── ModelCacheManagement.vue
│ │ │ │ └── ProgressIndicator.vue
│ │ │ ├── index.html
│ │ │ ├── main.ts
│ │ │ └── style.css
│ │ ├── eslint.config.js
│ │ ├── inject-scripts
│ │ │ ├── click-helper.js
│ │ │ ├── fill-helper.js
│ │ │ ├── inject-bridge.js
│ │ │ ├── interactive-elements-helper.js
│ │ │ ├── keyboard-helper.js
│ │ │ ├── network-helper.js
│ │ │ ├── screenshot-helper.js
│ │ │ └── web-fetcher-helper.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── icon
│ │ │ │ ├── 128.png
│ │ │ │ ├── 16.png
│ │ │ │ ├── 32.png
│ │ │ │ ├── 48.png
│ │ │ │ └── 96.png
│ │ │ ├── libs
│ │ │ │ └── ort.min.js
│ │ │ └── wxt.svg
│ │ ├── README.md
│ │ ├── tsconfig.json
│ │ ├── utils
│ │ │ ├── content-indexer.ts
│ │ │ ├── i18n.ts
│ │ │ ├── image-utils.ts
│ │ │ ├── lru-cache.ts
│ │ │ ├── model-cache-manager.ts
│ │ │ ├── offscreen-manager.ts
│ │ │ ├── semantic-similarity-engine.ts
│ │ │ ├── simd-math-engine.ts
│ │ │ ├── text-chunker.ts
│ │ │ └── vector-database.ts
│ │ ├── workers
│ │ │ ├── ort-wasm-simd-threaded.jsep.mjs
│ │ │ ├── ort-wasm-simd-threaded.jsep.wasm
│ │ │ ├── ort-wasm-simd-threaded.mjs
│ │ │ ├── ort-wasm-simd-threaded.wasm
│ │ │ ├── simd_math_bg.wasm
│ │ │ ├── simd_math.js
│ │ │ └── similarity.worker.js
│ │ └── wxt.config.ts
│ └── native-server
│ ├── debug.sh
│ ├── install.md
│ ├── jest.config.js
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── cli.ts
│ │ ├── constant
│ │ │ └── index.ts
│ │ ├── file-handler.ts
│ │ ├── index.ts
│ │ ├── mcp
│ │ │ ├── mcp-server-stdio.ts
│ │ │ ├── mcp-server.ts
│ │ │ ├── register-tools.ts
│ │ │ └── stdio-config.json
│ │ ├── native-messaging-host.ts
│ │ ├── scripts
│ │ │ ├── browser-config.ts
│ │ │ ├── build.ts
│ │ │ ├── constant.ts
│ │ │ ├── postinstall.ts
│ │ │ ├── register-dev.ts
│ │ │ ├── register.ts
│ │ │ ├── run_host.bat
│ │ │ ├── run_host.sh
│ │ │ └── utils.ts
│ │ ├── server
│ │ │ ├── index.ts
│ │ │ └── server.test.ts
│ │ └── util
│ │ └── logger.ts
│ └── tsconfig.json
├── commitlint.config.cjs
├── docs
│ ├── ARCHITECTURE_zh.md
│ ├── ARCHITECTURE.md
│ ├── CHANGELOG.md
│ ├── CONTRIBUTING_zh.md
│ ├── CONTRIBUTING.md
│ ├── TOOLS_zh.md
│ ├── TOOLS.md
│ ├── TROUBLESHOOTING_zh.md
│ ├── TROUBLESHOOTING.md
│ └── WINDOWS_INSTALL_zh.md
├── eslint.config.js
├── LICENSE
├── package.json
├── packages
│ ├── shared
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── tools.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ └── wasm-simd
│ ├── .gitignore
│ ├── BUILD.md
│ ├── Cargo.toml
│ ├── package.json
│ ├── README.md
│ └── src
│ └── lib.rs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── prompt
│ ├── content-analize.md
│ ├── excalidraw-prompt.md
│ └── modify-web.md
├── README_zh.md
├── README.md
├── releases
│ ├── chrome-extension
│ │ └── latest
│ │ └── chrome-mcp-server-lastest.zip
│ └── README.md
└── test-inject-script.js
```
# Files
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/offscreen/main.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { SemanticSimilarityEngine } from '@/utils/semantic-similarity-engine';
2 | import {
3 | MessageTarget,
4 | SendMessageType,
5 | OFFSCREEN_MESSAGE_TYPES,
6 | BACKGROUND_MESSAGE_TYPES,
7 | } from '@/common/message-types';
8 |
9 | // Global semantic similarity engine instance
10 | let similarityEngine: SemanticSimilarityEngine | null = null;
11 | interface OffscreenMessage {
12 | target: MessageTarget | string;
13 | type: SendMessageType | string;
14 | }
15 |
16 | interface SimilarityEngineInitMessage extends OffscreenMessage {
17 | type: SendMessageType.SimilarityEngineInit;
18 | config: any;
19 | }
20 |
21 | interface SimilarityEngineComputeBatchMessage extends OffscreenMessage {
22 | type: SendMessageType.SimilarityEngineComputeBatch;
23 | pairs: { text1: string; text2: string }[];
24 | options?: Record<string, any>;
25 | }
26 |
27 | interface SimilarityEngineGetEmbeddingMessage extends OffscreenMessage {
28 | type: 'similarityEngineCompute';
29 | text: string;
30 | options?: Record<string, any>;
31 | }
32 |
33 | interface SimilarityEngineGetEmbeddingsBatchMessage extends OffscreenMessage {
34 | type: 'similarityEngineBatchCompute';
35 | texts: string[];
36 | options?: Record<string, any>;
37 | }
38 |
39 | interface SimilarityEngineStatusMessage extends OffscreenMessage {
40 | type: 'similarityEngineStatus';
41 | }
42 |
43 | type MessageResponse = {
44 | result?: string;
45 | error?: string;
46 | success?: boolean;
47 | similarities?: number[];
48 | embedding?: number[];
49 | embeddings?: number[][];
50 | isInitialized?: boolean;
51 | currentConfig?: any;
52 | };
53 |
54 | // Listen for messages from the extension
55 | chrome.runtime.onMessage.addListener(
56 | (
57 | message: OffscreenMessage,
58 | _sender: chrome.runtime.MessageSender,
59 | sendResponse: (response: MessageResponse) => void,
60 | ) => {
61 | if (message.target !== MessageTarget.Offscreen) {
62 | return;
63 | }
64 |
65 | try {
66 | switch (message.type) {
67 | case SendMessageType.SimilarityEngineInit:
68 | case OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT: {
69 | const initMsg = message as SimilarityEngineInitMessage;
70 | console.log('Offscreen: Received similarity engine init message:', message.type);
71 | handleSimilarityEngineInit(initMsg.config)
72 | .then(() => sendResponse({ success: true }))
73 | .catch((error) => sendResponse({ success: false, error: error.message }));
74 | break;
75 | }
76 |
77 | case SendMessageType.SimilarityEngineComputeBatch: {
78 | const computeMsg = message as SimilarityEngineComputeBatchMessage;
79 | handleComputeSimilarityBatch(computeMsg.pairs, computeMsg.options)
80 | .then((similarities) => sendResponse({ success: true, similarities }))
81 | .catch((error) => sendResponse({ success: false, error: error.message }));
82 | break;
83 | }
84 |
85 | case OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_COMPUTE: {
86 | const embeddingMsg = message as SimilarityEngineGetEmbeddingMessage;
87 | handleGetEmbedding(embeddingMsg.text, embeddingMsg.options)
88 | .then((embedding) => {
89 | console.log('Offscreen: Sending embedding response:', {
90 | length: embedding.length,
91 | type: typeof embedding,
92 | constructor: embedding.constructor.name,
93 | isFloat32Array: embedding instanceof Float32Array,
94 | firstFewValues: Array.from(embedding.slice(0, 5)),
95 | });
96 | const embeddingArray = Array.from(embedding);
97 | console.log('Offscreen: Converted to array:', {
98 | length: embeddingArray.length,
99 | type: typeof embeddingArray,
100 | isArray: Array.isArray(embeddingArray),
101 | firstFewValues: embeddingArray.slice(0, 5),
102 | });
103 | sendResponse({ success: true, embedding: embeddingArray });
104 | })
105 | .catch((error) => sendResponse({ success: false, error: error.message }));
106 | break;
107 | }
108 |
109 | case OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_BATCH_COMPUTE: {
110 | const batchMsg = message as SimilarityEngineGetEmbeddingsBatchMessage;
111 | handleGetEmbeddingsBatch(batchMsg.texts, batchMsg.options)
112 | .then((embeddings) =>
113 | sendResponse({
114 | success: true,
115 | embeddings: embeddings.map((emb) => Array.from(emb)),
116 | }),
117 | )
118 | .catch((error) => sendResponse({ success: false, error: error.message }));
119 | break;
120 | }
121 |
122 | case OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_STATUS: {
123 | handleGetEngineStatus()
124 | .then((status: any) => sendResponse({ success: true, ...status }))
125 | .catch((error: any) => sendResponse({ success: false, error: error.message }));
126 | break;
127 | }
128 |
129 | default:
130 | sendResponse({ error: `Unknown message type: ${message.type}` });
131 | }
132 | } catch (error) {
133 | if (error instanceof Error) {
134 | sendResponse({ error: error.message });
135 | } else {
136 | sendResponse({ error: 'Unknown error occurred' });
137 | }
138 | }
139 |
140 | // Return true to indicate we'll respond asynchronously
141 | return true;
142 | },
143 | );
144 |
145 | // Global variable to track current model state
146 | let currentModelConfig: any = null;
147 |
148 | /**
149 | * Check if engine reinitialization is needed
150 | */
151 | function needsReinitialization(newConfig: any): boolean {
152 | if (!similarityEngine || !currentModelConfig) {
153 | return true;
154 | }
155 |
156 | // Check if key configuration has changed
157 | const keyFields = ['modelPreset', 'modelVersion', 'modelIdentifier', 'dimension'];
158 | for (const field of keyFields) {
159 | if (newConfig[field] !== currentModelConfig[field]) {
160 | console.log(
161 | `Offscreen: ${field} changed from ${currentModelConfig[field]} to ${newConfig[field]}`,
162 | );
163 | return true;
164 | }
165 | }
166 |
167 | return false;
168 | }
169 |
170 | /**
171 | * Progress callback function type
172 | */
173 | type ProgressCallback = (progress: { status: string; progress: number; message?: string }) => void;
174 |
175 | /**
176 | * Initialize semantic similarity engine
177 | */
178 | async function handleSimilarityEngineInit(config: any): Promise<void> {
179 | console.log('Offscreen: Initializing semantic similarity engine with config:', config);
180 | console.log('Offscreen: Config useLocalFiles:', config.useLocalFiles);
181 | console.log('Offscreen: Config modelPreset:', config.modelPreset);
182 | console.log('Offscreen: Config modelVersion:', config.modelVersion);
183 | console.log('Offscreen: Config modelDimension:', config.modelDimension);
184 | console.log('Offscreen: Config modelIdentifier:', config.modelIdentifier);
185 |
186 | // Check if reinitialization is needed
187 | const needsReinit = needsReinitialization(config);
188 | console.log('Offscreen: Needs reinitialization:', needsReinit);
189 |
190 | if (!needsReinit) {
191 | console.log('Offscreen: Using existing engine (no changes detected)');
192 | await updateModelStatus('ready', 100);
193 | return;
194 | }
195 |
196 | // If engine already exists, clean up old instance first (support model switching)
197 | if (similarityEngine) {
198 | console.log('Offscreen: Cleaning up existing engine for model switch...');
199 | try {
200 | // Properly call dispose method to clean up all resources
201 | await similarityEngine.dispose();
202 | console.log('Offscreen: Previous engine disposed successfully');
203 | } catch (error) {
204 | console.warn('Offscreen: Failed to dispose previous engine:', error);
205 | }
206 | similarityEngine = null;
207 | currentModelConfig = null;
208 |
209 | // Clear vector data in IndexedDB to ensure data consistency
210 | try {
211 | console.log('Offscreen: Clearing IndexedDB vector data for model switch...');
212 | await clearVectorIndexedDB();
213 | console.log('Offscreen: IndexedDB vector data cleared successfully');
214 | } catch (error) {
215 | console.warn('Offscreen: Failed to clear IndexedDB vector data:', error);
216 | }
217 | }
218 |
219 | try {
220 | // Update status to initializing
221 | await updateModelStatus('initializing', 10);
222 |
223 | // Create progress callback function
224 | const progressCallback: ProgressCallback = async (progress) => {
225 | console.log('Offscreen: Progress update:', progress);
226 | await updateModelStatus(progress.status, progress.progress);
227 | };
228 |
229 | // Create engine instance and pass progress callback
230 | similarityEngine = new SemanticSimilarityEngine(config);
231 | console.log('Offscreen: Starting engine initialization with progress tracking...');
232 |
233 | // Use enhanced initialization method (if progress callback is supported)
234 | if (typeof (similarityEngine as any).initializeWithProgress === 'function') {
235 | await (similarityEngine as any).initializeWithProgress(progressCallback);
236 | } else {
237 | // Fallback to standard initialization method
238 | console.log('Offscreen: Using standard initialization (no progress callback support)');
239 | await updateModelStatus('downloading', 30);
240 | await similarityEngine.initialize();
241 | await updateModelStatus('ready', 100);
242 | }
243 |
244 | // Save current configuration
245 | currentModelConfig = { ...config };
246 |
247 | console.log('Offscreen: Semantic similarity engine initialized successfully');
248 | } catch (error) {
249 | console.error('Offscreen: Failed to initialize semantic similarity engine:', error);
250 | // Update status to error
251 | const errorMessage = error instanceof Error ? error.message : 'Unknown initialization error';
252 | const errorType = analyzeErrorType(errorMessage);
253 | await updateModelStatus('error', 0, errorMessage, errorType);
254 | // Clean up failed instance
255 | similarityEngine = null;
256 | currentModelConfig = null;
257 | throw error;
258 | }
259 | }
260 |
261 | /**
262 | * Clear vector data in IndexedDB
263 | */
264 | async function clearVectorIndexedDB(): Promise<void> {
265 | try {
266 | // Clear vector search related IndexedDB databases
267 | const dbNames = ['VectorSearchDB', 'ContentIndexerDB', 'SemanticSimilarityDB'];
268 |
269 | for (const dbName of dbNames) {
270 | try {
271 | // Try to delete database
272 | const deleteRequest = indexedDB.deleteDatabase(dbName);
273 | await new Promise<void>((resolve, _reject) => {
274 | deleteRequest.onsuccess = () => {
275 | console.log(`Offscreen: Successfully deleted database: ${dbName}`);
276 | resolve();
277 | };
278 | deleteRequest.onerror = () => {
279 | console.warn(`Offscreen: Failed to delete database: ${dbName}`, deleteRequest.error);
280 | resolve(); // 不阻塞其他数据库的清理
281 | };
282 | deleteRequest.onblocked = () => {
283 | console.warn(`Offscreen: Database deletion blocked: ${dbName}`);
284 | resolve(); // 不阻塞其他数据库的清理
285 | };
286 | });
287 | } catch (error) {
288 | console.warn(`Offscreen: Error deleting database ${dbName}:`, error);
289 | }
290 | }
291 | } catch (error) {
292 | console.error('Offscreen: Failed to clear vector IndexedDB:', error);
293 | throw error;
294 | }
295 | }
296 |
297 | // Analyze error type
298 | function analyzeErrorType(errorMessage: string): 'network' | 'file' | 'unknown' {
299 | const message = errorMessage.toLowerCase();
300 |
301 | if (
302 | message.includes('network') ||
303 | message.includes('fetch') ||
304 | message.includes('timeout') ||
305 | message.includes('connection') ||
306 | message.includes('cors') ||
307 | message.includes('failed to fetch')
308 | ) {
309 | return 'network';
310 | }
311 |
312 | if (
313 | message.includes('corrupt') ||
314 | message.includes('invalid') ||
315 | message.includes('format') ||
316 | message.includes('parse') ||
317 | message.includes('decode') ||
318 | message.includes('onnx')
319 | ) {
320 | return 'file';
321 | }
322 |
323 | return 'unknown';
324 | }
325 |
326 | // Helper function to update model status
327 | async function updateModelStatus(
328 | status: string,
329 | progress: number,
330 | errorMessage?: string,
331 | errorType?: string,
332 | ) {
333 | try {
334 | const modelState = {
335 | status,
336 | downloadProgress: progress,
337 | isDownloading: status === 'downloading' || status === 'initializing',
338 | lastUpdated: Date.now(),
339 | errorMessage: errorMessage || '',
340 | errorType: errorType || '',
341 | };
342 |
343 | // In offscreen document, update storage through message passing to background script
344 | // because offscreen document may not have direct chrome.storage access
345 | if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
346 | await chrome.storage.local.set({ modelState });
347 | } else {
348 | // If chrome.storage is not available, pass message to background script
349 | console.log('Offscreen: chrome.storage not available, sending message to background');
350 | try {
351 | await chrome.runtime.sendMessage({
352 | type: BACKGROUND_MESSAGE_TYPES.UPDATE_MODEL_STATUS,
353 | modelState: modelState,
354 | });
355 | } catch (messageError) {
356 | console.error('Offscreen: Failed to send status update message:', messageError);
357 | }
358 | }
359 | } catch (error) {
360 | console.error('Offscreen: Failed to update model status:', error);
361 | }
362 | }
363 |
364 | /**
365 | * Batch compute semantic similarity
366 | */
367 | async function handleComputeSimilarityBatch(
368 | pairs: { text1: string; text2: string }[],
369 | options: Record<string, any> = {},
370 | ): Promise<number[]> {
371 | if (!similarityEngine) {
372 | throw new Error('Similarity engine not initialized. Please reinitialize the engine.');
373 | }
374 |
375 | console.log(`Offscreen: Computing similarities for ${pairs.length} pairs`);
376 | const similarities = await similarityEngine.computeSimilarityBatch(pairs, options);
377 | console.log('Offscreen: Similarity computation completed');
378 |
379 | return similarities;
380 | }
381 |
382 | /**
383 | * Get embedding vector for single text
384 | */
385 | async function handleGetEmbedding(
386 | text: string,
387 | options: Record<string, any> = {},
388 | ): Promise<Float32Array> {
389 | if (!similarityEngine) {
390 | throw new Error('Similarity engine not initialized. Please reinitialize the engine.');
391 | }
392 |
393 | console.log(`Offscreen: Getting embedding for text: "${text.substring(0, 50)}..."`);
394 | const embedding = await similarityEngine.getEmbedding(text, options);
395 | console.log('Offscreen: Embedding computation completed');
396 |
397 | return embedding;
398 | }
399 |
400 | /**
401 | * Batch get embedding vectors for texts
402 | */
403 | async function handleGetEmbeddingsBatch(
404 | texts: string[],
405 | options: Record<string, any> = {},
406 | ): Promise<Float32Array[]> {
407 | if (!similarityEngine) {
408 | throw new Error('Similarity engine not initialized. Please reinitialize the engine.');
409 | }
410 |
411 | console.log(`Offscreen: Getting embeddings for ${texts.length} texts`);
412 | const embeddings = await similarityEngine.getEmbeddingsBatch(texts, options);
413 | console.log('Offscreen: Batch embedding computation completed');
414 |
415 | return embeddings;
416 | }
417 |
418 | /**
419 | * Get engine status
420 | */
421 | async function handleGetEngineStatus(): Promise<{
422 | isInitialized: boolean;
423 | currentConfig: any;
424 | }> {
425 | return {
426 | isInitialized: !!similarityEngine,
427 | currentConfig: currentModelConfig,
428 | };
429 | }
430 |
431 | console.log('Offscreen: Semantic similarity engine handler loaded');
432 |
```
--------------------------------------------------------------------------------
/app/native-server/src/scripts/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import fs from 'fs';
2 | import path from 'path';
3 | import os from 'os';
4 | import { execSync } from 'child_process';
5 | import { promisify } from 'util';
6 | import { COMMAND_NAME, DESCRIPTION, EXTENSION_ID, HOST_NAME } from './constant';
7 | import { BrowserType, getBrowserConfig, detectInstalledBrowsers } from './browser-config';
8 |
9 | export const access = promisify(fs.access);
10 | export const mkdir = promisify(fs.mkdir);
11 | export const writeFile = promisify(fs.writeFile);
12 |
13 | /**
14 | * 打印彩色文本
15 | */
16 | export function colorText(text: string, color: string): string {
17 | const colors: Record<string, string> = {
18 | red: '\x1b[31m',
19 | green: '\x1b[32m',
20 | yellow: '\x1b[33m',
21 | blue: '\x1b[34m',
22 | reset: '\x1b[0m',
23 | };
24 |
25 | return colors[color] + text + colors.reset;
26 | }
27 |
28 | /**
29 | * Get user-level manifest file path
30 | */
31 | export function getUserManifestPath(): string {
32 | if (os.platform() === 'win32') {
33 | // Windows: %APPDATA%\Google\Chrome\NativeMessagingHosts\
34 | return path.join(
35 | process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
36 | 'Google',
37 | 'Chrome',
38 | 'NativeMessagingHosts',
39 | `${HOST_NAME}.json`,
40 | );
41 | } else if (os.platform() === 'darwin') {
42 | // macOS: ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/
43 | return path.join(
44 | os.homedir(),
45 | 'Library',
46 | 'Application Support',
47 | 'Google',
48 | 'Chrome',
49 | 'NativeMessagingHosts',
50 | `${HOST_NAME}.json`,
51 | );
52 | } else {
53 | // Linux: ~/.config/google-chrome/NativeMessagingHosts/
54 | return path.join(
55 | os.homedir(),
56 | '.config',
57 | 'google-chrome',
58 | 'NativeMessagingHosts',
59 | `${HOST_NAME}.json`,
60 | );
61 | }
62 | }
63 |
64 | /**
65 | * Get system-level manifest file path
66 | */
67 | export function getSystemManifestPath(): string {
68 | if (os.platform() === 'win32') {
69 | // Windows: %ProgramFiles%\Google\Chrome\NativeMessagingHosts\
70 | return path.join(
71 | process.env.ProgramFiles || 'C:\\Program Files',
72 | 'Google',
73 | 'Chrome',
74 | 'NativeMessagingHosts',
75 | `${HOST_NAME}.json`,
76 | );
77 | } else if (os.platform() === 'darwin') {
78 | // macOS: /Library/Google/Chrome/NativeMessagingHosts/
79 | return path.join('/Library', 'Google', 'Chrome', 'NativeMessagingHosts', `${HOST_NAME}.json`);
80 | } else {
81 | // Linux: /etc/opt/chrome/native-messaging-hosts/
82 | return path.join('/etc', 'opt', 'chrome', 'native-messaging-hosts', `${HOST_NAME}.json`);
83 | }
84 | }
85 |
86 | /**
87 | * Get native host startup script file path
88 | */
89 | export async function getMainPath(): Promise<string> {
90 | try {
91 | const packageDistDir = path.join(__dirname, '..');
92 | const wrapperScriptName = process.platform === 'win32' ? 'run_host.bat' : 'run_host.sh';
93 | const absoluteWrapperPath = path.resolve(packageDistDir, wrapperScriptName);
94 | return absoluteWrapperPath;
95 | } catch (error) {
96 | console.log(colorText('Cannot find global package path, using current directory', 'yellow'));
97 | throw error;
98 | }
99 | }
100 |
101 | /**
102 | * 确保关键文件具有执行权限
103 | */
104 | export async function ensureExecutionPermissions(): Promise<void> {
105 | try {
106 | const packageDistDir = path.join(__dirname, '..');
107 |
108 | if (process.platform === 'win32') {
109 | // Windows 平台处理
110 | await ensureWindowsFilePermissions(packageDistDir);
111 | return;
112 | }
113 |
114 | // Unix/Linux 平台处理
115 | const filesToCheck = [
116 | path.join(packageDistDir, 'index.js'),
117 | path.join(packageDistDir, 'run_host.sh'),
118 | path.join(packageDistDir, 'cli.js'),
119 | ];
120 |
121 | for (const filePath of filesToCheck) {
122 | if (fs.existsSync(filePath)) {
123 | try {
124 | fs.chmodSync(filePath, '755');
125 | console.log(
126 | colorText(`✓ Set execution permissions for ${path.basename(filePath)}`, 'green'),
127 | );
128 | } catch (err: any) {
129 | console.warn(
130 | colorText(
131 | `⚠️ Unable to set execution permissions for ${path.basename(filePath)}: ${err.message}`,
132 | 'yellow',
133 | ),
134 | );
135 | }
136 | } else {
137 | console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
138 | }
139 | }
140 | } catch (error: any) {
141 | console.warn(colorText(`⚠️ Error ensuring execution permissions: ${error.message}`, 'yellow'));
142 | }
143 | }
144 |
145 | /**
146 | * Windows 平台文件权限处理
147 | */
148 | async function ensureWindowsFilePermissions(packageDistDir: string): Promise<void> {
149 | const filesToCheck = [
150 | path.join(packageDistDir, 'index.js'),
151 | path.join(packageDistDir, 'run_host.bat'),
152 | path.join(packageDistDir, 'cli.js'),
153 | ];
154 |
155 | for (const filePath of filesToCheck) {
156 | if (fs.existsSync(filePath)) {
157 | try {
158 | // 检查文件是否为只读,如果是则移除只读属性
159 | const stats = fs.statSync(filePath);
160 | if (!(stats.mode & parseInt('200', 8))) {
161 | // 检查写权限
162 | // 尝试移除只读属性
163 | fs.chmodSync(filePath, stats.mode | parseInt('200', 8));
164 | console.log(
165 | colorText(`✓ Removed read-only attribute from ${path.basename(filePath)}`, 'green'),
166 | );
167 | }
168 |
169 | // 验证文件可读性
170 | fs.accessSync(filePath, fs.constants.R_OK);
171 | console.log(
172 | colorText(`✓ Verified file accessibility for ${path.basename(filePath)}`, 'green'),
173 | );
174 | } catch (err: any) {
175 | console.warn(
176 | colorText(
177 | `⚠️ Unable to verify file permissions for ${path.basename(filePath)}: ${err.message}`,
178 | 'yellow',
179 | ),
180 | );
181 | }
182 | } else {
183 | console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
184 | }
185 | }
186 | }
187 |
188 | /**
189 | * Create Native Messaging host manifest content
190 | */
191 | export async function createManifestContent(): Promise<any> {
192 | const mainPath = await getMainPath();
193 |
194 | return {
195 | name: HOST_NAME,
196 | description: DESCRIPTION,
197 | path: mainPath, // Node.js可执行文件路径
198 | type: 'stdio',
199 | allowed_origins: [`chrome-extension://${EXTENSION_ID}/`],
200 | };
201 | }
202 |
203 | /**
204 | * 验证Windows注册表项是否存在
205 | */
206 | function verifyWindowsRegistryEntry(registryKey: string, expectedPath: string): boolean {
207 | if (os.platform() !== 'win32') {
208 | return true; // 非Windows平台跳过验证
209 | }
210 |
211 | try {
212 | const result = execSync(`reg query "${registryKey}" /ve`, { encoding: 'utf8', stdio: 'pipe' });
213 | const lines = result.split('\n');
214 | for (const line of lines) {
215 | if (line.includes('REG_SZ') && line.includes(expectedPath.replace(/\\/g, '\\\\'))) {
216 | return true;
217 | }
218 | }
219 | return false;
220 | } catch (error) {
221 | return false;
222 | }
223 | }
224 |
225 | /**
226 | * 尝试注册用户级别的Native Messaging主机
227 | */
228 | export async function tryRegisterUserLevelHost(targetBrowsers?: BrowserType[]): Promise<boolean> {
229 | try {
230 | console.log(colorText('Attempting to register user-level Native Messaging host...', 'blue'));
231 |
232 | // 1. 确保执行权限
233 | await ensureExecutionPermissions();
234 |
235 | // 2. 确定要注册的浏览器
236 | const browsersToRegister = targetBrowsers || detectInstalledBrowsers();
237 | if (browsersToRegister.length === 0) {
238 | // 如果没有检测到浏览器,默认注册Chrome和Chromium
239 | browsersToRegister.push(BrowserType.CHROME, BrowserType.CHROMIUM);
240 | console.log(
241 | colorText('No browsers detected, registering for Chrome and Chromium by default', 'yellow'),
242 | );
243 | } else {
244 | console.log(colorText(`Detected browsers: ${browsersToRegister.join(', ')}`, 'blue'));
245 | }
246 |
247 | // 3. 创建清单内容
248 | const manifest = await createManifestContent();
249 |
250 | let successCount = 0;
251 | const results: { browser: string; success: boolean; error?: string }[] = [];
252 |
253 | // 4. 为每个浏览器注册
254 | for (const browserType of browsersToRegister) {
255 | const config = getBrowserConfig(browserType);
256 | console.log(colorText(`\nRegistering for ${config.displayName}...`, 'blue'));
257 |
258 | try {
259 | // 确保目录存在
260 | await mkdir(path.dirname(config.userManifestPath), { recursive: true });
261 |
262 | // 写入清单文件
263 | await writeFile(config.userManifestPath, JSON.stringify(manifest, null, 2));
264 | console.log(colorText(`✓ Manifest written to ${config.userManifestPath}`, 'green'));
265 |
266 | // Windows需要额外注册表项
267 | if (os.platform() === 'win32' && config.registryKey) {
268 | try {
269 | const escapedPath = config.userManifestPath.replace(/\\/g, '\\\\');
270 | const regCommand = `reg add "${config.registryKey}" /ve /t REG_SZ /d "${escapedPath}" /f`;
271 | execSync(regCommand, { stdio: 'pipe' });
272 |
273 | if (verifyWindowsRegistryEntry(config.registryKey, config.userManifestPath)) {
274 | console.log(colorText(`✓ Registry entry created for ${config.displayName}`, 'green'));
275 | } else {
276 | throw new Error('Registry verification failed');
277 | }
278 | } catch (error: any) {
279 | throw new Error(`Registry error: ${error.message}`);
280 | }
281 | }
282 |
283 | successCount++;
284 | results.push({ browser: config.displayName, success: true });
285 | console.log(colorText(`✓ Successfully registered ${config.displayName}`, 'green'));
286 | } catch (error: any) {
287 | results.push({ browser: config.displayName, success: false, error: error.message });
288 | console.log(
289 | colorText(`✗ Failed to register ${config.displayName}: ${error.message}`, 'red'),
290 | );
291 | }
292 | }
293 |
294 | // 5. 报告结果
295 | console.log(colorText('\n===== Registration Summary =====', 'blue'));
296 | for (const result of results) {
297 | if (result.success) {
298 | console.log(colorText(`✓ ${result.browser}: Success`, 'green'));
299 | } else {
300 | console.log(colorText(`✗ ${result.browser}: Failed - ${result.error}`, 'red'));
301 | }
302 | }
303 |
304 | return successCount > 0;
305 | } catch (error) {
306 | console.log(
307 | colorText(
308 | `User-level registration failed: ${error instanceof Error ? error.message : String(error)}`,
309 | 'yellow',
310 | ),
311 | );
312 | return false;
313 | }
314 | }
315 |
316 | // 导入is-admin包(仅在Windows平台使用)
317 | let isAdmin: () => boolean = () => false;
318 | if (process.platform === 'win32') {
319 | try {
320 | isAdmin = require('is-admin');
321 | } catch (error) {
322 | console.warn('缺少is-admin依赖,Windows平台下可能无法正确检测管理员权限');
323 | console.warn(error);
324 | }
325 | }
326 |
327 | /**
328 | * 使用提升权限注册系统级清单
329 | */
330 | export async function registerWithElevatedPermissions(): Promise<void> {
331 | try {
332 | console.log(colorText('Attempting to register system-level manifest...', 'blue'));
333 |
334 | // 1. 确保执行权限
335 | await ensureExecutionPermissions();
336 |
337 | // 2. 准备清单内容
338 | const manifest = await createManifestContent();
339 |
340 | // 3. 获取系统级清单路径
341 | const manifestPath = getSystemManifestPath();
342 |
343 | // 4. 创建临时清单文件
344 | const tempManifestPath = path.join(os.tmpdir(), `${HOST_NAME}.json`);
345 | await writeFile(tempManifestPath, JSON.stringify(manifest, null, 2));
346 |
347 | // 5. 检测是否已经有管理员权限
348 | const isRoot = process.getuid && process.getuid() === 0; // Unix/Linux/Mac
349 | const hasAdminRights = process.platform === 'win32' ? isAdmin() : false; // Windows平台检测管理员权限
350 | const hasElevatedPermissions = isRoot || hasAdminRights;
351 |
352 | // 准备命令
353 | const command =
354 | os.platform() === 'win32'
355 | ? `if not exist "${path.dirname(manifestPath)}" mkdir "${path.dirname(manifestPath)}" && copy "${tempManifestPath}" "${manifestPath}"`
356 | : `mkdir -p "${path.dirname(manifestPath)}" && cp "${tempManifestPath}" "${manifestPath}" && chmod 644 "${manifestPath}"`;
357 |
358 | if (hasElevatedPermissions) {
359 | // 已经有管理员权限,直接执行命令
360 | try {
361 | // 创建目录
362 | if (!fs.existsSync(path.dirname(manifestPath))) {
363 | fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
364 | }
365 |
366 | // 复制文件
367 | fs.copyFileSync(tempManifestPath, manifestPath);
368 |
369 | // 设置权限(非Windows平台)
370 | if (os.platform() !== 'win32') {
371 | fs.chmodSync(manifestPath, '644');
372 | }
373 |
374 | console.log(colorText('System-level manifest registration successful!', 'green'));
375 | } catch (error: any) {
376 | console.error(
377 | colorText(`System-level manifest installation failed: ${error.message}`, 'red'),
378 | );
379 | throw error;
380 | }
381 | } else {
382 | // 没有管理员权限,打印手动操作提示
383 | console.log(
384 | colorText('⚠️ Administrator privileges required for system-level installation', 'yellow'),
385 | );
386 | console.log(
387 | colorText(
388 | 'Please run one of the following commands with administrator privileges:',
389 | 'blue',
390 | ),
391 | );
392 |
393 | if (os.platform() === 'win32') {
394 | console.log(colorText(' 1. Open Command Prompt as Administrator and run:', 'blue'));
395 | console.log(colorText(` ${command}`, 'cyan'));
396 | } else {
397 | console.log(colorText(' 1. Run with sudo:', 'blue'));
398 | console.log(colorText(` sudo ${command}`, 'cyan'));
399 | }
400 |
401 | console.log(
402 | colorText(' 2. Or run the registration command with elevated privileges:', 'blue'),
403 | );
404 | console.log(colorText(` sudo ${COMMAND_NAME} register --system`, 'cyan'));
405 |
406 | throw new Error('Administrator privileges required for system-level installation');
407 | }
408 |
409 | // 6. Windows特殊处理 - 设置系统级注册表
410 | if (os.platform() === 'win32') {
411 | const registryKey = `HKLM\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`;
412 | // 确保路径使用正确的转义格式
413 | const escapedPath = manifestPath.replace(/\\/g, '\\\\');
414 | const regCommand = `reg add "${registryKey}" /ve /t REG_SZ /d "${escapedPath}" /f`;
415 |
416 | console.log(colorText(`Creating system registry entry: ${registryKey}`, 'blue'));
417 | console.log(colorText(`Manifest path: ${manifestPath}`, 'blue'));
418 |
419 | if (hasElevatedPermissions) {
420 | // 已经有管理员权限,直接执行注册表命令
421 | try {
422 | execSync(regCommand, { stdio: 'pipe' });
423 |
424 | // 验证注册表项是否创建成功
425 | if (verifyWindowsRegistryEntry(registryKey, manifestPath)) {
426 | console.log(colorText('Windows registry entry created successfully!', 'green'));
427 | } else {
428 | console.log(colorText('⚠️ Registry entry created but verification failed', 'yellow'));
429 | }
430 | } catch (error: any) {
431 | console.error(
432 | colorText(`Windows registry entry creation failed: ${error.message}`, 'red'),
433 | );
434 | console.error(colorText(`Command: ${regCommand}`, 'red'));
435 | throw error;
436 | }
437 | } else {
438 | // 没有管理员权限,打印手动操作提示
439 | console.log(
440 | colorText(
441 | '⚠️ Administrator privileges required for Windows registry modification',
442 | 'yellow',
443 | ),
444 | );
445 | console.log(colorText('Please run the following command as Administrator:', 'blue'));
446 | console.log(colorText(` ${regCommand}`, 'cyan'));
447 | console.log(colorText('Or run the registration command with elevated privileges:', 'blue'));
448 | console.log(
449 | colorText(
450 | ` Run Command Prompt as Administrator and execute: ${COMMAND_NAME} register --system`,
451 | 'cyan',
452 | ),
453 | );
454 |
455 | throw new Error('Administrator privileges required for Windows registry modification');
456 | }
457 | }
458 | } catch (error: any) {
459 | console.error(colorText(`注册失败: ${error.message}`, 'red'));
460 | throw error;
461 | }
462 | }
463 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/common.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createErrorResponse, ToolResult } from '@/common/tool-handler';
2 | import { BaseBrowserToolExecutor } from '../base-browser';
3 | import { TOOL_NAMES } from 'chrome-mcp-shared';
4 |
5 | // Default window dimensions
6 | const DEFAULT_WINDOW_WIDTH = 1280;
7 | const DEFAULT_WINDOW_HEIGHT = 720;
8 |
9 | interface NavigateToolParams {
10 | url?: string;
11 | newWindow?: boolean;
12 | width?: number;
13 | height?: number;
14 | refresh?: boolean;
15 | }
16 |
17 | /**
18 | * Tool for navigating to URLs in browser tabs or windows
19 | */
20 | class NavigateTool extends BaseBrowserToolExecutor {
21 | name = TOOL_NAMES.BROWSER.NAVIGATE;
22 |
23 | async execute(args: NavigateToolParams): Promise<ToolResult> {
24 | const { newWindow = false, width, height, url, refresh = false } = args;
25 |
26 | console.log(
27 | `Attempting to ${refresh ? 'refresh current tab' : `open URL: ${url}`} with options:`,
28 | args,
29 | );
30 |
31 | try {
32 | // Handle refresh option first
33 | if (refresh) {
34 | console.log('Refreshing current active tab');
35 |
36 | // Get current active tab
37 | const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
38 |
39 | if (!activeTab || !activeTab.id) {
40 | return createErrorResponse('No active tab found to refresh');
41 | }
42 |
43 | // Reload the tab
44 | await chrome.tabs.reload(activeTab.id);
45 |
46 | console.log(`Refreshed tab ID: ${activeTab.id}`);
47 |
48 | // Get updated tab information
49 | const updatedTab = await chrome.tabs.get(activeTab.id);
50 |
51 | return {
52 | content: [
53 | {
54 | type: 'text',
55 | text: JSON.stringify({
56 | success: true,
57 | message: 'Successfully refreshed current tab',
58 | tabId: updatedTab.id,
59 | windowId: updatedTab.windowId,
60 | url: updatedTab.url,
61 | }),
62 | },
63 | ],
64 | isError: false,
65 | };
66 | }
67 |
68 | // Validate that url is provided when not refreshing
69 | if (!url) {
70 | return createErrorResponse('URL parameter is required when refresh is not true');
71 | }
72 |
73 | // 1. Check if URL is already open
74 | // Get all tabs and manually compare URLs
75 | console.log(`Checking if URL is already open: ${url}`);
76 | // Get all tabs
77 | const allTabs = await chrome.tabs.query({});
78 | // Manually filter matching tabs
79 | const tabs = allTabs.filter((tab) => {
80 | // Normalize URLs for comparison (remove trailing slashes)
81 | const tabUrl = tab.url?.endsWith('/') ? tab.url.slice(0, -1) : tab.url;
82 | const targetUrl = url.endsWith('/') ? url.slice(0, -1) : url;
83 | return tabUrl === targetUrl;
84 | });
85 | console.log(`Found ${tabs.length} matching tabs`);
86 |
87 | if (tabs && tabs.length > 0) {
88 | const existingTab = tabs[0];
89 | console.log(
90 | `URL already open in Tab ID: ${existingTab.id}, Window ID: ${existingTab.windowId}`,
91 | );
92 |
93 | if (existingTab.id !== undefined) {
94 | // Activate the tab
95 | await chrome.tabs.update(existingTab.id, { active: true });
96 |
97 | if (existingTab.windowId !== undefined) {
98 | // Bring the window containing this tab to the foreground and focus it
99 | await chrome.windows.update(existingTab.windowId, { focused: true });
100 | }
101 |
102 | console.log(`Activated existing Tab ID: ${existingTab.id}`);
103 | // Get updated tab information and return it
104 | const updatedTab = await chrome.tabs.get(existingTab.id);
105 |
106 | return {
107 | content: [
108 | {
109 | type: 'text',
110 | text: JSON.stringify({
111 | success: true,
112 | message: 'Activated existing tab',
113 | tabId: updatedTab.id,
114 | windowId: updatedTab.windowId,
115 | url: updatedTab.url,
116 | }),
117 | },
118 | ],
119 | isError: false,
120 | };
121 | }
122 | }
123 |
124 | // 2. If URL is not already open, decide how to open it based on options
125 | const openInNewWindow = newWindow || typeof width === 'number' || typeof height === 'number';
126 |
127 | if (openInNewWindow) {
128 | console.log('Opening URL in a new window.');
129 |
130 | // Create new window
131 | const newWindow = await chrome.windows.create({
132 | url: url,
133 | width: typeof width === 'number' ? width : DEFAULT_WINDOW_WIDTH,
134 | height: typeof height === 'number' ? height : DEFAULT_WINDOW_HEIGHT,
135 | focused: true,
136 | });
137 |
138 | if (newWindow && newWindow.id !== undefined) {
139 | console.log(`URL opened in new Window ID: ${newWindow.id}`);
140 |
141 | return {
142 | content: [
143 | {
144 | type: 'text',
145 | text: JSON.stringify({
146 | success: true,
147 | message: 'Opened URL in new window',
148 | windowId: newWindow.id,
149 | tabs: newWindow.tabs
150 | ? newWindow.tabs.map((tab) => ({
151 | tabId: tab.id,
152 | url: tab.url,
153 | }))
154 | : [],
155 | }),
156 | },
157 | ],
158 | isError: false,
159 | };
160 | }
161 | } else {
162 | console.log('Opening URL in the last active window.');
163 | // Try to open a new tab in the most recently active window
164 | const lastFocusedWindow = await chrome.windows.getLastFocused({ populate: false });
165 |
166 | if (lastFocusedWindow && lastFocusedWindow.id !== undefined) {
167 | console.log(`Found last focused Window ID: ${lastFocusedWindow.id}`);
168 |
169 | const newTab = await chrome.tabs.create({
170 | url: url,
171 | windowId: lastFocusedWindow.id,
172 | active: true,
173 | });
174 |
175 | // Ensure the window also gets focus
176 | await chrome.windows.update(lastFocusedWindow.id, { focused: true });
177 |
178 | console.log(
179 | `URL opened in new Tab ID: ${newTab.id} in existing Window ID: ${lastFocusedWindow.id}`,
180 | );
181 |
182 | return {
183 | content: [
184 | {
185 | type: 'text',
186 | text: JSON.stringify({
187 | success: true,
188 | message: 'Opened URL in new tab in existing window',
189 | tabId: newTab.id,
190 | windowId: lastFocusedWindow.id,
191 | url: newTab.url,
192 | }),
193 | },
194 | ],
195 | isError: false,
196 | };
197 | } else {
198 | // In rare cases, if there's no recently active window (e.g., browser just started with no windows)
199 | // Fall back to opening in a new window
200 | console.warn('No last focused window found, falling back to creating a new window.');
201 |
202 | const fallbackWindow = await chrome.windows.create({
203 | url: url,
204 | width: DEFAULT_WINDOW_WIDTH,
205 | height: DEFAULT_WINDOW_HEIGHT,
206 | focused: true,
207 | });
208 |
209 | if (fallbackWindow && fallbackWindow.id !== undefined) {
210 | console.log(`URL opened in fallback new Window ID: ${fallbackWindow.id}`);
211 |
212 | return {
213 | content: [
214 | {
215 | type: 'text',
216 | text: JSON.stringify({
217 | success: true,
218 | message: 'Opened URL in new window',
219 | windowId: fallbackWindow.id,
220 | tabs: fallbackWindow.tabs
221 | ? fallbackWindow.tabs.map((tab) => ({
222 | tabId: tab.id,
223 | url: tab.url,
224 | }))
225 | : [],
226 | }),
227 | },
228 | ],
229 | isError: false,
230 | };
231 | }
232 | }
233 | }
234 |
235 | // If all attempts fail, return a generic error
236 | return createErrorResponse('Failed to open URL: Unknown error occurred');
237 | } catch (error) {
238 | if (chrome.runtime.lastError) {
239 | console.error(`Chrome API Error: ${chrome.runtime.lastError.message}`, error);
240 | return createErrorResponse(`Chrome API Error: ${chrome.runtime.lastError.message}`);
241 | } else {
242 | console.error('Error in navigate:', error);
243 | return createErrorResponse(
244 | `Error navigating to URL: ${error instanceof Error ? error.message : String(error)}`,
245 | );
246 | }
247 | }
248 | }
249 | }
250 | export const navigateTool = new NavigateTool();
251 |
252 | interface CloseTabsToolParams {
253 | tabIds?: number[];
254 | url?: string;
255 | }
256 |
257 | /**
258 | * Tool for closing browser tabs
259 | */
260 | class CloseTabsTool extends BaseBrowserToolExecutor {
261 | name = TOOL_NAMES.BROWSER.CLOSE_TABS;
262 |
263 | async execute(args: CloseTabsToolParams): Promise<ToolResult> {
264 | const { tabIds, url } = args;
265 | let urlPattern = url;
266 | console.log(`Attempting to close tabs with options:`, args);
267 |
268 | try {
269 | // If URL is provided, close all tabs matching that URL
270 | if (urlPattern) {
271 | console.log(`Searching for tabs with URL: ${url}`);
272 | if (!urlPattern.endsWith('/')) {
273 | urlPattern += '/*';
274 | }
275 | const tabs = await chrome.tabs.query({ url });
276 |
277 | if (!tabs || tabs.length === 0) {
278 | console.log(`No tabs found with URL: ${url}`);
279 | return {
280 | content: [
281 | {
282 | type: 'text',
283 | text: JSON.stringify({
284 | success: false,
285 | message: `No tabs found with URL: ${url}`,
286 | closedCount: 0,
287 | }),
288 | },
289 | ],
290 | isError: false,
291 | };
292 | }
293 |
294 | console.log(`Found ${tabs.length} tabs with URL: ${url}`);
295 | const tabIdsToClose = tabs
296 | .map((tab) => tab.id)
297 | .filter((id): id is number => id !== undefined);
298 |
299 | if (tabIdsToClose.length === 0) {
300 | return createErrorResponse('Found tabs but could not get their IDs');
301 | }
302 |
303 | await chrome.tabs.remove(tabIdsToClose);
304 |
305 | return {
306 | content: [
307 | {
308 | type: 'text',
309 | text: JSON.stringify({
310 | success: true,
311 | message: `Closed ${tabIdsToClose.length} tabs with URL: ${url}`,
312 | closedCount: tabIdsToClose.length,
313 | closedTabIds: tabIdsToClose,
314 | }),
315 | },
316 | ],
317 | isError: false,
318 | };
319 | }
320 |
321 | // If tabIds are provided, close those tabs
322 | if (tabIds && tabIds.length > 0) {
323 | console.log(`Closing tabs with IDs: ${tabIds.join(', ')}`);
324 |
325 | // Verify that all tabIds exist
326 | const existingTabs = await Promise.all(
327 | tabIds.map(async (tabId) => {
328 | try {
329 | return await chrome.tabs.get(tabId);
330 | } catch (error) {
331 | console.warn(`Tab with ID ${tabId} not found`);
332 | return null;
333 | }
334 | }),
335 | );
336 |
337 | const validTabIds = existingTabs
338 | .filter((tab): tab is chrome.tabs.Tab => tab !== null)
339 | .map((tab) => tab.id)
340 | .filter((id): id is number => id !== undefined);
341 |
342 | if (validTabIds.length === 0) {
343 | return {
344 | content: [
345 | {
346 | type: 'text',
347 | text: JSON.stringify({
348 | success: false,
349 | message: 'None of the provided tab IDs exist',
350 | closedCount: 0,
351 | }),
352 | },
353 | ],
354 | isError: false,
355 | };
356 | }
357 |
358 | await chrome.tabs.remove(validTabIds);
359 |
360 | return {
361 | content: [
362 | {
363 | type: 'text',
364 | text: JSON.stringify({
365 | success: true,
366 | message: `Closed ${validTabIds.length} tabs`,
367 | closedCount: validTabIds.length,
368 | closedTabIds: validTabIds,
369 | invalidTabIds: tabIds.filter((id) => !validTabIds.includes(id)),
370 | }),
371 | },
372 | ],
373 | isError: false,
374 | };
375 | }
376 |
377 | // If no tabIds or URL provided, close the current active tab
378 | console.log('No tabIds or URL provided, closing active tab');
379 | const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
380 |
381 | if (!activeTab || !activeTab.id) {
382 | return createErrorResponse('No active tab found');
383 | }
384 |
385 | await chrome.tabs.remove(activeTab.id);
386 |
387 | return {
388 | content: [
389 | {
390 | type: 'text',
391 | text: JSON.stringify({
392 | success: true,
393 | message: 'Closed active tab',
394 | closedCount: 1,
395 | closedTabIds: [activeTab.id],
396 | }),
397 | },
398 | ],
399 | isError: false,
400 | };
401 | } catch (error) {
402 | console.error('Error in CloseTabsTool.execute:', error);
403 | return createErrorResponse(
404 | `Error closing tabs: ${error instanceof Error ? error.message : String(error)}`,
405 | );
406 | }
407 | }
408 | }
409 |
410 | export const closeTabsTool = new CloseTabsTool();
411 |
412 | interface GoBackOrForwardToolParams {
413 | isForward?: boolean;
414 | }
415 |
416 | /**
417 | * Tool for navigating back or forward in browser history
418 | */
419 | class GoBackOrForwardTool extends BaseBrowserToolExecutor {
420 | name = TOOL_NAMES.BROWSER.GO_BACK_OR_FORWARD;
421 |
422 | async execute(args: GoBackOrForwardToolParams): Promise<ToolResult> {
423 | const { isForward = false } = args;
424 |
425 | console.log(`Attempting to navigate ${isForward ? 'forward' : 'back'} in browser history`);
426 |
427 | try {
428 | // Get current active tab
429 | const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
430 |
431 | if (!activeTab || !activeTab.id) {
432 | return createErrorResponse('No active tab found');
433 | }
434 |
435 | // Navigate back or forward based on the isForward parameter
436 | if (isForward) {
437 | await chrome.tabs.goForward(activeTab.id);
438 | console.log(`Navigated forward in tab ID: ${activeTab.id}`);
439 | } else {
440 | await chrome.tabs.goBack(activeTab.id);
441 | console.log(`Navigated back in tab ID: ${activeTab.id}`);
442 | }
443 |
444 | // Get updated tab information
445 | const updatedTab = await chrome.tabs.get(activeTab.id);
446 |
447 | return {
448 | content: [
449 | {
450 | type: 'text',
451 | text: JSON.stringify({
452 | success: true,
453 | message: `Successfully navigated ${isForward ? 'forward' : 'back'} in browser history`,
454 | tabId: updatedTab.id,
455 | windowId: updatedTab.windowId,
456 | url: updatedTab.url,
457 | }),
458 | },
459 | ],
460 | isError: false,
461 | };
462 | } catch (error) {
463 | if (chrome.runtime.lastError) {
464 | console.error(`Chrome API Error: ${chrome.runtime.lastError.message}`, error);
465 | return createErrorResponse(`Chrome API Error: ${chrome.runtime.lastError.message}`);
466 | } else {
467 | console.error('Error in GoBackOrForwardTool.execute:', error);
468 | return createErrorResponse(
469 | `Error navigating ${isForward ? 'forward' : 'back'}: ${
470 | error instanceof Error ? error.message : String(error)
471 | }`,
472 | );
473 | }
474 | }
475 | }
476 | }
477 |
478 | export const goBackOrForwardTool = new GoBackOrForwardTool();
479 |
480 | interface SwitchTabToolParams {
481 | tabId: number;
482 | windowId?: number;
483 | }
484 |
485 | /**
486 | * Tool for switching the active tab
487 | */
488 | class SwitchTabTool extends BaseBrowserToolExecutor {
489 | name = TOOL_NAMES.BROWSER.SWITCH_TAB;
490 |
491 | async execute(args: SwitchTabToolParams): Promise<ToolResult> {
492 | const { tabId, windowId } = args;
493 |
494 | console.log(`Attempting to switch to tab ID: ${tabId} in window ID: ${windowId}`);
495 |
496 | try {
497 | if (windowId !== undefined) {
498 | await chrome.windows.update(windowId, { focused: true });
499 | }
500 | await chrome.tabs.update(tabId, { active: true });
501 |
502 | const updatedTab = await chrome.tabs.get(tabId);
503 |
504 | return {
505 | content: [
506 | {
507 | type: 'text',
508 | text: JSON.stringify({
509 | success: true,
510 | message: `Successfully switched to tab ID: ${tabId}`,
511 | tabId: updatedTab.id,
512 | windowId: updatedTab.windowId,
513 | url: updatedTab.url,
514 | }),
515 | },
516 | ],
517 | isError: false,
518 | };
519 | } catch (error) {
520 | if (chrome.runtime.lastError) {
521 | console.error(`Chrome API Error: ${chrome.runtime.lastError.message}`, error);
522 | return createErrorResponse(`Chrome API Error: ${chrome.runtime.lastError.message}`);
523 | } else {
524 | console.error('Error in SwitchTabTool.execute:', error);
525 | return createErrorResponse(
526 | `Error switching tab: ${error instanceof Error ? error.message : String(error)}`,
527 | );
528 | }
529 | }
530 | }
531 | }
532 |
533 | export const switchTabTool = new SwitchTabTool();
534 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/content-indexer.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Content index manager
3 | * Responsible for automatically extracting, chunking and indexing tab content
4 | */
5 |
6 | import { TextChunker } from './text-chunker';
7 | import { VectorDatabase, getGlobalVectorDatabase } from './vector-database';
8 | import {
9 | SemanticSimilarityEngine,
10 | SemanticSimilarityEngineProxy,
11 | PREDEFINED_MODELS,
12 | type ModelPreset,
13 | } from './semantic-similarity-engine';
14 | import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
15 |
16 | export interface IndexingOptions {
17 | autoIndex?: boolean;
18 | maxChunksPerPage?: number;
19 | skipDuplicates?: boolean;
20 | }
21 |
22 | export class ContentIndexer {
23 | private textChunker: TextChunker;
24 | private vectorDatabase!: VectorDatabase;
25 | private semanticEngine!: SemanticSimilarityEngine | SemanticSimilarityEngineProxy;
26 | private isInitialized = false;
27 | private isInitializing = false;
28 | private initPromise: Promise<void> | null = null;
29 | private indexedPages = new Set<string>();
30 | private readonly options: Required<IndexingOptions>;
31 |
32 | constructor(options?: IndexingOptions) {
33 | this.options = {
34 | autoIndex: true,
35 | maxChunksPerPage: 50,
36 | skipDuplicates: true,
37 | ...options,
38 | };
39 |
40 | this.textChunker = new TextChunker();
41 | }
42 |
43 | /**
44 | * Get current selected model configuration
45 | */
46 | private async getCurrentModelConfig() {
47 | try {
48 | const result = await chrome.storage.local.get(['selectedModel', 'selectedVersion']);
49 | const selectedModel = (result.selectedModel as ModelPreset) || 'multilingual-e5-small';
50 | const selectedVersion =
51 | (result.selectedVersion as 'full' | 'quantized' | 'compressed') || 'quantized';
52 |
53 | const modelInfo = PREDEFINED_MODELS[selectedModel];
54 |
55 | return {
56 | modelPreset: selectedModel,
57 | modelIdentifier: modelInfo.modelIdentifier,
58 | dimension: modelInfo.dimension,
59 | modelVersion: selectedVersion,
60 | useLocalFiles: false,
61 | maxLength: 256,
62 | cacheSize: 1000,
63 | forceOffscreen: true,
64 | };
65 | } catch (error) {
66 | console.error('ContentIndexer: Failed to get current model config, using default:', error);
67 | return {
68 | modelPreset: 'multilingual-e5-small' as const,
69 | modelIdentifier: 'Xenova/multilingual-e5-small',
70 | dimension: 384,
71 | modelVersion: 'quantized' as const,
72 | useLocalFiles: false,
73 | maxLength: 256,
74 | cacheSize: 1000,
75 | forceOffscreen: true,
76 | };
77 | }
78 | }
79 |
80 | /**
81 | * Initialize content indexer
82 | */
83 | public async initialize(): Promise<void> {
84 | if (this.isInitialized) return;
85 | if (this.isInitializing && this.initPromise) return this.initPromise;
86 |
87 | this.isInitializing = true;
88 | this.initPromise = this._doInitialize().finally(() => {
89 | this.isInitializing = false;
90 | });
91 |
92 | return this.initPromise;
93 | }
94 |
95 | private async _doInitialize(): Promise<void> {
96 | try {
97 | // Get current selected model configuration
98 | const engineConfig = await this.getCurrentModelConfig();
99 |
100 | // Use proxy class to reuse engine instance in offscreen
101 | this.semanticEngine = new SemanticSimilarityEngineProxy(engineConfig);
102 | await this.semanticEngine.initialize();
103 |
104 | this.vectorDatabase = await getGlobalVectorDatabase({
105 | dimension: engineConfig.dimension,
106 | efSearch: 50,
107 | });
108 | await this.vectorDatabase.initialize();
109 |
110 | this.setupTabEventListeners();
111 |
112 | this.isInitialized = true;
113 | } catch (error) {
114 | console.error('ContentIndexer: Initialization failed:', error);
115 | this.isInitialized = false;
116 | throw error;
117 | }
118 | }
119 |
120 | /**
121 | * Index content of specified tab
122 | */
123 | public async indexTabContent(tabId: number): Promise<void> {
124 | // Check if semantic engine is ready before attempting to index
125 | if (!this.isSemanticEngineReady() && !this.isSemanticEngineInitializing()) {
126 | console.log(
127 | `ContentIndexer: Skipping tab ${tabId} - semantic engine not ready and not initializing`,
128 | );
129 | return;
130 | }
131 |
132 | if (!this.isInitialized) {
133 | // Only initialize if semantic engine is already ready
134 | if (!this.isSemanticEngineReady()) {
135 | console.log(
136 | `ContentIndexer: Skipping tab ${tabId} - ContentIndexer not initialized and semantic engine not ready`,
137 | );
138 | return;
139 | }
140 | await this.initialize();
141 | }
142 |
143 | try {
144 | const tab = await chrome.tabs.get(tabId);
145 | if (!tab.url || !this.shouldIndexUrl(tab.url)) {
146 | console.log(`ContentIndexer: Skipping tab ${tabId} - URL not indexable`);
147 | return;
148 | }
149 |
150 | const pageKey = `${tab.url}_${tab.title}`;
151 | if (this.options.skipDuplicates && this.indexedPages.has(pageKey)) {
152 | console.log(`ContentIndexer: Skipping tab ${tabId} - already indexed`);
153 | return;
154 | }
155 |
156 | console.log(`ContentIndexer: Starting to index tab ${tabId}: ${tab.title}`);
157 |
158 | const content = await this.extractTabContent(tabId);
159 | if (!content) {
160 | console.log(`ContentIndexer: No content extracted from tab ${tabId}`);
161 | return;
162 | }
163 |
164 | const chunks = this.textChunker.chunkText(content.textContent, content.title);
165 | console.log(`ContentIndexer: Generated ${chunks.length} chunks for tab ${tabId}`);
166 |
167 | const chunksToIndex = chunks.slice(0, this.options.maxChunksPerPage);
168 | if (chunks.length > this.options.maxChunksPerPage) {
169 | console.log(
170 | `ContentIndexer: Limited chunks from ${chunks.length} to ${this.options.maxChunksPerPage}`,
171 | );
172 | }
173 |
174 | for (const chunk of chunksToIndex) {
175 | try {
176 | const embedding = await this.semanticEngine.getEmbedding(chunk.text);
177 | const label = await this.vectorDatabase.addDocument(
178 | tabId,
179 | tab.url!,
180 | tab.title || '',
181 | chunk,
182 | embedding,
183 | );
184 | console.log(`ContentIndexer: Indexed chunk ${chunk.index} with label ${label}`);
185 | } catch (error) {
186 | console.error(`ContentIndexer: Failed to index chunk ${chunk.index}:`, error);
187 | }
188 | }
189 |
190 | this.indexedPages.add(pageKey);
191 |
192 | console.log(
193 | `ContentIndexer: Successfully indexed ${chunksToIndex.length} chunks for tab ${tabId}`,
194 | );
195 | } catch (error) {
196 | console.error(`ContentIndexer: Failed to index tab ${tabId}:`, error);
197 | }
198 | }
199 |
200 | /**
201 | * Search content
202 | */
203 | public async searchContent(query: string, topK: number = 10) {
204 | // Check if semantic engine is ready before attempting to search
205 | if (!this.isSemanticEngineReady() && !this.isSemanticEngineInitializing()) {
206 | throw new Error(
207 | 'Semantic engine is not ready yet. Please initialize the semantic engine first.',
208 | );
209 | }
210 |
211 | if (!this.isInitialized) {
212 | // Only initialize if semantic engine is already ready
213 | if (!this.isSemanticEngineReady()) {
214 | throw new Error(
215 | 'ContentIndexer not initialized and semantic engine not ready. Please initialize the semantic engine first.',
216 | );
217 | }
218 | await this.initialize();
219 | }
220 |
221 | try {
222 | const queryEmbedding = await this.semanticEngine.getEmbedding(query);
223 | const results = await this.vectorDatabase.search(queryEmbedding, topK);
224 |
225 | console.log(`ContentIndexer: Found ${results.length} results for query: "${query}"`);
226 | return results;
227 | } catch (error) {
228 | console.error('ContentIndexer: Search failed:', error);
229 |
230 | if (error instanceof Error && error.message.includes('not initialized')) {
231 | console.log(
232 | 'ContentIndexer: Attempting to reinitialize semantic engine and retry search...',
233 | );
234 | try {
235 | await this.semanticEngine.initialize();
236 | const queryEmbedding = await this.semanticEngine.getEmbedding(query);
237 | const results = await this.vectorDatabase.search(queryEmbedding, topK);
238 |
239 | console.log(
240 | `ContentIndexer: Retry successful, found ${results.length} results for query: "${query}"`,
241 | );
242 | return results;
243 | } catch (retryError) {
244 | console.error('ContentIndexer: Retry after reinitialization also failed:', retryError);
245 | throw retryError;
246 | }
247 | }
248 |
249 | throw error;
250 | }
251 | }
252 |
253 | /**
254 | * Remove tab index
255 | */
256 | public async removeTabIndex(tabId: number): Promise<void> {
257 | if (!this.isInitialized) {
258 | return;
259 | }
260 |
261 | try {
262 | await this.vectorDatabase.removeTabDocuments(tabId);
263 |
264 | for (const pageKey of this.indexedPages) {
265 | if (pageKey.includes(`tab_${tabId}_`)) {
266 | this.indexedPages.delete(pageKey);
267 | }
268 | }
269 |
270 | console.log(`ContentIndexer: Removed index for tab ${tabId}`);
271 | } catch (error) {
272 | console.error(`ContentIndexer: Failed to remove index for tab ${tabId}:`, error);
273 | }
274 | }
275 |
276 | /**
277 | * Check if semantic engine is ready (checks both local and global state)
278 | */
279 | public isSemanticEngineReady(): boolean {
280 | return this.semanticEngine && this.semanticEngine.isInitialized;
281 | }
282 |
283 | /**
284 | * Check if global semantic engine is ready (in background/offscreen)
285 | */
286 | public async isGlobalSemanticEngineReady(): Promise<boolean> {
287 | try {
288 | // Since ContentIndexer runs in background script, directly call the function instead of sending message
289 | const { handleGetModelStatus } = await import('@/entrypoints/background/semantic-similarity');
290 | const response = await handleGetModelStatus();
291 | return (
292 | response &&
293 | response.success &&
294 | response.status &&
295 | response.status.initializationStatus === 'ready'
296 | );
297 | } catch (error) {
298 | console.error('ContentIndexer: Failed to check global semantic engine status:', error);
299 | return false;
300 | }
301 | }
302 |
303 | /**
304 | * Check if semantic engine is initializing
305 | */
306 | public isSemanticEngineInitializing(): boolean {
307 | return (
308 | this.isInitializing || (this.semanticEngine && (this.semanticEngine as any).isInitializing)
309 | );
310 | }
311 |
312 | /**
313 | * Reinitialize content indexer (for model switching)
314 | */
315 | public async reinitialize(): Promise<void> {
316 | console.log('ContentIndexer: Reinitializing for model switch...');
317 |
318 | this.isInitialized = false;
319 | this.isInitializing = false;
320 | this.initPromise = null;
321 |
322 | await this.performCompleteDataCleanupForModelSwitch();
323 |
324 | this.indexedPages.clear();
325 | console.log('ContentIndexer: Cleared indexed pages cache');
326 |
327 | try {
328 | console.log('ContentIndexer: Creating new semantic engine proxy...');
329 | const newEngineConfig = await this.getCurrentModelConfig();
330 | console.log('ContentIndexer: New engine config:', newEngineConfig);
331 |
332 | this.semanticEngine = new SemanticSimilarityEngineProxy(newEngineConfig);
333 | console.log('ContentIndexer: New semantic engine proxy created');
334 |
335 | await this.semanticEngine.initialize();
336 | console.log('ContentIndexer: Semantic engine proxy initialization completed');
337 | } catch (error) {
338 | console.error('ContentIndexer: Failed to create new semantic engine proxy:', error);
339 | throw error;
340 | }
341 |
342 | console.log(
343 | 'ContentIndexer: New semantic engine proxy is ready, proceeding with initialization',
344 | );
345 |
346 | await this.initialize();
347 |
348 | console.log('ContentIndexer: Reinitialization completed successfully');
349 | }
350 |
351 | /**
352 | * Perform complete data cleanup for model switching
353 | */
354 | private async performCompleteDataCleanupForModelSwitch(): Promise<void> {
355 | console.log('ContentIndexer: Starting complete data cleanup for model switch...');
356 |
357 | try {
358 | // Clear existing vector database instance
359 | if (this.vectorDatabase) {
360 | try {
361 | console.log('ContentIndexer: Clearing existing vector database instance...');
362 | await this.vectorDatabase.clear();
363 | console.log('ContentIndexer: Vector database instance cleared successfully');
364 | } catch (error) {
365 | console.warn('ContentIndexer: Failed to clear vector database instance:', error);
366 | }
367 | }
368 |
369 | try {
370 | const { clearAllVectorData } = await import('./vector-database');
371 | await clearAllVectorData();
372 | console.log('ContentIndexer: Cleared all vector data for model switch');
373 | } catch (error) {
374 | console.warn('ContentIndexer: Failed to clear vector data:', error);
375 | }
376 |
377 | try {
378 | const keysToRemove = [
379 | 'hnswlib_document_mappings_tab_content_index.dat',
380 | 'hnswlib_document_mappings_content_index.dat',
381 | 'hnswlib_document_mappings_vector_index.dat',
382 | 'vectorDatabaseStats',
383 | 'lastCleanupTime',
384 | ];
385 | await chrome.storage.local.remove(keysToRemove);
386 | console.log('ContentIndexer: Cleared chrome.storage model-related data');
387 | } catch (error) {
388 | console.warn('ContentIndexer: Failed to clear chrome.storage data:', error);
389 | }
390 |
391 | try {
392 | const deleteVectorDB = indexedDB.deleteDatabase('VectorDatabaseStorage');
393 | await new Promise<void>((resolve) => {
394 | deleteVectorDB.onsuccess = () => {
395 | console.log('ContentIndexer: VectorDatabaseStorage database deleted');
396 | resolve();
397 | };
398 | deleteVectorDB.onerror = () => {
399 | console.warn('ContentIndexer: Failed to delete VectorDatabaseStorage database');
400 | resolve(); // Don't block the process
401 | };
402 | deleteVectorDB.onblocked = () => {
403 | console.warn('ContentIndexer: VectorDatabaseStorage database deletion blocked');
404 | resolve(); // Don't block the process
405 | };
406 | });
407 |
408 | // Clean up hnswlib-index database
409 | const deleteHnswDB = indexedDB.deleteDatabase('/hnswlib-index');
410 | await new Promise<void>((resolve) => {
411 | deleteHnswDB.onsuccess = () => {
412 | console.log('ContentIndexer: /hnswlib-index database deleted');
413 | resolve();
414 | };
415 | deleteHnswDB.onerror = () => {
416 | console.warn('ContentIndexer: Failed to delete /hnswlib-index database');
417 | resolve(); // Don't block the process
418 | };
419 | deleteHnswDB.onblocked = () => {
420 | console.warn('ContentIndexer: /hnswlib-index database deletion blocked');
421 | resolve(); // Don't block the process
422 | };
423 | });
424 |
425 | console.log('ContentIndexer: All IndexedDB databases cleared for model switch');
426 | } catch (error) {
427 | console.warn('ContentIndexer: Failed to clear IndexedDB databases:', error);
428 | }
429 |
430 | console.log('ContentIndexer: Complete data cleanup for model switch finished successfully');
431 | } catch (error) {
432 | console.error('ContentIndexer: Complete data cleanup for model switch failed:', error);
433 | throw error;
434 | }
435 | }
436 |
437 | /**
438 | * Manually trigger semantic engine initialization (async, don't wait for completion)
439 | * Note: This should only be called after the semantic engine is already initialized
440 | */
441 | public startSemanticEngineInitialization(): void {
442 | if (!this.isInitialized && !this.isInitializing) {
443 | console.log('ContentIndexer: Checking if semantic engine is ready...');
444 |
445 | // Check if global semantic engine is ready before initializing ContentIndexer
446 | this.isGlobalSemanticEngineReady()
447 | .then((isReady) => {
448 | if (isReady) {
449 | console.log('ContentIndexer: Starting initialization (semantic engine ready)...');
450 | this.initialize().catch((error) => {
451 | console.error('ContentIndexer: Background initialization failed:', error);
452 | });
453 | } else {
454 | console.log('ContentIndexer: Semantic engine not ready, skipping initialization');
455 | }
456 | })
457 | .catch((error) => {
458 | console.error('ContentIndexer: Failed to check semantic engine status:', error);
459 | });
460 | }
461 | }
462 |
463 | /**
464 | * Get indexing statistics
465 | */
466 | public getStats() {
467 | const vectorStats = this.vectorDatabase
468 | ? this.vectorDatabase.getStats()
469 | : {
470 | totalDocuments: 0,
471 | totalTabs: 0,
472 | indexSize: 0,
473 | };
474 |
475 | return {
476 | ...vectorStats,
477 | indexedPages: this.indexedPages.size,
478 | isInitialized: this.isInitialized,
479 | semanticEngineReady: this.isSemanticEngineReady(),
480 | semanticEngineInitializing: this.isSemanticEngineInitializing(),
481 | };
482 | }
483 |
484 | /**
485 | * Clear all indexes
486 | */
487 | public async clearAllIndexes(): Promise<void> {
488 | if (!this.isInitialized) {
489 | return;
490 | }
491 |
492 | try {
493 | await this.vectorDatabase.clear();
494 | this.indexedPages.clear();
495 | console.log('ContentIndexer: All indexes cleared');
496 | } catch (error) {
497 | console.error('ContentIndexer: Failed to clear indexes:', error);
498 | }
499 | }
500 | private setupTabEventListeners(): void {
501 | chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
502 | if (this.options.autoIndex && changeInfo.status === 'complete' && tab.url) {
503 | setTimeout(() => {
504 | if (!this.isSemanticEngineReady() && !this.isSemanticEngineInitializing()) {
505 | console.log(
506 | `ContentIndexer: Skipping auto-index for tab ${tabId} - semantic engine not ready`,
507 | );
508 | return;
509 | }
510 |
511 | this.indexTabContent(tabId).catch((error) => {
512 | console.error(`ContentIndexer: Auto-indexing failed for tab ${tabId}:`, error);
513 | });
514 | }, 2000);
515 | }
516 | });
517 |
518 | chrome.tabs.onRemoved.addListener(async (tabId) => {
519 | await this.removeTabIndex(tabId);
520 | });
521 |
522 | if (chrome.webNavigation) {
523 | chrome.webNavigation.onCommitted.addListener(async (details) => {
524 | if (details.frameId === 0) {
525 | await this.removeTabIndex(details.tabId);
526 | }
527 | });
528 | }
529 | }
530 |
531 | private shouldIndexUrl(url: string): boolean {
532 | const excludePatterns = [
533 | /^chrome:\/\//,
534 | /^chrome-extension:\/\//,
535 | /^edge:\/\//,
536 | /^about:/,
537 | /^moz-extension:\/\//,
538 | /^file:\/\//,
539 | ];
540 |
541 | return !excludePatterns.some((pattern) => pattern.test(url));
542 | }
543 |
544 | private async extractTabContent(
545 | tabId: number,
546 | ): Promise<{ textContent: string; title: string } | null> {
547 | try {
548 | await chrome.scripting.executeScript({
549 | target: { tabId },
550 | files: ['inject-scripts/web-fetcher-helper.js'],
551 | });
552 |
553 | const response = await chrome.tabs.sendMessage(tabId, {
554 | action: TOOL_MESSAGE_TYPES.WEB_FETCHER_GET_TEXT_CONTENT,
555 | });
556 |
557 | if (response.success && response.textContent) {
558 | return {
559 | textContent: response.textContent,
560 | title: response.title || '',
561 | };
562 | } else {
563 | console.error(
564 | `ContentIndexer: Failed to extract content from tab ${tabId}:`,
565 | response.error,
566 | );
567 | return null;
568 | }
569 | } catch (error) {
570 | console.error(`ContentIndexer: Error extracting content from tab ${tabId}:`, error);
571 | return null;
572 | }
573 | }
574 | }
575 |
576 | let globalContentIndexer: ContentIndexer | null = null;
577 |
578 | /**
579 | * Get global ContentIndexer instance
580 | */
581 | export function getGlobalContentIndexer(): ContentIndexer {
582 | if (!globalContentIndexer) {
583 | globalContentIndexer = new ContentIndexer();
584 | }
585 | return globalContentIndexer;
586 | }
587 |
```
--------------------------------------------------------------------------------
/packages/shared/src/tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { type Tool } from '@modelcontextprotocol/sdk/types.js';
2 |
3 | export const TOOL_NAMES = {
4 | BROWSER: {
5 | GET_WINDOWS_AND_TABS: 'get_windows_and_tabs',
6 | SEARCH_TABS_CONTENT: 'search_tabs_content',
7 | NAVIGATE: 'chrome_navigate',
8 | SCREENSHOT: 'chrome_screenshot',
9 | CLOSE_TABS: 'chrome_close_tabs',
10 | SWITCH_TAB: 'chrome_switch_tab',
11 | GO_BACK_OR_FORWARD: 'chrome_go_back_or_forward',
12 | WEB_FETCHER: 'chrome_get_web_content',
13 | CLICK: 'chrome_click_element',
14 | FILL: 'chrome_fill_or_select',
15 | GET_INTERACTIVE_ELEMENTS: 'chrome_get_interactive_elements',
16 | NETWORK_CAPTURE_START: 'chrome_network_capture_start',
17 | NETWORK_CAPTURE_STOP: 'chrome_network_capture_stop',
18 | NETWORK_REQUEST: 'chrome_network_request',
19 | NETWORK_DEBUGGER_START: 'chrome_network_debugger_start',
20 | NETWORK_DEBUGGER_STOP: 'chrome_network_debugger_stop',
21 | KEYBOARD: 'chrome_keyboard',
22 | HISTORY: 'chrome_history',
23 | BOOKMARK_SEARCH: 'chrome_bookmark_search',
24 | BOOKMARK_ADD: 'chrome_bookmark_add',
25 | BOOKMARK_DELETE: 'chrome_bookmark_delete',
26 | INJECT_SCRIPT: 'chrome_inject_script',
27 | SEND_COMMAND_TO_INJECT_SCRIPT: 'chrome_send_command_to_inject_script',
28 | CONSOLE: 'chrome_console',
29 | FILE_UPLOAD: 'chrome_upload_file',
30 | },
31 | };
32 |
33 | export const TOOL_SCHEMAS: Tool[] = [
34 | {
35 | name: TOOL_NAMES.BROWSER.GET_WINDOWS_AND_TABS,
36 | description: 'Get all currently open browser windows and tabs',
37 | inputSchema: {
38 | type: 'object',
39 | properties: {},
40 | required: [],
41 | },
42 | },
43 | {
44 | name: TOOL_NAMES.BROWSER.NAVIGATE,
45 | description: 'Navigate to a URL or refresh the current tab',
46 | inputSchema: {
47 | type: 'object',
48 | properties: {
49 | url: { type: 'string', description: 'URL to navigate to the website specified' },
50 | newWindow: {
51 | type: 'boolean',
52 | description: 'Create a new window to navigate to the URL or not. Defaults to false',
53 | },
54 | width: { type: 'number', description: 'Viewport width in pixels (default: 1280)' },
55 | height: { type: 'number', description: 'Viewport height in pixels (default: 720)' },
56 | refresh: {
57 | type: 'boolean',
58 | description:
59 | 'Refresh the current active tab instead of navigating to a URL. When true, the url parameter is ignored. Defaults to false',
60 | },
61 | },
62 | required: [],
63 | },
64 | },
65 | {
66 | name: TOOL_NAMES.BROWSER.SCREENSHOT,
67 | description:
68 | 'Take a screenshot of the current page or a specific element(if you want to see the page, recommend to use chrome_get_web_content first)',
69 | inputSchema: {
70 | type: 'object',
71 | properties: {
72 | name: { type: 'string', description: 'Name for the screenshot, if saving as PNG' },
73 | selector: { type: 'string', description: 'CSS selector for element to screenshot' },
74 | width: { type: 'number', description: 'Width in pixels (default: 800)' },
75 | height: { type: 'number', description: 'Height in pixels (default: 600)' },
76 | storeBase64: {
77 | type: 'boolean',
78 | description:
79 | 'return screenshot in base64 format (default: false) if you want to see the page, recommend set this to be true',
80 | },
81 | fullPage: {
82 | type: 'boolean',
83 | description: 'Store screenshot of the entire page (default: true)',
84 | },
85 | savePng: {
86 | type: 'boolean',
87 | description:
88 | 'Save screenshot as PNG file (default: true),if you want to see the page, recommend set this to be false, and set storeBase64 to be true',
89 | },
90 | },
91 | required: [],
92 | },
93 | },
94 | {
95 | name: TOOL_NAMES.BROWSER.CLOSE_TABS,
96 | description: 'Close one or more browser tabs',
97 | inputSchema: {
98 | type: 'object',
99 | properties: {
100 | tabIds: {
101 | type: 'array',
102 | items: { type: 'number' },
103 | description: 'Array of tab IDs to close. If not provided, will close the active tab.',
104 | },
105 | url: {
106 | type: 'string',
107 | description: 'Close tabs matching this URL. Can be used instead of tabIds.',
108 | },
109 | },
110 | required: [],
111 | },
112 | },
113 | {
114 | name: TOOL_NAMES.BROWSER.SWITCH_TAB,
115 | description: 'Switch to a specific browser tab',
116 | inputSchema: {
117 | type: 'object',
118 | properties: {
119 | tabId: {
120 | type: 'number',
121 | description: 'The ID of the tab to switch to.',
122 | },
123 | windowId: {
124 | type: 'number',
125 | description: 'The ID of the window where the tab is located.',
126 | },
127 | },
128 | required: ['tabId'],
129 | },
130 | },
131 | {
132 | name: TOOL_NAMES.BROWSER.GO_BACK_OR_FORWARD,
133 | description: 'Navigate back or forward in browser history',
134 | inputSchema: {
135 | type: 'object',
136 | properties: {
137 | isForward: {
138 | type: 'boolean',
139 | description: 'Go forward in history if true, go back if false (default: false)',
140 | },
141 | },
142 | required: [],
143 | },
144 | },
145 | {
146 | name: TOOL_NAMES.BROWSER.WEB_FETCHER,
147 | description: 'Fetch content from a web page',
148 | inputSchema: {
149 | type: 'object',
150 | properties: {
151 | url: {
152 | type: 'string',
153 | description: 'URL to fetch content from. If not provided, uses the current active tab',
154 | },
155 | htmlContent: {
156 | type: 'boolean',
157 | description:
158 | 'Get the visible HTML content of the page. If true, textContent will be ignored (default: false)',
159 | },
160 | textContent: {
161 | type: 'boolean',
162 | description:
163 | 'Get the visible text content of the page with metadata. Ignored if htmlContent is true (default: true)',
164 | },
165 |
166 | selector: {
167 | type: 'string',
168 | description:
169 | 'CSS selector to get content from a specific element. If provided, only content from this element will be returned',
170 | },
171 | },
172 | required: [],
173 | },
174 | },
175 | {
176 | name: TOOL_NAMES.BROWSER.CLICK,
177 | description: 'Click on an element in the current page or at specific coordinates',
178 | inputSchema: {
179 | type: 'object',
180 | properties: {
181 | selector: {
182 | type: 'string',
183 | description:
184 | 'CSS selector for the element to click. Either selector or coordinates must be provided. if coordinates are not provided, the selector must be provided.',
185 | },
186 | coordinates: {
187 | type: 'object',
188 | description:
189 | 'Coordinates to click at (relative to viewport). If provided, takes precedence over selector.',
190 | properties: {
191 | x: {
192 | type: 'number',
193 | description: 'X coordinate relative to the viewport',
194 | },
195 | y: {
196 | type: 'number',
197 | description: 'Y coordinate relative to the viewport',
198 | },
199 | },
200 | required: ['x', 'y'],
201 | },
202 | waitForNavigation: {
203 | type: 'boolean',
204 | description: 'Wait for page navigation to complete after click (default: false)',
205 | },
206 | timeout: {
207 | type: 'number',
208 | description:
209 | 'Timeout in milliseconds for waiting for the element or navigation (default: 5000)',
210 | },
211 | },
212 | required: [],
213 | },
214 | },
215 | {
216 | name: TOOL_NAMES.BROWSER.FILL,
217 | description: 'Fill a form element or select an option with the specified value',
218 | inputSchema: {
219 | type: 'object',
220 | properties: {
221 | selector: {
222 | type: 'string',
223 | description: 'CSS selector for the input element to fill or select',
224 | },
225 | value: {
226 | type: 'string',
227 | description: 'Value to fill or select into the element',
228 | },
229 | },
230 | required: ['selector', 'value'],
231 | },
232 | },
233 | {
234 | name: TOOL_NAMES.BROWSER.GET_INTERACTIVE_ELEMENTS,
235 | description: 'Get interactive elements from the current page',
236 | inputSchema: {
237 | type: 'object',
238 | properties: {
239 | textQuery: {
240 | type: 'string',
241 | description: 'Text to search for within interactive elements (fuzzy search)',
242 | },
243 | selector: {
244 | type: 'string',
245 | description:
246 | 'CSS selector to filter interactive elements. Takes precedence over textQuery if both are provided.',
247 | },
248 | includeCoordinates: {
249 | type: 'boolean',
250 | description: 'Include element coordinates in the response (default: true)',
251 | },
252 | },
253 | required: [],
254 | },
255 | },
256 | {
257 | name: TOOL_NAMES.BROWSER.NETWORK_REQUEST,
258 | description: 'Send a network request from the browser with cookies and other browser context',
259 | inputSchema: {
260 | type: 'object',
261 | properties: {
262 | url: {
263 | type: 'string',
264 | description: 'URL to send the request to',
265 | },
266 | method: {
267 | type: 'string',
268 | description: 'HTTP method to use (default: GET)',
269 | },
270 | headers: {
271 | type: 'object',
272 | description: 'Headers to include in the request',
273 | },
274 | body: {
275 | type: 'string',
276 | description: 'Body of the request (for POST, PUT, etc.)',
277 | },
278 | timeout: {
279 | type: 'number',
280 | description: 'Timeout in milliseconds (default: 30000)',
281 | },
282 | },
283 | required: ['url'],
284 | },
285 | },
286 | {
287 | name: TOOL_NAMES.BROWSER.NETWORK_DEBUGGER_START,
288 | description:
289 | 'Start capturing network requests from a web page using Chrome Debugger API(with responseBody)',
290 | inputSchema: {
291 | type: 'object',
292 | properties: {
293 | url: {
294 | type: 'string',
295 | description:
296 | 'URL to capture network requests from. If not provided, uses the current active tab',
297 | },
298 | },
299 | required: [],
300 | },
301 | },
302 | {
303 | name: TOOL_NAMES.BROWSER.NETWORK_DEBUGGER_STOP,
304 | description:
305 | 'Stop capturing network requests using Chrome Debugger API and return the captured data',
306 | inputSchema: {
307 | type: 'object',
308 | properties: {},
309 | required: [],
310 | },
311 | },
312 | {
313 | name: TOOL_NAMES.BROWSER.NETWORK_CAPTURE_START,
314 | description:
315 | 'Start capturing network requests from a web page using Chrome webRequest API(without responseBody)',
316 | inputSchema: {
317 | type: 'object',
318 | properties: {
319 | url: {
320 | type: 'string',
321 | description:
322 | 'URL to capture network requests from. If not provided, uses the current active tab',
323 | },
324 | },
325 | required: [],
326 | },
327 | },
328 | {
329 | name: TOOL_NAMES.BROWSER.NETWORK_CAPTURE_STOP,
330 | description:
331 | 'Stop capturing network requests using webRequest API and return the captured data',
332 | inputSchema: {
333 | type: 'object',
334 | properties: {},
335 | required: [],
336 | },
337 | },
338 | {
339 | name: TOOL_NAMES.BROWSER.KEYBOARD,
340 | description: 'Simulate keyboard events in the browser',
341 | inputSchema: {
342 | type: 'object',
343 | properties: {
344 | keys: {
345 | type: 'string',
346 | description: 'Keys to simulate (e.g., "Enter", "Ctrl+C", "A,B,C" for sequence)',
347 | },
348 | selector: {
349 | type: 'string',
350 | description:
351 | 'CSS selector for the element to send keyboard events to (optional, defaults to active element)',
352 | },
353 | delay: {
354 | type: 'number',
355 | description: 'Delay between key sequences in milliseconds (optional, default: 0)',
356 | },
357 | },
358 | required: ['keys'],
359 | },
360 | },
361 | {
362 | name: TOOL_NAMES.BROWSER.HISTORY,
363 | description: 'Retrieve and search browsing history from Chrome',
364 | inputSchema: {
365 | type: 'object',
366 | properties: {
367 | text: {
368 | type: 'string',
369 | description:
370 | 'Text to search for in history URLs and titles. Leave empty to retrieve all history entries within the time range.',
371 | },
372 | startTime: {
373 | type: 'string',
374 | description:
375 | 'Start time as a date string. Supports ISO format (e.g., "2023-10-01", "2023-10-01T14:30:00"), relative times (e.g., "1 day ago", "2 weeks ago", "3 months ago", "1 year ago"), and special keywords ("now", "today", "yesterday"). Default: 24 hours ago',
376 | },
377 | endTime: {
378 | type: 'string',
379 | description:
380 | 'End time as a date string. Supports ISO format (e.g., "2023-10-31", "2023-10-31T14:30:00"), relative times (e.g., "1 day ago", "2 weeks ago", "3 months ago", "1 year ago"), and special keywords ("now", "today", "yesterday"). Default: current time',
381 | },
382 | maxResults: {
383 | type: 'number',
384 | description:
385 | 'Maximum number of history entries to return. Use this to limit results for performance or to focus on the most relevant entries. (default: 100)',
386 | },
387 | excludeCurrentTabs: {
388 | type: 'boolean',
389 | description:
390 | "When set to true, filters out URLs that are currently open in any browser tab. Useful for finding pages you've visited but don't have open anymore. (default: false)",
391 | },
392 | },
393 | required: [],
394 | },
395 | },
396 | {
397 | name: TOOL_NAMES.BROWSER.BOOKMARK_SEARCH,
398 | description: 'Search Chrome bookmarks by title and URL',
399 | inputSchema: {
400 | type: 'object',
401 | properties: {
402 | query: {
403 | type: 'string',
404 | description:
405 | 'Search query to match against bookmark titles and URLs. Leave empty to retrieve all bookmarks.',
406 | },
407 | maxResults: {
408 | type: 'number',
409 | description: 'Maximum number of bookmarks to return (default: 50)',
410 | },
411 | folderPath: {
412 | type: 'string',
413 | description:
414 | 'Optional folder path or ID to limit search to a specific bookmark folder. Can be a path string (e.g., "Work/Projects") or a folder ID.',
415 | },
416 | },
417 | required: [],
418 | },
419 | },
420 | {
421 | name: TOOL_NAMES.BROWSER.BOOKMARK_ADD,
422 | description: 'Add a new bookmark to Chrome',
423 | inputSchema: {
424 | type: 'object',
425 | properties: {
426 | url: {
427 | type: 'string',
428 | description: 'URL to bookmark. If not provided, uses the current active tab URL.',
429 | },
430 | title: {
431 | type: 'string',
432 | description: 'Title for the bookmark. If not provided, uses the page title from the URL.',
433 | },
434 | parentId: {
435 | type: 'string',
436 | description:
437 | 'Parent folder path or ID to add the bookmark to. Can be a path string (e.g., "Work/Projects") or a folder ID. If not provided, adds to the "Bookmarks Bar" folder.',
438 | },
439 | createFolder: {
440 | type: 'boolean',
441 | description: 'Whether to create the parent folder if it does not exist (default: false)',
442 | },
443 | },
444 | required: [],
445 | },
446 | },
447 | {
448 | name: TOOL_NAMES.BROWSER.BOOKMARK_DELETE,
449 | description: 'Delete a bookmark from Chrome',
450 | inputSchema: {
451 | type: 'object',
452 | properties: {
453 | bookmarkId: {
454 | type: 'string',
455 | description: 'ID of the bookmark to delete. Either bookmarkId or url must be provided.',
456 | },
457 | url: {
458 | type: 'string',
459 | description: 'URL of the bookmark to delete. Used if bookmarkId is not provided.',
460 | },
461 | title: {
462 | type: 'string',
463 | description: 'Title of the bookmark to help with matching when deleting by URL.',
464 | },
465 | },
466 | required: [],
467 | },
468 | },
469 | {
470 | name: TOOL_NAMES.BROWSER.SEARCH_TABS_CONTENT,
471 | description:
472 | 'search for related content from the currently open tab and return the corresponding web pages.',
473 | inputSchema: {
474 | type: 'object',
475 | properties: {
476 | query: {
477 | type: 'string',
478 | description: 'the query to search for related content.',
479 | },
480 | },
481 | required: ['query'],
482 | },
483 | },
484 | {
485 | name: TOOL_NAMES.BROWSER.INJECT_SCRIPT,
486 | description:
487 | 'inject the user-specified content script into the webpage. By default, inject into the currently active tab',
488 | inputSchema: {
489 | type: 'object',
490 | properties: {
491 | url: {
492 | type: 'string',
493 | description:
494 | 'If a URL is specified, inject the script into the webpage corresponding to the URL.',
495 | },
496 | type: {
497 | type: 'string',
498 | description:
499 | 'the javaScript world for a script to execute within. must be ISOLATED or MAIN',
500 | },
501 | jsScript: {
502 | type: 'string',
503 | description: 'the content script to inject',
504 | },
505 | },
506 | required: ['type', 'jsScript'],
507 | },
508 | },
509 | {
510 | name: TOOL_NAMES.BROWSER.SEND_COMMAND_TO_INJECT_SCRIPT,
511 | description:
512 | 'if the script injected using chrome_inject_script listens for user-defined events, this tool can be used to trigger those events',
513 | inputSchema: {
514 | type: 'object',
515 | properties: {
516 | tabId: {
517 | type: 'number',
518 | description:
519 | 'the tab where you previously injected the script(if not provided, use the currently active tab)',
520 | },
521 | eventName: {
522 | type: 'string',
523 | description: 'the eventName your injected content script listen for',
524 | },
525 | payload: {
526 | type: 'string',
527 | description: 'the payload passed to event, must be a json string',
528 | },
529 | },
530 | required: ['eventName'],
531 | },
532 | },
533 | {
534 | name: TOOL_NAMES.BROWSER.CONSOLE,
535 | description:
536 | 'Capture and retrieve all console output from the current active browser tab/page. This captures console messages that existed before the tool was called.',
537 | inputSchema: {
538 | type: 'object',
539 | properties: {
540 | url: {
541 | type: 'string',
542 | description:
543 | 'URL to navigate to and capture console from. If not provided, uses the current active tab',
544 | },
545 | includeExceptions: {
546 | type: 'boolean',
547 | description: 'Include uncaught exceptions in the output (default: true)',
548 | },
549 | maxMessages: {
550 | type: 'number',
551 | description: 'Maximum number of console messages to capture (default: 100)',
552 | },
553 | },
554 | required: [],
555 | },
556 | },
557 | {
558 | name: TOOL_NAMES.BROWSER.FILE_UPLOAD,
559 | description: 'Upload files to web forms with file input elements using Chrome DevTools Protocol',
560 | inputSchema: {
561 | type: 'object',
562 | properties: {
563 | selector: {
564 | type: 'string',
565 | description: 'CSS selector for the file input element (input[type="file"])',
566 | },
567 | filePath: {
568 | type: 'string',
569 | description: 'Local file path to upload',
570 | },
571 | fileUrl: {
572 | type: 'string',
573 | description: 'URL to download file from before uploading',
574 | },
575 | base64Data: {
576 | type: 'string',
577 | description: 'Base64 encoded file data to upload',
578 | },
579 | fileName: {
580 | type: 'string',
581 | description: 'Optional filename when using base64 or URL (default: "uploaded-file")',
582 | },
583 | multiple: {
584 | type: 'boolean',
585 | description: 'Whether the input accepts multiple files (default: false)',
586 | },
587 | },
588 | required: ['selector'],
589 | },
590 | },
591 | ];
592 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/bookmark.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { createErrorResponse, ToolResult } from '@/common/tool-handler';
2 | import { BaseBrowserToolExecutor } from '../base-browser';
3 | import { TOOL_NAMES } from 'chrome-mcp-shared';
4 | import { getMessage } from '@/utils/i18n';
5 |
6 | /**
7 | * Bookmark search tool parameters interface
8 | */
9 | interface BookmarkSearchToolParams {
10 | query?: string; // Search keywords for matching bookmark titles and URLs
11 | maxResults?: number; // Maximum number of results to return
12 | folderPath?: string; // Optional, specify which folder to search in (can be ID or path string like "Work/Projects")
13 | }
14 |
15 | /**
16 | * Bookmark add tool parameters interface
17 | */
18 | interface BookmarkAddToolParams {
19 | url?: string; // URL to add as bookmark, if not provided use current active tab URL
20 | title?: string; // Bookmark title, if not provided use page title
21 | parentId?: string; // Parent folder ID or path string (like "Work/Projects"), if not provided add to "Bookmarks Bar" folder
22 | createFolder?: boolean; // Whether to automatically create parent folder if it doesn't exist
23 | }
24 |
25 | /**
26 | * Bookmark delete tool parameters interface
27 | */
28 | interface BookmarkDeleteToolParams {
29 | bookmarkId?: string; // ID of bookmark to delete
30 | url?: string; // URL of bookmark to delete (if ID not provided, search by URL)
31 | title?: string; // Title of bookmark to delete (used for auxiliary matching, used together with URL)
32 | }
33 |
34 | // --- Helper Functions ---
35 |
36 | /**
37 | * Get the complete folder path of a bookmark
38 | * @param bookmarkNodeId ID of the bookmark or folder
39 | * @returns Returns folder path string (e.g., "Bookmarks Bar > Folder A > Subfolder B")
40 | */
41 | async function getBookmarkFolderPath(bookmarkNodeId: string): Promise<string> {
42 | const pathParts: string[] = [];
43 |
44 | try {
45 | // First get the node itself to check if it's a bookmark or folder
46 | const initialNodes = await chrome.bookmarks.get(bookmarkNodeId);
47 | if (initialNodes.length > 0 && initialNodes[0]) {
48 | const initialNode = initialNodes[0];
49 |
50 | // Build path starting from parent node (same for both bookmarks and folders)
51 | let pathNodeId = initialNode.parentId;
52 | while (pathNodeId) {
53 | const parentNodes = await chrome.bookmarks.get(pathNodeId);
54 | if (parentNodes.length === 0) break;
55 |
56 | const parentNode = parentNodes[0];
57 | if (parentNode.title) {
58 | pathParts.unshift(parentNode.title);
59 | }
60 |
61 | if (!parentNode.parentId) break;
62 | pathNodeId = parentNode.parentId;
63 | }
64 | }
65 | } catch (error) {
66 | console.error(`Error getting bookmark path for node ID ${bookmarkNodeId}:`, error);
67 | return pathParts.join(' > ') || 'Error getting path';
68 | }
69 |
70 | return pathParts.join(' > ');
71 | }
72 |
73 | /**
74 | * Find bookmark folder by ID or path string
75 | * If it's an ID, validate it
76 | * If it's a path string, try to parse it
77 | * @param pathOrId Path string (e.g., "Work/Projects") or folder ID
78 | * @returns Returns folder node, or null if not found
79 | */
80 | async function findFolderByPathOrId(
81 | pathOrId: string,
82 | ): Promise<chrome.bookmarks.BookmarkTreeNode | null> {
83 | try {
84 | const nodes = await chrome.bookmarks.get(pathOrId);
85 | if (nodes && nodes.length > 0 && !nodes[0].url) {
86 | return nodes[0];
87 | }
88 | } catch (e) {
89 | // do nothing, try to parse as path string
90 | }
91 |
92 | const pathParts = pathOrId
93 | .split('/')
94 | .map((p) => p.trim())
95 | .filter((p) => p.length > 0);
96 | if (pathParts.length === 0) return null;
97 |
98 | const rootChildren = await chrome.bookmarks.getChildren('0');
99 |
100 | let currentNodes = rootChildren;
101 | let foundFolder: chrome.bookmarks.BookmarkTreeNode | null = null;
102 |
103 | for (let i = 0; i < pathParts.length; i++) {
104 | const part = pathParts[i];
105 | foundFolder = null;
106 | let matchedNodeThisLevel: chrome.bookmarks.BookmarkTreeNode | null = null;
107 |
108 | for (const node of currentNodes) {
109 | if (!node.url && node.title.toLowerCase() === part.toLowerCase()) {
110 | matchedNodeThisLevel = node;
111 | break;
112 | }
113 | }
114 |
115 | if (matchedNodeThisLevel) {
116 | if (i === pathParts.length - 1) {
117 | foundFolder = matchedNodeThisLevel;
118 | } else {
119 | currentNodes = await chrome.bookmarks.getChildren(matchedNodeThisLevel.id);
120 | }
121 | } else {
122 | return null;
123 | }
124 | }
125 |
126 | return foundFolder;
127 | }
128 |
129 | /**
130 | * Create folder path (if it doesn't exist)
131 | * @param folderPath Folder path string (e.g., "Work/Projects/Subproject")
132 | * @param parentId Optional parent folder ID, defaults to "Bookmarks Bar"
133 | * @returns Returns the created or found final folder node
134 | */
135 | async function createFolderPath(
136 | folderPath: string,
137 | parentId?: string,
138 | ): Promise<chrome.bookmarks.BookmarkTreeNode> {
139 | const pathParts = folderPath
140 | .split('/')
141 | .map((p) => p.trim())
142 | .filter((p) => p.length > 0);
143 |
144 | if (pathParts.length === 0) {
145 | throw new Error('Folder path cannot be empty');
146 | }
147 |
148 | // If no parent ID specified, use "Bookmarks Bar" folder
149 | let currentParentId: string = parentId || '';
150 | if (!currentParentId) {
151 | const rootChildren = await chrome.bookmarks.getChildren('0');
152 | // Find "Bookmarks Bar" folder (usually ID is '1', but search by title for compatibility)
153 | const bookmarkBarFolder = rootChildren.find(
154 | (node) =>
155 | !node.url &&
156 | (node.title === getMessage('bookmarksBarLabel') ||
157 | node.title === 'Bookmarks bar' ||
158 | node.title === 'Bookmarks Bar'),
159 | );
160 | currentParentId = bookmarkBarFolder?.id || '1'; // fallback to default ID
161 | }
162 |
163 | let currentFolder: chrome.bookmarks.BookmarkTreeNode | null = null;
164 |
165 | // Create or find folders level by level
166 | for (const folderName of pathParts) {
167 | const children: chrome.bookmarks.BookmarkTreeNode[] =
168 | await chrome.bookmarks.getChildren(currentParentId);
169 |
170 | // Check if folder with same name already exists
171 | const existingFolder: chrome.bookmarks.BookmarkTreeNode | undefined = children.find(
172 | (child: chrome.bookmarks.BookmarkTreeNode) =>
173 | !child.url && child.title.toLowerCase() === folderName.toLowerCase(),
174 | );
175 |
176 | if (existingFolder) {
177 | currentFolder = existingFolder;
178 | currentParentId = existingFolder.id;
179 | } else {
180 | // Create new folder
181 | currentFolder = await chrome.bookmarks.create({
182 | parentId: currentParentId,
183 | title: folderName,
184 | });
185 | currentParentId = currentFolder.id;
186 | }
187 | }
188 |
189 | if (!currentFolder) {
190 | throw new Error('Failed to create folder path');
191 | }
192 |
193 | return currentFolder;
194 | }
195 |
196 | /**
197 | * Flatten bookmark tree (or node array) to bookmark list (excluding folders)
198 | * @param nodes Bookmark tree nodes to flatten
199 | * @returns Returns actual bookmark node array (nodes with URLs)
200 | */
201 | function flattenBookmarkNodesToBookmarks(
202 | nodes: chrome.bookmarks.BookmarkTreeNode[],
203 | ): chrome.bookmarks.BookmarkTreeNode[] {
204 | const result: chrome.bookmarks.BookmarkTreeNode[] = [];
205 | const stack = [...nodes]; // Use stack for iterative traversal to avoid deep recursion issues
206 |
207 | while (stack.length > 0) {
208 | const node = stack.pop();
209 | if (!node) continue;
210 |
211 | if (node.url) {
212 | // It's a bookmark
213 | result.push(node);
214 | }
215 |
216 | if (node.children) {
217 | // Add child nodes to stack for processing
218 | for (let i = node.children.length - 1; i >= 0; i--) {
219 | stack.push(node.children[i]);
220 | }
221 | }
222 | }
223 |
224 | return result;
225 | }
226 |
227 | /**
228 | * Find bookmarks by URL and title
229 | * @param url Bookmark URL
230 | * @param title Optional bookmark title for auxiliary matching
231 | * @returns Returns array of matching bookmarks
232 | */
233 | async function findBookmarksByUrl(
234 | url: string,
235 | title?: string,
236 | ): Promise<chrome.bookmarks.BookmarkTreeNode[]> {
237 | // Use Chrome API to search by URL
238 | const searchResults = await chrome.bookmarks.search({ url });
239 |
240 | if (!title) {
241 | return searchResults;
242 | }
243 |
244 | // If title is provided, further filter results
245 | const titleLower = title.toLowerCase();
246 | return searchResults.filter(
247 | (bookmark) => bookmark.title && bookmark.title.toLowerCase().includes(titleLower),
248 | );
249 | }
250 |
251 | /**
252 | * Bookmark search tool
253 | * Used to search bookmarks in Chrome browser
254 | */
255 | class BookmarkSearchTool extends BaseBrowserToolExecutor {
256 | name = TOOL_NAMES.BROWSER.BOOKMARK_SEARCH;
257 |
258 | /**
259 | * Execute bookmark search
260 | */
261 | async execute(args: BookmarkSearchToolParams): Promise<ToolResult> {
262 | const { query = '', maxResults = 50, folderPath } = args;
263 |
264 | console.log(
265 | `BookmarkSearchTool: Searching bookmarks, keywords: "${query}", folder path: "${folderPath}"`,
266 | );
267 |
268 | try {
269 | let bookmarksToSearch: chrome.bookmarks.BookmarkTreeNode[] = [];
270 | let targetFolderNode: chrome.bookmarks.BookmarkTreeNode | null = null;
271 |
272 | // If folder path is specified, find that folder first
273 | if (folderPath) {
274 | targetFolderNode = await findFolderByPathOrId(folderPath);
275 | if (!targetFolderNode) {
276 | return createErrorResponse(`Specified folder not found: "${folderPath}"`);
277 | }
278 | // Get all bookmarks in that folder and its subfolders
279 | const subTree = await chrome.bookmarks.getSubTree(targetFolderNode.id);
280 | bookmarksToSearch =
281 | subTree.length > 0 ? flattenBookmarkNodesToBookmarks(subTree[0].children || []) : [];
282 | }
283 |
284 | let filteredBookmarks: chrome.bookmarks.BookmarkTreeNode[];
285 |
286 | if (query) {
287 | if (targetFolderNode) {
288 | // Has query keywords and specified folder: manually filter bookmarks from folder
289 | const lowerCaseQuery = query.toLowerCase();
290 | filteredBookmarks = bookmarksToSearch.filter(
291 | (bookmark) =>
292 | (bookmark.title && bookmark.title.toLowerCase().includes(lowerCaseQuery)) ||
293 | (bookmark.url && bookmark.url.toLowerCase().includes(lowerCaseQuery)),
294 | );
295 | } else {
296 | // Has query keywords but no specified folder: use API search
297 | filteredBookmarks = await chrome.bookmarks.search({ query });
298 | // API search may return folders (if title matches), filter them out
299 | filteredBookmarks = filteredBookmarks.filter((item) => !!item.url);
300 | }
301 | } else {
302 | // No query keywords
303 | if (!targetFolderNode) {
304 | // No folder path specified, get all bookmarks
305 | const tree = await chrome.bookmarks.getTree();
306 | bookmarksToSearch = flattenBookmarkNodesToBookmarks(tree);
307 | }
308 | filteredBookmarks = bookmarksToSearch;
309 | }
310 |
311 | // Limit number of results
312 | const limitedResults = filteredBookmarks.slice(0, maxResults);
313 |
314 | // Add folder path information for each bookmark
315 | const resultsWithPath = await Promise.all(
316 | limitedResults.map(async (bookmark) => {
317 | const path = await getBookmarkFolderPath(bookmark.id);
318 | return {
319 | id: bookmark.id,
320 | title: bookmark.title,
321 | url: bookmark.url,
322 | dateAdded: bookmark.dateAdded,
323 | folderPath: path,
324 | };
325 | }),
326 | );
327 |
328 | return {
329 | content: [
330 | {
331 | type: 'text',
332 | text: JSON.stringify(
333 | {
334 | success: true,
335 | totalResults: resultsWithPath.length,
336 | query: query || null,
337 | folderSearched: targetFolderNode
338 | ? targetFolderNode.title || targetFolderNode.id
339 | : 'All bookmarks',
340 | bookmarks: resultsWithPath,
341 | },
342 | null,
343 | 2,
344 | ),
345 | },
346 | ],
347 | isError: false,
348 | };
349 | } catch (error) {
350 | console.error('Error searching bookmarks:', error);
351 | return createErrorResponse(
352 | `Error searching bookmarks: ${error instanceof Error ? error.message : String(error)}`,
353 | );
354 | }
355 | }
356 | }
357 |
358 | /**
359 | * Bookmark add tool
360 | * Used to add new bookmarks to Chrome browser
361 | */
362 | class BookmarkAddTool extends BaseBrowserToolExecutor {
363 | name = TOOL_NAMES.BROWSER.BOOKMARK_ADD;
364 |
365 | /**
366 | * Execute add bookmark operation
367 | */
368 | async execute(args: BookmarkAddToolParams): Promise<ToolResult> {
369 | const { url, title, parentId, createFolder = false } = args;
370 |
371 | console.log(`BookmarkAddTool: Adding bookmark, options:`, args);
372 |
373 | try {
374 | // If no URL provided, use current active tab
375 | let bookmarkUrl = url;
376 | let bookmarkTitle = title;
377 |
378 | if (!bookmarkUrl) {
379 | // Get current active tab
380 | const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
381 | if (!tabs[0] || !tabs[0].url) {
382 | // tab.url might be undefined (e.g., chrome:// pages)
383 | return createErrorResponse('No active tab with valid URL found, and no URL provided');
384 | }
385 |
386 | bookmarkUrl = tabs[0].url;
387 | if (!bookmarkTitle) {
388 | bookmarkTitle = tabs[0].title || bookmarkUrl; // If tab title is empty, use URL as title
389 | }
390 | }
391 |
392 | if (!bookmarkUrl) {
393 | // Should have been caught above, but as a safety measure
394 | return createErrorResponse('URL is required to create bookmark');
395 | }
396 |
397 | // Parse parentId (could be ID or path string)
398 | let actualParentId: string | undefined = undefined;
399 | if (parentId) {
400 | let folderNode = await findFolderByPathOrId(parentId);
401 |
402 | if (!folderNode && createFolder) {
403 | // If folder doesn't exist and creation is allowed, create folder path
404 | try {
405 | folderNode = await createFolderPath(parentId);
406 | } catch (createError) {
407 | return createErrorResponse(
408 | `Failed to create folder path: ${createError instanceof Error ? createError.message : String(createError)}`,
409 | );
410 | }
411 | }
412 |
413 | if (folderNode) {
414 | actualParentId = folderNode.id;
415 | } else {
416 | // Check if parentId might be a direct ID missed by findFolderByPathOrId (e.g., root folder '1')
417 | try {
418 | const nodes = await chrome.bookmarks.get(parentId);
419 | if (nodes && nodes.length > 0 && !nodes[0].url) {
420 | actualParentId = nodes[0].id;
421 | } else {
422 | return createErrorResponse(
423 | `Specified parent folder (ID/path: "${parentId}") not found or is not a folder${createFolder ? ', and creation failed' : '. You can set createFolder=true to auto-create folders'}`,
424 | );
425 | }
426 | } catch (e) {
427 | return createErrorResponse(
428 | `Specified parent folder (ID/path: "${parentId}") not found or invalid${createFolder ? ', and creation failed' : '. You can set createFolder=true to auto-create folders'}`,
429 | );
430 | }
431 | }
432 | } else {
433 | // If no parentId specified, default to "Bookmarks Bar"
434 | const rootChildren = await chrome.bookmarks.getChildren('0');
435 | const bookmarkBarFolder = rootChildren.find(
436 | (node) =>
437 | !node.url &&
438 | (node.title === getMessage('bookmarksBarLabel') ||
439 | node.title === 'Bookmarks bar' ||
440 | node.title === 'Bookmarks Bar'),
441 | );
442 | actualParentId = bookmarkBarFolder?.id || '1'; // fallback to default ID
443 | }
444 | // If actualParentId is still undefined, chrome.bookmarks.create will use default "Other Bookmarks", but we've set Bookmarks Bar
445 |
446 | // Create bookmark
447 | const newBookmark = await chrome.bookmarks.create({
448 | parentId: actualParentId, // If undefined, API uses default value
449 | title: bookmarkTitle || bookmarkUrl, // Ensure title is never empty
450 | url: bookmarkUrl,
451 | });
452 |
453 | // Get bookmark path
454 | const path = await getBookmarkFolderPath(newBookmark.id);
455 |
456 | return {
457 | content: [
458 | {
459 | type: 'text',
460 | text: JSON.stringify(
461 | {
462 | success: true,
463 | message: 'Bookmark added successfully',
464 | bookmark: {
465 | id: newBookmark.id,
466 | title: newBookmark.title,
467 | url: newBookmark.url,
468 | dateAdded: newBookmark.dateAdded,
469 | folderPath: path,
470 | },
471 | folderCreated: createFolder && parentId ? 'Folder created if necessary' : false,
472 | },
473 | null,
474 | 2,
475 | ),
476 | },
477 | ],
478 | isError: false,
479 | };
480 | } catch (error) {
481 | console.error('Error adding bookmark:', error);
482 | const errorMessage = error instanceof Error ? error.message : String(error);
483 |
484 | // Provide more specific error messages for common error cases, such as trying to bookmark chrome:// URLs
485 | if (errorMessage.includes("Can't bookmark URLs of type")) {
486 | return createErrorResponse(
487 | `Error adding bookmark: Cannot bookmark this type of URL (e.g., chrome:// system pages). ${errorMessage}`,
488 | );
489 | }
490 |
491 | return createErrorResponse(`Error adding bookmark: ${errorMessage}`);
492 | }
493 | }
494 | }
495 |
496 | /**
497 | * Bookmark delete tool
498 | * Used to delete bookmarks in Chrome browser
499 | */
500 | class BookmarkDeleteTool extends BaseBrowserToolExecutor {
501 | name = TOOL_NAMES.BROWSER.BOOKMARK_DELETE;
502 |
503 | /**
504 | * Execute delete bookmark operation
505 | */
506 | async execute(args: BookmarkDeleteToolParams): Promise<ToolResult> {
507 | const { bookmarkId, url, title } = args;
508 |
509 | console.log(`BookmarkDeleteTool: Deleting bookmark, options:`, args);
510 |
511 | if (!bookmarkId && !url) {
512 | return createErrorResponse('Must provide bookmark ID or URL to delete bookmark');
513 | }
514 |
515 | try {
516 | let bookmarksToDelete: chrome.bookmarks.BookmarkTreeNode[] = [];
517 |
518 | if (bookmarkId) {
519 | // Delete by ID
520 | try {
521 | const nodes = await chrome.bookmarks.get(bookmarkId);
522 | if (nodes && nodes.length > 0 && nodes[0].url) {
523 | bookmarksToDelete = nodes;
524 | } else {
525 | return createErrorResponse(
526 | `Bookmark with ID "${bookmarkId}" not found, or the ID does not correspond to a bookmark`,
527 | );
528 | }
529 | } catch (error) {
530 | return createErrorResponse(`Invalid bookmark ID: "${bookmarkId}"`);
531 | }
532 | } else if (url) {
533 | // Delete by URL
534 | bookmarksToDelete = await findBookmarksByUrl(url, title);
535 | if (bookmarksToDelete.length === 0) {
536 | return createErrorResponse(
537 | `No bookmark found with URL "${url}"${title ? ` (title contains: "${title}")` : ''}`,
538 | );
539 | }
540 | }
541 |
542 | // Delete found bookmarks
543 | const deletedBookmarks = [];
544 | const errors = [];
545 |
546 | for (const bookmark of bookmarksToDelete) {
547 | try {
548 | // Get path information before deletion
549 | const path = await getBookmarkFolderPath(bookmark.id);
550 |
551 | await chrome.bookmarks.remove(bookmark.id);
552 |
553 | deletedBookmarks.push({
554 | id: bookmark.id,
555 | title: bookmark.title,
556 | url: bookmark.url,
557 | folderPath: path,
558 | });
559 | } catch (error) {
560 | const errorMsg = error instanceof Error ? error.message : String(error);
561 | errors.push(
562 | `Failed to delete bookmark "${bookmark.title}" (ID: ${bookmark.id}): ${errorMsg}`,
563 | );
564 | }
565 | }
566 |
567 | if (deletedBookmarks.length === 0) {
568 | return createErrorResponse(`Failed to delete bookmarks: ${errors.join('; ')}`);
569 | }
570 |
571 | const result: any = {
572 | success: true,
573 | message: `Successfully deleted ${deletedBookmarks.length} bookmark(s)`,
574 | deletedBookmarks,
575 | };
576 |
577 | if (errors.length > 0) {
578 | result.partialSuccess = true;
579 | result.errors = errors;
580 | }
581 |
582 | return {
583 | content: [
584 | {
585 | type: 'text',
586 | text: JSON.stringify(result, null, 2),
587 | },
588 | ],
589 | isError: false,
590 | };
591 | } catch (error) {
592 | console.error('Error deleting bookmark:', error);
593 | return createErrorResponse(
594 | `Error deleting bookmark: ${error instanceof Error ? error.message : String(error)}`,
595 | );
596 | }
597 | }
598 | }
599 |
600 | export const bookmarkSearchTool = new BookmarkSearchTool();
601 | export const bookmarkAddTool = new BookmarkAddTool();
602 | export const bookmarkDeleteTool = new BookmarkDeleteTool();
603 |
```
--------------------------------------------------------------------------------
/app/chrome-extension/workers/ort-wasm-simd-threaded.mjs:
--------------------------------------------------------------------------------
```
1 | var ortWasmThreaded = (() => {
2 | var _scriptName = import.meta.url;
3 |
4 | return (
5 | async function(moduleArg = {}) {
6 | var moduleRtn;
7 |
8 | var f=moduleArg,aa,ba,ca=new Promise((a,b)=>{aa=a;ba=b}),da="object"==typeof window,k="undefined"!=typeof WorkerGlobalScope,l="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node&&"renderer"!=process.type,m=k&&self.name?.startsWith("em-pthread");if(l){const {createRequire:a}=await import("module");var require=a(import.meta.url),n=require("worker_threads");global.Worker=n.Worker;m=(k=!n.jb)&&"em-pthread"==n.workerData}
9 | f.mountExternalData=(a,b)=>{a.startsWith("./")&&(a=a.substring(2));(f.Sa||(f.Sa=new Map)).set(a,b)};f.unmountExternalData=()=>{delete f.Sa};var SharedArrayBuffer=globalThis.SharedArrayBuffer??(new WebAssembly.Memory({initial:0,maximum:0,lb:!0})).buffer.constructor,ea=Object.assign({},f),fa="./this.program",q=(a,b)=>{throw b;},r="",ha,t;
10 | if(l){var fs=require("fs"),ia=require("path");import.meta.url.startsWith("data:")||(r=ia.dirname(require("url").fileURLToPath(import.meta.url))+"/");t=a=>{a=u(a)?new URL(a):a;return fs.readFileSync(a)};ha=async a=>{a=u(a)?new URL(a):a;return fs.readFileSync(a,void 0)};!f.thisProgram&&1<process.argv.length&&(fa=process.argv[1].replace(/\\/g,"/"));process.argv.slice(2);q=(a,b)=>{process.exitCode=a;throw b;}}else if(da||k)k?r=self.location.href:"undefined"!=typeof document&&document.currentScript&&
11 | (r=document.currentScript.src),_scriptName&&(r=_scriptName),r.startsWith("blob:")?r="":r=r.slice(0,r.replace(/[?#].*/,"").lastIndexOf("/")+1),l||(k&&(t=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),ha=async a=>{if(u(a))return new Promise((c,d)=>{var e=new XMLHttpRequest;e.open("GET",a,!0);e.responseType="arraybuffer";e.onload=()=>{200==e.status||0==e.status&&e.response?c(e.response):d(e.status)};e.onerror=d;e.send(null)});
12 | var b=await fetch(a,{credentials:"same-origin"});if(b.ok)return b.arrayBuffer();throw Error(b.status+" : "+b.url);});var ja=console.log.bind(console),ka=console.error.bind(console);l&&(ja=(...a)=>fs.writeSync(1,a.join(" ")+"\n"),ka=(...a)=>fs.writeSync(2,a.join(" ")+"\n"));var la=ja,w=ka;Object.assign(f,ea);ea=null;var x=f.wasmBinary,y,ma,z=!1,A,B,na,oa,pa,qa,ra,C,sa,u=a=>a.startsWith("file://");function D(){y.buffer!=B.buffer&&E();return B}function F(){y.buffer!=B.buffer&&E();return na}
13 | function ta(){y.buffer!=B.buffer&&E();return oa}function G(){y.buffer!=B.buffer&&E();return pa}function H(){y.buffer!=B.buffer&&E();return qa}function va(){y.buffer!=B.buffer&&E();return ra}function I(){y.buffer!=B.buffer&&E();return sa}
14 | if(m){var wa;if(l){var xa=n.parentPort;xa.on("message",b=>onmessage({data:b}));Object.assign(globalThis,{self:global,postMessage:b=>xa.postMessage(b)})}var ya=!1;w=function(...b){b=b.join(" ");l?fs.writeSync(2,b+"\n"):console.error(b)};self.alert=function(...b){postMessage({Ra:"alert",text:b.join(" "),eb:J()})};self.onunhandledrejection=b=>{throw b.reason||b;};function a(b){try{var c=b.data,d=c.Ra;if("load"===d){let e=[];self.onmessage=g=>e.push(g);self.startWorker=()=>{postMessage({Ra:"loaded"});
15 | for(let g of e)a(g);self.onmessage=a};for(const g of c.Za)if(!f[g]||f[g].proxy)f[g]=(...h)=>{postMessage({Ra:"callHandler",Ya:g,args:h})},"print"==g&&(la=f[g]),"printErr"==g&&(w=f[g]);y=c.gb;E();wa(c.hb)}else if("run"===d){za(c.Qa);Aa(c.Qa,0,0,1,0,0);Ba();Ca(c.Qa);ya||=!0;try{Da(c.bb,c.Va)}catch(e){if("unwind"!=e)throw e;}}else"setimmediate"!==c.target&&("checkMailbox"===d?ya&&K():d&&(w(`worker: received unknown command ${d}`),w(c)))}catch(e){throw Ea(),e;}}self.onmessage=a}
16 | function E(){var a=y.buffer;f.HEAP8=B=new Int8Array(a);f.HEAP16=oa=new Int16Array(a);f.HEAPU8=na=new Uint8Array(a);f.HEAPU16=new Uint16Array(a);f.HEAP32=pa=new Int32Array(a);f.HEAPU32=qa=new Uint32Array(a);f.HEAPF32=ra=new Float32Array(a);f.HEAPF64=sa=new Float64Array(a);f.HEAP64=C=new BigInt64Array(a);f.HEAPU64=new BigUint64Array(a)}m||(y=new WebAssembly.Memory({initial:256,maximum:65536,shared:!0}),E());function Fa(){m?startWorker(f):L.$()}var M=0,N=null;
17 | function Ga(){M--;if(0==M&&N){var a=N;N=null;a()}}function O(a){a="Aborted("+a+")";w(a);z=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");ba(a);throw a;}var Ha;async function Ia(a){if(!x)try{var b=await ha(a);return new Uint8Array(b)}catch{}if(a==Ha&&x)a=new Uint8Array(x);else if(t)a=t(a);else throw"both async and sync fetching of the wasm failed";return a}
18 | async function Ja(a,b){try{var c=await Ia(a);return await WebAssembly.instantiate(c,b)}catch(d){w(`failed to asynchronously prepare wasm: ${d}`),O(d)}}async function Ka(a){var b=Ha;if(!x&&"function"==typeof WebAssembly.instantiateStreaming&&!u(b)&&!l)try{var c=fetch(b,{credentials:"same-origin"});return await WebAssembly.instantiateStreaming(c,a)}catch(d){w(`wasm streaming compile failed: ${d}`),w("falling back to ArrayBuffer instantiation")}return Ja(b,a)}
19 | function La(){Ma={j:Na,b:Oa,E:Pa,f:Qa,U:Ra,A:Sa,C:Ta,V:Ua,S:Va,L:Wa,R:Xa,n:Ya,B:Za,y:$a,T:ab,z:bb,_:cb,O:db,w:eb,F:fb,t:gb,i:hb,N:Ca,X:ib,I:jb,J:kb,K:lb,G:mb,H:nb,u:ob,q:pb,Z:qb,o:rb,k:sb,Y:tb,d:ub,W:vb,x:wb,c:xb,e:yb,h:zb,v:Ab,s:Bb,r:Cb,P:Db,Q:Eb,D:Fb,g:Gb,m:Hb,M:Ib,l:Jb,a:y,p:Kb};return{a:Ma}}
20 | var Mb={802156:(a,b,c,d,e)=>{if("undefined"==typeof f||!f.Sa)return 1;a=Lb(Number(a>>>0));a.startsWith("./")&&(a=a.substring(2));a=f.Sa.get(a);if(!a)return 2;b=Number(b>>>0);c=Number(c>>>0);d=Number(d>>>0);if(b+c>a.byteLength)return 3;try{const g=a.subarray(b,b+c);switch(e){case 0:F().set(g,d>>>0);break;case 1:f.ib?f.ib(d,g):f.kb(d,g);break;default:return 4}return 0}catch{return 4}},802980:()=>"undefined"!==typeof wasmOffsetConverter};function Na(){return"undefined"!==typeof wasmOffsetConverter}
21 | class Nb{name="ExitStatus";constructor(a){this.message=`Program terminated with exit(${a})`;this.status=a}}
22 | var Ob=a=>{a.terminate();a.onmessage=()=>{}},Pb=[],Sb=a=>{0==Q.length&&(Qb(),Rb(Q[0]));var b=Q.pop();if(!b)return 6;R.push(b);S[a.Qa]=b;b.Qa=a.Qa;var c={Ra:"run",bb:a.ab,Va:a.Va,Qa:a.Qa};l&&b.unref();b.postMessage(c,a.Xa);return 0},T=0,V=(a,b,...c)=>{for(var d=2*c.length,e=Tb(),g=Ub(8*d),h=g>>>3,p=0;p<c.length;p++){var v=c[p];"bigint"==typeof v?(C[h+2*p]=1n,C[h+2*p+1]=v):(C[h+2*p]=0n,I()[h+2*p+1>>>0]=v)}a=Vb(a,0,d,g,b);U(e);return a};
23 | function Kb(a){if(m)return V(0,1,a);A=a;if(!(0<T)){for(var b of R)Ob(b);for(b of Q)Ob(b);Q=[];R=[];S={};z=!0}q(a,new Nb(a))}function Wb(a){if(m)return V(1,0,a);Fb(a)}var Fb=a=>{A=a;if(m)throw Wb(a),"unwind";Kb(a)},Q=[],R=[],Xb=[],S={};function Yb(){for(var a=f.numThreads-1;a--;)Qb();Pb.unshift(()=>{M++;Zb(()=>Ga())})}var ac=a=>{var b=a.Qa;delete S[b];Q.push(a);R.splice(R.indexOf(a),1);a.Qa=0;$b(b)};function Ba(){Xb.forEach(a=>a())}
24 | var Rb=a=>new Promise(b=>{a.onmessage=g=>{g=g.data;var h=g.Ra;if(g.Ta&&g.Ta!=J()){var p=S[g.Ta];p?p.postMessage(g,g.Xa):w(`Internal error! Worker sent a message "${h}" to target pthread ${g.Ta}, but that thread no longer exists!`)}else if("checkMailbox"===h)K();else if("spawnThread"===h)Sb(g);else if("cleanupThread"===h)ac(S[g.cb]);else if("loaded"===h)a.loaded=!0,l&&!a.Qa&&a.unref(),b(a);else if("alert"===h)alert(`Thread ${g.eb}: ${g.text}`);else if("setimmediate"===g.target)a.postMessage(g);else if("callHandler"===
25 | h)f[g.Ya](...g.args);else h&&w(`worker sent an unknown command ${h}`)};a.onerror=g=>{w(`${"worker sent an error!"} ${g.filename}:${g.lineno}: ${g.message}`);throw g;};l&&(a.on("message",g=>a.onmessage({data:g})),a.on("error",g=>a.onerror(g)));var c=[],d=[],e;for(e of d)f.propertyIsEnumerable(e)&&c.push(e);a.postMessage({Ra:"load",Za:c,gb:y,hb:ma})});function Zb(a){m?a():Promise.all(Q.map(Rb)).then(a)}
26 | function Qb(){var a=new Worker(new URL(import.meta.url),{type:"module",workerData:"em-pthread",name:"em-pthread"});Q.push(a)}var za=a=>{E();var b=H()[a+52>>>2>>>0];a=H()[a+56>>>2>>>0];bc(b,b-a);U(b)},W=[],cc,Da=(a,b)=>{T=0;var c=W[a];c||(a>=W.length&&(W.length=a+1),W[a]=c=cc.get(a));a=c(b);0<T?A=a:dc(a)};class ec{constructor(a){this.Ua=a-24}}var fc=0,gc=0;
27 | function Oa(a,b,c){a>>>=0;var d=new ec(a);b>>>=0;c>>>=0;H()[d.Ua+16>>>2>>>0]=0;H()[d.Ua+4>>>2>>>0]=b;H()[d.Ua+8>>>2>>>0]=c;fc=a;gc++;throw fc;}function hc(a,b,c,d){return m?V(2,1,a,b,c,d):Pa(a,b,c,d)}function Pa(a,b,c,d){a>>>=0;b>>>=0;c>>>=0;d>>>=0;if("undefined"==typeof SharedArrayBuffer)return 6;var e=[];if(m&&0===e.length)return hc(a,b,c,d);a={ab:c,Qa:a,Va:d,Xa:e};return m?(a.Ra="spawnThread",postMessage(a,e),0):Sb(a)}
28 | var ic="undefined"!=typeof TextDecoder?new TextDecoder:void 0,jc=(a,b=0,c=NaN)=>{b>>>=0;var d=b+c;for(c=b;a[c]&&!(c>=d);)++c;if(16<c-b&&a.buffer&&ic)return ic.decode(a.buffer instanceof ArrayBuffer?a.subarray(b,c):a.slice(b,c));for(d="";b<c;){var e=a[b++];if(e&128){var g=a[b++]&63;if(192==(e&224))d+=String.fromCharCode((e&31)<<6|g);else{var h=a[b++]&63;e=224==(e&240)?(e&15)<<12|g<<6|h:(e&7)<<18|g<<12|h<<6|a[b++]&63;65536>e?d+=String.fromCharCode(e):(e-=65536,d+=String.fromCharCode(55296|e>>10,56320|
29 | e&1023))}}else d+=String.fromCharCode(e)}return d},Lb=(a,b)=>(a>>>=0)?jc(F(),a,b):"";function Qa(a,b,c){return m?V(3,1,a,b,c):0}function Ra(a,b){if(m)return V(4,1,a,b)}
30 | var X=(a,b,c)=>{var d=F();b>>>=0;if(0<c){var e=b;c=b+c-1;for(var g=0;g<a.length;++g){var h=a.charCodeAt(g);if(55296<=h&&57343>=h){var p=a.charCodeAt(++g);h=65536+((h&1023)<<10)|p&1023}if(127>=h){if(b>=c)break;d[b++>>>0]=h}else{if(2047>=h){if(b+1>=c)break;d[b++>>>0]=192|h>>6}else{if(65535>=h){if(b+2>=c)break;d[b++>>>0]=224|h>>12}else{if(b+3>=c)break;d[b++>>>0]=240|h>>18;d[b++>>>0]=128|h>>12&63}d[b++>>>0]=128|h>>6&63}d[b++>>>0]=128|h&63}}d[b>>>0]=0;a=b-e}else a=0;return a};
31 | function Sa(a,b){if(m)return V(5,1,a,b)}function Ta(a,b,c){if(m)return V(6,1,a,b,c)}function Ua(a,b,c){return m?V(7,1,a,b,c):0}function Va(a,b){if(m)return V(8,1,a,b)}function Wa(a,b,c){if(m)return V(9,1,a,b,c)}function Xa(a,b,c,d){if(m)return V(10,1,a,b,c,d)}function Ya(a,b,c,d){if(m)return V(11,1,a,b,c,d)}function Za(a,b,c,d){if(m)return V(12,1,a,b,c,d)}function $a(a){if(m)return V(13,1,a)}function ab(a,b){if(m)return V(14,1,a,b)}function bb(a,b,c){if(m)return V(15,1,a,b,c)}var cb=()=>O("");
32 | function db(a){Aa(a>>>0,!k,1,!da,131072,!1);Ba()}var kc=a=>{if(!z)try{if(a(),!(0<T))try{m?dc(A):Fb(A)}catch(b){b instanceof Nb||"unwind"==b||q(1,b)}}catch(b){b instanceof Nb||"unwind"==b||q(1,b)}};function Ca(a){a>>>=0;"function"===typeof Atomics.fb&&(Atomics.fb(G(),a>>>2,a).value.then(K),a+=128,Atomics.store(G(),a>>>2,1))}var K=()=>{var a=J();a&&(Ca(a),kc(lc))};function eb(a,b){a>>>=0;a==b>>>0?setTimeout(K):m?postMessage({Ta:a,Ra:"checkMailbox"}):(a=S[a])&&a.postMessage({Ra:"checkMailbox"})}
33 | var mc=[];function fb(a,b,c,d,e){b>>>=0;d/=2;mc.length=d;c=e>>>0>>>3;for(e=0;e<d;e++)mc[e]=C[c+2*e]?C[c+2*e+1]:I()[c+2*e+1>>>0];return(b?Mb[b]:nc[a])(...mc)}var gb=()=>{T=0};function hb(a){a>>>=0;m?postMessage({Ra:"cleanupThread",cb:a}):ac(S[a])}function ib(a){l&&S[a>>>0].ref()}
34 | function jb(a,b){a=-9007199254740992>a||9007199254740992<a?NaN:Number(a);b>>>=0;a=new Date(1E3*a);G()[b>>>2>>>0]=a.getUTCSeconds();G()[b+4>>>2>>>0]=a.getUTCMinutes();G()[b+8>>>2>>>0]=a.getUTCHours();G()[b+12>>>2>>>0]=a.getUTCDate();G()[b+16>>>2>>>0]=a.getUTCMonth();G()[b+20>>>2>>>0]=a.getUTCFullYear()-1900;G()[b+24>>>2>>>0]=a.getUTCDay();a=(a.getTime()-Date.UTC(a.getUTCFullYear(),0,1,0,0,0,0))/864E5|0;G()[b+28>>>2>>>0]=a}
35 | var oc=a=>0===a%4&&(0!==a%100||0===a%400),pc=[0,31,60,91,121,152,182,213,244,274,305,335],qc=[0,31,59,90,120,151,181,212,243,273,304,334];
36 | function kb(a,b){a=-9007199254740992>a||9007199254740992<a?NaN:Number(a);b>>>=0;a=new Date(1E3*a);G()[b>>>2>>>0]=a.getSeconds();G()[b+4>>>2>>>0]=a.getMinutes();G()[b+8>>>2>>>0]=a.getHours();G()[b+12>>>2>>>0]=a.getDate();G()[b+16>>>2>>>0]=a.getMonth();G()[b+20>>>2>>>0]=a.getFullYear()-1900;G()[b+24>>>2>>>0]=a.getDay();var c=(oc(a.getFullYear())?pc:qc)[a.getMonth()]+a.getDate()-1|0;G()[b+28>>>2>>>0]=c;G()[b+36>>>2>>>0]=-(60*a.getTimezoneOffset());c=(new Date(a.getFullYear(),6,1)).getTimezoneOffset();
37 | var d=(new Date(a.getFullYear(),0,1)).getTimezoneOffset();a=(c!=d&&a.getTimezoneOffset()==Math.min(d,c))|0;G()[b+32>>>2>>>0]=a}
38 | function lb(a){a>>>=0;var b=new Date(G()[a+20>>>2>>>0]+1900,G()[a+16>>>2>>>0],G()[a+12>>>2>>>0],G()[a+8>>>2>>>0],G()[a+4>>>2>>>0],G()[a>>>2>>>0],0),c=G()[a+32>>>2>>>0],d=b.getTimezoneOffset(),e=(new Date(b.getFullYear(),6,1)).getTimezoneOffset(),g=(new Date(b.getFullYear(),0,1)).getTimezoneOffset(),h=Math.min(g,e);0>c?G()[a+32>>>2>>>0]=Number(e!=g&&h==d):0<c!=(h==d)&&(e=Math.max(g,e),b.setTime(b.getTime()+6E4*((0<c?h:e)-d)));G()[a+24>>>2>>>0]=b.getDay();c=(oc(b.getFullYear())?pc:qc)[b.getMonth()]+
39 | b.getDate()-1|0;G()[a+28>>>2>>>0]=c;G()[a>>>2>>>0]=b.getSeconds();G()[a+4>>>2>>>0]=b.getMinutes();G()[a+8>>>2>>>0]=b.getHours();G()[a+12>>>2>>>0]=b.getDate();G()[a+16>>>2>>>0]=b.getMonth();G()[a+20>>>2>>>0]=b.getYear();a=b.getTime();return BigInt(isNaN(a)?-1:a/1E3)}function mb(a,b,c,d,e,g,h){return m?V(16,1,a,b,c,d,e,g,h):-52}function nb(a,b,c,d,e,g){if(m)return V(17,1,a,b,c,d,e,g)}var Y={},xb=()=>performance.timeOrigin+performance.now();
40 | function ob(a,b){if(m)return V(18,1,a,b);Y[a]&&(clearTimeout(Y[a].id),delete Y[a]);if(!b)return 0;var c=setTimeout(()=>{delete Y[a];kc(()=>rc(a,performance.timeOrigin+performance.now()))},b);Y[a]={id:c,mb:b};return 0}
41 | function pb(a,b,c,d){a>>>=0;b>>>=0;c>>>=0;d>>>=0;var e=(new Date).getFullYear(),g=(new Date(e,0,1)).getTimezoneOffset();e=(new Date(e,6,1)).getTimezoneOffset();var h=Math.max(g,e);H()[a>>>2>>>0]=60*h;G()[b>>>2>>>0]=Number(g!=e);b=p=>{var v=Math.abs(p);return`UTC${0<=p?"-":"+"}${String(Math.floor(v/60)).padStart(2,"0")}${String(v%60).padStart(2,"0")}`};a=b(g);b=b(e);e<g?(X(a,c,17),X(b,d,17)):(X(a,d,17),X(b,c,17))}var tb=()=>Date.now(),sc=1;
42 | function qb(a,b,c){if(!(0<=a&&3>=a))return 28;if(0===a)a=Date.now();else if(sc)a=performance.timeOrigin+performance.now();else return 52;C[c>>>0>>>3]=BigInt(Math.round(1E6*a));return 0}var tc=[];function rb(a,b,c){a>>>=0;b>>>=0;c>>>=0;tc.length=0;for(var d;d=F()[b++>>>0];){var e=105!=d;e&=112!=d;c+=e&&c%8?4:0;tc.push(112==d?H()[c>>>2>>>0]:106==d?C[c>>>3]:105==d?G()[c>>>2>>>0]:I()[c>>>3>>>0]);c+=e?8:4}return Mb[a](...tc)}var sb=()=>{};function ub(a,b){return w(Lb(a>>>0,b>>>0))}
43 | var vb=()=>{T+=1;throw"unwind";};function wb(){return 4294901760}var yb=()=>l?require("os").cpus().length:navigator.hardwareConcurrency;function zb(){O("Cannot use emscripten_pc_get_function without -sUSE_OFFSET_CONVERTER");return 0}
44 | function Ab(a){a>>>=0;var b=F().length;if(a<=b||4294901760<a)return!1;for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);a:{d=(Math.min(4294901760,65536*Math.ceil(Math.max(a,d)/65536))-y.buffer.byteLength+65535)/65536|0;try{y.grow(d);E();var e=1;break a}catch(g){}e=void 0}if(e)return!0}return!1}var uc=()=>{O("Cannot use convertFrameToPC (needed by __builtin_return_address) without -sUSE_OFFSET_CONVERTER");return 0},Z={},vc=a=>{a.forEach(b=>{var c=uc();c&&(Z[c]=b)})};
45 | function Bb(){var a=Error().stack.toString().split("\n");"Error"==a[0]&&a.shift();vc(a);Z.Wa=uc();Z.$a=a;return Z.Wa}function Cb(a,b,c){a>>>=0;b>>>=0;if(Z.Wa==a)var d=Z.$a;else d=Error().stack.toString().split("\n"),"Error"==d[0]&&d.shift(),vc(d);for(var e=3;d[e]&&uc()!=a;)++e;for(a=0;a<c&&d[a+e];++a)G()[b+4*a>>>2>>>0]=uc();return a}
46 | var wc={},yc=()=>{if(!xc){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:fa||"./this.program"},b;for(b in wc)void 0===wc[b]?delete a[b]:a[b]=wc[b];var c=[];for(b in a)c.push(`${b}=${a[b]}`);xc=c}return xc},xc;
47 | function Db(a,b){if(m)return V(19,1,a,b);a>>>=0;b>>>=0;var c=0;yc().forEach((d,e)=>{var g=b+c;e=H()[a+4*e>>>2>>>0]=g;for(g=0;g<d.length;++g)D()[e++>>>0]=d.charCodeAt(g);D()[e>>>0]=0;c+=d.length+1});return 0}function Eb(a,b){if(m)return V(20,1,a,b);a>>>=0;b>>>=0;var c=yc();H()[a>>>2>>>0]=c.length;var d=0;c.forEach(e=>d+=e.length+1);H()[b>>>2>>>0]=d;return 0}function Gb(a){return m?V(21,1,a):52}function Hb(a,b,c,d){return m?V(22,1,a,b,c,d):52}function Ib(a,b,c,d){return m?V(23,1,a,b,c,d):70}
48 | var zc=[null,[],[]];function Jb(a,b,c,d){if(m)return V(24,1,a,b,c,d);b>>>=0;c>>>=0;d>>>=0;for(var e=0,g=0;g<c;g++){var h=H()[b>>>2>>>0],p=H()[b+4>>>2>>>0];b+=8;for(var v=0;v<p;v++){var P=F()[h+v>>>0],ua=zc[a];0===P||10===P?((1===a?la:w)(jc(ua)),ua.length=0):ua.push(P)}e+=p}H()[d>>>2>>>0]=e;return 0}m||Yb();var nc=[Kb,Wb,hc,Qa,Ra,Sa,Ta,Ua,Va,Wa,Xa,Ya,Za,$a,ab,bb,mb,nb,ob,Db,Eb,Gb,Hb,Ib,Jb],Ma,L;
49 | (async function(){function a(d,e){L=d.exports;L=Ac();Xb.push(L.Da);cc=L.Ea;ma=e;Ga();return L}M++;var b=La();if(f.instantiateWasm)return new Promise(d=>{f.instantiateWasm(b,(e,g)=>{a(e,g);d(e.exports)})});if(m)return new Promise(d=>{wa=e=>{var g=new WebAssembly.Instance(e,La());d(a(g,e))}});Ha??=f.locateFile?f.locateFile?f.locateFile("ort-wasm-simd-threaded.wasm",r):r+"ort-wasm-simd-threaded.wasm":(new URL("ort-wasm-simd-threaded.wasm",import.meta.url)).href;try{var c=await Ka(b);return a(c.instance,
50 | c.module)}catch(d){return ba(d),Promise.reject(d)}})();f._OrtInit=(a,b)=>(f._OrtInit=L.aa)(a,b);f._OrtGetLastError=(a,b)=>(f._OrtGetLastError=L.ba)(a,b);f._OrtCreateSessionOptions=(a,b,c,d,e,g,h,p,v,P)=>(f._OrtCreateSessionOptions=L.ca)(a,b,c,d,e,g,h,p,v,P);f._OrtAppendExecutionProvider=(a,b,c,d,e)=>(f._OrtAppendExecutionProvider=L.da)(a,b,c,d,e);f._OrtAddFreeDimensionOverride=(a,b,c)=>(f._OrtAddFreeDimensionOverride=L.ea)(a,b,c);
51 | f._OrtAddSessionConfigEntry=(a,b,c)=>(f._OrtAddSessionConfigEntry=L.fa)(a,b,c);f._OrtReleaseSessionOptions=a=>(f._OrtReleaseSessionOptions=L.ga)(a);f._OrtCreateSession=(a,b,c)=>(f._OrtCreateSession=L.ha)(a,b,c);f._OrtReleaseSession=a=>(f._OrtReleaseSession=L.ia)(a);f._OrtGetInputOutputCount=(a,b,c)=>(f._OrtGetInputOutputCount=L.ja)(a,b,c);f._OrtGetInputOutputMetadata=(a,b,c,d)=>(f._OrtGetInputOutputMetadata=L.ka)(a,b,c,d);f._OrtFree=a=>(f._OrtFree=L.la)(a);
52 | f._OrtCreateTensor=(a,b,c,d,e,g)=>(f._OrtCreateTensor=L.ma)(a,b,c,d,e,g);f._OrtGetTensorData=(a,b,c,d,e)=>(f._OrtGetTensorData=L.na)(a,b,c,d,e);f._OrtReleaseTensor=a=>(f._OrtReleaseTensor=L.oa)(a);f._OrtCreateRunOptions=(a,b,c,d)=>(f._OrtCreateRunOptions=L.pa)(a,b,c,d);f._OrtAddRunConfigEntry=(a,b,c)=>(f._OrtAddRunConfigEntry=L.qa)(a,b,c);f._OrtReleaseRunOptions=a=>(f._OrtReleaseRunOptions=L.ra)(a);f._OrtCreateBinding=a=>(f._OrtCreateBinding=L.sa)(a);
53 | f._OrtBindInput=(a,b,c)=>(f._OrtBindInput=L.ta)(a,b,c);f._OrtBindOutput=(a,b,c,d)=>(f._OrtBindOutput=L.ua)(a,b,c,d);f._OrtClearBoundOutputs=a=>(f._OrtClearBoundOutputs=L.va)(a);f._OrtReleaseBinding=a=>(f._OrtReleaseBinding=L.wa)(a);f._OrtRunWithBinding=(a,b,c,d,e)=>(f._OrtRunWithBinding=L.xa)(a,b,c,d,e);f._OrtRun=(a,b,c,d,e,g,h,p)=>(f._OrtRun=L.ya)(a,b,c,d,e,g,h,p);f._OrtEndProfiling=a=>(f._OrtEndProfiling=L.za)(a);var J=()=>(J=L.Aa)();f._free=a=>(f._free=L.Ba)(a);f._malloc=a=>(f._malloc=L.Ca)(a);
54 | var Aa=(a,b,c,d,e,g)=>(Aa=L.Fa)(a,b,c,d,e,g),Ea=()=>(Ea=L.Ga)(),Vb=(a,b,c,d,e)=>(Vb=L.Ha)(a,b,c,d,e),$b=a=>($b=L.Ia)(a),dc=a=>(dc=L.Ja)(a),rc=(a,b)=>(rc=L.Ka)(a,b),lc=()=>(lc=L.La)(),bc=(a,b)=>(bc=L.Ma)(a,b),U=a=>(U=L.Na)(a),Ub=a=>(Ub=L.Oa)(a),Tb=()=>(Tb=L.Pa)();function Ac(){var a=L;a=Object.assign({},a);var b=d=>()=>d()>>>0,c=d=>e=>d(e)>>>0;a.Aa=b(a.Aa);a.Ca=c(a.Ca);a.Oa=c(a.Oa);a.Pa=b(a.Pa);a.__cxa_get_exception_ptr=c(a.__cxa_get_exception_ptr);return a}f.stackSave=()=>Tb();f.stackRestore=a=>U(a);
55 | f.stackAlloc=a=>Ub(a);f.setValue=function(a,b,c="i8"){c.endsWith("*")&&(c="*");switch(c){case "i1":D()[a>>>0]=b;break;case "i8":D()[a>>>0]=b;break;case "i16":ta()[a>>>1>>>0]=b;break;case "i32":G()[a>>>2>>>0]=b;break;case "i64":C[a>>>3]=BigInt(b);break;case "float":va()[a>>>2>>>0]=b;break;case "double":I()[a>>>3>>>0]=b;break;case "*":H()[a>>>2>>>0]=b;break;default:O(`invalid type for setValue: ${c}`)}};
56 | f.getValue=function(a,b="i8"){b.endsWith("*")&&(b="*");switch(b){case "i1":return D()[a>>>0];case "i8":return D()[a>>>0];case "i16":return ta()[a>>>1>>>0];case "i32":return G()[a>>>2>>>0];case "i64":return C[a>>>3];case "float":return va()[a>>>2>>>0];case "double":return I()[a>>>3>>>0];case "*":return H()[a>>>2>>>0];default:O(`invalid type for getValue: ${b}`)}};f.UTF8ToString=Lb;f.stringToUTF8=X;
57 | f.lengthBytesUTF8=a=>{for(var b=0,c=0;c<a.length;++c){var d=a.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=d?(b+=4,++c):b+=3}return b};function Bc(){if(0<M)N=Bc;else if(m)aa(f),Fa();else{for(;0<Pb.length;)Pb.shift()(f);0<M?N=Bc:(f.calledRun=!0,z||(Fa(),aa(f)))}}Bc();f.PTR_SIZE=4;moduleRtn=ca;
58 |
59 |
60 | return moduleRtn;
61 | }
62 | );
63 | })();
64 | export default ortWasmThreaded;
65 | var isPthread = globalThis.self?.name?.startsWith('em-pthread');
66 | var isNode = typeof globalThis.process?.versions?.node == 'string';
67 | if (isNode) isPthread = (await import('worker_threads')).workerData === 'em-pthread';
68 |
69 | // When running as a pthread, construct a new instance on startup
70 | isPthread && ortWasmThreaded();
71 |
```