This is page 2 of 8. Use http://codebase.md/hangwin/mcp-chrome?page={x} to view the full context.
# Directory Structure
```
├── .gitattributes
├── .github
│ └── workflows
│ └── build-release.yml
├── .gitignore
├── .husky
│ ├── commit-msg
│ └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── .vscode
│ └── extensions.json
├── app
│ ├── chrome-extension
│ │ ├── _locales
│ │ │ ├── de
│ │ │ │ └── messages.json
│ │ │ ├── en
│ │ │ │ └── messages.json
│ │ │ ├── ja
│ │ │ │ └── messages.json
│ │ │ ├── ko
│ │ │ │ └── messages.json
│ │ │ ├── zh_CN
│ │ │ │ └── messages.json
│ │ │ └── zh_TW
│ │ │ └── messages.json
│ │ ├── .env.example
│ │ ├── assets
│ │ │ └── vue.svg
│ │ ├── common
│ │ │ ├── constants.ts
│ │ │ ├── message-types.ts
│ │ │ └── tool-handler.ts
│ │ ├── entrypoints
│ │ │ ├── background
│ │ │ │ ├── index.ts
│ │ │ │ ├── native-host.ts
│ │ │ │ ├── semantic-similarity.ts
│ │ │ │ ├── storage-manager.ts
│ │ │ │ └── tools
│ │ │ │ ├── base-browser.ts
│ │ │ │ ├── browser
│ │ │ │ │ ├── bookmark.ts
│ │ │ │ │ ├── common.ts
│ │ │ │ │ ├── console.ts
│ │ │ │ │ ├── file-upload.ts
│ │ │ │ │ ├── history.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── inject-script.ts
│ │ │ │ │ ├── interaction.ts
│ │ │ │ │ ├── keyboard.ts
│ │ │ │ │ ├── network-capture-debugger.ts
│ │ │ │ │ ├── network-capture-web-request.ts
│ │ │ │ │ ├── network-request.ts
│ │ │ │ │ ├── screenshot.ts
│ │ │ │ │ ├── vector-search.ts
│ │ │ │ │ ├── web-fetcher.ts
│ │ │ │ │ └── window.ts
│ │ │ │ └── index.ts
│ │ │ ├── content.ts
│ │ │ ├── offscreen
│ │ │ │ ├── index.html
│ │ │ │ └── main.ts
│ │ │ └── popup
│ │ │ ├── App.vue
│ │ │ ├── components
│ │ │ │ ├── ConfirmDialog.vue
│ │ │ │ ├── icons
│ │ │ │ │ ├── BoltIcon.vue
│ │ │ │ │ ├── CheckIcon.vue
│ │ │ │ │ ├── DatabaseIcon.vue
│ │ │ │ │ ├── DocumentIcon.vue
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── TabIcon.vue
│ │ │ │ │ ├── TrashIcon.vue
│ │ │ │ │ └── VectorIcon.vue
│ │ │ │ ├── ModelCacheManagement.vue
│ │ │ │ └── ProgressIndicator.vue
│ │ │ ├── index.html
│ │ │ ├── main.ts
│ │ │ └── style.css
│ │ ├── eslint.config.js
│ │ ├── inject-scripts
│ │ │ ├── click-helper.js
│ │ │ ├── fill-helper.js
│ │ │ ├── inject-bridge.js
│ │ │ ├── interactive-elements-helper.js
│ │ │ ├── keyboard-helper.js
│ │ │ ├── network-helper.js
│ │ │ ├── screenshot-helper.js
│ │ │ └── web-fetcher-helper.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── icon
│ │ │ │ ├── 128.png
│ │ │ │ ├── 16.png
│ │ │ │ ├── 32.png
│ │ │ │ ├── 48.png
│ │ │ │ └── 96.png
│ │ │ ├── libs
│ │ │ │ └── ort.min.js
│ │ │ └── wxt.svg
│ │ ├── README.md
│ │ ├── tsconfig.json
│ │ ├── utils
│ │ │ ├── content-indexer.ts
│ │ │ ├── i18n.ts
│ │ │ ├── image-utils.ts
│ │ │ ├── lru-cache.ts
│ │ │ ├── model-cache-manager.ts
│ │ │ ├── offscreen-manager.ts
│ │ │ ├── semantic-similarity-engine.ts
│ │ │ ├── simd-math-engine.ts
│ │ │ ├── text-chunker.ts
│ │ │ └── vector-database.ts
│ │ ├── workers
│ │ │ ├── ort-wasm-simd-threaded.jsep.mjs
│ │ │ ├── ort-wasm-simd-threaded.jsep.wasm
│ │ │ ├── ort-wasm-simd-threaded.mjs
│ │ │ ├── ort-wasm-simd-threaded.wasm
│ │ │ ├── simd_math_bg.wasm
│ │ │ ├── simd_math.js
│ │ │ └── similarity.worker.js
│ │ └── wxt.config.ts
│ └── native-server
│ ├── debug.sh
│ ├── install.md
│ ├── jest.config.js
│ ├── package.json
│ ├── README.md
│ ├── src
│ │ ├── cli.ts
│ │ ├── constant
│ │ │ └── index.ts
│ │ ├── file-handler.ts
│ │ ├── index.ts
│ │ ├── mcp
│ │ │ ├── mcp-server-stdio.ts
│ │ │ ├── mcp-server.ts
│ │ │ ├── register-tools.ts
│ │ │ └── stdio-config.json
│ │ ├── native-messaging-host.ts
│ │ ├── scripts
│ │ │ ├── browser-config.ts
│ │ │ ├── build.ts
│ │ │ ├── constant.ts
│ │ │ ├── postinstall.ts
│ │ │ ├── register-dev.ts
│ │ │ ├── register.ts
│ │ │ ├── run_host.bat
│ │ │ ├── run_host.sh
│ │ │ └── utils.ts
│ │ ├── server
│ │ │ ├── index.ts
│ │ │ └── server.test.ts
│ │ └── util
│ │ └── logger.ts
│ └── tsconfig.json
├── commitlint.config.cjs
├── docs
│ ├── ARCHITECTURE_zh.md
│ ├── ARCHITECTURE.md
│ ├── CHANGELOG.md
│ ├── CONTRIBUTING_zh.md
│ ├── CONTRIBUTING.md
│ ├── TOOLS_zh.md
│ ├── TOOLS.md
│ ├── TROUBLESHOOTING_zh.md
│ ├── TROUBLESHOOTING.md
│ └── WINDOWS_INSTALL_zh.md
├── eslint.config.js
├── LICENSE
├── package.json
├── packages
│ ├── shared
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── tools.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ └── wasm-simd
│ ├── .gitignore
│ ├── BUILD.md
│ ├── Cargo.toml
│ ├── package.json
│ ├── README.md
│ └── src
│ └── lib.rs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── prompt
│ ├── content-analize.md
│ ├── excalidraw-prompt.md
│ └── modify-web.md
├── README_zh.md
├── README.md
├── releases
│ ├── chrome-extension
│ │ └── latest
│ │ └── chrome-mcp-server-lastest.zip
│ └── README.md
└── test-inject-script.js
```
# Files
--------------------------------------------------------------------------------
/app/chrome-extension/utils/text-chunker.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Text chunking utility
* Based on semantic chunking strategy, splits long text into small chunks suitable for vectorization
*/
export interface TextChunk {
text: string;
source: string;
index: number;
wordCount: number;
}
export interface ChunkingOptions {
maxWordsPerChunk?: number;
overlapSentences?: number;
minChunkLength?: number;
includeTitle?: boolean;
}
export class TextChunker {
private readonly defaultOptions: Required<ChunkingOptions> = {
maxWordsPerChunk: 80,
overlapSentences: 1,
minChunkLength: 20,
includeTitle: true,
};
public chunkText(content: string, title?: string, options?: ChunkingOptions): TextChunk[] {
const opts = { ...this.defaultOptions, ...options };
const chunks: TextChunk[] = [];
if (opts.includeTitle && title?.trim() && title.trim().length > 5) {
chunks.push({
text: title.trim(),
source: 'title',
index: 0,
wordCount: title.trim().split(/\s+/).length,
});
}
const cleanContent = content.trim();
if (!cleanContent) {
return chunks;
}
const sentences = this.splitIntoSentences(cleanContent);
if (sentences.length === 0) {
return this.fallbackChunking(cleanContent, chunks, opts);
}
const hasLongSentences = sentences.some(
(s: string) => s.split(/\s+/).length > opts.maxWordsPerChunk,
);
if (hasLongSentences) {
return this.mixedChunking(sentences, chunks, opts);
}
return this.groupSentencesIntoChunks(sentences, chunks, opts);
}
private splitIntoSentences(content: string): string[] {
const processedContent = content
.replace(/([。!?])\s*/g, '$1\n')
.replace(/([.!?])\s+(?=[A-Z])/g, '$1\n')
.replace(/([.!?]["'])\s+(?=[A-Z])/g, '$1\n')
.replace(/([.!?])\s*$/gm, '$1\n')
.replace(/([。!?][""])\s*/g, '$1\n')
.replace(/\n\s*\n/g, '\n');
const sentences = processedContent
.split('\n')
.map((s) => s.trim())
.filter((s) => s.length > 15);
if (sentences.length < 3 && content.length > 500) {
return this.aggressiveSentenceSplitting(content);
}
return sentences;
}
private aggressiveSentenceSplitting(content: string): string[] {
const sentences = content
.replace(/([.!?。!?])/g, '$1\n')
.replace(/([;;::])/g, '$1\n')
.replace(/([))])\s*(?=[\u4e00-\u9fa5A-Z])/g, '$1\n')
.split('\n')
.map((s) => s.trim())
.filter((s) => s.length > 15);
const maxWordsPerChunk = 80;
const finalSentences: string[] = [];
for (const sentence of sentences) {
const words = sentence.split(/\s+/);
if (words.length <= maxWordsPerChunk) {
finalSentences.push(sentence);
} else {
const overlapWords = 5;
for (let i = 0; i < words.length; i += maxWordsPerChunk - overlapWords) {
const chunkWords = words.slice(i, i + maxWordsPerChunk);
const chunkText = chunkWords.join(' ');
if (chunkText.length > 15) {
finalSentences.push(chunkText);
}
}
}
}
return finalSentences;
}
/**
* Group sentences into chunks
*/
private groupSentencesIntoChunks(
sentences: string[],
existingChunks: TextChunk[],
options: Required<ChunkingOptions>,
): TextChunk[] {
const chunks = [...existingChunks];
let chunkIndex = chunks.length;
let i = 0;
while (i < sentences.length) {
let currentChunkText = '';
let currentWordCount = 0;
let sentencesUsed = 0;
while (i + sentencesUsed < sentences.length && currentWordCount < options.maxWordsPerChunk) {
const sentence = sentences[i + sentencesUsed];
const sentenceWords = sentence.split(/\s+/).length;
if (currentWordCount + sentenceWords > options.maxWordsPerChunk && currentWordCount > 0) {
break;
}
currentChunkText += (currentChunkText ? ' ' : '') + sentence;
currentWordCount += sentenceWords;
sentencesUsed++;
}
if (currentChunkText.trim().length > options.minChunkLength) {
chunks.push({
text: currentChunkText.trim(),
source: `content_chunk_${chunkIndex}`,
index: chunkIndex,
wordCount: currentWordCount,
});
chunkIndex++;
}
i += Math.max(1, sentencesUsed - options.overlapSentences);
}
return chunks;
}
/**
* Mixed chunking method (handles long sentences)
*/
private mixedChunking(
sentences: string[],
existingChunks: TextChunk[],
options: Required<ChunkingOptions>,
): TextChunk[] {
const chunks = [...existingChunks];
let chunkIndex = chunks.length;
for (const sentence of sentences) {
const sentenceWords = sentence.split(/\s+/).length;
if (sentenceWords <= options.maxWordsPerChunk) {
chunks.push({
text: sentence.trim(),
source: `sentence_chunk_${chunkIndex}`,
index: chunkIndex,
wordCount: sentenceWords,
});
chunkIndex++;
} else {
const words = sentence.split(/\s+/);
for (let i = 0; i < words.length; i += options.maxWordsPerChunk) {
const chunkWords = words.slice(i, i + options.maxWordsPerChunk);
const chunkText = chunkWords.join(' ');
if (chunkText.length > options.minChunkLength) {
chunks.push({
text: chunkText,
source: `long_sentence_chunk_${chunkIndex}_part_${Math.floor(i / options.maxWordsPerChunk)}`,
index: chunkIndex,
wordCount: chunkWords.length,
});
}
}
chunkIndex++;
}
}
return chunks;
}
/**
* Fallback chunking (when sentence splitting fails)
*/
private fallbackChunking(
content: string,
existingChunks: TextChunk[],
options: Required<ChunkingOptions>,
): TextChunk[] {
const chunks = [...existingChunks];
let chunkIndex = chunks.length;
const paragraphs = content
.split(/\n\s*\n/)
.filter((p) => p.trim().length > options.minChunkLength);
if (paragraphs.length > 1) {
paragraphs.forEach((paragraph, index) => {
const cleanParagraph = paragraph.trim();
if (cleanParagraph.length > 0) {
const words = cleanParagraph.split(/\s+/);
const maxWordsPerChunk = 150;
for (let i = 0; i < words.length; i += maxWordsPerChunk) {
const chunkWords = words.slice(i, i + maxWordsPerChunk);
const chunkText = chunkWords.join(' ');
if (chunkText.length > options.minChunkLength) {
chunks.push({
text: chunkText,
source: `paragraph_${index}_chunk_${Math.floor(i / maxWordsPerChunk)}`,
index: chunkIndex,
wordCount: chunkWords.length,
});
chunkIndex++;
}
}
}
});
} else {
const words = content.trim().split(/\s+/);
const maxWordsPerChunk = 150;
for (let i = 0; i < words.length; i += maxWordsPerChunk) {
const chunkWords = words.slice(i, i + maxWordsPerChunk);
const chunkText = chunkWords.join(' ');
if (chunkText.length > options.minChunkLength) {
chunks.push({
text: chunkText,
source: `content_chunk_${Math.floor(i / maxWordsPerChunk)}`,
index: chunkIndex,
wordCount: chunkWords.length,
});
chunkIndex++;
}
}
}
return chunks;
}
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/web-fetcher.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
interface WebFetcherToolParams {
htmlContent?: boolean; // get the visible HTML content of the current page. default: false
textContent?: boolean; // get the visible text content of the current page. default: true
url?: string; // optional URL to fetch content from (if not provided, uses active tab)
selector?: string; // optional CSS selector to get content from a specific element
}
class WebFetcherTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.WEB_FETCHER;
/**
* Execute web fetcher operation
*/
async execute(args: WebFetcherToolParams): Promise<ToolResult> {
// Handle mutually exclusive parameters: if htmlContent is true, textContent is forced to false
const htmlContent = args.htmlContent === true;
const textContent = htmlContent ? false : args.textContent !== false; // Default is true, unless htmlContent is true or textContent is explicitly set to false
const url = args.url;
const selector = args.selector;
console.log(`Starting web fetcher with options:`, {
htmlContent,
textContent,
url,
selector,
});
try {
// Get tab to fetch content from
let tab;
if (url) {
// If URL is provided, check if it's already open
console.log(`Checking if URL is already open: ${url}`);
const allTabs = await chrome.tabs.query({});
// Find tab with matching URL
const matchingTabs = allTabs.filter((t) => {
// Normalize URLs for comparison (remove trailing slashes)
const tabUrl = t.url?.endsWith('/') ? t.url.slice(0, -1) : t.url;
const targetUrl = url.endsWith('/') ? url.slice(0, -1) : url;
return tabUrl === targetUrl;
});
if (matchingTabs.length > 0) {
// Use existing tab
tab = matchingTabs[0];
console.log(`Found existing tab with URL: ${url}, tab ID: ${tab.id}`);
} else {
// Create new tab with the URL
console.log(`No existing tab found with URL: ${url}, creating new tab`);
tab = await chrome.tabs.create({ url, active: true });
// Wait for page to load
console.log('Waiting for page to load...');
await new Promise((resolve) => setTimeout(resolve, 3000));
}
} else {
// Use active tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]) {
return createErrorResponse('No active tab found');
}
tab = tabs[0];
}
if (!tab.id) {
return createErrorResponse('Tab has no ID');
}
// Make sure tab is active
await chrome.tabs.update(tab.id, { active: true });
// Prepare result object
const result: any = {
success: true,
url: tab.url,
title: tab.title,
};
await this.injectContentScript(tab.id, ['inject-scripts/web-fetcher-helper.js']);
// Get HTML content if requested
if (htmlContent) {
const htmlResponse = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.WEB_FETCHER_GET_HTML_CONTENT,
selector: selector,
});
if (htmlResponse.success) {
result.htmlContent = htmlResponse.htmlContent;
} else {
console.error('Failed to get HTML content:', htmlResponse.error);
result.htmlContentError = htmlResponse.error;
}
}
// Get text content if requested (and htmlContent is not true)
if (textContent) {
const textResponse = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.WEB_FETCHER_GET_TEXT_CONTENT,
selector: selector,
});
if (textResponse.success) {
result.textContent = textResponse.textContent;
// Include article metadata if available
if (textResponse.article) {
result.article = {
title: textResponse.article.title,
byline: textResponse.article.byline,
siteName: textResponse.article.siteName,
excerpt: textResponse.article.excerpt,
lang: textResponse.article.lang,
};
}
// Include page metadata if available
if (textResponse.metadata) {
result.metadata = textResponse.metadata;
}
} else {
console.error('Failed to get text content:', textResponse.error);
result.textContentError = textResponse.error;
}
}
// Interactive elements feature has been removed
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
isError: false,
};
} catch (error) {
console.error('Error in web fetcher:', error);
return createErrorResponse(
`Error fetching web content: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const webFetcherTool = new WebFetcherTool();
interface GetInteractiveElementsToolParams {
textQuery?: string; // Text to search for within interactive elements (fuzzy search)
selector?: string; // CSS selector to filter interactive elements
includeCoordinates?: boolean; // Include element coordinates in the response (default: true)
types?: string[]; // Types of interactive elements to include (default: all types)
}
class GetInteractiveElementsTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.GET_INTERACTIVE_ELEMENTS;
/**
* Execute get interactive elements operation
*/
async execute(args: GetInteractiveElementsToolParams): Promise<ToolResult> {
const { textQuery, selector, includeCoordinates = true, types } = args;
console.log(`Starting get interactive elements with options:`, args);
try {
// Get current tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]) {
return createErrorResponse('No active tab found');
}
const tab = tabs[0];
if (!tab.id) {
return createErrorResponse('Active tab has no ID');
}
// Ensure content script is injected
await this.injectContentScript(tab.id, ['inject-scripts/interactive-elements-helper.js']);
// Send message to content script
const result = await this.sendMessageToTab(tab.id, {
action: TOOL_MESSAGE_TYPES.GET_INTERACTIVE_ELEMENTS,
textQuery,
selector,
includeCoordinates,
types,
});
if (!result.success) {
return createErrorResponse(result.error || 'Failed to get interactive elements');
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
elements: result.elements,
count: result.elements.length,
query: {
textQuery,
selector,
types: types || 'all',
},
}),
},
],
isError: false,
};
} catch (error) {
console.error('Error in get interactive elements operation:', error);
return createErrorResponse(
`Error getting interactive elements: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
export const getInteractiveElementsTool = new GetInteractiveElementsTool();
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/native-host.ts:
--------------------------------------------------------------------------------
```typescript
import { NativeMessageType } from 'chrome-mcp-shared';
import { BACKGROUND_MESSAGE_TYPES } from '@/common/message-types';
import {
NATIVE_HOST,
ICONS,
NOTIFICATIONS,
STORAGE_KEYS,
ERROR_MESSAGES,
SUCCESS_MESSAGES,
} from '@/common/constants';
import { handleCallTool } from './tools';
let nativePort: chrome.runtime.Port | null = null;
export const HOST_NAME = NATIVE_HOST.NAME;
/**
* Server status management interface
*/
interface ServerStatus {
isRunning: boolean;
port?: number;
lastUpdated: number;
}
let currentServerStatus: ServerStatus = {
isRunning: false,
lastUpdated: Date.now(),
};
/**
* Save server status to chrome.storage
*/
async function saveServerStatus(status: ServerStatus): Promise<void> {
try {
await chrome.storage.local.set({ [STORAGE_KEYS.SERVER_STATUS]: status });
} catch (error) {
console.error(ERROR_MESSAGES.SERVER_STATUS_SAVE_FAILED, error);
}
}
/**
* Load server status from chrome.storage
*/
async function loadServerStatus(): Promise<ServerStatus> {
try {
const result = await chrome.storage.local.get([STORAGE_KEYS.SERVER_STATUS]);
if (result[STORAGE_KEYS.SERVER_STATUS]) {
return result[STORAGE_KEYS.SERVER_STATUS];
}
} catch (error) {
console.error(ERROR_MESSAGES.SERVER_STATUS_LOAD_FAILED, error);
}
return {
isRunning: false,
lastUpdated: Date.now(),
};
}
/**
* Broadcast server status change to all listeners
*/
function broadcastServerStatusChange(status: ServerStatus): void {
chrome.runtime
.sendMessage({
type: BACKGROUND_MESSAGE_TYPES.SERVER_STATUS_CHANGED,
payload: status,
})
.catch(() => {
// Ignore errors if no listeners are present
});
}
/**
* Connect to the native messaging host
*/
export function connectNativeHost(port: number = NATIVE_HOST.DEFAULT_PORT) {
if (nativePort) {
return;
}
try {
nativePort = chrome.runtime.connectNative(HOST_NAME);
nativePort.onMessage.addListener(async (message) => {
// chrome.notifications.create({
// type: NOTIFICATIONS.TYPE,
// iconUrl: chrome.runtime.getURL(ICONS.NOTIFICATION),
// title: 'Message from native host',
// message: `Received data from host: ${JSON.stringify(message)}`,
// priority: NOTIFICATIONS.PRIORITY,
// });
if (message.type === NativeMessageType.PROCESS_DATA && message.requestId) {
const requestId = message.requestId;
const requestPayload = message.payload;
nativePort?.postMessage({
responseToRequestId: requestId,
payload: {
status: 'success',
message: SUCCESS_MESSAGES.TOOL_EXECUTED,
data: requestPayload,
},
});
} else if (message.type === NativeMessageType.CALL_TOOL && message.requestId) {
const requestId = message.requestId;
try {
const result = await handleCallTool(message.payload);
nativePort?.postMessage({
responseToRequestId: requestId,
payload: {
status: 'success',
message: SUCCESS_MESSAGES.TOOL_EXECUTED,
data: result,
},
});
} catch (error) {
nativePort?.postMessage({
responseToRequestId: requestId,
payload: {
status: 'error',
message: ERROR_MESSAGES.TOOL_EXECUTION_FAILED,
error: error instanceof Error ? error.message : String(error),
},
});
}
} else if (message.type === NativeMessageType.SERVER_STARTED) {
const port = message.payload?.port;
currentServerStatus = {
isRunning: true,
port: port,
lastUpdated: Date.now(),
};
await saveServerStatus(currentServerStatus);
broadcastServerStatusChange(currentServerStatus);
console.log(`${SUCCESS_MESSAGES.SERVER_STARTED} on port ${port}`);
} else if (message.type === NativeMessageType.SERVER_STOPPED) {
currentServerStatus = {
isRunning: false,
port: currentServerStatus.port, // Keep last known port for reconnection
lastUpdated: Date.now(),
};
await saveServerStatus(currentServerStatus);
broadcastServerStatusChange(currentServerStatus);
console.log(SUCCESS_MESSAGES.SERVER_STOPPED);
} else if (message.type === NativeMessageType.ERROR_FROM_NATIVE_HOST) {
console.error('Error from native host:', message.payload?.message || 'Unknown error');
} else if (message.type === 'file_operation_response') {
// Forward file operation response back to the requesting tool
chrome.runtime.sendMessage(message).catch(() => {
// Ignore if no listeners
});
}
});
nativePort.onDisconnect.addListener(() => {
console.error(ERROR_MESSAGES.NATIVE_DISCONNECTED, chrome.runtime.lastError);
nativePort = null;
});
nativePort.postMessage({ type: NativeMessageType.START, payload: { port } });
} catch (error) {
console.error(ERROR_MESSAGES.NATIVE_CONNECTION_FAILED, error);
}
}
/**
* Initialize native host listeners and load initial state
*/
export const initNativeHostListener = () => {
// Initialize server status from storage
loadServerStatus()
.then((status) => {
currentServerStatus = status;
})
.catch((error) => {
console.error(ERROR_MESSAGES.SERVER_STATUS_LOAD_FAILED, error);
});
chrome.runtime.onStartup.addListener(connectNativeHost);
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (
message === NativeMessageType.CONNECT_NATIVE ||
message.type === NativeMessageType.CONNECT_NATIVE
) {
const port =
typeof message === 'object' && message.port ? message.port : NATIVE_HOST.DEFAULT_PORT;
connectNativeHost(port);
sendResponse({ success: true, port });
return true;
}
if (message.type === NativeMessageType.PING_NATIVE) {
const connected = nativePort !== null;
sendResponse({ connected });
return true;
}
if (message.type === NativeMessageType.DISCONNECT_NATIVE) {
if (nativePort) {
nativePort.disconnect();
nativePort = null;
sendResponse({ success: true });
} else {
sendResponse({ success: false, error: 'No active connection' });
}
return true;
}
if (message.type === BACKGROUND_MESSAGE_TYPES.GET_SERVER_STATUS) {
sendResponse({
success: true,
serverStatus: currentServerStatus,
connected: nativePort !== null,
});
return true;
}
if (message.type === BACKGROUND_MESSAGE_TYPES.REFRESH_SERVER_STATUS) {
loadServerStatus()
.then((storedStatus) => {
currentServerStatus = storedStatus;
sendResponse({
success: true,
serverStatus: currentServerStatus,
connected: nativePort !== null,
});
})
.catch((error) => {
console.error(ERROR_MESSAGES.SERVER_STATUS_LOAD_FAILED, error);
sendResponse({
success: false,
error: ERROR_MESSAGES.SERVER_STATUS_LOAD_FAILED,
serverStatus: currentServerStatus,
connected: nativePort !== null,
});
});
return true;
}
// Forward file operation messages to native host
if (message.type === 'forward_to_native' && message.message) {
if (nativePort) {
nativePort.postMessage(message.message);
sendResponse({ success: true });
} else {
sendResponse({ success: false, error: 'Native host not connected' });
}
return true;
}
});
};
```
--------------------------------------------------------------------------------
/app/native-server/src/scripts/browser-config.ts:
--------------------------------------------------------------------------------
```typescript
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { execSync } from 'child_process';
import { HOST_NAME } from './constant';
export enum BrowserType {
CHROME = 'chrome',
CHROMIUM = 'chromium',
}
export interface BrowserConfig {
type: BrowserType;
displayName: string;
userManifestPath: string;
systemManifestPath: string;
registryKey?: string; // Windows only
systemRegistryKey?: string; // Windows only
}
/**
* Get the user-level manifest path for a specific browser
*/
function getUserManifestPathForBrowser(browser: BrowserType): string {
const platform = os.platform();
if (platform === 'win32') {
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
switch (browser) {
case BrowserType.CHROME:
return path.join(appData, 'Google', 'Chrome', 'NativeMessagingHosts', `${HOST_NAME}.json`);
case BrowserType.CHROMIUM:
return path.join(appData, 'Chromium', 'NativeMessagingHosts', `${HOST_NAME}.json`);
default:
return path.join(appData, 'Google', 'Chrome', 'NativeMessagingHosts', `${HOST_NAME}.json`);
}
} else if (platform === 'darwin') {
const home = os.homedir();
switch (browser) {
case BrowserType.CHROME:
return path.join(
home,
'Library',
'Application Support',
'Google',
'Chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
case BrowserType.CHROMIUM:
return path.join(
home,
'Library',
'Application Support',
'Chromium',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
default:
return path.join(
home,
'Library',
'Application Support',
'Google',
'Chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
}
} else {
// Linux
const home = os.homedir();
switch (browser) {
case BrowserType.CHROME:
return path.join(
home,
'.config',
'google-chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
case BrowserType.CHROMIUM:
return path.join(home, '.config', 'chromium', 'NativeMessagingHosts', `${HOST_NAME}.json`);
default:
return path.join(
home,
'.config',
'google-chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
}
}
}
/**
* Get the system-level manifest path for a specific browser
*/
function getSystemManifestPathForBrowser(browser: BrowserType): string {
const platform = os.platform();
if (platform === 'win32') {
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
switch (browser) {
case BrowserType.CHROME:
return path.join(
programFiles,
'Google',
'Chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
case BrowserType.CHROMIUM:
return path.join(programFiles, 'Chromium', 'NativeMessagingHosts', `${HOST_NAME}.json`);
default:
return path.join(
programFiles,
'Google',
'Chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
}
} else if (platform === 'darwin') {
switch (browser) {
case BrowserType.CHROME:
return path.join(
'/Library',
'Google',
'Chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
case BrowserType.CHROMIUM:
return path.join(
'/Library',
'Application Support',
'Chromium',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
default:
return path.join(
'/Library',
'Google',
'Chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
}
} else {
// Linux
switch (browser) {
case BrowserType.CHROME:
return path.join('/etc', 'opt', 'chrome', 'native-messaging-hosts', `${HOST_NAME}.json`);
case BrowserType.CHROMIUM:
return path.join('/etc', 'chromium', 'native-messaging-hosts', `${HOST_NAME}.json`);
default:
return path.join('/etc', 'opt', 'chrome', 'native-messaging-hosts', `${HOST_NAME}.json`);
}
}
}
/**
* Get Windows registry keys for a browser
*/
function getRegistryKeys(browser: BrowserType): { user: string; system: string } | undefined {
if (os.platform() !== 'win32') return undefined;
const browserPaths: Record<BrowserType, { user: string; system: string }> = {
[BrowserType.CHROME]: {
user: `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`,
system: `HKLM\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`,
},
[BrowserType.CHROMIUM]: {
user: `HKCU\\Software\\Chromium\\NativeMessagingHosts\\${HOST_NAME}`,
system: `HKLM\\Software\\Chromium\\NativeMessagingHosts\\${HOST_NAME}`,
},
};
return browserPaths[browser];
}
/**
* Get browser configuration
*/
export function getBrowserConfig(browser: BrowserType): BrowserConfig {
const registryKeys = getRegistryKeys(browser);
return {
type: browser,
displayName: browser.charAt(0).toUpperCase() + browser.slice(1),
userManifestPath: getUserManifestPathForBrowser(browser),
systemManifestPath: getSystemManifestPathForBrowser(browser),
registryKey: registryKeys?.user,
systemRegistryKey: registryKeys?.system,
};
}
/**
* Detect installed browsers on the system
*/
export function detectInstalledBrowsers(): BrowserType[] {
const detectedBrowsers: BrowserType[] = [];
const platform = os.platform();
if (platform === 'win32') {
// Check Windows registry for installed browsers
const browsers: Array<{ type: BrowserType; registryPath: string }> = [
{ type: BrowserType.CHROME, registryPath: 'HKLM\\SOFTWARE\\Google\\Chrome' },
{ type: BrowserType.CHROMIUM, registryPath: 'HKLM\\SOFTWARE\\Chromium' },
];
for (const browser of browsers) {
try {
execSync(`reg query "${browser.registryPath}" 2>nul`, { stdio: 'pipe' });
detectedBrowsers.push(browser.type);
} catch {
// Browser not installed
}
}
} else if (platform === 'darwin') {
// Check macOS Applications folder
const browsers: Array<{ type: BrowserType; appPath: string }> = [
{ type: BrowserType.CHROME, appPath: '/Applications/Google Chrome.app' },
{ type: BrowserType.CHROMIUM, appPath: '/Applications/Chromium.app' },
];
for (const browser of browsers) {
if (fs.existsSync(browser.appPath)) {
detectedBrowsers.push(browser.type);
}
}
} else {
// Check Linux paths using which command
const browsers: Array<{ type: BrowserType; commands: string[] }> = [
{ type: BrowserType.CHROME, commands: ['google-chrome', 'google-chrome-stable'] },
{ type: BrowserType.CHROMIUM, commands: ['chromium', 'chromium-browser'] },
];
for (const browser of browsers) {
for (const cmd of browser.commands) {
try {
execSync(`which ${cmd} 2>/dev/null`, { stdio: 'pipe' });
detectedBrowsers.push(browser.type);
break; // Found one command, no need to check others
} catch {
// Command not found
}
}
}
}
return detectedBrowsers;
}
/**
* Get all supported browser configs
*/
export function getAllBrowserConfigs(): BrowserConfig[] {
return Object.values(BrowserType).map((browser) => getBrowserConfig(browser));
}
/**
* Parse browser type from string
*/
export function parseBrowserType(browserStr: string): BrowserType | undefined {
const normalized = browserStr.toLowerCase();
return Object.values(BrowserType).find((type) => type === normalized);
}
```
--------------------------------------------------------------------------------
/packages/wasm-simd/src/lib.rs:
--------------------------------------------------------------------------------
```rust
use wasm_bindgen::prelude::*;
use wide::f32x4;
// 设置 panic hook 以便在浏览器中调试
#[wasm_bindgen(start)]
pub fn main() {
console_error_panic_hook::set_once();
}
#[wasm_bindgen]
pub struct SIMDMath;
#[wasm_bindgen]
impl SIMDMath {
#[wasm_bindgen(constructor)]
pub fn new() -> SIMDMath {
SIMDMath
}
// 辅助函数:仅计算点积 (SIMD)
#[inline]
fn dot_product_simd_only(&self, vec_a: &[f32], vec_b: &[f32]) -> f32 {
let len = vec_a.len();
let simd_lanes = 4;
let simd_len = len - (len % simd_lanes);
let mut dot_sum_simd = f32x4::ZERO;
for i in (0..simd_len).step_by(simd_lanes) {
// 使用 try_from 和 new 方法,这是 wide 库的正确 API
let a_array: [f32; 4] = vec_a[i..i + simd_lanes].try_into().unwrap();
let b_array: [f32; 4] = vec_b[i..i + simd_lanes].try_into().unwrap();
let a_chunk = f32x4::new(a_array);
let b_chunk = f32x4::new(b_array);
dot_sum_simd = a_chunk.mul_add(b_chunk, dot_sum_simd);
}
let mut dot_product = dot_sum_simd.reduce_add();
for i in simd_len..len {
dot_product += vec_a[i] * vec_b[i];
}
dot_product
}
#[wasm_bindgen]
pub fn cosine_similarity(&self, vec_a: &[f32], vec_b: &[f32]) -> f32 {
if vec_a.len() != vec_b.len() || vec_a.is_empty() {
return 0.0;
}
let len = vec_a.len();
let simd_lanes = 4;
let simd_len = len - (len % simd_lanes);
let mut dot_sum_simd = f32x4::ZERO;
let mut norm_a_sum_simd = f32x4::ZERO;
let mut norm_b_sum_simd = f32x4::ZERO;
// SIMD 处理
for i in (0..simd_len).step_by(simd_lanes) {
let a_array: [f32; 4] = vec_a[i..i + simd_lanes].try_into().unwrap();
let b_array: [f32; 4] = vec_b[i..i + simd_lanes].try_into().unwrap();
let a_chunk = f32x4::new(a_array);
let b_chunk = f32x4::new(b_array);
// 使用 Fused Multiply-Add (FMA)
dot_sum_simd = a_chunk.mul_add(b_chunk, dot_sum_simd);
norm_a_sum_simd = a_chunk.mul_add(a_chunk, norm_a_sum_simd);
norm_b_sum_simd = b_chunk.mul_add(b_chunk, norm_b_sum_simd);
}
// 水平求和
let mut dot_product = dot_sum_simd.reduce_add();
let mut norm_a_sq = norm_a_sum_simd.reduce_add();
let mut norm_b_sq = norm_b_sum_simd.reduce_add();
// 处理剩余元素
for i in simd_len..len {
dot_product += vec_a[i] * vec_b[i];
norm_a_sq += vec_a[i] * vec_a[i];
norm_b_sq += vec_b[i] * vec_b[i];
}
// 优化的数值稳定性处理
let norm_a = norm_a_sq.sqrt();
let norm_b = norm_b_sq.sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
return 0.0;
}
let magnitude = norm_a * norm_b;
// 限制结果在 [-1.0, 1.0] 范围内,处理浮点精度误差
(dot_product / magnitude).max(-1.0).min(1.0)
}
#[wasm_bindgen]
pub fn batch_similarity(&self, vectors: &[f32], query: &[f32], vector_dim: usize) -> Vec<f32> {
if vector_dim == 0 { return Vec::new(); }
if vectors.len() % vector_dim != 0 { return Vec::new(); }
if query.len() != vector_dim { return Vec::new(); }
let num_vectors = vectors.len() / vector_dim;
let mut results = Vec::with_capacity(num_vectors);
// 预计算查询向量的范数
let query_norm_sq = self.compute_norm_squared_simd(query);
if query_norm_sq == 0.0 {
return vec![0.0; num_vectors];
}
let query_norm = query_norm_sq.sqrt();
for i in 0..num_vectors {
let start = i * vector_dim;
let vector_slice = &vectors[start..start + vector_dim];
// dot_product_and_norm_simd 计算 vector_slice (vec_a) 的范数
let (dot_product, vector_norm_sq) = self.dot_product_and_norm_simd(vector_slice, query);
if vector_norm_sq == 0.0 {
results.push(0.0);
} else {
let vector_norm = vector_norm_sq.sqrt();
let similarity = dot_product / (vector_norm * query_norm);
results.push(similarity.max(-1.0).min(1.0));
}
}
results
}
// 辅助函数:SIMD 计算范数平方
#[inline]
fn compute_norm_squared_simd(&self, vec: &[f32]) -> f32 {
let len = vec.len();
let simd_lanes = 4;
let simd_len = len - (len % simd_lanes);
let mut norm_sum_simd = f32x4::ZERO;
for i in (0..simd_len).step_by(simd_lanes) {
let array: [f32; 4] = vec[i..i + simd_lanes].try_into().unwrap();
let chunk = f32x4::new(array);
norm_sum_simd = chunk.mul_add(chunk, norm_sum_simd);
}
let mut norm_sq = norm_sum_simd.reduce_add();
for i in simd_len..len {
norm_sq += vec[i] * vec[i];
}
norm_sq
}
// 辅助函数:同时计算点积和vec_a的范数平方
#[inline]
fn dot_product_and_norm_simd(&self, vec_a: &[f32], vec_b: &[f32]) -> (f32, f32) {
let len = vec_a.len(); // 假设 vec_a.len() == vec_b.len()
let simd_lanes = 4;
let simd_len = len - (len % simd_lanes);
let mut dot_sum_simd = f32x4::ZERO;
let mut norm_a_sum_simd = f32x4::ZERO;
for i in (0..simd_len).step_by(simd_lanes) {
let a_array: [f32; 4] = vec_a[i..i + simd_lanes].try_into().unwrap();
let b_array: [f32; 4] = vec_b[i..i + simd_lanes].try_into().unwrap();
let a_chunk = f32x4::new(a_array);
let b_chunk = f32x4::new(b_array);
dot_sum_simd = a_chunk.mul_add(b_chunk, dot_sum_simd);
norm_a_sum_simd = a_chunk.mul_add(a_chunk, norm_a_sum_simd);
}
let mut dot_product = dot_sum_simd.reduce_add();
let mut norm_a_sq = norm_a_sum_simd.reduce_add();
for i in simd_len..len {
dot_product += vec_a[i] * vec_b[i];
norm_a_sq += vec_a[i] * vec_a[i];
}
(dot_product, norm_a_sq)
}
// 批量矩阵相似度计算 - 优化版
#[wasm_bindgen]
pub fn similarity_matrix(&self, vectors_a: &[f32], vectors_b: &[f32], vector_dim: usize) -> Vec<f32> {
if vector_dim == 0 || vectors_a.len() % vector_dim != 0 || vectors_b.len() % vector_dim != 0 {
return Vec::new();
}
let num_a = vectors_a.len() / vector_dim;
let num_b = vectors_b.len() / vector_dim;
let mut results = Vec::with_capacity(num_a * num_b);
// 1. 预计算 vectors_a 的范数
let norms_a: Vec<f32> = (0..num_a)
.map(|i| {
let start = i * vector_dim;
let vec_a_slice = &vectors_a[start..start + vector_dim];
self.compute_norm_squared_simd(vec_a_slice).sqrt()
})
.collect();
// 2. 预计算 vectors_b 的范数
let norms_b: Vec<f32> = (0..num_b)
.map(|j| {
let start = j * vector_dim;
let vec_b_slice = &vectors_b[start..start + vector_dim];
self.compute_norm_squared_simd(vec_b_slice).sqrt()
})
.collect();
for i in 0..num_a {
let start_a = i * vector_dim;
let vec_a = &vectors_a[start_a..start_a + vector_dim];
let norm_a = norms_a[i];
if norm_a == 0.0 {
// 如果 norm_a 为 0,所有相似度都为 0
for _ in 0..num_b {
results.push(0.0);
}
continue;
}
for j in 0..num_b {
let start_b = j * vector_dim;
let vec_b = &vectors_b[start_b..start_b + vector_dim];
let norm_b = norms_b[j];
if norm_b == 0.0 {
results.push(0.0);
continue;
}
// 使用专用的点积函数
let dot_product = self.dot_product_simd_only(vec_a, vec_b);
let magnitude = norm_a * norm_b;
// magnitude 不应该为零,因为已经检查了 norm_a/norm_b
let similarity = (dot_product / magnitude).max(-1.0).min(1.0);
results.push(similarity);
}
}
results
}
}
```
--------------------------------------------------------------------------------
/test-inject-script.js:
--------------------------------------------------------------------------------
```javascript
(() => {
const SCRIPT_ID = 'excalidraw-control-script';
if (window[SCRIPT_ID]) {
return;
}
function getExcalidrawAPIFromDOM(domElement) {
if (!domElement) {
return null;
}
const reactFiberKey = Object.keys(domElement).find(
(key) => key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$'),
);
if (!reactFiberKey) {
return null;
}
let fiberNode = domElement[reactFiberKey];
if (!fiberNode) {
return null;
}
function isExcalidrawAPI(obj) {
return (
typeof obj === 'object' &&
obj !== null &&
typeof obj.updateScene === 'function' &&
typeof obj.getSceneElements === 'function' &&
typeof obj.getAppState === 'function'
);
}
function findApiInObject(objToSearch) {
if (isExcalidrawAPI(objToSearch)) {
return objToSearch;
}
if (typeof objToSearch === 'object' && objToSearch !== null) {
for (const key in objToSearch) {
if (Object.prototype.hasOwnProperty.call(objToSearch, key)) {
const found = findApiInObject(objToSearch[key]);
if (found) {
return found;
}
}
}
}
return null;
}
let excalidrawApiInstance = null;
let attempts = 0;
const MAX_TRAVERSAL_ATTEMPTS = 25;
while (fiberNode && attempts < MAX_TRAVERSAL_ATTEMPTS) {
if (fiberNode.stateNode && fiberNode.stateNode.props) {
const api = findApiInObject(fiberNode.stateNode.props);
if (api) {
excalidrawApiInstance = api;
break;
}
if (isExcalidrawAPI(fiberNode.stateNode.props.excalidrawAPI)) {
excalidrawApiInstance = fiberNode.stateNode.props.excalidrawAPI;
break;
}
}
if (fiberNode.memoizedProps) {
const api = findApiInObject(fiberNode.memoizedProps);
if (api) {
excalidrawApiInstance = api;
break;
}
if (isExcalidrawAPI(fiberNode.memoizedProps.excalidrawAPI)) {
excalidrawApiInstance = fiberNode.memoizedProps.excalidrawAPI;
break;
}
}
if (fiberNode.tag === 1 && fiberNode.stateNode && fiberNode.stateNode.state) {
const api = findApiInObject(fiberNode.stateNode.state);
if (api) {
excalidrawApiInstance = api;
break;
}
}
if (
fiberNode.tag === 0 ||
fiberNode.tag === 2 ||
fiberNode.tag === 14 ||
fiberNode.tag === 15 ||
fiberNode.tag === 11
) {
if (fiberNode.memoizedState) {
let currentHook = fiberNode.memoizedState;
let hookAttempts = 0;
const MAX_HOOK_ATTEMPTS = 15;
while (currentHook && hookAttempts < MAX_HOOK_ATTEMPTS) {
const api = findApiInObject(currentHook.memoizedState);
if (api) {
excalidrawApiInstance = api;
break;
}
currentHook = currentHook.next;
hookAttempts++;
}
if (excalidrawApiInstance) break;
}
}
if (fiberNode.stateNode) {
const api = findApiInObject(fiberNode.stateNode);
if (api && api !== fiberNode.stateNode.props && api !== fiberNode.stateNode.state) {
excalidrawApiInstance = api;
break;
}
}
if (
fiberNode.tag === 9 &&
fiberNode.memoizedProps &&
typeof fiberNode.memoizedProps.value !== 'undefined'
) {
const api = findApiInObject(fiberNode.memoizedProps.value);
if (api) {
excalidrawApiInstance = api;
break;
}
}
if (fiberNode.return) {
fiberNode = fiberNode.return;
} else {
break;
}
attempts++;
}
if (excalidrawApiInstance) {
window.excalidrawAPI = excalidrawApiInstance;
console.log('现在您可以通过 `window.foundExcalidrawAPI` 在控制台访问它。');
} else {
console.error('在检查组件树后未能找到 excalidrawAPI。');
}
return excalidrawApiInstance;
}
function createFullExcalidrawElement(skeleton) {
const id = Math.random().toString(36).substring(2, 9);
const seed = Math.floor(Math.random() * 2 ** 31);
const versionNonce = Math.floor(Math.random() * 2 ** 31);
const defaults = {
isDeleted: false,
fillStyle: 'hachure',
strokeWidth: 1,
strokeStyle: 'solid',
roughness: 1,
opacity: 100,
angle: 0,
groupIds: [],
strokeColor: '#000000',
backgroundColor: 'transparent',
version: 1,
locked: false,
};
const fullElement = {
id: id,
seed: seed,
versionNonce: versionNonce,
updated: Date.now(),
...defaults,
...skeleton,
};
return fullElement;
}
let targetElementForAPI = document.querySelector('.excalidraw-app');
if (targetElementForAPI) {
getExcalidrawAPIFromDOM(targetElementForAPI);
}
const eventHandler = {
getSceneElements: () => {
try {
return window.excalidrawAPI.getSceneElements();
} catch (error) {
return {
error: true,
msg: JSON.stringify(error),
};
}
},
addElement: (param) => {
try {
const existingElements = window.excalidrawAPI.getSceneElements();
const newElements = [...existingElements];
param.eles.forEach((ele, idx) => {
const newEle = createFullExcalidrawElement(ele);
newEle.index = `a${existingElements.length + idx + 1}`;
newElements.push(newEle);
});
console.log('newElements ==>', newElements);
const appState = window.excalidrawAPI.getAppState();
window.excalidrawAPI.updateScene({
elements: newElements,
appState: appState,
commitToHistory: true,
});
return {
success: true,
};
} catch (error) {
return {
error: true,
msg: JSON.stringify(error),
};
}
},
deleteElement: (param) => {
try {
const existingElements = window.excalidrawAPI.getSceneElements();
const newElements = [...existingElements];
const idx = newElements.findIndex((e) => e.id === param.id);
if (idx >= 0) {
newElements.splice(idx, 1);
const appState = window.excalidrawAPI.getAppState();
window.excalidrawAPI.updateScene({
elements: newElements,
appState: appState,
commitToHistory: true,
});
return {
success: true,
};
} else {
return {
error: true,
msg: 'element not found',
};
}
} catch (error) {
return {
error: true,
msg: JSON.stringify(error),
};
}
},
updateElement: (param) => {
try {
const existingElements = window.excalidrawAPI.getSceneElements();
const resIds = [];
for (let i = 0; i < param.length; i++) {
const idx = existingElements.findIndex((e) => e.id === param[i].id);
if (idx >= 0) {
resIds.push[idx];
window.excalidrawAPI.mutateElement(existingElements[idx], { ...param[i] });
}
}
return {
success: true,
msg: `已更新元素:${resIds.join(',')}`,
};
} catch (error) {
return {
error: true,
msg: JSON.stringify(error),
};
}
},
cleanup: () => {
try {
window.excalidrawAPI.resetScene();
return {
success: true,
};
} catch (error) {
return {
error: true,
msg: JSON.stringify(error),
};
}
},
};
const handleExecution = (event) => {
const { action, payload, requestId } = event.detail;
const param = JSON.parse(payload || '{}');
let data, error;
try {
const handler = eventHandler[action];
if (!handler) {
error = 'event name not found';
}
data = handler(param);
} catch (e) {
error = e.message;
}
window.dispatchEvent(
new CustomEvent('chrome-mcp:response', { detail: { requestId, data, error } }),
);
};
// --- Lifecycle Functions ---
const initialize = () => {
window.addEventListener('chrome-mcp:execute', handleExecution);
window.addEventListener('chrome-mcp:cleanup', cleanup);
window[SCRIPT_ID] = true;
};
const cleanup = () => {
window.removeEventListener('chrome-mcp:execute', handleExecution);
window.removeEventListener('chrome-mcp:cleanup', cleanup);
delete window[SCRIPT_ID];
delete window.excalidrawAPI;
};
initialize();
})();
```
--------------------------------------------------------------------------------
/app/native-server/src/scripts/postinstall.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import fs from 'fs';
import os from 'os';
import path from 'path';
import { COMMAND_NAME } from './constant';
import { colorText, tryRegisterUserLevelHost } from './utils';
// Check if this script is run directly
const isDirectRun = require.main === module;
// Detect global installation for both npm and pnpm
function detectGlobalInstall(): boolean {
// npm uses npm_config_global
if (process.env.npm_config_global === 'true') {
return true;
}
// pnpm detection methods
// Method 1: Check if PNPM_HOME is set and current path contains it
if (process.env.PNPM_HOME && __dirname.includes(process.env.PNPM_HOME)) {
return true;
}
// Method 2: Check if we're in a global pnpm directory structure
// pnpm global packages are typically installed in ~/.local/share/pnpm/global/5/node_modules
// Windows: %APPDATA%\pnpm\global\5\node_modules
const globalPnpmPatterns =
process.platform === 'win32'
? ['\\pnpm\\global\\', '\\pnpm-global\\', '\\AppData\\Roaming\\pnpm\\']
: ['/pnpm/global/', '/.local/share/pnpm/', '/pnpm-global/'];
if (globalPnpmPatterns.some((pattern) => __dirname.includes(pattern))) {
return true;
}
// Method 3: Check npm_config_prefix for pnpm
if (process.env.npm_config_prefix && __dirname.includes(process.env.npm_config_prefix)) {
return true;
}
// Method 4: Windows-specific global installation paths
if (process.platform === 'win32') {
const windowsGlobalPatterns = [
'\\npm\\node_modules\\',
'\\AppData\\Roaming\\npm\\node_modules\\',
'\\Program Files\\nodejs\\node_modules\\',
'\\nodejs\\node_modules\\',
];
if (windowsGlobalPatterns.some((pattern) => __dirname.includes(pattern))) {
return true;
}
}
return false;
}
const isGlobalInstall = detectGlobalInstall();
/**
* Write Node.js path for run_host scripts to avoid fragile relative paths
*/
async function writeNodePath(): Promise<void> {
try {
const nodePath = process.execPath;
const nodePathFile = path.join(__dirname, '..', 'node_path.txt');
console.log(colorText(`Writing Node.js path: ${nodePath}`, 'blue'));
fs.writeFileSync(nodePathFile, nodePath, 'utf8');
console.log(colorText('✓ Node.js path written for run_host scripts', 'green'));
} catch (error: any) {
console.warn(colorText(`⚠️ Failed to write Node.js path: ${error.message}`, 'yellow'));
}
}
/**
* 确保执行权限(无论是否为全局安装)
*/
async function ensureExecutionPermissions(): Promise<void> {
if (process.platform === 'win32') {
// Windows 平台处理
await ensureWindowsFilePermissions();
return;
}
// Unix/Linux 平台处理
const filesToCheck = [
path.join(__dirname, '..', 'index.js'),
path.join(__dirname, '..', 'run_host.sh'),
path.join(__dirname, '..', 'cli.js'),
];
for (const filePath of filesToCheck) {
if (fs.existsSync(filePath)) {
try {
fs.chmodSync(filePath, '755');
console.log(
colorText(`✓ Set execution permissions for ${path.basename(filePath)}`, 'green'),
);
} catch (err: any) {
console.warn(
colorText(
`⚠️ Unable to set execution permissions for ${path.basename(filePath)}: ${err.message}`,
'yellow',
),
);
}
} else {
console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
}
}
}
/**
* Windows 平台文件权限处理
*/
async function ensureWindowsFilePermissions(): Promise<void> {
const filesToCheck = [
path.join(__dirname, '..', 'index.js'),
path.join(__dirname, '..', 'run_host.bat'),
path.join(__dirname, '..', 'cli.js'),
];
for (const filePath of filesToCheck) {
if (fs.existsSync(filePath)) {
try {
// 检查文件是否为只读,如果是则移除只读属性
const stats = fs.statSync(filePath);
if (!(stats.mode & parseInt('200', 8))) {
// 检查写权限
// 尝试移除只读属性
fs.chmodSync(filePath, stats.mode | parseInt('200', 8));
console.log(
colorText(`✓ Removed read-only attribute from ${path.basename(filePath)}`, 'green'),
);
}
// 验证文件可读性
fs.accessSync(filePath, fs.constants.R_OK);
console.log(
colorText(`✓ Verified file accessibility for ${path.basename(filePath)}`, 'green'),
);
} catch (err: any) {
console.warn(
colorText(
`⚠️ Unable to verify file permissions for ${path.basename(filePath)}: ${err.message}`,
'yellow',
),
);
}
} else {
console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
}
}
}
async function tryRegisterNativeHost(): Promise<void> {
try {
console.log(colorText('Attempting to register Chrome Native Messaging host...', 'blue'));
// Always ensure execution permissions, regardless of installation type
await ensureExecutionPermissions();
if (isGlobalInstall) {
// First try user-level installation (no elevated permissions required)
const userLevelSuccess = await tryRegisterUserLevelHost();
if (!userLevelSuccess) {
// User-level installation failed, suggest using register command
console.log(
colorText(
'User-level installation failed, system-level installation may be needed',
'yellow',
),
);
console.log(
colorText('Please run the following command for system-level installation:', 'blue'),
);
console.log(` ${COMMAND_NAME} register --system`);
printManualInstructions();
}
} else {
// Local installation mode, don't attempt automatic registration
console.log(
colorText('Local installation detected, skipping automatic registration', 'yellow'),
);
printManualInstructions();
}
} catch (error) {
console.log(
colorText(
`注册过程中出现错误: ${error instanceof Error ? error.message : String(error)}`,
'red',
),
);
printManualInstructions();
}
}
/**
* 打印手动安装指南
*/
function printManualInstructions(): void {
console.log('\n' + colorText('===== Manual Registration Guide =====', 'blue'));
console.log(colorText('1. Try user-level installation (recommended):', 'yellow'));
if (isGlobalInstall) {
console.log(` ${COMMAND_NAME} register`);
} else {
console.log(` npx ${COMMAND_NAME} register`);
}
console.log(
colorText('\n2. If user-level installation fails, try system-level installation:', 'yellow'),
);
console.log(colorText(' Use --system parameter (auto-elevate permissions):', 'yellow'));
if (isGlobalInstall) {
console.log(` ${COMMAND_NAME} register --system`);
} else {
console.log(` npx ${COMMAND_NAME} register --system`);
}
console.log(colorText('\n Or use administrator privileges directly:', 'yellow'));
if (os.platform() === 'win32') {
console.log(
colorText(
' Please run Command Prompt or PowerShell as administrator and execute:',
'yellow',
),
);
if (isGlobalInstall) {
console.log(` ${COMMAND_NAME} register`);
} else {
console.log(` npx ${COMMAND_NAME} register`);
}
} else {
console.log(colorText(' Please run the following command in terminal:', 'yellow'));
if (isGlobalInstall) {
console.log(` sudo ${COMMAND_NAME} register`);
} else {
console.log(` sudo npx ${COMMAND_NAME} register`);
}
}
console.log(
'\n' +
colorText(
'Ensure Chrome extension is installed and refresh the extension to connect to local service.',
'blue',
),
);
}
/**
* 主函数
*/
async function main(): Promise<void> {
console.log(colorText(`Installing ${COMMAND_NAME}...`, 'green'));
// Debug information
console.log(colorText('Installation environment debug info:', 'blue'));
console.log(` __dirname: ${__dirname}`);
console.log(` npm_config_global: ${process.env.npm_config_global}`);
console.log(` PNPM_HOME: ${process.env.PNPM_HOME}`);
console.log(` npm_config_prefix: ${process.env.npm_config_prefix}`);
console.log(` isGlobalInstall: ${isGlobalInstall}`);
// Always ensure execution permissions first
await ensureExecutionPermissions();
// Write Node.js path for run_host scripts to use
await writeNodePath();
// If global installation, try automatic registration
if (isGlobalInstall) {
await tryRegisterNativeHost();
} else {
console.log(colorText('Local installation detected', 'yellow'));
printManualInstructions();
}
}
// Only execute main function when running this script directly
if (isDirectRun) {
main().catch((error) => {
console.error(
colorText(
`Installation script error: ${error instanceof Error ? error.message : String(error)}`,
'red',
),
);
});
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/vector-search.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Vectorized tab content search tool
* Uses vector database for efficient semantic search
*/
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
import { ContentIndexer } from '@/utils/content-indexer';
import { LIMITS, ERROR_MESSAGES } from '@/common/constants';
import type { SearchResult } from '@/utils/vector-database';
interface VectorSearchResult {
tabId: number;
url: string;
title: string;
semanticScore: number;
matchedSnippet: string;
chunkSource: string;
timestamp: number;
}
/**
* Tool for vectorized search of tab content using semantic similarity
*/
class VectorSearchTabsContentTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.SEARCH_TABS_CONTENT;
private contentIndexer: ContentIndexer;
private isInitialized = false;
constructor() {
super();
this.contentIndexer = new ContentIndexer({
autoIndex: true,
maxChunksPerPage: LIMITS.MAX_SEARCH_RESULTS,
skipDuplicates: true,
});
}
private async initializeIndexer(): Promise<void> {
try {
await this.contentIndexer.initialize();
this.isInitialized = true;
console.log('VectorSearchTabsContentTool: Content indexer initialized successfully');
} catch (error) {
console.error('VectorSearchTabsContentTool: Failed to initialize content indexer:', error);
this.isInitialized = false;
}
}
async execute(args: { query: string }): Promise<ToolResult> {
try {
const { query } = args;
if (!query || query.trim().length === 0) {
return createErrorResponse(
ERROR_MESSAGES.INVALID_PARAMETERS + ': Query parameter is required and cannot be empty',
);
}
console.log(`VectorSearchTabsContentTool: Starting vector search with query: "${query}"`);
// Check semantic engine status
if (!this.contentIndexer.isSemanticEngineReady()) {
if (this.contentIndexer.isSemanticEngineInitializing()) {
return createErrorResponse(
'Vector search engine is still initializing (model downloading). Please wait a moment and try again.',
);
} else {
// Try to initialize
console.log('VectorSearchTabsContentTool: Initializing content indexer...');
await this.initializeIndexer();
// Check semantic engine status again
if (!this.contentIndexer.isSemanticEngineReady()) {
return createErrorResponse('Failed to initialize vector search engine');
}
}
}
// Execute vector search, get more results for deduplication
const searchResults = await this.contentIndexer.searchContent(query, 50);
// Convert search results format
const vectorSearchResults = this.convertSearchResults(searchResults);
// Deduplicate by tab, keep only the highest similarity fragment per tab
const deduplicatedResults = this.deduplicateByTab(vectorSearchResults);
// Sort by similarity and get top 10 results
const topResults = deduplicatedResults
.sort((a, b) => b.semanticScore - a.semanticScore)
.slice(0, 10);
// Get index statistics
const stats = this.contentIndexer.getStats();
const result = {
success: true,
totalTabsSearched: stats.totalTabs,
matchedTabsCount: topResults.length,
vectorSearchEnabled: true,
indexStats: {
totalDocuments: stats.totalDocuments,
totalTabs: stats.totalTabs,
indexedPages: stats.indexedPages,
semanticEngineReady: stats.semanticEngineReady,
semanticEngineInitializing: stats.semanticEngineInitializing,
},
matchedTabs: topResults.map((result) => ({
tabId: result.tabId,
url: result.url,
title: result.title,
semanticScore: result.semanticScore,
matchedSnippets: [result.matchedSnippet],
chunkSource: result.chunkSource,
timestamp: result.timestamp,
})),
};
console.log(
`VectorSearchTabsContentTool: Found ${topResults.length} results with vector search`,
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
isError: false,
};
} catch (error) {
console.error('VectorSearchTabsContentTool: Search failed:', error);
return createErrorResponse(
`Vector search failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Ensure all tabs are indexed
*/
private async ensureTabsIndexed(tabs: chrome.tabs.Tab[]): Promise<void> {
const indexPromises = tabs
.filter((tab) => tab.id)
.map(async (tab) => {
try {
await this.contentIndexer.indexTabContent(tab.id!);
} catch (error) {
console.warn(`VectorSearchTabsContentTool: Failed to index tab ${tab.id}:`, error);
}
});
await Promise.allSettled(indexPromises);
}
/**
* Convert search results format
*/
private convertSearchResults(searchResults: SearchResult[]): VectorSearchResult[] {
return searchResults.map((result) => ({
tabId: result.document.tabId,
url: result.document.url,
title: result.document.title,
semanticScore: result.similarity,
matchedSnippet: this.extractSnippet(result.document.chunk.text),
chunkSource: result.document.chunk.source,
timestamp: result.document.timestamp,
}));
}
/**
* Deduplicate by tab, keep only the highest similarity fragment per tab
*/
private deduplicateByTab(results: VectorSearchResult[]): VectorSearchResult[] {
const tabMap = new Map<number, VectorSearchResult>();
for (const result of results) {
const existingResult = tabMap.get(result.tabId);
// If this tab has no result yet, or current result has higher similarity, update it
if (!existingResult || result.semanticScore > existingResult.semanticScore) {
tabMap.set(result.tabId, result);
}
}
return Array.from(tabMap.values());
}
/**
* Extract text snippet for display
*/
private extractSnippet(text: string, maxLength: number = 200): string {
if (text.length <= maxLength) {
return text;
}
// Try to truncate at sentence boundary
const truncated = text.substring(0, maxLength);
const lastSentenceEnd = Math.max(
truncated.lastIndexOf('.'),
truncated.lastIndexOf('!'),
truncated.lastIndexOf('?'),
truncated.lastIndexOf('。'),
truncated.lastIndexOf('!'),
truncated.lastIndexOf('?'),
);
if (lastSentenceEnd > maxLength * 0.7) {
return truncated.substring(0, lastSentenceEnd + 1);
}
// If no suitable sentence boundary found, truncate at word boundary
const lastSpaceIndex = truncated.lastIndexOf(' ');
if (lastSpaceIndex > maxLength * 0.8) {
return truncated.substring(0, lastSpaceIndex) + '...';
}
return truncated + '...';
}
/**
* Get index statistics
*/
public async getIndexStats() {
if (!this.isInitialized) {
// Don't automatically initialize - just return basic stats
return {
totalDocuments: 0,
totalTabs: 0,
indexSize: 0,
indexedPages: 0,
isInitialized: false,
semanticEngineReady: false,
semanticEngineInitializing: false,
};
}
return this.contentIndexer.getStats();
}
/**
* Manually rebuild index
*/
public async rebuildIndex(): Promise<void> {
if (!this.isInitialized) {
await this.initializeIndexer();
}
try {
// Clear existing indexes
await this.contentIndexer.clearAllIndexes();
// Get all tabs and reindex
const windows = await chrome.windows.getAll({ populate: true });
const allTabs: chrome.tabs.Tab[] = [];
for (const window of windows) {
if (window.tabs) {
allTabs.push(...window.tabs);
}
}
const validTabs = allTabs.filter(
(tab) =>
tab.id &&
tab.url &&
!tab.url.startsWith('chrome://') &&
!tab.url.startsWith('chrome-extension://') &&
!tab.url.startsWith('edge://') &&
!tab.url.startsWith('about:'),
);
await this.ensureTabsIndexed(validTabs);
console.log(`VectorSearchTabsContentTool: Rebuilt index for ${validTabs.length} tabs`);
} catch (error) {
console.error('VectorSearchTabsContentTool: Failed to rebuild index:', error);
throw error;
}
}
/**
* Manually index specified tab
*/
public async indexTab(tabId: number): Promise<void> {
if (!this.isInitialized) {
await this.initializeIndexer();
}
await this.contentIndexer.indexTabContent(tabId);
}
/**
* Remove index for specified tab
*/
public async removeTabIndex(tabId: number): Promise<void> {
if (!this.isInitialized) {
return;
}
await this.contentIndexer.removeTabIndex(tabId);
}
}
// Export tool instance
export const vectorSearchTabsContentTool = new VectorSearchTabsContentTool();
```
--------------------------------------------------------------------------------
/app/native-server/src/native-messaging-host.ts:
--------------------------------------------------------------------------------
```typescript
import { stdin, stdout } from 'process';
import { Server } from './server';
import { v4 as uuidv4 } from 'uuid';
import { NativeMessageType } from 'chrome-mcp-shared';
import { TIMEOUTS } from './constant';
import fileHandler from './file-handler';
interface PendingRequest {
resolve: (value: any) => void;
reject: (reason?: any) => void;
timeoutId: NodeJS.Timeout;
}
export class NativeMessagingHost {
private associatedServer: Server | null = null;
private pendingRequests: Map<string, PendingRequest> = new Map();
public setServer(serverInstance: Server): void {
this.associatedServer = serverInstance;
}
// add message handler to wait for start server
public start(): void {
try {
this.setupMessageHandling();
} catch (error: any) {
process.exit(1);
}
}
private setupMessageHandling(): void {
let buffer = Buffer.alloc(0);
let expectedLength = -1;
stdin.on('readable', () => {
let chunk;
while ((chunk = stdin.read()) !== null) {
buffer = Buffer.concat([buffer, chunk]);
if (expectedLength === -1 && buffer.length >= 4) {
expectedLength = buffer.readUInt32LE(0);
buffer = buffer.slice(4);
}
if (expectedLength !== -1 && buffer.length >= expectedLength) {
const messageBuffer = buffer.slice(0, expectedLength);
buffer = buffer.slice(expectedLength);
try {
const message = JSON.parse(messageBuffer.toString());
this.handleMessage(message);
} catch (error: any) {
this.sendError(`Failed to parse message: ${error.message}`);
}
expectedLength = -1; // reset to get next data
}
}
});
stdin.on('end', () => {
this.cleanup();
});
stdin.on('error', () => {
this.cleanup();
});
}
private async handleMessage(message: any): Promise<void> {
if (!message || typeof message !== 'object') {
this.sendError('Invalid message format');
return;
}
if (message.responseToRequestId) {
const requestId = message.responseToRequestId;
const pending = this.pendingRequests.get(requestId);
if (pending) {
clearTimeout(pending.timeoutId);
if (message.error) {
pending.reject(new Error(message.error));
} else {
pending.resolve(message.payload);
}
this.pendingRequests.delete(requestId);
} else {
// just ignore
}
return;
}
// Handle directive messages from Chrome
try {
switch (message.type) {
case NativeMessageType.START:
await this.startServer(message.payload?.port || 3000);
break;
case NativeMessageType.STOP:
await this.stopServer();
break;
// Keep ping/pong for simple liveness detection, but this differs from request-response pattern
case 'ping_from_extension':
this.sendMessage({ type: 'pong_to_extension' });
break;
case 'file_operation':
await this.handleFileOperation(message);
break;
default:
// Double check when message type is not supported
if (!message.responseToRequestId) {
this.sendError(
`Unknown message type or non-response message: ${message.type || 'no type'}`,
);
}
}
} catch (error: any) {
this.sendError(`Failed to handle directive message: ${error.message}`);
}
}
/**
* Handle file operations from the extension
*/
private async handleFileOperation(message: any): Promise<void> {
try {
const result = await fileHandler.handleFileRequest(message.payload);
if (message.requestId) {
// Send response back with the request ID
this.sendMessage({
type: 'file_operation_response',
responseToRequestId: message.requestId,
payload: result,
});
} else {
// No request ID, just send result
this.sendMessage({
type: 'file_operation_result',
payload: result,
});
}
} catch (error: any) {
const errorResponse = {
success: false,
error: error.message || 'Unknown error during file operation',
};
if (message.requestId) {
this.sendMessage({
type: 'file_operation_response',
responseToRequestId: message.requestId,
error: errorResponse.error,
});
} else {
this.sendError(`File operation failed: ${errorResponse.error}`);
}
}
}
/**
* Send request to Chrome and wait for response
* @param messagePayload Data to send to Chrome
* @param timeoutMs Timeout for waiting response (milliseconds)
* @returns Promise, resolves to Chrome's returned payload on success, rejects on failure
*/
public sendRequestToExtensionAndWait(
messagePayload: any,
messageType: string = 'request_data',
timeoutMs: number = TIMEOUTS.DEFAULT_REQUEST_TIMEOUT,
): Promise<any> {
return new Promise((resolve, reject) => {
const requestId = uuidv4(); // Generate unique request ID
const timeoutId = setTimeout(() => {
this.pendingRequests.delete(requestId); // Remove from Map after timeout
reject(new Error(`Request timed out after ${timeoutMs}ms`));
}, timeoutMs);
// Store request's resolve/reject functions and timeout ID
this.pendingRequests.set(requestId, { resolve, reject, timeoutId });
// Send message with requestId to Chrome
this.sendMessage({
type: messageType, // Define a request type, e.g. 'request_data'
payload: messagePayload,
requestId: requestId, // <--- Key: include request ID
});
});
}
/**
* Start Fastify server (now accepts Server instance)
*/
private async startServer(port: number): Promise<void> {
if (!this.associatedServer) {
this.sendError('Internal error: server instance not set');
return;
}
try {
if (this.associatedServer.isRunning) {
this.sendMessage({
type: NativeMessageType.ERROR,
payload: { message: 'Server is already running' },
});
return;
}
await this.associatedServer.start(port, this);
this.sendMessage({
type: NativeMessageType.SERVER_STARTED,
payload: { port },
});
} catch (error: any) {
this.sendError(`Failed to start server: ${error.message}`);
}
}
/**
* Stop Fastify server
*/
private async stopServer(): Promise<void> {
if (!this.associatedServer) {
this.sendError('Internal error: server instance not set');
return;
}
try {
// Check status through associatedServer
if (!this.associatedServer.isRunning) {
this.sendMessage({
type: NativeMessageType.ERROR,
payload: { message: 'Server is not running' },
});
return;
}
await this.associatedServer.stop();
// this.serverStarted = false; // Server should update its own status after successful stop
this.sendMessage({ type: NativeMessageType.SERVER_STOPPED }); // Distinguish from previous 'stopped'
} catch (error: any) {
this.sendError(`Failed to stop server: ${error.message}`);
}
}
/**
* Send message to Chrome extension
*/
public sendMessage(message: any): void {
try {
const messageString = JSON.stringify(message);
const messageBuffer = Buffer.from(messageString);
const headerBuffer = Buffer.alloc(4);
headerBuffer.writeUInt32LE(messageBuffer.length, 0);
// Ensure atomic write
stdout.write(Buffer.concat([headerBuffer, messageBuffer]), (err) => {
if (err) {
// Consider how to handle write failure, may affect request completion
} else {
// Message sent successfully, no action needed
}
});
} catch (error: any) {
// Catch JSON.stringify or Buffer operation errors
// If preparation stage fails, associated request may never be sent
// Need to consider whether to reject corresponding Promise (if called within sendRequestToExtensionAndWait)
}
}
/**
* Send error message to Chrome extension (mainly for sending non-request-response type errors)
*/
private sendError(errorMessage: string): void {
this.sendMessage({
type: NativeMessageType.ERROR_FROM_NATIVE_HOST, // Use more explicit type
payload: { message: errorMessage },
});
}
/**
* Clean up resources
*/
private cleanup(): void {
// Reject all pending requests
this.pendingRequests.forEach((pending) => {
clearTimeout(pending.timeoutId);
pending.reject(new Error('Native host is shutting down or Chrome disconnected.'));
});
this.pendingRequests.clear();
if (this.associatedServer && this.associatedServer.isRunning) {
this.associatedServer
.stop()
.then(() => {
process.exit(0);
})
.catch(() => {
process.exit(1);
});
} else {
process.exit(0);
}
}
}
const nativeMessagingHostInstance = new NativeMessagingHost();
export default nativeMessagingHostInstance;
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/i18n.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Chrome Extension i18n utility
* Provides safe access to chrome.i18n.getMessage with fallbacks
*/
// Fallback messages for when Chrome APIs aren't available (English)
const fallbackMessages: Record<string, string> = {
// Extension metadata
extensionName: 'chrome-mcp-server',
extensionDescription: 'Exposes browser capabilities with your own chrome',
// Section headers
nativeServerConfigLabel: 'Native Server Configuration',
semanticEngineLabel: 'Semantic Engine',
embeddingModelLabel: 'Embedding Model',
indexDataManagementLabel: 'Index Data Management',
modelCacheManagementLabel: 'Model Cache Management',
// Status labels
statusLabel: 'Status',
runningStatusLabel: 'Running Status',
connectionStatusLabel: 'Connection Status',
lastUpdatedLabel: 'Last Updated:',
// Connection states
connectButton: 'Connect',
disconnectButton: 'Disconnect',
connectingStatus: 'Connecting...',
connectedStatus: 'Connected',
disconnectedStatus: 'Disconnected',
detectingStatus: 'Detecting...',
// Server states
serviceRunningStatus: 'Service Running (Port: {0})',
serviceNotConnectedStatus: 'Service Not Connected',
connectedServiceNotStartedStatus: 'Connected, Service Not Started',
// Configuration labels
mcpServerConfigLabel: 'MCP Server Configuration',
connectionPortLabel: 'Connection Port',
refreshStatusButton: 'Refresh Status',
copyConfigButton: 'Copy Configuration',
// Action buttons
retryButton: 'Retry',
cancelButton: 'Cancel',
confirmButton: 'Confirm',
saveButton: 'Save',
closeButton: 'Close',
resetButton: 'Reset',
// Progress states
initializingStatus: 'Initializing...',
processingStatus: 'Processing...',
loadingStatus: 'Loading...',
clearingStatus: 'Clearing...',
cleaningStatus: 'Cleaning...',
downloadingStatus: 'Downloading...',
// Semantic engine states
semanticEngineReadyStatus: 'Semantic Engine Ready',
semanticEngineInitializingStatus: 'Semantic Engine Initializing...',
semanticEngineInitFailedStatus: 'Semantic Engine Initialization Failed',
semanticEngineNotInitStatus: 'Semantic Engine Not Initialized',
initSemanticEngineButton: 'Initialize Semantic Engine',
reinitializeButton: 'Reinitialize',
// Model states
downloadingModelStatus: 'Downloading Model... {0}%',
switchingModelStatus: 'Switching Model...',
modelLoadedStatus: 'Model Loaded',
modelFailedStatus: 'Model Failed to Load',
// Model descriptions
lightweightModelDescription: 'Lightweight Multilingual Model',
betterThanSmallDescription: 'Slightly larger than e5-small, but better performance',
multilingualModelDescription: 'Multilingual Semantic Model',
// Performance levels
fastPerformance: 'Fast',
balancedPerformance: 'Balanced',
accuratePerformance: 'Accurate',
// Error messages
networkErrorMessage: 'Network connection error, please check network and retry',
modelCorruptedErrorMessage: 'Model file corrupted or incomplete, please retry download',
unknownErrorMessage: 'Unknown error, please check if your network can access HuggingFace',
permissionDeniedErrorMessage: 'Permission denied',
timeoutErrorMessage: 'Operation timed out',
// Data statistics
indexedPagesLabel: 'Indexed Pages',
indexSizeLabel: 'Index Size',
activeTabsLabel: 'Active Tabs',
vectorDocumentsLabel: 'Vector Documents',
cacheSizeLabel: 'Cache Size',
cacheEntriesLabel: 'Cache Entries',
// Data management
clearAllDataButton: 'Clear All Data',
clearAllCacheButton: 'Clear All Cache',
cleanExpiredCacheButton: 'Clean Expired Cache',
exportDataButton: 'Export Data',
importDataButton: 'Import Data',
// Dialog titles
confirmClearDataTitle: 'Confirm Clear Data',
settingsTitle: 'Settings',
aboutTitle: 'About',
helpTitle: 'Help',
// Dialog messages
clearDataWarningMessage:
'This operation will clear all indexed webpage content and vector data, including:',
clearDataList1: 'All webpage text content index',
clearDataList2: 'Vector embedding data',
clearDataList3: 'Search history and cache',
clearDataIrreversibleWarning:
'This operation is irreversible! After clearing, you need to browse webpages again to rebuild the index.',
confirmClearButton: 'Confirm Clear',
// Cache states
cacheDetailsLabel: 'Cache Details',
noCacheDataMessage: 'No cache data',
loadingCacheInfoStatus: 'Loading cache information...',
processingCacheStatus: 'Processing cache...',
expiredLabel: 'Expired',
// Browser integration
bookmarksBarLabel: 'Bookmarks Bar',
newTabLabel: 'New Tab',
currentPageLabel: 'Current Page',
// Accessibility
menuLabel: 'Menu',
navigationLabel: 'Navigation',
mainContentLabel: 'Main Content',
// Future features
languageSelectorLabel: 'Language',
themeLabel: 'Theme',
lightTheme: 'Light',
darkTheme: 'Dark',
autoTheme: 'Auto',
advancedSettingsLabel: 'Advanced Settings',
debugModeLabel: 'Debug Mode',
verboseLoggingLabel: 'Verbose Logging',
// Notifications
successNotification: 'Operation completed successfully',
warningNotification: 'Warning: Please review before proceeding',
infoNotification: 'Information',
configCopiedNotification: 'Configuration copied to clipboard',
dataClearedNotification: 'Data cleared successfully',
// Units
bytesUnit: 'bytes',
kilobytesUnit: 'KB',
megabytesUnit: 'MB',
gigabytesUnit: 'GB',
itemsUnit: 'items',
pagesUnit: 'pages',
// Legacy keys for backwards compatibility
nativeServerConfig: 'Native Server Configuration',
runningStatus: 'Running Status',
refreshStatus: 'Refresh Status',
lastUpdated: 'Last Updated:',
mcpServerConfig: 'MCP Server Configuration',
connectionPort: 'Connection Port',
connecting: 'Connecting...',
disconnect: 'Disconnect',
connect: 'Connect',
semanticEngine: 'Semantic Engine',
embeddingModel: 'Embedding Model',
retry: 'Retry',
indexDataManagement: 'Index Data Management',
clearing: 'Clearing...',
clearAllData: 'Clear All Data',
copyConfig: 'Copy Configuration',
serviceRunning: 'Service Running (Port: {0})',
connectedServiceNotStarted: 'Connected, Service Not Started',
serviceNotConnected: 'Service Not Connected',
detecting: 'Detecting...',
lightweightModel: 'Lightweight Multilingual Model',
betterThanSmall: 'Slightly larger than e5-small, but better performance',
multilingualModel: 'Multilingual Semantic Model',
fast: 'Fast',
balanced: 'Balanced',
accurate: 'Accurate',
semanticEngineReady: 'Semantic Engine Ready',
semanticEngineInitializing: 'Semantic Engine Initializing...',
semanticEngineInitFailed: 'Semantic Engine Initialization Failed',
semanticEngineNotInit: 'Semantic Engine Not Initialized',
downloadingModel: 'Downloading Model... {0}%',
switchingModel: 'Switching Model...',
networkError: 'Network connection error, please check network and retry',
modelCorrupted: 'Model file corrupted or incomplete, please retry download',
unknownError: 'Unknown error, please check if your network can access HuggingFace',
reinitialize: 'Reinitialize',
initializing: 'Initializing...',
initSemanticEngine: 'Initialize Semantic Engine',
indexedPages: 'Indexed Pages',
indexSize: 'Index Size',
activeTabs: 'Active Tabs',
vectorDocuments: 'Vector Documents',
confirmClearData: 'Confirm Clear Data',
clearDataWarning:
'This operation will clear all indexed webpage content and vector data, including:',
clearDataIrreversible:
'This operation is irreversible! After clearing, you need to browse webpages again to rebuild the index.',
confirmClear: 'Confirm Clear',
cancel: 'Cancel',
confirm: 'Confirm',
processing: 'Processing...',
modelCacheManagement: 'Model Cache Management',
cacheSize: 'Cache Size',
cacheEntries: 'Cache Entries',
cacheDetails: 'Cache Details',
noCacheData: 'No cache data',
loadingCacheInfo: 'Loading cache information...',
processingCache: 'Processing cache...',
cleaning: 'Cleaning...',
cleanExpiredCache: 'Clean Expired Cache',
clearAllCache: 'Clear All Cache',
expired: 'Expired',
bookmarksBar: 'Bookmarks Bar',
};
/**
* Safe i18n message getter with fallback support
* @param key Message key
* @param substitutions Optional substitution values
* @returns Localized message or fallback
*/
export function getMessage(key: string, substitutions?: string[]): string {
try {
// Check if Chrome extension APIs are available
if (typeof chrome !== 'undefined' && chrome.i18n && chrome.i18n.getMessage) {
const message = chrome.i18n.getMessage(key, substitutions);
if (message) {
return message;
}
}
} catch (error) {
console.warn(`Failed to get i18n message for key "${key}":`, error);
}
// Fallback to English messages
let fallback = fallbackMessages[key] || key;
// Handle substitutions in fallback messages
if (substitutions && substitutions.length > 0) {
substitutions.forEach((value, index) => {
fallback = fallback.replace(`{${index}}`, value);
});
}
return fallback;
}
/**
* Check if Chrome extension i18n APIs are available
*/
export function isI18nAvailable(): boolean {
try {
return (
typeof chrome !== 'undefined' && chrome.i18n && typeof chrome.i18n.getMessage === 'function'
);
} catch {
return false;
}
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/file-upload.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
interface FileUploadToolParams {
selector: string; // CSS selector for the file input element
filePath?: string; // Local file path
fileUrl?: string; // URL to download file from
base64Data?: string; // Base64 encoded file data
fileName?: string; // Optional filename when using base64 or URL
multiple?: boolean; // Whether to allow multiple files
}
/**
* Tool for uploading files to web forms using Chrome DevTools Protocol
* Similar to Playwright's setInputFiles implementation
*/
class FileUploadTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.FILE_UPLOAD;
private activeDebuggers: Map<number, boolean> = new Map();
constructor() {
super();
// Clean up debuggers on tab removal
chrome.tabs.onRemoved.addListener((tabId) => {
if (this.activeDebuggers.has(tabId)) {
this.cleanupDebugger(tabId);
}
});
}
/**
* Execute file upload operation using Chrome DevTools Protocol
*/
async execute(args: FileUploadToolParams): Promise<ToolResult> {
const { selector, filePath, fileUrl, base64Data, fileName, multiple = false } = args;
console.log(`Starting file upload operation with options:`, args);
// Validate input
if (!selector) {
return createErrorResponse('Selector is required for file upload');
}
if (!filePath && !fileUrl && !base64Data) {
return createErrorResponse(
'One of filePath, fileUrl, or base64Data must be provided',
);
}
let tabId: number | undefined;
try {
// Get current tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tabs[0]?.id) {
return createErrorResponse('No active tab found');
}
tabId = tabs[0].id;
// Prepare file paths
let files: string[] = [];
if (filePath) {
// Direct file path provided
files = [filePath];
} else if (fileUrl || base64Data) {
// For URL or base64, we need to use the native messaging host
// to download or save the file temporarily
const tempFilePath = await this.prepareFileFromRemote({
fileUrl,
base64Data,
fileName: fileName || 'uploaded-file',
});
if (!tempFilePath) {
return createErrorResponse('Failed to prepare file for upload');
}
files = [tempFilePath];
}
// Attach debugger to the tab
await this.attachDebugger(tabId);
// Enable necessary CDP domains
await chrome.debugger.sendCommand({ tabId }, 'DOM.enable', {});
await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable', {});
// Get the document
const { root } = await chrome.debugger.sendCommand(
{ tabId },
'DOM.getDocument',
{ depth: -1, pierce: true },
) as { root: { nodeId: number } };
// Find the file input element using the selector
const { nodeId } = await chrome.debugger.sendCommand(
{ tabId },
'DOM.querySelector',
{
nodeId: root.nodeId,
selector: selector,
},
) as { nodeId: number };
if (!nodeId || nodeId === 0) {
throw new Error(`Element with selector "${selector}" not found`);
}
// Verify it's actually a file input
const { node } = await chrome.debugger.sendCommand(
{ tabId },
'DOM.describeNode',
{ nodeId },
) as { node: { nodeName: string; attributes?: string[] } };
if (node.nodeName !== 'INPUT') {
throw new Error(`Element with selector "${selector}" is not an input element`);
}
// Check if it's a file input by looking for type="file" in attributes
const attributes = node.attributes || [];
let isFileInput = false;
for (let i = 0; i < attributes.length; i += 2) {
if (attributes[i] === 'type' && attributes[i + 1] === 'file') {
isFileInput = true;
break;
}
}
if (!isFileInput) {
throw new Error(`Element with selector "${selector}" is not a file input (type="file")`);
}
// Set the files on the input element
// This is the key CDP command that Playwright and Puppeteer use
await chrome.debugger.sendCommand(
{ tabId },
'DOM.setFileInputFiles',
{
nodeId: nodeId,
files: files,
},
);
// Trigger change event to ensure the page reacts to the file upload
await chrome.debugger.sendCommand(
{ tabId },
'Runtime.evaluate',
{
expression: `
(function() {
const element = document.querySelector('${selector.replace(/'/g, "\\'")}');
if (element) {
const event = new Event('change', { bubbles: true });
element.dispatchEvent(event);
return true;
}
return false;
})()
`,
},
);
// Clean up debugger
await this.detachDebugger(tabId);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: 'File(s) uploaded successfully',
files: files,
selector: selector,
fileCount: files.length,
}),
},
],
isError: false,
};
} catch (error) {
console.error('Error in file upload operation:', error);
// Clean up debugger if attached
if (tabId !== undefined && this.activeDebuggers.has(tabId)) {
await this.detachDebugger(tabId);
}
return createErrorResponse(
`Error uploading file: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Attach debugger to a tab
*/
private async attachDebugger(tabId: number): Promise<void> {
// Check if debugger is already attached
const targets = await chrome.debugger.getTargets();
const existingTarget = targets.find(
(t) => t.tabId === tabId && t.attached,
);
if (existingTarget) {
if (existingTarget.extensionId === chrome.runtime.id) {
// Our extension already attached
console.log('Debugger already attached by this extension');
return;
} else {
throw new Error(
'Debugger is already attached to this tab by another extension or DevTools',
);
}
}
// Attach debugger
await chrome.debugger.attach({ tabId }, '1.3');
this.activeDebuggers.set(tabId, true);
console.log(`Debugger attached to tab ${tabId}`);
}
/**
* Detach debugger from a tab
*/
private async detachDebugger(tabId: number): Promise<void> {
if (!this.activeDebuggers.has(tabId)) {
return;
}
try {
await chrome.debugger.detach({ tabId });
console.log(`Debugger detached from tab ${tabId}`);
} catch (error) {
console.warn(`Error detaching debugger from tab ${tabId}:`, error);
} finally {
this.activeDebuggers.delete(tabId);
}
}
/**
* Clean up debugger connection
*/
private cleanupDebugger(tabId: number): void {
this.activeDebuggers.delete(tabId);
}
/**
* Prepare file from URL or base64 data using native messaging host
*/
private async prepareFileFromRemote(options: {
fileUrl?: string;
base64Data?: string;
fileName: string;
}): Promise<string | null> {
const { fileUrl, base64Data, fileName } = options;
return new Promise((resolve) => {
const requestId = `file-upload-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const timeout = setTimeout(() => {
console.error('File preparation request timed out');
resolve(null);
}, 30000); // 30 second timeout
// Create listener for the response
const handleMessage = (message: any) => {
if (message.type === 'file_operation_response' &&
message.responseToRequestId === requestId) {
clearTimeout(timeout);
chrome.runtime.onMessage.removeListener(handleMessage);
if (message.payload?.success && message.payload?.filePath) {
resolve(message.payload.filePath);
} else {
console.error('Native host failed to prepare file:', message.error || message.payload?.error);
resolve(null);
}
}
};
// Add listener
chrome.runtime.onMessage.addListener(handleMessage);
// Send message to background script to forward to native host
chrome.runtime.sendMessage({
type: 'forward_to_native',
message: {
type: 'file_operation',
requestId: requestId,
payload: {
action: 'prepareFile',
fileUrl,
base64Data,
fileName,
},
},
}).catch((error) => {
console.error('Error sending message to background:', error);
clearTimeout(timeout);
chrome.runtime.onMessage.removeListener(handleMessage);
resolve(null);
});
});
}
}
export const fileUploadTool = new FileUploadTool();
```
--------------------------------------------------------------------------------
/app/native-server/src/server/index.ts:
--------------------------------------------------------------------------------
```typescript
import Fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import cors from '@fastify/cors';
import {
NATIVE_SERVER_PORT,
TIMEOUTS,
SERVER_CONFIG,
HTTP_STATUS,
ERROR_MESSAGES,
} from '../constant';
import { NativeMessagingHost } from '../native-messaging-host';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { randomUUID } from 'node:crypto';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { getMcpServer } from '../mcp/mcp-server';
// Define request body type (if data needs to be retrieved from HTTP requests)
interface ExtensionRequestPayload {
data?: any; // Data you want to pass to the extension
}
export class Server {
private fastify: FastifyInstance;
public isRunning = false; // Changed to public or provide a getter
private nativeHost: NativeMessagingHost | null = null;
private transportsMap: Map<string, StreamableHTTPServerTransport | SSEServerTransport> =
new Map();
constructor() {
this.fastify = Fastify({ logger: SERVER_CONFIG.LOGGER_ENABLED });
this.setupPlugins();
this.setupRoutes();
}
/**
* Associate NativeMessagingHost instance
*/
public setNativeHost(nativeHost: NativeMessagingHost): void {
this.nativeHost = nativeHost;
}
private async setupPlugins(): Promise<void> {
await this.fastify.register(cors, {
origin: SERVER_CONFIG.CORS_ORIGIN,
});
}
private setupRoutes(): void {
// for ping
this.fastify.get(
'/ask-extension',
async (request: FastifyRequest<{ Body: ExtensionRequestPayload }>, reply: FastifyReply) => {
if (!this.nativeHost) {
return reply
.status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
.send({ error: ERROR_MESSAGES.NATIVE_HOST_NOT_AVAILABLE });
}
if (!this.isRunning) {
return reply
.status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
.send({ error: ERROR_MESSAGES.SERVER_NOT_RUNNING });
}
try {
// wait from extension message
const extensionResponse = await this.nativeHost.sendRequestToExtensionAndWait(
request.query,
'process_data',
TIMEOUTS.EXTENSION_REQUEST_TIMEOUT,
);
return reply.status(HTTP_STATUS.OK).send({ status: 'success', data: extensionResponse });
} catch (error: any) {
if (error.message.includes('timed out')) {
return reply
.status(HTTP_STATUS.GATEWAY_TIMEOUT)
.send({ status: 'error', message: ERROR_MESSAGES.REQUEST_TIMEOUT });
} else {
return reply.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
status: 'error',
message: `Failed to get response from extension: ${error.message}`,
});
}
}
},
);
// Compatible with SSE
this.fastify.get('/sse', async (_, reply) => {
try {
// Set SSE headers
reply.raw.writeHead(HTTP_STATUS.OK, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
// Create SSE transport
const transport = new SSEServerTransport('/messages', reply.raw);
this.transportsMap.set(transport.sessionId, transport);
reply.raw.on('close', () => {
this.transportsMap.delete(transport.sessionId);
});
const server = getMcpServer();
await server.connect(transport);
// Keep connection open
reply.raw.write(':\n\n');
} catch (error) {
if (!reply.sent) {
reply.code(HTTP_STATUS.INTERNAL_SERVER_ERROR).send(ERROR_MESSAGES.INTERNAL_SERVER_ERROR);
}
}
});
// Compatible with SSE
this.fastify.post('/messages', async (req, reply) => {
try {
const { sessionId } = req.query as any;
const transport = this.transportsMap.get(sessionId) as SSEServerTransport;
if (!sessionId || !transport) {
reply.code(HTTP_STATUS.BAD_REQUEST).send('No transport found for sessionId');
return;
}
await transport.handlePostMessage(req.raw, reply.raw, req.body);
} catch (error) {
if (!reply.sent) {
reply.code(HTTP_STATUS.INTERNAL_SERVER_ERROR).send(ERROR_MESSAGES.INTERNAL_SERVER_ERROR);
}
}
});
// POST /mcp: Handle client-to-server messages
this.fastify.post('/mcp', async (request, reply) => {
const sessionId = request.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport | undefined = this.transportsMap.get(
sessionId || '',
) as StreamableHTTPServerTransport;
if (transport) {
// transport found, do nothing
} else if (!sessionId && isInitializeRequest(request.body)) {
const newSessionId = randomUUID(); // Generate session ID
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => newSessionId, // Use pre-generated ID
onsessioninitialized: (initializedSessionId) => {
// Ensure transport instance exists and session ID matches
if (transport && initializedSessionId === newSessionId) {
this.transportsMap.set(initializedSessionId, transport);
}
},
});
transport.onclose = () => {
if (transport?.sessionId && this.transportsMap.get(transport.sessionId)) {
this.transportsMap.delete(transport.sessionId);
}
};
await getMcpServer().connect(transport);
} else {
reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_MCP_REQUEST });
return;
}
try {
await transport.handleRequest(request.raw, reply.raw, request.body);
} catch (error) {
if (!reply.sent) {
reply
.code(HTTP_STATUS.INTERNAL_SERVER_ERROR)
.send({ error: ERROR_MESSAGES.MCP_REQUEST_PROCESSING_ERROR });
}
}
});
this.fastify.get('/mcp', async (request, reply) => {
const sessionId = request.headers['mcp-session-id'] as string | undefined;
const transport = sessionId
? (this.transportsMap.get(sessionId) as StreamableHTTPServerTransport)
: undefined;
if (!transport) {
reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_SSE_SESSION });
return;
}
reply.raw.setHeader('Content-Type', 'text/event-stream');
reply.raw.setHeader('Cache-Control', 'no-cache');
reply.raw.setHeader('Connection', 'keep-alive');
reply.raw.flushHeaders(); // Ensure headers are sent immediately
try {
// transport.handleRequest will take over the response stream
await transport.handleRequest(request.raw, reply.raw);
if (!reply.sent) {
// If transport didn't send anything (unlikely for SSE initial handshake)
reply.hijack(); // Prevent Fastify from automatically sending response
}
} catch (error) {
if (!reply.raw.writableEnded) {
reply.raw.end();
}
}
request.socket.on('close', () => {
request.log.info(`SSE client disconnected for session: ${sessionId}`);
// transport's onclose should handle its own cleanup
});
});
this.fastify.delete('/mcp', async (request, reply) => {
const sessionId = request.headers['mcp-session-id'] as string | undefined;
const transport = sessionId
? (this.transportsMap.get(sessionId) as StreamableHTTPServerTransport)
: undefined;
if (!transport) {
reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_SESSION_ID });
return;
}
try {
await transport.handleRequest(request.raw, reply.raw);
// Assume transport.handleRequest will send response or transport.onclose will cleanup
if (!reply.sent) {
reply.code(HTTP_STATUS.NO_CONTENT).send();
}
} catch (error) {
if (!reply.sent) {
reply
.code(HTTP_STATUS.INTERNAL_SERVER_ERROR)
.send({ error: ERROR_MESSAGES.MCP_SESSION_DELETION_ERROR });
}
}
});
}
public async start(port = NATIVE_SERVER_PORT, nativeHost: NativeMessagingHost): Promise<void> {
if (!this.nativeHost) {
this.nativeHost = nativeHost; // Ensure nativeHost is set
} else if (this.nativeHost !== nativeHost) {
this.nativeHost = nativeHost; // Update to the passed instance
}
if (this.isRunning) {
return;
}
try {
await this.fastify.listen({ port, host: SERVER_CONFIG.HOST });
this.isRunning = true; // Update running status
// No need to return, Promise resolves void by default
} catch (err) {
this.isRunning = false; // Startup failed, reset status
// Throw error instead of exiting directly, let caller (possibly NativeHost) handle
throw err; // or return Promise.reject(err);
// process.exit(1); // Not recommended to exit directly here
}
}
public async stop(): Promise<void> {
if (!this.isRunning) {
return;
}
// this.nativeHost = null; // Not recommended to nullify here, association relationship may still be needed
try {
await this.fastify.close();
this.isRunning = false; // Update running status
} catch (err) {
// Even if closing fails, mark as not running, but log the error
this.isRunning = false;
throw err; // Throw error
}
}
public getInstance(): FastifyInstance {
return this.fastify;
}
}
const serverInstance = new Server();
export default serverInstance;
```
--------------------------------------------------------------------------------
/docs/TOOLS.md:
--------------------------------------------------------------------------------
```markdown
# Chrome MCP Server API Reference 📚
Complete reference for all available tools and their parameters.
## 📋 Table of Contents
- [Browser Management](#browser-management)
- [Screenshots & Visual](#screenshots--visual)
- [Network Monitoring](#network-monitoring)
- [Content Analysis](#content-analysis)
- [Interaction](#interaction)
- [Data Management](#data-management)
- [Response Format](#response-format)
## 📊 Browser Management
### `get_windows_and_tabs`
List all currently open browser windows and tabs.
**Parameters**: None
**Response**:
```json
{
"windowCount": 2,
"tabCount": 5,
"windows": [
{
"windowId": 123,
"tabs": [
{
"tabId": 456,
"url": "https://example.com",
"title": "Example Page",
"active": true
}
]
}
]
}
```
### `chrome_navigate`
Navigate to a URL with optional viewport control.
**Parameters**:
- `url` (string, required): URL to navigate to
- `newWindow` (boolean, optional): Create new window (default: false)
- `width` (number, optional): Viewport width in pixels (default: 1280)
- `height` (number, optional): Viewport height in pixels (default: 720)
**Example**:
```json
{
"url": "https://example.com",
"newWindow": true,
"width": 1920,
"height": 1080
}
```
### `chrome_close_tabs`
Close specific tabs or windows.
**Parameters**:
- `tabIds` (array, optional): Array of tab IDs to close
- `windowIds` (array, optional): Array of window IDs to close
**Example**:
```json
{
"tabIds": [123, 456],
"windowIds": [789]
}
```
### `chrome_switch_tab`
Switch to a specific browser tab.
**Parameters**:
- `tabId` (number, required): The ID of the tab to switch to.
- `windowId` (number, optional): The ID of the window where the tab is located.
**Example**:
```json
{
"tabId": 456,
"windowId": 123
}
```
### `chrome_go_back_or_forward`
Navigate browser history.
**Parameters**:
- `direction` (string, required): "back" or "forward"
- `tabId` (number, optional): Specific tab ID (default: active tab)
**Example**:
```json
{
"direction": "back",
"tabId": 123
}
```
## 📸 Screenshots & Visual
### `chrome_screenshot`
Take advanced screenshots with various options.
**Parameters**:
- `name` (string, optional): Screenshot filename
- `selector` (string, optional): CSS selector for element screenshot
- `width` (number, optional): Width in pixels (default: 800)
- `height` (number, optional): Height in pixels (default: 600)
- `storeBase64` (boolean, optional): Return base64 data (default: false)
- `fullPage` (boolean, optional): Capture full page (default: true)
**Example**:
```json
{
"selector": ".main-content",
"fullPage": true,
"storeBase64": true,
"width": 1920,
"height": 1080
}
```
**Response**:
```json
{
"success": true,
"base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
"dimensions": {
"width": 1920,
"height": 1080
}
}
```
## 🌐 Network Monitoring
### `chrome_network_capture_start`
Start capturing network requests using webRequest API.
**Parameters**:
- `url` (string, optional): URL to navigate to and capture
- `maxCaptureTime` (number, optional): Maximum capture time in ms (default: 30000)
- `inactivityTimeout` (number, optional): Stop after inactivity in ms (default: 3000)
- `includeStatic` (boolean, optional): Include static resources (default: false)
**Example**:
```json
{
"url": "https://api.example.com",
"maxCaptureTime": 60000,
"includeStatic": false
}
```
### `chrome_network_capture_stop`
Stop network capture and return collected data.
**Parameters**: None
**Response**:
```json
{
"success": true,
"capturedRequests": [
{
"url": "https://api.example.com/data",
"method": "GET",
"status": 200,
"requestHeaders": {...},
"responseHeaders": {...},
"responseTime": 150
}
],
"summary": {
"totalRequests": 15,
"captureTime": 5000
}
}
```
### `chrome_network_debugger_start`
Start capturing with Chrome Debugger API (includes response bodies).
**Parameters**:
- `url` (string, optional): URL to navigate to and capture
### `chrome_network_debugger_stop`
Stop debugger capture and return data with response bodies.
### `chrome_network_request`
Send custom HTTP requests.
**Parameters**:
- `url` (string, required): Request URL
- `method` (string, optional): HTTP method (default: "GET")
- `headers` (object, optional): Request headers
- `body` (string, optional): Request body
**Example**:
```json
{
"url": "https://api.example.com/data",
"method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": "{\"key\": \"value\"}"
}
```
## 🔍 Content Analysis
### `search_tabs_content`
AI-powered semantic search across browser tabs.
**Parameters**:
- `query` (string, required): Search query
**Example**:
```json
{
"query": "machine learning tutorials"
}
```
**Response**:
```json
{
"success": true,
"totalTabsSearched": 10,
"matchedTabsCount": 3,
"vectorSearchEnabled": true,
"indexStats": {
"totalDocuments": 150,
"totalTabs": 10,
"semanticEngineReady": true
},
"matchedTabs": [
{
"tabId": 123,
"url": "https://example.com/ml-tutorial",
"title": "Machine Learning Tutorial",
"semanticScore": 0.85,
"matchedSnippets": ["Introduction to machine learning..."],
"chunkSource": "content"
}
]
}
```
### `chrome_get_web_content`
Extract HTML or text content from web pages.
**Parameters**:
- `format` (string, optional): "html" or "text" (default: "text")
- `selector` (string, optional): CSS selector for specific elements
- `tabId` (number, optional): Specific tab ID (default: active tab)
**Example**:
```json
{
"format": "text",
"selector": ".article-content"
}
```
### `chrome_get_interactive_elements`
Find clickable and interactive elements on the page.
**Parameters**:
- `tabId` (number, optional): Specific tab ID (default: active tab)
**Response**:
```json
{
"elements": [
{
"selector": "#submit-button",
"type": "button",
"text": "Submit",
"visible": true,
"clickable": true
}
]
}
```
## 🎯 Interaction
### `chrome_click_element`
Click elements using CSS selectors.
**Parameters**:
- `selector` (string, required): CSS selector for target element
- `tabId` (number, optional): Specific tab ID (default: active tab)
**Example**:
```json
{
"selector": "#submit-button"
}
```
### `chrome_fill_or_select`
Fill form fields or select options.
**Parameters**:
- `selector` (string, required): CSS selector for target element
- `value` (string, required): Value to fill or select
- `tabId` (number, optional): Specific tab ID (default: active tab)
**Example**:
```json
{
"selector": "#email-input",
"value": "[email protected]"
}
```
### `chrome_keyboard`
Simulate keyboard input and shortcuts.
**Parameters**:
- `keys` (string, required): Key combination (e.g., "Ctrl+C", "Enter")
- `selector` (string, optional): Target element selector
- `delay` (number, optional): Delay between keystrokes in ms (default: 0)
**Example**:
```json
{
"keys": "Ctrl+A",
"selector": "#text-input",
"delay": 100
}
```
## 📚 Data Management
### `chrome_history`
Search browser history with filters.
**Parameters**:
- `text` (string, optional): Search text in URL/title
- `startTime` (string, optional): Start date (ISO format)
- `endTime` (string, optional): End date (ISO format)
- `maxResults` (number, optional): Maximum results (default: 100)
- `excludeCurrentTabs` (boolean, optional): Exclude current tabs (default: true)
**Example**:
```json
{
"text": "github",
"startTime": "2024-01-01",
"maxResults": 50
}
```
### `chrome_bookmark_search`
Search bookmarks by keywords.
**Parameters**:
- `query` (string, optional): Search keywords
- `maxResults` (number, optional): Maximum results (default: 100)
- `folderPath` (string, optional): Search within specific folder
**Example**:
```json
{
"query": "documentation",
"maxResults": 20,
"folderPath": "Work/Resources"
}
```
### `chrome_bookmark_add`
Add new bookmarks with folder support.
**Parameters**:
- `url` (string, optional): URL to bookmark (default: current tab)
- `title` (string, optional): Bookmark title (default: page title)
- `parentId` (string, optional): Parent folder ID or path
- `createFolder` (boolean, optional): Create folder if not exists (default: false)
**Example**:
```json
{
"url": "https://example.com",
"title": "Example Site",
"parentId": "Work/Resources",
"createFolder": true
}
```
### `chrome_bookmark_delete`
Delete bookmarks by ID or URL.
**Parameters**:
- `bookmarkId` (string, optional): Bookmark ID to delete
- `url` (string, optional): URL to find and delete
**Example**:
```json
{
"url": "https://example.com"
}
```
## 📋 Response Format
All tools return responses in the following format:
```json
{
"content": [
{
"type": "text",
"text": "JSON string containing the actual response data"
}
],
"isError": false
}
```
For errors:
```json
{
"content": [
{
"type": "text",
"text": "Error message describing what went wrong"
}
],
"isError": true
}
```
## 🔧 Usage Examples
### Complete Workflow Example
```javascript
// 1. Navigate to a page
await callTool('chrome_navigate', {
url: 'https://example.com',
});
// 2. Take a screenshot
const screenshot = await callTool('chrome_screenshot', {
fullPage: true,
storeBase64: true,
});
// 3. Start network monitoring
await callTool('chrome_network_capture_start', {
maxCaptureTime: 30000,
});
// 4. Interact with the page
await callTool('chrome_click_element', {
selector: '#load-data-button',
});
// 5. Search content semantically
const searchResults = await callTool('search_tabs_content', {
query: 'user data analysis',
});
// 6. Stop network capture
const networkData = await callTool('chrome_network_capture_stop');
// 7. Save bookmark
await callTool('chrome_bookmark_add', {
title: 'Data Analysis Page',
parentId: 'Work/Analytics',
});
```
This API provides comprehensive browser automation capabilities with AI-enhanced content analysis and semantic search features.
```
--------------------------------------------------------------------------------
/app/chrome-extension/workers/simd_math.js:
--------------------------------------------------------------------------------
```javascript
let wasm;
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
let WASM_VECTOR_LEN = 0;
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
});
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
let cachedDataViewMemory0 = null;
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
}
return cachedDataViewMemory0;
}
export function main() {
wasm.main();
}
let cachedFloat32ArrayMemory0 = null;
function getFloat32ArrayMemory0() {
if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.byteLength === 0) {
cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer);
}
return cachedFloat32ArrayMemory0;
}
function passArrayF32ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 4, 4) >>> 0;
getFloat32ArrayMemory0().set(arg, ptr / 4);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function getArrayF32FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
}
const SIMDMathFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_simdmath_free(ptr >>> 0, 1));
export class SIMDMath {
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
SIMDMathFinalization.unregister(this);
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_simdmath_free(ptr, 0);
}
constructor() {
const ret = wasm.simdmath_new();
this.__wbg_ptr = ret >>> 0;
SIMDMathFinalization.register(this, this.__wbg_ptr, this);
return this;
}
/**
* @param {Float32Array} vec_a
* @param {Float32Array} vec_b
* @returns {number}
*/
cosine_similarity(vec_a, vec_b) {
const ptr0 = passArrayF32ToWasm0(vec_a, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArrayF32ToWasm0(vec_b, wasm.__wbindgen_malloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.simdmath_cosine_similarity(this.__wbg_ptr, ptr0, len0, ptr1, len1);
return ret;
}
/**
* @param {Float32Array} vectors
* @param {Float32Array} query
* @param {number} vector_dim
* @returns {Float32Array}
*/
batch_similarity(vectors, query, vector_dim) {
const ptr0 = passArrayF32ToWasm0(vectors, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArrayF32ToWasm0(query, wasm.__wbindgen_malloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.simdmath_batch_similarity(this.__wbg_ptr, ptr0, len0, ptr1, len1, vector_dim);
var v3 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v3;
}
/**
* @param {Float32Array} vectors_a
* @param {Float32Array} vectors_b
* @param {number} vector_dim
* @returns {Float32Array}
*/
similarity_matrix(vectors_a, vectors_b, vector_dim) {
const ptr0 = passArrayF32ToWasm0(vectors_a, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passArrayF32ToWasm0(vectors_b, wasm.__wbindgen_malloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.simdmath_similarity_matrix(this.__wbg_ptr, ptr0, len0, ptr1, len1, vector_dim);
var v3 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
return v3;
}
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function(arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
console.error(getStringFromWasm0(arg0, arg1));
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
};
imports.wbg.__wbg_new_8a6f238a6ece86ea = function() {
const ret = new Error();
return ret;
};
imports.wbg.__wbg_stack_0ed75d68575b0f3c = function(arg0, arg1) {
const ret = arg1.stack;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_init_externref_table = function() {
const table = wasm.__wbindgen_export_3;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
;
};
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
return imports;
}
function __wbg_init_memory(imports, memory) {
}
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
__wbg_init.__wbindgen_wasm_module = module;
cachedDataViewMemory0 = null;
cachedFloat32ArrayMemory0 = null;
cachedUint8ArrayMemory0 = null;
wasm.__wbindgen_start();
return wasm;
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (typeof module !== 'undefined') {
if (Object.getPrototypeOf(module) === Object.prototype) {
({module} = module)
} else {
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
}
}
const imports = __wbg_get_imports();
__wbg_init_memory(imports);
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_init(module_or_path) {
if (wasm !== undefined) return wasm;
if (typeof module_or_path !== 'undefined') {
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
({module_or_path} = module_or_path)
} else {
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
}
}
if (typeof module_or_path === 'undefined') {
module_or_path = new URL('simd_math_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
module_or_path = fetch(module_or_path);
}
__wbg_init_memory(imports);
const { instance, module } = await __wbg_load(await module_or_path, imports);
return __wbg_finalize_init(instance, module);
}
export { initSync };
export default __wbg_init;
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/tools/browser/console.ts:
--------------------------------------------------------------------------------
```typescript
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';
const DEBUGGER_PROTOCOL_VERSION = '1.3';
const DEFAULT_MAX_MESSAGES = 100;
interface ConsoleToolParams {
url?: string;
includeExceptions?: boolean;
maxMessages?: number;
}
interface ConsoleMessage {
timestamp: number;
level: string;
text: string;
args?: any[];
source?: string;
url?: string;
lineNumber?: number;
stackTrace?: any;
}
interface ConsoleException {
timestamp: number;
text: string;
url?: string;
lineNumber?: number;
columnNumber?: number;
stackTrace?: any;
}
interface ConsoleResult {
success: boolean;
message: string;
tabId: number;
tabUrl: string;
tabTitle: string;
captureStartTime: number;
captureEndTime: number;
totalDurationMs: number;
messages: ConsoleMessage[];
exceptions: ConsoleException[];
messageCount: number;
exceptionCount: number;
messageLimitReached: boolean;
}
/**
* Tool for capturing console output from browser tabs
*/
class ConsoleTool extends BaseBrowserToolExecutor {
name = TOOL_NAMES.BROWSER.CONSOLE;
async execute(args: ConsoleToolParams): Promise<ToolResult> {
const { url, includeExceptions = true, maxMessages = DEFAULT_MAX_MESSAGES } = args;
let targetTab: chrome.tabs.Tab;
try {
if (url) {
// Navigate to the specified URL
targetTab = await this.navigateToUrl(url);
} else {
// Use current active tab
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!activeTab?.id) {
return createErrorResponse('No active tab found and no URL provided.');
}
targetTab = activeTab;
}
if (!targetTab?.id) {
return createErrorResponse('Failed to identify target tab.');
}
const tabId = targetTab.id;
// Capture console messages (one-time capture)
const result = await this.captureConsoleMessages(tabId, {
includeExceptions,
maxMessages,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
isError: false,
};
} catch (error: any) {
console.error('ConsoleTool: Critical error during execute:', error);
return createErrorResponse(`Error in ConsoleTool: ${error.message || String(error)}`);
}
}
private async navigateToUrl(url: string): Promise<chrome.tabs.Tab> {
// Check if URL is already open
const existingTabs = await chrome.tabs.query({ url });
if (existingTabs.length > 0 && existingTabs[0]?.id) {
const tab = existingTabs[0];
// Activate the existing tab
await chrome.tabs.update(tab.id!, { active: true });
await chrome.windows.update(tab.windowId, { focused: true });
return tab;
} else {
// Create new tab with the URL
const newTab = await chrome.tabs.create({ url, active: true });
// Wait for tab to be ready
await this.waitForTabReady(newTab.id!);
return newTab;
}
}
private async waitForTabReady(tabId: number): Promise<void> {
return new Promise((resolve) => {
const checkTab = async () => {
try {
const tab = await chrome.tabs.get(tabId);
if (tab.status === 'complete') {
resolve();
} else {
setTimeout(checkTab, 100);
}
} catch (error) {
// Tab might be closed, resolve anyway
resolve();
}
};
checkTab();
});
}
private formatConsoleArgs(args: any[]): string {
if (!args || args.length === 0) return '';
return args
.map((arg) => {
if (arg.type === 'string') {
return arg.value || '';
} else if (arg.type === 'number') {
return String(arg.value || '');
} else if (arg.type === 'boolean') {
return String(arg.value || '');
} else if (arg.type === 'object') {
return arg.description || '[Object]';
} else if (arg.type === 'undefined') {
return 'undefined';
} else if (arg.type === 'function') {
return arg.description || '[Function]';
} else {
return arg.description || arg.value || String(arg);
}
})
.join(' ');
}
private async captureConsoleMessages(
tabId: number,
options: {
includeExceptions: boolean;
maxMessages: number;
},
): Promise<ConsoleResult> {
const { includeExceptions, maxMessages } = options;
const startTime = Date.now();
const messages: ConsoleMessage[] = [];
const exceptions: ConsoleException[] = [];
let limitReached = false;
try {
// Get tab information
const tab = await chrome.tabs.get(tabId);
// Check if debugger is already attached
const targets = await chrome.debugger.getTargets();
const existingTarget = targets.find(
(t) => t.tabId === tabId && t.attached && t.type === 'page',
);
if (existingTarget && !existingTarget.extensionId) {
throw new Error(
`Debugger is already attached to tab ${tabId} by another tool (e.g., DevTools).`,
);
}
// Attach debugger
try {
await chrome.debugger.attach({ tabId }, DEBUGGER_PROTOCOL_VERSION);
} catch (error: any) {
if (error.message?.includes('Cannot attach to the target with an attached client')) {
throw new Error(
`Debugger is already attached to tab ${tabId}. This might be DevTools or another extension.`,
);
}
throw error;
}
// Set up event listener to collect messages
const collectedMessages: any[] = [];
const collectedExceptions: any[] = [];
const eventListener = (source: chrome.debugger.Debuggee, method: string, params?: any) => {
if (source.tabId !== tabId) return;
if (method === 'Log.entryAdded' && params?.entry) {
collectedMessages.push(params.entry);
} else if (method === 'Runtime.consoleAPICalled' && params) {
// Convert Runtime.consoleAPICalled to Log.entryAdded format
const logEntry = {
timestamp: params.timestamp,
level: params.type || 'log',
text: this.formatConsoleArgs(params.args || []),
source: 'console-api',
url: params.stackTrace?.callFrames?.[0]?.url,
lineNumber: params.stackTrace?.callFrames?.[0]?.lineNumber,
stackTrace: params.stackTrace,
args: params.args,
};
collectedMessages.push(logEntry);
} else if (
method === 'Runtime.exceptionThrown' &&
includeExceptions &&
params?.exceptionDetails
) {
collectedExceptions.push(params.exceptionDetails);
}
};
chrome.debugger.onEvent.addListener(eventListener);
try {
// Enable Runtime domain first to capture console API calls and exceptions
await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable');
// Also enable Log domain to capture other log entries
await chrome.debugger.sendCommand({ tabId }, 'Log.enable');
// Wait for all messages to be flushed
await new Promise((resolve) => setTimeout(resolve, 2000));
// Process collected messages
for (const entry of collectedMessages) {
if (messages.length >= maxMessages) {
limitReached = true;
break;
}
const message: ConsoleMessage = {
timestamp: entry.timestamp,
level: entry.level || 'log',
text: entry.text || '',
source: entry.source,
url: entry.url,
lineNumber: entry.lineNumber,
};
if (entry.stackTrace) {
message.stackTrace = entry.stackTrace;
}
if (entry.args && Array.isArray(entry.args)) {
message.args = entry.args;
}
messages.push(message);
}
// Process collected exceptions
for (const exceptionDetails of collectedExceptions) {
const exception: ConsoleException = {
timestamp: Date.now(),
text:
exceptionDetails.text ||
exceptionDetails.exception?.description ||
'Unknown exception',
url: exceptionDetails.url,
lineNumber: exceptionDetails.lineNumber,
columnNumber: exceptionDetails.columnNumber,
};
if (exceptionDetails.stackTrace) {
exception.stackTrace = exceptionDetails.stackTrace;
}
exceptions.push(exception);
}
} finally {
// Clean up
chrome.debugger.onEvent.removeListener(eventListener);
try {
await chrome.debugger.sendCommand({ tabId }, 'Runtime.disable');
} catch (e) {
console.warn(`ConsoleTool: Error disabling Runtime for tab ${tabId}:`, e);
}
try {
await chrome.debugger.sendCommand({ tabId }, 'Log.disable');
} catch (e) {
console.warn(`ConsoleTool: Error disabling Log for tab ${tabId}:`, e);
}
try {
await chrome.debugger.detach({ tabId });
} catch (e) {
console.warn(`ConsoleTool: Error detaching debugger for tab ${tabId}:`, e);
}
}
const endTime = Date.now();
// Sort messages by timestamp
messages.sort((a, b) => a.timestamp - b.timestamp);
exceptions.sort((a, b) => a.timestamp - b.timestamp);
return {
success: true,
message: `Console capture completed for tab ${tabId}. ${messages.length} messages, ${exceptions.length} exceptions captured.`,
tabId,
tabUrl: tab.url || '',
tabTitle: tab.title || '',
captureStartTime: startTime,
captureEndTime: endTime,
totalDurationMs: endTime - startTime,
messages,
exceptions,
messageCount: messages.length,
exceptionCount: exceptions.length,
messageLimitReached: limitReached,
};
} catch (error: any) {
console.error(`ConsoleTool: Error capturing console messages for tab ${tabId}:`, error);
throw error;
}
}
}
export const consoleTool = new ConsoleTool();
```
--------------------------------------------------------------------------------
/app/chrome-extension/utils/model-cache-manager.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Model Cache Manager
*/
const CACHE_NAME = 'onnx-model-cache-v1';
const CACHE_EXPIRY_DAYS = 30;
const MAX_CACHE_SIZE_MB = 500;
export interface CacheMetadata {
timestamp: number;
modelUrl: string;
size: number;
version: string;
}
export interface CacheEntry {
url: string;
size: number;
sizeMB: number;
timestamp: number;
age: string;
expired: boolean;
}
export interface CacheStats {
totalSize: number;
totalSizeMB: number;
entryCount: number;
entries: CacheEntry[];
}
interface CacheEntryDetails {
url: string;
timestamp: number;
size: number;
}
export class ModelCacheManager {
private static instance: ModelCacheManager | null = null;
public static getInstance(): ModelCacheManager {
if (!ModelCacheManager.instance) {
ModelCacheManager.instance = new ModelCacheManager();
}
return ModelCacheManager.instance;
}
private constructor() {}
private getCacheMetadataKey(modelUrl: string): string {
const encodedUrl = encodeURIComponent(modelUrl);
return `https://cache-metadata.local/${encodedUrl}`;
}
private isCacheExpired(metadata: CacheMetadata): boolean {
const now = Date.now();
const expiryTime = metadata.timestamp + CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000;
return now > expiryTime;
}
private isMetadataUrl(url: string): boolean {
return url.startsWith('https://cache-metadata.local/');
}
private async collectCacheEntries(): Promise<{
entries: CacheEntryDetails[];
totalSize: number;
entryCount: number;
}> {
const cache = await caches.open(CACHE_NAME);
const keys = await cache.keys();
const entries: CacheEntryDetails[] = [];
let totalSize = 0;
let entryCount = 0;
for (const request of keys) {
if (this.isMetadataUrl(request.url)) continue;
const response = await cache.match(request);
if (response) {
const blob = await response.blob();
const size = blob.size;
totalSize += size;
entryCount++;
const metadataResponse = await cache.match(this.getCacheMetadataKey(request.url));
let timestamp = 0;
if (metadataResponse) {
try {
const metadata: CacheMetadata = await metadataResponse.json();
timestamp = metadata.timestamp;
} catch (error) {
console.warn('Failed to parse cache metadata:', error);
}
}
entries.push({
url: request.url,
timestamp,
size,
});
}
}
return { entries, totalSize, entryCount };
}
public async cleanupCacheOnDemand(newDataSize: number = 0): Promise<void> {
const cache = await caches.open(CACHE_NAME);
const { entries, totalSize } = await this.collectCacheEntries();
const maxSizeBytes = MAX_CACHE_SIZE_MB * 1024 * 1024;
const projectedSize = totalSize + newDataSize;
if (projectedSize <= maxSizeBytes) {
return;
}
console.log(
`Cache size (${(totalSize / 1024 / 1024).toFixed(2)}MB) + new data (${(newDataSize / 1024 / 1024).toFixed(2)}MB) exceeds limit (${MAX_CACHE_SIZE_MB}MB), cleaning up...`,
);
const expiredEntries: CacheEntryDetails[] = [];
const validEntries: CacheEntryDetails[] = [];
for (const entry of entries) {
const metadataResponse = await cache.match(this.getCacheMetadataKey(entry.url));
let isExpired = false;
if (metadataResponse) {
try {
const metadata: CacheMetadata = await metadataResponse.json();
isExpired = this.isCacheExpired(metadata);
} catch (error) {
isExpired = true;
}
} else {
isExpired = true;
}
if (isExpired) {
expiredEntries.push(entry);
} else {
validEntries.push(entry);
}
}
let currentSize = totalSize;
for (const entry of expiredEntries) {
await cache.delete(entry.url);
await cache.delete(this.getCacheMetadataKey(entry.url));
currentSize -= entry.size;
console.log(
`Cleaned up expired cache entry: ${entry.url} (${(entry.size / 1024 / 1024).toFixed(2)}MB)`,
);
}
if (currentSize + newDataSize > maxSizeBytes) {
validEntries.sort((a, b) => a.timestamp - b.timestamp);
for (const entry of validEntries) {
if (currentSize + newDataSize <= maxSizeBytes) break;
await cache.delete(entry.url);
await cache.delete(this.getCacheMetadataKey(entry.url));
currentSize -= entry.size;
console.log(
`Cleaned up old cache entry: ${entry.url} (${(entry.size / 1024 / 1024).toFixed(2)}MB)`,
);
}
}
console.log(`Cache cleanup complete. New size: ${(currentSize / 1024 / 1024).toFixed(2)}MB`);
}
public async storeCacheMetadata(modelUrl: string, size: number): Promise<void> {
const cache = await caches.open(CACHE_NAME);
const metadata: CacheMetadata = {
timestamp: Date.now(),
modelUrl,
size,
version: CACHE_NAME,
};
const metadataResponse = new Response(JSON.stringify(metadata), {
headers: { 'Content-Type': 'application/json' },
});
await cache.put(this.getCacheMetadataKey(modelUrl), metadataResponse);
}
public async getCachedModelData(modelUrl: string): Promise<ArrayBuffer | null> {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(modelUrl);
if (!cachedResponse) {
return null;
}
const metadataResponse = await cache.match(this.getCacheMetadataKey(modelUrl));
if (metadataResponse) {
try {
const metadata: CacheMetadata = await metadataResponse.json();
if (!this.isCacheExpired(metadata)) {
console.log('Model found in cache and not expired. Loading from cache.');
return cachedResponse.arrayBuffer();
} else {
console.log('Cached model is expired, removing...');
await this.deleteCacheEntry(modelUrl);
return null;
}
} catch (error) {
console.warn('Failed to parse cache metadata, treating as expired:', error);
await this.deleteCacheEntry(modelUrl);
return null;
}
} else {
console.log('Cached model has no metadata, treating as expired...');
await this.deleteCacheEntry(modelUrl);
return null;
}
}
public async storeModelData(modelUrl: string, data: ArrayBuffer): Promise<void> {
await this.cleanupCacheOnDemand(data.byteLength);
const cache = await caches.open(CACHE_NAME);
const response = new Response(data);
await cache.put(modelUrl, response);
await this.storeCacheMetadata(modelUrl, data.byteLength);
console.log(
`Model cached successfully (${(data.byteLength / 1024 / 1024).toFixed(2)}MB): ${modelUrl}`,
);
}
public async deleteCacheEntry(modelUrl: string): Promise<void> {
const cache = await caches.open(CACHE_NAME);
await cache.delete(modelUrl);
await cache.delete(this.getCacheMetadataKey(modelUrl));
}
public async clearAllCache(): Promise<void> {
const cache = await caches.open(CACHE_NAME);
const keys = await cache.keys();
for (const request of keys) {
await cache.delete(request);
}
console.log('All model cache entries cleared');
}
public async getCacheStats(): Promise<CacheStats> {
const { entries, totalSize, entryCount } = await this.collectCacheEntries();
const cache = await caches.open(CACHE_NAME);
const cacheEntries: CacheEntry[] = [];
for (const entry of entries) {
const metadataResponse = await cache.match(this.getCacheMetadataKey(entry.url));
let expired = false;
if (metadataResponse) {
try {
const metadata: CacheMetadata = await metadataResponse.json();
expired = this.isCacheExpired(metadata);
} catch (error) {
expired = true;
}
} else {
expired = true;
}
const age =
entry.timestamp > 0
? `${Math.round((Date.now() - entry.timestamp) / (1000 * 60 * 60 * 24))} days`
: 'unknown';
cacheEntries.push({
url: entry.url,
size: entry.size,
sizeMB: Number((entry.size / 1024 / 1024).toFixed(2)),
timestamp: entry.timestamp,
age,
expired,
});
}
return {
totalSize,
totalSizeMB: Number((totalSize / 1024 / 1024).toFixed(2)),
entryCount,
entries: cacheEntries.sort((a, b) => b.timestamp - a.timestamp),
};
}
public async manualCleanup(): Promise<void> {
await this.cleanupCacheOnDemand(0);
console.log('Manual cache cleanup completed');
}
/**
* Check if a specific model is cached and not expired
* @param modelUrl The model URL to check
* @returns Promise<boolean> True if model is cached and valid
*/
public async isModelCached(modelUrl: string): Promise<boolean> {
try {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(modelUrl);
if (!cachedResponse) {
return false;
}
const metadataResponse = await cache.match(this.getCacheMetadataKey(modelUrl));
if (metadataResponse) {
try {
const metadata: CacheMetadata = await metadataResponse.json();
return !this.isCacheExpired(metadata);
} catch (error) {
console.warn('Failed to parse cache metadata for cache check:', error);
return false;
}
} else {
// No metadata means expired
return false;
}
} catch (error) {
console.error('Error checking model cache:', error);
return false;
}
}
/**
* Check if any valid (non-expired) model cache exists
* @returns Promise<boolean> True if at least one valid model cache exists
*/
public async hasAnyValidCache(): Promise<boolean> {
try {
const cache = await caches.open(CACHE_NAME);
const keys = await cache.keys();
for (const request of keys) {
if (this.isMetadataUrl(request.url)) continue;
const metadataResponse = await cache.match(this.getCacheMetadataKey(request.url));
if (metadataResponse) {
try {
const metadata: CacheMetadata = await metadataResponse.json();
if (!this.isCacheExpired(metadata)) {
return true; // Found at least one valid cache
}
} catch (error) {
// Skip invalid metadata
continue;
}
}
}
return false;
} catch (error) {
console.error('Error checking for valid cache:', error);
return false;
}
}
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/keyboard-helper.js:
--------------------------------------------------------------------------------
```javascript
/* eslint-disable */
// keyboard-helper.js
// This script is injected into the page to handle keyboard event simulation
if (window.__KEYBOARD_HELPER_INITIALIZED__) {
// Already initialized, skip
} else {
window.__KEYBOARD_HELPER_INITIALIZED__ = true;
// A map for special keys to their KeyboardEvent properties
// Key names should be lowercase for matching
const SPECIAL_KEY_MAP = {
enter: { key: 'Enter', code: 'Enter', keyCode: 13 },
tab: { key: 'Tab', code: 'Tab', keyCode: 9 },
esc: { key: 'Escape', code: 'Escape', keyCode: 27 },
escape: { key: 'Escape', code: 'Escape', keyCode: 27 },
space: { key: ' ', code: 'Space', keyCode: 32 },
backspace: { key: 'Backspace', code: 'Backspace', keyCode: 8 },
delete: { key: 'Delete', code: 'Delete', keyCode: 46 },
del: { key: 'Delete', code: 'Delete', keyCode: 46 },
up: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
arrowup: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
down: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
arrowdown: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
left: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
arrowleft: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
right: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
arrowright: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
home: { key: 'Home', code: 'Home', keyCode: 36 },
end: { key: 'End', code: 'End', keyCode: 35 },
pageup: { key: 'PageUp', code: 'PageUp', keyCode: 33 },
pagedown: { key: 'PageDown', code: 'PageDown', keyCode: 34 },
insert: { key: 'Insert', code: 'Insert', keyCode: 45 },
// Function keys
...Object.fromEntries(
Array.from({ length: 12 }, (_, i) => [
`f${i + 1}`,
{ key: `F${i + 1}`, code: `F${i + 1}`, keyCode: 112 + i },
]),
),
};
const MODIFIER_KEYS = {
ctrl: 'ctrlKey',
control: 'ctrlKey',
alt: 'altKey',
shift: 'shiftKey',
meta: 'metaKey',
command: 'metaKey',
cmd: 'metaKey',
};
/**
* Parses a key string (e.g., "Ctrl+Shift+A", "Enter") into a main key and modifiers.
* @param {string} keyString - String representation of a single key press (can include modifiers).
* @returns { {key: string, code: string, keyCode: number, charCode?: number, modifiers: {ctrlKey:boolean, altKey:boolean, shiftKey:boolean, metaKey:boolean}} | null }
* Returns null if the keyString is invalid or represents only modifiers.
*/
function parseSingleKeyCombination(keyString) {
const parts = keyString.split('+').map((part) => part.trim().toLowerCase());
const modifiers = {
ctrlKey: false,
altKey: false,
shiftKey: false,
metaKey: false,
};
let mainKeyPart = null;
for (const part of parts) {
if (MODIFIER_KEYS[part]) {
modifiers[MODIFIER_KEYS[part]] = true;
} else if (mainKeyPart === null) {
// First non-modifier is the main key
mainKeyPart = part;
} else {
// Invalid format: multiple main keys in a single combination (e.g., "Ctrl+A+B")
console.error(`Invalid key combination string: ${keyString}. Multiple main keys found.`);
return null;
}
}
if (!mainKeyPart) {
// This case could happen if the keyString is something like "Ctrl+" or just "Ctrl"
// If the intent was to press JUST 'Control', the input should be 'Control' not 'Control+'
// Let's check if mainKeyPart is actually a modifier name used as a main key
if (Object.keys(MODIFIER_KEYS).includes(parts[parts.length - 1]) && parts.length === 1) {
mainKeyPart = parts[parts.length - 1]; // e.g. user wants to press "Control" key itself
// For "Control" key itself, key: "Control", code: "ControlLeft" (or Right)
if (mainKeyPart === 'ctrl' || mainKeyPart === 'control')
return { key: 'Control', code: 'ControlLeft', keyCode: 17, modifiers };
if (mainKeyPart === 'alt') return { key: 'Alt', code: 'AltLeft', keyCode: 18, modifiers };
if (mainKeyPart === 'shift')
return { key: 'Shift', code: 'ShiftLeft', keyCode: 16, modifiers };
if (mainKeyPart === 'meta' || mainKeyPart === 'command' || mainKeyPart === 'cmd')
return { key: 'Meta', code: 'MetaLeft', keyCode: 91, modifiers };
} else {
console.error(`Invalid key combination string: ${keyString}. No main key specified.`);
return null;
}
}
const specialKey = SPECIAL_KEY_MAP[mainKeyPart];
if (specialKey) {
return { ...specialKey, modifiers };
}
// For single characters or other unmapped keys
if (mainKeyPart.length === 1) {
const charCode = mainKeyPart.charCodeAt(0);
// If Shift is active and it's a letter, use the uppercase version for 'key'
// This mimics more closely how keyboards behave.
let keyChar = mainKeyPart;
if (modifiers.shiftKey && mainKeyPart.match(/^[a-z]$/i)) {
keyChar = mainKeyPart.toUpperCase();
}
return {
key: keyChar,
code: `Key${mainKeyPart.toUpperCase()}`, // 'a' -> KeyA, 'A' -> KeyA
keyCode: charCode,
charCode: charCode, // charCode is legacy, but some old systems might use it
modifiers,
};
}
console.error(`Unknown key: ${mainKeyPart} in string "${keyString}"`);
return null; // Or handle as an error
}
/**
* Simulates a single key press (keydown, (keypress), keyup) for a parsed key.
* @param { {key: string, code: string, keyCode: number, charCode?: number, modifiers: object} } parsedKeyInfo
* @param {Element} element - Target element.
* @returns {{success: boolean, error?: string}}
*/
function dispatchKeyEvents(parsedKeyInfo, element) {
if (!parsedKeyInfo) return { success: false, error: 'Invalid key info provided for dispatch.' };
const { key, code, keyCode, charCode, modifiers } = parsedKeyInfo;
const eventOptions = {
key: key,
code: code,
bubbles: true,
cancelable: true,
composed: true, // Important for shadow DOM
view: window,
...modifiers, // ctrlKey, altKey, shiftKey, metaKey
// keyCode/which are deprecated but often set for compatibility
keyCode: keyCode || (key.length === 1 ? key.charCodeAt(0) : 0),
which: keyCode || (key.length === 1 ? key.charCodeAt(0) : 0),
};
try {
const kdRes = element.dispatchEvent(new KeyboardEvent('keydown', eventOptions));
// keypress is deprecated, but simulate if it's a character key or Enter
// Only dispatch if keydown was not cancelled and it's a character producing key
if (kdRes && (key.length === 1 || key === 'Enter' || key === ' ')) {
const keypressOptions = { ...eventOptions };
if (charCode) keypressOptions.charCode = charCode;
element.dispatchEvent(new KeyboardEvent('keypress', keypressOptions));
}
element.dispatchEvent(new KeyboardEvent('keyup', eventOptions));
return { success: true };
} catch (error) {
console.error(`Error dispatching key events for "${key}":`, error);
return {
success: false,
error: `Error dispatching key events for "${key}": ${error.message}`,
};
}
}
/**
* Simulate keyboard events on an element or document
* @param {string} keysSequenceString - String representation of key(s) (e.g., "Enter", "Ctrl+C, A, B")
* @param {Element} targetElement - Element to dispatch events on (optional)
* @param {number} delay - Delay between key sequences in milliseconds (optional)
* @returns {Promise<Object>} - Result of the keyboard operation
*/
async function simulateKeyboard(keysSequenceString, targetElement = null, delay = 0) {
try {
const element = targetElement || document.activeElement || document.body;
if (element !== document.activeElement && typeof element.focus === 'function') {
element.focus();
await new Promise((resolve) => setTimeout(resolve, 50)); // Small delay for focus
}
const keyCombinations = keysSequenceString
.split(',')
.map((k) => k.trim())
.filter((k) => k.length > 0);
const operationResults = [];
for (let i = 0; i < keyCombinations.length; i++) {
const comboString = keyCombinations[i];
const parsedKeyInfo = parseSingleKeyCombination(comboString);
if (!parsedKeyInfo) {
operationResults.push({
keyCombination: comboString,
success: false,
error: `Invalid key string or combination: ${comboString}`,
});
continue; // Skip to next combination in sequence
}
const dispatchResult = dispatchKeyEvents(parsedKeyInfo, element);
operationResults.push({
keyCombination: comboString,
...dispatchResult,
});
if (dispatchResult.error) {
// Optionally, decide if sequence should stop on first error
// For now, we continue but log the error in results
console.warn(
`Failed to simulate key combination "${comboString}": ${dispatchResult.error}`,
);
}
if (delay > 0 && i < keyCombinations.length - 1) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
// Check if all individual operations were successful
const overallSuccess = operationResults.every((r) => r.success);
return {
success: overallSuccess,
message: overallSuccess
? `Keyboard events simulated successfully: ${keysSequenceString}`
: `Some keyboard events failed for: ${keysSequenceString}`,
results: operationResults, // Detailed results for each key combination
targetElement: {
tagName: element.tagName,
id: element.id,
className: element.className,
type: element.type, // if applicable e.g. for input
},
};
} catch (error) {
console.error('Error in simulateKeyboard:', error);
return {
success: false,
error: `Error simulating keyboard events: ${error.message}`,
results: [],
};
}
}
// Listener for messages from the extension
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
if (request.action === 'simulateKeyboard') {
let targetEl = null;
if (request.selector) {
targetEl = document.querySelector(request.selector);
if (!targetEl) {
sendResponse({
success: false,
error: `Element with selector "${request.selector}" not found`,
results: [],
});
return true; // Keep channel open for async response
}
}
simulateKeyboard(request.keys, targetEl, request.delay)
.then(sendResponse)
.catch((error) => {
// This catch is for unexpected errors in simulateKeyboard promise chain itself
console.error('Unexpected error in simulateKeyboard promise chain:', error);
sendResponse({
success: false,
error: `Unexpected error during keyboard simulation: ${error.message}`,
results: [],
});
});
return true; // Indicates async response is expected
} else if (request.action === 'chrome_keyboard_ping') {
sendResponse({ status: 'pong', initialized: true }); // Respond that it's initialized
return false; // Synchronous response
}
// Not our message, or no async response needed
return false;
});
}
```
--------------------------------------------------------------------------------
/app/chrome-extension/entrypoints/background/semantic-similarity.ts:
--------------------------------------------------------------------------------
```typescript
import type { ModelPreset } from '@/utils/semantic-similarity-engine';
import { OffscreenManager } from '@/utils/offscreen-manager';
import { BACKGROUND_MESSAGE_TYPES, OFFSCREEN_MESSAGE_TYPES } from '@/common/message-types';
import { STORAGE_KEYS, ERROR_MESSAGES } from '@/common/constants';
import { hasAnyModelCache } from '@/utils/semantic-similarity-engine';
/**
* Model configuration state management interface
*/
interface ModelConfig {
modelPreset: ModelPreset;
modelVersion: 'full' | 'quantized' | 'compressed';
modelDimension: number;
}
let currentBackgroundModelConfig: ModelConfig | null = null;
/**
* Initialize semantic engine only if model cache exists
* This is called during plugin startup to avoid downloading models unnecessarily
*/
export async function initializeSemanticEngineIfCached(): Promise<boolean> {
try {
console.log('Background: Checking if semantic engine should be initialized from cache...');
const hasCachedModel = await hasAnyModelCache();
if (!hasCachedModel) {
console.log('Background: No cached models found, skipping semantic engine initialization');
return false;
}
console.log('Background: Found cached models, initializing semantic engine...');
await initializeDefaultSemanticEngine();
return true;
} catch (error) {
console.error('Background: Error during conditional semantic engine initialization:', error);
return false;
}
}
/**
* Initialize default semantic engine model
*/
export async function initializeDefaultSemanticEngine(): Promise<void> {
try {
console.log('Background: Initializing default semantic engine...');
// Update status to initializing
await updateModelStatus('initializing', 0);
const result = await chrome.storage.local.get([STORAGE_KEYS.SEMANTIC_MODEL, 'selectedVersion']);
const defaultModel =
(result[STORAGE_KEYS.SEMANTIC_MODEL] as ModelPreset) || 'multilingual-e5-small';
const defaultVersion =
(result.selectedVersion as 'full' | 'quantized' | 'compressed') || 'quantized';
const { PREDEFINED_MODELS } = await import('@/utils/semantic-similarity-engine');
const modelInfo = PREDEFINED_MODELS[defaultModel];
await OffscreenManager.getInstance().ensureOffscreenDocument();
const response = await chrome.runtime.sendMessage({
target: 'offscreen',
type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT,
config: {
useLocalFiles: false,
modelPreset: defaultModel,
modelVersion: defaultVersion,
modelDimension: modelInfo.dimension,
forceOffscreen: true,
},
});
if (response && response.success) {
currentBackgroundModelConfig = {
modelPreset: defaultModel,
modelVersion: defaultVersion,
modelDimension: modelInfo.dimension,
};
console.log('Semantic engine initialized successfully:', currentBackgroundModelConfig);
// Update status to ready
await updateModelStatus('ready', 100);
// Also initialize ContentIndexer now that semantic engine is ready
try {
const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
const contentIndexer = getGlobalContentIndexer();
contentIndexer.startSemanticEngineInitialization();
console.log('ContentIndexer initialization triggered after semantic engine initialization');
} catch (indexerError) {
console.warn(
'Failed to initialize ContentIndexer after semantic engine initialization:',
indexerError,
);
}
} else {
const errorMessage = response?.error || ERROR_MESSAGES.TOOL_EXECUTION_FAILED;
await updateModelStatus('error', 0, errorMessage, 'unknown');
throw new Error(errorMessage);
}
} catch (error: any) {
console.error('Background: Failed to initialize default semantic engine:', error);
const errorMessage = error?.message || 'Unknown error during semantic engine initialization';
await updateModelStatus('error', 0, errorMessage, 'unknown');
// Don't throw error, let the extension continue running
}
}
/**
* Check if model switch is needed
*/
function needsModelSwitch(
modelPreset: ModelPreset,
modelVersion: 'full' | 'quantized' | 'compressed',
modelDimension?: number,
): boolean {
if (!currentBackgroundModelConfig) {
return true;
}
const keyFields = ['modelPreset', 'modelVersion', 'modelDimension'];
for (const field of keyFields) {
const newValue =
field === 'modelPreset'
? modelPreset
: field === 'modelVersion'
? modelVersion
: modelDimension;
if (newValue !== currentBackgroundModelConfig[field as keyof ModelConfig]) {
return true;
}
}
return false;
}
/**
* Handle model switching
*/
export async function handleModelSwitch(
modelPreset: ModelPreset,
modelVersion: 'full' | 'quantized' | 'compressed' = 'quantized',
modelDimension?: number,
previousDimension?: number,
): Promise<{ success: boolean; error?: string }> {
try {
const needsSwitch = needsModelSwitch(modelPreset, modelVersion, modelDimension);
if (!needsSwitch) {
await updateModelStatus('ready', 100);
return { success: true };
}
await updateModelStatus('downloading', 0);
try {
await OffscreenManager.getInstance().ensureOffscreenDocument();
} catch (offscreenError) {
console.error('Background: Failed to create offscreen document:', offscreenError);
const errorMessage = `Failed to create offscreen document: ${offscreenError}`;
await updateModelStatus('error', 0, errorMessage, 'unknown');
return { success: false, error: errorMessage };
}
const response = await chrome.runtime.sendMessage({
target: 'offscreen',
type: OFFSCREEN_MESSAGE_TYPES.SIMILARITY_ENGINE_INIT,
config: {
useLocalFiles: false,
modelPreset: modelPreset,
modelVersion: modelVersion,
modelDimension: modelDimension,
forceOffscreen: true,
},
});
if (response && response.success) {
currentBackgroundModelConfig = {
modelPreset: modelPreset,
modelVersion: modelVersion,
modelDimension: modelDimension!,
};
// Only reinitialize ContentIndexer when dimension changes
try {
if (modelDimension && previousDimension && modelDimension !== previousDimension) {
const { getGlobalContentIndexer } = await import('@/utils/content-indexer');
const contentIndexer = getGlobalContentIndexer();
await contentIndexer.reinitialize();
}
} catch (indexerError) {
console.warn('Background: Failed to reinitialize ContentIndexer:', indexerError);
}
await updateModelStatus('ready', 100);
return { success: true };
} else {
const errorMessage = response?.error || 'Failed to switch model';
const errorType = analyzeErrorType(errorMessage);
await updateModelStatus('error', 0, errorMessage, errorType);
throw new Error(errorMessage);
}
} catch (error: any) {
console.error('Model switch failed:', error);
const errorMessage = error.message || 'Unknown error';
const errorType = analyzeErrorType(errorMessage);
await updateModelStatus('error', 0, errorMessage, errorType);
return { success: false, error: errorMessage };
}
}
/**
* Get model status
*/
export async function handleGetModelStatus(): Promise<{
success: boolean;
status?: any;
error?: string;
}> {
try {
if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
console.error('Background: chrome.storage.local is not available for status query');
return {
success: true,
status: {
initializationStatus: 'idle',
downloadProgress: 0,
isDownloading: false,
lastUpdated: Date.now(),
},
};
}
const result = await chrome.storage.local.get(['modelState']);
const modelState = result.modelState || {
status: 'idle',
downloadProgress: 0,
isDownloading: false,
lastUpdated: Date.now(),
};
return {
success: true,
status: {
initializationStatus: modelState.status,
downloadProgress: modelState.downloadProgress,
isDownloading: modelState.isDownloading,
lastUpdated: modelState.lastUpdated,
errorMessage: modelState.errorMessage,
errorType: modelState.errorType,
},
};
} catch (error: any) {
console.error('Failed to get model status:', error);
return { success: false, error: error.message };
}
}
/**
* Update model status
*/
export async function updateModelStatus(
status: string,
progress: number,
errorMessage?: string,
errorType?: string,
): Promise<void> {
try {
// Check if chrome.storage is available
if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
console.error('Background: chrome.storage.local is not available for status update');
return;
}
const modelState = {
status,
downloadProgress: progress,
isDownloading: status === 'downloading' || status === 'initializing',
lastUpdated: Date.now(),
errorMessage: errorMessage || '',
errorType: errorType || '',
};
await chrome.storage.local.set({ modelState });
} catch (error) {
console.error('Failed to update model status:', error);
}
}
/**
* Handle model status updates from offscreen document
*/
export async function handleUpdateModelStatus(
modelState: any,
): Promise<{ success: boolean; error?: string }> {
try {
// Check if chrome.storage is available
if (typeof chrome === 'undefined' || !chrome.storage || !chrome.storage.local) {
console.error('Background: chrome.storage.local is not available');
return { success: false, error: 'chrome.storage.local is not available' };
}
await chrome.storage.local.set({ modelState });
return { success: true };
} catch (error: any) {
console.error('Background: Failed to update model status:', error);
return { success: false, error: error.message };
}
}
/**
* Analyze error type based on error message
*/
function analyzeErrorType(errorMessage: string): 'network' | 'file' | 'unknown' {
const message = errorMessage.toLowerCase();
if (
message.includes('network') ||
message.includes('fetch') ||
message.includes('timeout') ||
message.includes('connection') ||
message.includes('cors') ||
message.includes('failed to fetch')
) {
return 'network';
}
if (
message.includes('corrupt') ||
message.includes('invalid') ||
message.includes('format') ||
message.includes('parse') ||
message.includes('decode') ||
message.includes('onnx')
) {
return 'file';
}
return 'unknown';
}
/**
* Initialize semantic similarity module message listeners
*/
export const initSemanticSimilarityListener = () => {
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === BACKGROUND_MESSAGE_TYPES.SWITCH_SEMANTIC_MODEL) {
handleModelSwitch(
message.modelPreset,
message.modelVersion,
message.modelDimension,
message.previousDimension,
)
.then((result: { success: boolean; error?: string }) => sendResponse(result))
.catch((error: any) => sendResponse({ success: false, error: error.message }));
return true;
} else if (message.type === BACKGROUND_MESSAGE_TYPES.GET_MODEL_STATUS) {
handleGetModelStatus()
.then((result: { success: boolean; status?: any; error?: string }) => sendResponse(result))
.catch((error: any) => sendResponse({ success: false, error: error.message }));
return true;
} else if (message.type === BACKGROUND_MESSAGE_TYPES.UPDATE_MODEL_STATUS) {
handleUpdateModelStatus(message.modelState)
.then((result: { success: boolean; error?: string }) => sendResponse(result))
.catch((error: any) => sendResponse({ success: false, error: error.message }));
return true;
} else if (message.type === BACKGROUND_MESSAGE_TYPES.INITIALIZE_SEMANTIC_ENGINE) {
initializeDefaultSemanticEngine()
.then(() => sendResponse({ success: true }))
.catch((error: any) => sendResponse({ success: false, error: error.message }));
return true;
}
});
};
```
--------------------------------------------------------------------------------
/app/chrome-extension/inject-scripts/interactive-elements-helper.js:
--------------------------------------------------------------------------------
```javascript
/* eslint-disable */
// interactive-elements-helper.js
// This script is injected into the page to find interactive elements.
// Final version by Calvin, featuring a multi-layered fallback strategy
// and comprehensive element support, built on a performant and reliable core.
(function () {
// Prevent re-initialization
if (window.__INTERACTIVE_ELEMENTS_HELPER_INITIALIZED__) {
return;
}
window.__INTERACTIVE_ELEMENTS_HELPER_INITIALIZED__ = true;
/**
* @typedef {Object} ElementInfo
* @property {string} type - The type of the element (e.g., 'button', 'link').
* @property {string} selector - A CSS selector to uniquely identify the element.
* @property {string} text - The visible text or accessible name of the element.
* @property {boolean} isInteractive - Whether the element is currently interactive.
* @property {Object} [coordinates] - The coordinates of the element if requested.
* @property {boolean} [disabled] - For elements that can be disabled.
* @property {string} [href] - For links.
* @property {boolean} [checked] - for checkboxes and radio buttons.
*/
/**
* Configuration for element types and their corresponding selectors.
* Now more comprehensive with common ARIA roles.
*/
const ELEMENT_CONFIG = {
button: 'button, input[type="button"], input[type="submit"], [role="button"]',
link: 'a[href], [role="link"]',
input:
'input:not([type="button"]):not([type="submit"]):not([type="checkbox"]):not([type="radio"])',
checkbox: 'input[type="checkbox"], [role="checkbox"]',
radio: 'input[type="radio"], [role="radio"]',
textarea: 'textarea',
select: 'select',
tab: '[role="tab"]',
// Generic interactive elements: combines tabindex, common roles, and explicit handlers.
// This is the key to finding custom-built interactive components.
interactive: `[onclick], [tabindex]:not([tabindex^="-"]), [role="menuitem"], [role="slider"], [role="option"], [role="treeitem"]`,
};
// A combined selector for ANY interactive element, used in the fallback logic.
const ANY_INTERACTIVE_SELECTOR = Object.values(ELEMENT_CONFIG).join(', ');
// --- Core Helper Functions ---
/**
* Checks if an element is genuinely visible on the page.
* "Visible" means it's not styled with display:none, visibility:hidden, etc.
* This check intentionally IGNORES whether the element is within the current viewport.
* @param {Element} el The element to check.
* @returns {boolean} True if the element is visible.
*/
function isElementVisible(el) {
if (!el || !el.isConnected) return false;
const style = window.getComputedStyle(el);
if (
style.display === 'none' ||
style.visibility === 'hidden' ||
parseFloat(style.opacity) === 0
) {
return false;
}
const rect = el.getBoundingClientRect();
return rect.width > 0 || rect.height > 0 || el.tagName === 'A'; // Allow zero-size anchors as they can still be navigated
}
/**
* Checks if an element is considered interactive (not disabled or hidden from accessibility).
* @param {Element} el The element to check.
* @returns {boolean} True if the element is interactive.
*/
function isElementInteractive(el) {
if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true') {
return false;
}
if (el.closest('[aria-hidden="true"]')) {
return false;
}
return true;
}
/**
* Generates a reasonably stable CSS selector for a given element.
* @param {Element} el The element.
* @returns {string} A CSS selector.
*/
function generateSelector(el) {
if (!(el instanceof Element)) return '';
if (el.id) {
const idSelector = `#${CSS.escape(el.id)}`;
if (document.querySelectorAll(idSelector).length === 1) return idSelector;
}
for (const attr of ['data-testid', 'data-cy', 'name']) {
const attrValue = el.getAttribute(attr);
if (attrValue) {
const attrSelector = `[${attr}="${CSS.escape(attrValue)}"]`;
if (document.querySelectorAll(attrSelector).length === 1) return attrSelector;
}
}
let path = '';
let current = el;
while (current && current.nodeType === Node.ELEMENT_NODE && current.tagName !== 'BODY') {
let selector = current.tagName.toLowerCase();
const parent = current.parentElement;
if (parent) {
const siblings = Array.from(parent.children).filter(
(child) => child.tagName === current.tagName,
);
if (siblings.length > 1) {
const index = siblings.indexOf(current) + 1;
selector += `:nth-of-type(${index})`;
}
}
path = path ? `${selector} > ${path}` : selector;
current = parent;
}
return path ? `body > ${path}` : 'body';
}
/**
* Finds the accessible name for an element (label, aria-label, etc.).
* @param {Element} el The element.
* @returns {string} The accessible name.
*/
function getAccessibleName(el) {
const labelledby = el.getAttribute('aria-labelledby');
if (labelledby) {
const labelElement = document.getElementById(labelledby);
if (labelElement) return labelElement.textContent?.trim() || '';
}
const ariaLabel = el.getAttribute('aria-label');
if (ariaLabel) return ariaLabel.trim();
if (el.id) {
const label = document.querySelector(`label[for="${el.id}"]`);
if (label) return label.textContent?.trim() || '';
}
const parentLabel = el.closest('label');
if (parentLabel) return parentLabel.textContent?.trim() || '';
return (
el.getAttribute('placeholder') ||
el.getAttribute('value') ||
el.textContent?.trim() ||
el.getAttribute('title') ||
''
);
}
/**
* Simple subsequence matching for fuzzy search.
* @param {string} text The text to search within.
* @param {string} query The query subsequence.
* @returns {boolean}
*/
function fuzzyMatch(text, query) {
if (!text || !query) return false;
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
let textIndex = 0;
let queryIndex = 0;
while (textIndex < lowerText.length && queryIndex < lowerQuery.length) {
if (lowerText[textIndex] === lowerQuery[queryIndex]) {
queryIndex++;
}
textIndex++;
}
return queryIndex === lowerQuery.length;
}
/**
* Creates the standardized info object for an element.
* Modified to handle the new 'text' type from the final fallback.
*/
function createElementInfo(el, type, includeCoordinates, isInteractiveOverride = null) {
const isActuallyInteractive = isElementInteractive(el);
const info = {
type,
selector: generateSelector(el),
text: getAccessibleName(el) || el.textContent?.trim(),
isInteractive: isInteractiveOverride !== null ? isInteractiveOverride : isActuallyInteractive,
disabled: el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true',
};
if (includeCoordinates) {
const rect = el.getBoundingClientRect();
info.coordinates = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
},
};
}
return info;
}
/**
* [CORE UTILITY] Finds interactive elements based on a set of types.
* This is our high-performance Layer 1 search function.
*/
function findInteractiveElements(options = {}) {
const { textQuery, includeCoordinates = true, types = Object.keys(ELEMENT_CONFIG) } = options;
const selectorsToFind = types
.map((type) => ELEMENT_CONFIG[type])
.filter(Boolean)
.join(', ');
if (!selectorsToFind) return [];
const targetElements = Array.from(document.querySelectorAll(selectorsToFind));
const uniqueElements = new Set(targetElements);
const results = [];
for (const el of uniqueElements) {
if (!isElementVisible(el) || !isElementInteractive(el)) continue;
const accessibleName = getAccessibleName(el);
if (textQuery && !fuzzyMatch(accessibleName, textQuery)) continue;
let elementType = 'unknown';
for (const [type, typeSelector] of Object.entries(ELEMENT_CONFIG)) {
if (el.matches(typeSelector)) {
elementType = type;
break;
}
}
results.push(createElementInfo(el, elementType, includeCoordinates));
}
return results;
}
/**
* [ORCHESTRATOR] The main entry point that implements the 3-layer fallback logic.
* @param {object} options - The main search options.
* @returns {ElementInfo[]}
*/
function findElementsByTextWithFallback(options = {}) {
const { textQuery, includeCoordinates = true } = options;
if (!textQuery) {
return findInteractiveElements({ ...options, types: Object.keys(ELEMENT_CONFIG) });
}
// --- Layer 1: High-reliability search for interactive elements matching text ---
let results = findInteractiveElements({ ...options, types: Object.keys(ELEMENT_CONFIG) });
if (results.length > 0) {
return results;
}
// --- Layer 2: Find text, then find its interactive ancestor ---
const lowerCaseText = textQuery.toLowerCase();
const xPath = `//text()[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '${lowerCaseText}')]`;
const textNodes = document.evaluate(
xPath,
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null,
);
const interactiveElements = new Set();
if (textNodes.snapshotLength > 0) {
for (let i = 0; i < textNodes.snapshotLength; i++) {
const parentElement = textNodes.snapshotItem(i).parentElement;
if (parentElement) {
const interactiveAncestor = parentElement.closest(ANY_INTERACTIVE_SELECTOR);
if (
interactiveAncestor &&
isElementVisible(interactiveAncestor) &&
isElementInteractive(interactiveAncestor)
) {
interactiveElements.add(interactiveAncestor);
}
}
}
if (interactiveElements.size > 0) {
return Array.from(interactiveElements).map((el) => {
let elementType = 'interactive';
for (const [type, typeSelector] of Object.entries(ELEMENT_CONFIG)) {
if (el.matches(typeSelector)) {
elementType = type;
break;
}
}
return createElementInfo(el, elementType, includeCoordinates);
});
}
}
// --- Layer 3: Final fallback, return any element containing the text ---
const leafElements = new Set();
for (let i = 0; i < textNodes.snapshotLength; i++) {
const parentElement = textNodes.snapshotItem(i).parentElement;
if (parentElement && isElementVisible(parentElement)) {
leafElements.add(parentElement);
}
}
const finalElements = Array.from(leafElements).filter((el) => {
return ![...leafElements].some((otherEl) => el !== otherEl && el.contains(otherEl));
});
return finalElements.map((el) => createElementInfo(el, 'text', includeCoordinates, true));
}
// --- Chrome Message Listener ---
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
if (request.action === 'getInteractiveElements') {
try {
let elements;
if (request.selector) {
// If a selector is provided, bypass the text-based logic and use a direct query.
const foundEls = Array.from(document.querySelectorAll(request.selector));
elements = foundEls.map((el) =>
createElementInfo(
el,
'selected',
request.includeCoordinates !== false,
isElementInteractive(el),
),
);
} else {
// Otherwise, use our powerful multi-layered text search
elements = findElementsByTextWithFallback(request);
}
sendResponse({ success: true, elements });
} catch (error) {
console.error('Error in getInteractiveElements:', error);
sendResponse({ success: false, error: error.message });
}
return true; // Async response
} else if (request.action === 'chrome_get_interactive_elements_ping') {
sendResponse({ status: 'pong' });
return false;
}
});
console.log('Interactive elements helper script loaded');
})();
```
--------------------------------------------------------------------------------
/app/chrome-extension/_locales/en/messages.json:
--------------------------------------------------------------------------------
```json
{
"extensionName": {
"message": "chrome-mcp-server",
"description": "Extension name"
},
"extensionDescription": {
"message": "Exposes browser capabilities with your own chrome",
"description": "Extension description"
},
"nativeServerConfigLabel": {
"message": "Native Server Configuration",
"description": "Main section header for native server settings"
},
"semanticEngineLabel": {
"message": "Semantic Engine",
"description": "Main section header for semantic engine"
},
"embeddingModelLabel": {
"message": "Embedding Model",
"description": "Main section header for model selection"
},
"indexDataManagementLabel": {
"message": "Index Data Management",
"description": "Main section header for data management"
},
"modelCacheManagementLabel": {
"message": "Model Cache Management",
"description": "Main section header for cache management"
},
"statusLabel": {
"message": "Status",
"description": "Generic status label"
},
"runningStatusLabel": {
"message": "Running Status",
"description": "Server running status label"
},
"connectionStatusLabel": {
"message": "Connection Status",
"description": "Connection status label"
},
"lastUpdatedLabel": {
"message": "Last Updated:",
"description": "Last updated timestamp label"
},
"connectButton": {
"message": "Connect",
"description": "Connect button text"
},
"disconnectButton": {
"message": "Disconnect",
"description": "Disconnect button text"
},
"connectingStatus": {
"message": "Connecting...",
"description": "Connecting status message"
},
"connectedStatus": {
"message": "Connected",
"description": "Connected status message"
},
"disconnectedStatus": {
"message": "Disconnected",
"description": "Disconnected status message"
},
"detectingStatus": {
"message": "Detecting...",
"description": "Detecting status message"
},
"serviceRunningStatus": {
"message": "Service Running (Port: $PORT$)",
"description": "Service running with port number",
"placeholders": {
"port": {
"content": "$1",
"example": "12306"
}
}
},
"serviceNotConnectedStatus": {
"message": "Service Not Connected",
"description": "Service not connected status"
},
"connectedServiceNotStartedStatus": {
"message": "Connected, Service Not Started",
"description": "Connected but service not started status"
},
"mcpServerConfigLabel": {
"message": "MCP Server Configuration",
"description": "MCP server configuration section label"
},
"connectionPortLabel": {
"message": "Connection Port",
"description": "Connection port input label"
},
"refreshStatusButton": {
"message": "Refresh Status",
"description": "Refresh status button tooltip"
},
"copyConfigButton": {
"message": "Copy Configuration",
"description": "Copy configuration button text"
},
"retryButton": {
"message": "Retry",
"description": "Retry button text"
},
"cancelButton": {
"message": "Cancel",
"description": "Cancel button text"
},
"confirmButton": {
"message": "Confirm",
"description": "Confirm button text"
},
"saveButton": {
"message": "Save",
"description": "Save button text"
},
"closeButton": {
"message": "Close",
"description": "Close button text"
},
"resetButton": {
"message": "Reset",
"description": "Reset button text"
},
"initializingStatus": {
"message": "Initializing...",
"description": "Initializing progress message"
},
"processingStatus": {
"message": "Processing...",
"description": "Processing progress message"
},
"loadingStatus": {
"message": "Loading...",
"description": "Loading progress message"
},
"clearingStatus": {
"message": "Clearing...",
"description": "Clearing progress message"
},
"cleaningStatus": {
"message": "Cleaning...",
"description": "Cleaning progress message"
},
"downloadingStatus": {
"message": "Downloading...",
"description": "Downloading progress message"
},
"semanticEngineReadyStatus": {
"message": "Semantic Engine Ready",
"description": "Semantic engine ready status"
},
"semanticEngineInitializingStatus": {
"message": "Semantic Engine Initializing...",
"description": "Semantic engine initializing status"
},
"semanticEngineInitFailedStatus": {
"message": "Semantic Engine Initialization Failed",
"description": "Semantic engine initialization failed status"
},
"semanticEngineNotInitStatus": {
"message": "Semantic Engine Not Initialized",
"description": "Semantic engine not initialized status"
},
"initSemanticEngineButton": {
"message": "Initialize Semantic Engine",
"description": "Initialize semantic engine button text"
},
"reinitializeButton": {
"message": "Reinitialize",
"description": "Reinitialize button text"
},
"downloadingModelStatus": {
"message": "Downloading Model... $PROGRESS$%",
"description": "Model download progress with percentage",
"placeholders": {
"progress": {
"content": "$1",
"example": "50"
}
}
},
"switchingModelStatus": {
"message": "Switching Model...",
"description": "Model switching progress message"
},
"modelLoadedStatus": {
"message": "Model Loaded",
"description": "Model successfully loaded status"
},
"modelFailedStatus": {
"message": "Model Failed to Load",
"description": "Model failed to load status"
},
"lightweightModelDescription": {
"message": "Lightweight Multilingual Model",
"description": "Description for lightweight model option"
},
"betterThanSmallDescription": {
"message": "Slightly larger than e5-small, but better performance",
"description": "Description for medium model option"
},
"multilingualModelDescription": {
"message": "Multilingual Semantic Model",
"description": "Description for multilingual model option"
},
"fastPerformance": {
"message": "Fast",
"description": "Fast performance indicator"
},
"balancedPerformance": {
"message": "Balanced",
"description": "Balanced performance indicator"
},
"accuratePerformance": {
"message": "Accurate",
"description": "Accurate performance indicator"
},
"networkErrorMessage": {
"message": "Network connection error, please check network and retry",
"description": "Network connection error message"
},
"modelCorruptedErrorMessage": {
"message": "Model file corrupted or incomplete, please retry download",
"description": "Model corruption error message"
},
"unknownErrorMessage": {
"message": "Unknown error, please check if your network can access HuggingFace",
"description": "Unknown error fallback message"
},
"permissionDeniedErrorMessage": {
"message": "Permission denied",
"description": "Permission denied error message"
},
"timeoutErrorMessage": {
"message": "Operation timed out",
"description": "Timeout error message"
},
"indexedPagesLabel": {
"message": "Indexed Pages",
"description": "Number of indexed pages label"
},
"indexSizeLabel": {
"message": "Index Size",
"description": "Index size label"
},
"activeTabsLabel": {
"message": "Active Tabs",
"description": "Number of active tabs label"
},
"vectorDocumentsLabel": {
"message": "Vector Documents",
"description": "Number of vector documents label"
},
"cacheSizeLabel": {
"message": "Cache Size",
"description": "Cache size label"
},
"cacheEntriesLabel": {
"message": "Cache Entries",
"description": "Number of cache entries label"
},
"clearAllDataButton": {
"message": "Clear All Data",
"description": "Clear all data button text"
},
"clearAllCacheButton": {
"message": "Clear All Cache",
"description": "Clear all cache button text"
},
"cleanExpiredCacheButton": {
"message": "Clean Expired Cache",
"description": "Clean expired cache button text"
},
"exportDataButton": {
"message": "Export Data",
"description": "Export data button text"
},
"importDataButton": {
"message": "Import Data",
"description": "Import data button text"
},
"confirmClearDataTitle": {
"message": "Confirm Clear Data",
"description": "Clear data confirmation dialog title"
},
"settingsTitle": {
"message": "Settings",
"description": "Settings dialog title"
},
"aboutTitle": {
"message": "About",
"description": "About dialog title"
},
"helpTitle": {
"message": "Help",
"description": "Help dialog title"
},
"clearDataWarningMessage": {
"message": "This operation will clear all indexed webpage content and vector data, including:",
"description": "Clear data warning message"
},
"clearDataList1": {
"message": "All webpage text content index",
"description": "First item in clear data list"
},
"clearDataList2": {
"message": "Vector embedding data",
"description": "Second item in clear data list"
},
"clearDataList3": {
"message": "Search history and cache",
"description": "Third item in clear data list"
},
"clearDataIrreversibleWarning": {
"message": "This operation is irreversible! After clearing, you need to browse webpages again to rebuild the index.",
"description": "Irreversible operation warning"
},
"confirmClearButton": {
"message": "Confirm Clear",
"description": "Confirm clear action button"
},
"cacheDetailsLabel": {
"message": "Cache Details",
"description": "Cache details section label"
},
"noCacheDataMessage": {
"message": "No cache data",
"description": "No cache data available message"
},
"loadingCacheInfoStatus": {
"message": "Loading cache information...",
"description": "Loading cache information status"
},
"processingCacheStatus": {
"message": "Processing cache...",
"description": "Processing cache status"
},
"expiredLabel": {
"message": "Expired",
"description": "Expired item label"
},
"bookmarksBarLabel": {
"message": "Bookmarks Bar",
"description": "Bookmarks bar folder name"
},
"newTabLabel": {
"message": "New Tab",
"description": "New tab label"
},
"currentPageLabel": {
"message": "Current Page",
"description": "Current page label"
},
"menuLabel": {
"message": "Menu",
"description": "Menu accessibility label"
},
"navigationLabel": {
"message": "Navigation",
"description": "Navigation accessibility label"
},
"mainContentLabel": {
"message": "Main Content",
"description": "Main content accessibility label"
},
"languageSelectorLabel": {
"message": "Language",
"description": "Language selector label"
},
"themeLabel": {
"message": "Theme",
"description": "Theme selector label"
},
"lightTheme": {
"message": "Light",
"description": "Light theme option"
},
"darkTheme": {
"message": "Dark",
"description": "Dark theme option"
},
"autoTheme": {
"message": "Auto",
"description": "Auto theme option"
},
"advancedSettingsLabel": {
"message": "Advanced Settings",
"description": "Advanced settings section label"
},
"debugModeLabel": {
"message": "Debug Mode",
"description": "Debug mode toggle label"
},
"verboseLoggingLabel": {
"message": "Verbose Logging",
"description": "Verbose logging toggle label"
},
"successNotification": {
"message": "Operation completed successfully",
"description": "Generic success notification"
},
"warningNotification": {
"message": "Warning: Please review before proceeding",
"description": "Generic warning notification"
},
"infoNotification": {
"message": "Information",
"description": "Generic info notification"
},
"configCopiedNotification": {
"message": "Configuration copied to clipboard",
"description": "Configuration copied success message"
},
"dataClearedNotification": {
"message": "Data cleared successfully",
"description": "Data cleared success message"
},
"bytesUnit": {
"message": "bytes",
"description": "Bytes unit"
},
"kilobytesUnit": {
"message": "KB",
"description": "Kilobytes unit"
},
"megabytesUnit": {
"message": "MB",
"description": "Megabytes unit"
},
"gigabytesUnit": {
"message": "GB",
"description": "Gigabytes unit"
},
"itemsUnit": {
"message": "items",
"description": "Items count unit"
},
"pagesUnit": {
"message": "pages",
"description": "Pages count unit"
}
}
```