Add browser extension, docs, mcp server updates
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
const { defineConfig } = require('@vscode/test-cli');
|
||||||
|
|
||||||
|
module.exports = defineConfig([
|
||||||
|
{
|
||||||
|
label: 'unitTests',
|
||||||
|
files: 'out/test/**/*.test.js',
|
||||||
|
version: 'stable',
|
||||||
|
mocha: {
|
||||||
|
ui: 'tdd',
|
||||||
|
timeout: 20000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'browserExtension',
|
||||||
|
files: 'out/test/browser-extension/**/*.test.js',
|
||||||
|
version: 'stable',
|
||||||
|
mocha: {
|
||||||
|
ui: 'bdd',
|
||||||
|
timeout: 10000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'mcpTools',
|
||||||
|
files: 'out/test/mcp-tools/**/*.test.js',
|
||||||
|
version: 'stable',
|
||||||
|
workspaceFolder: './test-workspace',
|
||||||
|
mocha: {
|
||||||
|
ui: 'bdd',
|
||||||
|
timeout: 20000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
Copyright 2025 Hanzo AI
|
Copyright 2025 Hanzo Industries Inc
|
||||||
Copyright 2024 Hanzo AI
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"hanzo": {
|
|
||||||
"command": "node",
|
|
||||||
"args": [
|
|
||||||
"/path/to/hanzo/extension/dist/mcp-server.js"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"HANZO_WORKSPACE": "/path/to/your/workspace"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-17853
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -2,7 +2,7 @@
|
|||||||
"name": "hanzo-ai",
|
"name": "hanzo-ai",
|
||||||
"displayName": "Hanzo AI",
|
"displayName": "Hanzo AI",
|
||||||
"description": "The ultimate meta AI development platform. Manage and run all LLMs and CLI tools (Claude, Codex, Gemini, OpenHands, Aider) in one unified interface. Features MCP integration, async execution, git worktrees, and universal context sync.",
|
"description": "The ultimate meta AI development platform. Manage and run all LLMs and CLI tools (Claude, Codex, Gemini, OpenHands, Aider) in one unified interface. Features MCP integration, async execution, git worktrees, and universal context sync.",
|
||||||
"version": "1.5.4",
|
"version": "1.5.7",
|
||||||
"publisher": "hanzo-ai",
|
"publisher": "hanzo-ai",
|
||||||
"private": false,
|
"private": false,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -295,6 +295,7 @@
|
|||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/inquirer": "^9.0.8",
|
"@types/inquirer": "^9.0.8",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/minimatch": "^5.1.2",
|
"@types/minimatch": "^5.1.2",
|
||||||
"@types/mocha": "^10.0.10",
|
"@types/mocha": "^10.0.10",
|
||||||
@@ -308,7 +309,8 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitest/coverage-v8": "^0.34.6",
|
"@vitest/coverage-v8": "^0.34.6",
|
||||||
"@vscode/test-electron": "^2.3.8",
|
"@vscode/test-cli": "^0.0.11",
|
||||||
|
"@vscode/test-electron": "^2.5.2",
|
||||||
"@vscode/vsce": "^2.24.0",
|
"@vscode/vsce": "^2.24.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.2.0",
|
||||||
@@ -317,6 +319,7 @@
|
|||||||
"esbuild": "^0.25.6",
|
"esbuild": "^0.25.6",
|
||||||
"eslint": "^8.26.0",
|
"eslint": "^8.26.0",
|
||||||
"glob": "^10.3.10",
|
"glob": "^10.3.10",
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
"mocha": "^11.0.1",
|
"mocha": "^11.0.1",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"sinon": "^21.0.0",
|
"sinon": "^21.0.0",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"hanzo-dev": "./dist/cli/dev.js"
|
"hanzo-dev": "./dist/cli/dev.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "esbuild src/cli/dev.ts --bundle --platform=node --target=node16 --outfile=dist/cli/dev.js --external:vscode --external:inquirer --banner:js='#!/usr/bin/env node' && chmod +x dist/cli/dev.js",
|
||||||
"dev": "tsc --watch",
|
"dev": "tsc --watch",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program
|
||||||
|
.name('@hanzo/dev')
|
||||||
|
.description('Hanzo Dev - Meta AI development CLI')
|
||||||
|
.version('1.0.0');
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('claude [query...]')
|
||||||
|
.description('Run Claude AI')
|
||||||
|
.action((query) => {
|
||||||
|
console.log(chalk.blue('🤖 Running Claude AI...'));
|
||||||
|
console.log('Query:', query.join(' '));
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('codex [query...]')
|
||||||
|
.description('Run OpenAI Codex')
|
||||||
|
.action((query) => {
|
||||||
|
console.log(chalk.green('🤖 Running OpenAI Codex...'));
|
||||||
|
console.log('Query:', query.join(' '));
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('gemini [query...]')
|
||||||
|
.description('Run Google Gemini')
|
||||||
|
.action((query) => {
|
||||||
|
console.log(chalk.yellow('🤖 Running Google Gemini...'));
|
||||||
|
console.log('Query:', query.join(' '));
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse();
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"description": "Hanzo MCP Server - Model Context Protocol tools for AI development",
|
"description": "Hanzo MCP Server - Model Context Protocol tools for AI development",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "esbuild src/index.ts --bundle --platform=node --target=node16 --outfile=dist/index.js --external:@modelcontextprotocol/sdk --external:vscode",
|
||||||
"dev": "tsc --watch",
|
"dev": "tsc --watch",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// @hanzo/mcp - MCP Server Package
|
||||||
|
export * from '@modelcontextprotocol/sdk';
|
||||||
|
|
||||||
|
// Export a simple MCP server factory
|
||||||
|
export function createMCPServer(config?: any) {
|
||||||
|
console.log('Creating MCP server with config:', config);
|
||||||
|
return {
|
||||||
|
start: () => console.log('MCP server started'),
|
||||||
|
stop: () => console.log('MCP server stopped')
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -34,6 +34,10 @@ const packageJson = {
|
|||||||
scripts: {
|
scripts: {
|
||||||
start: 'node server.js'
|
start: 'node server.js'
|
||||||
},
|
},
|
||||||
|
dependencies: {
|
||||||
|
'@modelcontextprotocol/sdk': '^1.0.0',
|
||||||
|
'zod': '^3.22.0'
|
||||||
|
},
|
||||||
keywords: [
|
keywords: [
|
||||||
'mcp',
|
'mcp',
|
||||||
'model-context-protocol',
|
'model-context-protocol',
|
||||||
@@ -126,7 +130,7 @@ if (command === 'install' || (!command && !args.includes('--help')) || args.incl
|
|||||||
// Add Hanzo MCP
|
// Add Hanzo MCP
|
||||||
config.mcpServers['hanzo-mcp'] = {
|
config.mcpServers['hanzo-mcp'] = {
|
||||||
command: 'node',
|
command: 'node',
|
||||||
args: [path.join(__dirname, 'server.js')],
|
args: [path.join(__dirname, 'server.js'), '--anon'],
|
||||||
env: {}
|
env: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -157,15 +161,19 @@ if (command === 'install' || (!command && !args.includes('--help')) || args.incl
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else if (command === 'start') {
|
} else if (command === 'start') {
|
||||||
// Start the MCP server directly
|
// Start the MCP server directly (default to anonymous mode)
|
||||||
const server = spawn('node', [path.join(__dirname, 'server.js')], {
|
const serverArgs = [path.join(__dirname, 'server.js')];
|
||||||
|
if (!args.includes('--auth')) {
|
||||||
|
serverArgs.push('--anon');
|
||||||
|
}
|
||||||
|
// Execute synchronously to pass through stdio properly
|
||||||
|
require('child_process').execFileSync('node', serverArgs, {
|
||||||
stdio: 'inherit'
|
stdio: 'inherit'
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on('error', (err) => {
|
} else if (args.length === 0 || args[0] === '--anon' || args[0] === 'start') {
|
||||||
console.error('Failed to start server:', err);
|
// When called without arguments or with start/--anon (as MCP server), run directly
|
||||||
process.exit(1);
|
require(path.join(__dirname, 'server.js'));
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log('Hanzo MCP - Model Context Protocol Server');
|
console.log('Hanzo MCP - Model Context Protocol Server');
|
||||||
@@ -210,6 +218,8 @@ Add to your \`claude_desktop_config.json\`:
|
|||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
Note: By default, the server runs in anonymous mode. To use with authentication, add \`--auth\` flag.
|
||||||
|
|
||||||
### Claude Code
|
### Claude Code
|
||||||
|
|
||||||
For Claude Code, install the DXT extension or run:
|
For Claude Code, install the DXT extension or run:
|
||||||
|
|||||||
@@ -36,7 +36,15 @@ const mockVscode = {
|
|||||||
showInformationMessage: console.log,
|
showInformationMessage: console.log,
|
||||||
visibleTextEditors: [],
|
visibleTextEditors: [],
|
||||||
createWebviewPanel: () => null,
|
createWebviewPanel: () => null,
|
||||||
showTextDocument: () => null
|
showTextDocument: () => null,
|
||||||
|
createOutputChannel: (name) => ({
|
||||||
|
appendLine: (text) => console.log(\`[\${name}] \${text}\`),
|
||||||
|
append: (text) => process.stdout.write(text),
|
||||||
|
clear: () => {},
|
||||||
|
dispose: () => {},
|
||||||
|
hide: () => {},
|
||||||
|
show: () => {}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
openExternal: async (uri) => {
|
openExternal: async (uri) => {
|
||||||
@@ -125,14 +133,14 @@ async function build() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also build to dist folder for npm package
|
// Build the fixed version for npm package
|
||||||
await esbuild.build({
|
await esbuild.build({
|
||||||
entryPoints: ['src/mcp-server-standalone-auth.ts'],
|
entryPoints: ['src/mcp-server-fixed.ts'],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
platform: 'node',
|
platform: 'node',
|
||||||
target: 'node18',
|
target: 'node18',
|
||||||
outfile: 'dist/mcp-server.js',
|
outfile: 'dist/mcp-server.js',
|
||||||
external: [],
|
external: ['@modelcontextprotocol/sdk', 'zod'],
|
||||||
loader: { '.ts': 'ts' },
|
loader: { '.ts': 'ts' },
|
||||||
plugins: [{
|
plugins: [{
|
||||||
name: 'vscode-mock',
|
name: 'vscode-mock',
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
function testFunction() {
|
|
||||||
console.log('This is a test');
|
|
||||||
return 42;
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestClass {
|
|
||||||
constructor() {
|
|
||||||
this.value = 'test';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
*.ts
|
||||||
|
!*.d.ts
|
||||||
|
tsconfig.json
|
||||||
|
build.js
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// Background Service Worker for Browser Extension
|
||||||
|
import { BrowserControl } from './browser-control';
|
||||||
|
import { WebGPUAI } from './webgpu-ai';
|
||||||
|
|
||||||
|
// Initialize browser control
|
||||||
|
const browserControl = new BrowserControl();
|
||||||
|
const webgpuAI = new WebGPUAI();
|
||||||
|
|
||||||
|
// Initialize WebGPU on startup
|
||||||
|
chrome.runtime.onInstalled.addListener(async () => {
|
||||||
|
console.log('[Hanzo] Extension installed, initializing WebGPU...');
|
||||||
|
|
||||||
|
const gpuAvailable = await webgpuAI.initialize();
|
||||||
|
if (gpuAvailable) {
|
||||||
|
console.log('[Hanzo] WebGPU available, loading local models...');
|
||||||
|
|
||||||
|
// Load small local model for browser control
|
||||||
|
try {
|
||||||
|
await webgpuAI.loadModel({
|
||||||
|
name: 'hanzo-browser-control',
|
||||||
|
url: chrome.runtime.getURL('models/browser-control-4bit.bin'),
|
||||||
|
quantization: '4bit',
|
||||||
|
maxTokens: 512
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Hanzo] Failed to load local model:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle messages from content scripts
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
handleMessage(request, sender, sendResponse);
|
||||||
|
return true; // Keep channel open for async response
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleMessage(request: any, sender: chrome.runtime.MessageSender, sendResponse: Function) {
|
||||||
|
switch (request.action) {
|
||||||
|
case 'runLocalAI':
|
||||||
|
try {
|
||||||
|
const result = await webgpuAI.runInference(
|
||||||
|
request.model || 'hanzo-browser-control',
|
||||||
|
request.prompt
|
||||||
|
);
|
||||||
|
sendResponse({ success: true, result });
|
||||||
|
} catch (error) {
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'launchAIWorker':
|
||||||
|
if (sender.tab?.id) {
|
||||||
|
await browserControl.launchAIWorker(sender.tab.id, request.model);
|
||||||
|
sendResponse({ success: true });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'readTabFS':
|
||||||
|
try {
|
||||||
|
const content = await browserControl.readTab(request.path);
|
||||||
|
sendResponse({ success: true, content });
|
||||||
|
} catch (error) {
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'writeTabFS':
|
||||||
|
try {
|
||||||
|
await browserControl.writeTab(request.path, request.content);
|
||||||
|
sendResponse({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'listTabFS':
|
||||||
|
const tabs = await browserControl.listTabs();
|
||||||
|
sendResponse({ success: true, tabs });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to WebSocket for MCP communication
|
||||||
|
let ws: WebSocket | null = null;
|
||||||
|
|
||||||
|
function connectToMCP() {
|
||||||
|
ws = new WebSocket('ws://localhost:3001/browser-extension');
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('[Hanzo] Connected to MCP server');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
handleMCPMessage(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('[Hanzo] WebSocket error:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('[Hanzo] Disconnected from MCP server, reconnecting...');
|
||||||
|
setTimeout(connectToMCP, 5000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMCPMessage(data: any) {
|
||||||
|
switch (data.type) {
|
||||||
|
case 'browserControl':
|
||||||
|
// Execute browser control commands from MCP
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||||
|
if (tabs[0]?.id) {
|
||||||
|
browserControl.launchAIWorker(tabs[0].id, data.model);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect on startup
|
||||||
|
connectToMCP();
|
||||||
|
|
||||||
|
// Export for testing
|
||||||
|
export { browserControl, webgpuAI };
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
// Browser Control API for AI agents
|
||||||
|
// Enables AI to control browser tabs and implement FUSE-like filesystem
|
||||||
|
|
||||||
|
interface TabFileSystem {
|
||||||
|
path: string;
|
||||||
|
tabId: number;
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BrowserControl {
|
||||||
|
private tabFS: Map<string, TabFileSystem> = new Map();
|
||||||
|
private aiWorkers: Map<number, Worker> = new Map();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initializeTabFileSystem();
|
||||||
|
this.setupMessageHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeTabFileSystem() {
|
||||||
|
// Mount tabs as filesystem paths
|
||||||
|
if (typeof chrome !== 'undefined' && chrome.tabs) {
|
||||||
|
chrome.tabs.query({}, (tabs) => {
|
||||||
|
tabs.forEach((tab, index) => {
|
||||||
|
if (tab.id && tab.url) {
|
||||||
|
const path = `/tabs/${index}/${this.sanitizePath(tab.title || 'untitled')}`;
|
||||||
|
this.tabFS.set(path, {
|
||||||
|
path,
|
||||||
|
tabId: tab.id,
|
||||||
|
url: tab.url,
|
||||||
|
title: tab.title || 'Untitled'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for tab updates
|
||||||
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||||
|
if (changeInfo.status === 'complete') {
|
||||||
|
this.updateTabFS(tab);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizePath(title: string): string {
|
||||||
|
return title.replace(/[^a-zA-Z0-9-_]/g, '_').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTabFS(tab: chrome.tabs.Tab) {
|
||||||
|
if (!tab.id || !tab.url) return;
|
||||||
|
|
||||||
|
const existingEntry = Array.from(this.tabFS.values()).find(
|
||||||
|
entry => entry.tabId === tab.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const path = existingEntry?.path ||
|
||||||
|
`/tabs/${this.tabFS.size}/${this.sanitizePath(tab.title || 'untitled')}`;
|
||||||
|
|
||||||
|
this.tabFS.set(path, {
|
||||||
|
path,
|
||||||
|
tabId: tab.id,
|
||||||
|
url: tab.url,
|
||||||
|
title: tab.title || 'Untitled'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// FUSE-like filesystem operations
|
||||||
|
async readTab(path: string): Promise<string> {
|
||||||
|
const entry = this.tabFS.get(path);
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error(`Tab not found: ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.tabs.sendMessage(entry.tabId, {
|
||||||
|
action: 'getContent'
|
||||||
|
}, (response) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(chrome.runtime.lastError);
|
||||||
|
} else {
|
||||||
|
resolve(response?.content || '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeTab(path: string, content: string): Promise<void> {
|
||||||
|
const entry = this.tabFS.get(path);
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error(`Tab not found: ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.tabs.sendMessage(entry.tabId, {
|
||||||
|
action: 'setContent',
|
||||||
|
content
|
||||||
|
}, (response) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(chrome.runtime.lastError);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTabs(): Promise<string[]> {
|
||||||
|
return Array.from(this.tabFS.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI Control APIs
|
||||||
|
async launchAIWorker(tabId: number, modelName: string): Promise<void> {
|
||||||
|
// Create a dedicated worker for this tab
|
||||||
|
const worker = new Worker(new URL('./ai-worker.js', import.meta.url), {
|
||||||
|
type: 'module'
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.postMessage({
|
||||||
|
type: 'init',
|
||||||
|
tabId,
|
||||||
|
modelName
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.onmessage = (event) => {
|
||||||
|
this.handleAIWorkerMessage(tabId, event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.aiWorkers.set(tabId, worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleAIWorkerMessage(tabId: number, data: any) {
|
||||||
|
switch (data.type) {
|
||||||
|
case 'click':
|
||||||
|
this.performClick(tabId, data.selector);
|
||||||
|
break;
|
||||||
|
case 'type':
|
||||||
|
this.performType(tabId, data.selector, data.text);
|
||||||
|
break;
|
||||||
|
case 'navigate':
|
||||||
|
this.navigateTo(tabId, data.url);
|
||||||
|
break;
|
||||||
|
case 'screenshot':
|
||||||
|
this.captureScreenshot(tabId).then(screenshot => {
|
||||||
|
const worker = this.aiWorkers.get(tabId);
|
||||||
|
worker?.postMessage({
|
||||||
|
type: 'screenshot',
|
||||||
|
data: screenshot
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async performClick(tabId: number, selector: string) {
|
||||||
|
chrome.tabs.sendMessage(tabId, {
|
||||||
|
action: 'click',
|
||||||
|
selector
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async performType(tabId: number, selector: string, text: string) {
|
||||||
|
chrome.tabs.sendMessage(tabId, {
|
||||||
|
action: 'type',
|
||||||
|
selector,
|
||||||
|
text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async navigateTo(tabId: number, url: string) {
|
||||||
|
chrome.tabs.update(tabId, { url });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async captureScreenshot(tabId: number): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.tabs.captureVisibleTab(null, {
|
||||||
|
format: 'png',
|
||||||
|
quality: 100
|
||||||
|
}, (dataUrl) => {
|
||||||
|
resolve(dataUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-tab communication for parallel AI execution
|
||||||
|
async broadcastToAI(message: any) {
|
||||||
|
this.aiWorkers.forEach((worker, tabId) => {
|
||||||
|
worker.postMessage({
|
||||||
|
type: 'broadcast',
|
||||||
|
message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup message handlers for content script
|
||||||
|
private setupMessageHandlers() {
|
||||||
|
if (typeof chrome !== 'undefined' && chrome.runtime) {
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
if (request.from === 'content' && sender.tab?.id) {
|
||||||
|
this.handleContentMessage(sender.tab.id, request, sendResponse);
|
||||||
|
return true; // Keep channel open for async response
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleContentMessage(
|
||||||
|
tabId: number,
|
||||||
|
request: any,
|
||||||
|
sendResponse: (response: any) => void
|
||||||
|
) {
|
||||||
|
const worker = this.aiWorkers.get(tabId);
|
||||||
|
if (worker) {
|
||||||
|
worker.postMessage({
|
||||||
|
type: 'contentMessage',
|
||||||
|
data: request
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up one-time listener for response
|
||||||
|
const responseHandler = (event: MessageEvent) => {
|
||||||
|
if (event.data.type === 'contentResponse') {
|
||||||
|
sendResponse(event.data.response);
|
||||||
|
worker.removeEventListener('message', responseHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.addEventListener('message', responseHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,11 @@ const { execSync } = require('child_process');
|
|||||||
async function build() {
|
async function build() {
|
||||||
console.log('Building browser extension...');
|
console.log('Building browser extension...');
|
||||||
|
|
||||||
// Ensure dist directory exists
|
// Ensure dist directories exist
|
||||||
fs.mkdirSync('dist/browser-extension', { recursive: true });
|
fs.mkdirSync('dist/browser-extension', { recursive: true });
|
||||||
|
fs.mkdirSync('dist/browser-extension/chrome', { recursive: true });
|
||||||
|
fs.mkdirSync('dist/browser-extension/firefox', { recursive: true });
|
||||||
|
fs.mkdirSync('dist/browser-extension/safari', { recursive: true });
|
||||||
|
|
||||||
// Build content script
|
// Build content script
|
||||||
await esbuild.build({
|
await esbuild.build({
|
||||||
@@ -17,10 +20,41 @@ async function build() {
|
|||||||
bundle: true,
|
bundle: true,
|
||||||
outfile: 'dist/browser-extension/content-script.js',
|
outfile: 'dist/browser-extension/content-script.js',
|
||||||
platform: 'browser',
|
platform: 'browser',
|
||||||
target: 'chrome90',
|
target: ['chrome90', 'firefox91', 'safari14'],
|
||||||
sourcemap: 'inline'
|
sourcemap: 'inline'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Build background script with WebGPU support
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: ['src/browser-extension/background.ts'],
|
||||||
|
bundle: true,
|
||||||
|
outfile: 'dist/browser-extension/background.js',
|
||||||
|
platform: 'browser',
|
||||||
|
target: ['chrome90', 'firefox91', 'safari14'],
|
||||||
|
format: 'esm',
|
||||||
|
external: ['chrome', 'browser']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build WebGPU AI module
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: ['src/browser-extension/webgpu-ai.ts'],
|
||||||
|
bundle: true,
|
||||||
|
outfile: 'dist/browser-extension/webgpu-ai.js',
|
||||||
|
platform: 'browser',
|
||||||
|
target: 'es2020',
|
||||||
|
format: 'esm'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build browser control module
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: ['src/browser-extension/browser-control.ts'],
|
||||||
|
bundle: true,
|
||||||
|
outfile: 'dist/browser-extension/browser-control.js',
|
||||||
|
platform: 'browser',
|
||||||
|
target: 'es2020',
|
||||||
|
format: 'esm'
|
||||||
|
});
|
||||||
|
|
||||||
// Build CLI and server (for npm package)
|
// Build CLI and server (for npm package)
|
||||||
await esbuild.build({
|
await esbuild.build({
|
||||||
entryPoints: ['src/browser-extension/cli.ts'],
|
entryPoints: ['src/browser-extension/cli.ts'],
|
||||||
@@ -43,12 +77,59 @@ async function build() {
|
|||||||
console.log('Building TypeScript declarations...');
|
console.log('Building TypeScript declarations...');
|
||||||
execSync('cd src/browser-extension && npx tsc', { stdio: 'inherit' });
|
execSync('cd src/browser-extension && npx tsc', { stdio: 'inherit' });
|
||||||
|
|
||||||
// Copy manifest
|
// Copy manifests for different browsers
|
||||||
fs.copyFileSync(
|
fs.copyFileSync(
|
||||||
'src/browser-extension/manifest.json',
|
'src/browser-extension/manifest.json',
|
||||||
'dist/browser-extension/manifest.json'
|
'dist/browser-extension/manifest.json'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Chrome version
|
||||||
|
fs.copyFileSync(
|
||||||
|
'src/browser-extension/manifest.json',
|
||||||
|
'dist/browser-extension/chrome/manifest.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Firefox version
|
||||||
|
fs.copyFileSync(
|
||||||
|
'src/browser-extension/manifest-firefox.json',
|
||||||
|
'dist/browser-extension/firefox/manifest.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Safari needs special handling - create stub
|
||||||
|
fs.writeFileSync('dist/browser-extension/safari/Info.plist', `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Hanzo AI Dev Assistant</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>ai.hanzo.browser-extension</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0.0</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0.0</string>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.Safari.web-extension</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>SafariWebExtensionHandler</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>`);
|
||||||
|
|
||||||
|
// Copy common files to each browser directory
|
||||||
|
['chrome', 'firefox', 'safari'].forEach(browser => {
|
||||||
|
fs.copyFileSync(
|
||||||
|
'dist/browser-extension/content-script.js',
|
||||||
|
`dist/browser-extension/${browser}/content-script.js`
|
||||||
|
);
|
||||||
|
fs.copyFileSync(
|
||||||
|
'dist/browser-extension/background.js',
|
||||||
|
`dist/browser-extension/${browser}/background.js`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Copy package.json for npm
|
// Copy package.json for npm
|
||||||
const pkg = JSON.parse(fs.readFileSync('src/browser-extension/package.json', 'utf8'));
|
const pkg = JSON.parse(fs.readFileSync('src/browser-extension/package.json', 'utf8'));
|
||||||
pkg.main = 'cli.js';
|
pkg.main = 'cli.js';
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 2,
|
||||||
|
"name": "Hanzo AI Dev Assistant",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Click-to-code navigation with WebGPU AI and browser control",
|
||||||
|
"permissions": [
|
||||||
|
"activeTab",
|
||||||
|
"storage",
|
||||||
|
"tabs",
|
||||||
|
"webNavigation",
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["content-script.js"],
|
||||||
|
"run_at": "document_idle"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"background": {
|
||||||
|
"scripts": ["background.js"],
|
||||||
|
"persistent": false
|
||||||
|
},
|
||||||
|
"browser_action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icon16.png",
|
||||||
|
"48": "icon48.png",
|
||||||
|
"128": "icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
"ai-worker.js",
|
||||||
|
"models/*"
|
||||||
|
],
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "hanzo-ai@hanzo.ai",
|
||||||
|
"strict_min_version": "91.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,18 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Hanzo AI Dev Assistant",
|
"name": "Hanzo AI Dev Assistant",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Click-to-code navigation with source-map support",
|
"description": "Click-to-code navigation with WebGPU AI and browser control",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"storage"
|
"storage",
|
||||||
|
"tabs",
|
||||||
|
"webNavigation",
|
||||||
|
"scripting"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"http://localhost/*",
|
"http://localhost/*",
|
||||||
"https://localhost/*"
|
"https://localhost/*",
|
||||||
|
"<all_urls>"
|
||||||
],
|
],
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
@@ -18,6 +22,10 @@
|
|||||||
"run_at": "document_idle"
|
"run_at": "document_idle"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js",
|
||||||
|
"type": "module"
|
||||||
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"default_popup": "popup.html",
|
"default_popup": "popup.html",
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
@@ -25,5 +33,11 @@
|
|||||||
"48": "icon48.png",
|
"48": "icon48.png",
|
||||||
"128": "icon128.png"
|
"128": "icon128.png"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["ai-worker.js", "models/*"],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "@hanzoai/browser-devtools",
|
"name": "@hanzo/browser-extension",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Browser extension for click-to-code navigation with MCP integration",
|
"description": "Browser extension for click-to-code navigation with MCP integration",
|
||||||
"main": "dist/index.js",
|
"main": "cli.js",
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"hanzo-browser-server": "./dist/cli.js"
|
"hanzo-browser-server": "./cli.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && node build.js",
|
"start": "node cli.js"
|
||||||
"prepublishOnly": "npm run build",
|
|
||||||
"start": "node dist/cli.js"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/",
|
"cli.js",
|
||||||
"extension/"
|
"browser-extension-server.js",
|
||||||
|
"content-script.js",
|
||||||
|
"manifest.json",
|
||||||
|
"README.md"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"mcp",
|
"mcp",
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
/* Hanzo AI Browser Extension Popup Styles */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #FF6B6B;
|
||||||
|
--primary-dark: #E55555;
|
||||||
|
--bg: #0A0A0A;
|
||||||
|
--surface: #1A1A1A;
|
||||||
|
--surface-light: #2A2A2A;
|
||||||
|
--text: #FFFFFF;
|
||||||
|
--text-dim: #888888;
|
||||||
|
--border: #333333;
|
||||||
|
--success: #4CAF50;
|
||||||
|
--warning: #FFC107;
|
||||||
|
--error: #F44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 380px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: var(--surface);
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
.section {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--surface-light);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #3A3A3A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
background: var(--surface-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Info */
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--surface-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info > div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Features */
|
||||||
|
.features {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-toggle label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--surface-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-toggle label:hover {
|
||||||
|
background: #3A3A3A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-toggle input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
position: relative;
|
||||||
|
width: 44px;
|
||||||
|
height: 24px;
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .toggle {
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .toggle::after {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-info small {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status */
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.connected {
|
||||||
|
background: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.warning {
|
||||||
|
background: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.error {
|
||||||
|
background: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-detail {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Guide */
|
||||||
|
.guide {
|
||||||
|
background: var(--surface-light);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide h3 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
.settings-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group input[type="text"],
|
||||||
|
.setting-group input[type="number"],
|
||||||
|
.setting-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utilities */
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border);
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text a {
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.connecting {
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Hanzo AI Dev Assistant</title>
|
||||||
|
<link rel="stylesheet" href="popup.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<img src="icon48.png" alt="Hanzo AI" class="logo">
|
||||||
|
<h1>Hanzo AI</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Login Section -->
|
||||||
|
<div id="login-section" class="section">
|
||||||
|
<h2>Sign In</h2>
|
||||||
|
<p class="subtitle">Connect to your Hanzo AI account</p>
|
||||||
|
<button id="login-btn" class="btn btn-primary">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3"/>
|
||||||
|
</svg>
|
||||||
|
Sign in with Hanzo IAM
|
||||||
|
</button>
|
||||||
|
<p class="help-text">
|
||||||
|
<a href="https://iam.hanzo.ai/register" target="_blank">Create account</a> •
|
||||||
|
<a href="https://iam.hanzo.ai/forgot" target="_blank">Forgot password?</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Section (shown after login) -->
|
||||||
|
<div id="main-section" class="section hidden">
|
||||||
|
<div class="user-info">
|
||||||
|
<img id="user-avatar" class="avatar" src="" alt="">
|
||||||
|
<div>
|
||||||
|
<div id="user-name" class="user-name"></div>
|
||||||
|
<div id="user-email" class="user-email"></div>
|
||||||
|
</div>
|
||||||
|
<button id="logout-btn" class="btn-icon" title="Sign out">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- Features -->
|
||||||
|
<div class="features">
|
||||||
|
<div class="feature-toggle">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="source-maps" checked>
|
||||||
|
<span class="toggle"></span>
|
||||||
|
<div class="feature-info">
|
||||||
|
<strong>Source Maps</strong>
|
||||||
|
<small>Click-to-code navigation</small>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-toggle">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="webgpu-ai" checked>
|
||||||
|
<span class="toggle"></span>
|
||||||
|
<div class="feature-info">
|
||||||
|
<strong>WebGPU AI</strong>
|
||||||
|
<small>Local AI inference</small>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-toggle">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="browser-control">
|
||||||
|
<span class="toggle"></span>
|
||||||
|
<div class="feature-info">
|
||||||
|
<strong>Browser Control</strong>
|
||||||
|
<small>AI browser automation</small>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-toggle">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="tab-filesystem">
|
||||||
|
<span class="toggle"></span>
|
||||||
|
<div class="feature-info">
|
||||||
|
<strong>Tab Filesystem</strong>
|
||||||
|
<small>Mount tabs as files</small>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- Connection Status -->
|
||||||
|
<div class="status">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-dot" id="mcp-status"></span>
|
||||||
|
<span>MCP Server</span>
|
||||||
|
<span id="mcp-port" class="status-detail">Port 3001</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-dot" id="gpu-status"></span>
|
||||||
|
<span>WebGPU</span>
|
||||||
|
<span id="gpu-detail" class="status-detail">Checking...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="actions">
|
||||||
|
<button id="test-connection" class="btn btn-secondary">Test Connection</button>
|
||||||
|
<button id="open-settings" class="btn btn-secondary">Settings</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Guide -->
|
||||||
|
<div class="guide">
|
||||||
|
<h3>Quick Guide</h3>
|
||||||
|
<p><kbd>Alt</kbd> + <kbd>Click</kbd> any element to navigate to source</p>
|
||||||
|
<p><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>H</kbd> to open Hanzo AI panel</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Section -->
|
||||||
|
<div id="settings-section" class="section hidden">
|
||||||
|
<div class="settings-header">
|
||||||
|
<button id="back-btn" class="btn-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<h2>Settings</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-content">
|
||||||
|
<div class="setting-group">
|
||||||
|
<h3>Connection</h3>
|
||||||
|
<label>
|
||||||
|
MCP Server URL
|
||||||
|
<input type="text" id="mcp-url" value="ws://localhost:3001/browser-extension">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<h3>AI Models</h3>
|
||||||
|
<label>
|
||||||
|
Local Model Path
|
||||||
|
<input type="text" id="model-path" placeholder="/models/browser-control-4bit.bin">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Model Quantization
|
||||||
|
<select id="model-quant">
|
||||||
|
<option value="4bit">4-bit (Fastest)</option>
|
||||||
|
<option value="8bit">8-bit (Balanced)</option>
|
||||||
|
<option value="fp16">FP16 (Best Quality)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<h3>Browser Control</h3>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="safe-mode" checked>
|
||||||
|
Safe Mode (require confirmation for actions)
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Max Workers
|
||||||
|
<input type="number" id="max-workers" value="3" min="1" max="10">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="save-settings" class="btn btn-primary">Save Settings</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
// Server-side Framework Support for Browser Extension
|
||||||
|
// Detects and extracts source information from Rails, Django, Flask, etc.
|
||||||
|
|
||||||
|
interface ServerFrameworkInfo {
|
||||||
|
framework: string;
|
||||||
|
file?: string;
|
||||||
|
line?: number;
|
||||||
|
controller?: string;
|
||||||
|
action?: string;
|
||||||
|
template?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServerFrameworkDetector {
|
||||||
|
|
||||||
|
// Rails detection and extraction
|
||||||
|
detectRails(element: HTMLElement): ServerFrameworkInfo | null {
|
||||||
|
// Rails adds data attributes in development
|
||||||
|
const railsData = {
|
||||||
|
controller: element.getAttribute('data-controller'),
|
||||||
|
action: element.getAttribute('data-action'),
|
||||||
|
turboFrame: element.getAttribute('data-turbo-frame'),
|
||||||
|
// Rails 7+ with Stimulus
|
||||||
|
stimulusController: element.getAttribute('data-controller'),
|
||||||
|
// ViewComponent data
|
||||||
|
viewComponent: element.getAttribute('data-view-component'),
|
||||||
|
viewComponentPath: element.getAttribute('data-view-component-path')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for Rails-specific meta tags
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]');
|
||||||
|
const railsEnv = document.querySelector('meta[name="rails-env"]');
|
||||||
|
|
||||||
|
if (csrfToken || railsEnv || Object.values(railsData).some(v => v)) {
|
||||||
|
// Look for Rails stack trace comments in development
|
||||||
|
const comments = this.extractHTMLComments(element);
|
||||||
|
const templateMatch = comments.find(c => c.includes('BEGIN') && c.includes('app/views'));
|
||||||
|
|
||||||
|
if (templateMatch) {
|
||||||
|
// Extract template path from comment
|
||||||
|
// <!-- BEGIN app/views/posts/show.html.erb -->
|
||||||
|
const match = templateMatch.match(/BEGIN\s+(app\/views\/[^\s]+)/);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
framework: 'rails',
|
||||||
|
template: match[1],
|
||||||
|
controller: railsData.controller || this.extractRailsController(match[1]),
|
||||||
|
action: railsData.action || this.extractRailsAction(match[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to data attributes
|
||||||
|
if (railsData.viewComponentPath) {
|
||||||
|
return {
|
||||||
|
framework: 'rails-viewcomponent',
|
||||||
|
file: railsData.viewComponentPath,
|
||||||
|
controller: railsData.viewComponent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
framework: 'rails',
|
||||||
|
controller: railsData.controller || railsData.stimulusController,
|
||||||
|
action: railsData.action
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Django detection and extraction
|
||||||
|
detectDjango(element: HTMLElement): ServerFrameworkInfo | null {
|
||||||
|
// Django Debug Toolbar adds data
|
||||||
|
const djangoDebug = document.getElementById('djDebug');
|
||||||
|
|
||||||
|
// Django templates in debug mode often have comments
|
||||||
|
const comments = this.extractHTMLComments(element);
|
||||||
|
const djangoTemplate = comments.find(c =>
|
||||||
|
c.includes('TEMPLATE_DEBUG') ||
|
||||||
|
c.includes('templates/') ||
|
||||||
|
c.includes('{% block')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Django CMS specific
|
||||||
|
const djangoCMS = element.getAttribute('data-cms-plugin');
|
||||||
|
|
||||||
|
// Django REST Framework
|
||||||
|
const djangoREST = document.querySelector('meta[name="generator"][content*="Django REST"]');
|
||||||
|
|
||||||
|
if (djangoDebug || djangoTemplate || djangoCMS || djangoREST) {
|
||||||
|
// Extract template info from comments
|
||||||
|
// <!-- BEGIN: templates/blog/post_detail.html -->
|
||||||
|
const templateMatch = djangoTemplate?.match(/templates\/([^\s]+)/);
|
||||||
|
|
||||||
|
// Extract view info from Django debug
|
||||||
|
if (djangoDebug) {
|
||||||
|
const viewInfo = this.extractDjangoViewInfo();
|
||||||
|
return {
|
||||||
|
framework: 'django',
|
||||||
|
file: viewInfo.file,
|
||||||
|
line: viewInfo.line,
|
||||||
|
template: templateMatch?.[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
framework: 'django',
|
||||||
|
template: templateMatch?.[1],
|
||||||
|
controller: djangoCMS
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flask detection and extraction
|
||||||
|
detectFlask(element: HTMLElement): ServerFrameworkInfo | null {
|
||||||
|
// Flask debug mode adds specific classes
|
||||||
|
const flaskDebugger = document.querySelector('.flask-debugger');
|
||||||
|
const werkzeugDebugger = document.querySelector('#werkzeug-debugger');
|
||||||
|
|
||||||
|
// Flask often uses Jinja2 templates
|
||||||
|
const comments = this.extractHTMLComments(element);
|
||||||
|
const jinjaTemplate = comments.find(c =>
|
||||||
|
c.includes('{%') ||
|
||||||
|
c.includes('{{') ||
|
||||||
|
c.includes('templates/')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Flask-Admin specific
|
||||||
|
const flaskAdmin = document.querySelector('meta[name="generator"][content*="Flask-Admin"]');
|
||||||
|
|
||||||
|
if (flaskDebugger || werkzeugDebugger || jinjaTemplate || flaskAdmin) {
|
||||||
|
// Extract template from comments
|
||||||
|
// <!-- Template: templates/user/profile.html -->
|
||||||
|
const templateMatch = jinjaTemplate?.match(/[Tt]emplate:\s*([^\s]+)/);
|
||||||
|
|
||||||
|
// Extract route from Flask debugger
|
||||||
|
const routeInfo = this.extractFlaskRouteInfo(element);
|
||||||
|
|
||||||
|
return {
|
||||||
|
framework: 'flask',
|
||||||
|
template: templateMatch?.[1],
|
||||||
|
controller: routeInfo.blueprint,
|
||||||
|
action: routeInfo.endpoint,
|
||||||
|
file: routeInfo.file,
|
||||||
|
line: routeInfo.line
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Laravel detection
|
||||||
|
detectLaravel(element: HTMLElement): ServerFrameworkInfo | null {
|
||||||
|
// Laravel adds specific meta tags and comments
|
||||||
|
const laravelToken = document.querySelector('meta[name="csrf-token"]');
|
||||||
|
const laravelDebugbar = document.querySelector('.phpdebugbar');
|
||||||
|
|
||||||
|
// Blade template comments
|
||||||
|
const comments = this.extractHTMLComments(element);
|
||||||
|
const bladeTemplate = comments.find(c =>
|
||||||
|
c.includes('resources/views/') ||
|
||||||
|
c.includes('@extends') ||
|
||||||
|
c.includes('@section')
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((laravelToken && bladeTemplate) || laravelDebugbar) {
|
||||||
|
// Extract Blade template path
|
||||||
|
const templateMatch = bladeTemplate?.match(/resources\/views\/([^\s]+)/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
framework: 'laravel',
|
||||||
|
template: templateMatch?.[1]?.replace(/\.blade\.php$/, '').replace(/\./g, '/') + '.blade.php',
|
||||||
|
controller: element.getAttribute('data-controller'),
|
||||||
|
action: element.getAttribute('data-action')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phoenix/Elixir detection
|
||||||
|
detectPhoenix(element: HTMLElement): ServerFrameworkInfo | null {
|
||||||
|
// Phoenix LiveView specific
|
||||||
|
const phxMain = document.querySelector('[data-phx-main]');
|
||||||
|
const phxSession = element.getAttribute('data-phx-session');
|
||||||
|
const phxStatic = element.getAttribute('data-phx-static');
|
||||||
|
|
||||||
|
if (phxMain || phxSession || phxStatic) {
|
||||||
|
// Phoenix adds source info in dev
|
||||||
|
const phxView = element.getAttribute('data-phx-view');
|
||||||
|
const phxComponent = element.closest('[data-phx-component]')?.getAttribute('data-phx-component');
|
||||||
|
|
||||||
|
return {
|
||||||
|
framework: 'phoenix',
|
||||||
|
controller: phxView || phxComponent,
|
||||||
|
template: element.getAttribute('data-phx-template')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASP.NET Core detection
|
||||||
|
detectASPNET(element: HTMLElement): ServerFrameworkInfo | null {
|
||||||
|
// ASP.NET Core tag helpers
|
||||||
|
const aspAction = element.getAttribute('asp-action');
|
||||||
|
const aspController = element.getAttribute('asp-controller');
|
||||||
|
const aspPage = element.getAttribute('asp-page');
|
||||||
|
|
||||||
|
// Blazor components
|
||||||
|
const blazorComponent = element.getAttribute('b-component');
|
||||||
|
|
||||||
|
if (aspAction || aspController || aspPage || blazorComponent) {
|
||||||
|
return {
|
||||||
|
framework: blazorComponent ? 'blazor' : 'aspnet',
|
||||||
|
controller: aspController || blazorComponent,
|
||||||
|
action: aspAction,
|
||||||
|
template: aspPage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Express.js detection (with template engines)
|
||||||
|
detectExpress(element: HTMLElement): ServerFrameworkInfo | null {
|
||||||
|
// Common template engine markers
|
||||||
|
const comments = this.extractHTMLComments(element);
|
||||||
|
|
||||||
|
// Pug/Jade templates
|
||||||
|
const pugTemplate = comments.find(c => c.includes('.pug') || c.includes('.jade'));
|
||||||
|
|
||||||
|
// EJS templates
|
||||||
|
const ejsTemplate = comments.find(c => c.includes('.ejs') || c.includes('<%'));
|
||||||
|
|
||||||
|
// Handlebars
|
||||||
|
const hbsTemplate = comments.find(c => c.includes('.hbs') || c.includes('{{'));
|
||||||
|
|
||||||
|
if (pugTemplate || ejsTemplate || hbsTemplate) {
|
||||||
|
const templateMatch = (pugTemplate || ejsTemplate || hbsTemplate)?.match(/views\/([^\s]+)/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
framework: 'express',
|
||||||
|
template: templateMatch?.[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WordPress detection
|
||||||
|
detectWordPress(element: HTMLElement): ServerFrameworkInfo | null {
|
||||||
|
// WordPress adds specific classes and meta tags
|
||||||
|
const wpHead = document.querySelector('meta[name="generator"][content*="WordPress"]');
|
||||||
|
const wpAdmin = document.getElementById('wpadminbar');
|
||||||
|
|
||||||
|
// WordPress debug comments
|
||||||
|
const comments = this.extractHTMLComments(element);
|
||||||
|
const wpTemplate = comments.find(c =>
|
||||||
|
c.includes('Template:') ||
|
||||||
|
c.includes('wp-content/themes/')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (wpHead || wpAdmin || wpTemplate) {
|
||||||
|
const templateMatch = wpTemplate?.match(/Template:\s*([^\s]+)/);
|
||||||
|
const themeMatch = wpTemplate?.match(/themes\/([^\/]+)\/(.+)/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
framework: 'wordpress',
|
||||||
|
template: templateMatch?.[1] || themeMatch?.[2],
|
||||||
|
controller: themeMatch?.[1] // theme name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
private extractHTMLComments(element: HTMLElement): string[] {
|
||||||
|
const comments: string[] = [];
|
||||||
|
|
||||||
|
// Simple fallback for test environments - look for comment-like patterns in innerHTML
|
||||||
|
const htmlContent = element.innerHTML + (element.parentElement?.innerHTML || '');
|
||||||
|
const commentMatches = htmlContent.match(/<!--([^-]|[-][^-])*-->/g);
|
||||||
|
if (commentMatches) {
|
||||||
|
commentMatches.forEach(match => {
|
||||||
|
// Extract content between <!-- and -->
|
||||||
|
const content = match.replace(/<!--\s*/, '').replace(/\s*-->/, '');
|
||||||
|
comments.push(content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try using TreeWalker if available (browser environment)
|
||||||
|
if (typeof document.createTreeWalker === 'function' && typeof NodeFilter !== 'undefined') {
|
||||||
|
try {
|
||||||
|
const walker = document.createTreeWalker(
|
||||||
|
element,
|
||||||
|
NodeFilter.SHOW_COMMENT,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
let node;
|
||||||
|
while (node = walker.nextNode()) {
|
||||||
|
const content = node.textContent || '';
|
||||||
|
if (!comments.includes(content)) {
|
||||||
|
comments.push(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check parents up to body
|
||||||
|
let parent = element.parentElement;
|
||||||
|
while (parent && parent !== document.body) {
|
||||||
|
const parentWalker = document.createTreeWalker(
|
||||||
|
parent,
|
||||||
|
NodeFilter.SHOW_COMMENT,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
while (node = parentWalker.nextNode()) {
|
||||||
|
const content = node.textContent || '';
|
||||||
|
if (!comments.includes(content)) {
|
||||||
|
comments.push(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// TreeWalker failed, rely on regex fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractRailsController(templatePath: string): string {
|
||||||
|
// app/views/posts/show.html.erb -> PostsController
|
||||||
|
// app/views/admin/users/index.html.erb -> AdminUsersController
|
||||||
|
const match = templatePath.match(/views\/(.+)\//);
|
||||||
|
if (match) {
|
||||||
|
const parts = match[1].split('/');
|
||||||
|
return parts.map(part =>
|
||||||
|
part.split('_').map(word =>
|
||||||
|
word.charAt(0).toUpperCase() + word.slice(1)
|
||||||
|
).join('')
|
||||||
|
).join('') + 'Controller';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractRailsAction(templatePath: string): string {
|
||||||
|
// app/views/posts/show.html.erb -> show
|
||||||
|
const match = templatePath.match(/\/([^\/]+)\.html\./);
|
||||||
|
return match?.[1] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractDjangoViewInfo(): { file?: string, line?: number } {
|
||||||
|
// Try to extract from Django Debug Toolbar
|
||||||
|
const toolbar = document.getElementById('djDebugToolbar');
|
||||||
|
if (toolbar) {
|
||||||
|
// Look for view info in toolbar panels
|
||||||
|
const viewPanel = toolbar.querySelector('.djDebugPanelContent .view-info');
|
||||||
|
if (viewPanel) {
|
||||||
|
const text = viewPanel.textContent || '';
|
||||||
|
const fileMatch = text.match(/File:\s*([^\s]+)/);
|
||||||
|
const lineMatch = text.match(/Line:\s*(\d+)/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
file: fileMatch?.[1],
|
||||||
|
line: lineMatch ? parseInt(lineMatch[1]) : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractFlaskRouteInfo(element: HTMLElement): {
|
||||||
|
blueprint?: string,
|
||||||
|
endpoint?: string,
|
||||||
|
file?: string,
|
||||||
|
line?: number
|
||||||
|
} {
|
||||||
|
// Try to extract from Werkzeug debugger
|
||||||
|
const debuggerEl = document.getElementById('werkzeug-debugger');
|
||||||
|
if (debuggerEl) {
|
||||||
|
const traceback = debuggerEl.querySelector('.traceback');
|
||||||
|
if (traceback) {
|
||||||
|
const text = traceback.textContent || '';
|
||||||
|
|
||||||
|
// Extract endpoint from traceback
|
||||||
|
const endpointMatch = text.match(/endpoint:\s*'([^']+)'/);
|
||||||
|
const fileMatch = text.match(/File\s+"([^"]+)",\s+line\s+(\d+)/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
endpoint: endpointMatch?.[1],
|
||||||
|
file: fileMatch?.[1],
|
||||||
|
line: fileMatch ? parseInt(fileMatch[2]) : undefined,
|
||||||
|
blueprint: endpointMatch?.[1]?.split('.')[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main detection method
|
||||||
|
detect(element: HTMLElement): ServerFrameworkInfo | null {
|
||||||
|
// Try each framework detector
|
||||||
|
const detectors = [
|
||||||
|
() => this.detectRails(element),
|
||||||
|
() => this.detectDjango(element),
|
||||||
|
() => this.detectFlask(element),
|
||||||
|
() => this.detectLaravel(element),
|
||||||
|
() => this.detectPhoenix(element),
|
||||||
|
() => this.detectASPNET(element),
|
||||||
|
() => this.detectExpress(element),
|
||||||
|
() => this.detectWordPress(element)
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const detector of detectors) {
|
||||||
|
const result = detector();
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,522 @@
|
|||||||
|
/* Hanzo AI Browser Extension Sidebar */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-primary: #0A0A0A;
|
||||||
|
--bg-secondary: #141414;
|
||||||
|
--bg-tertiary: #1F1F1F;
|
||||||
|
--bg-hover: #2A2A2A;
|
||||||
|
|
||||||
|
--text-primary: #FFFFFF;
|
||||||
|
--text-secondary: #A0A0A0;
|
||||||
|
--text-tertiary: #6B6B6B;
|
||||||
|
|
||||||
|
--accent: #FF6B6B;
|
||||||
|
--accent-hover: #FF5252;
|
||||||
|
--accent-dim: #FF6B6B20;
|
||||||
|
|
||||||
|
--success: #4CAF50;
|
||||||
|
--warning: #FFC107;
|
||||||
|
--error: #F44336;
|
||||||
|
|
||||||
|
--border: #2A2A2A;
|
||||||
|
--shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-container {
|
||||||
|
width: 320px;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.icon-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn.small {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auth Section */
|
||||||
|
.auth-section {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Indicators */
|
||||||
|
.status-indicator {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.connected {
|
||||||
|
background: var(--success);
|
||||||
|
box-shadow: 0 0 0 3px var(--success)20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.warning {
|
||||||
|
background: var(--warning);
|
||||||
|
box-shadow: 0 0 0 3px var(--warning)20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.disconnected {
|
||||||
|
background: var(--error);
|
||||||
|
box-shadow: 0 0 0 3px var(--error)20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Profile */
|
||||||
|
.user-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Panels */
|
||||||
|
.panel {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Rows */
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-row span:first-child {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Agent List */
|
||||||
|
.agent-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-item:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: var(--accent-dim);
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-name {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-status {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Filesystem */
|
||||||
|
.tab-filesystem {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Process List */
|
||||||
|
.process-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-name {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-time {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions */
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Panel */
|
||||||
|
.settings-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group h4 {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item span {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item input[type="text"],
|
||||||
|
.setting-item select {
|
||||||
|
width: 180px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item input[type="number"] {
|
||||||
|
width: 60px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item input[type="checkbox"] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty States */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Hanzo AI Control Panel</title>
|
||||||
|
<link rel="stylesheet" href="sidebar.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="sidebar-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="sidebar-header">
|
||||||
|
<div class="brand">
|
||||||
|
<svg class="logo" viewBox="0 0 32 32" fill="none">
|
||||||
|
<path d="M16 2L2 9v7c0 8.5 5.5 16.5 14 18 8.5-1.5 14-9.5 14-18V9L16 2z" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M16 8v8l6 4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>Hanzo AI</span>
|
||||||
|
</div>
|
||||||
|
<button id="minimize-btn" class="icon-btn" title="Minimize">
|
||||||
|
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor">
|
||||||
|
<path d="M13 9l-3 3-3-3M13 6l-3 3-3-3" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Auth Section -->
|
||||||
|
<section id="auth-section" class="auth-section">
|
||||||
|
<div class="auth-status" id="auth-status">
|
||||||
|
<div class="status-indicator disconnected"></div>
|
||||||
|
<span>Not connected</span>
|
||||||
|
</div>
|
||||||
|
<button id="auth-btn" class="primary-btn">
|
||||||
|
Connect to Hanzo IAM
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Main Content (shown after auth) -->
|
||||||
|
<main id="main-content" class="main-content hidden">
|
||||||
|
<!-- User Profile -->
|
||||||
|
<div class="user-profile">
|
||||||
|
<img id="user-avatar" class="user-avatar" src="" alt="">
|
||||||
|
<div class="user-details">
|
||||||
|
<div id="user-name" class="user-name"></div>
|
||||||
|
<div id="user-email" class="user-email"></div>
|
||||||
|
</div>
|
||||||
|
<button id="logout-btn" class="icon-btn" title="Disconnect">
|
||||||
|
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor">
|
||||||
|
<path d="M7 17l-5-5 5-5M2 12h10M10 3h6a2 2 0 012 2v10a2 2 0 01-2 2h-6" stroke-width="1.5"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MCP Connection Status -->
|
||||||
|
<div class="panel">
|
||||||
|
<h3>MCP Server</h3>
|
||||||
|
<div class="mcp-status">
|
||||||
|
<div class="status-row">
|
||||||
|
<span>Status</span>
|
||||||
|
<span id="mcp-status" class="status-value">
|
||||||
|
<span class="status-indicator connected"></span>
|
||||||
|
Connected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-row">
|
||||||
|
<span>Endpoint</span>
|
||||||
|
<span class="status-value mono">localhost:3001</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-row">
|
||||||
|
<span>Tools Available</span>
|
||||||
|
<span id="mcp-tools" class="status-value">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Agents -->
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h3>Active Agents</h3>
|
||||||
|
<span id="agent-count" class="badge">0</span>
|
||||||
|
</div>
|
||||||
|
<div id="agent-list" class="agent-list">
|
||||||
|
<!-- Agents will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Filesystem -->
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h3>Tab Filesystem</h3>
|
||||||
|
<button class="icon-btn small" id="refresh-tabs" title="Refresh">
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor">
|
||||||
|
<path d="M14 2v5h-5M2 7a6 6 0 0111.5-2.5L14 5M2 14v-5h5M14 9a6 6 0 01-11.5 2.5L2 11" stroke-width="1.5"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="tab-fs" class="tab-filesystem">
|
||||||
|
<!-- Tab filesystem tree will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WebGPU Status -->
|
||||||
|
<div class="panel">
|
||||||
|
<h3>WebGPU AI</h3>
|
||||||
|
<div class="gpu-status">
|
||||||
|
<div class="status-row">
|
||||||
|
<span>Status</span>
|
||||||
|
<span id="gpu-status" class="status-value">
|
||||||
|
<span class="status-indicator warning"></span>
|
||||||
|
Checking...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-row">
|
||||||
|
<span>Model</span>
|
||||||
|
<span id="gpu-model" class="status-value mono">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-row">
|
||||||
|
<span>Memory</span>
|
||||||
|
<span id="gpu-memory" class="status-value">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Running Processes -->
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h3>Processes</h3>
|
||||||
|
<span id="process-count" class="badge">0</span>
|
||||||
|
</div>
|
||||||
|
<div id="process-list" class="process-list">
|
||||||
|
<!-- Processes will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="actions">
|
||||||
|
<button id="launch-agent" class="action-btn">
|
||||||
|
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor">
|
||||||
|
<path d="M10 4v12M4 10h12" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
Launch Agent
|
||||||
|
</button>
|
||||||
|
<button id="settings-btn" class="action-btn">
|
||||||
|
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor">
|
||||||
|
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" stroke-width="2"/>
|
||||||
|
<path d="M13.7 7.3l1.1-1.9a8 8 0 011.7 1l-1.1 1.9m0 5.4l1.1 1.9a8 8 0 01-1.7 1l-1.1-1.9M6.3 12.7l-1.1 1.9a8 8 0 01-1.7-1l1.1-1.9m0-5.4L3.5 4.4a8 8 0 011.7-1l1.1 1.9" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Settings Panel -->
|
||||||
|
<div id="settings-panel" class="settings-panel hidden">
|
||||||
|
<div class="settings-header">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<button id="close-settings" class="icon-btn">
|
||||||
|
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor">
|
||||||
|
<path d="M14 6l-8 8M6 6l8 8" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-content">
|
||||||
|
<div class="setting-group">
|
||||||
|
<h4>MCP Connection</h4>
|
||||||
|
<label class="setting-item">
|
||||||
|
<span>Server URL</span>
|
||||||
|
<input type="text" id="mcp-url" value="ws://localhost:3001/browser-extension">
|
||||||
|
</label>
|
||||||
|
<label class="setting-item">
|
||||||
|
<span>Auto-reconnect</span>
|
||||||
|
<input type="checkbox" id="auto-reconnect" checked>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<h4>AI Models</h4>
|
||||||
|
<label class="setting-item">
|
||||||
|
<span>Enable WebGPU</span>
|
||||||
|
<input type="checkbox" id="enable-webgpu" checked>
|
||||||
|
</label>
|
||||||
|
<label class="setting-item">
|
||||||
|
<span>Model Quantization</span>
|
||||||
|
<select id="quantization">
|
||||||
|
<option value="4bit">4-bit (Fast)</option>
|
||||||
|
<option value="8bit">8-bit (Balanced)</option>
|
||||||
|
<option value="fp16">FP16 (Quality)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<h4>Browser Control</h4>
|
||||||
|
<label class="setting-item">
|
||||||
|
<span>Safe Mode</span>
|
||||||
|
<input type="checkbox" id="safe-mode" checked>
|
||||||
|
</label>
|
||||||
|
<label class="setting-item">
|
||||||
|
<span>Max Concurrent Agents</span>
|
||||||
|
<input type="number" id="max-agents" value="3" min="1" max="10">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="save-settings" class="primary-btn">Save Settings</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="sidebar.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
// Hanzo AI Browser Extension Sidebar Script
|
||||||
|
|
||||||
|
class HanzoSidebar {
|
||||||
|
constructor() {
|
||||||
|
this.authToken = null;
|
||||||
|
this.user = null;
|
||||||
|
this.mcpConnection = null;
|
||||||
|
this.agents = new Map();
|
||||||
|
this.processes = new Map();
|
||||||
|
|
||||||
|
this.initializeUI();
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.checkAuthStatus();
|
||||||
|
this.connectToMCP();
|
||||||
|
this.startMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeUI() {
|
||||||
|
// Get DOM elements
|
||||||
|
this.elements = {
|
||||||
|
authSection: document.getElementById('auth-section'),
|
||||||
|
authStatus: document.getElementById('auth-status'),
|
||||||
|
authBtn: document.getElementById('auth-btn'),
|
||||||
|
mainContent: document.getElementById('main-content'),
|
||||||
|
userAvatar: document.getElementById('user-avatar'),
|
||||||
|
userName: document.getElementById('user-name'),
|
||||||
|
userEmail: document.getElementById('user-email'),
|
||||||
|
logoutBtn: document.getElementById('logout-btn'),
|
||||||
|
|
||||||
|
mcpStatus: document.getElementById('mcp-status'),
|
||||||
|
mcpTools: document.getElementById('mcp-tools'),
|
||||||
|
|
||||||
|
agentCount: document.getElementById('agent-count'),
|
||||||
|
agentList: document.getElementById('agent-list'),
|
||||||
|
|
||||||
|
tabFs: document.getElementById('tab-fs'),
|
||||||
|
refreshTabs: document.getElementById('refresh-tabs'),
|
||||||
|
|
||||||
|
gpuStatus: document.getElementById('gpu-status'),
|
||||||
|
gpuModel: document.getElementById('gpu-model'),
|
||||||
|
gpuMemory: document.getElementById('gpu-memory'),
|
||||||
|
|
||||||
|
processCount: document.getElementById('process-count'),
|
||||||
|
processList: document.getElementById('process-list'),
|
||||||
|
|
||||||
|
launchAgent: document.getElementById('launch-agent'),
|
||||||
|
settingsBtn: document.getElementById('settings-btn'),
|
||||||
|
settingsPanel: document.getElementById('settings-panel'),
|
||||||
|
closeSettings: document.getElementById('close-settings'),
|
||||||
|
saveSettings: document.getElementById('save-settings')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Auth
|
||||||
|
this.elements.authBtn.addEventListener('click', () => this.authenticate());
|
||||||
|
this.elements.logoutBtn.addEventListener('click', () => this.logout());
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
this.elements.launchAgent.addEventListener('click', () => this.showAgentLauncher());
|
||||||
|
this.elements.refreshTabs.addEventListener('click', () => this.refreshTabFilesystem());
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
this.elements.settingsBtn.addEventListener('click', () => this.showSettings());
|
||||||
|
this.elements.closeSettings.addEventListener('click', () => this.hideSettings());
|
||||||
|
this.elements.saveSettings.addEventListener('click', () => this.saveSettings());
|
||||||
|
|
||||||
|
// Listen for messages from background script
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
this.handleMessage(request, sender, sendResponse);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAuthStatus() {
|
||||||
|
try {
|
||||||
|
const { authToken, user } = await chrome.storage.local.get(['authToken', 'user']);
|
||||||
|
|
||||||
|
if (authToken && user) {
|
||||||
|
this.authToken = authToken;
|
||||||
|
this.user = user;
|
||||||
|
this.showAuthenticated();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking auth status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async authenticate() {
|
||||||
|
this.elements.authBtn.disabled = true;
|
||||||
|
this.elements.authBtn.textContent = 'Connecting...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Open IAM login in new tab
|
||||||
|
const authUrl = 'https://iam.hanzo.ai/auth/browser-extension';
|
||||||
|
const authTab = await chrome.tabs.create({ url: authUrl });
|
||||||
|
|
||||||
|
// Listen for auth callback
|
||||||
|
const authListener = (tabId, changeInfo, tab) => {
|
||||||
|
if (tabId === authTab.id && changeInfo.url) {
|
||||||
|
const url = new URL(changeInfo.url);
|
||||||
|
|
||||||
|
if (url.hostname === 'localhost' && url.searchParams.has('token')) {
|
||||||
|
const token = url.searchParams.get('token');
|
||||||
|
const user = {
|
||||||
|
id: url.searchParams.get('user_id'),
|
||||||
|
name: url.searchParams.get('name'),
|
||||||
|
email: url.searchParams.get('email'),
|
||||||
|
avatar: url.searchParams.get('avatar')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store auth data
|
||||||
|
chrome.storage.local.set({ authToken: token, user });
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
this.authToken = token;
|
||||||
|
this.user = user;
|
||||||
|
this.showAuthenticated();
|
||||||
|
|
||||||
|
// Close auth tab
|
||||||
|
chrome.tabs.remove(tabId);
|
||||||
|
chrome.tabs.onUpdated.removeListener(authListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.tabs.onUpdated.addListener(authListener);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Authentication error:', error);
|
||||||
|
this.showError('Failed to authenticate');
|
||||||
|
} finally {
|
||||||
|
this.elements.authBtn.disabled = false;
|
||||||
|
this.elements.authBtn.textContent = 'Connect to Hanzo IAM';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showAuthenticated() {
|
||||||
|
// Update auth status
|
||||||
|
this.elements.authStatus.innerHTML = `
|
||||||
|
<div class="status-indicator connected"></div>
|
||||||
|
<span>Connected</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show user info
|
||||||
|
if (this.user) {
|
||||||
|
this.elements.userAvatar.src = this.user.avatar || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="%23FF6B6B"/></svg>';
|
||||||
|
this.elements.userName.textContent = this.user.name || 'User';
|
||||||
|
this.elements.userEmail.textContent = this.user.email || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show main content
|
||||||
|
this.elements.authSection.style.display = 'none';
|
||||||
|
this.elements.mainContent.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
await chrome.storage.local.remove(['authToken', 'user']);
|
||||||
|
this.authToken = null;
|
||||||
|
this.user = null;
|
||||||
|
|
||||||
|
// Reset UI
|
||||||
|
this.elements.authSection.style.display = 'flex';
|
||||||
|
this.elements.mainContent.classList.add('hidden');
|
||||||
|
this.elements.authStatus.innerHTML = `
|
||||||
|
<div class="status-indicator disconnected"></div>
|
||||||
|
<span>Not connected</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectToMCP() {
|
||||||
|
try {
|
||||||
|
// Send message to background script to check MCP connection
|
||||||
|
const response = await chrome.runtime.sendMessage({ action: 'getMCPStatus' });
|
||||||
|
|
||||||
|
if (response?.connected) {
|
||||||
|
this.updateMCPStatus(true, response.tools || 0);
|
||||||
|
} else {
|
||||||
|
this.updateMCPStatus(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MCP connection error:', error);
|
||||||
|
this.updateMCPStatus(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMCPStatus(connected, toolCount = 0) {
|
||||||
|
const statusEl = this.elements.mcpStatus;
|
||||||
|
statusEl.innerHTML = connected ? `
|
||||||
|
<span class="status-indicator connected"></span>
|
||||||
|
Connected
|
||||||
|
` : `
|
||||||
|
<span class="status-indicator disconnected"></span>
|
||||||
|
Disconnected
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.elements.mcpTools.textContent = toolCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshTabFilesystem() {
|
||||||
|
try {
|
||||||
|
const tabs = await chrome.tabs.query({});
|
||||||
|
const filesystem = tabs.map((tab, index) => ({
|
||||||
|
path: `/tabs/${index}/${this.sanitizePath(tab.title || 'untitled')}`,
|
||||||
|
tabId: tab.id,
|
||||||
|
url: tab.url,
|
||||||
|
title: tab.title || 'Untitled',
|
||||||
|
active: tab.active
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
this.elements.tabFs.innerHTML = filesystem.map(item => `
|
||||||
|
<div class="tab-item ${item.active ? 'active' : ''}" data-tab-id="${item.tabId}">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M2 2v12h12V2H2zm1 1h10v10H3V3z"/>
|
||||||
|
</svg>
|
||||||
|
<span title="${item.url}">${item.path}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Add click handlers
|
||||||
|
this.elements.tabFs.querySelectorAll('.tab-item').forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
const tabId = parseInt(item.dataset.tabId);
|
||||||
|
chrome.tabs.update(tabId, { active: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing tab filesystem:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizePath(title) {
|
||||||
|
return title.replace(/[^a-zA-Z0-9-_]/g, '_').toLowerCase().substring(0, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkWebGPU() {
|
||||||
|
try {
|
||||||
|
const response = await chrome.runtime.sendMessage({ action: 'checkWebGPU' });
|
||||||
|
|
||||||
|
if (response?.available) {
|
||||||
|
this.elements.gpuStatus.innerHTML = `
|
||||||
|
<span class="status-indicator connected"></span>
|
||||||
|
Available
|
||||||
|
`;
|
||||||
|
this.elements.gpuModel.textContent = response.adapter || 'Unknown';
|
||||||
|
this.elements.gpuMemory.textContent = response.memory || '-';
|
||||||
|
} else {
|
||||||
|
this.elements.gpuStatus.innerHTML = `
|
||||||
|
<span class="status-indicator disconnected"></span>
|
||||||
|
Not Available
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WebGPU check error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAgentList() {
|
||||||
|
const agentArray = Array.from(this.agents.values());
|
||||||
|
this.elements.agentCount.textContent = agentArray.length;
|
||||||
|
|
||||||
|
if (agentArray.length === 0) {
|
||||||
|
this.elements.agentList.innerHTML = '<div class="empty-state">No active agents</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.elements.agentList.innerHTML = agentArray.map(agent => `
|
||||||
|
<div class="agent-item">
|
||||||
|
<div class="agent-icon">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="12" cy="12" r="10" stroke-width="2"/>
|
||||||
|
<path d="M12 6v6l4 2" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="agent-info">
|
||||||
|
<div class="agent-name">${agent.name}</div>
|
||||||
|
<div class="agent-status">${agent.status} • Tab ${agent.tabId}</div>
|
||||||
|
</div>
|
||||||
|
<div class="agent-actions">
|
||||||
|
<button class="icon-btn small" onclick="hanzoSidebar.pauseAgent('${agent.id}')">
|
||||||
|
<svg viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M6 3v10M10 3v10" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="icon-btn small" onclick="hanzoSidebar.stopAgent('${agent.id}')">
|
||||||
|
<svg viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<rect x="4" y="4" width="8" height="8" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProcessList() {
|
||||||
|
const processArray = Array.from(this.processes.values());
|
||||||
|
this.elements.processCount.textContent = processArray.length;
|
||||||
|
|
||||||
|
if (processArray.length === 0) {
|
||||||
|
this.elements.processList.innerHTML = '<div class="empty-state">No running processes</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.elements.processList.innerHTML = processArray.map(proc => `
|
||||||
|
<div class="process-item">
|
||||||
|
<span class="process-name">${proc.name}</span>
|
||||||
|
<span class="process-time">${this.formatDuration(proc.startTime)}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDuration(startTime) {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const seconds = Math.floor(elapsed / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
|
||||||
|
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
||||||
|
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
||||||
|
return `${seconds}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showAgentLauncher() {
|
||||||
|
// Create agent launcher modal
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'agent-launcher-modal';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>Launch Agent</h3>
|
||||||
|
<label>
|
||||||
|
Agent Type
|
||||||
|
<select id="agent-type">
|
||||||
|
<option value="browser-control">Browser Control</option>
|
||||||
|
<option value="data-extraction">Data Extraction</option>
|
||||||
|
<option value="form-automation">Form Automation</option>
|
||||||
|
<option value="testing">Testing</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Target Tab
|
||||||
|
<select id="target-tab">
|
||||||
|
<option value="current">Current Tab</option>
|
||||||
|
<option value="all">All Tabs</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Model
|
||||||
|
<select id="agent-model">
|
||||||
|
<option value="local">Local (WebGPU)</option>
|
||||||
|
<option value="claude">Claude</option>
|
||||||
|
<option value="gpt4">GPT-4</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn secondary" onclick="this.parentElement.parentElement.parentElement.remove()">Cancel</button>
|
||||||
|
<button class="btn primary" onclick="hanzoSidebar.launchAgent()">Launch</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async launchAgent() {
|
||||||
|
const type = document.getElementById('agent-type').value;
|
||||||
|
const target = document.getElementById('target-tab').value;
|
||||||
|
const model = document.getElementById('agent-model').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
action: 'launchAgent',
|
||||||
|
config: { type, target, model }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
this.agents.set(response.agentId, {
|
||||||
|
id: response.agentId,
|
||||||
|
name: `${type}-${response.agentId.slice(0, 6)}`,
|
||||||
|
status: 'Running',
|
||||||
|
tabId: response.tabId,
|
||||||
|
type,
|
||||||
|
model
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateAgentList();
|
||||||
|
document.querySelector('.agent-launcher-modal').remove();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error launching agent:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showSettings() {
|
||||||
|
this.elements.settingsPanel.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSettings() {
|
||||||
|
this.elements.settingsPanel.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
const settings = {
|
||||||
|
mcpUrl: document.getElementById('mcp-url').value,
|
||||||
|
autoReconnect: document.getElementById('auto-reconnect').checked,
|
||||||
|
enableWebGPU: document.getElementById('enable-webgpu').checked,
|
||||||
|
quantization: document.getElementById('quantization').value,
|
||||||
|
safeMode: document.getElementById('safe-mode').checked,
|
||||||
|
maxAgents: parseInt(document.getElementById('max-agents').value)
|
||||||
|
};
|
||||||
|
|
||||||
|
await chrome.storage.local.set({ settings });
|
||||||
|
this.hideSettings();
|
||||||
|
this.showNotification('Settings saved');
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'notification';
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => notification.remove(), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
const error = document.createElement('div');
|
||||||
|
error.className = 'notification error';
|
||||||
|
error.textContent = message;
|
||||||
|
document.body.appendChild(error);
|
||||||
|
|
||||||
|
setTimeout(() => error.remove(), 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(request, sender, sendResponse) {
|
||||||
|
switch (request.action) {
|
||||||
|
case 'agentUpdate':
|
||||||
|
if (request.agentId && this.agents.has(request.agentId)) {
|
||||||
|
this.agents.get(request.agentId).status = request.status;
|
||||||
|
this.updateAgentList();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'processUpdate':
|
||||||
|
if (request.processId) {
|
||||||
|
if (request.status === 'started') {
|
||||||
|
this.processes.set(request.processId, {
|
||||||
|
id: request.processId,
|
||||||
|
name: request.name,
|
||||||
|
startTime: Date.now()
|
||||||
|
});
|
||||||
|
} else if (request.status === 'ended') {
|
||||||
|
this.processes.delete(request.processId);
|
||||||
|
}
|
||||||
|
this.updateProcessList();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseAgent(agentId) {
|
||||||
|
chrome.runtime.sendMessage({ action: 'pauseAgent', agentId });
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAgent(agentId) {
|
||||||
|
chrome.runtime.sendMessage({ action: 'stopAgent', agentId });
|
||||||
|
this.agents.delete(agentId);
|
||||||
|
this.updateAgentList();
|
||||||
|
}
|
||||||
|
|
||||||
|
startMonitoring() {
|
||||||
|
// Initial checks
|
||||||
|
this.refreshTabFilesystem();
|
||||||
|
this.checkWebGPU();
|
||||||
|
|
||||||
|
// Periodic updates
|
||||||
|
setInterval(() => {
|
||||||
|
this.connectToMCP();
|
||||||
|
this.updateProcessList();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Tab change listener
|
||||||
|
chrome.tabs.onUpdated.addListener(() => this.refreshTabFilesystem());
|
||||||
|
chrome.tabs.onRemoved.addListener(() => this.refreshTabFilesystem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize sidebar
|
||||||
|
const hanzoSidebar = new HanzoSidebar();
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
// WebGPU AI Runner for Browser Extension
|
||||||
|
// Enables local AI inference directly in the browser
|
||||||
|
|
||||||
|
interface ModelConfig {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
quantization: '4bit' | '8bit' | 'fp16';
|
||||||
|
maxTokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebGPUAI {
|
||||||
|
private device: GPUDevice | null = null;
|
||||||
|
private models: Map<string, any> = new Map();
|
||||||
|
|
||||||
|
async initialize(): Promise<boolean> {
|
||||||
|
if (!navigator.gpu) {
|
||||||
|
console.warn('[Hanzo AI] WebGPU not supported in this browser');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const adapter = await navigator.gpu.requestAdapter();
|
||||||
|
if (!adapter) {
|
||||||
|
console.warn('[Hanzo AI] No GPU adapter found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.device = await adapter.requestDevice();
|
||||||
|
console.log('[Hanzo AI] WebGPU initialized successfully');
|
||||||
|
|
||||||
|
// Check for required features
|
||||||
|
const features = adapter.features;
|
||||||
|
console.log('[Hanzo AI] GPU Features:', Array.from(features));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Hanzo AI] WebGPU initialization failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadModel(config: ModelConfig): Promise<void> {
|
||||||
|
if (!this.device) {
|
||||||
|
throw new Error('WebGPU not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Hanzo AI] Loading model: ${config.name}`);
|
||||||
|
|
||||||
|
// Load model weights
|
||||||
|
const response = await fetch(config.url);
|
||||||
|
const modelData = await response.arrayBuffer();
|
||||||
|
|
||||||
|
// Create GPU buffers for model
|
||||||
|
const modelBuffer = this.device.createBuffer({
|
||||||
|
size: modelData.byteLength,
|
||||||
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
||||||
|
});
|
||||||
|
|
||||||
|
this.device.queue.writeBuffer(modelBuffer, 0, modelData);
|
||||||
|
|
||||||
|
// Store model configuration
|
||||||
|
this.models.set(config.name, {
|
||||||
|
buffer: modelBuffer,
|
||||||
|
config: config
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async runInference(modelName: string, input: string): Promise<string> {
|
||||||
|
const model = this.models.get(modelName);
|
||||||
|
if (!model || !this.device) {
|
||||||
|
throw new Error(`Model ${modelName} not loaded`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokenize input
|
||||||
|
const tokens = this.tokenize(input);
|
||||||
|
|
||||||
|
// Create input buffer
|
||||||
|
const inputBuffer = this.device.createBuffer({
|
||||||
|
size: tokens.length * 4, // 32-bit integers
|
||||||
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
||||||
|
});
|
||||||
|
|
||||||
|
this.device.queue.writeBuffer(inputBuffer, 0, new Int32Array(tokens));
|
||||||
|
|
||||||
|
// Create output buffer
|
||||||
|
const outputBuffer = this.device.createBuffer({
|
||||||
|
size: model.config.maxTokens * 4,
|
||||||
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create compute pipeline
|
||||||
|
const pipeline = await this.createInferencePipeline(model);
|
||||||
|
|
||||||
|
// Run inference
|
||||||
|
const commandEncoder = this.device.createCommandEncoder();
|
||||||
|
const passEncoder = commandEncoder.beginComputePass();
|
||||||
|
|
||||||
|
passEncoder.setPipeline(pipeline);
|
||||||
|
passEncoder.setBindGroup(0, this.createBindGroup(model, inputBuffer, outputBuffer));
|
||||||
|
passEncoder.dispatchWorkgroups(Math.ceil(tokens.length / 64));
|
||||||
|
passEncoder.end();
|
||||||
|
|
||||||
|
this.device.queue.submit([commandEncoder.finish()]);
|
||||||
|
|
||||||
|
// Read results
|
||||||
|
const resultBuffer = await this.readBuffer(outputBuffer);
|
||||||
|
const result = this.detokenize(new Int32Array(resultBuffer));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private tokenize(text: string): number[] {
|
||||||
|
// Simple tokenization - in production use proper tokenizer
|
||||||
|
return text.split(/\s+/).map(word => this.hashCode(word));
|
||||||
|
}
|
||||||
|
|
||||||
|
private detokenize(tokens: Int32Array): string {
|
||||||
|
// Simple detokenization - in production use proper detokenizer
|
||||||
|
return Array.from(tokens).map(t => `token_${t}`).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private hashCode(str: string): number {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + char;
|
||||||
|
hash = hash & hash; // Convert to 32-bit integer
|
||||||
|
}
|
||||||
|
return Math.abs(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createInferencePipeline(model: any): Promise<GPUComputePipeline> {
|
||||||
|
if (!this.device) throw new Error('Device not initialized');
|
||||||
|
|
||||||
|
const shaderModule = this.device.createShaderModule({
|
||||||
|
code: `
|
||||||
|
@group(0) @binding(0) var<storage, read> model_weights: array<f32>;
|
||||||
|
@group(0) @binding(1) var<storage, read> input_tokens: array<i32>;
|
||||||
|
@group(0) @binding(2) var<storage, read_write> output_tokens: array<i32>;
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||||
|
let idx = global_id.x;
|
||||||
|
if (idx >= arrayLength(&input_tokens)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified inference - real implementation would use proper transformer
|
||||||
|
let token = input_tokens[idx];
|
||||||
|
let weight_idx = token % arrayLength(&model_weights);
|
||||||
|
let weight = model_weights[weight_idx];
|
||||||
|
|
||||||
|
// Generate next token (simplified)
|
||||||
|
output_tokens[idx] = i32(weight * f32(token));
|
||||||
|
}
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.device.createComputePipeline({
|
||||||
|
layout: 'auto',
|
||||||
|
compute: {
|
||||||
|
module: shaderModule,
|
||||||
|
entryPoint: 'main'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createBindGroup(model: any, inputBuffer: GPUBuffer, outputBuffer: GPUBuffer): GPUBindGroup {
|
||||||
|
if (!this.device) throw new Error('Device not initialized');
|
||||||
|
|
||||||
|
const bindGroupLayout = this.device.createBindGroupLayout({
|
||||||
|
entries: [
|
||||||
|
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
|
||||||
|
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
|
||||||
|
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.device.createBindGroup({
|
||||||
|
layout: bindGroupLayout,
|
||||||
|
entries: [
|
||||||
|
{ binding: 0, resource: { buffer: model.buffer } },
|
||||||
|
{ binding: 1, resource: { buffer: inputBuffer } },
|
||||||
|
{ binding: 2, resource: { buffer: outputBuffer } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readBuffer(buffer: GPUBuffer): Promise<ArrayBuffer> {
|
||||||
|
if (!this.device) throw new Error('Device not initialized');
|
||||||
|
|
||||||
|
const readBuffer = this.device.createBuffer({
|
||||||
|
size: buffer.size,
|
||||||
|
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
||||||
|
});
|
||||||
|
|
||||||
|
const commandEncoder = this.device.createCommandEncoder();
|
||||||
|
commandEncoder.copyBufferToBuffer(buffer, 0, readBuffer, 0, buffer.size);
|
||||||
|
this.device.queue.submit([commandEncoder.finish()]);
|
||||||
|
|
||||||
|
await readBuffer.mapAsync(GPUMapMode.READ);
|
||||||
|
const data = readBuffer.getMappedRange().slice(0);
|
||||||
|
readBuffer.unmap();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed MCP server using official SDK - auth is optional and happens in conversation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
// Import our existing tools
|
||||||
|
const mockVscode = {
|
||||||
|
workspace: {
|
||||||
|
workspaceFolders: process.env.HANZO_WORKSPACE ? [{
|
||||||
|
uri: { fsPath: process.env.HANZO_WORKSPACE }
|
||||||
|
}] : undefined,
|
||||||
|
getConfiguration: (section: string) => ({
|
||||||
|
get: (key: string, defaultValue: any) => {
|
||||||
|
const envKey = `HANZO_${section.toUpperCase()}_${key.toUpperCase().replace(/\./g, '_')}`;
|
||||||
|
return process.env[envKey] || defaultValue;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
findFiles: async () => [],
|
||||||
|
fs: {
|
||||||
|
readFile: async (uri: any) => {
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const content = await fs.readFile(uri.fsPath || uri);
|
||||||
|
return Buffer.from(content);
|
||||||
|
},
|
||||||
|
writeFile: async (uri: any, content: any) => {
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
await fs.writeFile(uri.fsPath || uri, content);
|
||||||
|
},
|
||||||
|
createDirectory: async (uri: any) => {
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
await fs.mkdir(uri.fsPath || uri, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
showErrorMessage: console.error,
|
||||||
|
showInformationMessage: console.log,
|
||||||
|
visibleTextEditors: [],
|
||||||
|
createOutputChannel: (name: string) => ({
|
||||||
|
appendLine: (text: string) => console.error(`[${name}] ${text}`),
|
||||||
|
append: (text: string) => process.stderr.write(text),
|
||||||
|
clear: () => {},
|
||||||
|
dispose: () => {},
|
||||||
|
hide: () => {},
|
||||||
|
show: () => {}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
openExternal: async (uri: any) => {
|
||||||
|
console.error(`[Hanzo] Opening: ${uri}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Uri: {
|
||||||
|
file: (filePath: string) => ({ fsPath: filePath }),
|
||||||
|
parse: (str: string) => ({ fsPath: str })
|
||||||
|
},
|
||||||
|
ExtensionContext: class {
|
||||||
|
globalState = {
|
||||||
|
get: (key: string, defaultValue?: any) => {
|
||||||
|
return defaultValue;
|
||||||
|
},
|
||||||
|
update: async (key: string, value: any) => {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
extensionPath = __dirname;
|
||||||
|
subscriptions: any[] = [];
|
||||||
|
},
|
||||||
|
SymbolKind: {
|
||||||
|
Function: 11,
|
||||||
|
Class: 4,
|
||||||
|
Method: 5,
|
||||||
|
Variable: 12,
|
||||||
|
Constant: 13,
|
||||||
|
Interface: 10
|
||||||
|
},
|
||||||
|
version: '1.0.0',
|
||||||
|
commands: {
|
||||||
|
executeCommand: async () => []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replace global vscode with mock
|
||||||
|
(global as any).vscode = mockVscode;
|
||||||
|
|
||||||
|
// Import tools after mocking vscode
|
||||||
|
import { MCPTools } from './mcp/tools';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.error('[Hanzo MCP] Starting server v1.5.7...');
|
||||||
|
|
||||||
|
// Create the MCP server
|
||||||
|
const server = new McpServer({
|
||||||
|
name: 'hanzo-mcp',
|
||||||
|
version: '1.5.7'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create mock context
|
||||||
|
const context = new mockVscode.ExtensionContext();
|
||||||
|
|
||||||
|
// Track auth state
|
||||||
|
let isAuthenticated = false;
|
||||||
|
let authToken: string | null = null;
|
||||||
|
|
||||||
|
// Initialize tools
|
||||||
|
const tools = new MCPTools(context as any);
|
||||||
|
await tools.initialize();
|
||||||
|
|
||||||
|
// Register the login tool
|
||||||
|
server.registerTool(
|
||||||
|
'hanzo_login',
|
||||||
|
{
|
||||||
|
title: 'Hanzo Login',
|
||||||
|
description: 'Login to Hanzo AI for cloud features (optional)',
|
||||||
|
inputSchema: z.object({})
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const deviceCode = Math.random().toString(36).substring(2, 15);
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: `🔐 To login to Hanzo AI and unlock cloud features:
|
||||||
|
|
||||||
|
1. Visit: https://hanzo.ai/device?code=${deviceCode}
|
||||||
|
2. Sign in with your Hanzo account
|
||||||
|
3. Enter device code: ${deviceCode}
|
||||||
|
|
||||||
|
Once authenticated, you'll have access to:
|
||||||
|
✨ Cloud-hosted MCP servers (4000+ tools)
|
||||||
|
✨ Hanzo AI router (200+ LLMs)
|
||||||
|
✨ Vector storage and search
|
||||||
|
✨ Shared context across sessions
|
||||||
|
✨ Priority support
|
||||||
|
|
||||||
|
For now, continuing with local tools only. All core functionality works without login!`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register all the tools from MCPTools
|
||||||
|
const allTools = tools.getAllTools();
|
||||||
|
for (const tool of allTools) {
|
||||||
|
server.registerTool(
|
||||||
|
tool.name,
|
||||||
|
{
|
||||||
|
title: tool.title || tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: tool.inputSchema
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
try {
|
||||||
|
const result = await tool.handler(args);
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: `Error: ${error.message}`
|
||||||
|
}],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and connect transport
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
|
||||||
|
console.error(`[Hanzo MCP] Server ready with ${allTools.length + 1} tools`);
|
||||||
|
console.error('[Hanzo MCP] Auth is optional - use hanzo_login tool to enable cloud features');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle errors gracefully
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.error('[Hanzo MCP] Uncaught exception:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('[Hanzo MCP] Unhandled rejection:', reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('[Hanzo MCP] Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple MCP server that just works - no auth timeouts, auth happens in conversation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import { MCPTools } from './mcp/tools';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// Mock VS Code API for standalone operation
|
||||||
|
const mockVscode = {
|
||||||
|
workspace: {
|
||||||
|
workspaceFolders: process.env.HANZO_WORKSPACE ? [{
|
||||||
|
uri: { fsPath: process.env.HANZO_WORKSPACE }
|
||||||
|
}] : undefined,
|
||||||
|
getConfiguration: (section: string) => ({
|
||||||
|
get: (key: string, defaultValue: any) => {
|
||||||
|
const envKey = `HANZO_${section.toUpperCase()}_${key.toUpperCase().replace(/\./g, '_')}`;
|
||||||
|
return process.env[envKey] || defaultValue;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
findFiles: async () => [],
|
||||||
|
fs: {
|
||||||
|
readFile: async (uri: any) => {
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const content = await fs.readFile(uri.fsPath || uri);
|
||||||
|
return Buffer.from(content);
|
||||||
|
},
|
||||||
|
writeFile: async (uri: any, content: any) => {
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
await fs.writeFile(uri.fsPath || uri, content);
|
||||||
|
},
|
||||||
|
createDirectory: async (uri: any) => {
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
await fs.mkdir(uri.fsPath || uri, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
showErrorMessage: console.error,
|
||||||
|
showInformationMessage: console.log,
|
||||||
|
visibleTextEditors: [],
|
||||||
|
createOutputChannel: (name: string) => ({
|
||||||
|
appendLine: (text: string) => console.error(`[${name}] ${text}`),
|
||||||
|
append: (text: string) => process.stderr.write(text),
|
||||||
|
clear: () => {},
|
||||||
|
dispose: () => {},
|
||||||
|
hide: () => {},
|
||||||
|
show: () => {}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
openExternal: async (uri: any) => {
|
||||||
|
console.error(`[Hanzo] Opening: ${uri}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Uri: {
|
||||||
|
file: (path: string) => ({ fsPath: path }),
|
||||||
|
parse: (str: string) => ({ fsPath: str })
|
||||||
|
},
|
||||||
|
ExtensionContext: class {
|
||||||
|
globalState = {
|
||||||
|
get: (key: string, defaultValue?: any) => {
|
||||||
|
// Simple in-memory storage for session
|
||||||
|
return defaultValue;
|
||||||
|
},
|
||||||
|
update: async (key: string, value: any) => {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
extensionPath = __dirname;
|
||||||
|
subscriptions: any[] = [];
|
||||||
|
},
|
||||||
|
SymbolKind: {
|
||||||
|
Function: 11,
|
||||||
|
Class: 4,
|
||||||
|
Method: 5,
|
||||||
|
Variable: 12,
|
||||||
|
Constant: 13,
|
||||||
|
Interface: 10
|
||||||
|
},
|
||||||
|
version: '1.0.0',
|
||||||
|
commands: {
|
||||||
|
executeCommand: async () => []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replace global vscode with mock
|
||||||
|
(global as any).vscode = mockVscode;
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.error('[Hanzo MCP] Starting server...');
|
||||||
|
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: 'hanzo-mcp',
|
||||||
|
version: '1.5.7',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create mock context
|
||||||
|
const context = new mockVscode.ExtensionContext();
|
||||||
|
|
||||||
|
// Add auth status to context (always start anonymous, auth via tool)
|
||||||
|
(context as any).isAuthenticated = false;
|
||||||
|
(context as any).authToken = null;
|
||||||
|
|
||||||
|
// Initialize tools
|
||||||
|
const tools = new MCPTools(context as any);
|
||||||
|
await tools.initialize();
|
||||||
|
|
||||||
|
// Add a login tool
|
||||||
|
server.setRequestHandler('tools/call', async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
|
// Special handling for login
|
||||||
|
if (name === 'hanzo_login') {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `🔐 To login to Hanzo AI and unlock cloud features:
|
||||||
|
|
||||||
|
1. Visit: https://hanzo.ai/login?device=mcp&code=${Math.random().toString(36).substring(7)}
|
||||||
|
2. Sign in with your Hanzo account
|
||||||
|
3. Your MCP server will be automatically authenticated
|
||||||
|
|
||||||
|
Once logged in, you'll have access to:
|
||||||
|
- Cloud-hosted MCP servers (4000+ tools)
|
||||||
|
- Hanzo AI router (200+ LLMs)
|
||||||
|
- Vector storage and search
|
||||||
|
- Shared context across sessions
|
||||||
|
|
||||||
|
For now, continuing with local tools only.`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and execute tool
|
||||||
|
const tool = tools.getAllTools().find(t => t.name === name);
|
||||||
|
if (!tool) {
|
||||||
|
throw new Error(`Tool not found: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await tool.handler(args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(`Tool execution failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// List all tools
|
||||||
|
server.setRequestHandler('tools/list', async () => {
|
||||||
|
const allTools = tools.getAllTools();
|
||||||
|
|
||||||
|
// Add login tool
|
||||||
|
const toolList = [
|
||||||
|
{
|
||||||
|
name: 'hanzo_login',
|
||||||
|
description: 'Login to Hanzo AI for cloud features (optional)',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...allTools.map(tool => ({
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: tool.inputSchema
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
return { tools: toolList };
|
||||||
|
});
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
|
||||||
|
console.error('[Hanzo MCP] Server ready with ' + (tools.getAllTools().length + 1) + ' tools');
|
||||||
|
console.error('[Hanzo MCP] Auth is optional - use hanzo_login tool to enable cloud features');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('[Hanzo MCP] Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|||||||
import { BrowserExtensionServer } from '../../mcp-tools/browser-extension-server';
|
import { BrowserExtensionServer } from '../../mcp-tools/browser-extension-server';
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
describe('Claude Code Browser Extension Integration', () => {
|
describe('Claude Code Browser Extension Integration', () => {
|
||||||
let server: BrowserExtensionServer;
|
let server: BrowserExtensionServer;
|
||||||
@@ -106,16 +107,24 @@ describe('Claude Code Browser Extension Integration', () => {
|
|||||||
|
|
||||||
describe('Fallback to data-hanzo-id', () => {
|
describe('Fallback to data-hanzo-id', () => {
|
||||||
it('should handle elements without source-map data', async () => {
|
it('should handle elements without source-map data', async () => {
|
||||||
|
// Create the .hanzo directory and id-map.json for test
|
||||||
|
const hanzoDir = path.join(process.cwd(), '.hanzo');
|
||||||
|
const mapFile = path.join(hanzoDir, 'id-map.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(hanzoDir)) {
|
||||||
|
fs.mkdirSync(hanzoDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write test id map
|
||||||
|
fs.writeFileSync(mapFile, JSON.stringify({
|
||||||
|
'hanzo-abc123': {
|
||||||
|
file: path.join(process.cwd(), 'src/legacy/OldComponent.js'),
|
||||||
|
line: 55
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
const elementSelected = new Promise((resolve) => {
|
const elementSelected = new Promise((resolve) => {
|
||||||
server.on('elementSelected', (data) => {
|
server.once('elementSelected', resolve);
|
||||||
// Mock the ID lookup
|
|
||||||
if (data.fallbackId === 'hanzo-abc123') {
|
|
||||||
resolve({
|
|
||||||
file: path.join(process.cwd(), 'src/legacy/OldComponent.js'),
|
|
||||||
line: 55
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
@@ -131,6 +140,11 @@ describe('Claude Code Browser Extension Integration', () => {
|
|||||||
file: expect.stringContaining('OldComponent.js'),
|
file: expect.stringContaining('OldComponent.js'),
|
||||||
line: 55
|
line: 55
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
if (fs.existsSync(mapFile)) {
|
||||||
|
fs.unlinkSync(mapFile);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,7 +159,7 @@ describe('Claude Code Browser Extension Integration', () => {
|
|||||||
file: data.file,
|
file: data.file,
|
||||||
line: data.line,
|
line: data.line,
|
||||||
column: data.column,
|
column: data.column,
|
||||||
reason: `User clicked ${data.framework || 'HTML'} element at ${data.domPath}`
|
reason: `User clicked ${data.framework ? data.framework.charAt(0).toUpperCase() + data.framework.slice(1) : 'HTML'} element at ${data.domPath}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { ServerFrameworkDetector } from '../../browser-extension/server-frameworks';
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
|
|
||||||
|
describe('Server Framework Detection - Edge Cases', () => {
|
||||||
|
let detector: ServerFrameworkDetector;
|
||||||
|
let dom: JSDOM;
|
||||||
|
let document: Document;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
detector = new ServerFrameworkDetector();
|
||||||
|
dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
||||||
|
document = dom.window.document;
|
||||||
|
// @ts-ignore
|
||||||
|
global.document = document;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Django Debug Toolbar extraction', () => {
|
||||||
|
it('should handle Django with toolbar but no view info', () => {
|
||||||
|
// Create Django debug toolbar
|
||||||
|
const toolbar = document.createElement('div');
|
||||||
|
toolbar.id = 'djDebug';
|
||||||
|
document.body.appendChild(toolbar);
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const result = detector.detectDjango(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'django'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Code coverage for edge cases', () => {
|
||||||
|
it('should cover Django toolbar with specific panel', () => {
|
||||||
|
// Cover the Django Debug Toolbar code path
|
||||||
|
const toolbar = document.createElement('div');
|
||||||
|
toolbar.id = 'djDebugToolbar';
|
||||||
|
const panel = document.createElement('div');
|
||||||
|
panel.className = 'djDebugPanelContent';
|
||||||
|
const viewInfo = document.createElement('div');
|
||||||
|
viewInfo.className = 'view-info';
|
||||||
|
viewInfo.textContent = 'View: myapp.views.index';
|
||||||
|
panel.appendChild(viewInfo);
|
||||||
|
toolbar.appendChild(panel);
|
||||||
|
document.body.appendChild(toolbar);
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const info = detector['extractDjangoViewInfo']();
|
||||||
|
|
||||||
|
// Just verify it doesn't crash and returns an object
|
||||||
|
expect(info).toBeDefined();
|
||||||
|
expect(typeof info).toBe('object');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error handling', () => {
|
||||||
|
it('should handle missing Werkzeug traceback gracefully', () => {
|
||||||
|
const debuggerEl = document.createElement('div');
|
||||||
|
debuggerEl.id = 'werkzeug-debugger';
|
||||||
|
document.body.appendChild(debuggerEl);
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const result = detector.detectFlask(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'flask'
|
||||||
|
});
|
||||||
|
expect(result?.file).toBeUndefined();
|
||||||
|
expect(result?.line).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Rails edge cases', () => {
|
||||||
|
it('should handle Rails template without controller extraction', () => {
|
||||||
|
document.head.innerHTML = '<meta name="csrf-token" content="abc123">';
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = '<!-- BEGIN app/views/show.html.erb --><p>Content</p>';
|
||||||
|
|
||||||
|
const result = detector.detectRails(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'rails',
|
||||||
|
template: 'app/views/show.html.erb',
|
||||||
|
controller: '',
|
||||||
|
action: 'show'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Rails without any comments', () => {
|
||||||
|
document.head.innerHTML = '<meta name="csrf-token" content="abc123">';
|
||||||
|
const element = document.createElement('div');
|
||||||
|
|
||||||
|
const result = detector.detectRails(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'rails'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Real browser environment', () => {
|
||||||
|
it('should handle createTreeWalker in real browser', () => {
|
||||||
|
// Mock NodeFilter
|
||||||
|
global.NodeFilter = { SHOW_COMMENT: 8 };
|
||||||
|
|
||||||
|
// Create a more realistic DOM structure
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const parent = document.createElement('div');
|
||||||
|
parent.appendChild(element);
|
||||||
|
document.body.appendChild(parent);
|
||||||
|
|
||||||
|
// Simulate TreeWalker behavior
|
||||||
|
const originalCreateTreeWalker = document.createTreeWalker;
|
||||||
|
let walkerCalled = false;
|
||||||
|
document.createTreeWalker = function() {
|
||||||
|
walkerCalled = true;
|
||||||
|
return {
|
||||||
|
nextNode: () => null
|
||||||
|
};
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const comments = detector['extractHTMLComments'](element);
|
||||||
|
|
||||||
|
expect(walkerCalled).toBe(true);
|
||||||
|
expect(comments).toEqual([]);
|
||||||
|
|
||||||
|
// Restore
|
||||||
|
document.createTreeWalker = originalCreateTreeWalker;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('All framework detection - null cases', () => {
|
||||||
|
it('should handle all detectors returning null', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
|
||||||
|
// Ensure no framework markers are present
|
||||||
|
document.head.innerHTML = '';
|
||||||
|
document.body.innerHTML = '<div></div>';
|
||||||
|
|
||||||
|
const result = detector.detect(element);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { ServerFrameworkDetector } from '../../browser-extension/server-frameworks';
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
|
|
||||||
|
describe('Server Framework Detection', () => {
|
||||||
|
let detector: ServerFrameworkDetector;
|
||||||
|
let dom: JSDOM;
|
||||||
|
let document: Document;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
detector = new ServerFrameworkDetector();
|
||||||
|
dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
||||||
|
document = dom.window.document;
|
||||||
|
// @ts-ignore
|
||||||
|
global.document = document;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Rails detection', () => {
|
||||||
|
it('should detect Rails via CSRF token', () => {
|
||||||
|
document.head.innerHTML = '<meta name="csrf-token" content="abc123">';
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.setAttribute('data-controller', 'posts');
|
||||||
|
element.setAttribute('data-action', 'show');
|
||||||
|
|
||||||
|
const result = detector.detectRails(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'rails',
|
||||||
|
controller: 'posts',
|
||||||
|
action: 'show'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Rails ViewComponent', () => {
|
||||||
|
document.head.innerHTML = '<meta name="csrf-token" content="abc123">';
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.setAttribute('data-view-component', 'ButtonComponent');
|
||||||
|
element.setAttribute('data-view-component-path', 'app/components/button_component.rb');
|
||||||
|
|
||||||
|
const result = detector.detectRails(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'rails-viewcomponent',
|
||||||
|
file: 'app/components/button_component.rb',
|
||||||
|
controller: 'ButtonComponent'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract Rails template from comments', () => {
|
||||||
|
document.head.innerHTML = '<meta name="csrf-token" content="abc123">';
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = '<!-- BEGIN app/views/posts/show.html.erb --><p>Content</p>';
|
||||||
|
|
||||||
|
const result = detector.detectRails(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'rails',
|
||||||
|
template: 'app/views/posts/show.html.erb',
|
||||||
|
controller: 'PostsController',
|
||||||
|
action: 'show'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Django detection', () => {
|
||||||
|
it('should detect Django Debug Toolbar', () => {
|
||||||
|
const debugEl = document.createElement('div');
|
||||||
|
debugEl.id = 'djDebug';
|
||||||
|
document.body.appendChild(debugEl);
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
|
||||||
|
const result = detector.detectDjango(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'django'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Django templates from comments', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = '<!-- TEMPLATE_DEBUG: templates/blog/post_detail.html --><p>Content</p>';
|
||||||
|
|
||||||
|
const result = detector.detectDjango(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'django',
|
||||||
|
template: 'blog/post_detail.html'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Django CMS plugins', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.setAttribute('data-cms-plugin', 'TextPlugin');
|
||||||
|
|
||||||
|
const result = detector.detectDjango(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'django',
|
||||||
|
controller: 'TextPlugin'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Flask detection', () => {
|
||||||
|
it('should detect Flask debug mode', () => {
|
||||||
|
const debugEl = document.createElement('div');
|
||||||
|
debugEl.className = 'flask-debugger';
|
||||||
|
document.body.appendChild(debugEl);
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
|
||||||
|
const result = detector.detectFlask(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'flask'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Werkzeug debugger', () => {
|
||||||
|
const debugEl = document.createElement('div');
|
||||||
|
debugEl.id = 'werkzeug-debugger';
|
||||||
|
document.body.appendChild(debugEl);
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
|
||||||
|
const result = detector.detectFlask(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'flask'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Jinja2 templates', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = '<!-- Template: templates/user/profile.html --><p>{{ user.name }}</p>';
|
||||||
|
|
||||||
|
const result = detector.detectFlask(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'flask',
|
||||||
|
template: 'templates/user/profile.html'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Laravel detection', () => {
|
||||||
|
it('should detect Laravel via CSRF and Blade templates', () => {
|
||||||
|
document.head.innerHTML = '<meta name="csrf-token" content="laravel-token">';
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = '<!-- resources/views/posts/index.blade.php --><p>Content</p>';
|
||||||
|
|
||||||
|
const result = detector.detectLaravel(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'laravel',
|
||||||
|
template: 'posts/index.blade.php'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect PHP Debugbar', () => {
|
||||||
|
const debugbar = document.createElement('div');
|
||||||
|
debugbar.className = 'phpdebugbar';
|
||||||
|
document.body.appendChild(debugbar);
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
|
||||||
|
const result = detector.detectLaravel(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'laravel'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Phoenix/LiveView detection', () => {
|
||||||
|
it('should detect Phoenix LiveView', () => {
|
||||||
|
const main = document.createElement('div');
|
||||||
|
main.setAttribute('data-phx-main', 'true');
|
||||||
|
document.body.appendChild(main);
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.setAttribute('data-phx-session', 'session-id');
|
||||||
|
element.setAttribute('data-phx-view', 'UserLive.Index');
|
||||||
|
|
||||||
|
const result = detector.detectPhoenix(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'phoenix',
|
||||||
|
controller: 'UserLive.Index'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Phoenix components', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.setAttribute('data-phx-component', 'MyAppWeb.ButtonComponent');
|
||||||
|
element.setAttribute('data-phx-static', 'static-id');
|
||||||
|
|
||||||
|
const result = detector.detectPhoenix(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'phoenix',
|
||||||
|
controller: 'MyAppWeb.ButtonComponent'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ASP.NET detection', () => {
|
||||||
|
it('should detect ASP.NET tag helpers', () => {
|
||||||
|
const element = document.createElement('a');
|
||||||
|
element.setAttribute('asp-controller', 'Home');
|
||||||
|
element.setAttribute('asp-action', 'Index');
|
||||||
|
|
||||||
|
const result = detector.detectASPNET(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'aspnet',
|
||||||
|
controller: 'Home',
|
||||||
|
action: 'Index'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Blazor components', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.setAttribute('b-component', 'Counter');
|
||||||
|
|
||||||
|
const result = detector.detectASPNET(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'blazor',
|
||||||
|
controller: 'Counter'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Razor pages', () => {
|
||||||
|
const element = document.createElement('form');
|
||||||
|
element.setAttribute('asp-page', '/Users/Edit');
|
||||||
|
|
||||||
|
const result = detector.detectASPNET(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'aspnet',
|
||||||
|
template: '/Users/Edit'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Express.js detection', () => {
|
||||||
|
it('should detect Pug templates', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = '<!-- views/layout.pug --><p>Content</p>';
|
||||||
|
|
||||||
|
const result = detector.detectExpress(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'express',
|
||||||
|
template: 'layout.pug'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect EJS templates', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = '<!-- views/users/profile.ejs --><p><%= user.name %></p>';
|
||||||
|
|
||||||
|
const result = detector.detectExpress(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'express',
|
||||||
|
template: 'users/profile.ejs'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Handlebars templates', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = '<!-- views/partials/header.hbs --><p>{{title}}</p>';
|
||||||
|
|
||||||
|
const result = detector.detectExpress(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'express',
|
||||||
|
template: 'partials/header.hbs'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WordPress detection', () => {
|
||||||
|
it('should detect WordPress via generator meta tag', () => {
|
||||||
|
document.head.innerHTML = '<meta name="generator" content="WordPress 6.0">';
|
||||||
|
const element = document.createElement('div');
|
||||||
|
|
||||||
|
const result = detector.detectWordPress(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'wordpress'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect WordPress admin bar', () => {
|
||||||
|
const adminBar = document.createElement('div');
|
||||||
|
adminBar.id = 'wpadminbar';
|
||||||
|
document.body.appendChild(adminBar);
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
|
||||||
|
const result = detector.detectWordPress(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'wordpress'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract WordPress template info', () => {
|
||||||
|
document.head.innerHTML = '<meta name="generator" content="WordPress">';
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = '<!-- Template: single-post.php --><article>Content</article>';
|
||||||
|
|
||||||
|
const result = detector.detectWordPress(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'wordpress',
|
||||||
|
template: 'single-post.php'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Main detect method', () => {
|
||||||
|
it('should try all detectors and return first match', () => {
|
||||||
|
// Setup Rails
|
||||||
|
document.head.innerHTML = '<meta name="csrf-token" content="abc123">';
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.setAttribute('data-controller', 'posts');
|
||||||
|
|
||||||
|
const result = detector.detect(element);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
framework: 'rails',
|
||||||
|
controller: 'posts'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null if no framework detected', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
|
||||||
|
const result = detector.detect(element);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple frameworks (first wins)', () => {
|
||||||
|
// Setup both Rails and Django markers
|
||||||
|
document.head.innerHTML = '<meta name="csrf-token" content="abc123">';
|
||||||
|
const djangoDebug = document.createElement('div');
|
||||||
|
djangoDebug.id = 'djDebug';
|
||||||
|
document.body.appendChild(djangoDebug);
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.setAttribute('data-controller', 'posts');
|
||||||
|
|
||||||
|
const result = detector.detect(element);
|
||||||
|
|
||||||
|
// Rails is checked first, so it should win
|
||||||
|
expect(result?.framework).toBe('rails');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Helper methods', () => {
|
||||||
|
it('should extract HTML comments from element tree', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.innerHTML = `
|
||||||
|
<!-- First comment -->
|
||||||
|
<p>Content</p>
|
||||||
|
<!-- Second comment -->
|
||||||
|
<div>
|
||||||
|
<!-- Nested comment -->
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const comments = detector['extractHTMLComments'](element);
|
||||||
|
|
||||||
|
expect(comments).toContain('First comment');
|
||||||
|
expect(comments).toContain('Second comment');
|
||||||
|
expect(comments).toContain('Nested comment');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract Rails controller name from template path', () => {
|
||||||
|
const controller = detector['extractRailsController']('app/views/admin/users/index.html.erb');
|
||||||
|
expect(controller).toBe('AdminUsersController');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract Rails action name from template path', () => {
|
||||||
|
const action = detector['extractRailsAction']('app/views/posts/show.html.erb');
|
||||||
|
expect(action).toBe('show');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { BrowserExtensionServer } from '../../mcp-tools/browser-extension-server';
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
describe('BrowserExtensionServer Coverage', () => {
|
||||||
|
let server: BrowserExtensionServer;
|
||||||
|
let ws: WebSocket;
|
||||||
|
const TEST_PORT = 3003;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
server = new BrowserExtensionServer(TEST_PORT, process.cwd());
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
ws = new WebSocket(`ws://localhost:${TEST_PORT}/browser-extension`);
|
||||||
|
await new Promise(resolve => ws.on('open', resolve));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
ws.close();
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge cases', () => {
|
||||||
|
it('should handle file paths that cannot be resolved', async () => {
|
||||||
|
const elementSelected = new Promise((resolve) => {
|
||||||
|
server.on('elementSelected', resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send event with unresolvable path
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
event: 'elementSelected',
|
||||||
|
framework: 'unknown',
|
||||||
|
domPath: 'div > span',
|
||||||
|
source: {
|
||||||
|
file: '/non/existent/path/component.js',
|
||||||
|
line: 1
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await elementSelected;
|
||||||
|
|
||||||
|
// Should still return the path even if it doesn't exist
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
file: '/non/existent/path/component.js',
|
||||||
|
line: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle lookupByHanzoId when file does not exist', async () => {
|
||||||
|
const elementSelected = new Promise((resolve, reject) => {
|
||||||
|
let timeout = setTimeout(() => {
|
||||||
|
resolve(null);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
server.on('elementSelected', (data) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send event with only fallbackId
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
event: 'elementSelected',
|
||||||
|
framework: null,
|
||||||
|
domPath: 'body > div',
|
||||||
|
fallbackId: 'hanzo-xyz789'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await elementSelected;
|
||||||
|
|
||||||
|
// Should return null when ID not found
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error reading ID map file', async () => {
|
||||||
|
// Create .hanzo directory but make map file unreadable
|
||||||
|
const hanzoDir = path.join(process.cwd(), '.hanzo');
|
||||||
|
const mapFile = path.join(hanzoDir, 'id-map.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(hanzoDir)) {
|
||||||
|
fs.mkdirSync(hanzoDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write invalid JSON
|
||||||
|
fs.writeFileSync(mapFile, 'invalid json content');
|
||||||
|
|
||||||
|
const elementSelected = new Promise((resolve, reject) => {
|
||||||
|
let timeout = setTimeout(() => {
|
||||||
|
resolve(null);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
server.on('elementSelected', (data) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
event: 'elementSelected',
|
||||||
|
framework: null,
|
||||||
|
domPath: 'body > div',
|
||||||
|
fallbackId: 'hanzo-error123'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await elementSelected;
|
||||||
|
expect(result).toBeNull();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
if (fs.existsSync(mapFile)) {
|
||||||
|
fs.unlinkSync(mapFile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Executable
+70
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Direct test of MCP server communication
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
console.log('Testing MCP server directly...\n');
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
const server = spawn('npx', ['-y', '@hanzo/mcp@latest', '--anon'], {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
HANZO_WORKSPACE: '/Users/z/work/hanzo',
|
||||||
|
MCP_TRANSPORT: 'stdio'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
server.stdout.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
console.log('STDOUT:', data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
server.stderr.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
console.log('STDERR:', data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('error', (err) => {
|
||||||
|
console.error('Failed to start server:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send MCP initialization after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('\nSending MCP initialization...');
|
||||||
|
const init = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1,
|
||||||
|
method: 'initialize',
|
||||||
|
params: {
|
||||||
|
protocolVersion: '0.1.0',
|
||||||
|
capabilities: {},
|
||||||
|
clientInfo: {
|
||||||
|
name: 'test-client',
|
||||||
|
version: '1.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
server.stdin.write(JSON.stringify(init) + '\n');
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Check results after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('\n=== Results ===');
|
||||||
|
console.log('Process still running:', !server.killed);
|
||||||
|
console.log('Exit code:', server.exitCode);
|
||||||
|
|
||||||
|
if (stdout.includes('"method":"tools/register"')) {
|
||||||
|
console.log('✅ Server is registering tools correctly');
|
||||||
|
} else {
|
||||||
|
console.log('❌ No tool registration detected');
|
||||||
|
}
|
||||||
|
|
||||||
|
server.kill();
|
||||||
|
process.exit(0);
|
||||||
|
}, 3000);
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Run browser extension tests only
|
||||||
|
npx vitest run src/test/browser-extension/*.test.ts --coverage
|
||||||
Executable
+74
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
console.log('Testing Hanzo App integration with @hanzo/mcp...\n');
|
||||||
|
|
||||||
|
async function testHanzoIntegration() {
|
||||||
|
const apiKey = process.env.HANZO_API_KEY;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
console.log('❌ HANZO_API_KEY environment variable not set');
|
||||||
|
console.log('Please set your Hanzo API key or run: hanzo login');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Hanzo API key found');
|
||||||
|
|
||||||
|
// Test 1: Basic API connectivity
|
||||||
|
try {
|
||||||
|
console.log('\n1. Testing API connectivity...');
|
||||||
|
const response = await axios.get('https://api.hanzo.ai/ext/v1/health', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('✅ API health check passed:', response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('❌ API health check failed:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: MCP server availability
|
||||||
|
try {
|
||||||
|
console.log('\n2. Testing MCP server availability...');
|
||||||
|
const response = await axios.get('https://api.hanzo.ai/mcp/v1/servers', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('✅ Available MCP servers:', response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('❌ MCP server list failed:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: LLM router integration
|
||||||
|
try {
|
||||||
|
console.log('\n3. Testing LLM router integration...');
|
||||||
|
const response = await axios.post('https://api.hanzo.ai/ext/v1/llm/models',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log('✅ Available LLM models:', response.data.models?.length || 0, 'models');
|
||||||
|
} catch (error) {
|
||||||
|
console.log('❌ LLM router test failed:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: Local vs Cloud MCP
|
||||||
|
console.log('\n4. Testing local-first MCP approach...');
|
||||||
|
console.log('- Local MCP tools work without API key');
|
||||||
|
console.log('- Cloud MCP provides access to 4000+ servers');
|
||||||
|
console.log('- Hanzo Zen enables 90% cost reduction for local AI');
|
||||||
|
|
||||||
|
console.log('\n✅ Integration test complete!');
|
||||||
|
console.log('\nTo use @hanzo/mcp with Hanzo App:');
|
||||||
|
console.log('1. Install: npx @hanzo/mcp');
|
||||||
|
console.log('2. Configure: Set HANZO_API_KEY or run hanzo login');
|
||||||
|
console.log('3. Use: Access 200+ LLMs and 4000+ MCP servers');
|
||||||
|
}
|
||||||
|
|
||||||
|
testHanzoIntegration().catch(console.error);
|
||||||
Executable
+84
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
console.log('Testing @hanzo/mcp parallel agent functionality...\n');
|
||||||
|
|
||||||
|
// Test configuration
|
||||||
|
const testConfig = {
|
||||||
|
task: "Analyze the package.json file and create a summary",
|
||||||
|
agents: [
|
||||||
|
{
|
||||||
|
name: "FileAnalyzer",
|
||||||
|
role: "Analyze file structure and dependencies",
|
||||||
|
tools: ["read", "grep", "search"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DocWriter",
|
||||||
|
role: "Create documentation based on analysis",
|
||||||
|
tools: ["write", "edit"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Reviewer",
|
||||||
|
role: "Review the analysis and documentation",
|
||||||
|
tools: ["read", "think", "critic"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
parallel: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spawn the MCP server process
|
||||||
|
const serverPath = path.join(__dirname, '..', 'dist', 'npm', 'server.js');
|
||||||
|
const mcpServer = spawn('node', [serverPath], {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
env: { ...process.env, HANZO_DEBUG: 'true' }
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
let errorOutput = '';
|
||||||
|
|
||||||
|
mcpServer.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
console.log('Server:', data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
mcpServer.stderr.on('data', (data) => {
|
||||||
|
errorOutput += data.toString();
|
||||||
|
console.error('Error:', data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
mcpServer.on('close', (code) => {
|
||||||
|
console.log(`\nMCP server exited with code ${code}`);
|
||||||
|
|
||||||
|
if (code === 0) {
|
||||||
|
console.log('✅ Test passed - MCP server started successfully');
|
||||||
|
} else {
|
||||||
|
console.log('❌ Test failed - MCP server exited with error');
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(code);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send test request after server starts
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('\nSending parallel agent test request...');
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1,
|
||||||
|
method: 'tools/call',
|
||||||
|
params: {
|
||||||
|
name: 'agent',
|
||||||
|
arguments: testConfig
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mcpServer.stdin.write(JSON.stringify(request) + '\n');
|
||||||
|
|
||||||
|
// Give it time to process
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('\nTest complete. Shutting down server...');
|
||||||
|
mcpServer.kill();
|
||||||
|
}, 5000);
|
||||||
|
}, 2000);
|
||||||
+3
-1
@@ -10,7 +10,9 @@ export default defineConfig({
|
|||||||
reporter: ['text', 'json', 'html', 'lcov'],
|
reporter: ['text', 'json', 'html', 'lcov'],
|
||||||
include: [
|
include: [
|
||||||
'src/cli-tools/**/*.ts',
|
'src/cli-tools/**/*.ts',
|
||||||
'src/cli/**/*.ts'
|
'src/cli/**/*.ts',
|
||||||
|
'src/browser-extension/**/*.ts',
|
||||||
|
'src/mcp-tools/**/*.ts'
|
||||||
],
|
],
|
||||||
exclude: [
|
exclude: [
|
||||||
'**/*.test.ts',
|
'**/*.test.ts',
|
||||||
|
|||||||
Reference in New Issue
Block a user