#
tokens: 47555/50000 18/120 files (page 2/10)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 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/native-server/src/file-handler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as fs from 'fs';
  2 | import * as path from 'path';
  3 | import * as os from 'os';
  4 | import * as crypto from 'crypto';
  5 | import fetch from 'node-fetch';
  6 | 
  7 | /**
  8 |  * File handler for managing file uploads through the native messaging host
  9 |  */
 10 | export class FileHandler {
 11 |   private tempDir: string;
 12 | 
 13 |   constructor() {
 14 |     // Create a temp directory for file operations
 15 |     this.tempDir = path.join(os.tmpdir(), 'chrome-mcp-uploads');
 16 |     if (!fs.existsSync(this.tempDir)) {
 17 |       fs.mkdirSync(this.tempDir, { recursive: true });
 18 |     }
 19 |   }
 20 | 
 21 |   /**
 22 |    * Handle file preparation request from the extension
 23 |    */
 24 |   async handleFileRequest(request: any): Promise<any> {
 25 |     const { action, fileUrl, base64Data, fileName, filePath } = request;
 26 | 
 27 |     try {
 28 |       switch (action) {
 29 |         case 'prepareFile':
 30 |           if (fileUrl) {
 31 |             return await this.downloadFile(fileUrl, fileName);
 32 |           } else if (base64Data) {
 33 |             return await this.saveBase64File(base64Data, fileName);
 34 |           } else if (filePath) {
 35 |             return await this.verifyFile(filePath);
 36 |           }
 37 |           break;
 38 | 
 39 |         case 'cleanupFile':
 40 |           return await this.cleanupFile(filePath);
 41 | 
 42 |         default:
 43 |           return {
 44 |             success: false,
 45 |             error: `Unknown file action: ${action}`,
 46 |           };
 47 |       }
 48 |     } catch (error) {
 49 |       return {
 50 |         success: false,
 51 |         error: error instanceof Error ? error.message : String(error),
 52 |       };
 53 |     }
 54 |   }
 55 | 
 56 |   /**
 57 |    * Download a file from URL and save to temp directory
 58 |    */
 59 |   private async downloadFile(fileUrl: string, fileName?: string): Promise<any> {
 60 |     try {
 61 |       const response = await fetch(fileUrl);
 62 |       if (!response.ok) {
 63 |         throw new Error(`Failed to download file: ${response.statusText}`);
 64 |       }
 65 | 
 66 |       // Generate filename if not provided
 67 |       const finalFileName = fileName || this.generateFileName(fileUrl);
 68 |       const filePath = path.join(this.tempDir, finalFileName);
 69 | 
 70 |       // Get the file buffer
 71 |       const buffer = await response.buffer();
 72 | 
 73 |       // Save to file
 74 |       fs.writeFileSync(filePath, buffer);
 75 | 
 76 |       return {
 77 |         success: true,
 78 |         filePath: filePath,
 79 |         fileName: finalFileName,
 80 |         size: buffer.length,
 81 |       };
 82 |     } catch (error) {
 83 |       throw new Error(`Failed to download file from URL: ${error}`);
 84 |     }
 85 |   }
 86 | 
 87 |   /**
 88 |    * Save base64 data as a file
 89 |    */
 90 |   private async saveBase64File(base64Data: string, fileName?: string): Promise<any> {
 91 |     try {
 92 |       // Remove data URL prefix if present
 93 |       const base64Content = base64Data.replace(/^data:.*?;base64,/, '');
 94 |       
 95 |       // Convert base64 to buffer
 96 |       const buffer = Buffer.from(base64Content, 'base64');
 97 | 
 98 |       // Generate filename if not provided
 99 |       const finalFileName = fileName || `upload-${Date.now()}.bin`;
100 |       const filePath = path.join(this.tempDir, finalFileName);
101 | 
102 |       // Save to file
103 |       fs.writeFileSync(filePath, buffer);
104 | 
105 |       return {
106 |         success: true,
107 |         filePath: filePath,
108 |         fileName: finalFileName,
109 |         size: buffer.length,
110 |       };
111 |     } catch (error) {
112 |       throw new Error(`Failed to save base64 file: ${error}`);
113 |     }
114 |   }
115 | 
116 |   /**
117 |    * Verify that a file exists and is accessible
118 |    */
119 |   private async verifyFile(filePath: string): Promise<any> {
120 |     try {
121 |       // Check if file exists
122 |       if (!fs.existsSync(filePath)) {
123 |         throw new Error(`File does not exist: ${filePath}`);
124 |       }
125 | 
126 |       // Get file stats
127 |       const stats = fs.statSync(filePath);
128 | 
129 |       // Check if it's actually a file
130 |       if (!stats.isFile()) {
131 |         throw new Error(`Path is not a file: ${filePath}`);
132 |       }
133 | 
134 |       // Check if file is readable
135 |       fs.accessSync(filePath, fs.constants.R_OK);
136 | 
137 |       return {
138 |         success: true,
139 |         filePath: filePath,
140 |         fileName: path.basename(filePath),
141 |         size: stats.size,
142 |       };
143 |     } catch (error) {
144 |       throw new Error(`Failed to verify file: ${error}`);
145 |     }
146 |   }
147 | 
148 |   /**
149 |    * Clean up a temporary file
150 |    */
151 |   private async cleanupFile(filePath: string): Promise<any> {
152 |     try {
153 |       // Only allow cleanup of files in our temp directory
154 |       if (!filePath.startsWith(this.tempDir)) {
155 |         return {
156 |           success: false,
157 |           error: 'Can only cleanup files in temp directory',
158 |         };
159 |       }
160 | 
161 |       if (fs.existsSync(filePath)) {
162 |         fs.unlinkSync(filePath);
163 |       }
164 | 
165 |       return {
166 |         success: true,
167 |         message: 'File cleaned up successfully',
168 |       };
169 |     } catch (error) {
170 |       return {
171 |         success: false,
172 |         error: `Failed to cleanup file: ${error}`,
173 |       };
174 |     }
175 |   }
176 | 
177 |   /**
178 |    * Generate a filename from URL or create a unique one
179 |    */
180 |   private generateFileName(url?: string): string {
181 |     if (url) {
182 |       try {
183 |         const urlObj = new URL(url);
184 |         const pathname = urlObj.pathname;
185 |         const basename = path.basename(pathname);
186 |         if (basename && basename !== '/') {
187 |           // Add random suffix to avoid collisions
188 |           const ext = path.extname(basename);
189 |           const name = path.basename(basename, ext);
190 |           const randomSuffix = crypto.randomBytes(4).toString('hex');
191 |           return `${name}-${randomSuffix}${ext}`;
192 |         }
193 |       } catch {
194 |         // Invalid URL, fall through to generate random name
195 |       }
196 |     }
197 | 
198 |     // Generate random filename
199 |     return `upload-${crypto.randomBytes(8).toString('hex')}.bin`;
200 |   }
201 | 
202 |   /**
203 |    * Clean up old temporary files (older than 1 hour)
204 |    */
205 |   cleanupOldFiles(): void {
206 |     try {
207 |       const now = Date.now();
208 |       const oneHour = 60 * 60 * 1000;
209 | 
210 |       const files = fs.readdirSync(this.tempDir);
211 |       for (const file of files) {
212 |         const filePath = path.join(this.tempDir, file);
213 |         const stats = fs.statSync(filePath);
214 |         if (now - stats.mtimeMs > oneHour) {
215 |           fs.unlinkSync(filePath);
216 |           console.log(`Cleaned up old temp file: ${file}`);
217 |         }
218 |       }
219 |     } catch (error) {
220 |       console.error('Error cleaning up old files:', error);
221 |     }
222 |   }
223 | }
224 | 
225 | export default new FileHandler();
```

--------------------------------------------------------------------------------
/app/chrome-extension/utils/image-utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Image processing utility functions
  3 |  */
  4 | 
  5 | /**
  6 |  * Create ImageBitmap from data URL (for OffscreenCanvas)
  7 |  * @param dataUrl Image data URL
  8 |  * @returns Created ImageBitmap object
  9 |  */
 10 | export async function createImageBitmapFromUrl(dataUrl: string): Promise<ImageBitmap> {
 11 |   const response = await fetch(dataUrl);
 12 |   const blob = await response.blob();
 13 |   return await createImageBitmap(blob);
 14 | }
 15 | 
 16 | /**
 17 |  * Stitch multiple image parts (dataURL) onto a single canvas
 18 |  * @param parts Array of image parts, each containing dataUrl and y coordinate
 19 |  * @param totalWidthPx Total width (pixels)
 20 |  * @param totalHeightPx Total height (pixels)
 21 |  * @returns Stitched canvas
 22 |  */
 23 | export async function stitchImages(
 24 |   parts: { dataUrl: string; y: number }[],
 25 |   totalWidthPx: number,
 26 |   totalHeightPx: number,
 27 | ): Promise<OffscreenCanvas> {
 28 |   const canvas = new OffscreenCanvas(totalWidthPx, totalHeightPx);
 29 |   const ctx = canvas.getContext('2d');
 30 | 
 31 |   if (!ctx) {
 32 |     throw new Error('Unable to get canvas context');
 33 |   }
 34 | 
 35 |   ctx.fillStyle = '#FFFFFF';
 36 |   ctx.fillRect(0, 0, canvas.width, canvas.height);
 37 | 
 38 |   for (const part of parts) {
 39 |     try {
 40 |       const img = await createImageBitmapFromUrl(part.dataUrl);
 41 |       const sx = 0;
 42 |       const sy = 0;
 43 |       const sWidth = img.width;
 44 |       let sHeight = img.height;
 45 |       const dy = part.y;
 46 | 
 47 |       if (dy + sHeight > totalHeightPx) {
 48 |         sHeight = totalHeightPx - dy;
 49 |       }
 50 | 
 51 |       if (sHeight <= 0) continue;
 52 | 
 53 |       ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, dy, sWidth, sHeight);
 54 |     } catch (error) {
 55 |       console.error('Error stitching image part:', error, part);
 56 |     }
 57 |   }
 58 |   return canvas;
 59 | }
 60 | 
 61 | /**
 62 |  * Crop image (from dataURL) to specified rectangle and resize
 63 |  * @param originalDataUrl Original image data URL
 64 |  * @param cropRectPx Crop rectangle (physical pixels)
 65 |  * @param dpr Device pixel ratio
 66 |  * @param targetWidthOpt Optional target output width (CSS pixels)
 67 |  * @param targetHeightOpt Optional target output height (CSS pixels)
 68 |  * @returns Cropped canvas
 69 |  */
 70 | export async function cropAndResizeImage(
 71 |   originalDataUrl: string,
 72 |   cropRectPx: { x: number; y: number; width: number; height: number },
 73 |   dpr: number = 1,
 74 |   targetWidthOpt?: number,
 75 |   targetHeightOpt?: number,
 76 | ): Promise<OffscreenCanvas> {
 77 |   const img = await createImageBitmapFromUrl(originalDataUrl);
 78 | 
 79 |   let sx = cropRectPx.x;
 80 |   let sy = cropRectPx.y;
 81 |   let sWidth = cropRectPx.width;
 82 |   let sHeight = cropRectPx.height;
 83 | 
 84 |   // Ensure crop area is within image boundaries
 85 |   if (sx < 0) {
 86 |     sWidth += sx;
 87 |     sx = 0;
 88 |   }
 89 |   if (sy < 0) {
 90 |     sHeight += sy;
 91 |     sy = 0;
 92 |   }
 93 |   if (sx + sWidth > img.width) {
 94 |     sWidth = img.width - sx;
 95 |   }
 96 |   if (sy + sHeight > img.height) {
 97 |     sHeight = img.height - sy;
 98 |   }
 99 | 
100 |   if (sWidth <= 0 || sHeight <= 0) {
101 |     throw new Error(
102 |       'Invalid calculated crop size (<=0). Element may not be visible or fully captured.',
103 |     );
104 |   }
105 | 
106 |   const finalCanvasWidthPx = targetWidthOpt ? targetWidthOpt * dpr : sWidth;
107 |   const finalCanvasHeightPx = targetHeightOpt ? targetHeightOpt * dpr : sHeight;
108 | 
109 |   const canvas = new OffscreenCanvas(finalCanvasWidthPx, finalCanvasHeightPx);
110 |   const ctx = canvas.getContext('2d');
111 | 
112 |   if (!ctx) {
113 |     throw new Error('Unable to get canvas context');
114 |   }
115 | 
116 |   ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, finalCanvasWidthPx, finalCanvasHeightPx);
117 | 
118 |   return canvas;
119 | }
120 | 
121 | /**
122 |  * Convert canvas to data URL
123 |  * @param canvas Canvas
124 |  * @param format Image format
125 |  * @param quality JPEG quality (0-1)
126 |  * @returns Data URL
127 |  */
128 | export async function canvasToDataURL(
129 |   canvas: OffscreenCanvas,
130 |   format: string = 'image/png',
131 |   quality?: number,
132 | ): Promise<string> {
133 |   const blob = await canvas.convertToBlob({
134 |     type: format,
135 |     quality: format === 'image/jpeg' ? quality : undefined,
136 |   });
137 | 
138 |   return new Promise((resolve, reject) => {
139 |     const reader = new FileReader();
140 |     reader.onloadend = () => resolve(reader.result as string);
141 |     reader.onerror = reject;
142 |     reader.readAsDataURL(blob);
143 |   });
144 | }
145 | 
146 | /**
147 |  * Compresses an image by scaling it and converting it to a target format with a specific quality.
148 |  * This is the most effective way to reduce image data size for transport or storage.
149 |  *
150 |  * @param {string} imageDataUrl - The original image data URL (e.g., from captureVisibleTab).
151 |  * @param {object} options - Compression options.
152 |  * @param {number} [options.scale=1.0] - The scaling factor for dimensions (e.g., 0.7 for 70%).
153 |  * @param {number} [options.quality=0.8] - The quality for lossy formats like JPEG (0.0 to 1.0).
154 |  * @param {string} [options.format='image/jpeg'] - The target image format.
155 |  * @returns {Promise<{dataUrl: string, mimeType: string}>} A promise that resolves to the compressed image data URL and its MIME type.
156 |  */
157 | export async function compressImage(
158 |   imageDataUrl: string,
159 |   options: { scale?: number; quality?: number; format?: 'image/jpeg' | 'image/webp' },
160 | ): Promise<{ dataUrl: string; mimeType: string }> {
161 |   const { scale = 1.0, quality = 0.8, format = 'image/jpeg' } = options;
162 | 
163 |   // 1. Create an ImageBitmap from the original data URL for efficient drawing.
164 |   const imageBitmap = await createImageBitmapFromUrl(imageDataUrl);
165 | 
166 |   // 2. Calculate the new dimensions based on the scale factor.
167 |   const newWidth = Math.round(imageBitmap.width * scale);
168 |   const newHeight = Math.round(imageBitmap.height * scale);
169 | 
170 |   // 3. Use OffscreenCanvas for performance, as it doesn't need to be in the DOM.
171 |   const canvas = new OffscreenCanvas(newWidth, newHeight);
172 |   const ctx = canvas.getContext('2d');
173 | 
174 |   if (!ctx) {
175 |     throw new Error('Failed to get 2D context from OffscreenCanvas');
176 |   }
177 | 
178 |   // 4. Draw the original image onto the smaller canvas, effectively resizing it.
179 |   ctx.drawImage(imageBitmap, 0, 0, newWidth, newHeight);
180 | 
181 |   // 5. Export the canvas content to the target format with the specified quality.
182 |   // This is the step that performs the data compression.
183 |   const compressedDataUrl = await canvas.convertToBlob({ type: format, quality: quality });
184 | 
185 |   // A helper to convert blob to data URL since OffscreenCanvas.toDataURL is not standard yet
186 |   // on all execution contexts (like service workers).
187 |   const dataUrl = await new Promise<string>((resolve) => {
188 |     const reader = new FileReader();
189 |     reader.onloadend = () => resolve(reader.result as string);
190 |     reader.readAsDataURL(compressedDataUrl);
191 |   });
192 | 
193 |   return { dataUrl, mimeType: format };
194 | }
195 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/popup/components/ModelCacheManagement.vue:
--------------------------------------------------------------------------------

```vue
  1 | <template>
  2 |   <div class="model-cache-section">
  3 |     <h2 class="section-title">{{ getMessage('modelCacheManagementLabel') }}</h2>
  4 | 
  5 |     <!-- Cache Statistics Grid -->
  6 |     <div class="stats-grid">
  7 |       <div class="stats-card">
  8 |         <div class="stats-header">
  9 |           <p class="stats-label">{{ getMessage('cacheSizeLabel') }}</p>
 10 |           <span class="stats-icon orange">
 11 |             <DatabaseIcon />
 12 |           </span>
 13 |         </div>
 14 |         <p class="stats-value">{{ cacheStats?.totalSizeMB || 0 }} MB</p>
 15 |       </div>
 16 | 
 17 |       <div class="stats-card">
 18 |         <div class="stats-header">
 19 |           <p class="stats-label">{{ getMessage('cacheEntriesLabel') }}</p>
 20 |           <span class="stats-icon purple">
 21 |             <VectorIcon />
 22 |           </span>
 23 |         </div>
 24 |         <p class="stats-value">{{ cacheStats?.entryCount || 0 }}</p>
 25 |       </div>
 26 |     </div>
 27 | 
 28 |     <!-- Cache Entries Details -->
 29 |     <div v-if="cacheStats && cacheStats.entries.length > 0" class="cache-details">
 30 |       <h3 class="cache-details-title">{{ getMessage('cacheDetailsLabel') }}</h3>
 31 |       <div class="cache-entries">
 32 |         <div v-for="entry in cacheStats.entries" :key="entry.url" class="cache-entry">
 33 |           <div class="entry-info">
 34 |             <div class="entry-url">{{ getModelNameFromUrl(entry.url) }}</div>
 35 |             <div class="entry-details">
 36 |               <span class="entry-size">{{ entry.sizeMB }} MB</span>
 37 |               <span class="entry-age">{{ entry.age }}</span>
 38 |               <span v-if="entry.expired" class="entry-expired">{{ getMessage('expiredLabel') }}</span>
 39 |             </div>
 40 |           </div>
 41 |         </div>
 42 |       </div>
 43 |     </div>
 44 | 
 45 |     <!-- No Cache Message -->
 46 |     <div v-else-if="cacheStats && cacheStats.entries.length === 0" class="no-cache">
 47 |       <p>{{ getMessage('noCacheDataMessage') }}</p>
 48 |     </div>
 49 | 
 50 |     <!-- Loading State -->
 51 |     <div v-else-if="!cacheStats" class="loading-cache">
 52 |       <p>{{ getMessage('loadingCacheInfoStatus') }}</p>
 53 |     </div>
 54 | 
 55 |     <!-- Progress Indicator -->
 56 |     <ProgressIndicator
 57 |       v-if="isManagingCache"
 58 |       :visible="isManagingCache"
 59 |       :text="isManagingCache ? getMessage('processingCacheStatus') : ''"
 60 |       :showSpinner="true"
 61 |     />
 62 | 
 63 |     <!-- Action Buttons -->
 64 |     <div class="cache-actions">
 65 |       <div class="secondary-button" :disabled="isManagingCache" @click="$emit('cleanup-cache')">
 66 |         <span class="stats-icon"><DatabaseIcon /></span>
 67 |         <span>{{
 68 |           isManagingCache ? getMessage('cleaningStatus') : getMessage('cleanExpiredCacheButton')
 69 |         }}</span>
 70 |       </div>
 71 | 
 72 |       <div class="danger-button" :disabled="isManagingCache" @click="$emit('clear-all-cache')">
 73 |         <span class="stats-icon"><TrashIcon /></span>
 74 |         <span>{{ isManagingCache ? getMessage('clearingStatus') : getMessage('clearAllCacheButton') }}</span>
 75 |       </div>
 76 |     </div>
 77 |   </div>
 78 | </template>
 79 | 
 80 | <script lang="ts" setup>
 81 | import ProgressIndicator from './ProgressIndicator.vue';
 82 | import { DatabaseIcon, VectorIcon, TrashIcon } from './icons';
 83 | import { getMessage } from '@/utils/i18n';
 84 | 
 85 | interface CacheEntry {
 86 |   url: string;
 87 |   size: number;
 88 |   sizeMB: number;
 89 |   timestamp: number;
 90 |   age: string;
 91 |   expired: boolean;
 92 | }
 93 | 
 94 | interface CacheStats {
 95 |   totalSize: number;
 96 |   totalSizeMB: number;
 97 |   entryCount: number;
 98 |   entries: CacheEntry[];
 99 | }
100 | 
101 | interface Props {
102 |   cacheStats: CacheStats | null;
103 |   isManagingCache: boolean;
104 | }
105 | 
106 | interface Emits {
107 |   (e: 'cleanup-cache'): void;
108 |   (e: 'clear-all-cache'): void;
109 | }
110 | 
111 | defineProps<Props>();
112 | defineEmits<Emits>();
113 | 
114 | const getModelNameFromUrl = (url: string) => {
115 |   // Extract model name from HuggingFace URL
116 |   const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)/);
117 |   if (match) {
118 |     return match[1];
119 |   }
120 |   return url.split('/').pop() || url;
121 | };
122 | </script>
123 | 
124 | <style scoped>
125 | .model-cache-section {
126 |   margin-bottom: 24px;
127 | }
128 | 
129 | .section-title {
130 |   font-size: 16px;
131 |   font-weight: 600;
132 |   color: #374151;
133 |   margin-bottom: 12px;
134 | }
135 | 
136 | .stats-grid {
137 |   display: grid;
138 |   grid-template-columns: 1fr 1fr;
139 |   gap: 12px;
140 |   margin-bottom: 16px;
141 | }
142 | 
143 | .stats-card {
144 |   background: white;
145 |   border-radius: 12px;
146 |   box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
147 |   padding: 16px;
148 | }
149 | 
150 | .stats-header {
151 |   display: flex;
152 |   align-items: center;
153 |   justify-content: space-between;
154 |   margin-bottom: 8px;
155 | }
156 | 
157 | .stats-label {
158 |   font-size: 14px;
159 |   font-weight: 500;
160 |   color: #64748b;
161 | }
162 | 
163 | .stats-icon {
164 |   padding: 8px;
165 |   border-radius: 8px;
166 |   width: 36px;
167 |   height: 36px;
168 | }
169 | 
170 | .stats-icon.orange {
171 |   background: #fed7aa;
172 |   color: #ea580c;
173 | }
174 | 
175 | .stats-icon.purple {
176 |   background: #e9d5ff;
177 |   color: #9333ea;
178 | }
179 | 
180 | .stats-value {
181 |   font-size: 30px;
182 |   font-weight: 700;
183 |   color: #0f172a;
184 |   margin: 0;
185 | }
186 | 
187 | .cache-details {
188 |   margin-bottom: 16px;
189 | }
190 | 
191 | .cache-details-title {
192 |   font-size: 14px;
193 |   font-weight: 600;
194 |   color: #374151;
195 |   margin: 0 0 12px 0;
196 | }
197 | 
198 | .cache-entries {
199 |   display: flex;
200 |   flex-direction: column;
201 |   gap: 8px;
202 | }
203 | 
204 | .cache-entry {
205 |   background: white;
206 |   border: 1px solid #e5e7eb;
207 |   border-radius: 8px;
208 |   padding: 12px;
209 | }
210 | 
211 | .entry-info {
212 |   display: flex;
213 |   justify-content: space-between;
214 |   align-items: center;
215 | }
216 | 
217 | .entry-url {
218 |   font-weight: 500;
219 |   color: #1f2937;
220 |   font-size: 14px;
221 | }
222 | 
223 | .entry-details {
224 |   display: flex;
225 |   gap: 8px;
226 |   align-items: center;
227 |   font-size: 12px;
228 | }
229 | 
230 | .entry-size {
231 |   background: #dbeafe;
232 |   color: #1e40af;
233 |   padding: 2px 6px;
234 |   border-radius: 4px;
235 | }
236 | 
237 | .entry-age {
238 |   color: #6b7280;
239 | }
240 | 
241 | .entry-expired {
242 |   background: #fee2e2;
243 |   color: #dc2626;
244 |   padding: 2px 6px;
245 |   border-radius: 4px;
246 | }
247 | 
248 | .no-cache,
249 | .loading-cache {
250 |   text-align: center;
251 |   color: #6b7280;
252 |   padding: 20px;
253 |   background: #f8fafc;
254 |   border-radius: 8px;
255 |   border: 1px solid #e2e8f0;
256 |   margin-bottom: 16px;
257 | }
258 | 
259 | .cache-actions {
260 |   display: flex;
261 |   flex-direction: column;
262 |   gap: 12px;
263 | }
264 | 
265 | .secondary-button {
266 |   background: #f1f5f9;
267 |   color: #475569;
268 |   border: 1px solid #cbd5e1;
269 |   padding: 8px 16px;
270 |   border-radius: 8px;
271 |   font-size: 14px;
272 |   font-weight: 500;
273 |   cursor: pointer;
274 |   transition: all 0.2s ease;
275 |   display: flex;
276 |   align-items: center;
277 |   gap: 8px;
278 |   width: 100%;
279 |   justify-content: center;
280 |   user-select: none;
281 |   cursor: pointer;
282 | }
283 | 
284 | .secondary-button:hover:not(:disabled) {
285 |   background: #e2e8f0;
286 |   border-color: #94a3b8;
287 | }
288 | 
289 | .secondary-button:disabled {
290 |   opacity: 0.5;
291 |   cursor: not-allowed;
292 | }
293 | 
294 | .danger-button {
295 |   width: 100%;
296 |   display: flex;
297 |   align-items: center;
298 |   justify-content: center;
299 |   gap: 8px;
300 |   background: white;
301 |   border: 1px solid #d1d5db;
302 |   color: #374151;
303 |   font-weight: 600;
304 |   padding: 12px 16px;
305 |   border-radius: 8px;
306 |   cursor: pointer;
307 |   user-select: none;
308 |   transition: all 0.2s ease;
309 | }
310 | 
311 | .danger-button:hover:not(:disabled) {
312 |   border-color: #ef4444;
313 |   color: #dc2626;
314 | }
315 | 
316 | .danger-button:disabled {
317 |   opacity: 0.6;
318 |   cursor: not-allowed;
319 | }
320 | </style>
321 | 
```

--------------------------------------------------------------------------------
/app/native-server/src/cli.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { program } from 'commander';
  4 | import * as fs from 'fs';
  5 | import * as path from 'path';
  6 | import {
  7 |   tryRegisterUserLevelHost,
  8 |   colorText,
  9 |   registerWithElevatedPermissions,
 10 |   ensureExecutionPermissions,
 11 | } from './scripts/utils';
 12 | import { BrowserType, parseBrowserType, detectInstalledBrowsers } from './scripts/browser-config';
 13 | 
 14 | // Import writeNodePath from postinstall
 15 | async function writeNodePath(): Promise<void> {
 16 |   try {
 17 |     const nodePath = process.execPath;
 18 |     const nodePathFile = path.join(__dirname, 'node_path.txt');
 19 | 
 20 |     console.log(colorText(`Writing Node.js path: ${nodePath}`, 'blue'));
 21 |     fs.writeFileSync(nodePathFile, nodePath, 'utf8');
 22 |     console.log(colorText('✓ Node.js path written for run_host scripts', 'green'));
 23 |   } catch (error: any) {
 24 |     console.warn(colorText(`⚠️ Failed to write Node.js path: ${error.message}`, 'yellow'));
 25 |   }
 26 | }
 27 | 
 28 | program
 29 |   .version(require('../package.json').version)
 30 |   .description('Mcp Chrome Bridge - Local service for communicating with Chrome extension');
 31 | 
 32 | // Register Native Messaging host
 33 | program
 34 |   .command('register')
 35 |   .description('Register Native Messaging host')
 36 |   .option('-f, --force', 'Force re-registration')
 37 |   .option('-s, --system', 'Use system-level installation (requires administrator/sudo privileges)')
 38 |   .option('-b, --browser <browser>', 'Register for specific browser (chrome, chromium, or all)')
 39 |   .option('-d, --detect', 'Auto-detect installed browsers')
 40 |   .action(async (options) => {
 41 |     try {
 42 |       // Write Node.js path for run_host scripts
 43 |       await writeNodePath();
 44 | 
 45 |       // Determine which browsers to register
 46 |       let targetBrowsers: BrowserType[] | undefined;
 47 | 
 48 |       if (options.browser) {
 49 |         if (options.browser.toLowerCase() === 'all') {
 50 |           targetBrowsers = [BrowserType.CHROME, BrowserType.CHROMIUM];
 51 |           console.log(colorText('Registering for all supported browsers...', 'blue'));
 52 |         } else {
 53 |           const browserType = parseBrowserType(options.browser);
 54 |           if (!browserType) {
 55 |             console.error(
 56 |               colorText(
 57 |                 `Invalid browser: ${options.browser}. Use 'chrome', 'chromium', or 'all'`,
 58 |                 'red',
 59 |               ),
 60 |             );
 61 |             process.exit(1);
 62 |           }
 63 |           targetBrowsers = [browserType];
 64 |         }
 65 |       } else if (options.detect) {
 66 |         targetBrowsers = detectInstalledBrowsers();
 67 |         if (targetBrowsers.length === 0) {
 68 |           console.log(
 69 |             colorText(
 70 |               'No supported browsers detected, will register for Chrome and Chromium',
 71 |               'yellow',
 72 |             ),
 73 |           );
 74 |           targetBrowsers = undefined; // Will use default behavior
 75 |         }
 76 |       }
 77 |       // If neither option specified, tryRegisterUserLevelHost will detect browsers
 78 | 
 79 |       // Detect if running with root/administrator privileges
 80 |       const isRoot = process.getuid && process.getuid() === 0; // Unix/Linux/Mac
 81 | 
 82 |       let isAdmin = false;
 83 |       if (process.platform === 'win32') {
 84 |         try {
 85 |           isAdmin = require('is-admin')(); // Windows requires additional package
 86 |         } catch (error) {
 87 |           console.warn(
 88 |             colorText('Warning: Unable to detect administrator privileges on Windows', 'yellow'),
 89 |           );
 90 |           isAdmin = false;
 91 |         }
 92 |       }
 93 | 
 94 |       const hasElevatedPermissions = isRoot || isAdmin;
 95 | 
 96 |       // If --system option is specified or running with root/administrator privileges
 97 |       if (options.system || hasElevatedPermissions) {
 98 |         // TODO: Update registerWithElevatedPermissions to support multiple browsers
 99 |         await registerWithElevatedPermissions();
100 |         console.log(
101 |           colorText('System-level Native Messaging host registered successfully!', 'green'),
102 |         );
103 |         console.log(
104 |           colorText(
105 |             'You can now use connectNative in Chrome extension to connect to this service.',
106 |             'blue',
107 |           ),
108 |         );
109 |       } else {
110 |         // Regular user-level installation
111 |         console.log(colorText('Registering user-level Native Messaging host...', 'blue'));
112 |         const success = await tryRegisterUserLevelHost(targetBrowsers);
113 | 
114 |         if (success) {
115 |           console.log(colorText('Native Messaging host registered successfully!', 'green'));
116 |           console.log(
117 |             colorText(
118 |               'You can now use connectNative in Chrome extension to connect to this service.',
119 |               'blue',
120 |             ),
121 |           );
122 |         } else {
123 |           console.log(
124 |             colorText(
125 |               'User-level registration failed, please try the following methods:',
126 |               'yellow',
127 |             ),
128 |           );
129 |           console.log(colorText('  1. sudo mcp-chrome-bridge register', 'yellow'));
130 |           console.log(colorText('  2. mcp-chrome-bridge register --system', 'yellow'));
131 |           process.exit(1);
132 |         }
133 |       }
134 |     } catch (error: any) {
135 |       console.error(colorText(`Registration failed: ${error.message}`, 'red'));
136 |       process.exit(1);
137 |     }
138 |   });
139 | 
140 | // Fix execution permissions
141 | program
142 |   .command('fix-permissions')
143 |   .description('Fix execution permissions for native host files')
144 |   .action(async () => {
145 |     try {
146 |       console.log(colorText('Fixing execution permissions...', 'blue'));
147 |       await ensureExecutionPermissions();
148 |       console.log(colorText('✓ Execution permissions fixed successfully!', 'green'));
149 |     } catch (error: any) {
150 |       console.error(colorText(`Failed to fix permissions: ${error.message}`, 'red'));
151 |       process.exit(1);
152 |     }
153 |   });
154 | 
155 | // Update port in stdio-config.json
156 | program
157 |   .command('update-port <port>')
158 |   .description('Update the port number in stdio-config.json')
159 |   .action(async (port: string) => {
160 |     try {
161 |       const portNumber = parseInt(port, 10);
162 |       if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) {
163 |         console.error(colorText('Error: Port must be a valid number between 1 and 65535', 'red'));
164 |         process.exit(1);
165 |       }
166 | 
167 |       const configPath = path.join(__dirname, 'mcp', 'stdio-config.json');
168 | 
169 |       if (!fs.existsSync(configPath)) {
170 |         console.error(colorText(`Error: Configuration file not found at ${configPath}`, 'red'));
171 |         process.exit(1);
172 |       }
173 | 
174 |       const configData = fs.readFileSync(configPath, 'utf8');
175 |       const config = JSON.parse(configData);
176 | 
177 |       const currentUrl = new URL(config.url);
178 |       currentUrl.port = portNumber.toString();
179 |       config.url = currentUrl.toString();
180 | 
181 |       fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
182 | 
183 |       console.log(colorText(`✓ Port updated successfully to ${portNumber}`, 'green'));
184 |       console.log(colorText(`Updated URL: ${config.url}`, 'blue'));
185 |     } catch (error: any) {
186 |       console.error(colorText(`Failed to update port: ${error.message}`, 'red'));
187 |       process.exit(1);
188 |     }
189 |   });
190 | 
191 | program.parse(process.argv);
192 | 
193 | // If no command provided, show help
194 | if (!process.argv.slice(2).length) {
195 |   program.outputHelp();
196 | }
197 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/inject-script.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 { ExecutionWorld } from '@/common/constants';
  5 | 
  6 | interface InjectScriptParam {
  7 |   url?: string;
  8 | }
  9 | interface ScriptConfig {
 10 |   type: ExecutionWorld;
 11 |   jsScript: string;
 12 | }
 13 | 
 14 | interface SendCommandToInjectScriptToolParam {
 15 |   tabId?: number;
 16 |   eventName: string;
 17 |   payload?: string;
 18 | }
 19 | 
 20 | const injectedTabs = new Map();
 21 | class InjectScriptTool extends BaseBrowserToolExecutor {
 22 |   name = TOOL_NAMES.BROWSER.INJECT_SCRIPT;
 23 |   async execute(args: InjectScriptParam & ScriptConfig): Promise<ToolResult> {
 24 |     try {
 25 |       const { url, type, jsScript } = args;
 26 |       let tab;
 27 | 
 28 |       if (!type || !jsScript) {
 29 |         return createErrorResponse('Param [type] and [jsScript] is required');
 30 |       }
 31 | 
 32 |       if (url) {
 33 |         // If URL is provided, check if it's already open
 34 |         console.log(`Checking if URL is already open: ${url}`);
 35 |         const allTabs = await chrome.tabs.query({});
 36 | 
 37 |         // Find tab with matching URL
 38 |         const matchingTabs = allTabs.filter((t) => {
 39 |           // Normalize URLs for comparison (remove trailing slashes)
 40 |           const tabUrl = t.url?.endsWith('/') ? t.url.slice(0, -1) : t.url;
 41 |           const targetUrl = url.endsWith('/') ? url.slice(0, -1) : url;
 42 |           return tabUrl === targetUrl;
 43 |         });
 44 | 
 45 |         if (matchingTabs.length > 0) {
 46 |           // Use existing tab
 47 |           tab = matchingTabs[0];
 48 |           console.log(`Found existing tab with URL: ${url}, tab ID: ${tab.id}`);
 49 |         } else {
 50 |           // Create new tab with the URL
 51 |           console.log(`No existing tab found with URL: ${url}, creating new tab`);
 52 |           tab = await chrome.tabs.create({ url, active: true });
 53 | 
 54 |           // Wait for page to load
 55 |           console.log('Waiting for page to load...');
 56 |           await new Promise((resolve) => setTimeout(resolve, 3000));
 57 |         }
 58 |       } else {
 59 |         // Use active tab
 60 |         const tabs = await chrome.tabs.query({ active: true });
 61 |         if (!tabs[0]) {
 62 |           return createErrorResponse('No active tab found');
 63 |         }
 64 |         tab = tabs[0];
 65 |       }
 66 | 
 67 |       if (!tab.id) {
 68 |         return createErrorResponse('Tab has no ID');
 69 |       }
 70 | 
 71 |       // Make sure tab is active
 72 |       await chrome.tabs.update(tab.id, { active: true });
 73 | 
 74 |       const res = await handleInject(tab.id!, { ...args });
 75 | 
 76 |       return {
 77 |         content: [
 78 |           {
 79 |             type: 'text',
 80 |             text: JSON.stringify(res),
 81 |           },
 82 |         ],
 83 |         isError: false,
 84 |       };
 85 |     } catch (error) {
 86 |       console.error('Error in InjectScriptTool.execute:', error);
 87 |       return createErrorResponse(
 88 |         `Inject script error: ${error instanceof Error ? error.message : String(error)}`,
 89 |       );
 90 |     }
 91 |   }
 92 | }
 93 | 
 94 | class SendCommandToInjectScriptTool extends BaseBrowserToolExecutor {
 95 |   name = TOOL_NAMES.BROWSER.SEND_COMMAND_TO_INJECT_SCRIPT;
 96 |   async execute(args: SendCommandToInjectScriptToolParam): Promise<ToolResult> {
 97 |     try {
 98 |       const { tabId, eventName, payload } = args;
 99 | 
100 |       if (!eventName) {
101 |         return createErrorResponse('Param [eventName] is required');
102 |       }
103 | 
104 |       if (tabId) {
105 |         const tabExists = await isTabExists(tabId);
106 |         if (!tabExists) {
107 |           return createErrorResponse('The tab:[tabId] is not exists');
108 |         }
109 |       }
110 | 
111 |       let finalTabId: number | undefined = tabId;
112 | 
113 |       if (finalTabId === undefined) {
114 |         // Use active tab
115 |         const tabs = await chrome.tabs.query({ active: true });
116 |         if (!tabs[0]) {
117 |           return createErrorResponse('No active tab found');
118 |         }
119 |         finalTabId = tabs[0].id;
120 |       }
121 | 
122 |       if (!finalTabId) {
123 |         return createErrorResponse('No active tab found');
124 |       }
125 | 
126 |       if (!injectedTabs.has(finalTabId)) {
127 |         throw new Error('No script injected in this tab.');
128 |       }
129 |       const result = await chrome.tabs.sendMessage(finalTabId, {
130 |         action: eventName,
131 |         payload,
132 |         targetWorld: injectedTabs.get(finalTabId).type, // The bridge uses this to decide whether to forward to MAIN world.
133 |       });
134 | 
135 |       return {
136 |         content: [
137 |           {
138 |             type: 'text',
139 |             text: JSON.stringify(result),
140 |           },
141 |         ],
142 |         isError: false,
143 |       };
144 |     } catch (error) {
145 |       console.error('Error in InjectScriptTool.execute:', error);
146 |       return createErrorResponse(
147 |         `Inject script error: ${error instanceof Error ? error.message : String(error)}`,
148 |       );
149 |     }
150 |   }
151 | }
152 | 
153 | async function isTabExists(tabId: number) {
154 |   try {
155 |     await chrome.tabs.get(tabId);
156 |     return true;
157 |   } catch (error) {
158 |     // An error is thrown if the tab doesn't exist.
159 |     return false;
160 |   }
161 | }
162 | 
163 | /**
164 |  * @description Handles the injection of user scripts into a specific tab.
165 |  * @param {number} tabId - The ID of the target tab.
166 |  * @param {object} scriptConfig - The configuration object for the script.
167 |  */
168 | async function handleInject(tabId: number, scriptConfig: ScriptConfig) {
169 |   if (injectedTabs.has(tabId)) {
170 |     // If already injected, run cleanup first to ensure a clean state.
171 |     console.log(`Tab ${tabId} already has injections. Cleaning up first.`);
172 |     await handleCleanup(tabId);
173 |   }
174 |   const { type, jsScript } = scriptConfig;
175 |   const hasMain = type === ExecutionWorld.MAIN;
176 | 
177 |   if (hasMain) {
178 |     // The bridge is essential for MAIN world communication and cleanup.
179 |     await chrome.scripting.executeScript({
180 |       target: { tabId },
181 |       files: ['inject-scripts/inject-bridge.js'],
182 |       world: ExecutionWorld.ISOLATED,
183 |     });
184 |     await chrome.scripting.executeScript({
185 |       target: { tabId },
186 |       func: (code) => new Function(code)(),
187 |       args: [jsScript],
188 |       world: ExecutionWorld.MAIN,
189 |     });
190 |   } else {
191 |     await chrome.scripting.executeScript({
192 |       target: { tabId },
193 |       func: (code) => new Function(code)(),
194 |       args: [jsScript],
195 |       world: ExecutionWorld.ISOLATED,
196 |     });
197 |   }
198 |   injectedTabs.set(tabId, scriptConfig);
199 |   console.log(`Scripts successfully injected into tab ${tabId}.`);
200 |   return { injected: true };
201 | }
202 | 
203 | /**
204 |  * @description Triggers the cleanup process in a specific tab.
205 |  * @param {number} tabId - The ID of the target tab.
206 |  */
207 | async function handleCleanup(tabId: number) {
208 |   if (!injectedTabs.has(tabId)) return;
209 |   // Send cleanup signal. The bridge will forward it to the MAIN world.
210 |   chrome.tabs
211 |     .sendMessage(tabId, { type: 'chrome-mcp:cleanup' })
212 |     .catch((err) =>
213 |       console.warn(`Could not send cleanup message to tab ${tabId}. It might have been closed.`),
214 |     );
215 | 
216 |   injectedTabs.delete(tabId);
217 |   console.log(`Cleanup signal sent to tab ${tabId}. State cleared.`);
218 | }
219 | 
220 | export const injectScriptTool = new InjectScriptTool();
221 | export const sendCommandToInjectScriptTool = new SendCommandToInjectScriptTool();
222 | 
223 | // --- Automatic Cleanup Listeners ---
224 | chrome.tabs.onRemoved.addListener((tabId) => {
225 |   if (injectedTabs.has(tabId)) {
226 |     console.log(`Tab ${tabId} closed. Cleaning up state.`);
227 |     injectedTabs.delete(tabId);
228 |   }
229 | });
230 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/click-helper.js:
--------------------------------------------------------------------------------

```javascript
  1 | /* eslint-disable */
  2 | // click-helper.js
  3 | // This script is injected into the page to handle click operations
  4 | 
  5 | if (window.__CLICK_HELPER_INITIALIZED__) {
  6 |   // Already initialized, skip
  7 | } else {
  8 |   window.__CLICK_HELPER_INITIALIZED__ = true;
  9 |   /**
 10 |    * Click on an element matching the selector or at specific coordinates
 11 |    * @param {string} selector - CSS selector for the element to click
 12 |    * @param {boolean} waitForNavigation - Whether to wait for navigation to complete after click
 13 |    * @param {number} timeout - Timeout in milliseconds for waiting for the element or navigation
 14 |    * @param {Object} coordinates - Optional coordinates for clicking at a specific position
 15 |    * @param {number} coordinates.x - X coordinate relative to the viewport
 16 |    * @param {number} coordinates.y - Y coordinate relative to the viewport
 17 |    * @returns {Promise<Object>} - Result of the click operation
 18 |    */
 19 |   async function clickElement(
 20 |     selector,
 21 |     waitForNavigation = false,
 22 |     timeout = 5000,
 23 |     coordinates = null,
 24 |   ) {
 25 |     try {
 26 |       let element = null;
 27 |       let elementInfo = null;
 28 |       let clickX, clickY;
 29 | 
 30 |       if (coordinates && typeof coordinates.x === 'number' && typeof coordinates.y === 'number') {
 31 |         clickX = coordinates.x;
 32 |         clickY = coordinates.y;
 33 | 
 34 |         element = document.elementFromPoint(clickX, clickY);
 35 | 
 36 |         if (element) {
 37 |           const rect = element.getBoundingClientRect();
 38 |           elementInfo = {
 39 |             tagName: element.tagName,
 40 |             id: element.id,
 41 |             className: element.className,
 42 |             text: element.textContent?.trim().substring(0, 100) || '',
 43 |             href: element.href || null,
 44 |             type: element.type || null,
 45 |             isVisible: true,
 46 |             rect: {
 47 |               x: rect.x,
 48 |               y: rect.y,
 49 |               width: rect.width,
 50 |               height: rect.height,
 51 |               top: rect.top,
 52 |               right: rect.right,
 53 |               bottom: rect.bottom,
 54 |               left: rect.left,
 55 |             },
 56 |             clickMethod: 'coordinates',
 57 |             clickPosition: { x: clickX, y: clickY },
 58 |           };
 59 |         } else {
 60 |           elementInfo = {
 61 |             clickMethod: 'coordinates',
 62 |             clickPosition: { x: clickX, y: clickY },
 63 |             warning: 'No element found at the specified coordinates',
 64 |           };
 65 |         }
 66 |       } else {
 67 |         element = document.querySelector(selector);
 68 |         if (!element) {
 69 |           return {
 70 |             error: `Element with selector "${selector}" not found`,
 71 |           };
 72 |         }
 73 | 
 74 |         const rect = element.getBoundingClientRect();
 75 |         elementInfo = {
 76 |           tagName: element.tagName,
 77 |           id: element.id,
 78 |           className: element.className,
 79 |           text: element.textContent?.trim().substring(0, 100) || '',
 80 |           href: element.href || null,
 81 |           type: element.type || null,
 82 |           isVisible: true,
 83 |           rect: {
 84 |             x: rect.x,
 85 |             y: rect.y,
 86 |             width: rect.width,
 87 |             height: rect.height,
 88 |             top: rect.top,
 89 |             right: rect.right,
 90 |             bottom: rect.bottom,
 91 |             left: rect.left,
 92 |           },
 93 |           clickMethod: 'selector',
 94 |         };
 95 | 
 96 |         // First sroll so that the element is in view, then check visibility.
 97 |         element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
 98 |         await new Promise((resolve) => setTimeout(resolve, 100));
 99 |         elementInfo.isVisible = isElementVisible(element);
100 |         if (!elementInfo.isVisible) {
101 |           return {
102 |             error: `Element with selector "${selector}" is not visible`,
103 |             elementInfo,
104 |           };
105 |         }
106 | 
107 |         const updatedRect = element.getBoundingClientRect();
108 |         clickX = updatedRect.left + updatedRect.width / 2;
109 |         clickY = updatedRect.top + updatedRect.height / 2;
110 |       }
111 | 
112 |       let navigationPromise;
113 |       if (waitForNavigation) {
114 |         navigationPromise = new Promise((resolve) => {
115 |           const beforeUnloadListener = () => {
116 |             window.removeEventListener('beforeunload', beforeUnloadListener);
117 |             resolve(true);
118 |           };
119 |           window.addEventListener('beforeunload', beforeUnloadListener);
120 | 
121 |           setTimeout(() => {
122 |             window.removeEventListener('beforeunload', beforeUnloadListener);
123 |             resolve(false);
124 |           }, timeout);
125 |         });
126 |       }
127 | 
128 |       if (element && elementInfo.clickMethod === 'selector') {
129 |         element.click();
130 |       } else {
131 |         simulateClick(clickX, clickY);
132 |       }
133 | 
134 |       // Wait for navigation if needed
135 |       let navigationOccurred = false;
136 |       if (waitForNavigation) {
137 |         navigationOccurred = await navigationPromise;
138 |       }
139 | 
140 |       return {
141 |         success: true,
142 |         message: 'Element clicked successfully',
143 |         elementInfo,
144 |         navigationOccurred,
145 |       };
146 |     } catch (error) {
147 |       return {
148 |         error: `Error clicking element: ${error.message}`,
149 |       };
150 |     }
151 |   }
152 | 
153 |   /**
154 |    * Simulate a mouse click at specific coordinates
155 |    * @param {number} x - X coordinate relative to the viewport
156 |    * @param {number} y - Y coordinate relative to the viewport
157 |    */
158 |   function simulateClick(x, y) {
159 |     const clickEvent = new MouseEvent('click', {
160 |       view: window,
161 |       bubbles: true,
162 |       cancelable: true,
163 |       clientX: x,
164 |       clientY: y,
165 |     });
166 | 
167 |     const element = document.elementFromPoint(x, y);
168 | 
169 |     if (element) {
170 |       element.dispatchEvent(clickEvent);
171 |     } else {
172 |       document.dispatchEvent(clickEvent);
173 |     }
174 |   }
175 | 
176 |   /**
177 |    * Check if an element is visible
178 |    * @param {Element} element - The element to check
179 |    * @returns {boolean} - Whether the element is visible
180 |    */
181 |   function isElementVisible(element) {
182 |     if (!element) return false;
183 | 
184 |     const style = window.getComputedStyle(element);
185 |     if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
186 |       return false;
187 |     }
188 | 
189 |     const rect = element.getBoundingClientRect();
190 |     if (rect.width === 0 || rect.height === 0) {
191 |       return false;
192 |     }
193 | 
194 |     if (
195 |       rect.bottom < 0 ||
196 |       rect.top > window.innerHeight ||
197 |       rect.right < 0 ||
198 |       rect.left > window.innerWidth
199 |     ) {
200 |       return false;
201 |     }
202 | 
203 |     const centerX = rect.left + rect.width / 2;
204 |     const centerY = rect.top + rect.height / 2;
205 | 
206 |     const elementAtPoint = document.elementFromPoint(centerX, centerY);
207 |     if (!elementAtPoint) return false;
208 | 
209 |     return element === elementAtPoint || element.contains(elementAtPoint);
210 |   }
211 | 
212 |   // Listen for messages from the extension
213 |   chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
214 |     if (request.action === 'clickElement') {
215 |       clickElement(
216 |         request.selector,
217 |         request.waitForNavigation,
218 |         request.timeout,
219 |         request.coordinates,
220 |       )
221 |         .then(sendResponse)
222 |         .catch((error) => {
223 |           sendResponse({
224 |             error: `Unexpected error: ${error.message}`,
225 |           });
226 |         });
227 |       return true; // Indicates async response
228 |     } else if (request.action === 'chrome_click_element_ping') {
229 |       sendResponse({ status: 'pong' });
230 |       return false;
231 |     }
232 |   });
233 | }
234 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/history.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 {
  5 |   parseISO,
  6 |   subDays,
  7 |   subWeeks,
  8 |   subMonths,
  9 |   subYears,
 10 |   startOfToday,
 11 |   startOfYesterday,
 12 |   isValid,
 13 |   format,
 14 | } from 'date-fns';
 15 | 
 16 | interface HistoryToolParams {
 17 |   text?: string;
 18 |   startTime?: string;
 19 |   endTime?: string;
 20 |   maxResults?: number;
 21 |   excludeCurrentTabs?: boolean;
 22 | }
 23 | 
 24 | interface HistoryItem {
 25 |   id: string;
 26 |   url?: string;
 27 |   title?: string;
 28 |   lastVisitTime?: number; // Timestamp in milliseconds
 29 |   visitCount?: number;
 30 |   typedCount?: number;
 31 | }
 32 | 
 33 | interface HistoryResult {
 34 |   items: HistoryItem[];
 35 |   totalCount: number;
 36 |   timeRange: {
 37 |     startTime: number;
 38 |     endTime: number;
 39 |     startTimeFormatted: string;
 40 |     endTimeFormatted: string;
 41 |   };
 42 |   query?: string;
 43 | }
 44 | 
 45 | class HistoryTool extends BaseBrowserToolExecutor {
 46 |   name = TOOL_NAMES.BROWSER.HISTORY;
 47 |   private static readonly ONE_DAY_MS = 24 * 60 * 60 * 1000;
 48 | 
 49 |   /**
 50 |    * Parse a date string into milliseconds since epoch.
 51 |    * Returns null if the date string is invalid.
 52 |    * Supports:
 53 |    *  - ISO date strings (e.g., "2023-10-31", "2023-10-31T14:30:00.000Z")
 54 |    *  - Relative times: "1 day ago", "2 weeks ago", "3 months ago", "1 year ago"
 55 |    *  - Special keywords: "now", "today", "yesterday"
 56 |    */
 57 |   private parseDateString(dateStr: string | undefined | null): number | null {
 58 |     if (!dateStr) {
 59 |       // If an empty or null string is passed, it might mean "no specific date",
 60 |       // depending on how you want to treat it. Returning null is safer.
 61 |       return null;
 62 |     }
 63 | 
 64 |     const now = new Date();
 65 |     const lowerDateStr = dateStr.toLowerCase().trim();
 66 | 
 67 |     if (lowerDateStr === 'now') return now.getTime();
 68 |     if (lowerDateStr === 'today') return startOfToday().getTime();
 69 |     if (lowerDateStr === 'yesterday') return startOfYesterday().getTime();
 70 | 
 71 |     const relativeMatch = lowerDateStr.match(
 72 |       /^(\d+)\s+(day|days|week|weeks|month|months|year|years)\s+ago$/,
 73 |     );
 74 |     if (relativeMatch) {
 75 |       const amount = parseInt(relativeMatch[1], 10);
 76 |       const unit = relativeMatch[2];
 77 |       let resultDate: Date;
 78 |       if (unit.startsWith('day')) resultDate = subDays(now, amount);
 79 |       else if (unit.startsWith('week')) resultDate = subWeeks(now, amount);
 80 |       else if (unit.startsWith('month')) resultDate = subMonths(now, amount);
 81 |       else if (unit.startsWith('year')) resultDate = subYears(now, amount);
 82 |       else return null; // Should not happen with the regex
 83 |       return resultDate.getTime();
 84 |     }
 85 | 
 86 |     // Try parsing as ISO or other common date string formats
 87 |     // Native Date constructor can be unreliable for non-standard formats.
 88 |     // date-fns' parseISO is good for ISO 8601.
 89 |     // For other formats, date-fns' parse function is more flexible.
 90 |     let parsedDate = parseISO(dateStr); // Handles "2023-10-31" or "2023-10-31T10:00:00"
 91 |     if (isValid(parsedDate)) {
 92 |       return parsedDate.getTime();
 93 |     }
 94 | 
 95 |     // Fallback to new Date() for other potential formats, but with caution
 96 |     parsedDate = new Date(dateStr);
 97 |     if (isValid(parsedDate) && dateStr.includes(parsedDate.getFullYear().toString())) {
 98 |       return parsedDate.getTime();
 99 |     }
100 | 
101 |     console.warn(`Could not parse date string: ${dateStr}`);
102 |     return null;
103 |   }
104 | 
105 |   /**
106 |    * Format a timestamp as a human-readable date string
107 |    */
108 |   private formatDate(timestamp: number): string {
109 |     // Using date-fns for consistent and potentially localized formatting
110 |     return format(timestamp, 'yyyy-MM-dd HH:mm:ss');
111 |   }
112 | 
113 |   async execute(args: HistoryToolParams): Promise<ToolResult> {
114 |     try {
115 |       console.log('Executing HistoryTool with args:', args);
116 | 
117 |       const {
118 |         text = '',
119 |         maxResults = 100, // Default to 100 results
120 |         excludeCurrentTabs = false,
121 |       } = args;
122 | 
123 |       const now = Date.now();
124 |       let startTimeMs: number;
125 |       let endTimeMs: number;
126 | 
127 |       // Parse startTime
128 |       if (args.startTime) {
129 |         const parsedStart = this.parseDateString(args.startTime);
130 |         if (parsedStart === null) {
131 |           return createErrorResponse(
132 |             `Invalid format for start time: "${args.startTime}". Supported formats: ISO (YYYY-MM-DD), "today", "yesterday", "X days/weeks/months/years ago".`,
133 |           );
134 |         }
135 |         startTimeMs = parsedStart;
136 |       } else {
137 |         // Default to 24 hours ago if startTime is not provided
138 |         startTimeMs = now - HistoryTool.ONE_DAY_MS;
139 |       }
140 | 
141 |       // Parse endTime
142 |       if (args.endTime) {
143 |         const parsedEnd = this.parseDateString(args.endTime);
144 |         if (parsedEnd === null) {
145 |           return createErrorResponse(
146 |             `Invalid format for end time: "${args.endTime}". Supported formats: ISO (YYYY-MM-DD), "today", "yesterday", "X days/weeks/months/years ago".`,
147 |           );
148 |         }
149 |         endTimeMs = parsedEnd;
150 |       } else {
151 |         // Default to current time if endTime is not provided
152 |         endTimeMs = now;
153 |       }
154 | 
155 |       // Validate time range
156 |       if (startTimeMs > endTimeMs) {
157 |         return createErrorResponse('Start time cannot be after end time.');
158 |       }
159 | 
160 |       console.log(
161 |         `Searching history from ${this.formatDate(startTimeMs)} to ${this.formatDate(endTimeMs)} for query "${text}"`,
162 |       );
163 | 
164 |       const historyItems = await chrome.history.search({
165 |         text,
166 |         startTime: startTimeMs,
167 |         endTime: endTimeMs,
168 |         maxResults,
169 |       });
170 | 
171 |       console.log(`Found ${historyItems.length} history items before filtering current tabs.`);
172 | 
173 |       let filteredItems = historyItems;
174 |       if (excludeCurrentTabs && historyItems.length > 0) {
175 |         const currentTabs = await chrome.tabs.query({});
176 |         const openUrls = new Set<string>();
177 | 
178 |         currentTabs.forEach((tab) => {
179 |           if (tab.url) {
180 |             openUrls.add(tab.url);
181 |           }
182 |         });
183 | 
184 |         if (openUrls.size > 0) {
185 |           filteredItems = historyItems.filter((item) => !(item.url && openUrls.has(item.url)));
186 |           console.log(
187 |             `Filtered out ${historyItems.length - filteredItems.length} items that are currently open. ${filteredItems.length} items remaining.`,
188 |           );
189 |         }
190 |       }
191 | 
192 |       const result: HistoryResult = {
193 |         items: filteredItems.map((item) => ({
194 |           id: item.id,
195 |           url: item.url,
196 |           title: item.title,
197 |           lastVisitTime: item.lastVisitTime,
198 |           visitCount: item.visitCount,
199 |           typedCount: item.typedCount,
200 |         })),
201 |         totalCount: filteredItems.length,
202 |         timeRange: {
203 |           startTime: startTimeMs,
204 |           endTime: endTimeMs,
205 |           startTimeFormatted: this.formatDate(startTimeMs),
206 |           endTimeFormatted: this.formatDate(endTimeMs),
207 |         },
208 |       };
209 | 
210 |       if (text) {
211 |         result.query = text;
212 |       }
213 | 
214 |       return {
215 |         content: [
216 |           {
217 |             type: 'text',
218 |             text: JSON.stringify(result, null, 2),
219 |           },
220 |         ],
221 |         isError: false,
222 |       };
223 |     } catch (error) {
224 |       console.error('Error in HistoryTool.execute:', error);
225 |       return createErrorResponse(
226 |         `Error retrieving browsing history: ${error instanceof Error ? error.message : String(error)}`,
227 |       );
228 |     }
229 |   }
230 | }
231 | 
232 | export const historyTool = new HistoryTool();
233 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/utils/text-chunker.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Text chunking utility
  3 |  * Based on semantic chunking strategy, splits long text into small chunks suitable for vectorization
  4 |  */
  5 | 
  6 | export interface TextChunk {
  7 |   text: string;
  8 |   source: string;
  9 |   index: number;
 10 |   wordCount: number;
 11 | }
 12 | 
 13 | export interface ChunkingOptions {
 14 |   maxWordsPerChunk?: number;
 15 |   overlapSentences?: number;
 16 |   minChunkLength?: number;
 17 |   includeTitle?: boolean;
 18 | }
 19 | 
 20 | export class TextChunker {
 21 |   private readonly defaultOptions: Required<ChunkingOptions> = {
 22 |     maxWordsPerChunk: 80,
 23 |     overlapSentences: 1,
 24 |     minChunkLength: 20,
 25 |     includeTitle: true,
 26 |   };
 27 | 
 28 |   public chunkText(content: string, title?: string, options?: ChunkingOptions): TextChunk[] {
 29 |     const opts = { ...this.defaultOptions, ...options };
 30 |     const chunks: TextChunk[] = [];
 31 | 
 32 |     if (opts.includeTitle && title?.trim() && title.trim().length > 5) {
 33 |       chunks.push({
 34 |         text: title.trim(),
 35 |         source: 'title',
 36 |         index: 0,
 37 |         wordCount: title.trim().split(/\s+/).length,
 38 |       });
 39 |     }
 40 | 
 41 |     const cleanContent = content.trim();
 42 |     if (!cleanContent) {
 43 |       return chunks;
 44 |     }
 45 | 
 46 |     const sentences = this.splitIntoSentences(cleanContent);
 47 | 
 48 |     if (sentences.length === 0) {
 49 |       return this.fallbackChunking(cleanContent, chunks, opts);
 50 |     }
 51 | 
 52 |     const hasLongSentences = sentences.some(
 53 |       (s: string) => s.split(/\s+/).length > opts.maxWordsPerChunk,
 54 |     );
 55 | 
 56 |     if (hasLongSentences) {
 57 |       return this.mixedChunking(sentences, chunks, opts);
 58 |     }
 59 | 
 60 |     return this.groupSentencesIntoChunks(sentences, chunks, opts);
 61 |   }
 62 | 
 63 |   private splitIntoSentences(content: string): string[] {
 64 |     const processedContent = content
 65 |       .replace(/([。!?])\s*/g, '$1\n')
 66 |       .replace(/([.!?])\s+(?=[A-Z])/g, '$1\n')
 67 |       .replace(/([.!?]["'])\s+(?=[A-Z])/g, '$1\n')
 68 |       .replace(/([.!?])\s*$/gm, '$1\n')
 69 |       .replace(/([。!?][""])\s*/g, '$1\n')
 70 |       .replace(/\n\s*\n/g, '\n');
 71 | 
 72 |     const sentences = processedContent
 73 |       .split('\n')
 74 |       .map((s) => s.trim())
 75 |       .filter((s) => s.length > 15);
 76 | 
 77 |     if (sentences.length < 3 && content.length > 500) {
 78 |       return this.aggressiveSentenceSplitting(content);
 79 |     }
 80 | 
 81 |     return sentences;
 82 |   }
 83 | 
 84 |   private aggressiveSentenceSplitting(content: string): string[] {
 85 |     const sentences = content
 86 |       .replace(/([.!?。!?])/g, '$1\n')
 87 |       .replace(/([;;::])/g, '$1\n')
 88 |       .replace(/([))])\s*(?=[\u4e00-\u9fa5A-Z])/g, '$1\n')
 89 |       .split('\n')
 90 |       .map((s) => s.trim())
 91 |       .filter((s) => s.length > 15);
 92 | 
 93 |     const maxWordsPerChunk = 80;
 94 |     const finalSentences: string[] = [];
 95 | 
 96 |     for (const sentence of sentences) {
 97 |       const words = sentence.split(/\s+/);
 98 |       if (words.length <= maxWordsPerChunk) {
 99 |         finalSentences.push(sentence);
100 |       } else {
101 |         const overlapWords = 5;
102 |         for (let i = 0; i < words.length; i += maxWordsPerChunk - overlapWords) {
103 |           const chunkWords = words.slice(i, i + maxWordsPerChunk);
104 |           const chunkText = chunkWords.join(' ');
105 |           if (chunkText.length > 15) {
106 |             finalSentences.push(chunkText);
107 |           }
108 |         }
109 |       }
110 |     }
111 | 
112 |     return finalSentences;
113 |   }
114 | 
115 |   /**
116 |    * Group sentences into chunks
117 |    */
118 |   private groupSentencesIntoChunks(
119 |     sentences: string[],
120 |     existingChunks: TextChunk[],
121 |     options: Required<ChunkingOptions>,
122 |   ): TextChunk[] {
123 |     const chunks = [...existingChunks];
124 |     let chunkIndex = chunks.length;
125 | 
126 |     let i = 0;
127 |     while (i < sentences.length) {
128 |       let currentChunkText = '';
129 |       let currentWordCount = 0;
130 |       let sentencesUsed = 0;
131 | 
132 |       while (i + sentencesUsed < sentences.length && currentWordCount < options.maxWordsPerChunk) {
133 |         const sentence = sentences[i + sentencesUsed];
134 |         const sentenceWords = sentence.split(/\s+/).length;
135 | 
136 |         if (currentWordCount + sentenceWords > options.maxWordsPerChunk && currentWordCount > 0) {
137 |           break;
138 |         }
139 | 
140 |         currentChunkText += (currentChunkText ? ' ' : '') + sentence;
141 |         currentWordCount += sentenceWords;
142 |         sentencesUsed++;
143 |       }
144 | 
145 |       if (currentChunkText.trim().length > options.minChunkLength) {
146 |         chunks.push({
147 |           text: currentChunkText.trim(),
148 |           source: `content_chunk_${chunkIndex}`,
149 |           index: chunkIndex,
150 |           wordCount: currentWordCount,
151 |         });
152 |         chunkIndex++;
153 |       }
154 | 
155 |       i += Math.max(1, sentencesUsed - options.overlapSentences);
156 |     }
157 |     return chunks;
158 |   }
159 | 
160 |   /**
161 |    * Mixed chunking method (handles long sentences)
162 |    */
163 |   private mixedChunking(
164 |     sentences: string[],
165 |     existingChunks: TextChunk[],
166 |     options: Required<ChunkingOptions>,
167 |   ): TextChunk[] {
168 |     const chunks = [...existingChunks];
169 |     let chunkIndex = chunks.length;
170 | 
171 |     for (const sentence of sentences) {
172 |       const sentenceWords = sentence.split(/\s+/).length;
173 | 
174 |       if (sentenceWords <= options.maxWordsPerChunk) {
175 |         chunks.push({
176 |           text: sentence.trim(),
177 |           source: `sentence_chunk_${chunkIndex}`,
178 |           index: chunkIndex,
179 |           wordCount: sentenceWords,
180 |         });
181 |         chunkIndex++;
182 |       } else {
183 |         const words = sentence.split(/\s+/);
184 |         for (let i = 0; i < words.length; i += options.maxWordsPerChunk) {
185 |           const chunkWords = words.slice(i, i + options.maxWordsPerChunk);
186 |           const chunkText = chunkWords.join(' ');
187 | 
188 |           if (chunkText.length > options.minChunkLength) {
189 |             chunks.push({
190 |               text: chunkText,
191 |               source: `long_sentence_chunk_${chunkIndex}_part_${Math.floor(i / options.maxWordsPerChunk)}`,
192 |               index: chunkIndex,
193 |               wordCount: chunkWords.length,
194 |             });
195 |           }
196 |         }
197 |         chunkIndex++;
198 |       }
199 |     }
200 | 
201 |     return chunks;
202 |   }
203 | 
204 |   /**
205 |    * Fallback chunking (when sentence splitting fails)
206 |    */
207 |   private fallbackChunking(
208 |     content: string,
209 |     existingChunks: TextChunk[],
210 |     options: Required<ChunkingOptions>,
211 |   ): TextChunk[] {
212 |     const chunks = [...existingChunks];
213 |     let chunkIndex = chunks.length;
214 | 
215 |     const paragraphs = content
216 |       .split(/\n\s*\n/)
217 |       .filter((p) => p.trim().length > options.minChunkLength);
218 | 
219 |     if (paragraphs.length > 1) {
220 |       paragraphs.forEach((paragraph, index) => {
221 |         const cleanParagraph = paragraph.trim();
222 |         if (cleanParagraph.length > 0) {
223 |           const words = cleanParagraph.split(/\s+/);
224 |           const maxWordsPerChunk = 150;
225 | 
226 |           for (let i = 0; i < words.length; i += maxWordsPerChunk) {
227 |             const chunkWords = words.slice(i, i + maxWordsPerChunk);
228 |             const chunkText = chunkWords.join(' ');
229 | 
230 |             if (chunkText.length > options.minChunkLength) {
231 |               chunks.push({
232 |                 text: chunkText,
233 |                 source: `paragraph_${index}_chunk_${Math.floor(i / maxWordsPerChunk)}`,
234 |                 index: chunkIndex,
235 |                 wordCount: chunkWords.length,
236 |               });
237 |               chunkIndex++;
238 |             }
239 |           }
240 |         }
241 |       });
242 |     } else {
243 |       const words = content.trim().split(/\s+/);
244 |       const maxWordsPerChunk = 150;
245 | 
246 |       for (let i = 0; i < words.length; i += maxWordsPerChunk) {
247 |         const chunkWords = words.slice(i, i + maxWordsPerChunk);
248 |         const chunkText = chunkWords.join(' ');
249 | 
250 |         if (chunkText.length > options.minChunkLength) {
251 |           chunks.push({
252 |             text: chunkText,
253 |             source: `content_chunk_${Math.floor(i / maxWordsPerChunk)}`,
254 |             index: chunkIndex,
255 |             wordCount: chunkWords.length,
256 |           });
257 |           chunkIndex++;
258 |         }
259 |       }
260 |     }
261 | 
262 |     return chunks;
263 |   }
264 | }
265 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/web-fetcher.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 { TOOL_MESSAGE_TYPES } from '@/common/message-types';
  5 | 
  6 | interface WebFetcherToolParams {
  7 |   htmlContent?: boolean; // get the visible HTML content of the current page. default: false
  8 |   textContent?: boolean; // get the visible text content of the current page. default: true
  9 |   url?: string; // optional URL to fetch content from (if not provided, uses active tab)
 10 |   selector?: string; // optional CSS selector to get content from a specific element
 11 | }
 12 | 
 13 | class WebFetcherTool extends BaseBrowserToolExecutor {
 14 |   name = TOOL_NAMES.BROWSER.WEB_FETCHER;
 15 | 
 16 |   /**
 17 |    * Execute web fetcher operation
 18 |    */
 19 |   async execute(args: WebFetcherToolParams): Promise<ToolResult> {
 20 |     // Handle mutually exclusive parameters: if htmlContent is true, textContent is forced to false
 21 |     const htmlContent = args.htmlContent === true;
 22 |     const textContent = htmlContent ? false : args.textContent !== false; // Default is true, unless htmlContent is true or textContent is explicitly set to false
 23 |     const url = args.url;
 24 |     const selector = args.selector;
 25 | 
 26 |     console.log(`Starting web fetcher with options:`, {
 27 |       htmlContent,
 28 |       textContent,
 29 |       url,
 30 |       selector,
 31 |     });
 32 | 
 33 |     try {
 34 |       // Get tab to fetch content from
 35 |       let tab;
 36 | 
 37 |       if (url) {
 38 |         // If URL is provided, check if it's already open
 39 |         console.log(`Checking if URL is already open: ${url}`);
 40 |         const allTabs = await chrome.tabs.query({});
 41 | 
 42 |         // Find tab with matching URL
 43 |         const matchingTabs = allTabs.filter((t) => {
 44 |           // Normalize URLs for comparison (remove trailing slashes)
 45 |           const tabUrl = t.url?.endsWith('/') ? t.url.slice(0, -1) : t.url;
 46 |           const targetUrl = url.endsWith('/') ? url.slice(0, -1) : url;
 47 |           return tabUrl === targetUrl;
 48 |         });
 49 | 
 50 |         if (matchingTabs.length > 0) {
 51 |           // Use existing tab
 52 |           tab = matchingTabs[0];
 53 |           console.log(`Found existing tab with URL: ${url}, tab ID: ${tab.id}`);
 54 |         } else {
 55 |           // Create new tab with the URL
 56 |           console.log(`No existing tab found with URL: ${url}, creating new tab`);
 57 |           tab = await chrome.tabs.create({ url, active: true });
 58 | 
 59 |           // Wait for page to load
 60 |           console.log('Waiting for page to load...');
 61 |           await new Promise((resolve) => setTimeout(resolve, 3000));
 62 |         }
 63 |       } else {
 64 |         // Use active tab
 65 |         const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
 66 |         if (!tabs[0]) {
 67 |           return createErrorResponse('No active tab found');
 68 |         }
 69 |         tab = tabs[0];
 70 |       }
 71 | 
 72 |       if (!tab.id) {
 73 |         return createErrorResponse('Tab has no ID');
 74 |       }
 75 | 
 76 |       // Make sure tab is active
 77 |       await chrome.tabs.update(tab.id, { active: true });
 78 | 
 79 |       // Prepare result object
 80 |       const result: any = {
 81 |         success: true,
 82 |         url: tab.url,
 83 |         title: tab.title,
 84 |       };
 85 | 
 86 |       await this.injectContentScript(tab.id, ['inject-scripts/web-fetcher-helper.js']);
 87 | 
 88 |       // Get HTML content if requested
 89 |       if (htmlContent) {
 90 |         const htmlResponse = await this.sendMessageToTab(tab.id, {
 91 |           action: TOOL_MESSAGE_TYPES.WEB_FETCHER_GET_HTML_CONTENT,
 92 |           selector: selector,
 93 |         });
 94 | 
 95 |         if (htmlResponse.success) {
 96 |           result.htmlContent = htmlResponse.htmlContent;
 97 |         } else {
 98 |           console.error('Failed to get HTML content:', htmlResponse.error);
 99 |           result.htmlContentError = htmlResponse.error;
100 |         }
101 |       }
102 | 
103 |       // Get text content if requested (and htmlContent is not true)
104 |       if (textContent) {
105 |         const textResponse = await this.sendMessageToTab(tab.id, {
106 |           action: TOOL_MESSAGE_TYPES.WEB_FETCHER_GET_TEXT_CONTENT,
107 |           selector: selector,
108 |         });
109 | 
110 |         if (textResponse.success) {
111 |           result.textContent = textResponse.textContent;
112 | 
113 |           // Include article metadata if available
114 |           if (textResponse.article) {
115 |             result.article = {
116 |               title: textResponse.article.title,
117 |               byline: textResponse.article.byline,
118 |               siteName: textResponse.article.siteName,
119 |               excerpt: textResponse.article.excerpt,
120 |               lang: textResponse.article.lang,
121 |             };
122 |           }
123 | 
124 |           // Include page metadata if available
125 |           if (textResponse.metadata) {
126 |             result.metadata = textResponse.metadata;
127 |           }
128 |         } else {
129 |           console.error('Failed to get text content:', textResponse.error);
130 |           result.textContentError = textResponse.error;
131 |         }
132 |       }
133 | 
134 |       // Interactive elements feature has been removed
135 | 
136 |       return {
137 |         content: [
138 |           {
139 |             type: 'text',
140 |             text: JSON.stringify(result),
141 |           },
142 |         ],
143 |         isError: false,
144 |       };
145 |     } catch (error) {
146 |       console.error('Error in web fetcher:', error);
147 |       return createErrorResponse(
148 |         `Error fetching web content: ${error instanceof Error ? error.message : String(error)}`,
149 |       );
150 |     }
151 |   }
152 | }
153 | 
154 | export const webFetcherTool = new WebFetcherTool();
155 | 
156 | interface GetInteractiveElementsToolParams {
157 |   textQuery?: string; // Text to search for within interactive elements (fuzzy search)
158 |   selector?: string; // CSS selector to filter interactive elements
159 |   includeCoordinates?: boolean; // Include element coordinates in the response (default: true)
160 |   types?: string[]; // Types of interactive elements to include (default: all types)
161 | }
162 | 
163 | class GetInteractiveElementsTool extends BaseBrowserToolExecutor {
164 |   name = TOOL_NAMES.BROWSER.GET_INTERACTIVE_ELEMENTS;
165 | 
166 |   /**
167 |    * Execute get interactive elements operation
168 |    */
169 |   async execute(args: GetInteractiveElementsToolParams): Promise<ToolResult> {
170 |     const { textQuery, selector, includeCoordinates = true, types } = args;
171 | 
172 |     console.log(`Starting get interactive elements with options:`, args);
173 | 
174 |     try {
175 |       // Get current tab
176 |       const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
177 |       if (!tabs[0]) {
178 |         return createErrorResponse('No active tab found');
179 |       }
180 | 
181 |       const tab = tabs[0];
182 |       if (!tab.id) {
183 |         return createErrorResponse('Active tab has no ID');
184 |       }
185 | 
186 |       // Ensure content script is injected
187 |       await this.injectContentScript(tab.id, ['inject-scripts/interactive-elements-helper.js']);
188 | 
189 |       // Send message to content script
190 |       const result = await this.sendMessageToTab(tab.id, {
191 |         action: TOOL_MESSAGE_TYPES.GET_INTERACTIVE_ELEMENTS,
192 |         textQuery,
193 |         selector,
194 |         includeCoordinates,
195 |         types,
196 |       });
197 | 
198 |       if (!result.success) {
199 |         return createErrorResponse(result.error || 'Failed to get interactive elements');
200 |       }
201 | 
202 |       return {
203 |         content: [
204 |           {
205 |             type: 'text',
206 |             text: JSON.stringify({
207 |               success: true,
208 |               elements: result.elements,
209 |               count: result.elements.length,
210 |               query: {
211 |                 textQuery,
212 |                 selector,
213 |                 types: types || 'all',
214 |               },
215 |             }),
216 |           },
217 |         ],
218 |         isError: false,
219 |       };
220 |     } catch (error) {
221 |       console.error('Error in get interactive elements operation:', error);
222 |       return createErrorResponse(
223 |         `Error getting interactive elements: ${error instanceof Error ? error.message : String(error)}`,
224 |       );
225 |     }
226 |   }
227 | }
228 | 
229 | export const getInteractiveElementsTool = new GetInteractiveElementsTool();
230 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/native-host.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { NativeMessageType } from 'chrome-mcp-shared';
  2 | import { BACKGROUND_MESSAGE_TYPES } from '@/common/message-types';
  3 | import {
  4 |   NATIVE_HOST,
  5 |   ICONS,
  6 |   NOTIFICATIONS,
  7 |   STORAGE_KEYS,
  8 |   ERROR_MESSAGES,
  9 |   SUCCESS_MESSAGES,
 10 | } from '@/common/constants';
 11 | import { handleCallTool } from './tools';
 12 | 
 13 | let nativePort: chrome.runtime.Port | null = null;
 14 | export const HOST_NAME = NATIVE_HOST.NAME;
 15 | 
 16 | /**
 17 |  * Server status management interface
 18 |  */
 19 | interface ServerStatus {
 20 |   isRunning: boolean;
 21 |   port?: number;
 22 |   lastUpdated: number;
 23 | }
 24 | 
 25 | let currentServerStatus: ServerStatus = {
 26 |   isRunning: false,
 27 |   lastUpdated: Date.now(),
 28 | };
 29 | 
 30 | /**
 31 |  * Save server status to chrome.storage
 32 |  */
 33 | async function saveServerStatus(status: ServerStatus): Promise<void> {
 34 |   try {
 35 |     await chrome.storage.local.set({ [STORAGE_KEYS.SERVER_STATUS]: status });
 36 |   } catch (error) {
 37 |     console.error(ERROR_MESSAGES.SERVER_STATUS_SAVE_FAILED, error);
 38 |   }
 39 | }
 40 | 
 41 | /**
 42 |  * Load server status from chrome.storage
 43 |  */
 44 | async function loadServerStatus(): Promise<ServerStatus> {
 45 |   try {
 46 |     const result = await chrome.storage.local.get([STORAGE_KEYS.SERVER_STATUS]);
 47 |     if (result[STORAGE_KEYS.SERVER_STATUS]) {
 48 |       return result[STORAGE_KEYS.SERVER_STATUS];
 49 |     }
 50 |   } catch (error) {
 51 |     console.error(ERROR_MESSAGES.SERVER_STATUS_LOAD_FAILED, error);
 52 |   }
 53 |   return {
 54 |     isRunning: false,
 55 |     lastUpdated: Date.now(),
 56 |   };
 57 | }
 58 | 
 59 | /**
 60 |  * Broadcast server status change to all listeners
 61 |  */
 62 | function broadcastServerStatusChange(status: ServerStatus): void {
 63 |   chrome.runtime
 64 |     .sendMessage({
 65 |       type: BACKGROUND_MESSAGE_TYPES.SERVER_STATUS_CHANGED,
 66 |       payload: status,
 67 |     })
 68 |     .catch(() => {
 69 |       // Ignore errors if no listeners are present
 70 |     });
 71 | }
 72 | 
 73 | /**
 74 |  * Connect to the native messaging host
 75 |  */
 76 | export function connectNativeHost(port: number = NATIVE_HOST.DEFAULT_PORT) {
 77 |   if (nativePort) {
 78 |     return;
 79 |   }
 80 | 
 81 |   try {
 82 |     nativePort = chrome.runtime.connectNative(HOST_NAME);
 83 | 
 84 |     nativePort.onMessage.addListener(async (message) => {
 85 |       // chrome.notifications.create({
 86 |       //   type: NOTIFICATIONS.TYPE,
 87 |       //   iconUrl: chrome.runtime.getURL(ICONS.NOTIFICATION),
 88 |       //   title: 'Message from native host',
 89 |       //   message: `Received data from host: ${JSON.stringify(message)}`,
 90 |       //   priority: NOTIFICATIONS.PRIORITY,
 91 |       // });
 92 | 
 93 |       if (message.type === NativeMessageType.PROCESS_DATA && message.requestId) {
 94 |         const requestId = message.requestId;
 95 |         const requestPayload = message.payload;
 96 | 
 97 |         nativePort?.postMessage({
 98 |           responseToRequestId: requestId,
 99 |           payload: {
100 |             status: 'success',
101 |             message: SUCCESS_MESSAGES.TOOL_EXECUTED,
102 |             data: requestPayload,
103 |           },
104 |         });
105 |       } else if (message.type === NativeMessageType.CALL_TOOL && message.requestId) {
106 |         const requestId = message.requestId;
107 |         try {
108 |           const result = await handleCallTool(message.payload);
109 |           nativePort?.postMessage({
110 |             responseToRequestId: requestId,
111 |             payload: {
112 |               status: 'success',
113 |               message: SUCCESS_MESSAGES.TOOL_EXECUTED,
114 |               data: result,
115 |             },
116 |           });
117 |         } catch (error) {
118 |           nativePort?.postMessage({
119 |             responseToRequestId: requestId,
120 |             payload: {
121 |               status: 'error',
122 |               message: ERROR_MESSAGES.TOOL_EXECUTION_FAILED,
123 |               error: error instanceof Error ? error.message : String(error),
124 |             },
125 |           });
126 |         }
127 |       } else if (message.type === NativeMessageType.SERVER_STARTED) {
128 |         const port = message.payload?.port;
129 |         currentServerStatus = {
130 |           isRunning: true,
131 |           port: port,
132 |           lastUpdated: Date.now(),
133 |         };
134 |         await saveServerStatus(currentServerStatus);
135 |         broadcastServerStatusChange(currentServerStatus);
136 |         console.log(`${SUCCESS_MESSAGES.SERVER_STARTED} on port ${port}`);
137 |       } else if (message.type === NativeMessageType.SERVER_STOPPED) {
138 |         currentServerStatus = {
139 |           isRunning: false,
140 |           port: currentServerStatus.port, // Keep last known port for reconnection
141 |           lastUpdated: Date.now(),
142 |         };
143 |         await saveServerStatus(currentServerStatus);
144 |         broadcastServerStatusChange(currentServerStatus);
145 |         console.log(SUCCESS_MESSAGES.SERVER_STOPPED);
146 |       } else if (message.type === NativeMessageType.ERROR_FROM_NATIVE_HOST) {
147 |         console.error('Error from native host:', message.payload?.message || 'Unknown error');
148 |       } else if (message.type === 'file_operation_response') {
149 |         // Forward file operation response back to the requesting tool
150 |         chrome.runtime.sendMessage(message).catch(() => {
151 |           // Ignore if no listeners
152 |         });
153 |       }
154 |     });
155 | 
156 |     nativePort.onDisconnect.addListener(() => {
157 |       console.error(ERROR_MESSAGES.NATIVE_DISCONNECTED, chrome.runtime.lastError);
158 |       nativePort = null;
159 |     });
160 | 
161 |     nativePort.postMessage({ type: NativeMessageType.START, payload: { port } });
162 |   } catch (error) {
163 |     console.error(ERROR_MESSAGES.NATIVE_CONNECTION_FAILED, error);
164 |   }
165 | }
166 | 
167 | /**
168 |  * Initialize native host listeners and load initial state
169 |  */
170 | export const initNativeHostListener = () => {
171 |   // Initialize server status from storage
172 |   loadServerStatus()
173 |     .then((status) => {
174 |       currentServerStatus = status;
175 |     })
176 |     .catch((error) => {
177 |       console.error(ERROR_MESSAGES.SERVER_STATUS_LOAD_FAILED, error);
178 |     });
179 | 
180 |   chrome.runtime.onStartup.addListener(connectNativeHost);
181 | 
182 |   chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
183 |     if (
184 |       message === NativeMessageType.CONNECT_NATIVE ||
185 |       message.type === NativeMessageType.CONNECT_NATIVE
186 |     ) {
187 |       const port =
188 |         typeof message === 'object' && message.port ? message.port : NATIVE_HOST.DEFAULT_PORT;
189 |       connectNativeHost(port);
190 |       sendResponse({ success: true, port });
191 |       return true;
192 |     }
193 | 
194 |     if (message.type === NativeMessageType.PING_NATIVE) {
195 |       const connected = nativePort !== null;
196 |       sendResponse({ connected });
197 |       return true;
198 |     }
199 | 
200 |     if (message.type === NativeMessageType.DISCONNECT_NATIVE) {
201 |       if (nativePort) {
202 |         nativePort.disconnect();
203 |         nativePort = null;
204 |         sendResponse({ success: true });
205 |       } else {
206 |         sendResponse({ success: false, error: 'No active connection' });
207 |       }
208 |       return true;
209 |     }
210 | 
211 |     if (message.type === BACKGROUND_MESSAGE_TYPES.GET_SERVER_STATUS) {
212 |       sendResponse({
213 |         success: true,
214 |         serverStatus: currentServerStatus,
215 |         connected: nativePort !== null,
216 |       });
217 |       return true;
218 |     }
219 | 
220 |     if (message.type === BACKGROUND_MESSAGE_TYPES.REFRESH_SERVER_STATUS) {
221 |       loadServerStatus()
222 |         .then((storedStatus) => {
223 |           currentServerStatus = storedStatus;
224 |           sendResponse({
225 |             success: true,
226 |             serverStatus: currentServerStatus,
227 |             connected: nativePort !== null,
228 |           });
229 |         })
230 |         .catch((error) => {
231 |           console.error(ERROR_MESSAGES.SERVER_STATUS_LOAD_FAILED, error);
232 |           sendResponse({
233 |             success: false,
234 |             error: ERROR_MESSAGES.SERVER_STATUS_LOAD_FAILED,
235 |             serverStatus: currentServerStatus,
236 |             connected: nativePort !== null,
237 |           });
238 |         });
239 |       return true;
240 |     }
241 | 
242 |     // Forward file operation messages to native host
243 |     if (message.type === 'forward_to_native' && message.message) {
244 |       if (nativePort) {
245 |         nativePort.postMessage(message.message);
246 |         sendResponse({ success: true });
247 |       } else {
248 |         sendResponse({ success: false, error: 'Native host not connected' });
249 |       }
250 |       return true;
251 |     }
252 |   });
253 | };
254 | 
```

--------------------------------------------------------------------------------
/app/native-server/src/scripts/browser-config.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as fs from 'fs';
  2 | import * as os from 'os';
  3 | import * as path from 'path';
  4 | import { execSync } from 'child_process';
  5 | import { HOST_NAME } from './constant';
  6 | 
  7 | export enum BrowserType {
  8 |   CHROME = 'chrome',
  9 |   CHROMIUM = 'chromium',
 10 | }
 11 | 
 12 | export interface BrowserConfig {
 13 |   type: BrowserType;
 14 |   displayName: string;
 15 |   userManifestPath: string;
 16 |   systemManifestPath: string;
 17 |   registryKey?: string; // Windows only
 18 |   systemRegistryKey?: string; // Windows only
 19 | }
 20 | 
 21 | /**
 22 |  * Get the user-level manifest path for a specific browser
 23 |  */
 24 | function getUserManifestPathForBrowser(browser: BrowserType): string {
 25 |   const platform = os.platform();
 26 | 
 27 |   if (platform === 'win32') {
 28 |     const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
 29 |     switch (browser) {
 30 |       case BrowserType.CHROME:
 31 |         return path.join(appData, 'Google', 'Chrome', 'NativeMessagingHosts', `${HOST_NAME}.json`);
 32 |       case BrowserType.CHROMIUM:
 33 |         return path.join(appData, 'Chromium', 'NativeMessagingHosts', `${HOST_NAME}.json`);
 34 |       default:
 35 |         return path.join(appData, 'Google', 'Chrome', 'NativeMessagingHosts', `${HOST_NAME}.json`);
 36 |     }
 37 |   } else if (platform === 'darwin') {
 38 |     const home = os.homedir();
 39 |     switch (browser) {
 40 |       case BrowserType.CHROME:
 41 |         return path.join(
 42 |           home,
 43 |           'Library',
 44 |           'Application Support',
 45 |           'Google',
 46 |           'Chrome',
 47 |           'NativeMessagingHosts',
 48 |           `${HOST_NAME}.json`,
 49 |         );
 50 |       case BrowserType.CHROMIUM:
 51 |         return path.join(
 52 |           home,
 53 |           'Library',
 54 |           'Application Support',
 55 |           'Chromium',
 56 |           'NativeMessagingHosts',
 57 |           `${HOST_NAME}.json`,
 58 |         );
 59 |       default:
 60 |         return path.join(
 61 |           home,
 62 |           'Library',
 63 |           'Application Support',
 64 |           'Google',
 65 |           'Chrome',
 66 |           'NativeMessagingHosts',
 67 |           `${HOST_NAME}.json`,
 68 |         );
 69 |     }
 70 |   } else {
 71 |     // Linux
 72 |     const home = os.homedir();
 73 |     switch (browser) {
 74 |       case BrowserType.CHROME:
 75 |         return path.join(
 76 |           home,
 77 |           '.config',
 78 |           'google-chrome',
 79 |           'NativeMessagingHosts',
 80 |           `${HOST_NAME}.json`,
 81 |         );
 82 |       case BrowserType.CHROMIUM:
 83 |         return path.join(home, '.config', 'chromium', 'NativeMessagingHosts', `${HOST_NAME}.json`);
 84 |       default:
 85 |         return path.join(
 86 |           home,
 87 |           '.config',
 88 |           'google-chrome',
 89 |           'NativeMessagingHosts',
 90 |           `${HOST_NAME}.json`,
 91 |         );
 92 |     }
 93 |   }
 94 | }
 95 | 
 96 | /**
 97 |  * Get the system-level manifest path for a specific browser
 98 |  */
 99 | function getSystemManifestPathForBrowser(browser: BrowserType): string {
100 |   const platform = os.platform();
101 | 
102 |   if (platform === 'win32') {
103 |     const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
104 |     switch (browser) {
105 |       case BrowserType.CHROME:
106 |         return path.join(
107 |           programFiles,
108 |           'Google',
109 |           'Chrome',
110 |           'NativeMessagingHosts',
111 |           `${HOST_NAME}.json`,
112 |         );
113 |       case BrowserType.CHROMIUM:
114 |         return path.join(programFiles, 'Chromium', 'NativeMessagingHosts', `${HOST_NAME}.json`);
115 |       default:
116 |         return path.join(
117 |           programFiles,
118 |           'Google',
119 |           'Chrome',
120 |           'NativeMessagingHosts',
121 |           `${HOST_NAME}.json`,
122 |         );
123 |     }
124 |   } else if (platform === 'darwin') {
125 |     switch (browser) {
126 |       case BrowserType.CHROME:
127 |         return path.join(
128 |           '/Library',
129 |           'Google',
130 |           'Chrome',
131 |           'NativeMessagingHosts',
132 |           `${HOST_NAME}.json`,
133 |         );
134 |       case BrowserType.CHROMIUM:
135 |         return path.join(
136 |           '/Library',
137 |           'Application Support',
138 |           'Chromium',
139 |           'NativeMessagingHosts',
140 |           `${HOST_NAME}.json`,
141 |         );
142 |       default:
143 |         return path.join(
144 |           '/Library',
145 |           'Google',
146 |           'Chrome',
147 |           'NativeMessagingHosts',
148 |           `${HOST_NAME}.json`,
149 |         );
150 |     }
151 |   } else {
152 |     // Linux
153 |     switch (browser) {
154 |       case BrowserType.CHROME:
155 |         return path.join('/etc', 'opt', 'chrome', 'native-messaging-hosts', `${HOST_NAME}.json`);
156 |       case BrowserType.CHROMIUM:
157 |         return path.join('/etc', 'chromium', 'native-messaging-hosts', `${HOST_NAME}.json`);
158 |       default:
159 |         return path.join('/etc', 'opt', 'chrome', 'native-messaging-hosts', `${HOST_NAME}.json`);
160 |     }
161 |   }
162 | }
163 | 
164 | /**
165 |  * Get Windows registry keys for a browser
166 |  */
167 | function getRegistryKeys(browser: BrowserType): { user: string; system: string } | undefined {
168 |   if (os.platform() !== 'win32') return undefined;
169 | 
170 |   const browserPaths: Record<BrowserType, { user: string; system: string }> = {
171 |     [BrowserType.CHROME]: {
172 |       user: `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`,
173 |       system: `HKLM\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`,
174 |     },
175 |     [BrowserType.CHROMIUM]: {
176 |       user: `HKCU\\Software\\Chromium\\NativeMessagingHosts\\${HOST_NAME}`,
177 |       system: `HKLM\\Software\\Chromium\\NativeMessagingHosts\\${HOST_NAME}`,
178 |     },
179 |   };
180 | 
181 |   return browserPaths[browser];
182 | }
183 | 
184 | /**
185 |  * Get browser configuration
186 |  */
187 | export function getBrowserConfig(browser: BrowserType): BrowserConfig {
188 |   const registryKeys = getRegistryKeys(browser);
189 | 
190 |   return {
191 |     type: browser,
192 |     displayName: browser.charAt(0).toUpperCase() + browser.slice(1),
193 |     userManifestPath: getUserManifestPathForBrowser(browser),
194 |     systemManifestPath: getSystemManifestPathForBrowser(browser),
195 |     registryKey: registryKeys?.user,
196 |     systemRegistryKey: registryKeys?.system,
197 |   };
198 | }
199 | 
200 | /**
201 |  * Detect installed browsers on the system
202 |  */
203 | export function detectInstalledBrowsers(): BrowserType[] {
204 |   const detectedBrowsers: BrowserType[] = [];
205 |   const platform = os.platform();
206 | 
207 |   if (platform === 'win32') {
208 |     // Check Windows registry for installed browsers
209 |     const browsers: Array<{ type: BrowserType; registryPath: string }> = [
210 |       { type: BrowserType.CHROME, registryPath: 'HKLM\\SOFTWARE\\Google\\Chrome' },
211 |       { type: BrowserType.CHROMIUM, registryPath: 'HKLM\\SOFTWARE\\Chromium' },
212 |     ];
213 | 
214 |     for (const browser of browsers) {
215 |       try {
216 |         execSync(`reg query "${browser.registryPath}" 2>nul`, { stdio: 'pipe' });
217 |         detectedBrowsers.push(browser.type);
218 |       } catch {
219 |         // Browser not installed
220 |       }
221 |     }
222 |   } else if (platform === 'darwin') {
223 |     // Check macOS Applications folder
224 |     const browsers: Array<{ type: BrowserType; appPath: string }> = [
225 |       { type: BrowserType.CHROME, appPath: '/Applications/Google Chrome.app' },
226 |       { type: BrowserType.CHROMIUM, appPath: '/Applications/Chromium.app' },
227 |     ];
228 | 
229 |     for (const browser of browsers) {
230 |       if (fs.existsSync(browser.appPath)) {
231 |         detectedBrowsers.push(browser.type);
232 |       }
233 |     }
234 |   } else {
235 |     // Check Linux paths using which command
236 |     const browsers: Array<{ type: BrowserType; commands: string[] }> = [
237 |       { type: BrowserType.CHROME, commands: ['google-chrome', 'google-chrome-stable'] },
238 |       { type: BrowserType.CHROMIUM, commands: ['chromium', 'chromium-browser'] },
239 |     ];
240 | 
241 |     for (const browser of browsers) {
242 |       for (const cmd of browser.commands) {
243 |         try {
244 |           execSync(`which ${cmd} 2>/dev/null`, { stdio: 'pipe' });
245 |           detectedBrowsers.push(browser.type);
246 |           break; // Found one command, no need to check others
247 |         } catch {
248 |           // Command not found
249 |         }
250 |       }
251 |     }
252 |   }
253 | 
254 |   return detectedBrowsers;
255 | }
256 | 
257 | /**
258 |  * Get all supported browser configs
259 |  */
260 | export function getAllBrowserConfigs(): BrowserConfig[] {
261 |   return Object.values(BrowserType).map((browser) => getBrowserConfig(browser));
262 | }
263 | 
264 | /**
265 |  * Parse browser type from string
266 |  */
267 | export function parseBrowserType(browserStr: string): BrowserType | undefined {
268 |   const normalized = browserStr.toLowerCase();
269 |   return Object.values(BrowserType).find((type) => type === normalized);
270 | }
271 | 
```

--------------------------------------------------------------------------------
/packages/wasm-simd/src/lib.rs:
--------------------------------------------------------------------------------

```rust
  1 | use wasm_bindgen::prelude::*;
  2 | use wide::f32x4;
  3 | 
  4 | // 设置 panic hook 以便在浏览器中调试
  5 | #[wasm_bindgen(start)]
  6 | pub fn main() {
  7 |     console_error_panic_hook::set_once();
  8 | }
  9 | 
 10 | #[wasm_bindgen]
 11 | pub struct SIMDMath;
 12 | 
 13 | #[wasm_bindgen]
 14 | impl SIMDMath {
 15 |     #[wasm_bindgen(constructor)]
 16 |     pub fn new() -> SIMDMath {
 17 |         SIMDMath
 18 |     }
 19 | 
 20 |     // 辅助函数:仅计算点积 (SIMD)
 21 |     #[inline]
 22 |     fn dot_product_simd_only(&self, vec_a: &[f32], vec_b: &[f32]) -> f32 {
 23 |         let len = vec_a.len();
 24 |         let simd_lanes = 4;
 25 |         let simd_len = len - (len % simd_lanes);
 26 |         let mut dot_sum_simd = f32x4::ZERO;
 27 | 
 28 |         for i in (0..simd_len).step_by(simd_lanes) {
 29 |             // 使用 try_from 和 new 方法,这是 wide 库的正确 API
 30 |             let a_array: [f32; 4] = vec_a[i..i + simd_lanes].try_into().unwrap();
 31 |             let b_array: [f32; 4] = vec_b[i..i + simd_lanes].try_into().unwrap();
 32 |             let a_chunk = f32x4::new(a_array);
 33 |             let b_chunk = f32x4::new(b_array);
 34 |             dot_sum_simd = a_chunk.mul_add(b_chunk, dot_sum_simd);
 35 |         }
 36 | 
 37 |         let mut dot_product = dot_sum_simd.reduce_add();
 38 |         for i in simd_len..len {
 39 |             dot_product += vec_a[i] * vec_b[i];
 40 |         }
 41 |         dot_product
 42 |     }
 43 | 
 44 |     #[wasm_bindgen]
 45 |     pub fn cosine_similarity(&self, vec_a: &[f32], vec_b: &[f32]) -> f32 {
 46 |         if vec_a.len() != vec_b.len() || vec_a.is_empty() {
 47 |             return 0.0;
 48 |         }
 49 | 
 50 |         let len = vec_a.len();
 51 |         let simd_lanes = 4;
 52 |         let simd_len = len - (len % simd_lanes);
 53 | 
 54 |         let mut dot_sum_simd = f32x4::ZERO;
 55 |         let mut norm_a_sum_simd = f32x4::ZERO;
 56 |         let mut norm_b_sum_simd = f32x4::ZERO;
 57 | 
 58 |         // SIMD 处理
 59 |         for i in (0..simd_len).step_by(simd_lanes) {
 60 |             let a_array: [f32; 4] = vec_a[i..i + simd_lanes].try_into().unwrap();
 61 |             let b_array: [f32; 4] = vec_b[i..i + simd_lanes].try_into().unwrap();
 62 |             let a_chunk = f32x4::new(a_array);
 63 |             let b_chunk = f32x4::new(b_array);
 64 | 
 65 |             // 使用 Fused Multiply-Add (FMA)
 66 |             dot_sum_simd = a_chunk.mul_add(b_chunk, dot_sum_simd);
 67 |             norm_a_sum_simd = a_chunk.mul_add(a_chunk, norm_a_sum_simd);
 68 |             norm_b_sum_simd = b_chunk.mul_add(b_chunk, norm_b_sum_simd);
 69 |         }
 70 | 
 71 |         // 水平求和
 72 |         let mut dot_product = dot_sum_simd.reduce_add();
 73 |         let mut norm_a_sq = norm_a_sum_simd.reduce_add();
 74 |         let mut norm_b_sq = norm_b_sum_simd.reduce_add();
 75 | 
 76 |         // 处理剩余元素
 77 |         for i in simd_len..len {
 78 |             dot_product += vec_a[i] * vec_b[i];
 79 |             norm_a_sq += vec_a[i] * vec_a[i];
 80 |             norm_b_sq += vec_b[i] * vec_b[i];
 81 |         }
 82 | 
 83 |         // 优化的数值稳定性处理
 84 |         let norm_a = norm_a_sq.sqrt();
 85 |         let norm_b = norm_b_sq.sqrt();
 86 | 
 87 |         if norm_a == 0.0 || norm_b == 0.0 {
 88 |             return 0.0;
 89 |         }
 90 | 
 91 |         let magnitude = norm_a * norm_b;
 92 |         // 限制结果在 [-1.0, 1.0] 范围内,处理浮点精度误差
 93 |         (dot_product / magnitude).max(-1.0).min(1.0)
 94 |     }
 95 | 
 96 |     #[wasm_bindgen]
 97 |     pub fn batch_similarity(&self, vectors: &[f32], query: &[f32], vector_dim: usize) -> Vec<f32> {
 98 |         if vector_dim == 0 { return Vec::new(); }
 99 |         if vectors.len() % vector_dim != 0 { return Vec::new(); }
100 |         if query.len() != vector_dim { return Vec::new(); }
101 | 
102 |         let num_vectors = vectors.len() / vector_dim;
103 |         let mut results = Vec::with_capacity(num_vectors);
104 | 
105 |         // 预计算查询向量的范数
106 |         let query_norm_sq = self.compute_norm_squared_simd(query);
107 |         if query_norm_sq == 0.0 {
108 |             return vec![0.0; num_vectors];
109 |         }
110 |         let query_norm = query_norm_sq.sqrt();
111 | 
112 |         for i in 0..num_vectors {
113 |             let start = i * vector_dim;
114 |             let vector_slice = &vectors[start..start + vector_dim];
115 | 
116 |             // dot_product_and_norm_simd 计算 vector_slice (vec_a) 的范数
117 |             let (dot_product, vector_norm_sq) = self.dot_product_and_norm_simd(vector_slice, query);
118 | 
119 |             if vector_norm_sq == 0.0 {
120 |                 results.push(0.0);
121 |             } else {
122 |                 let vector_norm = vector_norm_sq.sqrt();
123 |                 let similarity = dot_product / (vector_norm * query_norm);
124 |                 results.push(similarity.max(-1.0).min(1.0));
125 |             }
126 |         }
127 |         results
128 |     }
129 | 
130 |     // 辅助函数:SIMD 计算范数平方
131 |     #[inline]
132 |     fn compute_norm_squared_simd(&self, vec: &[f32]) -> f32 {
133 |         let len = vec.len();
134 |         let simd_lanes = 4;
135 |         let simd_len = len - (len % simd_lanes);
136 |         let mut norm_sum_simd = f32x4::ZERO;
137 | 
138 |         for i in (0..simd_len).step_by(simd_lanes) {
139 |             let array: [f32; 4] = vec[i..i + simd_lanes].try_into().unwrap();
140 |             let chunk = f32x4::new(array);
141 |             norm_sum_simd = chunk.mul_add(chunk, norm_sum_simd);
142 |         }
143 | 
144 |         let mut norm_sq = norm_sum_simd.reduce_add();
145 |         for i in simd_len..len {
146 |             norm_sq += vec[i] * vec[i];
147 |         }
148 |         norm_sq
149 |     }
150 | 
151 |     // 辅助函数:同时计算点积和vec_a的范数平方
152 |     #[inline]
153 |     fn dot_product_and_norm_simd(&self, vec_a: &[f32], vec_b: &[f32]) -> (f32, f32) {
154 |         let len = vec_a.len(); // 假设 vec_a.len() == vec_b.len()
155 |         let simd_lanes = 4;
156 |         let simd_len = len - (len % simd_lanes);
157 | 
158 |         let mut dot_sum_simd = f32x4::ZERO;
159 |         let mut norm_a_sum_simd = f32x4::ZERO;
160 | 
161 |         for i in (0..simd_len).step_by(simd_lanes) {
162 |             let a_array: [f32; 4] = vec_a[i..i + simd_lanes].try_into().unwrap();
163 |             let b_array: [f32; 4] = vec_b[i..i + simd_lanes].try_into().unwrap();
164 |             let a_chunk = f32x4::new(a_array);
165 |             let b_chunk = f32x4::new(b_array);
166 | 
167 |             dot_sum_simd = a_chunk.mul_add(b_chunk, dot_sum_simd);
168 |             norm_a_sum_simd = a_chunk.mul_add(a_chunk, norm_a_sum_simd);
169 |         }
170 | 
171 |         let mut dot_product = dot_sum_simd.reduce_add();
172 |         let mut norm_a_sq = norm_a_sum_simd.reduce_add();
173 | 
174 |         for i in simd_len..len {
175 |             dot_product += vec_a[i] * vec_b[i];
176 |             norm_a_sq += vec_a[i] * vec_a[i];
177 |         }
178 |         (dot_product, norm_a_sq)
179 |     }
180 | 
181 |     // 批量矩阵相似度计算 - 优化版
182 |     #[wasm_bindgen]
183 |     pub fn similarity_matrix(&self, vectors_a: &[f32], vectors_b: &[f32], vector_dim: usize) -> Vec<f32> {
184 |         if vector_dim == 0 || vectors_a.len() % vector_dim != 0 || vectors_b.len() % vector_dim != 0 {
185 |             return Vec::new();
186 |         }
187 | 
188 |         let num_a = vectors_a.len() / vector_dim;
189 |         let num_b = vectors_b.len() / vector_dim;
190 |         let mut results = Vec::with_capacity(num_a * num_b);
191 | 
192 |         // 1. 预计算 vectors_a 的范数
193 |         let norms_a: Vec<f32> = (0..num_a)
194 |             .map(|i| {
195 |                 let start = i * vector_dim;
196 |                 let vec_a_slice = &vectors_a[start..start + vector_dim];
197 |                 self.compute_norm_squared_simd(vec_a_slice).sqrt()
198 |             })
199 |             .collect();
200 | 
201 |         // 2. 预计算 vectors_b 的范数
202 |         let norms_b: Vec<f32> = (0..num_b)
203 |             .map(|j| {
204 |                 let start = j * vector_dim;
205 |                 let vec_b_slice = &vectors_b[start..start + vector_dim];
206 |                 self.compute_norm_squared_simd(vec_b_slice).sqrt()
207 |             })
208 |             .collect();
209 | 
210 |         for i in 0..num_a {
211 |             let start_a = i * vector_dim;
212 |             let vec_a = &vectors_a[start_a..start_a + vector_dim];
213 |             let norm_a = norms_a[i];
214 | 
215 |             if norm_a == 0.0 {
216 |                 // 如果 norm_a 为 0,所有相似度都为 0
217 |                 for _ in 0..num_b {
218 |                     results.push(0.0);
219 |                 }
220 |                 continue;
221 |             }
222 | 
223 |             for j in 0..num_b {
224 |                 let start_b = j * vector_dim;
225 |                 let vec_b = &vectors_b[start_b..start_b + vector_dim];
226 |                 let norm_b = norms_b[j];
227 | 
228 |                 if norm_b == 0.0 {
229 |                     results.push(0.0);
230 |                     continue;
231 |                 }
232 | 
233 |                 // 使用专用的点积函数
234 |                 let dot_product = self.dot_product_simd_only(vec_a, vec_b);
235 |                 let magnitude = norm_a * norm_b;
236 | 
237 |                 // magnitude 不应该为零,因为已经检查了 norm_a/norm_b
238 |                 let similarity = (dot_product / magnitude).max(-1.0).min(1.0);
239 |                 results.push(similarity);
240 |             }
241 |         }
242 | 
243 |         results
244 |     }
245 | }
246 | 
```

--------------------------------------------------------------------------------
/test-inject-script.js:
--------------------------------------------------------------------------------

```javascript
  1 | (() => {
  2 |   const SCRIPT_ID = 'excalidraw-control-script';
  3 |   if (window[SCRIPT_ID]) {
  4 |     return;
  5 |   }
  6 |   function getExcalidrawAPIFromDOM(domElement) {
  7 |     if (!domElement) {
  8 |       return null;
  9 |     }
 10 |     const reactFiberKey = Object.keys(domElement).find(
 11 |       (key) => key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$'),
 12 |     );
 13 |     if (!reactFiberKey) {
 14 |       return null;
 15 |     }
 16 |     let fiberNode = domElement[reactFiberKey];
 17 |     if (!fiberNode) {
 18 |       return null;
 19 |     }
 20 |     function isExcalidrawAPI(obj) {
 21 |       return (
 22 |         typeof obj === 'object' &&
 23 |         obj !== null &&
 24 |         typeof obj.updateScene === 'function' &&
 25 |         typeof obj.getSceneElements === 'function' &&
 26 |         typeof obj.getAppState === 'function'
 27 |       );
 28 |     }
 29 |     function findApiInObject(objToSearch) {
 30 |       if (isExcalidrawAPI(objToSearch)) {
 31 |         return objToSearch;
 32 |       }
 33 |       if (typeof objToSearch === 'object' && objToSearch !== null) {
 34 |         for (const key in objToSearch) {
 35 |           if (Object.prototype.hasOwnProperty.call(objToSearch, key)) {
 36 |             const found = findApiInObject(objToSearch[key]);
 37 |             if (found) {
 38 |               return found;
 39 |             }
 40 |           }
 41 |         }
 42 |       }
 43 |       return null;
 44 |     }
 45 |     let excalidrawApiInstance = null;
 46 |     let attempts = 0;
 47 |     const MAX_TRAVERSAL_ATTEMPTS = 25;
 48 |     while (fiberNode && attempts < MAX_TRAVERSAL_ATTEMPTS) {
 49 |       if (fiberNode.stateNode && fiberNode.stateNode.props) {
 50 |         const api = findApiInObject(fiberNode.stateNode.props);
 51 |         if (api) {
 52 |           excalidrawApiInstance = api;
 53 |           break;
 54 |         }
 55 |         if (isExcalidrawAPI(fiberNode.stateNode.props.excalidrawAPI)) {
 56 |           excalidrawApiInstance = fiberNode.stateNode.props.excalidrawAPI;
 57 |           break;
 58 |         }
 59 |       }
 60 |       if (fiberNode.memoizedProps) {
 61 |         const api = findApiInObject(fiberNode.memoizedProps);
 62 |         if (api) {
 63 |           excalidrawApiInstance = api;
 64 |           break;
 65 |         }
 66 |         if (isExcalidrawAPI(fiberNode.memoizedProps.excalidrawAPI)) {
 67 |           excalidrawApiInstance = fiberNode.memoizedProps.excalidrawAPI;
 68 |           break;
 69 |         }
 70 |       }
 71 | 
 72 |       if (fiberNode.tag === 1 && fiberNode.stateNode && fiberNode.stateNode.state) {
 73 |         const api = findApiInObject(fiberNode.stateNode.state);
 74 |         if (api) {
 75 |           excalidrawApiInstance = api;
 76 |           break;
 77 |         }
 78 |       }
 79 | 
 80 |       if (
 81 |         fiberNode.tag === 0 ||
 82 |         fiberNode.tag === 2 ||
 83 |         fiberNode.tag === 14 ||
 84 |         fiberNode.tag === 15 ||
 85 |         fiberNode.tag === 11
 86 |       ) {
 87 |         if (fiberNode.memoizedState) {
 88 |           let currentHook = fiberNode.memoizedState;
 89 |           let hookAttempts = 0;
 90 |           const MAX_HOOK_ATTEMPTS = 15;
 91 |           while (currentHook && hookAttempts < MAX_HOOK_ATTEMPTS) {
 92 |             const api = findApiInObject(currentHook.memoizedState);
 93 |             if (api) {
 94 |               excalidrawApiInstance = api;
 95 |               break;
 96 |             }
 97 |             currentHook = currentHook.next;
 98 |             hookAttempts++;
 99 |           }
100 |           if (excalidrawApiInstance) break;
101 |         }
102 |       }
103 |       if (fiberNode.stateNode) {
104 |         const api = findApiInObject(fiberNode.stateNode);
105 |         if (api && api !== fiberNode.stateNode.props && api !== fiberNode.stateNode.state) {
106 |           excalidrawApiInstance = api;
107 |           break;
108 |         }
109 |       }
110 |       if (
111 |         fiberNode.tag === 9 &&
112 |         fiberNode.memoizedProps &&
113 |         typeof fiberNode.memoizedProps.value !== 'undefined'
114 |       ) {
115 |         const api = findApiInObject(fiberNode.memoizedProps.value);
116 |         if (api) {
117 |           excalidrawApiInstance = api;
118 |           break;
119 |         }
120 |       }
121 | 
122 |       if (fiberNode.return) {
123 |         fiberNode = fiberNode.return;
124 |       } else {
125 |         break;
126 |       }
127 |       attempts++;
128 |     }
129 | 
130 |     if (excalidrawApiInstance) {
131 |       window.excalidrawAPI = excalidrawApiInstance;
132 |       console.log('现在您可以通过 `window.foundExcalidrawAPI` 在控制台访问它。');
133 |     } else {
134 |       console.error('在检查组件树后未能找到 excalidrawAPI。');
135 |     }
136 |     return excalidrawApiInstance;
137 |   }
138 | 
139 |   function createFullExcalidrawElement(skeleton) {
140 |     const id = Math.random().toString(36).substring(2, 9);
141 | 
142 |     const seed = Math.floor(Math.random() * 2 ** 31);
143 |     const versionNonce = Math.floor(Math.random() * 2 ** 31);
144 | 
145 |     const defaults = {
146 |       isDeleted: false,
147 |       fillStyle: 'hachure',
148 |       strokeWidth: 1,
149 |       strokeStyle: 'solid',
150 |       roughness: 1,
151 |       opacity: 100,
152 |       angle: 0,
153 |       groupIds: [],
154 |       strokeColor: '#000000',
155 |       backgroundColor: 'transparent',
156 |       version: 1,
157 |       locked: false,
158 |     };
159 | 
160 |     const fullElement = {
161 |       id: id,
162 |       seed: seed,
163 |       versionNonce: versionNonce,
164 |       updated: Date.now(),
165 |       ...defaults,
166 |       ...skeleton,
167 |     };
168 | 
169 |     return fullElement;
170 |   }
171 | 
172 |   let targetElementForAPI = document.querySelector('.excalidraw-app');
173 | 
174 |   if (targetElementForAPI) {
175 |     getExcalidrawAPIFromDOM(targetElementForAPI);
176 |   }
177 | 
178 |   const eventHandler = {
179 |     getSceneElements: () => {
180 |       try {
181 |         return window.excalidrawAPI.getSceneElements();
182 |       } catch (error) {
183 |         return {
184 |           error: true,
185 |           msg: JSON.stringify(error),
186 |         };
187 |       }
188 |     },
189 |     addElement: (param) => {
190 |       try {
191 |         const existingElements = window.excalidrawAPI.getSceneElements();
192 |         const newElements = [...existingElements];
193 |         param.eles.forEach((ele, idx) => {
194 |           const newEle = createFullExcalidrawElement(ele);
195 |           newEle.index = `a${existingElements.length + idx + 1}`;
196 |           newElements.push(newEle);
197 |         });
198 |         console.log('newElements ==>', newElements);
199 |         const appState = window.excalidrawAPI.getAppState();
200 |         window.excalidrawAPI.updateScene({
201 |           elements: newElements,
202 |           appState: appState,
203 |           commitToHistory: true,
204 |         });
205 |         return {
206 |           success: true,
207 |         };
208 |       } catch (error) {
209 |         return {
210 |           error: true,
211 |           msg: JSON.stringify(error),
212 |         };
213 |       }
214 |     },
215 |     deleteElement: (param) => {
216 |       try {
217 |         const existingElements = window.excalidrawAPI.getSceneElements();
218 |         const newElements = [...existingElements];
219 |         const idx = newElements.findIndex((e) => e.id === param.id);
220 |         if (idx >= 0) {
221 |           newElements.splice(idx, 1);
222 |           const appState = window.excalidrawAPI.getAppState();
223 |           window.excalidrawAPI.updateScene({
224 |             elements: newElements,
225 |             appState: appState,
226 |             commitToHistory: true,
227 |           });
228 |           return {
229 |             success: true,
230 |           };
231 |         } else {
232 |           return {
233 |             error: true,
234 |             msg: 'element not found',
235 |           };
236 |         }
237 |       } catch (error) {
238 |         return {
239 |           error: true,
240 |           msg: JSON.stringify(error),
241 |         };
242 |       }
243 |     },
244 |     updateElement: (param) => {
245 |       try {
246 |         const existingElements = window.excalidrawAPI.getSceneElements();
247 |         const resIds = [];
248 |         for (let i = 0; i < param.length; i++) {
249 |           const idx = existingElements.findIndex((e) => e.id === param[i].id);
250 |           if (idx >= 0) {
251 |             resIds.push[idx];
252 |             window.excalidrawAPI.mutateElement(existingElements[idx], { ...param[i] });
253 |           }
254 |         }
255 |         return {
256 |           success: true,
257 |           msg: `已更新元素:${resIds.join(',')}`,
258 |         };
259 |       } catch (error) {
260 |         return {
261 |           error: true,
262 |           msg: JSON.stringify(error),
263 |         };
264 |       }
265 |     },
266 |     cleanup: () => {
267 |       try {
268 |         window.excalidrawAPI.resetScene();
269 |         return {
270 |           success: true,
271 |         };
272 |       } catch (error) {
273 |         return {
274 |           error: true,
275 |           msg: JSON.stringify(error),
276 |         };
277 |       }
278 |     },
279 |   };
280 | 
281 |   const handleExecution = (event) => {
282 |     const { action, payload, requestId } = event.detail;
283 |     const param = JSON.parse(payload || '{}');
284 |     let data, error;
285 |     try {
286 |       const handler = eventHandler[action];
287 |       if (!handler) {
288 |         error = 'event name not found';
289 |       }
290 |       data = handler(param);
291 |     } catch (e) {
292 |       error = e.message;
293 |     }
294 |     window.dispatchEvent(
295 |       new CustomEvent('chrome-mcp:response', { detail: { requestId, data, error } }),
296 |     );
297 |   };
298 | 
299 |   // --- Lifecycle Functions ---
300 |   const initialize = () => {
301 |     window.addEventListener('chrome-mcp:execute', handleExecution);
302 |     window.addEventListener('chrome-mcp:cleanup', cleanup);
303 |     window[SCRIPT_ID] = true;
304 |   };
305 | 
306 |   const cleanup = () => {
307 |     window.removeEventListener('chrome-mcp:execute', handleExecution);
308 |     window.removeEventListener('chrome-mcp:cleanup', cleanup);
309 |     delete window[SCRIPT_ID];
310 |     delete window.excalidrawAPI;
311 |   };
312 | 
313 |   initialize();
314 | })();
315 | 
```

--------------------------------------------------------------------------------
/app/native-server/src/scripts/postinstall.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import fs from 'fs';
  4 | import os from 'os';
  5 | import path from 'path';
  6 | import { COMMAND_NAME } from './constant';
  7 | import { colorText, tryRegisterUserLevelHost } from './utils';
  8 | 
  9 | // Check if this script is run directly
 10 | const isDirectRun = require.main === module;
 11 | 
 12 | // Detect global installation for both npm and pnpm
 13 | function detectGlobalInstall(): boolean {
 14 |   // npm uses npm_config_global
 15 |   if (process.env.npm_config_global === 'true') {
 16 |     return true;
 17 |   }
 18 | 
 19 |   // pnpm detection methods
 20 |   // Method 1: Check if PNPM_HOME is set and current path contains it
 21 |   if (process.env.PNPM_HOME && __dirname.includes(process.env.PNPM_HOME)) {
 22 |     return true;
 23 |   }
 24 | 
 25 |   // Method 2: Check if we're in a global pnpm directory structure
 26 |   // pnpm global packages are typically installed in ~/.local/share/pnpm/global/5/node_modules
 27 |   // Windows: %APPDATA%\pnpm\global\5\node_modules
 28 |   const globalPnpmPatterns =
 29 |     process.platform === 'win32'
 30 |       ? ['\\pnpm\\global\\', '\\pnpm-global\\', '\\AppData\\Roaming\\pnpm\\']
 31 |       : ['/pnpm/global/', '/.local/share/pnpm/', '/pnpm-global/'];
 32 | 
 33 |   if (globalPnpmPatterns.some((pattern) => __dirname.includes(pattern))) {
 34 |     return true;
 35 |   }
 36 | 
 37 |   // Method 3: Check npm_config_prefix for pnpm
 38 |   if (process.env.npm_config_prefix && __dirname.includes(process.env.npm_config_prefix)) {
 39 |     return true;
 40 |   }
 41 | 
 42 |   // Method 4: Windows-specific global installation paths
 43 |   if (process.platform === 'win32') {
 44 |     const windowsGlobalPatterns = [
 45 |       '\\npm\\node_modules\\',
 46 |       '\\AppData\\Roaming\\npm\\node_modules\\',
 47 |       '\\Program Files\\nodejs\\node_modules\\',
 48 |       '\\nodejs\\node_modules\\',
 49 |     ];
 50 | 
 51 |     if (windowsGlobalPatterns.some((pattern) => __dirname.includes(pattern))) {
 52 |       return true;
 53 |     }
 54 |   }
 55 | 
 56 |   return false;
 57 | }
 58 | 
 59 | const isGlobalInstall = detectGlobalInstall();
 60 | 
 61 | /**
 62 |  * Write Node.js path for run_host scripts to avoid fragile relative paths
 63 |  */
 64 | async function writeNodePath(): Promise<void> {
 65 |   try {
 66 |     const nodePath = process.execPath;
 67 |     const nodePathFile = path.join(__dirname, '..', 'node_path.txt');
 68 | 
 69 |     console.log(colorText(`Writing Node.js path: ${nodePath}`, 'blue'));
 70 |     fs.writeFileSync(nodePathFile, nodePath, 'utf8');
 71 |     console.log(colorText('✓ Node.js path written for run_host scripts', 'green'));
 72 |   } catch (error: any) {
 73 |     console.warn(colorText(`⚠️ Failed to write Node.js path: ${error.message}`, 'yellow'));
 74 |   }
 75 | }
 76 | 
 77 | /**
 78 |  * 确保执行权限(无论是否为全局安装)
 79 |  */
 80 | async function ensureExecutionPermissions(): Promise<void> {
 81 |   if (process.platform === 'win32') {
 82 |     // Windows 平台处理
 83 |     await ensureWindowsFilePermissions();
 84 |     return;
 85 |   }
 86 | 
 87 |   // Unix/Linux 平台处理
 88 |   const filesToCheck = [
 89 |     path.join(__dirname, '..', 'index.js'),
 90 |     path.join(__dirname, '..', 'run_host.sh'),
 91 |     path.join(__dirname, '..', 'cli.js'),
 92 |   ];
 93 | 
 94 |   for (const filePath of filesToCheck) {
 95 |     if (fs.existsSync(filePath)) {
 96 |       try {
 97 |         fs.chmodSync(filePath, '755');
 98 |         console.log(
 99 |           colorText(`✓ Set execution permissions for ${path.basename(filePath)}`, 'green'),
100 |         );
101 |       } catch (err: any) {
102 |         console.warn(
103 |           colorText(
104 |             `⚠️ Unable to set execution permissions for ${path.basename(filePath)}: ${err.message}`,
105 |             'yellow',
106 |           ),
107 |         );
108 |       }
109 |     } else {
110 |       console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
111 |     }
112 |   }
113 | }
114 | 
115 | /**
116 |  * Windows 平台文件权限处理
117 |  */
118 | async function ensureWindowsFilePermissions(): Promise<void> {
119 |   const filesToCheck = [
120 |     path.join(__dirname, '..', 'index.js'),
121 |     path.join(__dirname, '..', 'run_host.bat'),
122 |     path.join(__dirname, '..', 'cli.js'),
123 |   ];
124 | 
125 |   for (const filePath of filesToCheck) {
126 |     if (fs.existsSync(filePath)) {
127 |       try {
128 |         // 检查文件是否为只读,如果是则移除只读属性
129 |         const stats = fs.statSync(filePath);
130 |         if (!(stats.mode & parseInt('200', 8))) {
131 |           // 检查写权限
132 |           // 尝试移除只读属性
133 |           fs.chmodSync(filePath, stats.mode | parseInt('200', 8));
134 |           console.log(
135 |             colorText(`✓ Removed read-only attribute from ${path.basename(filePath)}`, 'green'),
136 |           );
137 |         }
138 | 
139 |         // 验证文件可读性
140 |         fs.accessSync(filePath, fs.constants.R_OK);
141 |         console.log(
142 |           colorText(`✓ Verified file accessibility for ${path.basename(filePath)}`, 'green'),
143 |         );
144 |       } catch (err: any) {
145 |         console.warn(
146 |           colorText(
147 |             `⚠️ Unable to verify file permissions for ${path.basename(filePath)}: ${err.message}`,
148 |             'yellow',
149 |           ),
150 |         );
151 |       }
152 |     } else {
153 |       console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
154 |     }
155 |   }
156 | }
157 | 
158 | async function tryRegisterNativeHost(): Promise<void> {
159 |   try {
160 |     console.log(colorText('Attempting to register Chrome Native Messaging host...', 'blue'));
161 | 
162 |     // Always ensure execution permissions, regardless of installation type
163 |     await ensureExecutionPermissions();
164 | 
165 |     if (isGlobalInstall) {
166 |       // First try user-level installation (no elevated permissions required)
167 |       const userLevelSuccess = await tryRegisterUserLevelHost();
168 | 
169 |       if (!userLevelSuccess) {
170 |         // User-level installation failed, suggest using register command
171 |         console.log(
172 |           colorText(
173 |             'User-level installation failed, system-level installation may be needed',
174 |             'yellow',
175 |           ),
176 |         );
177 |         console.log(
178 |           colorText('Please run the following command for system-level installation:', 'blue'),
179 |         );
180 |         console.log(`  ${COMMAND_NAME} register --system`);
181 |         printManualInstructions();
182 |       }
183 |     } else {
184 |       // Local installation mode, don't attempt automatic registration
185 |       console.log(
186 |         colorText('Local installation detected, skipping automatic registration', 'yellow'),
187 |       );
188 |       printManualInstructions();
189 |     }
190 |   } catch (error) {
191 |     console.log(
192 |       colorText(
193 |         `注册过程中出现错误: ${error instanceof Error ? error.message : String(error)}`,
194 |         'red',
195 |       ),
196 |     );
197 |     printManualInstructions();
198 |   }
199 | }
200 | 
201 | /**
202 |  * 打印手动安装指南
203 |  */
204 | function printManualInstructions(): void {
205 |   console.log('\n' + colorText('===== Manual Registration Guide =====', 'blue'));
206 | 
207 |   console.log(colorText('1. Try user-level installation (recommended):', 'yellow'));
208 |   if (isGlobalInstall) {
209 |     console.log(`  ${COMMAND_NAME} register`);
210 |   } else {
211 |     console.log(`  npx ${COMMAND_NAME} register`);
212 |   }
213 | 
214 |   console.log(
215 |     colorText('\n2. If user-level installation fails, try system-level installation:', 'yellow'),
216 |   );
217 | 
218 |   console.log(colorText('   Use --system parameter (auto-elevate permissions):', 'yellow'));
219 |   if (isGlobalInstall) {
220 |     console.log(`  ${COMMAND_NAME} register --system`);
221 |   } else {
222 |     console.log(`  npx ${COMMAND_NAME} register --system`);
223 |   }
224 | 
225 |   console.log(colorText('\n   Or use administrator privileges directly:', 'yellow'));
226 |   if (os.platform() === 'win32') {
227 |     console.log(
228 |       colorText(
229 |         '   Please run Command Prompt or PowerShell as administrator and execute:',
230 |         'yellow',
231 |       ),
232 |     );
233 |     if (isGlobalInstall) {
234 |       console.log(`  ${COMMAND_NAME} register`);
235 |     } else {
236 |       console.log(`  npx ${COMMAND_NAME} register`);
237 |     }
238 |   } else {
239 |     console.log(colorText('   Please run the following command in terminal:', 'yellow'));
240 |     if (isGlobalInstall) {
241 |       console.log(`  sudo ${COMMAND_NAME} register`);
242 |     } else {
243 |       console.log(`  sudo npx ${COMMAND_NAME} register`);
244 |     }
245 |   }
246 | 
247 |   console.log(
248 |     '\n' +
249 |       colorText(
250 |         'Ensure Chrome extension is installed and refresh the extension to connect to local service.',
251 |         'blue',
252 |       ),
253 |   );
254 | }
255 | 
256 | /**
257 |  * 主函数
258 |  */
259 | async function main(): Promise<void> {
260 |   console.log(colorText(`Installing ${COMMAND_NAME}...`, 'green'));
261 | 
262 |   // Debug information
263 |   console.log(colorText('Installation environment debug info:', 'blue'));
264 |   console.log(`  __dirname: ${__dirname}`);
265 |   console.log(`  npm_config_global: ${process.env.npm_config_global}`);
266 |   console.log(`  PNPM_HOME: ${process.env.PNPM_HOME}`);
267 |   console.log(`  npm_config_prefix: ${process.env.npm_config_prefix}`);
268 |   console.log(`  isGlobalInstall: ${isGlobalInstall}`);
269 | 
270 |   // Always ensure execution permissions first
271 |   await ensureExecutionPermissions();
272 | 
273 |   // Write Node.js path for run_host scripts to use
274 |   await writeNodePath();
275 | 
276 |   // If global installation, try automatic registration
277 |   if (isGlobalInstall) {
278 |     await tryRegisterNativeHost();
279 |   } else {
280 |     console.log(colorText('Local installation detected', 'yellow'));
281 |     printManualInstructions();
282 |   }
283 | }
284 | 
285 | // Only execute main function when running this script directly
286 | if (isDirectRun) {
287 |   main().catch((error) => {
288 |     console.error(
289 |       colorText(
290 |         `Installation script error: ${error instanceof Error ? error.message : String(error)}`,
291 |         'red',
292 |       ),
293 |     );
294 |   });
295 | }
296 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/vector-search.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Vectorized tab content search tool
  3 |  * Uses vector database for efficient semantic search
  4 |  */
  5 | 
  6 | import { createErrorResponse, ToolResult } from '@/common/tool-handler';
  7 | import { BaseBrowserToolExecutor } from '../base-browser';
  8 | import { TOOL_NAMES } from 'chrome-mcp-shared';
  9 | import { ContentIndexer } from '@/utils/content-indexer';
 10 | import { LIMITS, ERROR_MESSAGES } from '@/common/constants';
 11 | import type { SearchResult } from '@/utils/vector-database';
 12 | 
 13 | interface VectorSearchResult {
 14 |   tabId: number;
 15 |   url: string;
 16 |   title: string;
 17 |   semanticScore: number;
 18 |   matchedSnippet: string;
 19 |   chunkSource: string;
 20 |   timestamp: number;
 21 | }
 22 | 
 23 | /**
 24 |  * Tool for vectorized search of tab content using semantic similarity
 25 |  */
 26 | class VectorSearchTabsContentTool extends BaseBrowserToolExecutor {
 27 |   name = TOOL_NAMES.BROWSER.SEARCH_TABS_CONTENT;
 28 |   private contentIndexer: ContentIndexer;
 29 |   private isInitialized = false;
 30 | 
 31 |   constructor() {
 32 |     super();
 33 |     this.contentIndexer = new ContentIndexer({
 34 |       autoIndex: true,
 35 |       maxChunksPerPage: LIMITS.MAX_SEARCH_RESULTS,
 36 |       skipDuplicates: true,
 37 |     });
 38 |   }
 39 | 
 40 |   private async initializeIndexer(): Promise<void> {
 41 |     try {
 42 |       await this.contentIndexer.initialize();
 43 |       this.isInitialized = true;
 44 |       console.log('VectorSearchTabsContentTool: Content indexer initialized successfully');
 45 |     } catch (error) {
 46 |       console.error('VectorSearchTabsContentTool: Failed to initialize content indexer:', error);
 47 |       this.isInitialized = false;
 48 |     }
 49 |   }
 50 | 
 51 |   async execute(args: { query: string }): Promise<ToolResult> {
 52 |     try {
 53 |       const { query } = args;
 54 | 
 55 |       if (!query || query.trim().length === 0) {
 56 |         return createErrorResponse(
 57 |           ERROR_MESSAGES.INVALID_PARAMETERS + ': Query parameter is required and cannot be empty',
 58 |         );
 59 |       }
 60 | 
 61 |       console.log(`VectorSearchTabsContentTool: Starting vector search with query: "${query}"`);
 62 | 
 63 |       // Check semantic engine status
 64 |       if (!this.contentIndexer.isSemanticEngineReady()) {
 65 |         if (this.contentIndexer.isSemanticEngineInitializing()) {
 66 |           return createErrorResponse(
 67 |             'Vector search engine is still initializing (model downloading). Please wait a moment and try again.',
 68 |           );
 69 |         } else {
 70 |           // Try to initialize
 71 |           console.log('VectorSearchTabsContentTool: Initializing content indexer...');
 72 |           await this.initializeIndexer();
 73 | 
 74 |           // Check semantic engine status again
 75 |           if (!this.contentIndexer.isSemanticEngineReady()) {
 76 |             return createErrorResponse('Failed to initialize vector search engine');
 77 |           }
 78 |         }
 79 |       }
 80 | 
 81 |       // Execute vector search, get more results for deduplication
 82 |       const searchResults = await this.contentIndexer.searchContent(query, 50);
 83 | 
 84 |       // Convert search results format
 85 |       const vectorSearchResults = this.convertSearchResults(searchResults);
 86 | 
 87 |       // Deduplicate by tab, keep only the highest similarity fragment per tab
 88 |       const deduplicatedResults = this.deduplicateByTab(vectorSearchResults);
 89 | 
 90 |       // Sort by similarity and get top 10 results
 91 |       const topResults = deduplicatedResults
 92 |         .sort((a, b) => b.semanticScore - a.semanticScore)
 93 |         .slice(0, 10);
 94 | 
 95 |       // Get index statistics
 96 |       const stats = this.contentIndexer.getStats();
 97 | 
 98 |       const result = {
 99 |         success: true,
100 |         totalTabsSearched: stats.totalTabs,
101 |         matchedTabsCount: topResults.length,
102 |         vectorSearchEnabled: true,
103 |         indexStats: {
104 |           totalDocuments: stats.totalDocuments,
105 |           totalTabs: stats.totalTabs,
106 |           indexedPages: stats.indexedPages,
107 |           semanticEngineReady: stats.semanticEngineReady,
108 |           semanticEngineInitializing: stats.semanticEngineInitializing,
109 |         },
110 |         matchedTabs: topResults.map((result) => ({
111 |           tabId: result.tabId,
112 |           url: result.url,
113 |           title: result.title,
114 |           semanticScore: result.semanticScore,
115 |           matchedSnippets: [result.matchedSnippet],
116 |           chunkSource: result.chunkSource,
117 |           timestamp: result.timestamp,
118 |         })),
119 |       };
120 | 
121 |       console.log(
122 |         `VectorSearchTabsContentTool: Found ${topResults.length} results with vector search`,
123 |       );
124 | 
125 |       return {
126 |         content: [
127 |           {
128 |             type: 'text',
129 |             text: JSON.stringify(result, null, 2),
130 |           },
131 |         ],
132 |         isError: false,
133 |       };
134 |     } catch (error) {
135 |       console.error('VectorSearchTabsContentTool: Search failed:', error);
136 |       return createErrorResponse(
137 |         `Vector search failed: ${error instanceof Error ? error.message : String(error)}`,
138 |       );
139 |     }
140 |   }
141 | 
142 |   /**
143 |    * Ensure all tabs are indexed
144 |    */
145 |   private async ensureTabsIndexed(tabs: chrome.tabs.Tab[]): Promise<void> {
146 |     const indexPromises = tabs
147 |       .filter((tab) => tab.id)
148 |       .map(async (tab) => {
149 |         try {
150 |           await this.contentIndexer.indexTabContent(tab.id!);
151 |         } catch (error) {
152 |           console.warn(`VectorSearchTabsContentTool: Failed to index tab ${tab.id}:`, error);
153 |         }
154 |       });
155 | 
156 |     await Promise.allSettled(indexPromises);
157 |   }
158 | 
159 |   /**
160 |    * Convert search results format
161 |    */
162 |   private convertSearchResults(searchResults: SearchResult[]): VectorSearchResult[] {
163 |     return searchResults.map((result) => ({
164 |       tabId: result.document.tabId,
165 |       url: result.document.url,
166 |       title: result.document.title,
167 |       semanticScore: result.similarity,
168 |       matchedSnippet: this.extractSnippet(result.document.chunk.text),
169 |       chunkSource: result.document.chunk.source,
170 |       timestamp: result.document.timestamp,
171 |     }));
172 |   }
173 | 
174 |   /**
175 |    * Deduplicate by tab, keep only the highest similarity fragment per tab
176 |    */
177 |   private deduplicateByTab(results: VectorSearchResult[]): VectorSearchResult[] {
178 |     const tabMap = new Map<number, VectorSearchResult>();
179 | 
180 |     for (const result of results) {
181 |       const existingResult = tabMap.get(result.tabId);
182 | 
183 |       // If this tab has no result yet, or current result has higher similarity, update it
184 |       if (!existingResult || result.semanticScore > existingResult.semanticScore) {
185 |         tabMap.set(result.tabId, result);
186 |       }
187 |     }
188 | 
189 |     return Array.from(tabMap.values());
190 |   }
191 | 
192 |   /**
193 |    * Extract text snippet for display
194 |    */
195 |   private extractSnippet(text: string, maxLength: number = 200): string {
196 |     if (text.length <= maxLength) {
197 |       return text;
198 |     }
199 | 
200 |     // Try to truncate at sentence boundary
201 |     const truncated = text.substring(0, maxLength);
202 |     const lastSentenceEnd = Math.max(
203 |       truncated.lastIndexOf('.'),
204 |       truncated.lastIndexOf('!'),
205 |       truncated.lastIndexOf('?'),
206 |       truncated.lastIndexOf('。'),
207 |       truncated.lastIndexOf('!'),
208 |       truncated.lastIndexOf('?'),
209 |     );
210 | 
211 |     if (lastSentenceEnd > maxLength * 0.7) {
212 |       return truncated.substring(0, lastSentenceEnd + 1);
213 |     }
214 | 
215 |     // If no suitable sentence boundary found, truncate at word boundary
216 |     const lastSpaceIndex = truncated.lastIndexOf(' ');
217 |     if (lastSpaceIndex > maxLength * 0.8) {
218 |       return truncated.substring(0, lastSpaceIndex) + '...';
219 |     }
220 | 
221 |     return truncated + '...';
222 |   }
223 | 
224 |   /**
225 |    * Get index statistics
226 |    */
227 |   public async getIndexStats() {
228 |     if (!this.isInitialized) {
229 |       // Don't automatically initialize - just return basic stats
230 |       return {
231 |         totalDocuments: 0,
232 |         totalTabs: 0,
233 |         indexSize: 0,
234 |         indexedPages: 0,
235 |         isInitialized: false,
236 |         semanticEngineReady: false,
237 |         semanticEngineInitializing: false,
238 |       };
239 |     }
240 |     return this.contentIndexer.getStats();
241 |   }
242 | 
243 |   /**
244 |    * Manually rebuild index
245 |    */
246 |   public async rebuildIndex(): Promise<void> {
247 |     if (!this.isInitialized) {
248 |       await this.initializeIndexer();
249 |     }
250 | 
251 |     try {
252 |       // Clear existing indexes
253 |       await this.contentIndexer.clearAllIndexes();
254 | 
255 |       // Get all tabs and reindex
256 |       const windows = await chrome.windows.getAll({ populate: true });
257 |       const allTabs: chrome.tabs.Tab[] = [];
258 | 
259 |       for (const window of windows) {
260 |         if (window.tabs) {
261 |           allTabs.push(...window.tabs);
262 |         }
263 |       }
264 | 
265 |       const validTabs = allTabs.filter(
266 |         (tab) =>
267 |           tab.id &&
268 |           tab.url &&
269 |           !tab.url.startsWith('chrome://') &&
270 |           !tab.url.startsWith('chrome-extension://') &&
271 |           !tab.url.startsWith('edge://') &&
272 |           !tab.url.startsWith('about:'),
273 |       );
274 | 
275 |       await this.ensureTabsIndexed(validTabs);
276 | 
277 |       console.log(`VectorSearchTabsContentTool: Rebuilt index for ${validTabs.length} tabs`);
278 |     } catch (error) {
279 |       console.error('VectorSearchTabsContentTool: Failed to rebuild index:', error);
280 |       throw error;
281 |     }
282 |   }
283 | 
284 |   /**
285 |    * Manually index specified tab
286 |    */
287 |   public async indexTab(tabId: number): Promise<void> {
288 |     if (!this.isInitialized) {
289 |       await this.initializeIndexer();
290 |     }
291 | 
292 |     await this.contentIndexer.indexTabContent(tabId);
293 |   }
294 | 
295 |   /**
296 |    * Remove index for specified tab
297 |    */
298 |   public async removeTabIndex(tabId: number): Promise<void> {
299 |     if (!this.isInitialized) {
300 |       return;
301 |     }
302 | 
303 |     await this.contentIndexer.removeTabIndex(tabId);
304 |   }
305 | }
306 | 
307 | // Export tool instance
308 | export const vectorSearchTabsContentTool = new VectorSearchTabsContentTool();
309 | 
```

--------------------------------------------------------------------------------
/app/native-server/src/native-messaging-host.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { stdin, stdout } from 'process';
  2 | import { Server } from './server';
  3 | import { v4 as uuidv4 } from 'uuid';
  4 | import { NativeMessageType } from 'chrome-mcp-shared';
  5 | import { TIMEOUTS } from './constant';
  6 | import fileHandler from './file-handler';
  7 | 
  8 | interface PendingRequest {
  9 |   resolve: (value: any) => void;
 10 |   reject: (reason?: any) => void;
 11 |   timeoutId: NodeJS.Timeout;
 12 | }
 13 | 
 14 | export class NativeMessagingHost {
 15 |   private associatedServer: Server | null = null;
 16 |   private pendingRequests: Map<string, PendingRequest> = new Map();
 17 | 
 18 |   public setServer(serverInstance: Server): void {
 19 |     this.associatedServer = serverInstance;
 20 |   }
 21 | 
 22 |   // add message handler to wait for start server
 23 |   public start(): void {
 24 |     try {
 25 |       this.setupMessageHandling();
 26 |     } catch (error: any) {
 27 |       process.exit(1);
 28 |     }
 29 |   }
 30 | 
 31 |   private setupMessageHandling(): void {
 32 |     let buffer = Buffer.alloc(0);
 33 |     let expectedLength = -1;
 34 | 
 35 |     stdin.on('readable', () => {
 36 |       let chunk;
 37 |       while ((chunk = stdin.read()) !== null) {
 38 |         buffer = Buffer.concat([buffer, chunk]);
 39 | 
 40 |         if (expectedLength === -1 && buffer.length >= 4) {
 41 |           expectedLength = buffer.readUInt32LE(0);
 42 |           buffer = buffer.slice(4);
 43 |         }
 44 | 
 45 |         if (expectedLength !== -1 && buffer.length >= expectedLength) {
 46 |           const messageBuffer = buffer.slice(0, expectedLength);
 47 |           buffer = buffer.slice(expectedLength);
 48 | 
 49 |           try {
 50 |             const message = JSON.parse(messageBuffer.toString());
 51 |             this.handleMessage(message);
 52 |           } catch (error: any) {
 53 |             this.sendError(`Failed to parse message: ${error.message}`);
 54 |           }
 55 |           expectedLength = -1; // reset to get next data
 56 |         }
 57 |       }
 58 |     });
 59 | 
 60 |     stdin.on('end', () => {
 61 |       this.cleanup();
 62 |     });
 63 | 
 64 |     stdin.on('error', () => {
 65 |       this.cleanup();
 66 |     });
 67 |   }
 68 | 
 69 |   private async handleMessage(message: any): Promise<void> {
 70 |     if (!message || typeof message !== 'object') {
 71 |       this.sendError('Invalid message format');
 72 |       return;
 73 |     }
 74 | 
 75 |     if (message.responseToRequestId) {
 76 |       const requestId = message.responseToRequestId;
 77 |       const pending = this.pendingRequests.get(requestId);
 78 | 
 79 |       if (pending) {
 80 |         clearTimeout(pending.timeoutId);
 81 |         if (message.error) {
 82 |           pending.reject(new Error(message.error));
 83 |         } else {
 84 |           pending.resolve(message.payload);
 85 |         }
 86 |         this.pendingRequests.delete(requestId);
 87 |       } else {
 88 |         // just ignore
 89 |       }
 90 |       return;
 91 |     }
 92 | 
 93 |     // Handle directive messages from Chrome
 94 |     try {
 95 |       switch (message.type) {
 96 |         case NativeMessageType.START:
 97 |           await this.startServer(message.payload?.port || 3000);
 98 |           break;
 99 |         case NativeMessageType.STOP:
100 |           await this.stopServer();
101 |           break;
102 |         // Keep ping/pong for simple liveness detection, but this differs from request-response pattern
103 |         case 'ping_from_extension':
104 |           this.sendMessage({ type: 'pong_to_extension' });
105 |           break;
106 |         case 'file_operation':
107 |           await this.handleFileOperation(message);
108 |           break;
109 |         default:
110 |           // Double check when message type is not supported
111 |           if (!message.responseToRequestId) {
112 |             this.sendError(
113 |               `Unknown message type or non-response message: ${message.type || 'no type'}`,
114 |             );
115 |           }
116 |       }
117 |     } catch (error: any) {
118 |       this.sendError(`Failed to handle directive message: ${error.message}`);
119 |     }
120 |   }
121 | 
122 |   /**
123 |    * Handle file operations from the extension
124 |    */
125 |   private async handleFileOperation(message: any): Promise<void> {
126 |     try {
127 |       const result = await fileHandler.handleFileRequest(message.payload);
128 |       
129 |       if (message.requestId) {
130 |         // Send response back with the request ID
131 |         this.sendMessage({
132 |           type: 'file_operation_response',
133 |           responseToRequestId: message.requestId,
134 |           payload: result,
135 |         });
136 |       } else {
137 |         // No request ID, just send result
138 |         this.sendMessage({
139 |           type: 'file_operation_result',
140 |           payload: result,
141 |         });
142 |       }
143 |     } catch (error: any) {
144 |       const errorResponse = {
145 |         success: false,
146 |         error: error.message || 'Unknown error during file operation',
147 |       };
148 |       
149 |       if (message.requestId) {
150 |         this.sendMessage({
151 |           type: 'file_operation_response',
152 |           responseToRequestId: message.requestId,
153 |           error: errorResponse.error,
154 |         });
155 |       } else {
156 |         this.sendError(`File operation failed: ${errorResponse.error}`);
157 |       }
158 |     }
159 |   }
160 | 
161 |   /**
162 |    * Send request to Chrome and wait for response
163 |    * @param messagePayload Data to send to Chrome
164 |    * @param timeoutMs Timeout for waiting response (milliseconds)
165 |    * @returns Promise, resolves to Chrome's returned payload on success, rejects on failure
166 |    */
167 |   public sendRequestToExtensionAndWait(
168 |     messagePayload: any,
169 |     messageType: string = 'request_data',
170 |     timeoutMs: number = TIMEOUTS.DEFAULT_REQUEST_TIMEOUT,
171 |   ): Promise<any> {
172 |     return new Promise((resolve, reject) => {
173 |       const requestId = uuidv4(); // Generate unique request ID
174 | 
175 |       const timeoutId = setTimeout(() => {
176 |         this.pendingRequests.delete(requestId); // Remove from Map after timeout
177 |         reject(new Error(`Request timed out after ${timeoutMs}ms`));
178 |       }, timeoutMs);
179 | 
180 |       // Store request's resolve/reject functions and timeout ID
181 |       this.pendingRequests.set(requestId, { resolve, reject, timeoutId });
182 | 
183 |       // Send message with requestId to Chrome
184 |       this.sendMessage({
185 |         type: messageType, // Define a request type, e.g. 'request_data'
186 |         payload: messagePayload,
187 |         requestId: requestId, // <--- Key: include request ID
188 |       });
189 |     });
190 |   }
191 | 
192 |   /**
193 |    * Start Fastify server (now accepts Server instance)
194 |    */
195 |   private async startServer(port: number): Promise<void> {
196 |     if (!this.associatedServer) {
197 |       this.sendError('Internal error: server instance not set');
198 |       return;
199 |     }
200 |     try {
201 |       if (this.associatedServer.isRunning) {
202 |         this.sendMessage({
203 |           type: NativeMessageType.ERROR,
204 |           payload: { message: 'Server is already running' },
205 |         });
206 |         return;
207 |       }
208 | 
209 |       await this.associatedServer.start(port, this);
210 | 
211 |       this.sendMessage({
212 |         type: NativeMessageType.SERVER_STARTED,
213 |         payload: { port },
214 |       });
215 | 
216 |     } catch (error: any) {
217 |       this.sendError(`Failed to start server: ${error.message}`);
218 |     }
219 |   }
220 | 
221 |   /**
222 |    * Stop Fastify server
223 |    */
224 |   private async stopServer(): Promise<void> {
225 |     if (!this.associatedServer) {
226 |       this.sendError('Internal error: server instance not set');
227 |       return;
228 |     }
229 |     try {
230 |       // Check status through associatedServer
231 |       if (!this.associatedServer.isRunning) {
232 |         this.sendMessage({
233 |           type: NativeMessageType.ERROR,
234 |           payload: { message: 'Server is not running' },
235 |         });
236 |         return;
237 |       }
238 | 
239 |       await this.associatedServer.stop();
240 |       // this.serverStarted = false; // Server should update its own status after successful stop
241 | 
242 |       this.sendMessage({ type: NativeMessageType.SERVER_STOPPED }); // Distinguish from previous 'stopped'
243 |     } catch (error: any) {
244 |       this.sendError(`Failed to stop server: ${error.message}`);
245 |     }
246 |   }
247 | 
248 |   /**
249 |    * Send message to Chrome extension
250 |    */
251 |   public sendMessage(message: any): void {
252 |     try {
253 |       const messageString = JSON.stringify(message);
254 |       const messageBuffer = Buffer.from(messageString);
255 |       const headerBuffer = Buffer.alloc(4);
256 |       headerBuffer.writeUInt32LE(messageBuffer.length, 0);
257 |       // Ensure atomic write
258 |       stdout.write(Buffer.concat([headerBuffer, messageBuffer]), (err) => {
259 |         if (err) {
260 |           // Consider how to handle write failure, may affect request completion
261 |         } else {
262 |           // Message sent successfully, no action needed
263 |         }
264 |       });
265 |     } catch (error: any) {
266 |       // Catch JSON.stringify or Buffer operation errors
267 |       // If preparation stage fails, associated request may never be sent
268 |       // Need to consider whether to reject corresponding Promise (if called within sendRequestToExtensionAndWait)
269 |     }
270 |   }
271 | 
272 |   /**
273 |    * Send error message to Chrome extension (mainly for sending non-request-response type errors)
274 |    */
275 |   private sendError(errorMessage: string): void {
276 |     this.sendMessage({
277 |       type: NativeMessageType.ERROR_FROM_NATIVE_HOST, // Use more explicit type
278 |       payload: { message: errorMessage },
279 |     });
280 |   }
281 | 
282 | 
283 | 
284 |   /**
285 |    * Clean up resources
286 |    */
287 |   private cleanup(): void {
288 |     // Reject all pending requests
289 |     this.pendingRequests.forEach((pending) => {
290 |       clearTimeout(pending.timeoutId);
291 |       pending.reject(new Error('Native host is shutting down or Chrome disconnected.'));
292 |     });
293 |     this.pendingRequests.clear();
294 | 
295 |     if (this.associatedServer && this.associatedServer.isRunning) {
296 |       this.associatedServer
297 |         .stop()
298 |         .then(() => {
299 |           process.exit(0);
300 |         })
301 |         .catch(() => {
302 |           process.exit(1);
303 |         });
304 |     } else {
305 |       process.exit(0);
306 |     }
307 |   }
308 | }
309 | 
310 | const nativeMessagingHostInstance = new NativeMessagingHost();
311 | export default nativeMessagingHostInstance;
312 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/utils/i18n.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Chrome Extension i18n utility
  3 |  * Provides safe access to chrome.i18n.getMessage with fallbacks
  4 |  */
  5 | 
  6 | // Fallback messages for when Chrome APIs aren't available (English)
  7 | const fallbackMessages: Record<string, string> = {
  8 |   // Extension metadata
  9 |   extensionName: 'chrome-mcp-server',
 10 |   extensionDescription: 'Exposes browser capabilities with your own chrome',
 11 | 
 12 |   // Section headers
 13 |   nativeServerConfigLabel: 'Native Server Configuration',
 14 |   semanticEngineLabel: 'Semantic Engine',
 15 |   embeddingModelLabel: 'Embedding Model',
 16 |   indexDataManagementLabel: 'Index Data Management',
 17 |   modelCacheManagementLabel: 'Model Cache Management',
 18 | 
 19 |   // Status labels
 20 |   statusLabel: 'Status',
 21 |   runningStatusLabel: 'Running Status',
 22 |   connectionStatusLabel: 'Connection Status',
 23 |   lastUpdatedLabel: 'Last Updated:',
 24 | 
 25 |   // Connection states
 26 |   connectButton: 'Connect',
 27 |   disconnectButton: 'Disconnect',
 28 |   connectingStatus: 'Connecting...',
 29 |   connectedStatus: 'Connected',
 30 |   disconnectedStatus: 'Disconnected',
 31 |   detectingStatus: 'Detecting...',
 32 | 
 33 |   // Server states
 34 |   serviceRunningStatus: 'Service Running (Port: {0})',
 35 |   serviceNotConnectedStatus: 'Service Not Connected',
 36 |   connectedServiceNotStartedStatus: 'Connected, Service Not Started',
 37 | 
 38 |   // Configuration labels
 39 |   mcpServerConfigLabel: 'MCP Server Configuration',
 40 |   connectionPortLabel: 'Connection Port',
 41 |   refreshStatusButton: 'Refresh Status',
 42 |   copyConfigButton: 'Copy Configuration',
 43 | 
 44 |   // Action buttons
 45 |   retryButton: 'Retry',
 46 |   cancelButton: 'Cancel',
 47 |   confirmButton: 'Confirm',
 48 |   saveButton: 'Save',
 49 |   closeButton: 'Close',
 50 |   resetButton: 'Reset',
 51 | 
 52 |   // Progress states
 53 |   initializingStatus: 'Initializing...',
 54 |   processingStatus: 'Processing...',
 55 |   loadingStatus: 'Loading...',
 56 |   clearingStatus: 'Clearing...',
 57 |   cleaningStatus: 'Cleaning...',
 58 |   downloadingStatus: 'Downloading...',
 59 | 
 60 |   // Semantic engine states
 61 |   semanticEngineReadyStatus: 'Semantic Engine Ready',
 62 |   semanticEngineInitializingStatus: 'Semantic Engine Initializing...',
 63 |   semanticEngineInitFailedStatus: 'Semantic Engine Initialization Failed',
 64 |   semanticEngineNotInitStatus: 'Semantic Engine Not Initialized',
 65 |   initSemanticEngineButton: 'Initialize Semantic Engine',
 66 |   reinitializeButton: 'Reinitialize',
 67 | 
 68 |   // Model states
 69 |   downloadingModelStatus: 'Downloading Model... {0}%',
 70 |   switchingModelStatus: 'Switching Model...',
 71 |   modelLoadedStatus: 'Model Loaded',
 72 |   modelFailedStatus: 'Model Failed to Load',
 73 | 
 74 |   // Model descriptions
 75 |   lightweightModelDescription: 'Lightweight Multilingual Model',
 76 |   betterThanSmallDescription: 'Slightly larger than e5-small, but better performance',
 77 |   multilingualModelDescription: 'Multilingual Semantic Model',
 78 | 
 79 |   // Performance levels
 80 |   fastPerformance: 'Fast',
 81 |   balancedPerformance: 'Balanced',
 82 |   accuratePerformance: 'Accurate',
 83 | 
 84 |   // Error messages
 85 |   networkErrorMessage: 'Network connection error, please check network and retry',
 86 |   modelCorruptedErrorMessage: 'Model file corrupted or incomplete, please retry download',
 87 |   unknownErrorMessage: 'Unknown error, please check if your network can access HuggingFace',
 88 |   permissionDeniedErrorMessage: 'Permission denied',
 89 |   timeoutErrorMessage: 'Operation timed out',
 90 | 
 91 |   // Data statistics
 92 |   indexedPagesLabel: 'Indexed Pages',
 93 |   indexSizeLabel: 'Index Size',
 94 |   activeTabsLabel: 'Active Tabs',
 95 |   vectorDocumentsLabel: 'Vector Documents',
 96 |   cacheSizeLabel: 'Cache Size',
 97 |   cacheEntriesLabel: 'Cache Entries',
 98 | 
 99 |   // Data management
100 |   clearAllDataButton: 'Clear All Data',
101 |   clearAllCacheButton: 'Clear All Cache',
102 |   cleanExpiredCacheButton: 'Clean Expired Cache',
103 |   exportDataButton: 'Export Data',
104 |   importDataButton: 'Import Data',
105 | 
106 |   // Dialog titles
107 |   confirmClearDataTitle: 'Confirm Clear Data',
108 |   settingsTitle: 'Settings',
109 |   aboutTitle: 'About',
110 |   helpTitle: 'Help',
111 | 
112 |   // Dialog messages
113 |   clearDataWarningMessage:
114 |     'This operation will clear all indexed webpage content and vector data, including:',
115 |   clearDataList1: 'All webpage text content index',
116 |   clearDataList2: 'Vector embedding data',
117 |   clearDataList3: 'Search history and cache',
118 |   clearDataIrreversibleWarning:
119 |     'This operation is irreversible! After clearing, you need to browse webpages again to rebuild the index.',
120 |   confirmClearButton: 'Confirm Clear',
121 | 
122 |   // Cache states
123 |   cacheDetailsLabel: 'Cache Details',
124 |   noCacheDataMessage: 'No cache data',
125 |   loadingCacheInfoStatus: 'Loading cache information...',
126 |   processingCacheStatus: 'Processing cache...',
127 |   expiredLabel: 'Expired',
128 | 
129 |   // Browser integration
130 |   bookmarksBarLabel: 'Bookmarks Bar',
131 |   newTabLabel: 'New Tab',
132 |   currentPageLabel: 'Current Page',
133 | 
134 |   // Accessibility
135 |   menuLabel: 'Menu',
136 |   navigationLabel: 'Navigation',
137 |   mainContentLabel: 'Main Content',
138 | 
139 |   // Future features
140 |   languageSelectorLabel: 'Language',
141 |   themeLabel: 'Theme',
142 |   lightTheme: 'Light',
143 |   darkTheme: 'Dark',
144 |   autoTheme: 'Auto',
145 |   advancedSettingsLabel: 'Advanced Settings',
146 |   debugModeLabel: 'Debug Mode',
147 |   verboseLoggingLabel: 'Verbose Logging',
148 | 
149 |   // Notifications
150 |   successNotification: 'Operation completed successfully',
151 |   warningNotification: 'Warning: Please review before proceeding',
152 |   infoNotification: 'Information',
153 |   configCopiedNotification: 'Configuration copied to clipboard',
154 |   dataClearedNotification: 'Data cleared successfully',
155 | 
156 |   // Units
157 |   bytesUnit: 'bytes',
158 |   kilobytesUnit: 'KB',
159 |   megabytesUnit: 'MB',
160 |   gigabytesUnit: 'GB',
161 |   itemsUnit: 'items',
162 |   pagesUnit: 'pages',
163 | 
164 |   // Legacy keys for backwards compatibility
165 |   nativeServerConfig: 'Native Server Configuration',
166 |   runningStatus: 'Running Status',
167 |   refreshStatus: 'Refresh Status',
168 |   lastUpdated: 'Last Updated:',
169 |   mcpServerConfig: 'MCP Server Configuration',
170 |   connectionPort: 'Connection Port',
171 |   connecting: 'Connecting...',
172 |   disconnect: 'Disconnect',
173 |   connect: 'Connect',
174 |   semanticEngine: 'Semantic Engine',
175 |   embeddingModel: 'Embedding Model',
176 |   retry: 'Retry',
177 |   indexDataManagement: 'Index Data Management',
178 |   clearing: 'Clearing...',
179 |   clearAllData: 'Clear All Data',
180 |   copyConfig: 'Copy Configuration',
181 |   serviceRunning: 'Service Running (Port: {0})',
182 |   connectedServiceNotStarted: 'Connected, Service Not Started',
183 |   serviceNotConnected: 'Service Not Connected',
184 |   detecting: 'Detecting...',
185 |   lightweightModel: 'Lightweight Multilingual Model',
186 |   betterThanSmall: 'Slightly larger than e5-small, but better performance',
187 |   multilingualModel: 'Multilingual Semantic Model',
188 |   fast: 'Fast',
189 |   balanced: 'Balanced',
190 |   accurate: 'Accurate',
191 |   semanticEngineReady: 'Semantic Engine Ready',
192 |   semanticEngineInitializing: 'Semantic Engine Initializing...',
193 |   semanticEngineInitFailed: 'Semantic Engine Initialization Failed',
194 |   semanticEngineNotInit: 'Semantic Engine Not Initialized',
195 |   downloadingModel: 'Downloading Model... {0}%',
196 |   switchingModel: 'Switching Model...',
197 |   networkError: 'Network connection error, please check network and retry',
198 |   modelCorrupted: 'Model file corrupted or incomplete, please retry download',
199 |   unknownError: 'Unknown error, please check if your network can access HuggingFace',
200 |   reinitialize: 'Reinitialize',
201 |   initializing: 'Initializing...',
202 |   initSemanticEngine: 'Initialize Semantic Engine',
203 |   indexedPages: 'Indexed Pages',
204 |   indexSize: 'Index Size',
205 |   activeTabs: 'Active Tabs',
206 |   vectorDocuments: 'Vector Documents',
207 |   confirmClearData: 'Confirm Clear Data',
208 |   clearDataWarning:
209 |     'This operation will clear all indexed webpage content and vector data, including:',
210 |   clearDataIrreversible:
211 |     'This operation is irreversible! After clearing, you need to browse webpages again to rebuild the index.',
212 |   confirmClear: 'Confirm Clear',
213 |   cancel: 'Cancel',
214 |   confirm: 'Confirm',
215 |   processing: 'Processing...',
216 |   modelCacheManagement: 'Model Cache Management',
217 |   cacheSize: 'Cache Size',
218 |   cacheEntries: 'Cache Entries',
219 |   cacheDetails: 'Cache Details',
220 |   noCacheData: 'No cache data',
221 |   loadingCacheInfo: 'Loading cache information...',
222 |   processingCache: 'Processing cache...',
223 |   cleaning: 'Cleaning...',
224 |   cleanExpiredCache: 'Clean Expired Cache',
225 |   clearAllCache: 'Clear All Cache',
226 |   expired: 'Expired',
227 |   bookmarksBar: 'Bookmarks Bar',
228 | };
229 | 
230 | /**
231 |  * Safe i18n message getter with fallback support
232 |  * @param key Message key
233 |  * @param substitutions Optional substitution values
234 |  * @returns Localized message or fallback
235 |  */
236 | export function getMessage(key: string, substitutions?: string[]): string {
237 |   try {
238 |     // Check if Chrome extension APIs are available
239 |     if (typeof chrome !== 'undefined' && chrome.i18n && chrome.i18n.getMessage) {
240 |       const message = chrome.i18n.getMessage(key, substitutions);
241 |       if (message) {
242 |         return message;
243 |       }
244 |     }
245 |   } catch (error) {
246 |     console.warn(`Failed to get i18n message for key "${key}":`, error);
247 |   }
248 | 
249 |   // Fallback to English messages
250 |   let fallback = fallbackMessages[key] || key;
251 | 
252 |   // Handle substitutions in fallback messages
253 |   if (substitutions && substitutions.length > 0) {
254 |     substitutions.forEach((value, index) => {
255 |       fallback = fallback.replace(`{${index}}`, value);
256 |     });
257 |   }
258 | 
259 |   return fallback;
260 | }
261 | 
262 | /**
263 |  * Check if Chrome extension i18n APIs are available
264 |  */
265 | export function isI18nAvailable(): boolean {
266 |   try {
267 |     return (
268 |       typeof chrome !== 'undefined' && chrome.i18n && typeof chrome.i18n.getMessage === 'function'
269 |     );
270 |   } catch {
271 |     return false;
272 |   }
273 | }
274 | 
```

--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/file-upload.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 | interface FileUploadToolParams {
  6 |   selector: string; // CSS selector for the file input element
  7 |   filePath?: string; // Local file path
  8 |   fileUrl?: string; // URL to download file from
  9 |   base64Data?: string; // Base64 encoded file data
 10 |   fileName?: string; // Optional filename when using base64 or URL
 11 |   multiple?: boolean; // Whether to allow multiple files
 12 | }
 13 | 
 14 | /**
 15 |  * Tool for uploading files to web forms using Chrome DevTools Protocol
 16 |  * Similar to Playwright's setInputFiles implementation
 17 |  */
 18 | class FileUploadTool extends BaseBrowserToolExecutor {
 19 |   name = TOOL_NAMES.BROWSER.FILE_UPLOAD;
 20 |   private activeDebuggers: Map<number, boolean> = new Map();
 21 | 
 22 |   constructor() {
 23 |     super();
 24 |     // Clean up debuggers on tab removal
 25 |     chrome.tabs.onRemoved.addListener((tabId) => {
 26 |       if (this.activeDebuggers.has(tabId)) {
 27 |         this.cleanupDebugger(tabId);
 28 |       }
 29 |     });
 30 |   }
 31 | 
 32 |   /**
 33 |    * Execute file upload operation using Chrome DevTools Protocol
 34 |    */
 35 |   async execute(args: FileUploadToolParams): Promise<ToolResult> {
 36 |     const { selector, filePath, fileUrl, base64Data, fileName, multiple = false } = args;
 37 | 
 38 |     console.log(`Starting file upload operation with options:`, args);
 39 | 
 40 |     // Validate input
 41 |     if (!selector) {
 42 |       return createErrorResponse('Selector is required for file upload');
 43 |     }
 44 | 
 45 |     if (!filePath && !fileUrl && !base64Data) {
 46 |       return createErrorResponse(
 47 |         'One of filePath, fileUrl, or base64Data must be provided',
 48 |       );
 49 |     }
 50 | 
 51 |     let tabId: number | undefined;
 52 | 
 53 |     try {
 54 |       // Get current tab
 55 |       const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
 56 |       if (!tabs[0]?.id) {
 57 |         return createErrorResponse('No active tab found');
 58 |       }
 59 |       tabId = tabs[0].id;
 60 | 
 61 |       // Prepare file paths
 62 |       let files: string[] = [];
 63 | 
 64 |       if (filePath) {
 65 |         // Direct file path provided
 66 |         files = [filePath];
 67 |       } else if (fileUrl || base64Data) {
 68 |         // For URL or base64, we need to use the native messaging host
 69 |         // to download or save the file temporarily
 70 |         const tempFilePath = await this.prepareFileFromRemote({
 71 |           fileUrl,
 72 |           base64Data,
 73 |           fileName: fileName || 'uploaded-file',
 74 |         });
 75 |         if (!tempFilePath) {
 76 |           return createErrorResponse('Failed to prepare file for upload');
 77 |         }
 78 |         files = [tempFilePath];
 79 |       }
 80 | 
 81 |       // Attach debugger to the tab
 82 |       await this.attachDebugger(tabId);
 83 | 
 84 |       // Enable necessary CDP domains
 85 |       await chrome.debugger.sendCommand({ tabId }, 'DOM.enable', {});
 86 |       await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable', {});
 87 | 
 88 |       // Get the document
 89 |       const { root } = await chrome.debugger.sendCommand(
 90 |         { tabId },
 91 |         'DOM.getDocument',
 92 |         { depth: -1, pierce: true },
 93 |       ) as { root: { nodeId: number } };
 94 | 
 95 |       // Find the file input element using the selector
 96 |       const { nodeId } = await chrome.debugger.sendCommand(
 97 |         { tabId },
 98 |         'DOM.querySelector',
 99 |         {
100 |           nodeId: root.nodeId,
101 |           selector: selector,
102 |         },
103 |       ) as { nodeId: number };
104 | 
105 |       if (!nodeId || nodeId === 0) {
106 |         throw new Error(`Element with selector "${selector}" not found`);
107 |       }
108 | 
109 |       // Verify it's actually a file input
110 |       const { node } = await chrome.debugger.sendCommand(
111 |         { tabId },
112 |         'DOM.describeNode',
113 |         { nodeId },
114 |       ) as { node: { nodeName: string; attributes?: string[] } };
115 | 
116 |       if (node.nodeName !== 'INPUT') {
117 |         throw new Error(`Element with selector "${selector}" is not an input element`);
118 |       }
119 | 
120 |       // Check if it's a file input by looking for type="file" in attributes
121 |       const attributes = node.attributes || [];
122 |       let isFileInput = false;
123 |       for (let i = 0; i < attributes.length; i += 2) {
124 |         if (attributes[i] === 'type' && attributes[i + 1] === 'file') {
125 |           isFileInput = true;
126 |           break;
127 |         }
128 |       }
129 | 
130 |       if (!isFileInput) {
131 |         throw new Error(`Element with selector "${selector}" is not a file input (type="file")`);
132 |       }
133 | 
134 |       // Set the files on the input element
135 |       // This is the key CDP command that Playwright and Puppeteer use
136 |       await chrome.debugger.sendCommand(
137 |         { tabId },
138 |         'DOM.setFileInputFiles',
139 |         {
140 |           nodeId: nodeId,
141 |           files: files,
142 |         },
143 |       );
144 | 
145 |       // Trigger change event to ensure the page reacts to the file upload
146 |       await chrome.debugger.sendCommand(
147 |         { tabId },
148 |         'Runtime.evaluate',
149 |         {
150 |           expression: `
151 |             (function() {
152 |               const element = document.querySelector('${selector.replace(/'/g, "\\'")}');
153 |               if (element) {
154 |                 const event = new Event('change', { bubbles: true });
155 |                 element.dispatchEvent(event);
156 |                 return true;
157 |               }
158 |               return false;
159 |             })()
160 |           `,
161 |         },
162 |       );
163 | 
164 |       // Clean up debugger
165 |       await this.detachDebugger(tabId);
166 | 
167 |       return {
168 |         content: [
169 |           {
170 |             type: 'text',
171 |             text: JSON.stringify({
172 |               success: true,
173 |               message: 'File(s) uploaded successfully',
174 |               files: files,
175 |               selector: selector,
176 |               fileCount: files.length,
177 |             }),
178 |           },
179 |         ],
180 |         isError: false,
181 |       };
182 |     } catch (error) {
183 |       console.error('Error in file upload operation:', error);
184 |       
185 |       // Clean up debugger if attached
186 |       if (tabId !== undefined && this.activeDebuggers.has(tabId)) {
187 |         await this.detachDebugger(tabId);
188 |       }
189 | 
190 |       return createErrorResponse(
191 |         `Error uploading file: ${error instanceof Error ? error.message : String(error)}`,
192 |       );
193 |     }
194 |   }
195 | 
196 |   /**
197 |    * Attach debugger to a tab
198 |    */
199 |   private async attachDebugger(tabId: number): Promise<void> {
200 |     // Check if debugger is already attached
201 |     const targets = await chrome.debugger.getTargets();
202 |     const existingTarget = targets.find(
203 |       (t) => t.tabId === tabId && t.attached,
204 |     );
205 | 
206 |     if (existingTarget) {
207 |       if (existingTarget.extensionId === chrome.runtime.id) {
208 |         // Our extension already attached
209 |         console.log('Debugger already attached by this extension');
210 |         return;
211 |       } else {
212 |         throw new Error(
213 |           'Debugger is already attached to this tab by another extension or DevTools',
214 |         );
215 |       }
216 |     }
217 | 
218 |     // Attach debugger
219 |     await chrome.debugger.attach({ tabId }, '1.3');
220 |     this.activeDebuggers.set(tabId, true);
221 |     console.log(`Debugger attached to tab ${tabId}`);
222 |   }
223 | 
224 |   /**
225 |    * Detach debugger from a tab
226 |    */
227 |   private async detachDebugger(tabId: number): Promise<void> {
228 |     if (!this.activeDebuggers.has(tabId)) {
229 |       return;
230 |     }
231 | 
232 |     try {
233 |       await chrome.debugger.detach({ tabId });
234 |       console.log(`Debugger detached from tab ${tabId}`);
235 |     } catch (error) {
236 |       console.warn(`Error detaching debugger from tab ${tabId}:`, error);
237 |     } finally {
238 |       this.activeDebuggers.delete(tabId);
239 |     }
240 |   }
241 | 
242 |   /**
243 |    * Clean up debugger connection
244 |    */
245 |   private cleanupDebugger(tabId: number): void {
246 |     this.activeDebuggers.delete(tabId);
247 |   }
248 | 
249 |   /**
250 |    * Prepare file from URL or base64 data using native messaging host
251 |    */
252 |   private async prepareFileFromRemote(options: {
253 |     fileUrl?: string;
254 |     base64Data?: string;
255 |     fileName: string;
256 |   }): Promise<string | null> {
257 |     const { fileUrl, base64Data, fileName } = options;
258 | 
259 |     return new Promise((resolve) => {
260 |       const requestId = `file-upload-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
261 |       const timeout = setTimeout(() => {
262 |         console.error('File preparation request timed out');
263 |         resolve(null);
264 |       }, 30000); // 30 second timeout
265 | 
266 |       // Create listener for the response
267 |       const handleMessage = (message: any) => {
268 |         if (message.type === 'file_operation_response' && 
269 |             message.responseToRequestId === requestId) {
270 |           clearTimeout(timeout);
271 |           chrome.runtime.onMessage.removeListener(handleMessage);
272 |           
273 |           if (message.payload?.success && message.payload?.filePath) {
274 |             resolve(message.payload.filePath);
275 |           } else {
276 |             console.error('Native host failed to prepare file:', message.error || message.payload?.error);
277 |             resolve(null);
278 |           }
279 |         }
280 |       };
281 | 
282 |       // Add listener
283 |       chrome.runtime.onMessage.addListener(handleMessage);
284 | 
285 |       // Send message to background script to forward to native host
286 |       chrome.runtime.sendMessage({
287 |         type: 'forward_to_native',
288 |         message: {
289 |           type: 'file_operation',
290 |           requestId: requestId,
291 |           payload: {
292 |             action: 'prepareFile',
293 |             fileUrl,
294 |             base64Data,
295 |             fileName,
296 |           },
297 |         },
298 |       }).catch((error) => {
299 |         console.error('Error sending message to background:', error);
300 |         clearTimeout(timeout);
301 |         chrome.runtime.onMessage.removeListener(handleMessage);
302 |         resolve(null);
303 |       });
304 |     });
305 |   }
306 | }
307 | 
308 | export const fileUploadTool = new FileUploadTool();
```
Page 2/10FirstPrevNextLast