@hanzo/browser-extension 1.7.6 → 1.9.33
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/LICENSE +9 -18
- package/dist/browser-extension/content-script.js +1642 -123
- package/images/icon128.png +0 -0
- package/images/icon16.png +0 -0
- package/images/icon48.png +0 -0
- package/manifest.json +7 -2
- package/package.json +28 -10
- package/src/answer/AnswerEngine.tsx +553 -0
- package/src/chat-widget.tsx +96 -0
- package/src/gui-primitives.tsx +116 -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 +32 -13
- package/src/manifest.json +35 -11
- package/src/newtab.css +277 -0
- package/src/newtab.html +13 -0
- package/src/package.json +1 -1
- package/src/popup.css +352 -110
- package/src/popup.html +186 -47
- package/src/popup.js +196 -0
- package/src/sidebar.css +772 -156
- package/src/sidebar.html +268 -85
- package/src/sidebar.js +885 -0
- package/.github/workflows/docs.yml +0 -44
- package/.github/workflows/release.yml +0 -55
- package/README.md +0 -126
- package/dist/browser-extension/README.md +0 -43
- package/dist/browser-extension/ai-worker.js +0 -30
- package/dist/browser-extension/background-firefox.js +0 -875
- package/dist/browser-extension/background.js +0 -2732
- package/dist/browser-extension/browser-control.js +0 -683
- package/dist/browser-extension/cdp-bridge-server.js +0 -444
- package/dist/browser-extension/chrome/ai-worker.js +0 -571
- package/dist/browser-extension/chrome/background.js +0 -2732
- package/dist/browser-extension/chrome/callback.html +0 -17
- package/dist/browser-extension/chrome/content-script.js +0 -1270
- 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 +0 -57
- package/dist/browser-extension/chrome/popup.css +0 -519
- package/dist/browser-extension/chrome/popup.html +0 -242
- package/dist/browser-extension/chrome/popup.js +0 -278
- package/dist/browser-extension/chrome/sidebar.css +0 -1176
- package/dist/browser-extension/chrome/sidebar.html +0 -280
- package/dist/browser-extension/chrome/sidebar.js +0 -779
- package/dist/browser-extension/cli.js +0 -233
- package/dist/browser-extension/firefox/ai-worker.js +0 -571
- package/dist/browser-extension/firefox/background.js +0 -875
- package/dist/browser-extension/firefox/callback.html +0 -17
- package/dist/browser-extension/firefox/content-script.js +0 -1270
- 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 +0 -66
- package/dist/browser-extension/firefox/popup.css +0 -519
- package/dist/browser-extension/firefox/popup.html +0 -242
- package/dist/browser-extension/firefox/popup.js +0 -278
- package/dist/browser-extension/firefox/sidebar.css +0 -1176
- package/dist/browser-extension/firefox/sidebar.html +0 -280
- package/dist/browser-extension/firefox/sidebar.js +0 -779
- 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 +0 -57
- package/dist/browser-extension/package.json +0 -41
- package/dist/browser-extension/popup.css +0 -468
- package/dist/browser-extension/popup.html +0 -191
- package/dist/browser-extension/popup.js +0 -278
- package/dist/browser-extension/safari/Info.plist +0 -21
- package/dist/browser-extension/safari/ai-worker.js +0 -571
- package/dist/browser-extension/safari/background.js +0 -2732
- package/dist/browser-extension/safari/callback.html +0 -17
- package/dist/browser-extension/safari/content-script.js +0 -1270
- 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 +0 -519
- package/dist/browser-extension/safari/popup.html +0 -242
- package/dist/browser-extension/safari/popup.js +0 -278
- package/dist/browser-extension/safari/sidebar.css +0 -1176
- package/dist/browser-extension/safari/sidebar.html +0 -280
- package/dist/browser-extension/safari/sidebar.js +0 -779
- package/dist/browser-extension/sidebar.css +0 -718
- package/dist/browser-extension/sidebar.html +0 -184
- package/dist/browser-extension/sidebar.js +0 -779
- package/dist/browser-extension/webgpu-ai.js +0 -495
- package/dist/cdp-bridge-server.js +0 -411
- package/dist/hanzo-ai-chrome-1.7.2.zip +0 -0
- package/dist/hanzo-ai-chrome-1.7.3.zip +0 -0
- package/dist/hanzo-ai-firefox-1.7.2.zip +0 -0
- package/dist/hanzo-ai-firefox-1.7.3.zip +0 -0
- package/dist/safari/Hanzo AI/Hanzo AI.xcodeproj/project.pbxproj +0 -1047
- package/dist/safari/Hanzo AI/Hanzo AI.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json +0 -11
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -98
- 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 +0 -6
- package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json +0 -21
- 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 +0 -20
- package/dist/safari/Hanzo AI/Shared (App)/Resources/Icon.png +0 -0
- package/dist/safari/Hanzo AI/Shared (App)/Resources/Script.js +0 -24
- package/dist/safari/Hanzo AI/Shared (App)/Resources/Style.css +0 -61
- package/dist/safari/Hanzo AI/Shared (App)/ViewController.swift +0 -81
- package/dist/safari/Hanzo AI/Shared (Extension)/SafariWebExtensionHandler.swift +0 -42
- package/dist/safari/Hanzo AI/iOS (App)/AppDelegate.swift +0 -24
- package/dist/safari/Hanzo AI/iOS (App)/Base.lproj/LaunchScreen.storyboard +0 -36
- package/dist/safari/Hanzo AI/iOS (App)/Base.lproj/Main.storyboard +0 -38
- package/dist/safari/Hanzo AI/iOS (App)/Info.plist +0 -27
- package/dist/safari/Hanzo AI/iOS (App)/SceneDelegate.swift +0 -18
- package/dist/safari/Hanzo AI/iOS (Extension)/Info.plist +0 -13
- package/dist/safari/Hanzo AI/macOS (App)/AppDelegate.swift +0 -21
- package/dist/safari/Hanzo AI/macOS (App)/Base.lproj/Main.storyboard +0 -124
- package/dist/safari/Hanzo AI/macOS (App)/Info.plist +0 -8
- package/dist/safari/Hanzo AI/macOS (Extension)/Info.plist +0 -13
- package/docs/_config.yml +0 -13
- package/docs/_layouts/default.html +0 -435
- package/docs/index.md +0 -54
- package/hanzo-ai-chrome-v1.7.6.zip +0 -0
- package/hanzo-ai-firefox-v1.7.6.zip +0 -0
- package/install-extension.sh +0 -75
- package/test/claude-integration.test.ts +0 -251
- package/test/server-frameworks-edge-cases.test.ts +0 -145
- package/test/server-frameworks.test.ts +0 -395
- package/test-cdp-connection.js +0 -140
- package/tsconfig.json +0 -19
|
@@ -1,875 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
(() => {
|
|
3
|
-
// src/auth.ts
|
|
4
|
-
function generateRandomString(length) {
|
|
5
|
-
const array = new Uint8Array(length);
|
|
6
|
-
crypto.getRandomValues(array);
|
|
7
|
-
return Array.from(array, (b) => b.toString(36).padStart(2, "0")).join("").slice(0, length);
|
|
8
|
-
}
|
|
9
|
-
async function sha256(plain) {
|
|
10
|
-
const encoder = new TextEncoder();
|
|
11
|
-
return crypto.subtle.digest("SHA-256", encoder.encode(plain));
|
|
12
|
-
}
|
|
13
|
-
function base64UrlEncode(buffer) {
|
|
14
|
-
const bytes = new Uint8Array(buffer);
|
|
15
|
-
let binary = "";
|
|
16
|
-
for (const byte of bytes) {
|
|
17
|
-
binary += String.fromCharCode(byte);
|
|
18
|
-
}
|
|
19
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
20
|
-
}
|
|
21
|
-
async function generatePkceChallenge() {
|
|
22
|
-
const codeVerifier = generateRandomString(64);
|
|
23
|
-
const hash = await sha256(codeVerifier);
|
|
24
|
-
const codeChallenge = base64UrlEncode(hash);
|
|
25
|
-
return { codeVerifier, codeChallenge };
|
|
26
|
-
}
|
|
27
|
-
function generateState() {
|
|
28
|
-
return generateRandomString(32);
|
|
29
|
-
}
|
|
30
|
-
var IAM_LOGIN = "https://hanzo.id";
|
|
31
|
-
var IAM_API = "https://iam.hanzo.ai";
|
|
32
|
-
var CLIENT_ID = "app-hanzo";
|
|
33
|
-
var SCOPES = "openid profile email";
|
|
34
|
-
function getRedirectUri() {
|
|
35
|
-
return "https://hanzo.ai/callback";
|
|
36
|
-
}
|
|
37
|
-
var STORAGE_KEYS = {
|
|
38
|
-
accessToken: "hanzo_iam_access_token",
|
|
39
|
-
refreshToken: "hanzo_iam_refresh_token",
|
|
40
|
-
idToken: "hanzo_iam_id_token",
|
|
41
|
-
expiresAt: "hanzo_iam_expires_at",
|
|
42
|
-
user: "hanzo_iam_user"
|
|
43
|
-
};
|
|
44
|
-
async function storeTokens(tokens) {
|
|
45
|
-
const data = {
|
|
46
|
-
[STORAGE_KEYS.accessToken]: tokens.access_token
|
|
47
|
-
};
|
|
48
|
-
if (tokens.refresh_token) data[STORAGE_KEYS.refreshToken] = tokens.refresh_token;
|
|
49
|
-
if (tokens.id_token) data[STORAGE_KEYS.idToken] = tokens.id_token;
|
|
50
|
-
if (tokens.expires_in) {
|
|
51
|
-
data[STORAGE_KEYS.expiresAt] = Date.now() + tokens.expires_in * 1e3;
|
|
52
|
-
}
|
|
53
|
-
return chrome.storage.local.set(data);
|
|
54
|
-
}
|
|
55
|
-
async function getStoredTokens() {
|
|
56
|
-
return new Promise((resolve) => {
|
|
57
|
-
chrome.storage.local.get(
|
|
58
|
-
[STORAGE_KEYS.accessToken, STORAGE_KEYS.refreshToken, STORAGE_KEYS.expiresAt],
|
|
59
|
-
(result) => {
|
|
60
|
-
resolve({
|
|
61
|
-
accessToken: result[STORAGE_KEYS.accessToken] || null,
|
|
62
|
-
refreshToken: result[STORAGE_KEYS.refreshToken] || null,
|
|
63
|
-
expiresAt: result[STORAGE_KEYS.expiresAt] || null
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
async function login() {
|
|
70
|
-
const { codeVerifier, codeChallenge } = await generatePkceChallenge();
|
|
71
|
-
const state = generateState();
|
|
72
|
-
const redirectUri = getRedirectUri();
|
|
73
|
-
const authorizeUrl = new URL(`${IAM_LOGIN}/login/oauth/authorize`);
|
|
74
|
-
authorizeUrl.searchParams.set("client_id", CLIENT_ID);
|
|
75
|
-
authorizeUrl.searchParams.set("response_type", "code");
|
|
76
|
-
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
77
|
-
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
78
|
-
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
79
|
-
authorizeUrl.searchParams.set("scope", SCOPES);
|
|
80
|
-
authorizeUrl.searchParams.set("state", state);
|
|
81
|
-
const callbackUrl = await openAuthTab(authorizeUrl.toString(), redirectUri);
|
|
82
|
-
const url = new URL(callbackUrl);
|
|
83
|
-
const returnedState = url.searchParams.get("state");
|
|
84
|
-
if (returnedState !== state) throw new Error("State mismatch \u2014 possible CSRF");
|
|
85
|
-
const code = url.searchParams.get("code");
|
|
86
|
-
const directToken = url.searchParams.get("access_token");
|
|
87
|
-
if (directToken) {
|
|
88
|
-
const tokens = {
|
|
89
|
-
access_token: directToken,
|
|
90
|
-
refresh_token: url.searchParams.get("refresh_token") || void 0,
|
|
91
|
-
token_type: "Bearer"
|
|
92
|
-
};
|
|
93
|
-
await storeTokens(tokens);
|
|
94
|
-
} else if (code) {
|
|
95
|
-
const tokenResponse = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
|
|
96
|
-
method: "POST",
|
|
97
|
-
headers: { "Content-Type": "application/json" },
|
|
98
|
-
body: JSON.stringify({
|
|
99
|
-
grant_type: "authorization_code",
|
|
100
|
-
client_id: CLIENT_ID,
|
|
101
|
-
code,
|
|
102
|
-
redirect_uri: redirectUri,
|
|
103
|
-
code_verifier: codeVerifier
|
|
104
|
-
})
|
|
105
|
-
});
|
|
106
|
-
if (!tokenResponse.ok) {
|
|
107
|
-
const errText = await tokenResponse.text();
|
|
108
|
-
throw new Error(`Token exchange failed: ${errText}`);
|
|
109
|
-
}
|
|
110
|
-
const tokens = await tokenResponse.json();
|
|
111
|
-
await storeTokens(tokens);
|
|
112
|
-
} else {
|
|
113
|
-
const error = url.searchParams.get("error_description") || url.searchParams.get("error") || "No authorization code or token";
|
|
114
|
-
throw new Error(error);
|
|
115
|
-
}
|
|
116
|
-
const { accessToken } = await getStoredTokens();
|
|
117
|
-
if (!accessToken) throw new Error("Login succeeded but no token was stored");
|
|
118
|
-
const user = await fetchUserInfo(accessToken);
|
|
119
|
-
await chrome.storage.local.set({ [STORAGE_KEYS.user]: user });
|
|
120
|
-
return user;
|
|
121
|
-
}
|
|
122
|
-
function openAuthTab(authorizeUrl, redirectUriPrefix) {
|
|
123
|
-
return new Promise((resolve, reject) => {
|
|
124
|
-
let authTabId;
|
|
125
|
-
const timeout = setTimeout(() => {
|
|
126
|
-
cleanup();
|
|
127
|
-
reject(new Error("Login timed out"));
|
|
128
|
-
}, 3e5);
|
|
129
|
-
function cleanup() {
|
|
130
|
-
clearTimeout(timeout);
|
|
131
|
-
chrome.tabs.onUpdated.removeListener(onTabUpdated);
|
|
132
|
-
chrome.tabs.onRemoved.removeListener(onTabRemoved);
|
|
133
|
-
}
|
|
134
|
-
function onTabUpdated(tabId, changeInfo) {
|
|
135
|
-
if (tabId !== authTabId || !changeInfo.url) return;
|
|
136
|
-
if (changeInfo.url.startsWith(redirectUriPrefix)) {
|
|
137
|
-
const callbackUrl = changeInfo.url;
|
|
138
|
-
cleanup();
|
|
139
|
-
chrome.tabs.remove(tabId).catch(() => {
|
|
140
|
-
});
|
|
141
|
-
resolve(callbackUrl);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
function onTabRemoved(tabId) {
|
|
145
|
-
if (tabId !== authTabId) return;
|
|
146
|
-
cleanup();
|
|
147
|
-
reject(new Error("Login cancelled"));
|
|
148
|
-
}
|
|
149
|
-
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
|
150
|
-
chrome.tabs.onRemoved.addListener(onTabRemoved);
|
|
151
|
-
chrome.tabs.create({ url: authorizeUrl }, (tab) => {
|
|
152
|
-
if (chrome.runtime.lastError || !tab?.id) {
|
|
153
|
-
cleanup();
|
|
154
|
-
reject(new Error(chrome.runtime.lastError?.message || "Failed to open login tab"));
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
authTabId = tab.id;
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
async function logout() {
|
|
162
|
-
await chrome.storage.local.remove(Object.values(STORAGE_KEYS));
|
|
163
|
-
}
|
|
164
|
-
async function getValidAccessToken() {
|
|
165
|
-
const { accessToken, refreshToken, expiresAt } = await getStoredTokens();
|
|
166
|
-
if (!accessToken) return null;
|
|
167
|
-
if (expiresAt && Date.now() < expiresAt - 6e4) {
|
|
168
|
-
return accessToken;
|
|
169
|
-
}
|
|
170
|
-
if (refreshToken) {
|
|
171
|
-
try {
|
|
172
|
-
const refreshed = await refreshAccessToken(refreshToken);
|
|
173
|
-
return refreshed;
|
|
174
|
-
} catch {
|
|
175
|
-
await logout();
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return accessToken;
|
|
180
|
-
}
|
|
181
|
-
async function refreshAccessToken(refreshToken) {
|
|
182
|
-
const response = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
|
|
183
|
-
method: "POST",
|
|
184
|
-
headers: { "Content-Type": "application/json" },
|
|
185
|
-
body: JSON.stringify({
|
|
186
|
-
grant_type: "refresh_token",
|
|
187
|
-
client_id: CLIENT_ID,
|
|
188
|
-
refresh_token: refreshToken
|
|
189
|
-
})
|
|
190
|
-
});
|
|
191
|
-
if (!response.ok) throw new Error("Token refresh failed");
|
|
192
|
-
const tokens = await response.json();
|
|
193
|
-
await storeTokens(tokens);
|
|
194
|
-
return tokens.access_token;
|
|
195
|
-
}
|
|
196
|
-
async function fetchUserInfo(accessToken) {
|
|
197
|
-
const response = await fetch(`${IAM_API}/api/userinfo`, {
|
|
198
|
-
headers: { Authorization: `Bearer ${accessToken}` }
|
|
199
|
-
});
|
|
200
|
-
if (!response.ok) throw new Error("Failed to fetch user info");
|
|
201
|
-
return response.json();
|
|
202
|
-
}
|
|
203
|
-
async function getUserInfo() {
|
|
204
|
-
return new Promise((resolve) => {
|
|
205
|
-
chrome.storage.local.get([STORAGE_KEYS.user], (result) => {
|
|
206
|
-
resolve(result[STORAGE_KEYS.user] || null);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
async function isAuthenticated() {
|
|
211
|
-
const { accessToken } = await getStoredTokens();
|
|
212
|
-
return !!accessToken;
|
|
213
|
-
}
|
|
214
|
-
async function getAuthStatus() {
|
|
215
|
-
const [authenticated, user] = await Promise.all([
|
|
216
|
-
isAuthenticated(),
|
|
217
|
-
getUserInfo()
|
|
218
|
-
]);
|
|
219
|
-
return { authenticated, user };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// src/chat-client.ts
|
|
223
|
-
var API_BASE = "https://api.hanzo.ai";
|
|
224
|
-
async function listModels(token) {
|
|
225
|
-
const response = await fetch(`${API_BASE}/v1/models`, {
|
|
226
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
227
|
-
});
|
|
228
|
-
if (!response.ok) {
|
|
229
|
-
throw new Error(`Failed to list models: ${response.status}`);
|
|
230
|
-
}
|
|
231
|
-
const data = await response.json();
|
|
232
|
-
return data.data || data.models || data || [];
|
|
233
|
-
}
|
|
234
|
-
async function chatCompletion(token, options) {
|
|
235
|
-
const response = await fetch(`${API_BASE}/v1/chat/completions`, {
|
|
236
|
-
method: "POST",
|
|
237
|
-
headers: {
|
|
238
|
-
"Content-Type": "application/json",
|
|
239
|
-
Authorization: `Bearer ${token}`
|
|
240
|
-
},
|
|
241
|
-
body: JSON.stringify(options)
|
|
242
|
-
});
|
|
243
|
-
if (!response.ok) {
|
|
244
|
-
const text = await response.text();
|
|
245
|
-
throw new Error(`Chat API error ${response.status}: ${text}`);
|
|
246
|
-
}
|
|
247
|
-
const data = await response.json();
|
|
248
|
-
return data.choices?.[0]?.message?.content || "";
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// src/background-firefox.ts
|
|
252
|
-
var controlSession = {
|
|
253
|
-
active: false,
|
|
254
|
-
tabId: null,
|
|
255
|
-
task: null,
|
|
256
|
-
startedAt: null
|
|
257
|
-
};
|
|
258
|
-
var HanzoFirefoxExtension = class {
|
|
259
|
-
constructor() {
|
|
260
|
-
this.wsConnection = null;
|
|
261
|
-
this.cdpPort = 9223;
|
|
262
|
-
this.reconnectTimeout = 3e3;
|
|
263
|
-
console.log("[Hanzo] Firefox extension initializing...");
|
|
264
|
-
this.loadPort().then(() => this.connect());
|
|
265
|
-
}
|
|
266
|
-
async loadPort() {
|
|
267
|
-
try {
|
|
268
|
-
const result = await browser.storage.local.get(["cdpPort"]);
|
|
269
|
-
if (result.cdpPort) this.cdpPort = result.cdpPort;
|
|
270
|
-
} catch {
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
connect() {
|
|
274
|
-
const url = `ws://localhost:${this.cdpPort}/cdp`;
|
|
275
|
-
console.log(`[Hanzo] Connecting to ${url}`);
|
|
276
|
-
this.wsConnection = new WebSocket(url);
|
|
277
|
-
this.wsConnection.onopen = () => {
|
|
278
|
-
console.log("[Hanzo] WebSocket CONNECTED");
|
|
279
|
-
this.register();
|
|
280
|
-
};
|
|
281
|
-
this.wsConnection.onmessage = async (event) => {
|
|
282
|
-
try {
|
|
283
|
-
const message = JSON.parse(event.data);
|
|
284
|
-
console.log("[Hanzo] Received:", message.method || "response");
|
|
285
|
-
const response = await this.handleCommand(message);
|
|
286
|
-
this.send(response);
|
|
287
|
-
} catch (e) {
|
|
288
|
-
console.error("[Hanzo] Message handler error:", e);
|
|
289
|
-
this.send({ id: 0, error: { code: -1, message: String(e) } });
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
this.wsConnection.onerror = () => {
|
|
293
|
-
};
|
|
294
|
-
this.wsConnection.onclose = () => {
|
|
295
|
-
console.log("[Hanzo] Disconnected, reconnecting...");
|
|
296
|
-
setTimeout(() => this.connect(), this.reconnectTimeout);
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
isConnected() {
|
|
300
|
-
return this.wsConnection?.readyState === WebSocket.OPEN;
|
|
301
|
-
}
|
|
302
|
-
sendToWs(data) {
|
|
303
|
-
if (this.wsConnection?.readyState === WebSocket.OPEN) {
|
|
304
|
-
this.wsConnection.send(JSON.stringify(data));
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
register() {
|
|
308
|
-
this.sendToWs({
|
|
309
|
-
type: "register",
|
|
310
|
-
role: "cdp-provider",
|
|
311
|
-
capabilities: [
|
|
312
|
-
"navigate",
|
|
313
|
-
"screenshot",
|
|
314
|
-
"click",
|
|
315
|
-
"type",
|
|
316
|
-
"evaluate",
|
|
317
|
-
"tabs",
|
|
318
|
-
"fill",
|
|
319
|
-
"reload",
|
|
320
|
-
"goBack",
|
|
321
|
-
"goForward"
|
|
322
|
-
]
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
send(data) {
|
|
326
|
-
this.sendToWs(data);
|
|
327
|
-
}
|
|
328
|
-
async handleCommand(message) {
|
|
329
|
-
const { id, method, params = {} } = message;
|
|
330
|
-
try {
|
|
331
|
-
const result = await this.executeMethod(method, params);
|
|
332
|
-
return { id, result };
|
|
333
|
-
} catch (e) {
|
|
334
|
-
return { id, error: { code: -1, message: String(e) } };
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
executeScriptWithTimeout(tabId, code, timeoutMs = 1e4) {
|
|
338
|
-
return Promise.race([
|
|
339
|
-
browser.tabs.executeScript(tabId, { code }),
|
|
340
|
-
new Promise(
|
|
341
|
-
(_, reject) => setTimeout(() => reject(new Error("executeScript timeout")), timeoutMs)
|
|
342
|
-
)
|
|
343
|
-
]);
|
|
344
|
-
}
|
|
345
|
-
async executeMethod(method, params) {
|
|
346
|
-
const [activeTab] = await browser.tabs.query({
|
|
347
|
-
active: true,
|
|
348
|
-
currentWindow: true
|
|
349
|
-
});
|
|
350
|
-
switch (method) {
|
|
351
|
-
case "Target.getTargets": {
|
|
352
|
-
const allTabs = await browser.tabs.query({});
|
|
353
|
-
return {
|
|
354
|
-
targetInfos: allTabs.map((tab) => ({
|
|
355
|
-
targetId: `tab-${tab.id}`,
|
|
356
|
-
type: "page",
|
|
357
|
-
title: tab.title || "",
|
|
358
|
-
url: tab.url || ""
|
|
359
|
-
}))
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
case "hanzo.url": {
|
|
363
|
-
if (activeTab) return { result: { value: activeTab.url || "" } };
|
|
364
|
-
throw new Error("No active tab");
|
|
365
|
-
}
|
|
366
|
-
case "hanzo.title": {
|
|
367
|
-
if (activeTab) return { result: { value: activeTab.title || "" } };
|
|
368
|
-
throw new Error("No active tab");
|
|
369
|
-
}
|
|
370
|
-
case "hanzo.tabInfo": {
|
|
371
|
-
if (activeTab) {
|
|
372
|
-
return {
|
|
373
|
-
result: {
|
|
374
|
-
value: {
|
|
375
|
-
id: activeTab.id,
|
|
376
|
-
url: activeTab.url,
|
|
377
|
-
title: activeTab.title,
|
|
378
|
-
status: activeTab.status,
|
|
379
|
-
favIconUrl: activeTab.favIconUrl
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
throw new Error("No active tab");
|
|
385
|
-
}
|
|
386
|
-
case "Page.navigate": {
|
|
387
|
-
if (activeTab?.id) {
|
|
388
|
-
this.notifyOverlay(activeTab.id, "ai.control.status", { text: `Navigating to ${params.url}` });
|
|
389
|
-
await browser.tabs.update(activeTab.id, { url: params.url });
|
|
390
|
-
return { frameId: "main" };
|
|
391
|
-
}
|
|
392
|
-
throw new Error("No active tab");
|
|
393
|
-
}
|
|
394
|
-
case "Page.reload": {
|
|
395
|
-
if (activeTab?.id) {
|
|
396
|
-
await browser.tabs.reload(activeTab.id);
|
|
397
|
-
return { success: true };
|
|
398
|
-
}
|
|
399
|
-
throw new Error("No active tab");
|
|
400
|
-
}
|
|
401
|
-
case "Page.goBack": {
|
|
402
|
-
if (activeTab?.id) {
|
|
403
|
-
await browser.tabs.goBack(activeTab.id);
|
|
404
|
-
return { success: true };
|
|
405
|
-
}
|
|
406
|
-
throw new Error("No active tab");
|
|
407
|
-
}
|
|
408
|
-
case "Page.goForward": {
|
|
409
|
-
if (activeTab?.id) {
|
|
410
|
-
await browser.tabs.goForward(activeTab.id);
|
|
411
|
-
return { success: true };
|
|
412
|
-
}
|
|
413
|
-
throw new Error("No active tab");
|
|
414
|
-
}
|
|
415
|
-
case "Runtime.evaluate": {
|
|
416
|
-
if (activeTab?.id) {
|
|
417
|
-
const expression = params.expression;
|
|
418
|
-
const timeout = params.timeout || 1e4;
|
|
419
|
-
const results = await this.executeScriptWithTimeout(activeTab.id, expression, timeout);
|
|
420
|
-
return { result: { value: results[0] } };
|
|
421
|
-
}
|
|
422
|
-
throw new Error("No active tab");
|
|
423
|
-
}
|
|
424
|
-
case "Page.captureScreenshot":
|
|
425
|
-
case "hanzo.screenshot": {
|
|
426
|
-
const format = params.format || "jpeg";
|
|
427
|
-
const quality = params.quality || (format === "jpeg" ? 80 : void 0);
|
|
428
|
-
const opts = { format };
|
|
429
|
-
if (quality !== void 0) opts.quality = quality;
|
|
430
|
-
const dataUrl = await browser.tabs.captureVisibleTab(opts);
|
|
431
|
-
const base64 = dataUrl.replace(/^data:image\/\w+;base64,/, "");
|
|
432
|
-
return { data: base64 };
|
|
433
|
-
}
|
|
434
|
-
case "hanzo.click": {
|
|
435
|
-
if (activeTab?.id && params.selector) {
|
|
436
|
-
const selector = this.escapeSelector(params.selector);
|
|
437
|
-
this.notifyOverlay(activeTab.id, "ai.control.highlight", { selector: params.selector });
|
|
438
|
-
this.notifyOverlay(activeTab.id, "ai.control.status", { text: `Clicking ${params.selector}` });
|
|
439
|
-
await this.executeScriptWithTimeout(
|
|
440
|
-
activeTab.id,
|
|
441
|
-
`document.querySelector('${selector}')?.click()`
|
|
442
|
-
);
|
|
443
|
-
return { success: true };
|
|
444
|
-
}
|
|
445
|
-
throw new Error("No active tab or selector");
|
|
446
|
-
}
|
|
447
|
-
case "hanzo.fill": {
|
|
448
|
-
if (activeTab?.id && params.selector) {
|
|
449
|
-
const selector = this.escapeSelector(params.selector);
|
|
450
|
-
const value = this.escapeValue(params.value || "");
|
|
451
|
-
this.notifyOverlay(activeTab.id, "ai.control.highlight", { selector: params.selector });
|
|
452
|
-
this.notifyOverlay(activeTab.id, "ai.control.status", { text: `Filling ${params.selector}` });
|
|
453
|
-
await this.executeScriptWithTimeout(activeTab.id, `
|
|
454
|
-
var el = document.querySelector('${selector}');
|
|
455
|
-
if (el) {
|
|
456
|
-
el.value = '${value}';
|
|
457
|
-
el.dispatchEvent(new Event('input', {bubbles: true}));
|
|
458
|
-
el.dispatchEvent(new Event('change', {bubbles: true}));
|
|
459
|
-
}
|
|
460
|
-
`);
|
|
461
|
-
return { success: true };
|
|
462
|
-
}
|
|
463
|
-
throw new Error("No active tab or selector");
|
|
464
|
-
}
|
|
465
|
-
case "hanzo.control.start": {
|
|
466
|
-
const targetTabId = await this.resolveControlTabId(params.tabId);
|
|
467
|
-
if (targetTabId) {
|
|
468
|
-
await this.startControlSession(targetTabId, params.task);
|
|
469
|
-
return { success: true };
|
|
470
|
-
}
|
|
471
|
-
throw new Error("No active tab");
|
|
472
|
-
}
|
|
473
|
-
case "hanzo.control.stop": {
|
|
474
|
-
await this.stopControlSession();
|
|
475
|
-
return { success: true };
|
|
476
|
-
}
|
|
477
|
-
case "hanzo.type": {
|
|
478
|
-
if (activeTab?.id && params.text) {
|
|
479
|
-
const text = this.escapeValue(params.text);
|
|
480
|
-
await this.executeScriptWithTimeout(activeTab.id, `
|
|
481
|
-
var el = document.activeElement;
|
|
482
|
-
if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) {
|
|
483
|
-
el.value += '${text}';
|
|
484
|
-
el.dispatchEvent(new Event('input', {bubbles: true}));
|
|
485
|
-
}
|
|
486
|
-
`);
|
|
487
|
-
return { success: true };
|
|
488
|
-
}
|
|
489
|
-
throw new Error("No active tab or text");
|
|
490
|
-
}
|
|
491
|
-
case "Browser.getVersion": {
|
|
492
|
-
return {
|
|
493
|
-
protocolVersion: "1.3",
|
|
494
|
-
product: "Hanzo Firefox Extension",
|
|
495
|
-
userAgent: navigator.userAgent
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
case "hanzo.getTabs":
|
|
499
|
-
case "hanzo.listTabs": {
|
|
500
|
-
const tabs = await browser.tabs.query({});
|
|
501
|
-
return {
|
|
502
|
-
tabs: tabs.map((tab) => ({
|
|
503
|
-
id: tab.id,
|
|
504
|
-
url: tab.url,
|
|
505
|
-
title: tab.title,
|
|
506
|
-
active: tab.active
|
|
507
|
-
}))
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
default:
|
|
511
|
-
console.warn("[Hanzo] Unknown method:", method);
|
|
512
|
-
return { error: `Unknown method: ${method}` };
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
notifyOverlay(tabId, action, data) {
|
|
516
|
-
browser.tabs.sendMessage(tabId, { action, ...data }).catch(() => {
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
async getActiveTabId() {
|
|
520
|
-
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
|
521
|
-
return tab?.id ?? null;
|
|
522
|
-
}
|
|
523
|
-
async resolveControlTabId(tabId) {
|
|
524
|
-
if (typeof tabId === "number") return tabId;
|
|
525
|
-
if (controlSession.active && controlSession.tabId !== null) return controlSession.tabId;
|
|
526
|
-
return this.getActiveTabId();
|
|
527
|
-
}
|
|
528
|
-
async startControlSession(tabId, task) {
|
|
529
|
-
if (controlSession.active && controlSession.tabId !== null && controlSession.tabId !== tabId) {
|
|
530
|
-
this.notifyOverlay(controlSession.tabId, "ai.control.stop", {});
|
|
531
|
-
}
|
|
532
|
-
controlSession.active = true;
|
|
533
|
-
controlSession.tabId = tabId;
|
|
534
|
-
controlSession.task = task || "AI is controlling this page";
|
|
535
|
-
controlSession.startedAt = Date.now();
|
|
536
|
-
this.notifyOverlay(tabId, "ai.control.start", { task: controlSession.task });
|
|
537
|
-
}
|
|
538
|
-
async stopControlSession() {
|
|
539
|
-
if (controlSession.active && controlSession.tabId !== null) {
|
|
540
|
-
this.notifyOverlay(controlSession.tabId, "ai.control.stop", {});
|
|
541
|
-
}
|
|
542
|
-
controlSession.active = false;
|
|
543
|
-
controlSession.tabId = null;
|
|
544
|
-
controlSession.task = null;
|
|
545
|
-
controlSession.startedAt = null;
|
|
546
|
-
}
|
|
547
|
-
escapeSelector(selector) {
|
|
548
|
-
return selector.replace(/'/g, "\\'");
|
|
549
|
-
}
|
|
550
|
-
escapeValue(value) {
|
|
551
|
-
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
async function getRagConfig() {
|
|
555
|
-
const result = await browser.storage.local.get([
|
|
556
|
-
"hanzo_rag_endpoint",
|
|
557
|
-
"hanzo_rag_api_key",
|
|
558
|
-
"hanzo_rag_kb",
|
|
559
|
-
"hanzo_rag_top_k",
|
|
560
|
-
"hanzo_rag_include_tab_context"
|
|
561
|
-
]);
|
|
562
|
-
const topKParsed = Number.parseInt(String(result.hanzo_rag_top_k ?? 5), 10);
|
|
563
|
-
return {
|
|
564
|
-
endpoint: String(result.hanzo_rag_endpoint || "").trim(),
|
|
565
|
-
apiKey: String(result.hanzo_rag_api_key || "").trim(),
|
|
566
|
-
knowledgeBase: String(result.hanzo_rag_kb || "").trim(),
|
|
567
|
-
topK: Number.isFinite(topKParsed) ? Math.max(1, Math.min(topKParsed, 20)) : 5,
|
|
568
|
-
includeTabContext: result.hanzo_rag_include_tab_context !== false
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
function normalizeRagSnippets(raw) {
|
|
572
|
-
if (!raw) return [];
|
|
573
|
-
const candidates = [];
|
|
574
|
-
if (Array.isArray(raw)) candidates.push(...raw);
|
|
575
|
-
if (Array.isArray(raw?.snippets)) candidates.push(...raw.snippets);
|
|
576
|
-
if (Array.isArray(raw?.documents)) candidates.push(...raw.documents);
|
|
577
|
-
if (Array.isArray(raw?.items)) candidates.push(...raw.items);
|
|
578
|
-
if (Array.isArray(raw?.results)) candidates.push(...raw.results);
|
|
579
|
-
if (!candidates.length && typeof raw?.content === "string") {
|
|
580
|
-
candidates.push({ content: raw.content, source: raw.source || "rag-endpoint" });
|
|
581
|
-
}
|
|
582
|
-
return candidates.map((item) => {
|
|
583
|
-
const content = String(item?.content ?? item?.text ?? item?.snippet ?? item?.body ?? "").trim();
|
|
584
|
-
if (!content) return null;
|
|
585
|
-
return {
|
|
586
|
-
content,
|
|
587
|
-
title: item?.title ? String(item.title) : void 0,
|
|
588
|
-
source: item?.source ? String(item.source) : void 0,
|
|
589
|
-
score: typeof item?.score === "number" ? item.score : void 0,
|
|
590
|
-
url: item?.url ? String(item.url) : void 0
|
|
591
|
-
};
|
|
592
|
-
}).filter((item) => !!item).slice(0, 20);
|
|
593
|
-
}
|
|
594
|
-
async function queryRagEndpoint(params) {
|
|
595
|
-
if (!params.endpoint) return [];
|
|
596
|
-
const headers = { "Content-Type": "application/json" };
|
|
597
|
-
if (params.apiKey) headers.Authorization = `Bearer ${params.apiKey}`;
|
|
598
|
-
const response = await fetch(params.endpoint, {
|
|
599
|
-
method: "POST",
|
|
600
|
-
headers,
|
|
601
|
-
body: JSON.stringify({
|
|
602
|
-
query: params.query,
|
|
603
|
-
top_k: params.topK,
|
|
604
|
-
knowledge_base: params.knowledgeBase || void 0,
|
|
605
|
-
page_context: params.includeTabContext ? params.pageContext : void 0,
|
|
606
|
-
source: "hanzo-browser-extension-firefox"
|
|
607
|
-
})
|
|
608
|
-
});
|
|
609
|
-
if (!response.ok) {
|
|
610
|
-
const text = await response.text();
|
|
611
|
-
throw new Error(`RAG endpoint error ${response.status}: ${text || "Unknown error"}`);
|
|
612
|
-
}
|
|
613
|
-
const raw = await response.json();
|
|
614
|
-
return normalizeRagSnippets(raw).map((snippet) => ({
|
|
615
|
-
...snippet,
|
|
616
|
-
source: snippet.source || "rag-endpoint"
|
|
617
|
-
}));
|
|
618
|
-
}
|
|
619
|
-
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
620
|
-
(async () => {
|
|
621
|
-
switch (request.action) {
|
|
622
|
-
// --- Auth (shared module — same code as Chrome) ---
|
|
623
|
-
case "auth.login":
|
|
624
|
-
try {
|
|
625
|
-
const user = await login();
|
|
626
|
-
sendResponse({ success: true, user });
|
|
627
|
-
} catch (e) {
|
|
628
|
-
sendResponse({ success: false, error: e.message });
|
|
629
|
-
}
|
|
630
|
-
break;
|
|
631
|
-
case "auth.logout":
|
|
632
|
-
try {
|
|
633
|
-
await logout();
|
|
634
|
-
sendResponse({ success: true });
|
|
635
|
-
} catch (e) {
|
|
636
|
-
sendResponse({ success: false, error: e.message });
|
|
637
|
-
}
|
|
638
|
-
break;
|
|
639
|
-
case "auth.status":
|
|
640
|
-
try {
|
|
641
|
-
const status = await getAuthStatus();
|
|
642
|
-
sendResponse({ success: true, ...status });
|
|
643
|
-
} catch (e) {
|
|
644
|
-
sendResponse({ success: false, error: e.message });
|
|
645
|
-
}
|
|
646
|
-
break;
|
|
647
|
-
case "auth.getToken":
|
|
648
|
-
try {
|
|
649
|
-
const token = await getValidAccessToken();
|
|
650
|
-
sendResponse({ success: !!token, token });
|
|
651
|
-
} catch (e) {
|
|
652
|
-
sendResponse({ success: false, error: e.message });
|
|
653
|
-
}
|
|
654
|
-
break;
|
|
655
|
-
// --- Cloud Chat (shared module) ---
|
|
656
|
-
case "chat.listModels":
|
|
657
|
-
try {
|
|
658
|
-
const token = await getValidAccessToken();
|
|
659
|
-
if (!token) {
|
|
660
|
-
sendResponse({ success: false, error: "Not authenticated" });
|
|
661
|
-
break;
|
|
662
|
-
}
|
|
663
|
-
const models = await listModels(token);
|
|
664
|
-
sendResponse({ success: true, models });
|
|
665
|
-
} catch (e) {
|
|
666
|
-
sendResponse({ success: false, error: e.message });
|
|
667
|
-
}
|
|
668
|
-
break;
|
|
669
|
-
case "chat.complete":
|
|
670
|
-
try {
|
|
671
|
-
const token = await getValidAccessToken();
|
|
672
|
-
if (!token) {
|
|
673
|
-
sendResponse({ success: false, error: "Not authenticated" });
|
|
674
|
-
break;
|
|
675
|
-
}
|
|
676
|
-
const model = String(request.model || "gpt-4o");
|
|
677
|
-
const messagesInput = Array.isArray(request.messages) ? request.messages : [];
|
|
678
|
-
const messages = messagesInput.filter((m) => m && typeof m.content === "string" && typeof m.role === "string").map((m) => ({
|
|
679
|
-
role: m.role === "system" || m.role === "assistant" ? m.role : "user",
|
|
680
|
-
content: String(m.content)
|
|
681
|
-
})).slice(-24);
|
|
682
|
-
if (!messages.length) {
|
|
683
|
-
sendResponse({ success: false, error: "messages are required" });
|
|
684
|
-
break;
|
|
685
|
-
}
|
|
686
|
-
const content = await chatCompletion(token, {
|
|
687
|
-
model,
|
|
688
|
-
messages,
|
|
689
|
-
temperature: typeof request.temperature === "number" ? request.temperature : void 0,
|
|
690
|
-
max_tokens: typeof request.max_tokens === "number" ? request.max_tokens : void 0
|
|
691
|
-
});
|
|
692
|
-
sendResponse({ success: true, content });
|
|
693
|
-
} catch (e) {
|
|
694
|
-
sendResponse({ success: false, error: e.message });
|
|
695
|
-
}
|
|
696
|
-
break;
|
|
697
|
-
// --- ZAP Protocol (not available on Firefox — return proper status) ---
|
|
698
|
-
case "zap.status":
|
|
699
|
-
sendResponse({ success: true, zap: { connected: false, mcps: [], extensionId: "firefox" } });
|
|
700
|
-
break;
|
|
701
|
-
case "zap.discover":
|
|
702
|
-
sendResponse({ success: true, mcps: [] });
|
|
703
|
-
break;
|
|
704
|
-
case "zap.listTools":
|
|
705
|
-
sendResponse({ success: true, tools: [] });
|
|
706
|
-
break;
|
|
707
|
-
case "zap.connect":
|
|
708
|
-
case "zap.callTool":
|
|
709
|
-
sendResponse({ success: false, error: "ZAP protocol not available on Firefox yet" });
|
|
710
|
-
break;
|
|
711
|
-
// --- Element selection (click-to-code) ---
|
|
712
|
-
case "elementSelected":
|
|
713
|
-
if (hanzoExtension.isConnected()) {
|
|
714
|
-
hanzoExtension.sendToWs({
|
|
715
|
-
type: "elementSelected",
|
|
716
|
-
data: request.data
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
sendResponse({ success: true });
|
|
720
|
-
break;
|
|
721
|
-
// --- Tab Filesystem ---
|
|
722
|
-
case "listTabFS": {
|
|
723
|
-
const allTabs = await browser.tabs.query({});
|
|
724
|
-
sendResponse({
|
|
725
|
-
success: true,
|
|
726
|
-
tabs: allTabs.map((tab) => ({
|
|
727
|
-
id: tab.id,
|
|
728
|
-
url: tab.url,
|
|
729
|
-
title: tab.title,
|
|
730
|
-
active: tab.active
|
|
731
|
-
}))
|
|
732
|
-
});
|
|
733
|
-
break;
|
|
734
|
-
}
|
|
735
|
-
case "readTabFS":
|
|
736
|
-
sendResponse({ success: false, error: "Tab filesystem read not available on Firefox" });
|
|
737
|
-
break;
|
|
738
|
-
case "writeTabFS":
|
|
739
|
-
sendResponse({ success: false, error: "Tab filesystem write not available on Firefox" });
|
|
740
|
-
break;
|
|
741
|
-
// --- WebGPU / Local AI ---
|
|
742
|
-
case "checkWebGPU": {
|
|
743
|
-
try {
|
|
744
|
-
if (!navigator.gpu) {
|
|
745
|
-
sendResponse({ available: false });
|
|
746
|
-
break;
|
|
747
|
-
}
|
|
748
|
-
const adapter = await navigator.gpu.requestAdapter();
|
|
749
|
-
sendResponse({
|
|
750
|
-
available: !!adapter,
|
|
751
|
-
adapter: adapter?.name || "Unknown GPU",
|
|
752
|
-
models: []
|
|
753
|
-
});
|
|
754
|
-
} catch {
|
|
755
|
-
sendResponse({ available: false });
|
|
756
|
-
}
|
|
757
|
-
break;
|
|
758
|
-
}
|
|
759
|
-
case "runLocalAI":
|
|
760
|
-
sendResponse({ success: false, error: "Local AI not available on Firefox" });
|
|
761
|
-
break;
|
|
762
|
-
case "listAgents":
|
|
763
|
-
sendResponse({ success: true, agents: [] });
|
|
764
|
-
break;
|
|
765
|
-
case "stopAgent":
|
|
766
|
-
sendResponse({ success: false, error: "Agent workers not available on Firefox" });
|
|
767
|
-
break;
|
|
768
|
-
case "launchAIWorker":
|
|
769
|
-
sendResponse({ success: false, error: "AI workers not available on Firefox" });
|
|
770
|
-
break;
|
|
771
|
-
// --- RAG ---
|
|
772
|
-
case "rag.query": {
|
|
773
|
-
try {
|
|
774
|
-
const config = await getRagConfig();
|
|
775
|
-
const topKInput = Number.parseInt(String(request.topK ?? config.topK), 10);
|
|
776
|
-
const params = {
|
|
777
|
-
endpoint: String(request.endpoint ?? config.endpoint ?? "").trim(),
|
|
778
|
-
apiKey: String(request.apiKey ?? config.apiKey ?? "").trim(),
|
|
779
|
-
query: String(request.query || "").trim(),
|
|
780
|
-
topK: Number.isFinite(topKInput) ? Math.max(1, Math.min(topKInput, 20)) : 5,
|
|
781
|
-
knowledgeBase: String(request.knowledgeBase ?? config.knowledgeBase ?? "").trim(),
|
|
782
|
-
includeTabContext: request.includeTabContext !== void 0 ? !!request.includeTabContext : config.includeTabContext,
|
|
783
|
-
pageContext: request.pageContext && typeof request.pageContext === "object" ? {
|
|
784
|
-
url: request.pageContext.url ? String(request.pageContext.url) : void 0,
|
|
785
|
-
title: request.pageContext.title ? String(request.pageContext.title) : void 0
|
|
786
|
-
} : void 0
|
|
787
|
-
};
|
|
788
|
-
if (!params.query) {
|
|
789
|
-
sendResponse({ success: false, error: "Query is required" });
|
|
790
|
-
break;
|
|
791
|
-
}
|
|
792
|
-
if (!params.endpoint) {
|
|
793
|
-
sendResponse({ success: true, source: "none", snippets: [] });
|
|
794
|
-
break;
|
|
795
|
-
}
|
|
796
|
-
const snippets = await queryRagEndpoint(params);
|
|
797
|
-
sendResponse({ success: true, source: snippets.length ? "rag-endpoint" : "none", snippets });
|
|
798
|
-
} catch (e) {
|
|
799
|
-
sendResponse({ success: false, error: e.message });
|
|
800
|
-
}
|
|
801
|
-
break;
|
|
802
|
-
}
|
|
803
|
-
// --- Bridge status ---
|
|
804
|
-
case "bridge.status":
|
|
805
|
-
sendResponse({
|
|
806
|
-
success: true,
|
|
807
|
-
connected: hanzoExtension.isConnected(),
|
|
808
|
-
browsers: hanzoExtension.isConnected() ? ["firefox"] : []
|
|
809
|
-
});
|
|
810
|
-
break;
|
|
811
|
-
// --- Config ---
|
|
812
|
-
case "config.save":
|
|
813
|
-
sendResponse({ success: true });
|
|
814
|
-
break;
|
|
815
|
-
// --- In-page overlay (forwarded to content script) ---
|
|
816
|
-
case "page.overlay.toggle":
|
|
817
|
-
case "page.overlay.show":
|
|
818
|
-
case "page.overlay.hide":
|
|
819
|
-
case "page.overlay.status": {
|
|
820
|
-
try {
|
|
821
|
-
const tabId = typeof request.tabId === "number" ? request.tabId : sender.tab?.id ?? await hanzoExtension.getActiveTabId();
|
|
822
|
-
if (tabId === null) {
|
|
823
|
-
sendResponse({ success: false, error: "No target tab" });
|
|
824
|
-
break;
|
|
825
|
-
}
|
|
826
|
-
const response = await browser.tabs.sendMessage(tabId, { action: request.action }).catch(() => null);
|
|
827
|
-
sendResponse({ success: true, tabId, ...response || {} });
|
|
828
|
-
} catch (e) {
|
|
829
|
-
sendResponse({ success: false, error: e.message });
|
|
830
|
-
}
|
|
831
|
-
break;
|
|
832
|
-
}
|
|
833
|
-
// --- AI Control Overlay ---
|
|
834
|
-
case "ai.control.start":
|
|
835
|
-
case "ai.control.cursor":
|
|
836
|
-
case "ai.control.highlight":
|
|
837
|
-
case "ai.control.status": {
|
|
838
|
-
const targetTabId = request.tabId || controlSession.tabId || await hanzoExtension.getActiveTabId();
|
|
839
|
-
if (!targetTabId) {
|
|
840
|
-
sendResponse({ success: false, error: "No target tab found" });
|
|
841
|
-
break;
|
|
842
|
-
}
|
|
843
|
-
if (request.action !== "ai.control.start" && controlSession.active && controlSession.tabId && targetTabId !== controlSession.tabId) {
|
|
844
|
-
sendResponse({ success: false, error: `Control locked to tab ${controlSession.tabId}` });
|
|
845
|
-
break;
|
|
846
|
-
}
|
|
847
|
-
if (request.action === "ai.control.start") {
|
|
848
|
-
await hanzoExtension.startControlSession(targetTabId, request.task);
|
|
849
|
-
} else {
|
|
850
|
-
browser.tabs.sendMessage(targetTabId, request).catch(() => {
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
sendResponse({ success: true, tabId: targetTabId });
|
|
854
|
-
break;
|
|
855
|
-
}
|
|
856
|
-
case "ai.control.stop":
|
|
857
|
-
case "ai.control.cancel":
|
|
858
|
-
await hanzoExtension.stopControlSession();
|
|
859
|
-
sendResponse({ success: true });
|
|
860
|
-
break;
|
|
861
|
-
default:
|
|
862
|
-
sendResponse({ success: false, error: `Unknown action: ${request.action}` });
|
|
863
|
-
break;
|
|
864
|
-
}
|
|
865
|
-
})();
|
|
866
|
-
return true;
|
|
867
|
-
});
|
|
868
|
-
var hanzoExtension = new HanzoFirefoxExtension();
|
|
869
|
-
browser.tabs.onRemoved.addListener((tabId) => {
|
|
870
|
-
if (controlSession.active && controlSession.tabId === tabId) {
|
|
871
|
-
void hanzoExtension.stopControlSession();
|
|
872
|
-
}
|
|
873
|
-
});
|
|
874
|
-
console.log("[Hanzo] Firefox background script loaded");
|
|
875
|
-
})();
|