@hanzo/browser-extension 1.7.2 → 1.7.5

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.
@@ -1,5 +1,241 @@
1
1
  "use strict";
2
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_BASE = "https://hanzo.id";
31
+ var CLIENT_ID = "app-hanzo";
32
+ var SCOPES = "openid profile email";
33
+ function getRedirectUri() {
34
+ return "https://hanzo.ai/callback";
35
+ }
36
+ var STORAGE_KEYS = {
37
+ accessToken: "hanzo_iam_access_token",
38
+ refreshToken: "hanzo_iam_refresh_token",
39
+ idToken: "hanzo_iam_id_token",
40
+ expiresAt: "hanzo_iam_expires_at",
41
+ user: "hanzo_iam_user"
42
+ };
43
+ async function storeTokens(tokens) {
44
+ const data = {
45
+ [STORAGE_KEYS.accessToken]: tokens.access_token
46
+ };
47
+ if (tokens.refresh_token) data[STORAGE_KEYS.refreshToken] = tokens.refresh_token;
48
+ if (tokens.id_token) data[STORAGE_KEYS.idToken] = tokens.id_token;
49
+ if (tokens.expires_in) {
50
+ data[STORAGE_KEYS.expiresAt] = Date.now() + tokens.expires_in * 1e3;
51
+ }
52
+ return chrome.storage.local.set(data);
53
+ }
54
+ async function getStoredTokens() {
55
+ return new Promise((resolve) => {
56
+ chrome.storage.local.get(
57
+ [STORAGE_KEYS.accessToken, STORAGE_KEYS.refreshToken, STORAGE_KEYS.expiresAt],
58
+ (result) => {
59
+ resolve({
60
+ accessToken: result[STORAGE_KEYS.accessToken] || null,
61
+ refreshToken: result[STORAGE_KEYS.refreshToken] || null,
62
+ expiresAt: result[STORAGE_KEYS.expiresAt] || null
63
+ });
64
+ }
65
+ );
66
+ });
67
+ }
68
+ async function login() {
69
+ const { codeVerifier, codeChallenge } = await generatePkceChallenge();
70
+ const state = generateState();
71
+ const redirectUri = getRedirectUri();
72
+ const authorizeUrl = new URL(`${IAM_BASE}/login/oauth/authorize`);
73
+ authorizeUrl.searchParams.set("client_id", CLIENT_ID);
74
+ authorizeUrl.searchParams.set("response_type", "code");
75
+ authorizeUrl.searchParams.set("redirect_uri", redirectUri);
76
+ authorizeUrl.searchParams.set("code_challenge", codeChallenge);
77
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
78
+ authorizeUrl.searchParams.set("scope", SCOPES);
79
+ authorizeUrl.searchParams.set("state", state);
80
+ const callbackUrl = await openAuthTab(authorizeUrl.toString(), redirectUri);
81
+ const url = new URL(callbackUrl);
82
+ const returnedState = url.searchParams.get("state");
83
+ if (returnedState !== state) throw new Error("State mismatch \u2014 possible CSRF");
84
+ const code = url.searchParams.get("code");
85
+ if (!code) {
86
+ const error = url.searchParams.get("error_description") || url.searchParams.get("error") || "No authorization code";
87
+ throw new Error(error);
88
+ }
89
+ const tokenResponse = await fetch(`${IAM_BASE}/oauth/token`, {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
92
+ body: new URLSearchParams({
93
+ grant_type: "authorization_code",
94
+ client_id: CLIENT_ID,
95
+ code,
96
+ redirect_uri: redirectUri,
97
+ code_verifier: codeVerifier
98
+ })
99
+ });
100
+ if (!tokenResponse.ok) {
101
+ const errText = await tokenResponse.text();
102
+ throw new Error(`Token exchange failed: ${errText}`);
103
+ }
104
+ const tokens = await tokenResponse.json();
105
+ await storeTokens(tokens);
106
+ const user = await fetchUserInfo(tokens.access_token);
107
+ await chrome.storage.local.set({ [STORAGE_KEYS.user]: user });
108
+ return user;
109
+ }
110
+ function openAuthTab(authorizeUrl, redirectUriPrefix) {
111
+ return new Promise((resolve, reject) => {
112
+ let authTabId;
113
+ const timeout = setTimeout(() => {
114
+ cleanup();
115
+ reject(new Error("Login timed out"));
116
+ }, 3e5);
117
+ function cleanup() {
118
+ clearTimeout(timeout);
119
+ chrome.tabs.onUpdated.removeListener(onTabUpdated);
120
+ chrome.tabs.onRemoved.removeListener(onTabRemoved);
121
+ }
122
+ function onTabUpdated(tabId, changeInfo) {
123
+ if (tabId !== authTabId || !changeInfo.url) return;
124
+ if (changeInfo.url.startsWith(redirectUriPrefix)) {
125
+ const callbackUrl = changeInfo.url;
126
+ cleanup();
127
+ chrome.tabs.remove(tabId).catch(() => {
128
+ });
129
+ resolve(callbackUrl);
130
+ }
131
+ }
132
+ function onTabRemoved(tabId) {
133
+ if (tabId !== authTabId) return;
134
+ cleanup();
135
+ reject(new Error("Login cancelled"));
136
+ }
137
+ chrome.tabs.onUpdated.addListener(onTabUpdated);
138
+ chrome.tabs.onRemoved.addListener(onTabRemoved);
139
+ chrome.tabs.create({ url: authorizeUrl }, (tab) => {
140
+ if (chrome.runtime.lastError || !tab?.id) {
141
+ cleanup();
142
+ reject(new Error(chrome.runtime.lastError?.message || "Failed to open login tab"));
143
+ return;
144
+ }
145
+ authTabId = tab.id;
146
+ });
147
+ });
148
+ }
149
+ async function logout() {
150
+ await chrome.storage.local.remove(Object.values(STORAGE_KEYS));
151
+ }
152
+ async function getValidAccessToken() {
153
+ const { accessToken, refreshToken, expiresAt } = await getStoredTokens();
154
+ if (!accessToken) return null;
155
+ if (expiresAt && Date.now() < expiresAt - 6e4) {
156
+ return accessToken;
157
+ }
158
+ if (refreshToken) {
159
+ try {
160
+ const refreshed = await refreshAccessToken(refreshToken);
161
+ return refreshed;
162
+ } catch {
163
+ await logout();
164
+ return null;
165
+ }
166
+ }
167
+ return accessToken;
168
+ }
169
+ async function refreshAccessToken(refreshToken) {
170
+ const response = await fetch(`${IAM_BASE}/oauth/token`, {
171
+ method: "POST",
172
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
173
+ body: new URLSearchParams({
174
+ grant_type: "refresh_token",
175
+ client_id: CLIENT_ID,
176
+ refresh_token: refreshToken
177
+ })
178
+ });
179
+ if (!response.ok) throw new Error("Token refresh failed");
180
+ const tokens = await response.json();
181
+ await storeTokens(tokens);
182
+ return tokens.access_token;
183
+ }
184
+ async function fetchUserInfo(accessToken) {
185
+ const response = await fetch(`${IAM_BASE}/oauth/userinfo`, {
186
+ headers: { Authorization: `Bearer ${accessToken}` }
187
+ });
188
+ if (!response.ok) throw new Error("Failed to fetch user info");
189
+ return response.json();
190
+ }
191
+ async function getUserInfo() {
192
+ return new Promise((resolve) => {
193
+ chrome.storage.local.get([STORAGE_KEYS.user], (result) => {
194
+ resolve(result[STORAGE_KEYS.user] || null);
195
+ });
196
+ });
197
+ }
198
+ async function isAuthenticated() {
199
+ const { accessToken } = await getStoredTokens();
200
+ return !!accessToken;
201
+ }
202
+ async function getAuthStatus() {
203
+ const [authenticated, user] = await Promise.all([
204
+ isAuthenticated(),
205
+ getUserInfo()
206
+ ]);
207
+ return { authenticated, user };
208
+ }
209
+
210
+ // src/chat-client.ts
211
+ var API_BASE = "https://api.hanzo.ai";
212
+ async function listModels(token) {
213
+ const response = await fetch(`${API_BASE}/v1/models`, {
214
+ headers: { Authorization: `Bearer ${token}` }
215
+ });
216
+ if (!response.ok) {
217
+ throw new Error(`Failed to list models: ${response.status}`);
218
+ }
219
+ const data = await response.json();
220
+ return data.data || data.models || data || [];
221
+ }
222
+ async function chatCompletion(token, options) {
223
+ const response = await fetch(`${API_BASE}/v1/chat/completions`, {
224
+ method: "POST",
225
+ headers: {
226
+ "Content-Type": "application/json",
227
+ Authorization: `Bearer ${token}`
228
+ },
229
+ body: JSON.stringify(options)
230
+ });
231
+ if (!response.ok) {
232
+ const text = await response.text();
233
+ throw new Error(`Chat API error ${response.status}: ${text}`);
234
+ }
235
+ const data = await response.json();
236
+ return data.choices?.[0]?.message?.content || "";
237
+ }
238
+
3
239
  // src/background-firefox.ts
4
240
  var controlSession = {
5
241
  active: false,
@@ -13,7 +249,14 @@
13
249
  this.cdpPort = 9223;
14
250
  this.reconnectTimeout = 3e3;
15
251
  console.log("[Hanzo] Firefox extension initializing...");
16
- this.connect();
252
+ this.loadPort().then(() => this.connect());
253
+ }
254
+ async loadPort() {
255
+ try {
256
+ const result = await browser.storage.local.get(["cdpPort"]);
257
+ if (result.cdpPort) this.cdpPort = result.cdpPort;
258
+ } catch {
259
+ }
17
260
  }
18
261
  connect() {
19
262
  const url = `ws://localhost:${this.cdpPort}/cdp`;
@@ -34,16 +277,23 @@
34
277
  this.send({ id: 0, error: { code: -1, message: String(e) } });
35
278
  }
36
279
  };
37
- this.wsConnection.onerror = (error) => {
38
- console.error("[Hanzo] WebSocket error:", error);
280
+ this.wsConnection.onerror = () => {
39
281
  };
40
282
  this.wsConnection.onclose = () => {
41
283
  console.log("[Hanzo] Disconnected, reconnecting...");
42
284
  setTimeout(() => this.connect(), this.reconnectTimeout);
43
285
  };
44
286
  }
287
+ isConnected() {
288
+ return this.wsConnection?.readyState === WebSocket.OPEN;
289
+ }
290
+ sendToWs(data) {
291
+ if (this.wsConnection?.readyState === WebSocket.OPEN) {
292
+ this.wsConnection.send(JSON.stringify(data));
293
+ }
294
+ }
45
295
  register() {
46
- this.send({
296
+ this.sendToWs({
47
297
  type: "register",
48
298
  role: "cdp-provider",
49
299
  capabilities: [
@@ -61,9 +311,7 @@
61
311
  });
62
312
  }
63
313
  send(data) {
64
- if (this.wsConnection?.readyState === WebSocket.OPEN) {
65
- this.wsConnection.send(JSON.stringify(data));
66
- }
314
+ this.sendToWs(data);
67
315
  }
68
316
  async handleCommand(message) {
69
317
  const { id, method, params = {} } = message;
@@ -74,9 +322,6 @@
74
322
  return { id, error: { code: -1, message: String(e) } };
75
323
  }
76
324
  }
77
- /**
78
- * Wrap executeScript with a timeout to prevent blocking on GPU-heavy pages
79
- */
80
325
  executeScriptWithTimeout(tabId, code, timeoutMs = 1e4) {
81
326
  return Promise.race([
82
327
  browser.tabs.executeScript(tabId, { code }),
@@ -102,17 +347,12 @@
102
347
  }))
103
348
  };
104
349
  }
105
- // Native tab property reads - never block on page content
106
350
  case "hanzo.url": {
107
- if (activeTab) {
108
- return { result: { value: activeTab.url || "" } };
109
- }
351
+ if (activeTab) return { result: { value: activeTab.url || "" } };
110
352
  throw new Error("No active tab");
111
353
  }
112
354
  case "hanzo.title": {
113
- if (activeTab) {
114
- return { result: { value: activeTab.title || "" } };
115
- }
355
+ if (activeTab) return { result: { value: activeTab.title || "" } };
116
356
  throw new Error("No active tab");
117
357
  }
118
358
  case "hanzo.tabInfo": {
@@ -164,11 +404,7 @@
164
404
  if (activeTab?.id) {
165
405
  const expression = params.expression;
166
406
  const timeout = params.timeout || 1e4;
167
- const results = await this.executeScriptWithTimeout(
168
- activeTab.id,
169
- expression,
170
- timeout
171
- );
407
+ const results = await this.executeScriptWithTimeout(activeTab.id, expression, timeout);
172
408
  return { result: { value: results[0] } };
173
409
  }
174
410
  throw new Error("No active tab");
@@ -202,17 +438,14 @@
202
438
  const value = this.escapeValue(params.value || "");
203
439
  this.notifyOverlay(activeTab.id, "ai.control.highlight", { selector: params.selector });
204
440
  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
- );
441
+ await this.executeScriptWithTimeout(activeTab.id, `
442
+ var el = document.querySelector('${selector}');
443
+ if (el) {
444
+ el.value = '${value}';
445
+ el.dispatchEvent(new Event('input', {bubbles: true}));
446
+ el.dispatchEvent(new Event('change', {bubbles: true}));
447
+ }
448
+ `);
216
449
  return { success: true };
217
450
  }
218
451
  throw new Error("No active tab or selector");
@@ -232,16 +465,13 @@
232
465
  case "hanzo.type": {
233
466
  if (activeTab?.id && params.text) {
234
467
  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
- );
468
+ await this.executeScriptWithTimeout(activeTab.id, `
469
+ var el = document.activeElement;
470
+ if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) {
471
+ el.value += '${text}';
472
+ el.dispatchEvent(new Event('input', {bubbles: true}));
473
+ }
474
+ `);
245
475
  return { success: true };
246
476
  }
247
477
  throw new Error("No active tab or text");
@@ -309,31 +539,6 @@
309
539
  return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n");
310
540
  }
311
541
  };
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
542
  async function getRagConfig() {
338
543
  const result = await browser.storage.local.get([
339
544
  "hanzo_rag_endpoint",
@@ -376,9 +581,7 @@
376
581
  }
377
582
  async function queryRagEndpoint(params) {
378
583
  if (!params.endpoint) return [];
379
- const headers = {
380
- "Content-Type": "application/json"
381
- };
584
+ const headers = { "Content-Type": "application/json" };
382
585
  if (params.apiKey) headers.Authorization = `Bearer ${params.apiKey}`;
383
586
  const response = await fetch(params.endpoint, {
384
587
  method: "POST",
@@ -401,117 +604,129 @@
401
604
  source: snippet.source || "rag-endpoint"
402
605
  }));
403
606
  }
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
607
  browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
476
608
  (async () => {
477
609
  switch (request.action) {
610
+ // --- Auth (shared module — same code as Chrome) ---
478
611
  case "auth.login":
479
612
  try {
480
- const user = await firefoxLogin();
613
+ const user = await login();
481
614
  sendResponse({ success: true, user });
482
615
  } catch (e) {
483
616
  sendResponse({ success: false, error: e.message });
484
617
  }
485
618
  break;
486
619
  case "auth.logout":
487
- await browser.storage.local.remove(Object.values(STORAGE_KEYS));
620
+ try {
621
+ await logout();
622
+ sendResponse({ success: true });
623
+ } catch (e) {
624
+ sendResponse({ success: false, error: e.message });
625
+ }
626
+ break;
627
+ case "auth.status":
628
+ try {
629
+ const status = await getAuthStatus();
630
+ sendResponse({ success: true, ...status });
631
+ } catch (e) {
632
+ sendResponse({ success: false, error: e.message });
633
+ }
634
+ break;
635
+ case "auth.getToken":
636
+ try {
637
+ const token = await getValidAccessToken();
638
+ sendResponse({ success: !!token, token });
639
+ } catch (e) {
640
+ sendResponse({ success: false, error: e.message });
641
+ }
642
+ break;
643
+ // --- Cloud Chat (shared module) ---
644
+ case "chat.listModels":
645
+ try {
646
+ const token = await getValidAccessToken();
647
+ if (!token) {
648
+ sendResponse({ success: false, error: "Not authenticated" });
649
+ break;
650
+ }
651
+ const models = await listModels(token);
652
+ sendResponse({ success: true, models });
653
+ } catch (e) {
654
+ sendResponse({ success: false, error: e.message });
655
+ }
656
+ break;
657
+ case "chat.complete":
658
+ try {
659
+ const token = await getValidAccessToken();
660
+ if (!token) {
661
+ sendResponse({ success: false, error: "Not authenticated" });
662
+ break;
663
+ }
664
+ const model = String(request.model || "gpt-4o");
665
+ const messagesInput = Array.isArray(request.messages) ? request.messages : [];
666
+ const messages = messagesInput.filter((m) => m && typeof m.content === "string" && typeof m.role === "string").map((m) => ({
667
+ role: m.role === "system" || m.role === "assistant" ? m.role : "user",
668
+ content: String(m.content)
669
+ })).slice(-24);
670
+ if (!messages.length) {
671
+ sendResponse({ success: false, error: "messages are required" });
672
+ break;
673
+ }
674
+ const content = await chatCompletion(token, {
675
+ model,
676
+ messages,
677
+ temperature: typeof request.temperature === "number" ? request.temperature : void 0,
678
+ max_tokens: typeof request.max_tokens === "number" ? request.max_tokens : void 0
679
+ });
680
+ sendResponse({ success: true, content });
681
+ } catch (e) {
682
+ sendResponse({ success: false, error: e.message });
683
+ }
684
+ break;
685
+ // --- ZAP Protocol (not available on Firefox — return proper status) ---
686
+ case "zap.status":
687
+ sendResponse({ success: true, zap: { connected: false, mcps: [], extensionId: "firefox" } });
688
+ break;
689
+ case "zap.discover":
690
+ sendResponse({ success: true, mcps: [] });
691
+ break;
692
+ case "zap.listTools":
693
+ sendResponse({ success: true, tools: [] });
694
+ break;
695
+ case "zap.connect":
696
+ case "zap.callTool":
697
+ sendResponse({ success: false, error: "ZAP protocol not available on Firefox yet" });
698
+ break;
699
+ // --- Element selection (click-to-code) ---
700
+ case "elementSelected":
701
+ if (hanzoExtension.isConnected()) {
702
+ hanzoExtension.sendToWs({
703
+ type: "elementSelected",
704
+ data: request.data
705
+ });
706
+ }
488
707
  sendResponse({ success: true });
489
708
  break;
490
- case "auth.status": {
491
- const result = await browser.storage.local.get([STORAGE_KEYS.accessToken, STORAGE_KEYS.user]);
709
+ // --- Tab Filesystem ---
710
+ case "listTabFS": {
711
+ const allTabs = await browser.tabs.query({});
492
712
  sendResponse({
493
713
  success: true,
494
- authenticated: !!result[STORAGE_KEYS.accessToken],
495
- user: result[STORAGE_KEYS.user] || null
714
+ tabs: allTabs.map((tab) => ({
715
+ id: tab.id,
716
+ url: tab.url,
717
+ title: tab.title,
718
+ active: tab.active
719
+ }))
496
720
  });
497
721
  break;
498
722
  }
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 });
723
+ case "readTabFS":
724
+ sendResponse({ success: false, error: "Tab filesystem read not available on Firefox" });
502
725
  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 || [] });
726
+ case "writeTabFS":
727
+ sendResponse({ success: false, error: "Tab filesystem write not available on Firefox" });
513
728
  break;
514
- }
729
+ // --- WebGPU / Local AI ---
515
730
  case "checkWebGPU": {
516
731
  try {
517
732
  if (!navigator.gpu) {
@@ -529,15 +744,19 @@
529
744
  }
530
745
  break;
531
746
  }
747
+ case "runLocalAI":
748
+ sendResponse({ success: false, error: "Local AI not available on Firefox" });
749
+ break;
532
750
  case "listAgents":
533
751
  sendResponse({ success: true, agents: [] });
534
752
  break;
535
753
  case "stopAgent":
536
- sendResponse({ success: false, error: "Agent workers are not available in Firefox background mode yet" });
754
+ sendResponse({ success: false, error: "Agent workers not available on Firefox" });
537
755
  break;
538
756
  case "launchAIWorker":
539
- sendResponse({ success: false, error: "launchAIWorker is not implemented in Firefox background mode yet" });
757
+ sendResponse({ success: false, error: "AI workers not available on Firefox" });
540
758
  break;
759
+ // --- RAG ---
541
760
  case "rag.query": {
542
761
  try {
543
762
  const config = await getRagConfig();
@@ -569,7 +788,37 @@
569
788
  }
570
789
  break;
571
790
  }
572
- // --- AI Control Overlay (forwarded to content script) ---
791
+ // --- Bridge status ---
792
+ case "bridge.status":
793
+ sendResponse({
794
+ success: true,
795
+ connected: hanzoExtension.isConnected(),
796
+ browsers: hanzoExtension.isConnected() ? ["firefox"] : []
797
+ });
798
+ break;
799
+ // --- Config ---
800
+ case "config.save":
801
+ sendResponse({ success: true });
802
+ break;
803
+ // --- In-page overlay (forwarded to content script) ---
804
+ case "page.overlay.toggle":
805
+ case "page.overlay.show":
806
+ case "page.overlay.hide":
807
+ case "page.overlay.status": {
808
+ try {
809
+ const tabId = typeof request.tabId === "number" ? request.tabId : sender.tab?.id ?? await hanzoExtension.getActiveTabId();
810
+ if (tabId === null) {
811
+ sendResponse({ success: false, error: "No target tab" });
812
+ break;
813
+ }
814
+ const response = await browser.tabs.sendMessage(tabId, { action: request.action }).catch(() => null);
815
+ sendResponse({ success: true, tabId, ...response || {} });
816
+ } catch (e) {
817
+ sendResponse({ success: false, error: e.message });
818
+ }
819
+ break;
820
+ }
821
+ // --- AI Control Overlay ---
573
822
  case "ai.control.start":
574
823
  case "ai.control.cursor":
575
824
  case "ai.control.highlight":
@@ -593,9 +842,6 @@
593
842
  break;
594
843
  }
595
844
  case "ai.control.stop":
596
- await hanzoExtension.stopControlSession();
597
- sendResponse({ success: true });
598
- break;
599
845
  case "ai.control.cancel":
600
846
  await hanzoExtension.stopControlSession();
601
847
  sendResponse({ success: true });