Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b883a23cfd | ||
|
|
fb68636da9 | ||
|
|
d2a8547910 | ||
|
|
d9d506e707 | ||
|
|
04a42286c8 | ||
|
|
b4e34b2849 | ||
|
|
da91976f3e | ||
|
|
e6b369a5d2 | ||
|
|
6b31b22d0d | ||
|
|
21e012047b | ||
|
|
6b8431a09b | ||
|
|
114d3c92d8 | ||
|
|
966ec0fd60 |
@@ -257,7 +257,8 @@ jobs:
|
||||
- name: Collect release assets
|
||||
run: |
|
||||
mkdir -p release
|
||||
find artifacts -type f \( -name '*.zip' -o -name '*.vsix' -o -name '*.dxt' \) -exec cp {} release/ \;
|
||||
# Only collect artifacts with our expected naming patterns (prevents stale/duplicate files)
|
||||
find artifacts -type f \( -name 'hanzo-ai-*.zip' -o -name 'hanzo-ai-*.vsix' -o -name 'hanzoai-*.dxt' \) -exec cp {} release/ \;
|
||||
echo "Release assets:" && ls -la release/
|
||||
|
||||
- name: Create Release
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hanzo-ai-monorepo",
|
||||
"version": "1.7.12",
|
||||
"version": "1.7.17",
|
||||
"private": true,
|
||||
"description": "Hanzo AI Development Platform Monorepo",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -34,7 +34,7 @@ descFn('Auth + Chat', () => {
|
||||
await expect(loginPrompt).toBeVisible();
|
||||
|
||||
// Click "Sign in" — this sends auth.login to the background,
|
||||
// which opens a new tab to hanzo.id/login/oauth/authorize
|
||||
// which opens a new tab to hanzo.id/oauth/authorize
|
||||
const [authPage] = await Promise.all([
|
||||
context.waitForEvent('page'),
|
||||
sidebar.locator('#auth-btn').click(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hanzo/browser-extension",
|
||||
"version": "1.7.12",
|
||||
"version": "1.7.14",
|
||||
"description": "Hanzo AI Browser Extension",
|
||||
"main": "dist/background.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -137,7 +137,7 @@ export async function login(): Promise<UserInfo> {
|
||||
const redirectUri = getRedirectUri();
|
||||
|
||||
// Authorization Code + PKCE (OAuth 2.1 standard for public clients)
|
||||
const authorizeUrl = new URL(`${IAM_LOGIN}/login/oauth/authorize`);
|
||||
const authorizeUrl = new URL(`${IAM_LOGIN}/oauth/authorize`);
|
||||
authorizeUrl.searchParams.set('client_id', CLIENT_ID);
|
||||
authorizeUrl.searchParams.set('response_type', 'code');
|
||||
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
||||
@@ -168,7 +168,7 @@ export async function login(): Promise<UserInfo> {
|
||||
await storeTokens(tokens);
|
||||
} else if (code) {
|
||||
// Authorization code flow — exchange code for tokens via PKCE
|
||||
const tokenResponse = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
|
||||
const tokenResponse = await fetch(`${IAM_API}/oauth/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -309,7 +309,7 @@ export async function getValidAccessToken(): Promise<string | null> {
|
||||
* Refresh access token using refresh_token grant.
|
||||
*/
|
||||
async function refreshAccessToken(refreshToken: string): Promise<string> {
|
||||
const response = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
|
||||
const response = await fetch(`${IAM_API}/oauth/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -527,7 +527,7 @@ async function firefoxLogin(): Promise<any> {
|
||||
const state = generateRandomString(32);
|
||||
const redirectUri = 'https://hanzo.ai/callback';
|
||||
|
||||
const authorizeUrl = new URL(`${IAM_LOGIN}/login/oauth/authorize`);
|
||||
const authorizeUrl = new URL(`${IAM_LOGIN}/oauth/authorize`);
|
||||
authorizeUrl.searchParams.set('client_id', CLIENT_ID);
|
||||
authorizeUrl.searchParams.set('response_type', 'code');
|
||||
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
||||
@@ -582,7 +582,7 @@ async function firefoxLogin(): Promise<any> {
|
||||
await browser.storage.local.set(data);
|
||||
} else if (code) {
|
||||
// Authorization code flow — exchange code for tokens via PKCE
|
||||
const tokenResponse = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
|
||||
const tokenResponse = await fetch(`${IAM_API}/oauth/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -423,6 +423,425 @@ export class BrowserControl {
|
||||
`);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// DOM Read/Write/Observe
|
||||
// ===========================================================================
|
||||
|
||||
async getHTML(tabId: number, selector: string, outer: boolean = true): Promise<string | null> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
return el ? (${outer} ? el.outerHTML : el.innerHTML) : null;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async setHTML(tabId: number, selector: string, html: string, outer: boolean = false): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!el) return false;
|
||||
${outer ? `el.outerHTML = ${JSON.stringify(html)};` : `el.innerHTML = ${JSON.stringify(html)};`}
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async getText(tabId: number, selector: string): Promise<string | null> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
return el ? el.textContent : null;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async setText(tabId: number, selector: string, text: string): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!el) return false;
|
||||
el.textContent = ${JSON.stringify(text)};
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async getAttribute(tabId: number, selector: string, attr: string): Promise<string | null> {
|
||||
return this.executeScript(tabId, `
|
||||
document.querySelector(${JSON.stringify(selector)})?.getAttribute(${JSON.stringify(attr)}) ?? null
|
||||
`);
|
||||
}
|
||||
|
||||
async setAttribute(tabId: number, selector: string, attr: string, value: string): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!el) return false;
|
||||
el.setAttribute(${JSON.stringify(attr)}, ${JSON.stringify(value)});
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async removeAttribute(tabId: number, selector: string, attr: string): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!el) return false;
|
||||
el.removeAttribute(${JSON.stringify(attr)});
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async setStyle(tabId: number, selector: string, styles: Record<string, string>): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!el) return false;
|
||||
Object.assign(el.style, ${JSON.stringify(styles)});
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async addClass(tabId: number, selector: string, ...classNames: string[]): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!el) return false;
|
||||
el.classList.add(${classNames.map(c => JSON.stringify(c)).join(',')});
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async removeClass(tabId: number, selector: string, ...classNames: string[]): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!el) return false;
|
||||
el.classList.remove(${classNames.map(c => JSON.stringify(c)).join(',')});
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async insertElement(tabId: number, parentSelector: string, html: string, position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend' = 'beforeend'): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const parent = document.querySelector(${JSON.stringify(parentSelector)});
|
||||
if (!parent) return false;
|
||||
parent.insertAdjacentHTML(${JSON.stringify(position)}, ${JSON.stringify(html)});
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async removeElement(tabId: number, selector: string): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!el) return false;
|
||||
el.remove();
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async observeMutations(tabId: number, selector: string, options: { attributes?: boolean; childList?: boolean; characterData?: boolean; subtree?: boolean } = {}, durationMs: number = 5000): Promise<any[]> {
|
||||
const opts = { childList: true, subtree: true, attributes: true, ...options };
|
||||
return this.executeScript(tabId, `
|
||||
new Promise((resolve) => {
|
||||
const target = ${selector === 'document' ? 'document.documentElement' : `document.querySelector(${JSON.stringify(selector)})`};
|
||||
if (!target) { resolve([]); return; }
|
||||
const mutations = [];
|
||||
const observer = new MutationObserver((records) => {
|
||||
for (const r of records) {
|
||||
mutations.push({
|
||||
type: r.type,
|
||||
target: r.target.nodeName + (r.target.id ? '#' + r.target.id : ''),
|
||||
added: r.addedNodes.length,
|
||||
removed: r.removedNodes.length,
|
||||
attribute: r.attributeName || null,
|
||||
oldValue: r.oldValue?.substring(0, 200) || null,
|
||||
});
|
||||
if (mutations.length >= 200) { observer.disconnect(); resolve(mutations); }
|
||||
}
|
||||
});
|
||||
observer.observe(target, ${JSON.stringify(opts)});
|
||||
setTimeout(() => { observer.disconnect(); resolve(mutations); }, ${durationMs});
|
||||
})
|
||||
`);
|
||||
}
|
||||
|
||||
async getComputedStyles(tabId: number, selector: string, properties?: string[]): Promise<Record<string, string> | null> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!el) return null;
|
||||
const cs = window.getComputedStyle(el);
|
||||
${properties ? `return Object.fromEntries(${JSON.stringify(properties)}.map(p => [p, cs.getPropertyValue(p)]));` : `
|
||||
const props = ['display','visibility','opacity','position','width','height','margin','padding',
|
||||
'color','background','font-size','font-weight','border','overflow','z-index','transform'];
|
||||
return Object.fromEntries(props.map(p => [p, cs.getPropertyValue(p)]));`}
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async getBoundingRects(tabId: number, selector: string): Promise<any[]> {
|
||||
return this.executeScript(tabId, `
|
||||
Array.from(document.querySelectorAll(${JSON.stringify(selector)})).slice(0, 50).map(el => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return { tag: el.tagName.toLowerCase(), id: el.id, x: r.x, y: r.y, w: r.width, h: r.height };
|
||||
})
|
||||
`);
|
||||
}
|
||||
|
||||
async injectScript(tabId: number, url: string): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
new Promise((resolve) => {
|
||||
const s = document.createElement('script');
|
||||
s.src = ${JSON.stringify(url)};
|
||||
s.onload = () => resolve(true);
|
||||
s.onerror = () => resolve(false);
|
||||
document.head.appendChild(s);
|
||||
})
|
||||
`);
|
||||
}
|
||||
|
||||
async injectCSS(tabId: number, css: string): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = ${JSON.stringify(css)};
|
||||
document.head.appendChild(style);
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async getLocalStorage(tabId: number, key?: string): Promise<any> {
|
||||
if (key) {
|
||||
return this.executeScript(tabId, `localStorage.getItem(${JSON.stringify(key)})`);
|
||||
}
|
||||
return this.executeScript(tabId, `JSON.parse(JSON.stringify(localStorage))`);
|
||||
}
|
||||
|
||||
async setLocalStorage(tabId: number, key: string, value: string): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => { localStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)}); return true; })()
|
||||
`);
|
||||
}
|
||||
|
||||
async getCookies(tabId: number): Promise<any[]> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.get(tabId, (tab) => {
|
||||
if (!tab?.url) { resolve([]); return; }
|
||||
chrome.cookies.getAll({ url: tab.url }, (cookies) => {
|
||||
resolve(cookies || []);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async setCookie(tabId: number, cookie: { name: string; value: string; domain?: string; path?: string; secure?: boolean; httpOnly?: boolean; expirationDate?: number }): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.get(tabId, (tab) => {
|
||||
if (!tab?.url) { resolve(false); return; }
|
||||
chrome.cookies.set({ url: tab.url, ...cookie }, (c) => resolve(!!c));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async deleteCookie(tabId: number, name: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.get(tabId, (tab) => {
|
||||
if (!tab?.url) { resolve(false); return; }
|
||||
chrome.cookies.remove({ url: tab.url, name }, () => resolve(true));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getSessionStorage(tabId: number, key?: string): Promise<any> {
|
||||
if (key) {
|
||||
return this.executeScript(tabId, `sessionStorage.getItem(${JSON.stringify(key)})`);
|
||||
}
|
||||
return this.executeScript(tabId, `JSON.parse(JSON.stringify(sessionStorage))`);
|
||||
}
|
||||
|
||||
async setSessionStorage(tabId: number, key: string, value: string): Promise<boolean> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => { sessionStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)}); return true; })()
|
||||
`);
|
||||
}
|
||||
|
||||
async getIndexedDBDatabases(tabId: number): Promise<any[]> {
|
||||
return this.executeScript(tabId, `
|
||||
(async () => {
|
||||
const dbs = await indexedDB.databases();
|
||||
return dbs.map(db => ({ name: db.name, version: db.version }));
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async queryIndexedDB(tabId: number, dbName: string, storeName: string, query?: { key?: string; index?: string; count?: number }): Promise<any[]> {
|
||||
const count = query?.count || 100;
|
||||
return this.executeScript(tabId, `
|
||||
new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(${JSON.stringify(dbName)});
|
||||
req.onerror = () => reject(new Error(req.error?.message || 'Failed to open DB'));
|
||||
req.onsuccess = () => {
|
||||
const db = req.result;
|
||||
try {
|
||||
const tx = db.transaction(${JSON.stringify(storeName)}, 'readonly');
|
||||
const store = tx.objectStore(${JSON.stringify(storeName)});
|
||||
const src = ${query?.index ? `store.index(${JSON.stringify(query.index)})` : 'store'};
|
||||
const getReq = ${query?.key ? `src.get(${JSON.stringify(query.key)})` : `src.getAll(null, ${count})`};
|
||||
getReq.onsuccess = () => resolve(${query?.key ? '[getReq.result]' : 'getReq.result'});
|
||||
getReq.onerror = () => reject(new Error(getReq.error?.message || 'Query failed'));
|
||||
} catch(e) { reject(e); }
|
||||
};
|
||||
})
|
||||
`);
|
||||
}
|
||||
|
||||
async getIndexedDBStores(tabId: number, dbName: string): Promise<string[]> {
|
||||
return this.executeScript(tabId, `
|
||||
new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(${JSON.stringify(dbName)});
|
||||
req.onerror = () => reject(new Error(req.error?.message || 'Failed to open DB'));
|
||||
req.onsuccess = () => {
|
||||
resolve(Array.from(req.result.objectStoreNames));
|
||||
};
|
||||
})
|
||||
`);
|
||||
}
|
||||
|
||||
async clearSiteData(tabId: number, what: ('cookies' | 'localStorage' | 'sessionStorage' | 'indexedDB' | 'cache' | 'all')[] = ['all']): Promise<Record<string, boolean>> {
|
||||
const doAll = what.includes('all');
|
||||
const results: Record<string, boolean> = {};
|
||||
|
||||
if (doAll || what.includes('cookies')) {
|
||||
await new Promise<void>((resolve) => {
|
||||
chrome.tabs.get(tabId, (tab) => {
|
||||
if (!tab?.url) { results.cookies = false; resolve(); return; }
|
||||
chrome.cookies.getAll({ url: tab.url }, (cookies) => {
|
||||
Promise.all((cookies || []).map(c =>
|
||||
new Promise<void>(r => chrome.cookies.remove({ url: tab.url!, name: c.name }, () => r()))
|
||||
)).then(() => { results.cookies = true; resolve(); });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (doAll || what.includes('localStorage')) {
|
||||
results.localStorage = await this.executeScript(tabId, `(() => { localStorage.clear(); return true; })()`);
|
||||
}
|
||||
if (doAll || what.includes('sessionStorage')) {
|
||||
results.sessionStorage = await this.executeScript(tabId, `(() => { sessionStorage.clear(); return true; })()`);
|
||||
}
|
||||
if (doAll || what.includes('indexedDB')) {
|
||||
results.indexedDB = await this.executeScript(tabId, `
|
||||
(async () => {
|
||||
const dbs = await indexedDB.databases();
|
||||
await Promise.all(dbs.map(db => new Promise((r, j) => {
|
||||
const req = indexedDB.deleteDatabase(db.name);
|
||||
req.onsuccess = () => r(true);
|
||||
req.onerror = () => r(false);
|
||||
})));
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
if (doAll || what.includes('cache')) {
|
||||
results.cache = await this.executeScript(tabId, `
|
||||
(async () => {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(keys.map(k => caches.delete(k)));
|
||||
return true;
|
||||
})()
|
||||
`);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async getCacheStorageKeys(tabId: number): Promise<string[]> {
|
||||
return this.executeScript(tabId, `caches.keys()`);
|
||||
}
|
||||
|
||||
async checkWebGPU(tabId: number): Promise<{ available: boolean; adapter?: string; features?: string[]; limits?: Record<string, number> }> {
|
||||
return this.executeScript(tabId, `
|
||||
(async () => {
|
||||
if (!navigator.gpu) return { available: false, reason: 'WebGPU not supported' };
|
||||
try {
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
if (!adapter) return { available: false, reason: 'No GPU adapter' };
|
||||
const info = await adapter.requestAdapterInfo();
|
||||
return {
|
||||
available: true,
|
||||
adapter: info.description || info.device || info.vendor || 'Unknown',
|
||||
vendor: info.vendor,
|
||||
architecture: info.architecture,
|
||||
features: [...adapter.features].sort(),
|
||||
limits: {
|
||||
maxBufferSize: adapter.limits.maxBufferSize,
|
||||
maxTextureDimension2D: adapter.limits.maxTextureDimension2D,
|
||||
maxComputeWorkgroupSizeX: adapter.limits.maxComputeWorkgroupSizeX,
|
||||
maxBindGroups: adapter.limits.maxBindGroups,
|
||||
maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize,
|
||||
},
|
||||
};
|
||||
} catch(e) { return { available: false, reason: e.message }; }
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async hardReload(tabId: number): Promise<void> {
|
||||
chrome.tabs.reload(tabId, { bypassCache: true });
|
||||
}
|
||||
|
||||
async getServiceWorkers(tabId: number): Promise<any[]> {
|
||||
return this.executeScript(tabId, `
|
||||
(async () => {
|
||||
const regs = await navigator.serviceWorker?.getRegistrations() || [];
|
||||
return regs.map(r => ({
|
||||
scope: r.scope,
|
||||
active: r.active ? { state: r.active.state, scriptURL: r.active.scriptURL } : null,
|
||||
waiting: r.waiting ? { state: r.waiting.state } : null,
|
||||
installing: r.installing ? { state: r.installing.state } : null,
|
||||
}));
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async getPerformanceMetrics(tabId: number): Promise<any> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const nav = performance.getEntriesByType('navigation')[0] || {};
|
||||
const paint = performance.getEntriesByType('paint');
|
||||
const resources = performance.getEntriesByType('resource');
|
||||
return {
|
||||
timing: {
|
||||
domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.startTime),
|
||||
load: Math.round(nav.loadEventEnd - nav.startTime),
|
||||
ttfb: Math.round(nav.responseStart - nav.startTime),
|
||||
domInteractive: Math.round(nav.domInteractive - nav.startTime),
|
||||
},
|
||||
paint: Object.fromEntries(paint.map(p => [p.name, Math.round(p.startTime)])),
|
||||
resourceCount: resources.length,
|
||||
transferSize: resources.reduce((s, r) => s + (r.transferSize || 0), 0),
|
||||
memory: performance.memory ? {
|
||||
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
||||
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
||||
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
|
||||
} : null,
|
||||
};
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Navigation
|
||||
// ===========================================================================
|
||||
@@ -451,6 +870,85 @@ export class BrowserControl {
|
||||
});
|
||||
}
|
||||
|
||||
async getURL(tabId: number): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.get(tabId, (tab) => resolve(tab?.url || ''));
|
||||
});
|
||||
}
|
||||
|
||||
async getTitle(tabId: number): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.get(tabId, (tab) => resolve(tab?.title || ''));
|
||||
});
|
||||
}
|
||||
|
||||
async getTabInfo(tabId: number): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.get(tabId, (tab) => resolve({
|
||||
id: tab?.id,
|
||||
url: tab?.url,
|
||||
title: tab?.title,
|
||||
status: tab?.status,
|
||||
active: tab?.active,
|
||||
index: tab?.index,
|
||||
pinned: tab?.pinned,
|
||||
incognito: tab?.incognito,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
async getHistory(tabId: number, maxResults: number = 50): Promise<any[]> {
|
||||
return this.executeScript(tabId, `
|
||||
(() => {
|
||||
const entries = performance.getEntriesByType('navigation');
|
||||
return {
|
||||
length: history.length,
|
||||
scrollRestoration: history.scrollRestoration,
|
||||
current: location.href,
|
||||
navigation: entries.map(e => ({
|
||||
type: e.type,
|
||||
redirectCount: e.redirectCount,
|
||||
duration: Math.round(e.duration),
|
||||
})),
|
||||
};
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async browserFetch(tabId: number, url: string, options?: {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
mode?: string;
|
||||
credentials?: string;
|
||||
}): Promise<{ status: number; statusText: string; headers: Record<string, string>; body: string; url: string }> {
|
||||
const opts = {
|
||||
method: options?.method || 'GET',
|
||||
headers: options?.headers || {},
|
||||
body: options?.body,
|
||||
mode: options?.mode || 'cors',
|
||||
credentials: options?.credentials || 'include',
|
||||
};
|
||||
return this.executeScript(tabId, `
|
||||
(async () => {
|
||||
const opts = ${JSON.stringify(opts)};
|
||||
if (!opts.body) delete opts.body;
|
||||
const res = await fetch(${JSON.stringify(url)}, opts);
|
||||
const headers = {};
|
||||
res.headers.forEach((v, k) => headers[k] = v);
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
let body;
|
||||
if (ct.includes('json')) {
|
||||
body = JSON.stringify(await res.json());
|
||||
} else {
|
||||
body = await res.text();
|
||||
}
|
||||
if (body.length > 500000) body = body.substring(0, 500000) + '...(truncated)';
|
||||
return { status: res.status, statusText: res.statusText, headers, body, url: res.url };
|
||||
})()
|
||||
`);
|
||||
}
|
||||
|
||||
async closeTab(tabId: number): Promise<void> {
|
||||
chrome.tabs.remove(tabId);
|
||||
}
|
||||
@@ -692,9 +1190,33 @@ export class BrowserControl {
|
||||
case 'browser.scroll':
|
||||
sendResponse({ success: await this.scroll(tabId, request.x, request.y, request.selector) });
|
||||
break;
|
||||
case 'browser.dblclick':
|
||||
sendResponse({ success: await this.doubleClick(tabId, request.selector) });
|
||||
break;
|
||||
case 'browser.hover':
|
||||
sendResponse({ success: await this.hover(tabId, request.selector) });
|
||||
break;
|
||||
case 'browser.clear':
|
||||
sendResponse({ success: await this.fill(tabId, request.selector, '') });
|
||||
break;
|
||||
case 'browser.check':
|
||||
sendResponse({ success: await this.check(tabId, request.selector, true) });
|
||||
break;
|
||||
case 'browser.uncheck':
|
||||
sendResponse({ success: await this.check(tabId, request.selector, false) });
|
||||
break;
|
||||
case 'browser.focus':
|
||||
sendResponse({ success: await this.focus(tabId, request.selector) });
|
||||
break;
|
||||
case 'browser.blur':
|
||||
sendResponse({ success: await this.blur(tabId, request.selector) });
|
||||
break;
|
||||
case 'browser.drag':
|
||||
sendResponse({ success: await this.drag(tabId, request.from || request.selector, request.to) });
|
||||
break;
|
||||
case 'browser.scrollIntoView':
|
||||
sendResponse({ success: await this.scrollIntoView(tabId, request.selector) });
|
||||
break;
|
||||
case 'browser.pressKey':
|
||||
sendResponse({ success: await this.pressKey(tabId, request.key, request.modifiers) });
|
||||
break;
|
||||
@@ -708,6 +1230,43 @@ export class BrowserControl {
|
||||
await this.navigateTo(tabId, request.url);
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
case 'browser.goBack':
|
||||
await this.goBack(tabId);
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
case 'browser.goForward':
|
||||
await this.goForward(tabId);
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
case 'browser.reload':
|
||||
await this.reload(tabId);
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
case 'browser.getURL':
|
||||
sendResponse({ success: true, url: await this.getURL(tabId) });
|
||||
break;
|
||||
case 'browser.getTitle':
|
||||
sendResponse({ success: true, title: await this.getTitle(tabId) });
|
||||
break;
|
||||
case 'browser.getTabInfo':
|
||||
sendResponse({ success: true, info: await this.getTabInfo(tabId) });
|
||||
break;
|
||||
case 'browser.waitForNavigation':
|
||||
sendResponse({ success: await this.waitForNavigation(tabId, request.timeout) });
|
||||
break;
|
||||
case 'browser.getHistory':
|
||||
sendResponse({ success: true, history: await this.getHistory(tabId, request.maxResults) });
|
||||
break;
|
||||
case 'browser.fetch':
|
||||
sendResponse({ success: true, response: await this.browserFetch(tabId, request.url, request.options) });
|
||||
break;
|
||||
case 'browser.createTab':
|
||||
sendResponse({ success: true, tabId: await this.createTab(request.url || 'about:blank', request.active !== false) });
|
||||
break;
|
||||
case 'browser.closeTab':
|
||||
await this.closeTab(tabId);
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
case 'browser.waitForSelector':
|
||||
sendResponse({ success: await this.waitForSelector(tabId, request.selector, request.timeout) });
|
||||
break;
|
||||
@@ -720,6 +1279,66 @@ export class BrowserControl {
|
||||
case 'browser.querySelectorAll':
|
||||
sendResponse({ success: true, elements: await this.querySelectorAll(tabId, request.selector) });
|
||||
break;
|
||||
case 'browser.getHTML':
|
||||
sendResponse({ success: true, html: await this.getHTML(tabId, request.selector, request.outer !== false) });
|
||||
break;
|
||||
case 'browser.setHTML':
|
||||
sendResponse({ success: await this.setHTML(tabId, request.selector, request.html, request.outer) });
|
||||
break;
|
||||
case 'browser.getText':
|
||||
sendResponse({ success: true, text: await this.getText(tabId, request.selector) });
|
||||
break;
|
||||
case 'browser.setText':
|
||||
sendResponse({ success: await this.setText(tabId, request.selector, request.text) });
|
||||
break;
|
||||
case 'browser.getAttribute':
|
||||
sendResponse({ success: true, value: await this.getAttribute(tabId, request.selector, request.attr) });
|
||||
break;
|
||||
case 'browser.setAttribute':
|
||||
sendResponse({ success: await this.setAttribute(tabId, request.selector, request.attr, request.value) });
|
||||
break;
|
||||
case 'browser.removeAttribute':
|
||||
sendResponse({ success: await this.removeAttribute(tabId, request.selector, request.attr) });
|
||||
break;
|
||||
case 'browser.setStyle':
|
||||
sendResponse({ success: await this.setStyle(tabId, request.selector, request.styles) });
|
||||
break;
|
||||
case 'browser.addClass':
|
||||
sendResponse({ success: await this.addClass(tabId, request.selector, ...(request.classNames || [])) });
|
||||
break;
|
||||
case 'browser.removeClass':
|
||||
sendResponse({ success: await this.removeClass(tabId, request.selector, ...(request.classNames || [])) });
|
||||
break;
|
||||
case 'browser.insertElement':
|
||||
sendResponse({ success: await this.insertElement(tabId, request.selector, request.html, request.position) });
|
||||
break;
|
||||
case 'browser.removeElement':
|
||||
sendResponse({ success: await this.removeElement(tabId, request.selector) });
|
||||
break;
|
||||
case 'browser.observeMutations':
|
||||
sendResponse({ success: true, mutations: await this.observeMutations(tabId, request.selector || 'document', request.options, request.duration) });
|
||||
break;
|
||||
case 'browser.getComputedStyles':
|
||||
sendResponse({ success: true, styles: await this.getComputedStyles(tabId, request.selector, request.properties) });
|
||||
break;
|
||||
case 'browser.getBoundingRects':
|
||||
sendResponse({ success: true, rects: await this.getBoundingRects(tabId, request.selector) });
|
||||
break;
|
||||
case 'browser.injectScript':
|
||||
sendResponse({ success: await this.injectScript(tabId, request.url) });
|
||||
break;
|
||||
case 'browser.injectCSS':
|
||||
sendResponse({ success: await this.injectCSS(tabId, request.css) });
|
||||
break;
|
||||
case 'browser.getLocalStorage':
|
||||
sendResponse({ success: true, data: await this.getLocalStorage(tabId, request.key) });
|
||||
break;
|
||||
case 'browser.setLocalStorage':
|
||||
sendResponse({ success: await this.setLocalStorage(tabId, request.key, request.value) });
|
||||
break;
|
||||
case 'browser.getCookies':
|
||||
sendResponse({ success: true, cookies: await this.getCookies(tabId) });
|
||||
break;
|
||||
default:
|
||||
sendResponse({ success: false, error: `Unknown action: ${request.action}` });
|
||||
}
|
||||
|
||||
@@ -208,17 +208,20 @@ class CDPBridgeServer {
|
||||
});
|
||||
|
||||
// Screenshots
|
||||
case 'screenshot':
|
||||
case 'screenshot': {
|
||||
const screenshot = await this.sendRaw('hanzo.screenshot', {
|
||||
format: rest.format || 'png',
|
||||
fullPage: rest.fullPage
|
||||
fullPage: rest.fullPage,
|
||||
...(rest.tabId ? { tabId: rest.tabId } : {}),
|
||||
...(rest.tabIndex !== undefined ? { tabIndex: rest.tabIndex } : {}),
|
||||
});
|
||||
if (rest.filename && screenshot.data) {
|
||||
if (rest.filename && screenshot?.data) {
|
||||
const buffer = Buffer.from(screenshot.data, 'base64');
|
||||
fs.writeFileSync(rest.filename, buffer);
|
||||
return { saved: rest.filename, bytes: buffer.length };
|
||||
return { saved: rest.filename, bytes: buffer.length, data: screenshot.data };
|
||||
}
|
||||
return screenshot;
|
||||
}
|
||||
|
||||
case 'snapshot':
|
||||
// Accessibility snapshot
|
||||
@@ -283,12 +286,139 @@ class CDPBridgeServer {
|
||||
selector: rest.selector || rest.ref
|
||||
});
|
||||
|
||||
// Evaluate
|
||||
case 'evaluate':
|
||||
return this.sendRaw('Runtime.evaluate', {
|
||||
expression: rest.code || rest.function || rest.expression,
|
||||
returnByValue: true
|
||||
// Navigation
|
||||
case 'go_back':
|
||||
case 'navigate_back':
|
||||
return this.sendRaw('Page.goBack');
|
||||
|
||||
case 'go_forward':
|
||||
case 'navigate_forward':
|
||||
return this.sendRaw('Page.goForward');
|
||||
|
||||
case 'get_url':
|
||||
return this.sendRaw('hanzo.url');
|
||||
|
||||
case 'get_title':
|
||||
return this.sendRaw('hanzo.title');
|
||||
|
||||
case 'get_tab_info':
|
||||
case 'tab_info':
|
||||
return this.sendRaw('hanzo.tabInfo');
|
||||
|
||||
case 'get_history':
|
||||
case 'history':
|
||||
return this.sendRaw('hanzo.getHistory', { maxResults: rest.maxResults });
|
||||
|
||||
case 'wait_for_navigation':
|
||||
return this.sendRaw('hanzo.waitForNavigation', { timeout: rest.timeout });
|
||||
|
||||
case 'create_tab':
|
||||
return this.sendRaw('Target.createTarget', { url: rest.url || 'about:blank' });
|
||||
|
||||
case 'close_tab':
|
||||
return this.sendRaw('Target.closeTarget', { targetId: rest.tabId });
|
||||
|
||||
// Fetch through browser context (uses page cookies/auth)
|
||||
case 'fetch': {
|
||||
const fetchResult = await this.sendRaw('hanzo.fetch', {
|
||||
url: rest.url,
|
||||
options: {
|
||||
method: rest.method || rest.options?.method,
|
||||
headers: rest.headers || rest.options?.headers,
|
||||
body: rest.body || rest.options?.body,
|
||||
mode: rest.mode || rest.options?.mode,
|
||||
credentials: rest.credentials || rest.options?.credentials,
|
||||
},
|
||||
});
|
||||
return fetchResult;
|
||||
}
|
||||
|
||||
// Selectors / Waiting
|
||||
case 'wait_for_selector':
|
||||
return this.sendRaw('hanzo.waitForSelector', { selector: rest.selector, timeout: rest.timeout });
|
||||
|
||||
case 'query_selector_all':
|
||||
return this.sendRaw('hanzo.querySelectorAll', { selector: rest.selector });
|
||||
|
||||
case 'get_element_info':
|
||||
return this.sendRaw('hanzo.getElementInfo', { selector: rest.selector });
|
||||
|
||||
case 'get_page_info':
|
||||
return this.sendRaw('hanzo.getPageInfo', {});
|
||||
|
||||
// DOM Read/Write/Observe
|
||||
case 'get_html':
|
||||
return this.sendRaw('hanzo.getHTML', { selector: rest.selector, outer: rest.outer });
|
||||
|
||||
case 'set_html':
|
||||
return this.sendRaw('hanzo.setHTML', { selector: rest.selector, html: rest.html, outer: rest.outer });
|
||||
|
||||
case 'get_text':
|
||||
return this.sendRaw('hanzo.getText', { selector: rest.selector });
|
||||
|
||||
case 'set_text':
|
||||
return this.sendRaw('hanzo.setText', { selector: rest.selector, text: rest.text });
|
||||
|
||||
case 'get_attribute':
|
||||
return this.sendRaw('hanzo.getAttribute', { selector: rest.selector, attr: rest.attr });
|
||||
|
||||
case 'set_attribute':
|
||||
return this.sendRaw('hanzo.setAttribute', { selector: rest.selector, attr: rest.attr, value: rest.value });
|
||||
|
||||
case 'remove_attribute':
|
||||
return this.sendRaw('hanzo.removeAttribute', { selector: rest.selector, attr: rest.attr });
|
||||
|
||||
case 'set_style':
|
||||
return this.sendRaw('hanzo.setStyle', { selector: rest.selector, styles: rest.styles });
|
||||
|
||||
case 'add_class':
|
||||
return this.sendRaw('hanzo.addClass', { selector: rest.selector, classNames: rest.classNames });
|
||||
|
||||
case 'remove_class':
|
||||
return this.sendRaw('hanzo.removeClass', { selector: rest.selector, classNames: rest.classNames });
|
||||
|
||||
case 'insert_element':
|
||||
return this.sendRaw('hanzo.insertElement', { selector: rest.selector, html: rest.html, position: rest.position });
|
||||
|
||||
case 'remove_element':
|
||||
return this.sendRaw('hanzo.removeElement', { selector: rest.selector });
|
||||
|
||||
case 'observe_mutations':
|
||||
return this.sendRaw('hanzo.observeMutations', { selector: rest.selector, options: rest.options, duration: rest.duration });
|
||||
|
||||
case 'computed_styles':
|
||||
return this.sendRaw('hanzo.getComputedStyles', { selector: rest.selector, properties: rest.properties });
|
||||
|
||||
case 'bounding_rects':
|
||||
return this.sendRaw('hanzo.getBoundingRects', { selector: rest.selector });
|
||||
|
||||
case 'inject_script':
|
||||
return this.sendRaw('hanzo.injectScript', { url: rest.url });
|
||||
|
||||
case 'inject_css':
|
||||
return this.sendRaw('hanzo.injectCSS', { css: rest.css });
|
||||
|
||||
case 'local_storage':
|
||||
if (rest.value !== undefined) {
|
||||
return this.sendRaw('hanzo.setLocalStorage', { key: rest.key, value: rest.value });
|
||||
}
|
||||
return this.sendRaw('hanzo.getLocalStorage', { key: rest.key });
|
||||
|
||||
case 'cookies':
|
||||
return this.sendRaw('hanzo.getCookies', {});
|
||||
|
||||
// Evaluate
|
||||
case 'evaluate': {
|
||||
const evalResult = await this.sendRaw('Runtime.evaluate', {
|
||||
expression: rest.code || rest.function || rest.expression,
|
||||
returnByValue: true,
|
||||
...(rest.tabId ? { tabId: rest.tabId } : {}),
|
||||
...(rest.tabIndex !== undefined ? { tabIndex: rest.tabIndex } : {}),
|
||||
});
|
||||
// Unwrap CDP result envelope: {result: {type, value}} → value
|
||||
const value = evalResult?.result?.value ?? evalResult?.result ?? evalResult;
|
||||
return { result: value };
|
||||
}
|
||||
|
||||
// Wait
|
||||
case 'wait':
|
||||
@@ -358,7 +488,8 @@ async function startJSONRPCServer(bridgeServer: CDPBridgeServer) {
|
||||
const request = JSON.parse(body);
|
||||
const result = await bridgeServer.browser(request.params || request);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ result }));
|
||||
// Return result directly (no extra wrapping — Python expects flat dict)
|
||||
res.end(JSON.stringify(result && typeof result === 'object' ? result : { result }));
|
||||
} catch (error: any) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: error.message }));
|
||||
@@ -377,6 +508,16 @@ async function startJSONRPCServer(bridgeServer: CDPBridgeServer) {
|
||||
'click', 'dblclick', 'hover', 'type', 'fill', 'clear', 'press_key',
|
||||
'select_option', 'check', 'uncheck',
|
||||
'evaluate',
|
||||
'go_back', 'go_forward', 'get_url', 'get_title', 'get_tab_info',
|
||||
'wait_for_navigation', 'get_history', 'create_tab', 'close_tab',
|
||||
'fetch',
|
||||
'get_html', 'set_html', 'get_text', 'set_text',
|
||||
'get_attribute', 'set_attribute', 'remove_attribute',
|
||||
'set_style', 'add_class', 'remove_class',
|
||||
'insert_element', 'remove_element',
|
||||
'observe_mutations', 'computed_styles', 'bounding_rects',
|
||||
'inject_script', 'inject_css',
|
||||
'local_storage', 'cookies',
|
||||
'wait', 'wait_for_load',
|
||||
'tabs', 'new_tab', 'close_tab', 'select_tab',
|
||||
'console', 'network_requests',
|
||||
|
||||
@@ -404,6 +404,104 @@ export class CDPBridge {
|
||||
result = { data: screenshotData };
|
||||
break;
|
||||
|
||||
// Navigation
|
||||
case 'Page.goBack':
|
||||
await chrome.tabs.goBack(tabId);
|
||||
result = { success: true };
|
||||
break;
|
||||
|
||||
case 'Page.goForward':
|
||||
await chrome.tabs.goForward(tabId);
|
||||
result = { success: true };
|
||||
break;
|
||||
|
||||
case 'Page.reload':
|
||||
await chrome.tabs.reload(tabId);
|
||||
result = { success: true };
|
||||
break;
|
||||
|
||||
case 'hanzo.url':
|
||||
result = { result: { value: (await chrome.tabs.get(tabId))?.url || '' } };
|
||||
break;
|
||||
|
||||
case 'hanzo.title':
|
||||
result = { result: { value: (await chrome.tabs.get(tabId))?.title || '' } };
|
||||
break;
|
||||
|
||||
case 'hanzo.tabInfo': {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
result = { id: tab?.id, url: tab?.url, title: tab?.title, status: tab?.status, active: tab?.active, index: tab?.index };
|
||||
break;
|
||||
}
|
||||
|
||||
case 'hanzo.waitForNavigation':
|
||||
result = await new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
chrome.tabs.onUpdated.removeListener(listener);
|
||||
resolve({ success: false });
|
||||
}, params?.timeout || 30000);
|
||||
const listener = (updatedId: number, info: chrome.tabs.TabChangeInfo) => {
|
||||
if (updatedId === tabId && info.status === 'complete') {
|
||||
clearTimeout(timeout);
|
||||
chrome.tabs.onUpdated.removeListener(listener);
|
||||
resolve({ success: true });
|
||||
}
|
||||
};
|
||||
chrome.tabs.onUpdated.addListener(listener);
|
||||
});
|
||||
break;
|
||||
|
||||
// High-level hanzo.* commands — forward to BrowserControl via executeScript
|
||||
// Forward hanzo.* commands to BrowserControl via chrome.runtime
|
||||
// (hanzo.click, hanzo.fill, hanzo.screenshot handled above with overlay)
|
||||
case 'hanzo.dblclick':
|
||||
case 'hanzo.hover':
|
||||
case 'hanzo.type':
|
||||
case 'hanzo.clear':
|
||||
case 'hanzo.select':
|
||||
case 'hanzo.check':
|
||||
case 'hanzo.uncheck':
|
||||
case 'hanzo.waitForSelector':
|
||||
case 'hanzo.querySelectorAll':
|
||||
case 'hanzo.getElementInfo':
|
||||
case 'hanzo.getPageInfo':
|
||||
case 'hanzo.getHTML':
|
||||
case 'hanzo.setHTML':
|
||||
case 'hanzo.getText':
|
||||
case 'hanzo.setText':
|
||||
case 'hanzo.getAttribute':
|
||||
case 'hanzo.setAttribute':
|
||||
case 'hanzo.removeAttribute':
|
||||
case 'hanzo.setStyle':
|
||||
case 'hanzo.addClass':
|
||||
case 'hanzo.removeClass':
|
||||
case 'hanzo.insertElement':
|
||||
case 'hanzo.removeElement':
|
||||
case 'hanzo.observeMutations':
|
||||
case 'hanzo.getComputedStyles':
|
||||
case 'hanzo.getBoundingRects':
|
||||
case 'hanzo.injectScript':
|
||||
case 'hanzo.injectCSS':
|
||||
case 'hanzo.getLocalStorage':
|
||||
case 'hanzo.setLocalStorage':
|
||||
case 'hanzo.getCookies':
|
||||
case 'hanzo.getHistory':
|
||||
case 'hanzo.fetch': {
|
||||
// Map hanzo.X → browser.X and forward via chrome.runtime message
|
||||
const cmd = method.replace('hanzo.', '');
|
||||
// Normalize naming: hanzo.select → browser.select, hanzo.dblclick → browser.dblclick, etc.
|
||||
const actionMap: Record<string, string> = {
|
||||
'url': 'getURL', 'title': 'getTitle', 'tabInfo': 'getTabInfo',
|
||||
};
|
||||
const action = 'browser.' + (actionMap[cmd] || cmd);
|
||||
result = await new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage({ action, tabId, ...params }, (response) => {
|
||||
resolve(response || { error: chrome.runtime.lastError?.message });
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Control overlay management
|
||||
case 'hanzo.control.start':
|
||||
case 'hanzo.takeover.start':
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Hanzo AI",
|
||||
"version": "1.7.12",
|
||||
"version": "1.7.14",
|
||||
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Hanzo AI",
|
||||
"version": "1.7.12",
|
||||
"version": "1.7.16",
|
||||
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
@@ -11,6 +11,7 @@
|
||||
"tabs",
|
||||
"webNavigation",
|
||||
"scripting",
|
||||
"cookies",
|
||||
"debugger"
|
||||
],
|
||||
"host_permissions": [
|
||||
|
||||
@@ -490,141 +490,147 @@ html {
|
||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
/* Chat Input */
|
||||
/* ==================== Composer (Grok-style) ==================== */
|
||||
|
||||
.chat-input-area {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 12px 16px;
|
||||
background: rgba(0,0,0,0.5);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
.composer {
|
||||
padding: 12px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.chat-modern-composer {
|
||||
.composer-box {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.composer-box:focus-within {
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,0.06), 0 4px 16px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.composer-box textarea {
|
||||
width: 100%;
|
||||
padding: 14px 16px 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
resize: none;
|
||||
outline: none;
|
||||
min-height: 44px;
|
||||
max-height: 160px;
|
||||
}
|
||||
|
||||
.composer-box textarea::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.composer-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px 8px 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.model-selector {
|
||||
margin-bottom: 8px;
|
||||
.composer-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.model-selector select {
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
background: var(--bg-tertiary);
|
||||
.composer-model {
|
||||
padding: 3px 22px 3px 8px;
|
||||
background: var(--glass-strong);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 5l3 3 3-3' stroke='%23A0A0A0' stroke-width='1.5'/%3E%3C/svg%3E");
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10' fill='none'%3E%3Cpath d='M2.5 4l2.5 2.5 2.5-2.5' stroke='%236B6B6B' stroke-width='1.2'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
padding-right: 28px;
|
||||
background-position: right 6px center;
|
||||
white-space: nowrap;
|
||||
max-width: 140px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.model-selector select:focus {
|
||||
.composer-model:focus {
|
||||
border-color: var(--border-strong);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chat-flags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.flag-toggle {
|
||||
.composer-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.flag-toggle input[type="checkbox"] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.rag-status {
|
||||
margin-left: auto;
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
.composer-toggle input[type="checkbox"] {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
accent-color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-row textarea {
|
||||
flex: 1;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
line-height: 1.4;
|
||||
resize: none;
|
||||
outline: none;
|
||||
max-height: 120px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
.composer-toggle:has(input:checked) {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.input-row textarea:focus {
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,0.08);
|
||||
.composer-status {
|
||||
font-size: 10px;
|
||||
color: var(--text-tertiary);
|
||||
white-space: nowrap;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.chat-modern-input {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
.composer-send {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #000000;
|
||||
border-radius: 50%;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.send-btn:hover {
|
||||
.composer-send:hover {
|
||||
background: var(--accent-hover);
|
||||
box-shadow: 0 0 12px rgba(255,255,255,0.15);
|
||||
box-shadow: 0 0 12px rgba(255,255,255,0.12);
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.3;
|
||||
.composer-send:disabled {
|
||||
opacity: 0.2;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.send-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
.composer-send svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* ==================== Buttons ==================== */
|
||||
@@ -868,7 +874,7 @@ html {
|
||||
}
|
||||
|
||||
.tool-list {
|
||||
max-height: 120px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
@@ -880,6 +886,74 @@ html {
|
||||
scrollbar-color: rgba(255,255,255,0.08) transparent;
|
||||
}
|
||||
|
||||
.tool-category {
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tool-category:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tool-category-name {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.tool-category-tools {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* MCP Server List */
|
||||
.mcp-server-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mcp-server-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.mcp-server-item:hover {
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.mcp-server-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mcp-server-name {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mcp-server-tools {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ==================== User Profile ==================== */
|
||||
|
||||
.user-profile {
|
||||
|
||||
@@ -35,35 +35,35 @@
|
||||
<p>Ask anything. Powered by Hanzo Cloud.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chat-composer" class="chat-input-area">
|
||||
<div class="model-selector">
|
||||
<select id="model-select">
|
||||
<option value="auto">Auto (cost-aware)</option>
|
||||
<option value="claude-sonnet-4-20250514">Claude Sonnet 4</option>
|
||||
<option value="claude-opus-4-20250514">Claude Opus 4</option>
|
||||
<option value="gpt-4o">GPT-4o</option>
|
||||
<option value="zen-coder-flash">Zen Coder Flash</option>
|
||||
<option value="zen-max">Zen Max</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="chat-flags">
|
||||
<label class="flag-toggle">
|
||||
<input type="checkbox" id="rag-enabled" checked>
|
||||
<span>RAG</span>
|
||||
</label>
|
||||
<label class="flag-toggle">
|
||||
<input type="checkbox" id="tab-context-enabled" checked>
|
||||
<span>Tab Context</span>
|
||||
</label>
|
||||
<span id="rag-status" class="rag-status">Ready</span>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<div id="chat-composer" class="composer">
|
||||
<div class="composer-box">
|
||||
<textarea id="chat-input" placeholder="Ask anything..." rows="1"></textarea>
|
||||
<button id="send-btn" class="send-btn" title="Send">
|
||||
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor">
|
||||
<path d="M4 10l12-6-6 12-2-6-4-2z" stroke-width="1.5" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="composer-bar">
|
||||
<div class="composer-controls">
|
||||
<select id="model-select" class="composer-model">
|
||||
<option value="auto">Auto</option>
|
||||
<option value="claude-sonnet-4-20250514">Claude Sonnet 4</option>
|
||||
<option value="claude-opus-4-20250514">Claude Opus 4</option>
|
||||
<option value="gpt-4o">GPT-4o</option>
|
||||
<option value="zen-coder-flash">Zen Coder Flash</option>
|
||||
<option value="zen-max">Zen Max</option>
|
||||
</select>
|
||||
<label class="composer-toggle" title="RAG context">
|
||||
<input type="checkbox" id="rag-enabled" checked>
|
||||
<span>RAG</span>
|
||||
</label>
|
||||
<label class="composer-toggle" title="Include active tab">
|
||||
<input type="checkbox" id="tab-context-enabled" checked>
|
||||
<span>Tab</span>
|
||||
</label>
|
||||
<span id="rag-status" class="composer-status">Ready</span>
|
||||
</div>
|
||||
<button id="send-btn" class="composer-send" title="Send (Enter)">
|
||||
<svg viewBox="0 0 16 16" fill="none">
|
||||
<path d="M3 13V3l10 5-10 5z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span>MCP Server</span>
|
||||
<span>Hanzo Bot</span>
|
||||
<span id="hanzo-mcp-status" class="status-value">
|
||||
<span class="status-indicator disconnected"></span>
|
||||
Checking...
|
||||
@@ -108,21 +108,34 @@
|
||||
</svg>
|
||||
Start Node
|
||||
</button>
|
||||
<button class="action-btn small" id="start-hanzo-mcp" title="Start hanzo mcp serve">
|
||||
<button class="action-btn small" id="start-hanzo-mcp" title="Start hanzo bot">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12">
|
||||
<path d="M4 2l10 6-10 6V2z"/>
|
||||
</svg>
|
||||
Start MCP
|
||||
Start Bot
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MCP Tools -->
|
||||
<!-- Connected MCP Servers -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>Discovered Tools</h3>
|
||||
<h3>MCP Servers</h3>
|
||||
<span id="mcp-server-count" class="badge">0</span>
|
||||
</div>
|
||||
<div id="mcp-tool-list" class="tool-list">No tools discovered yet.</div>
|
||||
<div id="mcp-server-list" class="mcp-server-list">
|
||||
<div class="empty-state">No MCP servers connected</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Built-in + Discovered Tools -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>Available Tools</h3>
|
||||
<span id="tool-count-badge" class="badge">0</span>
|
||||
</div>
|
||||
<div id="builtin-tool-list" class="tool-list"></div>
|
||||
<div id="mcp-tool-list" class="tool-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Usage -->
|
||||
@@ -191,8 +204,8 @@
|
||||
<!-- Settings Tab -->
|
||||
<div id="tab-settings" class="tab-content hidden">
|
||||
<div class="settings-scroll">
|
||||
<!-- Account -->
|
||||
<div class="panel" id="account-panel">
|
||||
<!-- Account (shown when authenticated) -->
|
||||
<div class="panel hidden" id="account-panel">
|
||||
<h3>Account</h3>
|
||||
<div class="user-profile">
|
||||
<img id="user-avatar" class="user-avatar" src="" alt="">
|
||||
|
||||
+119
-32
@@ -53,8 +53,6 @@ class HanzoSidebar {
|
||||
authSection: document.getElementById('auth-section'),
|
||||
authBtn: document.getElementById('auth-btn'),
|
||||
tabBar: document.getElementById('tab-bar'),
|
||||
userBadge: document.getElementById('user-badge'),
|
||||
headerAvatar: document.getElementById('header-avatar'),
|
||||
|
||||
// Chat
|
||||
tabChat: document.getElementById('tab-chat'),
|
||||
@@ -71,6 +69,10 @@ class HanzoSidebar {
|
||||
mcpStatus: document.getElementById('mcp-status'),
|
||||
mcpTools: document.getElementById('mcp-tools'),
|
||||
mcpToolList: document.getElementById('mcp-tool-list'),
|
||||
builtinToolList: document.getElementById('builtin-tool-list'),
|
||||
toolCountBadge: document.getElementById('tool-count-badge'),
|
||||
mcpServerList: document.getElementById('mcp-server-list'),
|
||||
mcpServerCount: document.getElementById('mcp-server-count'),
|
||||
agentCount: document.getElementById('agent-count'),
|
||||
agentList: document.getElementById('agent-list'),
|
||||
tabFs: document.getElementById('tab-fs'),
|
||||
@@ -88,6 +90,7 @@ class HanzoSidebar {
|
||||
|
||||
// Settings
|
||||
tabSettings: document.getElementById('tab-settings'),
|
||||
accountPanel: document.getElementById('account-panel'),
|
||||
userAvatar: document.getElementById('user-avatar'),
|
||||
userName: document.getElementById('user-name'),
|
||||
userEmail: document.getElementById('user-email'),
|
||||
@@ -145,7 +148,7 @@ class HanzoSidebar {
|
||||
this.el.refreshTabs.addEventListener('click', () => this.refreshTabFilesystem());
|
||||
this.el.launchAgent.addEventListener('click', () => this.showAgentLauncher());
|
||||
this.el.startHanzoNode?.addEventListener('click', () => this.startHanzoService('node'));
|
||||
this.el.startHanzoMcp?.addEventListener('click', () => this.startHanzoService('mcp'));
|
||||
this.el.startHanzoMcp?.addEventListener('click', () => this.startHanzoService('bot'));
|
||||
|
||||
// Settings
|
||||
this.el.logoutBtn.addEventListener('click', () => this.logout());
|
||||
@@ -229,9 +232,10 @@ class HanzoSidebar {
|
||||
|
||||
async logout() {
|
||||
await chrome.runtime.sendMessage({ action: 'auth.logout' });
|
||||
this.el.userBadge.classList.add('hidden');
|
||||
this.el.userName.textContent = '';
|
||||
this.el.userEmail.textContent = '';
|
||||
if (this.el.userName) this.el.userName.textContent = '';
|
||||
if (this.el.userEmail) this.el.userEmail.textContent = '';
|
||||
if (this.el.userAvatar) this.el.userAvatar.src = '';
|
||||
if (this.el.accountPanel) this.el.accountPanel.classList.add('hidden');
|
||||
this.showChatLoginPrompt();
|
||||
this.switchTab('tools');
|
||||
}
|
||||
@@ -240,13 +244,20 @@ class HanzoSidebar {
|
||||
this.authenticated = true;
|
||||
if (user) {
|
||||
const avatar = user.picture || user.avatar || '';
|
||||
if (avatar) {
|
||||
this.el.headerAvatar.src = avatar;
|
||||
this.el.userBadge.classList.remove('hidden');
|
||||
if (avatar && this.el.userAvatar) {
|
||||
this.el.userAvatar.src = avatar;
|
||||
this.el.userAvatar.style.display = '';
|
||||
}
|
||||
if (this.el.userName) {
|
||||
this.el.userName.textContent = user.name || user.displayName || user.sub || 'User';
|
||||
}
|
||||
if (this.el.userEmail) {
|
||||
this.el.userEmail.textContent = user.email || '';
|
||||
}
|
||||
// Show the account panel
|
||||
if (this.el.accountPanel) {
|
||||
this.el.accountPanel.classList.remove('hidden');
|
||||
}
|
||||
this.el.userName.textContent = user.name || user.displayName || 'User';
|
||||
this.el.userEmail.textContent = user.email || '';
|
||||
}
|
||||
this.loadModels();
|
||||
this.loadConversation();
|
||||
@@ -259,6 +270,8 @@ class HanzoSidebar {
|
||||
if (chatLogin) chatLogin.classList.remove('hidden');
|
||||
const chatComposer = document.getElementById('chat-composer');
|
||||
if (chatComposer) chatComposer.classList.add('hidden');
|
||||
// Hide account panel when not authenticated
|
||||
if (this.el.accountPanel) this.el.accountPanel.classList.add('hidden');
|
||||
}
|
||||
|
||||
hideChatLoginPrompt() {
|
||||
@@ -319,6 +332,7 @@ class HanzoSidebar {
|
||||
ensureToolsInitialized() {
|
||||
if (this.toolsInitialized) return;
|
||||
this.toolsInitialized = true;
|
||||
this.renderBuiltinTools();
|
||||
void this.connectToMCP();
|
||||
void this.refreshTabFilesystem();
|
||||
void this.checkHanzoServices();
|
||||
@@ -357,11 +371,21 @@ class HanzoSidebar {
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
const text = this.el.chatInput.value.trim();
|
||||
const text = this.el.chatInput?.value?.trim();
|
||||
if (!text) return;
|
||||
if (!this.authenticated) {
|
||||
this.showError('Please sign in to use chat');
|
||||
return;
|
||||
// Double-check auth status in case it was set externally
|
||||
try {
|
||||
const status = await chrome.runtime.sendMessage({ action: 'auth.status' });
|
||||
if (status?.success && status.authenticated) {
|
||||
this.authenticated = true;
|
||||
if (status.user) this.setUser(status.user);
|
||||
}
|
||||
} catch {}
|
||||
if (!this.authenticated) {
|
||||
this.showError('Please sign in to use chat');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.streaming && this.streamController) {
|
||||
@@ -755,17 +779,71 @@ class HanzoSidebar {
|
||||
this.el.mcpTools.textContent = toolCount;
|
||||
}
|
||||
|
||||
// Built-in @hanzo/mcp tool categories (always available)
|
||||
static BUILTIN_TOOLS = [
|
||||
{ category: 'Core', tools: ['read', 'write', 'list', 'info', 'tree', 'create', 'delete', 'move', 'edit', 'patch', 'grep', 'find', 'search', 'bash', 'bg', 'ps', 'logs', 'kill'] },
|
||||
{ category: 'Intelligence', tools: ['ai', 'ast', 'vector'] },
|
||||
{ category: 'Workflow', tools: ['todo', 'plan', 'memory', 'mode'] },
|
||||
{ category: 'Dev', tools: ['vcs', 'refactor'] },
|
||||
{ category: 'Cloud', tools: ['iam', 'kms', 'paas', 'commerce', 'storage', 'auth', 'api'] },
|
||||
];
|
||||
|
||||
renderBuiltinTools() {
|
||||
if (!this.el.builtinToolList) return;
|
||||
|
||||
const totalBuiltin = HanzoSidebar.BUILTIN_TOOLS.reduce((sum, c) => sum + c.tools.length, 0);
|
||||
|
||||
this.el.builtinToolList.innerHTML = HanzoSidebar.BUILTIN_TOOLS.map(cat =>
|
||||
`<div class="tool-category">` +
|
||||
`<span class="tool-category-name">${this.escapeHtml(cat.category)}</span> ` +
|
||||
`<span class="tool-category-tools">${cat.tools.join(', ')}</span>` +
|
||||
`</div>`
|
||||
).join('');
|
||||
|
||||
this.updateToolCount(totalBuiltin);
|
||||
}
|
||||
|
||||
updateToolCount(builtinCount: number) {
|
||||
const zapCount = parseInt(this.el.mcpTools?.textContent || '0') || 0;
|
||||
const total = builtinCount + zapCount;
|
||||
if (this.el.toolCountBadge) this.el.toolCountBadge.textContent = String(total);
|
||||
}
|
||||
|
||||
updateMCPToolList(tools) {
|
||||
if (!this.el.mcpToolList) return;
|
||||
if (!tools || !tools.length) {
|
||||
this.el.mcpToolList.textContent = 'No tools discovered yet.';
|
||||
this.el.mcpToolList.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const names = Array.from(new Set(tools.map((tool) => tool.name))).sort();
|
||||
const preview = names.slice(0, 20);
|
||||
const suffix = names.length > preview.length ? `\n... +${names.length - preview.length} more` : '';
|
||||
this.el.mcpToolList.textContent = `${preview.join('\n')}${suffix}`;
|
||||
this.el.mcpToolList.textContent = `ZAP: ${preview.join(', ')}${suffix}`;
|
||||
|
||||
// Update total count
|
||||
const builtinCount = HanzoSidebar.BUILTIN_TOOLS.reduce((sum, c) => sum + c.tools.length, 0);
|
||||
this.updateToolCount(builtinCount);
|
||||
}
|
||||
|
||||
updateMCPServerList(mcps) {
|
||||
if (!this.el.mcpServerList) return;
|
||||
if (this.el.mcpServerCount) this.el.mcpServerCount.textContent = String(mcps?.length || 0);
|
||||
|
||||
if (!mcps || !mcps.length) {
|
||||
this.el.mcpServerList.innerHTML = '<div class="empty-state">No MCP servers connected</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.el.mcpServerList.innerHTML = mcps.map(mcp => `
|
||||
<div class="mcp-server-item">
|
||||
<div class="mcp-server-info">
|
||||
<span class="status-indicator connected"></span>
|
||||
<span class="mcp-server-name">${this.escapeHtml(mcp.name || mcp.id)}</span>
|
||||
</div>
|
||||
<span class="mcp-server-tools">${(mcp.tools?.length || 0)} tools</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async refreshUsageMetrics() {
|
||||
@@ -813,19 +891,18 @@ class HanzoSidebar {
|
||||
try {
|
||||
const resp = await fetch(`http://localhost:${nodePort}/health`, { signal: AbortSignal.timeout(2000) });
|
||||
if (resp.ok) {
|
||||
this.el.hanzoNodeStatus.innerHTML = `<span class="status-indicator connected"></span> Running`;
|
||||
this.el.startHanzoNode.textContent = 'Running';
|
||||
this.el.startHanzoNode.disabled = true;
|
||||
if (this.el.hanzoNodeStatus) this.el.hanzoNodeStatus.innerHTML = `<span class="status-indicator connected"></span> Running`;
|
||||
if (this.el.startHanzoNode) { this.el.startHanzoNode.textContent = 'Running'; this.el.startHanzoNode.disabled = true; }
|
||||
} else {
|
||||
this.el.hanzoNodeStatus.innerHTML = `<span class="status-indicator disconnected"></span> Stopped`;
|
||||
this.el.startHanzoNode.disabled = false;
|
||||
if (this.el.hanzoNodeStatus) this.el.hanzoNodeStatus.innerHTML = `<span class="status-indicator disconnected"></span> Stopped`;
|
||||
if (this.el.startHanzoNode) this.el.startHanzoNode.disabled = false;
|
||||
}
|
||||
} catch {
|
||||
this.el.hanzoNodeStatus.innerHTML = `<span class="status-indicator disconnected"></span> Stopped`;
|
||||
if (this.el.hanzoNodeStatus) this.el.hanzoNodeStatus.innerHTML = `<span class="status-indicator disconnected"></span> Stopped`;
|
||||
if (this.el.startHanzoNode) this.el.startHanzoNode.disabled = false;
|
||||
}
|
||||
|
||||
// Check hanzo MCP server (via ZAP status which already probes MCP)
|
||||
// Check hanzo bot/MCP via ZAP status
|
||||
try {
|
||||
const zapResp = await chrome.runtime.sendMessage({ action: 'zap.status' });
|
||||
const hasTools = zapResp?.success && zapResp.zap?.connected;
|
||||
@@ -838,39 +915,49 @@ class HanzoSidebar {
|
||||
this.el.startHanzoMcp.disabled = !!hasTools;
|
||||
if (hasTools) this.el.startHanzoMcp.textContent = 'Running';
|
||||
}
|
||||
|
||||
// Update connected MCP servers list
|
||||
if (zapResp?.success && zapResp.zap?.mcps?.length) {
|
||||
this.updateMCPServerList(zapResp.zap.mcps);
|
||||
} else {
|
||||
this.updateMCPServerList([]);
|
||||
}
|
||||
} catch {
|
||||
if (this.el.hanzoMcpStatus) {
|
||||
this.el.hanzoMcpStatus.innerHTML = `<span class="status-indicator disconnected"></span> Stopped`;
|
||||
}
|
||||
this.updateMCPServerList([]);
|
||||
}
|
||||
}
|
||||
|
||||
async startHanzoService(service: string) {
|
||||
// Open a new tab with instructions to start the service via terminal
|
||||
const commands = {
|
||||
node: 'hanzo node --port 52415',
|
||||
mcp: 'hanzo mcp serve',
|
||||
bot: 'npx @hanzo/mcp serve',
|
||||
};
|
||||
const labels = { node: 'Hanzo Node', bot: 'Hanzo Bot' };
|
||||
const cmd = commands[service] || commands.node;
|
||||
const label = labels[service] || service;
|
||||
|
||||
// Try sending command to CDP bridge (if running, it can spawn processes)
|
||||
// Try native messaging or CDP bridge first
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
action: 'cdp.exec',
|
||||
command: cmd,
|
||||
});
|
||||
if (response?.success) {
|
||||
this.showNotification(`Starting ${service}...`);
|
||||
// Re-check after a short delay
|
||||
this.showNotification(`Starting ${label}...`);
|
||||
setTimeout(() => this.checkHanzoServices(), 3000);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Fallback: open docs page with terminal command
|
||||
const url = `https://docs.hanzo.ai/cli#${service}`;
|
||||
chrome.tabs.create({ url });
|
||||
this.showNotification(`Run: ${cmd}`);
|
||||
// Fallback: show the command to run with a copy-friendly notification
|
||||
this.showNotification(`Run in terminal: ${cmd}`);
|
||||
|
||||
// Also try to open docs
|
||||
const docsSection = service === 'bot' ? 'mcp' : service;
|
||||
chrome.tabs.create({ url: `https://docs.hanzo.ai/cli#${docsSection}` });
|
||||
}
|
||||
|
||||
updateAgentList() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hanzo/dxt",
|
||||
"version": "1.7.12",
|
||||
"version": "1.7.14",
|
||||
"description": "Hanzo AI DXT Bundle",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hanzo/mcp",
|
||||
"version": "1.0.0",
|
||||
"description": "Hanzo MCP Server - Model Context Protocol tools for AI development",
|
||||
"version": "2.2.2",
|
||||
"description": "Hanzo MCP Server - Unified tool surface with AI, cloud control, vector search, and multi-framework UI",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hanzo/cli-tools",
|
||||
"version": "1.7.12",
|
||||
"version": "1.7.14",
|
||||
"description": "Hanzo AI CLI Tools Integration",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -173,7 +173,7 @@ export class HanzoAuth extends EventEmitter {
|
||||
|
||||
this.server.listen(port, () => {
|
||||
// Authorization Code + PKCE (OAuth 2.1 standard)
|
||||
const authUrl = new URL('/login/oauth/authorize', this.config.iamLoginUrl);
|
||||
const authUrl = new URL('/oauth/authorize', this.config.iamLoginUrl);
|
||||
authUrl.searchParams.set('client_id', this.config.clientId);
|
||||
authUrl.searchParams.set('response_type', 'code');
|
||||
authUrl.searchParams.set('redirect_uri', `http://localhost:${port}/callback`);
|
||||
@@ -200,7 +200,7 @@ export class HanzoAuth extends EventEmitter {
|
||||
}
|
||||
|
||||
private async exchangeCodeForTokens(code: string, codeVerifier: string): Promise<void> {
|
||||
const response = await fetch(`${this.config.iamUrl}/api/login/oauth/access_token`, {
|
||||
const response = await fetch(`${this.config.iamUrl}/oauth/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -318,7 +318,7 @@ export class HanzoAuth extends EventEmitter {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.config.iamUrl}/api/login/oauth/access_token`, {
|
||||
const response = await fetch(`${this.config.iamUrl}/oauth/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@hanzo/extension",
|
||||
"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.7.12",
|
||||
"version": "1.7.14",
|
||||
"publisher": "hanzo-ai",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
|
||||
@@ -104,7 +104,7 @@ export class StandaloneAuthManager {
|
||||
|
||||
private async refreshToken(refreshToken: string): Promise<AuthToken | null> {
|
||||
try {
|
||||
const response = await axios.post(`${this.casdoorConfig.endpoint}/api/login/oauth/access_token`, {
|
||||
const response = await axios.post(`${this.casdoorConfig.endpoint}/oauth/token`, {
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_id: this.casdoorConfig.clientId,
|
||||
@@ -166,7 +166,7 @@ export class StandaloneAuthManager {
|
||||
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
||||
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
||||
|
||||
const authUrl = new URL(`${this.casdoorConfig.loginUrl}/login/oauth/authorize`);
|
||||
const authUrl = new URL(`${this.casdoorConfig.loginUrl}/oauth/authorize`);
|
||||
authUrl.searchParams.append('client_id', this.casdoorConfig.clientId);
|
||||
authUrl.searchParams.append('response_type', 'code');
|
||||
authUrl.searchParams.append('redirect_uri', 'http://localhost:8765/callback');
|
||||
@@ -249,7 +249,7 @@ export class StandaloneAuthManager {
|
||||
};
|
||||
if (clientSecret) body.client_secret = clientSecret;
|
||||
|
||||
const tokenResp = await axios.post(`${endpoint}/api/login/oauth/access_token`, body);
|
||||
const tokenResp = await axios.post(`${endpoint}/oauth/token`, body);
|
||||
const tokens = tokenResp.data;
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(successHtml);
|
||||
|
||||
Reference in New Issue
Block a user