@hanzo/browser-extension 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/docs.yml +44 -0
- package/.github/workflows/release.yml +55 -0
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/browser-extension/README.md +43 -0
- package/dist/browser-extension/ai-worker.js +30 -0
- package/dist/browser-extension/background-firefox.js +617 -0
- package/dist/browser-extension/background.js +2737 -0
- package/dist/browser-extension/browser-control.js +683 -0
- package/dist/browser-extension/cdp-bridge-server.js +444 -0
- package/dist/browser-extension/chrome/ai-worker.js +571 -0
- package/dist/browser-extension/chrome/background.js +2737 -0
- package/dist/browser-extension/chrome/callback.html +17 -0
- package/dist/browser-extension/chrome/content-script.js +1270 -0
- package/dist/browser-extension/chrome/icon128.png +0 -0
- package/dist/browser-extension/chrome/icon16.png +0 -0
- package/dist/browser-extension/chrome/icon32.png +0 -0
- package/dist/browser-extension/chrome/icon48.png +0 -0
- package/dist/browser-extension/chrome/manifest.json +57 -0
- package/dist/browser-extension/chrome/popup.css +519 -0
- package/dist/browser-extension/chrome/popup.html +242 -0
- package/dist/browser-extension/chrome/popup.js +267 -0
- package/dist/browser-extension/chrome/sidebar.css +1176 -0
- package/dist/browser-extension/chrome/sidebar.html +280 -0
- package/dist/browser-extension/chrome/sidebar.js +778 -0
- package/dist/browser-extension/cli.js +233 -0
- package/dist/browser-extension/content-script.js +1270 -0
- package/dist/browser-extension/firefox/ai-worker.js +571 -0
- package/dist/browser-extension/firefox/background.js +617 -0
- package/dist/browser-extension/firefox/callback.html +17 -0
- package/dist/browser-extension/firefox/content-script.js +1270 -0
- package/dist/browser-extension/firefox/icon128.png +0 -0
- package/dist/browser-extension/firefox/icon16.png +0 -0
- package/dist/browser-extension/firefox/icon32.png +0 -0
- package/dist/browser-extension/firefox/icon48.png +0 -0
- package/dist/browser-extension/firefox/manifest.json +66 -0
- package/dist/browser-extension/firefox/popup.css +519 -0
- package/dist/browser-extension/firefox/popup.html +242 -0
- package/dist/browser-extension/firefox/popup.js +267 -0
- package/dist/browser-extension/firefox/sidebar.css +1176 -0
- package/dist/browser-extension/firefox/sidebar.html +280 -0
- package/dist/browser-extension/firefox/sidebar.js +778 -0
- package/dist/browser-extension/hanzo-browser-extension-firefox-1.1.0.zip +0 -0
- package/dist/browser-extension/icon128.png +0 -0
- package/dist/browser-extension/icon16.png +0 -0
- package/dist/browser-extension/icon32.png +0 -0
- package/dist/browser-extension/icon48.png +0 -0
- package/dist/browser-extension/manifest.json +57 -0
- package/dist/browser-extension/package.json +41 -0
- package/dist/browser-extension/popup.css +468 -0
- package/dist/browser-extension/popup.html +191 -0
- package/dist/browser-extension/popup.js +267 -0
- package/dist/browser-extension/safari/Info.plist +21 -0
- package/dist/browser-extension/safari/ai-worker.js +571 -0
- package/dist/browser-extension/safari/background.js +2737 -0
- package/dist/browser-extension/safari/callback.html +17 -0
- package/dist/browser-extension/safari/content-script.js +1270 -0
- package/dist/browser-extension/safari/icon128.png +0 -0
- package/dist/browser-extension/safari/icon16.png +0 -0
- package/dist/browser-extension/safari/icon32.png +0 -0
- package/dist/browser-extension/safari/icon48.png +0 -0
- package/dist/browser-extension/safari/popup.css +519 -0
- package/dist/browser-extension/safari/popup.html +242 -0
- package/dist/browser-extension/safari/popup.js +267 -0
- package/dist/browser-extension/safari/sidebar.css +1176 -0
- package/dist/browser-extension/safari/sidebar.html +280 -0
- package/dist/browser-extension/safari/sidebar.js +778 -0
- package/dist/browser-extension/sidebar.css +718 -0
- package/dist/browser-extension/sidebar.html +184 -0
- package/dist/browser-extension/sidebar.js +778 -0
- package/dist/browser-extension/webgpu-ai.js +495 -0
- package/dist/cdp-bridge-server.js +411 -0
- package/dist/safari/Hanzo AI/Hanzo AI.xcodeproj/project.pbxproj +1047 -0
- package/dist/safari/Hanzo AI/Hanzo AI.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json +98 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@1x.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@2x.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@1x.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@2x.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@1x.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@2x.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@1x.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@2x.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@1x.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@2x.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/universal-icon-1024@1x.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/Contents.json +6 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json +21 -0
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon128.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Resources/Base.lproj/Main.html +20 -0
- package/dist/safari/Hanzo AI/Shared (App)/Resources/Icon.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Resources/Script.js +24 -0
- package/dist/safari/Hanzo AI/Shared (App)/Resources/Style.css +61 -0
- package/dist/safari/Hanzo AI/Shared (App)/ViewController.swift +81 -0
- package/dist/safari/Hanzo AI/Shared (Extension)/SafariWebExtensionHandler.swift +42 -0
- package/dist/safari/Hanzo AI/iOS (App)/AppDelegate.swift +24 -0
- package/dist/safari/Hanzo AI/iOS (App)/Base.lproj/LaunchScreen.storyboard +36 -0
- package/dist/safari/Hanzo AI/iOS (App)/Base.lproj/Main.storyboard +38 -0
- package/dist/safari/Hanzo AI/iOS (App)/Info.plist +27 -0
- package/dist/safari/Hanzo AI/iOS (App)/SceneDelegate.swift +18 -0
- package/dist/safari/Hanzo AI/iOS (Extension)/Info.plist +13 -0
- package/dist/safari/Hanzo AI/macOS (App)/AppDelegate.swift +21 -0
- package/dist/safari/Hanzo AI/macOS (App)/Base.lproj/Main.storyboard +124 -0
- package/dist/safari/Hanzo AI/macOS (App)/Info.plist +8 -0
- package/dist/safari/Hanzo AI/macOS (Extension)/Info.plist +13 -0
- package/docs/_config.yml +13 -0
- package/docs/_layouts/default.html +435 -0
- package/docs/index.md +54 -0
- package/install-extension.sh +75 -0
- package/manifest.json +29 -0
- package/package.json +32 -0
- package/src/ai-worker.js +571 -0
- package/src/callback.html +17 -0
- package/src/chat-widget.tsx +93 -0
- package/src/icon128.png +0 -0
- package/src/icon16.png +0 -0
- package/src/icon32.png +0 -0
- package/src/icon48.png +0 -0
- package/src/manifest-firefox.json +66 -0
- package/src/manifest.json +57 -0
- package/src/package.json +41 -0
- package/src/popup.css +519 -0
- package/src/popup.html +242 -0
- package/src/popup.js +192 -0
- package/src/sidebar.css +1176 -0
- package/src/sidebar.html +280 -0
- package/src/sidebar.js +884 -0
- package/src/webgpu.d.ts +131 -0
- package/test/claude-integration.test.ts +251 -0
- package/test/server-frameworks-edge-cases.test.ts +145 -0
- package/test/server-frameworks.test.ts +395 -0
- package/test-cdp-connection.js +140 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// src/background-firefox.ts
|
|
4
|
+
var controlSession = {
|
|
5
|
+
active: false,
|
|
6
|
+
tabId: null,
|
|
7
|
+
task: null,
|
|
8
|
+
startedAt: null
|
|
9
|
+
};
|
|
10
|
+
var HanzoFirefoxExtension = class {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.wsConnection = null;
|
|
13
|
+
this.cdpPort = 9223;
|
|
14
|
+
this.reconnectTimeout = 3e3;
|
|
15
|
+
console.log("[Hanzo] Firefox extension initializing...");
|
|
16
|
+
this.connect();
|
|
17
|
+
}
|
|
18
|
+
connect() {
|
|
19
|
+
const url = `ws://localhost:${this.cdpPort}/cdp`;
|
|
20
|
+
console.log(`[Hanzo] Connecting to ${url}`);
|
|
21
|
+
this.wsConnection = new WebSocket(url);
|
|
22
|
+
this.wsConnection.onopen = () => {
|
|
23
|
+
console.log("[Hanzo] WebSocket CONNECTED");
|
|
24
|
+
this.register();
|
|
25
|
+
};
|
|
26
|
+
this.wsConnection.onmessage = async (event) => {
|
|
27
|
+
try {
|
|
28
|
+
const message = JSON.parse(event.data);
|
|
29
|
+
console.log("[Hanzo] Received:", message.method || "response");
|
|
30
|
+
const response = await this.handleCommand(message);
|
|
31
|
+
this.send(response);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error("[Hanzo] Message handler error:", e);
|
|
34
|
+
this.send({ id: 0, error: { code: -1, message: String(e) } });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
this.wsConnection.onerror = (error) => {
|
|
38
|
+
console.error("[Hanzo] WebSocket error:", error);
|
|
39
|
+
};
|
|
40
|
+
this.wsConnection.onclose = () => {
|
|
41
|
+
console.log("[Hanzo] Disconnected, reconnecting...");
|
|
42
|
+
setTimeout(() => this.connect(), this.reconnectTimeout);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
register() {
|
|
46
|
+
this.send({
|
|
47
|
+
type: "register",
|
|
48
|
+
role: "cdp-provider",
|
|
49
|
+
capabilities: [
|
|
50
|
+
"navigate",
|
|
51
|
+
"screenshot",
|
|
52
|
+
"click",
|
|
53
|
+
"type",
|
|
54
|
+
"evaluate",
|
|
55
|
+
"tabs",
|
|
56
|
+
"fill",
|
|
57
|
+
"reload",
|
|
58
|
+
"goBack",
|
|
59
|
+
"goForward"
|
|
60
|
+
]
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
send(data) {
|
|
64
|
+
if (this.wsConnection?.readyState === WebSocket.OPEN) {
|
|
65
|
+
this.wsConnection.send(JSON.stringify(data));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async handleCommand(message) {
|
|
69
|
+
const { id, method, params = {} } = message;
|
|
70
|
+
try {
|
|
71
|
+
const result = await this.executeMethod(method, params);
|
|
72
|
+
return { id, result };
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return { id, error: { code: -1, message: String(e) } };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Wrap executeScript with a timeout to prevent blocking on GPU-heavy pages
|
|
79
|
+
*/
|
|
80
|
+
executeScriptWithTimeout(tabId, code, timeoutMs = 1e4) {
|
|
81
|
+
return Promise.race([
|
|
82
|
+
browser.tabs.executeScript(tabId, { code }),
|
|
83
|
+
new Promise(
|
|
84
|
+
(_, reject) => setTimeout(() => reject(new Error("executeScript timeout")), timeoutMs)
|
|
85
|
+
)
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
async executeMethod(method, params) {
|
|
89
|
+
const [activeTab] = await browser.tabs.query({
|
|
90
|
+
active: true,
|
|
91
|
+
currentWindow: true
|
|
92
|
+
});
|
|
93
|
+
switch (method) {
|
|
94
|
+
case "Target.getTargets": {
|
|
95
|
+
const allTabs = await browser.tabs.query({});
|
|
96
|
+
return {
|
|
97
|
+
targetInfos: allTabs.map((tab) => ({
|
|
98
|
+
targetId: `tab-${tab.id}`,
|
|
99
|
+
type: "page",
|
|
100
|
+
title: tab.title || "",
|
|
101
|
+
url: tab.url || ""
|
|
102
|
+
}))
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Native tab property reads - never block on page content
|
|
106
|
+
case "hanzo.url": {
|
|
107
|
+
if (activeTab) {
|
|
108
|
+
return { result: { value: activeTab.url || "" } };
|
|
109
|
+
}
|
|
110
|
+
throw new Error("No active tab");
|
|
111
|
+
}
|
|
112
|
+
case "hanzo.title": {
|
|
113
|
+
if (activeTab) {
|
|
114
|
+
return { result: { value: activeTab.title || "" } };
|
|
115
|
+
}
|
|
116
|
+
throw new Error("No active tab");
|
|
117
|
+
}
|
|
118
|
+
case "hanzo.tabInfo": {
|
|
119
|
+
if (activeTab) {
|
|
120
|
+
return {
|
|
121
|
+
result: {
|
|
122
|
+
value: {
|
|
123
|
+
id: activeTab.id,
|
|
124
|
+
url: activeTab.url,
|
|
125
|
+
title: activeTab.title,
|
|
126
|
+
status: activeTab.status,
|
|
127
|
+
favIconUrl: activeTab.favIconUrl
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
throw new Error("No active tab");
|
|
133
|
+
}
|
|
134
|
+
case "Page.navigate": {
|
|
135
|
+
if (activeTab?.id) {
|
|
136
|
+
this.notifyOverlay(activeTab.id, "ai.control.status", { text: `Navigating to ${params.url}` });
|
|
137
|
+
await browser.tabs.update(activeTab.id, { url: params.url });
|
|
138
|
+
return { frameId: "main" };
|
|
139
|
+
}
|
|
140
|
+
throw new Error("No active tab");
|
|
141
|
+
}
|
|
142
|
+
case "Page.reload": {
|
|
143
|
+
if (activeTab?.id) {
|
|
144
|
+
await browser.tabs.reload(activeTab.id);
|
|
145
|
+
return { success: true };
|
|
146
|
+
}
|
|
147
|
+
throw new Error("No active tab");
|
|
148
|
+
}
|
|
149
|
+
case "Page.goBack": {
|
|
150
|
+
if (activeTab?.id) {
|
|
151
|
+
await browser.tabs.goBack(activeTab.id);
|
|
152
|
+
return { success: true };
|
|
153
|
+
}
|
|
154
|
+
throw new Error("No active tab");
|
|
155
|
+
}
|
|
156
|
+
case "Page.goForward": {
|
|
157
|
+
if (activeTab?.id) {
|
|
158
|
+
await browser.tabs.goForward(activeTab.id);
|
|
159
|
+
return { success: true };
|
|
160
|
+
}
|
|
161
|
+
throw new Error("No active tab");
|
|
162
|
+
}
|
|
163
|
+
case "Runtime.evaluate": {
|
|
164
|
+
if (activeTab?.id) {
|
|
165
|
+
const expression = params.expression;
|
|
166
|
+
const timeout = params.timeout || 1e4;
|
|
167
|
+
const results = await this.executeScriptWithTimeout(
|
|
168
|
+
activeTab.id,
|
|
169
|
+
expression,
|
|
170
|
+
timeout
|
|
171
|
+
);
|
|
172
|
+
return { result: { value: results[0] } };
|
|
173
|
+
}
|
|
174
|
+
throw new Error("No active tab");
|
|
175
|
+
}
|
|
176
|
+
case "Page.captureScreenshot":
|
|
177
|
+
case "hanzo.screenshot": {
|
|
178
|
+
const format = params.format || "jpeg";
|
|
179
|
+
const quality = params.quality || (format === "jpeg" ? 80 : void 0);
|
|
180
|
+
const opts = { format };
|
|
181
|
+
if (quality !== void 0) opts.quality = quality;
|
|
182
|
+
const dataUrl = await browser.tabs.captureVisibleTab(opts);
|
|
183
|
+
const base64 = dataUrl.replace(/^data:image\/\w+;base64,/, "");
|
|
184
|
+
return { data: base64 };
|
|
185
|
+
}
|
|
186
|
+
case "hanzo.click": {
|
|
187
|
+
if (activeTab?.id && params.selector) {
|
|
188
|
+
const selector = this.escapeSelector(params.selector);
|
|
189
|
+
this.notifyOverlay(activeTab.id, "ai.control.highlight", { selector: params.selector });
|
|
190
|
+
this.notifyOverlay(activeTab.id, "ai.control.status", { text: `Clicking ${params.selector}` });
|
|
191
|
+
await this.executeScriptWithTimeout(
|
|
192
|
+
activeTab.id,
|
|
193
|
+
`document.querySelector('${selector}')?.click()`
|
|
194
|
+
);
|
|
195
|
+
return { success: true };
|
|
196
|
+
}
|
|
197
|
+
throw new Error("No active tab or selector");
|
|
198
|
+
}
|
|
199
|
+
case "hanzo.fill": {
|
|
200
|
+
if (activeTab?.id && params.selector) {
|
|
201
|
+
const selector = this.escapeSelector(params.selector);
|
|
202
|
+
const value = this.escapeValue(params.value || "");
|
|
203
|
+
this.notifyOverlay(activeTab.id, "ai.control.highlight", { selector: params.selector });
|
|
204
|
+
this.notifyOverlay(activeTab.id, "ai.control.status", { text: `Filling ${params.selector}` });
|
|
205
|
+
await this.executeScriptWithTimeout(
|
|
206
|
+
activeTab.id,
|
|
207
|
+
`
|
|
208
|
+
var el = document.querySelector('${selector}');
|
|
209
|
+
if (el) {
|
|
210
|
+
el.value = '${value}';
|
|
211
|
+
el.dispatchEvent(new Event('input', {bubbles: true}));
|
|
212
|
+
el.dispatchEvent(new Event('change', {bubbles: true}));
|
|
213
|
+
}
|
|
214
|
+
`
|
|
215
|
+
);
|
|
216
|
+
return { success: true };
|
|
217
|
+
}
|
|
218
|
+
throw new Error("No active tab or selector");
|
|
219
|
+
}
|
|
220
|
+
case "hanzo.control.start": {
|
|
221
|
+
const targetTabId = await this.resolveControlTabId(params.tabId);
|
|
222
|
+
if (targetTabId) {
|
|
223
|
+
await this.startControlSession(targetTabId, params.task);
|
|
224
|
+
return { success: true };
|
|
225
|
+
}
|
|
226
|
+
throw new Error("No active tab");
|
|
227
|
+
}
|
|
228
|
+
case "hanzo.control.stop": {
|
|
229
|
+
await this.stopControlSession();
|
|
230
|
+
return { success: true };
|
|
231
|
+
}
|
|
232
|
+
case "hanzo.type": {
|
|
233
|
+
if (activeTab?.id && params.text) {
|
|
234
|
+
const text = this.escapeValue(params.text);
|
|
235
|
+
await this.executeScriptWithTimeout(
|
|
236
|
+
activeTab.id,
|
|
237
|
+
`
|
|
238
|
+
var el = document.activeElement;
|
|
239
|
+
if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) {
|
|
240
|
+
el.value += '${text}';
|
|
241
|
+
el.dispatchEvent(new Event('input', {bubbles: true}));
|
|
242
|
+
}
|
|
243
|
+
`
|
|
244
|
+
);
|
|
245
|
+
return { success: true };
|
|
246
|
+
}
|
|
247
|
+
throw new Error("No active tab or text");
|
|
248
|
+
}
|
|
249
|
+
case "Browser.getVersion": {
|
|
250
|
+
return {
|
|
251
|
+
protocolVersion: "1.3",
|
|
252
|
+
product: "Hanzo Firefox Extension",
|
|
253
|
+
userAgent: navigator.userAgent
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
case "hanzo.getTabs":
|
|
257
|
+
case "hanzo.listTabs": {
|
|
258
|
+
const tabs = await browser.tabs.query({});
|
|
259
|
+
return {
|
|
260
|
+
tabs: tabs.map((tab) => ({
|
|
261
|
+
id: tab.id,
|
|
262
|
+
url: tab.url,
|
|
263
|
+
title: tab.title,
|
|
264
|
+
active: tab.active
|
|
265
|
+
}))
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
default:
|
|
269
|
+
console.warn("[Hanzo] Unknown method:", method);
|
|
270
|
+
return { error: `Unknown method: ${method}` };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
notifyOverlay(tabId, action, data) {
|
|
274
|
+
browser.tabs.sendMessage(tabId, { action, ...data }).catch(() => {
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async getActiveTabId() {
|
|
278
|
+
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
|
279
|
+
return tab?.id ?? null;
|
|
280
|
+
}
|
|
281
|
+
async resolveControlTabId(tabId) {
|
|
282
|
+
if (typeof tabId === "number") return tabId;
|
|
283
|
+
if (controlSession.active && controlSession.tabId !== null) return controlSession.tabId;
|
|
284
|
+
return this.getActiveTabId();
|
|
285
|
+
}
|
|
286
|
+
async startControlSession(tabId, task) {
|
|
287
|
+
if (controlSession.active && controlSession.tabId !== null && controlSession.tabId !== tabId) {
|
|
288
|
+
this.notifyOverlay(controlSession.tabId, "ai.control.stop", {});
|
|
289
|
+
}
|
|
290
|
+
controlSession.active = true;
|
|
291
|
+
controlSession.tabId = tabId;
|
|
292
|
+
controlSession.task = task || "AI is controlling this page";
|
|
293
|
+
controlSession.startedAt = Date.now();
|
|
294
|
+
this.notifyOverlay(tabId, "ai.control.start", { task: controlSession.task });
|
|
295
|
+
}
|
|
296
|
+
async stopControlSession() {
|
|
297
|
+
if (controlSession.active && controlSession.tabId !== null) {
|
|
298
|
+
this.notifyOverlay(controlSession.tabId, "ai.control.stop", {});
|
|
299
|
+
}
|
|
300
|
+
controlSession.active = false;
|
|
301
|
+
controlSession.tabId = null;
|
|
302
|
+
controlSession.task = null;
|
|
303
|
+
controlSession.startedAt = null;
|
|
304
|
+
}
|
|
305
|
+
escapeSelector(selector) {
|
|
306
|
+
return selector.replace(/'/g, "\\'");
|
|
307
|
+
}
|
|
308
|
+
escapeValue(value) {
|
|
309
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
var IAM_BASE = "https://hanzo.id";
|
|
313
|
+
var API_BASE = "https://api.hanzo.ai";
|
|
314
|
+
var CLIENT_ID = "app-hanzo";
|
|
315
|
+
var SCOPES = "openid profile email";
|
|
316
|
+
var STORAGE_KEYS = {
|
|
317
|
+
accessToken: "hanzo_iam_access_token",
|
|
318
|
+
refreshToken: "hanzo_iam_refresh_token",
|
|
319
|
+
idToken: "hanzo_iam_id_token",
|
|
320
|
+
expiresAt: "hanzo_iam_expires_at",
|
|
321
|
+
user: "hanzo_iam_user"
|
|
322
|
+
};
|
|
323
|
+
function generateRandomString(length) {
|
|
324
|
+
const array = new Uint8Array(length);
|
|
325
|
+
crypto.getRandomValues(array);
|
|
326
|
+
return Array.from(array, (b) => b.toString(36).padStart(2, "0")).join("").slice(0, length);
|
|
327
|
+
}
|
|
328
|
+
async function sha256(plain) {
|
|
329
|
+
return crypto.subtle.digest("SHA-256", new TextEncoder().encode(plain));
|
|
330
|
+
}
|
|
331
|
+
function base64UrlEncode(buffer) {
|
|
332
|
+
const bytes = new Uint8Array(buffer);
|
|
333
|
+
let binary = "";
|
|
334
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
335
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
336
|
+
}
|
|
337
|
+
async function getRagConfig() {
|
|
338
|
+
const result = await browser.storage.local.get([
|
|
339
|
+
"hanzo_rag_endpoint",
|
|
340
|
+
"hanzo_rag_api_key",
|
|
341
|
+
"hanzo_rag_kb",
|
|
342
|
+
"hanzo_rag_top_k",
|
|
343
|
+
"hanzo_rag_include_tab_context"
|
|
344
|
+
]);
|
|
345
|
+
const topKParsed = Number.parseInt(String(result.hanzo_rag_top_k ?? 5), 10);
|
|
346
|
+
return {
|
|
347
|
+
endpoint: String(result.hanzo_rag_endpoint || "").trim(),
|
|
348
|
+
apiKey: String(result.hanzo_rag_api_key || "").trim(),
|
|
349
|
+
knowledgeBase: String(result.hanzo_rag_kb || "").trim(),
|
|
350
|
+
topK: Number.isFinite(topKParsed) ? Math.max(1, Math.min(topKParsed, 20)) : 5,
|
|
351
|
+
includeTabContext: result.hanzo_rag_include_tab_context !== false
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function normalizeRagSnippets(raw) {
|
|
355
|
+
if (!raw) return [];
|
|
356
|
+
const candidates = [];
|
|
357
|
+
if (Array.isArray(raw)) candidates.push(...raw);
|
|
358
|
+
if (Array.isArray(raw?.snippets)) candidates.push(...raw.snippets);
|
|
359
|
+
if (Array.isArray(raw?.documents)) candidates.push(...raw.documents);
|
|
360
|
+
if (Array.isArray(raw?.items)) candidates.push(...raw.items);
|
|
361
|
+
if (Array.isArray(raw?.results)) candidates.push(...raw.results);
|
|
362
|
+
if (!candidates.length && typeof raw?.content === "string") {
|
|
363
|
+
candidates.push({ content: raw.content, source: raw.source || "rag-endpoint" });
|
|
364
|
+
}
|
|
365
|
+
return candidates.map((item) => {
|
|
366
|
+
const content = String(item?.content ?? item?.text ?? item?.snippet ?? item?.body ?? "").trim();
|
|
367
|
+
if (!content) return null;
|
|
368
|
+
return {
|
|
369
|
+
content,
|
|
370
|
+
title: item?.title ? String(item.title) : void 0,
|
|
371
|
+
source: item?.source ? String(item.source) : void 0,
|
|
372
|
+
score: typeof item?.score === "number" ? item.score : void 0,
|
|
373
|
+
url: item?.url ? String(item.url) : void 0
|
|
374
|
+
};
|
|
375
|
+
}).filter((item) => !!item).slice(0, 20);
|
|
376
|
+
}
|
|
377
|
+
async function queryRagEndpoint(params) {
|
|
378
|
+
if (!params.endpoint) return [];
|
|
379
|
+
const headers = {
|
|
380
|
+
"Content-Type": "application/json"
|
|
381
|
+
};
|
|
382
|
+
if (params.apiKey) headers.Authorization = `Bearer ${params.apiKey}`;
|
|
383
|
+
const response = await fetch(params.endpoint, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers,
|
|
386
|
+
body: JSON.stringify({
|
|
387
|
+
query: params.query,
|
|
388
|
+
top_k: params.topK,
|
|
389
|
+
knowledge_base: params.knowledgeBase || void 0,
|
|
390
|
+
page_context: params.includeTabContext ? params.pageContext : void 0,
|
|
391
|
+
source: "hanzo-browser-extension-firefox"
|
|
392
|
+
})
|
|
393
|
+
});
|
|
394
|
+
if (!response.ok) {
|
|
395
|
+
const text = await response.text();
|
|
396
|
+
throw new Error(`RAG endpoint error ${response.status}: ${text || "Unknown error"}`);
|
|
397
|
+
}
|
|
398
|
+
const raw = await response.json();
|
|
399
|
+
return normalizeRagSnippets(raw).map((snippet) => ({
|
|
400
|
+
...snippet,
|
|
401
|
+
source: snippet.source || "rag-endpoint"
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
async function firefoxLogin() {
|
|
405
|
+
const codeVerifier = generateRandomString(64);
|
|
406
|
+
const codeChallenge = base64UrlEncode(await sha256(codeVerifier));
|
|
407
|
+
const state = generateRandomString(32);
|
|
408
|
+
const redirectUri = "https://hanzo.ai/callback";
|
|
409
|
+
const authorizeUrl = new URL(`${IAM_BASE}/login/oauth/authorize`);
|
|
410
|
+
authorizeUrl.searchParams.set("client_id", CLIENT_ID);
|
|
411
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
412
|
+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
413
|
+
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
414
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
415
|
+
authorizeUrl.searchParams.set("scope", SCOPES);
|
|
416
|
+
authorizeUrl.searchParams.set("state", state);
|
|
417
|
+
const callbackUrl = await new Promise((resolve, reject) => {
|
|
418
|
+
let authTabId;
|
|
419
|
+
const timeout = setTimeout(() => {
|
|
420
|
+
cleanup();
|
|
421
|
+
reject(new Error("Login timed out"));
|
|
422
|
+
}, 3e5);
|
|
423
|
+
function cleanup() {
|
|
424
|
+
clearTimeout(timeout);
|
|
425
|
+
browser.tabs.onUpdated.removeListener(onUpdated);
|
|
426
|
+
browser.tabs.onRemoved.removeListener(onRemoved);
|
|
427
|
+
}
|
|
428
|
+
function onUpdated(tabId, changeInfo) {
|
|
429
|
+
if (tabId !== authTabId || !changeInfo.url) return;
|
|
430
|
+
if (changeInfo.url.startsWith(redirectUri)) {
|
|
431
|
+
cleanup();
|
|
432
|
+
browser.tabs.remove(tabId).catch(() => {
|
|
433
|
+
});
|
|
434
|
+
resolve(changeInfo.url);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function onRemoved(tabId) {
|
|
438
|
+
if (tabId !== authTabId) return;
|
|
439
|
+
cleanup();
|
|
440
|
+
reject(new Error("Login cancelled"));
|
|
441
|
+
}
|
|
442
|
+
browser.tabs.onUpdated.addListener(onUpdated);
|
|
443
|
+
browser.tabs.onRemoved.addListener(onRemoved);
|
|
444
|
+
browser.tabs.create({ url: authorizeUrl.toString() }).then((tab) => {
|
|
445
|
+
authTabId = tab.id;
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
const url = new URL(callbackUrl);
|
|
449
|
+
if (url.searchParams.get("state") !== state) throw new Error("State mismatch");
|
|
450
|
+
const code = url.searchParams.get("code");
|
|
451
|
+
if (!code) throw new Error(url.searchParams.get("error_description") || "No code");
|
|
452
|
+
const tokenResponse = await fetch(`${IAM_BASE}/oauth/token`, {
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
455
|
+
body: new URLSearchParams({
|
|
456
|
+
grant_type: "authorization_code",
|
|
457
|
+
client_id: CLIENT_ID,
|
|
458
|
+
code,
|
|
459
|
+
redirect_uri: redirectUri,
|
|
460
|
+
code_verifier: codeVerifier
|
|
461
|
+
})
|
|
462
|
+
});
|
|
463
|
+
if (!tokenResponse.ok) throw new Error(`Token exchange failed: ${await tokenResponse.text()}`);
|
|
464
|
+
const tokens = await tokenResponse.json();
|
|
465
|
+
const data = { [STORAGE_KEYS.accessToken]: tokens.access_token };
|
|
466
|
+
if (tokens.refresh_token) data[STORAGE_KEYS.refreshToken] = tokens.refresh_token;
|
|
467
|
+
if (tokens.id_token) data[STORAGE_KEYS.idToken] = tokens.id_token;
|
|
468
|
+
if (tokens.expires_in) data[STORAGE_KEYS.expiresAt] = Date.now() + tokens.expires_in * 1e3;
|
|
469
|
+
await browser.storage.local.set(data);
|
|
470
|
+
const userResp = await fetch(`${IAM_BASE}/oauth/userinfo`, { headers: { Authorization: `Bearer ${tokens.access_token}` } });
|
|
471
|
+
const user = userResp.ok ? await userResp.json() : {};
|
|
472
|
+
await browser.storage.local.set({ [STORAGE_KEYS.user]: user });
|
|
473
|
+
return user;
|
|
474
|
+
}
|
|
475
|
+
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
476
|
+
(async () => {
|
|
477
|
+
switch (request.action) {
|
|
478
|
+
case "auth.login":
|
|
479
|
+
try {
|
|
480
|
+
const user = await firefoxLogin();
|
|
481
|
+
sendResponse({ success: true, user });
|
|
482
|
+
} catch (e) {
|
|
483
|
+
sendResponse({ success: false, error: e.message });
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
case "auth.logout":
|
|
487
|
+
await browser.storage.local.remove(Object.values(STORAGE_KEYS));
|
|
488
|
+
sendResponse({ success: true });
|
|
489
|
+
break;
|
|
490
|
+
case "auth.status": {
|
|
491
|
+
const result = await browser.storage.local.get([STORAGE_KEYS.accessToken, STORAGE_KEYS.user]);
|
|
492
|
+
sendResponse({
|
|
493
|
+
success: true,
|
|
494
|
+
authenticated: !!result[STORAGE_KEYS.accessToken],
|
|
495
|
+
user: result[STORAGE_KEYS.user] || null
|
|
496
|
+
});
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
case "auth.getToken": {
|
|
500
|
+
const r = await browser.storage.local.get([STORAGE_KEYS.accessToken]);
|
|
501
|
+
sendResponse({ success: !!r[STORAGE_KEYS.accessToken], token: r[STORAGE_KEYS.accessToken] || null });
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
case "chat.listModels": {
|
|
505
|
+
const t = await browser.storage.local.get([STORAGE_KEYS.accessToken]);
|
|
506
|
+
if (!t[STORAGE_KEYS.accessToken]) {
|
|
507
|
+
sendResponse({ success: false, error: "Not authenticated" });
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
const resp = await fetch(`${API_BASE}/v1/models`, { headers: { Authorization: `Bearer ${t[STORAGE_KEYS.accessToken]}` } });
|
|
511
|
+
const data = await resp.json();
|
|
512
|
+
sendResponse({ success: true, models: data.data || data.models || data || [] });
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
case "checkWebGPU": {
|
|
516
|
+
try {
|
|
517
|
+
if (!navigator.gpu) {
|
|
518
|
+
sendResponse({ available: false });
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
522
|
+
sendResponse({
|
|
523
|
+
available: !!adapter,
|
|
524
|
+
adapter: adapter?.name || "Unknown GPU",
|
|
525
|
+
models: []
|
|
526
|
+
});
|
|
527
|
+
} catch {
|
|
528
|
+
sendResponse({ available: false });
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
case "listAgents":
|
|
533
|
+
sendResponse({ success: true, agents: [] });
|
|
534
|
+
break;
|
|
535
|
+
case "stopAgent":
|
|
536
|
+
sendResponse({ success: false, error: "Agent workers are not available in Firefox background mode yet" });
|
|
537
|
+
break;
|
|
538
|
+
case "launchAIWorker":
|
|
539
|
+
sendResponse({ success: false, error: "launchAIWorker is not implemented in Firefox background mode yet" });
|
|
540
|
+
break;
|
|
541
|
+
case "rag.query": {
|
|
542
|
+
try {
|
|
543
|
+
const config = await getRagConfig();
|
|
544
|
+
const topKInput = Number.parseInt(String(request.topK ?? config.topK), 10);
|
|
545
|
+
const params = {
|
|
546
|
+
endpoint: String(request.endpoint ?? config.endpoint ?? "").trim(),
|
|
547
|
+
apiKey: String(request.apiKey ?? config.apiKey ?? "").trim(),
|
|
548
|
+
query: String(request.query || "").trim(),
|
|
549
|
+
topK: Number.isFinite(topKInput) ? Math.max(1, Math.min(topKInput, 20)) : 5,
|
|
550
|
+
knowledgeBase: String(request.knowledgeBase ?? config.knowledgeBase ?? "").trim(),
|
|
551
|
+
includeTabContext: request.includeTabContext !== void 0 ? !!request.includeTabContext : config.includeTabContext,
|
|
552
|
+
pageContext: request.pageContext && typeof request.pageContext === "object" ? {
|
|
553
|
+
url: request.pageContext.url ? String(request.pageContext.url) : void 0,
|
|
554
|
+
title: request.pageContext.title ? String(request.pageContext.title) : void 0
|
|
555
|
+
} : void 0
|
|
556
|
+
};
|
|
557
|
+
if (!params.query) {
|
|
558
|
+
sendResponse({ success: false, error: "Query is required" });
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
if (!params.endpoint) {
|
|
562
|
+
sendResponse({ success: true, source: "none", snippets: [] });
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
const snippets = await queryRagEndpoint(params);
|
|
566
|
+
sendResponse({ success: true, source: snippets.length ? "rag-endpoint" : "none", snippets });
|
|
567
|
+
} catch (e) {
|
|
568
|
+
sendResponse({ success: false, error: e.message });
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
// --- AI Control Overlay (forwarded to content script) ---
|
|
573
|
+
case "ai.control.start":
|
|
574
|
+
case "ai.control.cursor":
|
|
575
|
+
case "ai.control.highlight":
|
|
576
|
+
case "ai.control.status": {
|
|
577
|
+
const targetTabId = request.tabId || controlSession.tabId || await hanzoExtension.getActiveTabId();
|
|
578
|
+
if (!targetTabId) {
|
|
579
|
+
sendResponse({ success: false, error: "No target tab found" });
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
if (request.action !== "ai.control.start" && controlSession.active && controlSession.tabId && targetTabId !== controlSession.tabId) {
|
|
583
|
+
sendResponse({ success: false, error: `Control locked to tab ${controlSession.tabId}` });
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
if (request.action === "ai.control.start") {
|
|
587
|
+
await hanzoExtension.startControlSession(targetTabId, request.task);
|
|
588
|
+
} else {
|
|
589
|
+
browser.tabs.sendMessage(targetTabId, request).catch(() => {
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
sendResponse({ success: true, tabId: targetTabId });
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
case "ai.control.stop":
|
|
596
|
+
await hanzoExtension.stopControlSession();
|
|
597
|
+
sendResponse({ success: true });
|
|
598
|
+
break;
|
|
599
|
+
case "ai.control.cancel":
|
|
600
|
+
await hanzoExtension.stopControlSession();
|
|
601
|
+
sendResponse({ success: true });
|
|
602
|
+
break;
|
|
603
|
+
default:
|
|
604
|
+
sendResponse({ success: false, error: `Unknown action: ${request.action}` });
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
})();
|
|
608
|
+
return true;
|
|
609
|
+
});
|
|
610
|
+
var hanzoExtension = new HanzoFirefoxExtension();
|
|
611
|
+
browser.tabs.onRemoved.addListener((tabId) => {
|
|
612
|
+
if (controlSession.active && controlSession.tabId === tabId) {
|
|
613
|
+
void hanzoExtension.stopControlSession();
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
console.log("[Hanzo] Firefox background script loaded");
|
|
617
|
+
})();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Hanzo AI — Signing in...</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { background: #000; color: #fff; font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
|
8
|
+
.msg { text-align: center; opacity: 0.7; }
|
|
9
|
+
</style>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div class="msg">
|
|
13
|
+
<p>Signing in to Hanzo AI...</p>
|
|
14
|
+
<p><small>This tab will close automatically.</small></p>
|
|
15
|
+
</div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|