#
tokens: 40995/50000 7/120 files (page 4/10)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 4/10FirstPrevNextLast