Add browser extension, docs, mcp server updates

This commit is contained in:
Hanzo Dev
2025-07-15 17:15:43 -05:00
parent f6eeaacd4f
commit 11416c6930
68 changed files with 4374 additions and 17921 deletions
+32
View File
@@ -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 -2
View File
@@ -1,5 +1,4 @@
Copyright 2025 Hanzo AI
Copyright 2024 Hanzo AI
Copyright 2025 Hanzo Industries Inc
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:
-13
View File
@@ -1,13 +0,0 @@
{
"mcpServers": {
"hanzo": {
"command": "node",
"args": [
"/path/to/hanzo/extension/dist/mcp-server.js"
],
"env": {
"HANZO_WORKSPACE": "/path/to/your/workspace"
}
}
}
}
View File
View File
View File
View File
View File
-17853
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -2,7 +2,7 @@
"name": "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.",
"version": "1.5.4",
"version": "1.5.7",
"publisher": "hanzo-ai",
"private": false,
"license": "MIT",
@@ -295,6 +295,7 @@
"@types/better-sqlite3": "^7.6.13",
"@types/inquirer": "^9.0.8",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^21.1.7",
"@types/lodash": "^4.17.16",
"@types/minimatch": "^5.1.2",
"@types/mocha": "^10.0.10",
@@ -308,7 +309,8 @@
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.21.0",
"@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",
"archiver": "^7.0.1",
"better-sqlite3": "^12.2.0",
@@ -317,6 +319,7 @@
"esbuild": "^0.25.6",
"eslint": "^8.26.0",
"glob": "^10.3.10",
"jsdom": "^26.1.0",
"mocha": "^11.0.1",
"node-fetch": "^3.3.2",
"sinon": "^21.0.0",
+1 -1
View File
@@ -8,7 +8,7 @@
"hanzo-dev": "./dist/cli/dev.js"
},
"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",
"test": "jest",
"prepublishOnly": "npm run build"
+36
View File
@@ -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();
+1 -1
View File
@@ -4,7 +4,7 @@
"description": "Hanzo MCP Server - Model Context Protocol tools for AI development",
"main": "dist/index.js",
"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",
"test": "jest",
"prepublishOnly": "npm run build"
+11
View File
@@ -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')
};
}
+17 -7
View File
@@ -34,6 +34,10 @@ const packageJson = {
scripts: {
start: 'node server.js'
},
dependencies: {
'@modelcontextprotocol/sdk': '^1.0.0',
'zod': '^3.22.0'
},
keywords: [
'mcp',
'model-context-protocol',
@@ -126,7 +130,7 @@ if (command === 'install' || (!command && !args.includes('--help')) || args.incl
// Add Hanzo MCP
config.mcpServers['hanzo-mcp'] = {
command: 'node',
args: [path.join(__dirname, 'server.js')],
args: [path.join(__dirname, 'server.js'), '--anon'],
env: {}
};
@@ -157,15 +161,19 @@ if (command === 'install' || (!command && !args.includes('--help')) || args.incl
}
} else if (command === 'start') {
// Start the MCP server directly
const server = spawn('node', [path.join(__dirname, 'server.js')], {
// Start the MCP server directly (default to anonymous mode)
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'
});
server.on('error', (err) => {
console.error('Failed to start server:', err);
process.exit(1);
});
} else if (args.length === 0 || args[0] === '--anon' || args[0] === 'start') {
// When called without arguments or with start/--anon (as MCP server), run directly
require(path.join(__dirname, 'server.js'));
} else {
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
For Claude Code, install the DXT extension or run:
+12 -4
View File
@@ -36,7 +36,15 @@ const mockVscode = {
showInformationMessage: console.log,
visibleTextEditors: [],
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: {
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({
entryPoints: ['src/mcp-server-standalone-auth.ts'],
entryPoints: ['src/mcp-server-fixed.ts'],
bundle: true,
platform: 'node',
target: 'node18',
outfile: 'dist/mcp-server.js',
external: [],
external: ['@modelcontextprotocol/sdk', 'zod'],
loader: { '.ts': 'ts' },
plugins: [{
name: 'vscode-mock',
-11
View File
@@ -1,11 +0,0 @@
function testFunction() {
console.log('This is a test');
return 42;
}
class TestClass {
constructor() {
this.value = 'test';
}
}
+4
View File
@@ -0,0 +1,4 @@
*.ts
!*.d.ts
tsconfig.json
build.js
+125
View File
@@ -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 };
+231
View File
@@ -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);
}
}
}
+84 -3
View File
@@ -8,8 +8,11 @@ const { execSync } = require('child_process');
async function build() {
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/chrome', { recursive: true });
fs.mkdirSync('dist/browser-extension/firefox', { recursive: true });
fs.mkdirSync('dist/browser-extension/safari', { recursive: true });
// Build content script
await esbuild.build({
@@ -17,10 +20,41 @@ async function build() {
bundle: true,
outfile: 'dist/browser-extension/content-script.js',
platform: 'browser',
target: 'chrome90',
target: ['chrome90', 'firefox91', 'safari14'],
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)
await esbuild.build({
entryPoints: ['src/browser-extension/cli.ts'],
@@ -43,12 +77,59 @@ async function build() {
console.log('Building TypeScript declarations...');
execSync('cd src/browser-extension && npx tsc', { stdio: 'inherit' });
// Copy manifest
// Copy manifests for different browsers
fs.copyFileSync(
'src/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
const pkg = JSON.parse(fs.readFileSync('src/browser-extension/package.json', 'utf8'));
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"
}
}
}
+18 -4
View File
@@ -2,14 +2,18 @@
"manifest_version": 3,
"name": "Hanzo AI Dev Assistant",
"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": [
"activeTab",
"storage"
"storage",
"tabs",
"webNavigation",
"scripting"
],
"host_permissions": [
"http://localhost/*",
"https://localhost/*"
"https://localhost/*",
"<all_urls>"
],
"content_scripts": [
{
@@ -18,6 +22,10 @@
"run_at": "document_idle"
}
],
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
@@ -25,5 +33,11 @@
"48": "icon48.png",
"128": "icon128.png"
}
}
},
"web_accessible_resources": [
{
"resources": ["ai-worker.js", "models/*"],
"matches": ["<all_urls>"]
}
]
}
+9 -9
View File
@@ -1,20 +1,20 @@
{
"name": "@hanzoai/browser-devtools",
"name": "@hanzo/browser-extension",
"version": "1.0.0",
"description": "Browser extension for click-to-code navigation with MCP integration",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"main": "cli.js",
"bin": {
"hanzo-browser-server": "./dist/cli.js"
"hanzo-browser-server": "./cli.js"
},
"scripts": {
"build": "tsc && node build.js",
"prepublishOnly": "npm run build",
"start": "node dist/cli.js"
"start": "node cli.js"
},
"files": [
"dist/",
"extension/"
"cli.js",
"browser-extension-server.js",
"content-script.js",
"manifest.json",
"README.md"
],
"keywords": [
"mcp",
+381
View File
@@ -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;
}
+183
View File
@@ -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>
+432
View File
@@ -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;
}
}
+522
View File
@@ -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;
}
+212
View File
@@ -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>
+490
View File
@@ -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();
+207
View File
@@ -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;
}
}
+202
View File
@@ -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);
});
+203
View File
@@ -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 WebSocket from 'ws';
import * as path from 'path';
import * as fs from 'fs';
describe('Claude Code Browser Extension Integration', () => {
let server: BrowserExtensionServer;
@@ -106,16 +107,24 @@ describe('Claude Code Browser Extension Integration', () => {
describe('Fallback to data-hanzo-id', () => {
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) => {
server.on('elementSelected', (data) => {
// Mock the ID lookup
if (data.fallbackId === 'hanzo-abc123') {
resolve({
file: path.join(process.cwd(), 'src/legacy/OldComponent.js'),
line: 55
});
}
});
server.once('elementSelected', resolve);
});
ws.send(JSON.stringify({
@@ -131,6 +140,11 @@ describe('Claude Code Browser Extension Integration', () => {
file: expect.stringContaining('OldComponent.js'),
line: 55
});
// Cleanup
if (fs.existsSync(mapFile)) {
fs.unlinkSync(mapFile);
}
});
});
@@ -145,7 +159,7 @@ describe('Claude Code Browser Extension Integration', () => {
file: data.file,
line: data.line,
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);
}
});
});
});
+70
View File
@@ -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);
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Run browser extension tests only
npx vitest run src/test/browser-extension/*.test.ts --coverage
+74
View File
@@ -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);
View File
+84
View File
@@ -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
View File
@@ -10,7 +10,9 @@ export default defineConfig({
reporter: ['text', 'json', 'html', 'lcov'],
include: [
'src/cli-tools/**/*.ts',
'src/cli/**/*.ts'
'src/cli/**/*.ts',
'src/browser-extension/**/*.ts',
'src/mcp-tools/**/*.ts'
],
exclude: [
'**/*.test.ts',