@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,2732 +0,0 @@
|
|
|
1
|
-
// src/browser-control.ts
|
|
2
|
-
var BrowserControl = class {
|
|
3
|
-
constructor() {
|
|
4
|
-
this.tabFS = /* @__PURE__ */ new Map();
|
|
5
|
-
this.aiWorkers = /* @__PURE__ */ new Map();
|
|
6
|
-
this.agents = /* @__PURE__ */ new Map();
|
|
7
|
-
this.initializeTabFileSystem();
|
|
8
|
-
this.setupMessageHandlers();
|
|
9
|
-
}
|
|
10
|
-
initializeTabFileSystem() {
|
|
11
|
-
if (typeof chrome === "undefined" || !chrome.tabs) return;
|
|
12
|
-
chrome.tabs.query({}, (tabs) => {
|
|
13
|
-
tabs.forEach((tab, index) => {
|
|
14
|
-
if (tab.id && tab.url) {
|
|
15
|
-
const path = `/tabs/${index}/${this.sanitizePath(tab.title || "untitled")}`;
|
|
16
|
-
this.tabFS.set(path, {
|
|
17
|
-
path,
|
|
18
|
-
tabId: tab.id,
|
|
19
|
-
url: tab.url,
|
|
20
|
-
title: tab.title || "Untitled"
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
26
|
-
if (changeInfo.status === "complete") {
|
|
27
|
-
this.updateTabFS(tab);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
31
|
-
for (const [path, entry] of this.tabFS) {
|
|
32
|
-
if (entry.tabId === tabId) {
|
|
33
|
-
this.tabFS.delete(path);
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
sanitizePath(title) {
|
|
40
|
-
return title.replace(/[^a-zA-Z0-9-_]/g, "_").toLowerCase().substring(0, 60);
|
|
41
|
-
}
|
|
42
|
-
updateTabFS(tab) {
|
|
43
|
-
if (!tab.id || !tab.url) return;
|
|
44
|
-
const existingEntry = Array.from(this.tabFS.values()).find(
|
|
45
|
-
(entry) => entry.tabId === tab.id
|
|
46
|
-
);
|
|
47
|
-
const path = (existingEntry == null ? void 0 : existingEntry.path) || `/tabs/${this.tabFS.size}/${this.sanitizePath(tab.title || "untitled")}`;
|
|
48
|
-
this.tabFS.set(path, {
|
|
49
|
-
path,
|
|
50
|
-
tabId: tab.id,
|
|
51
|
-
url: tab.url,
|
|
52
|
-
title: tab.title || "Untitled"
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
// ===========================================================================
|
|
56
|
-
// FUSE-like Tab Filesystem
|
|
57
|
-
// ===========================================================================
|
|
58
|
-
async readTab(path) {
|
|
59
|
-
const entry = this.tabFS.get(path);
|
|
60
|
-
if (!entry) throw new Error(`Tab not found: ${path}`);
|
|
61
|
-
return new Promise((resolve, reject) => {
|
|
62
|
-
chrome.tabs.sendMessage(entry.tabId, { action: "getContent" }, (response) => {
|
|
63
|
-
if (chrome.runtime.lastError) {
|
|
64
|
-
reject(new Error(chrome.runtime.lastError.message));
|
|
65
|
-
} else {
|
|
66
|
-
resolve((response == null ? void 0 : response.content) || "");
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
async writeTab(path, content) {
|
|
72
|
-
const entry = this.tabFS.get(path);
|
|
73
|
-
if (!entry) throw new Error(`Tab not found: ${path}`);
|
|
74
|
-
return new Promise((resolve, reject) => {
|
|
75
|
-
chrome.tabs.sendMessage(entry.tabId, { action: "setContent", content }, (response) => {
|
|
76
|
-
if (chrome.runtime.lastError) {
|
|
77
|
-
reject(new Error(chrome.runtime.lastError.message));
|
|
78
|
-
} else {
|
|
79
|
-
resolve();
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
async listTabs() {
|
|
85
|
-
return Array.from(this.tabFS.keys());
|
|
86
|
-
}
|
|
87
|
-
// ===========================================================================
|
|
88
|
-
// Native Browser Actions
|
|
89
|
-
// ===========================================================================
|
|
90
|
-
async click(tabId, selector) {
|
|
91
|
-
return this.executeScript(tabId, `
|
|
92
|
-
(() => {
|
|
93
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
94
|
-
if (!el) return false;
|
|
95
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
96
|
-
el.click();
|
|
97
|
-
return true;
|
|
98
|
-
})()
|
|
99
|
-
`);
|
|
100
|
-
}
|
|
101
|
-
async clickAtPoint(tabId, x, y) {
|
|
102
|
-
return this.executeScript(tabId, `
|
|
103
|
-
(() => {
|
|
104
|
-
const el = document.elementFromPoint(${x}, ${y});
|
|
105
|
-
if (!el) return false;
|
|
106
|
-
el.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: ${x}, clientY: ${y} }));
|
|
107
|
-
return true;
|
|
108
|
-
})()
|
|
109
|
-
`);
|
|
110
|
-
}
|
|
111
|
-
async doubleClick(tabId, selector) {
|
|
112
|
-
return this.executeScript(tabId, `
|
|
113
|
-
(() => {
|
|
114
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
115
|
-
if (!el) return false;
|
|
116
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
117
|
-
el.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
|
|
118
|
-
return true;
|
|
119
|
-
})()
|
|
120
|
-
`);
|
|
121
|
-
}
|
|
122
|
-
async type(tabId, selector, text) {
|
|
123
|
-
return this.executeScript(tabId, `
|
|
124
|
-
(() => {
|
|
125
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
126
|
-
if (!el) return false;
|
|
127
|
-
el.focus();
|
|
128
|
-
for (const char of ${JSON.stringify(text)}) {
|
|
129
|
-
el.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
|
|
130
|
-
el.dispatchEvent(new KeyboardEvent('keypress', { key: char, bubbles: true }));
|
|
131
|
-
if ('value' in el) el.value += char;
|
|
132
|
-
el.dispatchEvent(new InputEvent('input', { data: char, inputType: 'insertText', bubbles: true }));
|
|
133
|
-
el.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
|
|
134
|
-
}
|
|
135
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
136
|
-
return true;
|
|
137
|
-
})()
|
|
138
|
-
`);
|
|
139
|
-
}
|
|
140
|
-
async fill(tabId, selector, value) {
|
|
141
|
-
return this.executeScript(tabId, `
|
|
142
|
-
(() => {
|
|
143
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
144
|
-
if (!el) return false;
|
|
145
|
-
el.focus();
|
|
146
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
147
|
-
Object.getPrototypeOf(el), 'value'
|
|
148
|
-
)?.set || Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
|
149
|
-
if (nativeSetter) {
|
|
150
|
-
nativeSetter.call(el, ${JSON.stringify(value)});
|
|
151
|
-
} else {
|
|
152
|
-
el.value = ${JSON.stringify(value)};
|
|
153
|
-
}
|
|
154
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
155
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
156
|
-
return true;
|
|
157
|
-
})()
|
|
158
|
-
`);
|
|
159
|
-
}
|
|
160
|
-
async select(tabId, selector, value) {
|
|
161
|
-
return this.executeScript(tabId, `
|
|
162
|
-
(() => {
|
|
163
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
164
|
-
if (!el || el.tagName !== 'SELECT') return false;
|
|
165
|
-
el.value = ${JSON.stringify(value)};
|
|
166
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
167
|
-
return true;
|
|
168
|
-
})()
|
|
169
|
-
`);
|
|
170
|
-
}
|
|
171
|
-
async check(tabId, selector, checked) {
|
|
172
|
-
return this.executeScript(tabId, `
|
|
173
|
-
(() => {
|
|
174
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
175
|
-
if (!el) return false;
|
|
176
|
-
if (el.checked !== ${checked}) el.click();
|
|
177
|
-
return true;
|
|
178
|
-
})()
|
|
179
|
-
`);
|
|
180
|
-
}
|
|
181
|
-
async hover(tabId, selector) {
|
|
182
|
-
return this.executeScript(tabId, `
|
|
183
|
-
(() => {
|
|
184
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
185
|
-
if (!el) return false;
|
|
186
|
-
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
187
|
-
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
|
188
|
-
return true;
|
|
189
|
-
})()
|
|
190
|
-
`);
|
|
191
|
-
}
|
|
192
|
-
async focus(tabId, selector) {
|
|
193
|
-
return this.executeScript(tabId, `
|
|
194
|
-
(() => {
|
|
195
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
196
|
-
if (!el) return false;
|
|
197
|
-
el.focus();
|
|
198
|
-
return true;
|
|
199
|
-
})()
|
|
200
|
-
`);
|
|
201
|
-
}
|
|
202
|
-
async blur(tabId, selector) {
|
|
203
|
-
return this.executeScript(tabId, `
|
|
204
|
-
(() => {
|
|
205
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
206
|
-
if (!el) return false;
|
|
207
|
-
el.blur();
|
|
208
|
-
return true;
|
|
209
|
-
})()
|
|
210
|
-
`);
|
|
211
|
-
}
|
|
212
|
-
async scroll(tabId, x, y, selector) {
|
|
213
|
-
if (selector) {
|
|
214
|
-
return this.executeScript(tabId, `
|
|
215
|
-
(() => {
|
|
216
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
217
|
-
if (!el) return false;
|
|
218
|
-
el.scrollBy({ left: ${x}, top: ${y}, behavior: 'smooth' });
|
|
219
|
-
return true;
|
|
220
|
-
})()
|
|
221
|
-
`);
|
|
222
|
-
}
|
|
223
|
-
return this.executeScript(tabId, `
|
|
224
|
-
(() => { window.scrollBy({ left: ${x}, top: ${y}, behavior: 'smooth' }); return true; })()
|
|
225
|
-
`);
|
|
226
|
-
}
|
|
227
|
-
async scrollIntoView(tabId, selector) {
|
|
228
|
-
return this.executeScript(tabId, `
|
|
229
|
-
(() => {
|
|
230
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
231
|
-
if (!el) return false;
|
|
232
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
233
|
-
return true;
|
|
234
|
-
})()
|
|
235
|
-
`);
|
|
236
|
-
}
|
|
237
|
-
async pressKey(tabId, key, modifiers = {}) {
|
|
238
|
-
return this.executeScript(tabId, `
|
|
239
|
-
(() => {
|
|
240
|
-
const opts = {
|
|
241
|
-
key: ${JSON.stringify(key)},
|
|
242
|
-
code: ${JSON.stringify(key)},
|
|
243
|
-
bubbles: true,
|
|
244
|
-
ctrlKey: ${!!modifiers.ctrl},
|
|
245
|
-
shiftKey: ${!!modifiers.shift},
|
|
246
|
-
altKey: ${!!modifiers.alt},
|
|
247
|
-
metaKey: ${!!modifiers.meta},
|
|
248
|
-
};
|
|
249
|
-
document.activeElement.dispatchEvent(new KeyboardEvent('keydown', opts));
|
|
250
|
-
document.activeElement.dispatchEvent(new KeyboardEvent('keypress', opts));
|
|
251
|
-
document.activeElement.dispatchEvent(new KeyboardEvent('keyup', opts));
|
|
252
|
-
return true;
|
|
253
|
-
})()
|
|
254
|
-
`);
|
|
255
|
-
}
|
|
256
|
-
async drag(tabId, fromSelector, toSelector) {
|
|
257
|
-
return this.executeScript(tabId, `
|
|
258
|
-
(() => {
|
|
259
|
-
const from = document.querySelector(${JSON.stringify(fromSelector)});
|
|
260
|
-
const to = document.querySelector(${JSON.stringify(toSelector)});
|
|
261
|
-
if (!from || !to) return false;
|
|
262
|
-
const fromRect = from.getBoundingClientRect();
|
|
263
|
-
const toRect = to.getBoundingClientRect();
|
|
264
|
-
const fromX = fromRect.left + fromRect.width / 2;
|
|
265
|
-
const fromY = fromRect.top + fromRect.height / 2;
|
|
266
|
-
const toX = toRect.left + toRect.width / 2;
|
|
267
|
-
const toY = toRect.top + toRect.height / 2;
|
|
268
|
-
from.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: fromX, clientY: fromY }));
|
|
269
|
-
from.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: toX, clientY: toY }));
|
|
270
|
-
to.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: toX, clientY: toY }));
|
|
271
|
-
to.dispatchEvent(new DragEvent('drop', { bubbles: true }));
|
|
272
|
-
return true;
|
|
273
|
-
})()
|
|
274
|
-
`);
|
|
275
|
-
}
|
|
276
|
-
async waitForSelector(tabId, selector, timeoutMs = 1e4) {
|
|
277
|
-
return this.executeScript(tabId, `
|
|
278
|
-
new Promise((resolve) => {
|
|
279
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
280
|
-
if (el) { resolve(true); return; }
|
|
281
|
-
const observer = new MutationObserver(() => {
|
|
282
|
-
if (document.querySelector(${JSON.stringify(selector)})) {
|
|
283
|
-
observer.disconnect();
|
|
284
|
-
resolve(true);
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
288
|
-
setTimeout(() => { observer.disconnect(); resolve(false); }, ${timeoutMs});
|
|
289
|
-
})
|
|
290
|
-
`);
|
|
291
|
-
}
|
|
292
|
-
async waitForNavigation(tabId, timeoutMs = 3e4) {
|
|
293
|
-
return new Promise((resolve) => {
|
|
294
|
-
const timer = setTimeout(() => {
|
|
295
|
-
chrome.tabs.onUpdated.removeListener(listener);
|
|
296
|
-
resolve(false);
|
|
297
|
-
}, timeoutMs);
|
|
298
|
-
const listener = (updatedTabId, changeInfo) => {
|
|
299
|
-
if (updatedTabId === tabId && changeInfo.status === "complete") {
|
|
300
|
-
clearTimeout(timer);
|
|
301
|
-
chrome.tabs.onUpdated.removeListener(listener);
|
|
302
|
-
resolve(true);
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
chrome.tabs.onUpdated.addListener(listener);
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
async evaluate(tabId, expression) {
|
|
309
|
-
return this.executeScript(tabId, expression);
|
|
310
|
-
}
|
|
311
|
-
async getElementInfo(tabId, selector) {
|
|
312
|
-
return this.executeScript(tabId, `
|
|
313
|
-
(() => {
|
|
314
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
315
|
-
if (!el) return null;
|
|
316
|
-
const rect = el.getBoundingClientRect();
|
|
317
|
-
const styles = window.getComputedStyle(el);
|
|
318
|
-
return {
|
|
319
|
-
tagName: el.tagName.toLowerCase(),
|
|
320
|
-
id: el.id,
|
|
321
|
-
className: el.className,
|
|
322
|
-
textContent: el.textContent?.substring(0, 500),
|
|
323
|
-
innerText: el.innerText?.substring(0, 500),
|
|
324
|
-
value: el.value,
|
|
325
|
-
href: el.href,
|
|
326
|
-
src: el.src,
|
|
327
|
-
type: el.type,
|
|
328
|
-
checked: el.checked,
|
|
329
|
-
disabled: el.disabled,
|
|
330
|
-
visible: styles.display !== 'none' && styles.visibility !== 'hidden' && rect.width > 0 && rect.height > 0,
|
|
331
|
-
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
332
|
-
attributes: Object.fromEntries(Array.from(el.attributes).map(a => [a.name, a.value])),
|
|
333
|
-
};
|
|
334
|
-
})()
|
|
335
|
-
`);
|
|
336
|
-
}
|
|
337
|
-
async getPageInfo(tabId) {
|
|
338
|
-
return this.executeScript(tabId, `
|
|
339
|
-
(() => ({
|
|
340
|
-
url: location.href,
|
|
341
|
-
title: document.title,
|
|
342
|
-
readyState: document.readyState,
|
|
343
|
-
scrollX: window.scrollX,
|
|
344
|
-
scrollY: window.scrollY,
|
|
345
|
-
innerWidth: window.innerWidth,
|
|
346
|
-
innerHeight: window.innerHeight,
|
|
347
|
-
documentWidth: document.documentElement.scrollWidth,
|
|
348
|
-
documentHeight: document.documentElement.scrollHeight,
|
|
349
|
-
forms: document.forms.length,
|
|
350
|
-
links: document.links.length,
|
|
351
|
-
images: document.images.length,
|
|
352
|
-
}))()
|
|
353
|
-
`);
|
|
354
|
-
}
|
|
355
|
-
async querySelectorAll(tabId, selector) {
|
|
356
|
-
return this.executeScript(tabId, `
|
|
357
|
-
(() => Array.from(document.querySelectorAll(${JSON.stringify(selector)})).slice(0, 100).map((el, i) => ({
|
|
358
|
-
index: i,
|
|
359
|
-
tagName: el.tagName.toLowerCase(),
|
|
360
|
-
id: el.id,
|
|
361
|
-
className: el.className,
|
|
362
|
-
textContent: el.textContent?.substring(0, 200),
|
|
363
|
-
visible: el.offsetWidth > 0 && el.offsetHeight > 0,
|
|
364
|
-
})))()
|
|
365
|
-
`);
|
|
366
|
-
}
|
|
367
|
-
// ===========================================================================
|
|
368
|
-
// Navigation
|
|
369
|
-
// ===========================================================================
|
|
370
|
-
async navigateTo(tabId, url) {
|
|
371
|
-
chrome.tabs.update(tabId, { url });
|
|
372
|
-
}
|
|
373
|
-
async reload(tabId) {
|
|
374
|
-
chrome.tabs.reload(tabId);
|
|
375
|
-
}
|
|
376
|
-
async goBack(tabId) {
|
|
377
|
-
chrome.tabs.goBack(tabId);
|
|
378
|
-
}
|
|
379
|
-
async goForward(tabId) {
|
|
380
|
-
chrome.tabs.goForward(tabId);
|
|
381
|
-
}
|
|
382
|
-
async createTab(url, active = true) {
|
|
383
|
-
return new Promise((resolve) => {
|
|
384
|
-
chrome.tabs.create({ url, active }, (tab) => {
|
|
385
|
-
resolve(tab.id);
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
async closeTab(tabId) {
|
|
390
|
-
chrome.tabs.remove(tabId);
|
|
391
|
-
}
|
|
392
|
-
// ===========================================================================
|
|
393
|
-
// Screenshots
|
|
394
|
-
// ===========================================================================
|
|
395
|
-
async captureScreenshot(tabId, options) {
|
|
396
|
-
return new Promise((resolve, reject) => {
|
|
397
|
-
const format = (options == null ? void 0 : options.format) || "jpeg";
|
|
398
|
-
const quality = (options == null ? void 0 : options.quality) || (format === "jpeg" ? 80 : void 0);
|
|
399
|
-
const captureOpts = { format };
|
|
400
|
-
if (quality !== void 0) captureOpts.quality = quality;
|
|
401
|
-
chrome.tabs.captureVisibleTab(captureOpts, (dataUrl) => {
|
|
402
|
-
if (chrome.runtime.lastError) {
|
|
403
|
-
reject(new Error(chrome.runtime.lastError.message));
|
|
404
|
-
} else {
|
|
405
|
-
resolve(dataUrl);
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
async captureFullPage(tabId) {
|
|
411
|
-
const pageInfo = await this.getPageInfo(tabId);
|
|
412
|
-
return this.captureScreenshot(tabId, { format: "png" });
|
|
413
|
-
}
|
|
414
|
-
// ===========================================================================
|
|
415
|
-
// AI Worker Management
|
|
416
|
-
// ===========================================================================
|
|
417
|
-
async launchAIWorker(tabId, modelName) {
|
|
418
|
-
const worker = new Worker(new URL("./ai-worker.js", import.meta.url), {
|
|
419
|
-
type: "module"
|
|
420
|
-
});
|
|
421
|
-
const agentId = `agent-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`;
|
|
422
|
-
worker.postMessage({ type: "init", payload: { tabId, modelName } });
|
|
423
|
-
worker.onmessage = (event) => {
|
|
424
|
-
this.handleAIWorkerMessage(tabId, agentId, event.data);
|
|
425
|
-
};
|
|
426
|
-
worker.onerror = (event) => {
|
|
427
|
-
console.error(`[Hanzo] AI worker error for tab ${tabId}:`, event.message);
|
|
428
|
-
const agent = this.agents.get(agentId);
|
|
429
|
-
if (agent) agent.status = "error";
|
|
430
|
-
};
|
|
431
|
-
this.aiWorkers.set(tabId, worker);
|
|
432
|
-
this.agents.set(agentId, {
|
|
433
|
-
id: agentId,
|
|
434
|
-
name: modelName,
|
|
435
|
-
tabId,
|
|
436
|
-
status: "running",
|
|
437
|
-
worker
|
|
438
|
-
});
|
|
439
|
-
return agentId;
|
|
440
|
-
}
|
|
441
|
-
stopAgent(agentId) {
|
|
442
|
-
const agent = this.agents.get(agentId);
|
|
443
|
-
if (!agent) return false;
|
|
444
|
-
agent.worker.terminate();
|
|
445
|
-
agent.status = "stopped";
|
|
446
|
-
this.aiWorkers.delete(agent.tabId);
|
|
447
|
-
this.agents.delete(agentId);
|
|
448
|
-
return true;
|
|
449
|
-
}
|
|
450
|
-
getAgents() {
|
|
451
|
-
return Array.from(this.agents.values()).map((a) => ({
|
|
452
|
-
id: a.id,
|
|
453
|
-
name: a.name,
|
|
454
|
-
tabId: a.tabId,
|
|
455
|
-
status: a.status
|
|
456
|
-
}));
|
|
457
|
-
}
|
|
458
|
-
handleAIWorkerMessage(tabId, agentId, data) {
|
|
459
|
-
switch (data.type) {
|
|
460
|
-
case "click":
|
|
461
|
-
this.click(tabId, data.selector);
|
|
462
|
-
break;
|
|
463
|
-
case "type":
|
|
464
|
-
this.type(tabId, data.selector, data.text);
|
|
465
|
-
break;
|
|
466
|
-
case "fill":
|
|
467
|
-
this.fill(tabId, data.selector, data.value);
|
|
468
|
-
break;
|
|
469
|
-
case "navigate":
|
|
470
|
-
this.navigateTo(tabId, data.url);
|
|
471
|
-
break;
|
|
472
|
-
case "scroll":
|
|
473
|
-
this.scroll(tabId, data.x || 0, data.y || 0, data.selector);
|
|
474
|
-
break;
|
|
475
|
-
case "hover":
|
|
476
|
-
this.hover(tabId, data.selector);
|
|
477
|
-
break;
|
|
478
|
-
case "select":
|
|
479
|
-
this.select(tabId, data.selector, data.value);
|
|
480
|
-
break;
|
|
481
|
-
case "pressKey":
|
|
482
|
-
this.pressKey(tabId, data.key, data.modifiers);
|
|
483
|
-
break;
|
|
484
|
-
case "waitForSelector":
|
|
485
|
-
this.waitForSelector(tabId, data.selector, data.timeout).then((found) => {
|
|
486
|
-
const worker = this.aiWorkers.get(tabId);
|
|
487
|
-
worker == null ? void 0 : worker.postMessage({ type: "waitResult", found });
|
|
488
|
-
});
|
|
489
|
-
break;
|
|
490
|
-
case "evaluate":
|
|
491
|
-
this.evaluate(tabId, data.expression).then((result) => {
|
|
492
|
-
const worker = this.aiWorkers.get(tabId);
|
|
493
|
-
worker == null ? void 0 : worker.postMessage({ type: "evalResult", result });
|
|
494
|
-
});
|
|
495
|
-
break;
|
|
496
|
-
case "screenshot":
|
|
497
|
-
this.captureScreenshot(tabId).then((screenshot) => {
|
|
498
|
-
const worker = this.aiWorkers.get(tabId);
|
|
499
|
-
worker == null ? void 0 : worker.postMessage({ type: "screenshot", data: screenshot });
|
|
500
|
-
});
|
|
501
|
-
break;
|
|
502
|
-
case "getElementInfo":
|
|
503
|
-
this.getElementInfo(tabId, data.selector).then((info) => {
|
|
504
|
-
const worker = this.aiWorkers.get(tabId);
|
|
505
|
-
worker == null ? void 0 : worker.postMessage({ type: "elementInfo", info });
|
|
506
|
-
});
|
|
507
|
-
break;
|
|
508
|
-
case "getPageInfo":
|
|
509
|
-
this.getPageInfo(tabId).then((info) => {
|
|
510
|
-
const worker = this.aiWorkers.get(tabId);
|
|
511
|
-
worker == null ? void 0 : worker.postMessage({ type: "pageInfo", info });
|
|
512
|
-
});
|
|
513
|
-
break;
|
|
514
|
-
case "querySelectorAll":
|
|
515
|
-
this.querySelectorAll(tabId, data.selector).then((elements) => {
|
|
516
|
-
const worker = this.aiWorkers.get(tabId);
|
|
517
|
-
worker == null ? void 0 : worker.postMessage({ type: "elements", elements });
|
|
518
|
-
});
|
|
519
|
-
break;
|
|
520
|
-
case "done":
|
|
521
|
-
case "complete": {
|
|
522
|
-
const agent = this.agents.get(agentId);
|
|
523
|
-
if (agent) {
|
|
524
|
-
agent.status = "stopped";
|
|
525
|
-
agent.worker.terminate();
|
|
526
|
-
this.aiWorkers.delete(tabId);
|
|
527
|
-
this.agents.delete(agentId);
|
|
528
|
-
}
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
// ===========================================================================
|
|
534
|
-
// Cross-tab Communication
|
|
535
|
-
// ===========================================================================
|
|
536
|
-
async broadcastToAI(message) {
|
|
537
|
-
this.aiWorkers.forEach((worker) => {
|
|
538
|
-
worker.postMessage({ type: "broadcast", message });
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
// ===========================================================================
|
|
542
|
-
// Content Script Message Handler
|
|
543
|
-
// ===========================================================================
|
|
544
|
-
setupMessageHandlers() {
|
|
545
|
-
if (typeof chrome === "undefined" || !chrome.runtime) return;
|
|
546
|
-
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
547
|
-
var _a, _b;
|
|
548
|
-
if (request.from === "content" && ((_a = sender.tab) == null ? void 0 : _a.id)) {
|
|
549
|
-
this.handleContentMessage(sender.tab.id, request, sendResponse);
|
|
550
|
-
return true;
|
|
551
|
-
}
|
|
552
|
-
if ((_b = request.action) == null ? void 0 : _b.startsWith("browser.")) {
|
|
553
|
-
this.handleBrowserAction(request, sendResponse);
|
|
554
|
-
return true;
|
|
555
|
-
}
|
|
556
|
-
if (request.action === "listAgents") {
|
|
557
|
-
sendResponse({ success: true, agents: this.getAgents() });
|
|
558
|
-
return true;
|
|
559
|
-
}
|
|
560
|
-
if (request.action === "stopAgent") {
|
|
561
|
-
const stopped = this.stopAgent(request.agentId);
|
|
562
|
-
sendResponse({ success: stopped });
|
|
563
|
-
return true;
|
|
564
|
-
}
|
|
565
|
-
if (request.action === "checkWebGPU") {
|
|
566
|
-
if (navigator.gpu) {
|
|
567
|
-
navigator.gpu.requestAdapter().then((adapter) => {
|
|
568
|
-
sendResponse({
|
|
569
|
-
available: !!adapter,
|
|
570
|
-
adapter: (adapter == null ? void 0 : adapter.name) || "Unknown GPU"
|
|
571
|
-
});
|
|
572
|
-
}).catch(() => {
|
|
573
|
-
sendResponse({ available: false });
|
|
574
|
-
});
|
|
575
|
-
} else {
|
|
576
|
-
sendResponse({ available: false });
|
|
577
|
-
}
|
|
578
|
-
return true;
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
async handleBrowserAction(request, sendResponse) {
|
|
583
|
-
const tabId = request.tabId;
|
|
584
|
-
try {
|
|
585
|
-
switch (request.action) {
|
|
586
|
-
case "browser.click":
|
|
587
|
-
sendResponse({ success: await this.click(tabId, request.selector) });
|
|
588
|
-
break;
|
|
589
|
-
case "browser.type":
|
|
590
|
-
sendResponse({ success: await this.type(tabId, request.selector, request.text) });
|
|
591
|
-
break;
|
|
592
|
-
case "browser.fill":
|
|
593
|
-
sendResponse({ success: await this.fill(tabId, request.selector, request.value) });
|
|
594
|
-
break;
|
|
595
|
-
case "browser.select":
|
|
596
|
-
sendResponse({ success: await this.select(tabId, request.selector, request.value) });
|
|
597
|
-
break;
|
|
598
|
-
case "browser.scroll":
|
|
599
|
-
sendResponse({ success: await this.scroll(tabId, request.x, request.y, request.selector) });
|
|
600
|
-
break;
|
|
601
|
-
case "browser.hover":
|
|
602
|
-
sendResponse({ success: await this.hover(tabId, request.selector) });
|
|
603
|
-
break;
|
|
604
|
-
case "browser.pressKey":
|
|
605
|
-
sendResponse({ success: await this.pressKey(tabId, request.key, request.modifiers) });
|
|
606
|
-
break;
|
|
607
|
-
case "browser.evaluate":
|
|
608
|
-
sendResponse({ success: true, result: await this.evaluate(tabId, request.expression) });
|
|
609
|
-
break;
|
|
610
|
-
case "browser.screenshot":
|
|
611
|
-
sendResponse({ success: true, data: await this.captureScreenshot(tabId, request.options) });
|
|
612
|
-
break;
|
|
613
|
-
case "browser.navigate":
|
|
614
|
-
await this.navigateTo(tabId, request.url);
|
|
615
|
-
sendResponse({ success: true });
|
|
616
|
-
break;
|
|
617
|
-
case "browser.waitForSelector":
|
|
618
|
-
sendResponse({ success: await this.waitForSelector(tabId, request.selector, request.timeout) });
|
|
619
|
-
break;
|
|
620
|
-
case "browser.getElementInfo":
|
|
621
|
-
sendResponse({ success: true, info: await this.getElementInfo(tabId, request.selector) });
|
|
622
|
-
break;
|
|
623
|
-
case "browser.getPageInfo":
|
|
624
|
-
sendResponse({ success: true, info: await this.getPageInfo(tabId) });
|
|
625
|
-
break;
|
|
626
|
-
case "browser.querySelectorAll":
|
|
627
|
-
sendResponse({ success: true, elements: await this.querySelectorAll(tabId, request.selector) });
|
|
628
|
-
break;
|
|
629
|
-
default:
|
|
630
|
-
sendResponse({ success: false, error: `Unknown action: ${request.action}` });
|
|
631
|
-
}
|
|
632
|
-
} catch (e) {
|
|
633
|
-
sendResponse({ success: false, error: e.message });
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
handleContentMessage(tabId, request, sendResponse) {
|
|
637
|
-
const worker = this.aiWorkers.get(tabId);
|
|
638
|
-
if (worker) {
|
|
639
|
-
worker.postMessage({ type: "contentMessage", data: request });
|
|
640
|
-
const responseHandler = (event) => {
|
|
641
|
-
if (event.data.type === "contentResponse") {
|
|
642
|
-
sendResponse(event.data.response);
|
|
643
|
-
worker.removeEventListener("message", responseHandler);
|
|
644
|
-
}
|
|
645
|
-
};
|
|
646
|
-
worker.addEventListener("message", responseHandler);
|
|
647
|
-
setTimeout(() => {
|
|
648
|
-
worker.removeEventListener("message", responseHandler);
|
|
649
|
-
}, 1e4);
|
|
650
|
-
} else {
|
|
651
|
-
sendResponse({ error: "No AI worker for this tab" });
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
// ===========================================================================
|
|
655
|
-
// Script Execution Helper
|
|
656
|
-
// ===========================================================================
|
|
657
|
-
executeScript(tabId, code) {
|
|
658
|
-
return new Promise((resolve, reject) => {
|
|
659
|
-
if (chrome.scripting) {
|
|
660
|
-
chrome.scripting.executeScript({
|
|
661
|
-
target: { tabId },
|
|
662
|
-
func: new Function(`return (${code})`)
|
|
663
|
-
}, (results) => {
|
|
664
|
-
var _a;
|
|
665
|
-
if (chrome.runtime.lastError) {
|
|
666
|
-
reject(new Error(chrome.runtime.lastError.message));
|
|
667
|
-
} else {
|
|
668
|
-
resolve((_a = results == null ? void 0 : results[0]) == null ? void 0 : _a.result);
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
} else {
|
|
672
|
-
chrome.tabs.executeScript(tabId, { code }, (results) => {
|
|
673
|
-
if (chrome.runtime.lastError) {
|
|
674
|
-
reject(new Error(chrome.runtime.lastError.message));
|
|
675
|
-
} else {
|
|
676
|
-
resolve(results == null ? void 0 : results[0]);
|
|
677
|
-
}
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
};
|
|
683
|
-
|
|
684
|
-
// src/webgpu-ai.ts
|
|
685
|
-
var WebGPUAI = class {
|
|
686
|
-
constructor() {
|
|
687
|
-
this.device = null;
|
|
688
|
-
this.models = /* @__PURE__ */ new Map();
|
|
689
|
-
// ---------------------------------------------------------------------------
|
|
690
|
-
// BPE Tokenizer (byte-level, GPT-2 compatible)
|
|
691
|
-
// ---------------------------------------------------------------------------
|
|
692
|
-
this.byteEncoder = {};
|
|
693
|
-
this.byteDecoder = {};
|
|
694
|
-
this.byteEncoderInitialized = false;
|
|
695
|
-
// ---------------------------------------------------------------------------
|
|
696
|
-
// WGSL Compute Shaders
|
|
697
|
-
// ---------------------------------------------------------------------------
|
|
698
|
-
this.matmulShader = `
|
|
699
|
-
@group(0) @binding(0) var<storage, read> weights: array<f32>;
|
|
700
|
-
@group(0) @binding(1) var<storage, read> input_tokens: array<f32>;
|
|
701
|
-
@group(0) @binding(2) var<storage, read_write> output_logits: array<f32>;
|
|
702
|
-
@group(0) @binding(3) var<storage, read> params: array<u32>;
|
|
703
|
-
|
|
704
|
-
// params[0] = input_size
|
|
705
|
-
// params[1] = output_size (vocab_size)
|
|
706
|
-
// params[2] = weights_offset
|
|
707
|
-
|
|
708
|
-
@compute @workgroup_size(256)
|
|
709
|
-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
710
|
-
let out_idx = gid.x;
|
|
711
|
-
let input_size = params[0];
|
|
712
|
-
let output_size = params[1];
|
|
713
|
-
let w_offset = params[2];
|
|
714
|
-
|
|
715
|
-
if (out_idx >= output_size) {
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
var sum: f32 = 0.0;
|
|
720
|
-
for (var j: u32 = 0u; j < input_size; j = j + 1u) {
|
|
721
|
-
let w_idx = w_offset + out_idx * input_size + j;
|
|
722
|
-
if (w_idx < arrayLength(&weights)) {
|
|
723
|
-
sum = sum + weights[w_idx] * input_tokens[j];
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
output_logits[out_idx] = sum;
|
|
727
|
-
}
|
|
728
|
-
`;
|
|
729
|
-
this.softmaxShader = `
|
|
730
|
-
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
|
|
731
|
-
@group(0) @binding(1) var<storage, read> size_buf: array<u32>;
|
|
732
|
-
|
|
733
|
-
@compute @workgroup_size(1)
|
|
734
|
-
fn main() {
|
|
735
|
-
let size = size_buf[0];
|
|
736
|
-
|
|
737
|
-
var max_val: f32 = data[0];
|
|
738
|
-
for (var i: u32 = 1u; i < size; i = i + 1u) {
|
|
739
|
-
if (data[i] > max_val) {
|
|
740
|
-
max_val = data[i];
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
var sum: f32 = 0.0;
|
|
745
|
-
for (var i: u32 = 0u; i < size; i = i + 1u) {
|
|
746
|
-
let e = exp(data[i] - max_val);
|
|
747
|
-
data[i] = e;
|
|
748
|
-
sum = sum + e;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
for (var i: u32 = 0u; i < size; i = i + 1u) {
|
|
752
|
-
data[i] = data[i] / sum;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
`;
|
|
756
|
-
}
|
|
757
|
-
async initialize() {
|
|
758
|
-
var _a, _b;
|
|
759
|
-
if (!navigator.gpu) {
|
|
760
|
-
console.warn("[Hanzo AI] WebGPU not supported in this browser");
|
|
761
|
-
return false;
|
|
762
|
-
}
|
|
763
|
-
try {
|
|
764
|
-
const adapter = await navigator.gpu.requestAdapter({
|
|
765
|
-
powerPreference: "high-performance"
|
|
766
|
-
});
|
|
767
|
-
if (!adapter) {
|
|
768
|
-
console.warn("[Hanzo AI] No GPU adapter found");
|
|
769
|
-
return false;
|
|
770
|
-
}
|
|
771
|
-
this.device = await adapter.requestDevice({
|
|
772
|
-
requiredLimits: {
|
|
773
|
-
maxStorageBufferBindingSize: ((_a = adapter.limits) == null ? void 0 : _a.maxStorageBufferBindingSize) || 134217728,
|
|
774
|
-
maxBufferSize: ((_b = adapter.limits) == null ? void 0 : _b.maxBufferSize) || 268435456
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
this.device.lost.then((info) => {
|
|
778
|
-
console.error("[Hanzo AI] GPU device lost:", info.message);
|
|
779
|
-
this.device = null;
|
|
780
|
-
});
|
|
781
|
-
console.log("[Hanzo AI] WebGPU initialized successfully");
|
|
782
|
-
const features = adapter.features;
|
|
783
|
-
console.log("[Hanzo AI] GPU Features:", Array.from(features));
|
|
784
|
-
return true;
|
|
785
|
-
} catch (error) {
|
|
786
|
-
console.error("[Hanzo AI] WebGPU initialization failed:", error);
|
|
787
|
-
return false;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
initByteEncoder() {
|
|
791
|
-
if (this.byteEncoderInitialized) return;
|
|
792
|
-
const bs = [];
|
|
793
|
-
for (let i = 33; i <= 126; i++) bs.push(i);
|
|
794
|
-
for (let i = 161; i <= 172; i++) bs.push(i);
|
|
795
|
-
for (let i = 174; i <= 255; i++) bs.push(i);
|
|
796
|
-
const cs = bs.slice();
|
|
797
|
-
let n = 0;
|
|
798
|
-
for (let b = 0; b < 256; b++) {
|
|
799
|
-
if (!bs.includes(b)) {
|
|
800
|
-
bs.push(b);
|
|
801
|
-
cs.push(256 + n);
|
|
802
|
-
n++;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
for (let i = 0; i < bs.length; i++) {
|
|
806
|
-
this.byteEncoder[bs[i]] = String.fromCharCode(cs[i]);
|
|
807
|
-
this.byteDecoder[String.fromCharCode(cs[i])] = bs[i];
|
|
808
|
-
}
|
|
809
|
-
this.byteEncoderInitialized = true;
|
|
810
|
-
}
|
|
811
|
-
tokenize(text, model) {
|
|
812
|
-
this.initByteEncoder();
|
|
813
|
-
if (!model.vocab.length) {
|
|
814
|
-
const tokens2 = [];
|
|
815
|
-
for (let i = 0; i < text.length; i++) {
|
|
816
|
-
tokens2.push(text.charCodeAt(i));
|
|
817
|
-
}
|
|
818
|
-
return tokens2;
|
|
819
|
-
}
|
|
820
|
-
const bytes = new TextEncoder().encode(text);
|
|
821
|
-
let word = "";
|
|
822
|
-
for (const b of bytes) {
|
|
823
|
-
word += this.byteEncoder[b] || String.fromCharCode(b);
|
|
824
|
-
}
|
|
825
|
-
const tokens = [];
|
|
826
|
-
let pos = 0;
|
|
827
|
-
while (pos < word.length) {
|
|
828
|
-
let bestLen = 0;
|
|
829
|
-
let bestIdx = -1;
|
|
830
|
-
const maxLen = Math.min(word.length - pos, 50);
|
|
831
|
-
for (let len = maxLen; len >= 1; len--) {
|
|
832
|
-
const candidate = word.substring(pos, pos + len);
|
|
833
|
-
const idx = model.vocabIndex.get(candidate);
|
|
834
|
-
if (idx !== void 0) {
|
|
835
|
-
bestLen = len;
|
|
836
|
-
bestIdx = idx;
|
|
837
|
-
break;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
if (bestIdx !== -1) {
|
|
841
|
-
tokens.push(bestIdx);
|
|
842
|
-
pos += bestLen;
|
|
843
|
-
} else {
|
|
844
|
-
tokens.push(word.charCodeAt(pos) % Math.max(model.vocab.length, 1));
|
|
845
|
-
pos++;
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
return tokens;
|
|
849
|
-
}
|
|
850
|
-
detokenize(tokenIds, model) {
|
|
851
|
-
this.initByteEncoder();
|
|
852
|
-
if (!model.vocab.length) {
|
|
853
|
-
const arr = tokenIds instanceof Int32Array ? Array.from(tokenIds) : tokenIds;
|
|
854
|
-
return String.fromCharCode(...arr.filter((t) => t > 0 && t < 65536));
|
|
855
|
-
}
|
|
856
|
-
let byteStr = "";
|
|
857
|
-
for (const id of tokenIds) {
|
|
858
|
-
if (id >= 0 && id < model.vocab.length) {
|
|
859
|
-
byteStr += model.vocab[id];
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
const decoded = [];
|
|
863
|
-
for (const ch of byteStr) {
|
|
864
|
-
if (ch in this.byteDecoder) {
|
|
865
|
-
decoded.push(this.byteDecoder[ch]);
|
|
866
|
-
} else {
|
|
867
|
-
decoded.push(ch.charCodeAt(0) & 255);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
return new TextDecoder().decode(new Uint8Array(decoded));
|
|
871
|
-
}
|
|
872
|
-
// ---------------------------------------------------------------------------
|
|
873
|
-
// Model Loading
|
|
874
|
-
// ---------------------------------------------------------------------------
|
|
875
|
-
async loadModel(config) {
|
|
876
|
-
if (!this.device) {
|
|
877
|
-
throw new Error("WebGPU not initialized");
|
|
878
|
-
}
|
|
879
|
-
console.log(`[Hanzo AI] Loading model: ${config.name}`);
|
|
880
|
-
const response = await fetch(config.url);
|
|
881
|
-
if (!response.ok) {
|
|
882
|
-
throw new Error(`Failed to fetch model ${config.name}: ${response.status}`);
|
|
883
|
-
}
|
|
884
|
-
const modelData = await response.arrayBuffer();
|
|
885
|
-
const modelBuffer = this.device.createBuffer({
|
|
886
|
-
size: modelData.byteLength,
|
|
887
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
888
|
-
label: `model-${config.name}`
|
|
889
|
-
});
|
|
890
|
-
this.device.queue.writeBuffer(modelBuffer, 0, modelData);
|
|
891
|
-
let vocab = [];
|
|
892
|
-
const vocabIndex = /* @__PURE__ */ new Map();
|
|
893
|
-
if (config.vocabUrl) {
|
|
894
|
-
try {
|
|
895
|
-
const vocabResp = await fetch(config.vocabUrl);
|
|
896
|
-
if (vocabResp.ok) {
|
|
897
|
-
const vocabData = await vocabResp.json();
|
|
898
|
-
if (Array.isArray(vocabData)) {
|
|
899
|
-
vocab = vocabData;
|
|
900
|
-
} else if (typeof vocabData === "object") {
|
|
901
|
-
const entries = Object.entries(vocabData);
|
|
902
|
-
entries.sort((a, b) => a[1] - b[1]);
|
|
903
|
-
vocab = entries.map((e) => e[0]);
|
|
904
|
-
}
|
|
905
|
-
for (let i = 0; i < vocab.length; i++) {
|
|
906
|
-
vocabIndex.set(vocab[i], i);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
} catch (e) {
|
|
910
|
-
console.warn(`[Hanzo AI] Vocabulary load failed for ${config.name}:`, e);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
this.models.set(config.name, {
|
|
914
|
-
buffer: modelBuffer,
|
|
915
|
-
config,
|
|
916
|
-
vocab,
|
|
917
|
-
vocabIndex
|
|
918
|
-
});
|
|
919
|
-
console.log(`[Hanzo AI] Model ${config.name} loaded: ${modelData.byteLength} bytes, vocab: ${vocab.length}`);
|
|
920
|
-
}
|
|
921
|
-
// ---------------------------------------------------------------------------
|
|
922
|
-
// Inference
|
|
923
|
-
// ---------------------------------------------------------------------------
|
|
924
|
-
async runInference(modelName, input) {
|
|
925
|
-
const model = this.models.get(modelName);
|
|
926
|
-
if (!model || !this.device) {
|
|
927
|
-
throw new Error(`Model ${modelName} not loaded or GPU unavailable`);
|
|
928
|
-
}
|
|
929
|
-
const tokens = this.tokenize(input, model);
|
|
930
|
-
const vocabSize = Math.max(model.vocab.length, 256);
|
|
931
|
-
const inputSize = Math.min(tokens.length, 2048);
|
|
932
|
-
const maxTokens = model.config.maxTokens || 128;
|
|
933
|
-
const inputData = new Float32Array(inputSize);
|
|
934
|
-
for (let i = 0; i < inputSize; i++) {
|
|
935
|
-
inputData[i] = tokens[i] / vocabSize;
|
|
936
|
-
}
|
|
937
|
-
const inputBuffer = this.device.createBuffer({
|
|
938
|
-
size: inputData.byteLength,
|
|
939
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
940
|
-
label: "input"
|
|
941
|
-
});
|
|
942
|
-
this.device.queue.writeBuffer(inputBuffer, 0, inputData);
|
|
943
|
-
const outputByteSize = vocabSize * 4;
|
|
944
|
-
const outputBuffer = this.device.createBuffer({
|
|
945
|
-
size: outputByteSize,
|
|
946
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
|
|
947
|
-
label: "output"
|
|
948
|
-
});
|
|
949
|
-
const paramsData = new Uint32Array([inputSize, vocabSize, 0]);
|
|
950
|
-
const paramsBuffer = this.device.createBuffer({
|
|
951
|
-
size: paramsData.byteLength,
|
|
952
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
953
|
-
label: "params"
|
|
954
|
-
});
|
|
955
|
-
this.device.queue.writeBuffer(paramsBuffer, 0, paramsData);
|
|
956
|
-
const readBuffer = this.device.createBuffer({
|
|
957
|
-
size: outputByteSize,
|
|
958
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
959
|
-
label: "readback"
|
|
960
|
-
});
|
|
961
|
-
const matmulModule = this.device.createShaderModule({ code: this.matmulShader });
|
|
962
|
-
const matmulLayout = this.device.createBindGroupLayout({
|
|
963
|
-
entries: [
|
|
964
|
-
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
965
|
-
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
966
|
-
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
967
|
-
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }
|
|
968
|
-
]
|
|
969
|
-
});
|
|
970
|
-
const matmulPipeline = this.device.createComputePipeline({
|
|
971
|
-
layout: this.device.createPipelineLayout({ bindGroupLayouts: [matmulLayout] }),
|
|
972
|
-
compute: { module: matmulModule, entryPoint: "main" }
|
|
973
|
-
});
|
|
974
|
-
const matmulBindGroup = this.device.createBindGroup({
|
|
975
|
-
layout: matmulLayout,
|
|
976
|
-
entries: [
|
|
977
|
-
{ binding: 0, resource: { buffer: model.buffer } },
|
|
978
|
-
{ binding: 1, resource: { buffer: inputBuffer } },
|
|
979
|
-
{ binding: 2, resource: { buffer: outputBuffer } },
|
|
980
|
-
{ binding: 3, resource: { buffer: paramsBuffer } }
|
|
981
|
-
]
|
|
982
|
-
});
|
|
983
|
-
const softmaxModule = this.device.createShaderModule({ code: this.softmaxShader });
|
|
984
|
-
const softmaxLayout = this.device.createBindGroupLayout({
|
|
985
|
-
entries: [
|
|
986
|
-
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
987
|
-
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }
|
|
988
|
-
]
|
|
989
|
-
});
|
|
990
|
-
const sizeBuf = this.device.createBuffer({
|
|
991
|
-
size: 4,
|
|
992
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
993
|
-
});
|
|
994
|
-
this.device.queue.writeBuffer(sizeBuf, 0, new Uint32Array([vocabSize]));
|
|
995
|
-
const softmaxPipeline = this.device.createComputePipeline({
|
|
996
|
-
layout: this.device.createPipelineLayout({ bindGroupLayouts: [softmaxLayout] }),
|
|
997
|
-
compute: { module: softmaxModule, entryPoint: "main" }
|
|
998
|
-
});
|
|
999
|
-
const softmaxBindGroup = this.device.createBindGroup({
|
|
1000
|
-
layout: softmaxLayout,
|
|
1001
|
-
entries: [
|
|
1002
|
-
{ binding: 0, resource: { buffer: outputBuffer } },
|
|
1003
|
-
{ binding: 1, resource: { buffer: sizeBuf } }
|
|
1004
|
-
]
|
|
1005
|
-
});
|
|
1006
|
-
const generatedTokens = [];
|
|
1007
|
-
const temperature = 0.7;
|
|
1008
|
-
for (let step = 0; step < maxTokens; step++) {
|
|
1009
|
-
const encoder = this.device.createCommandEncoder();
|
|
1010
|
-
const matmulPass = encoder.beginComputePass();
|
|
1011
|
-
matmulPass.setPipeline(matmulPipeline);
|
|
1012
|
-
matmulPass.setBindGroup(0, matmulBindGroup);
|
|
1013
|
-
matmulPass.dispatchWorkgroups(Math.ceil(vocabSize / 256));
|
|
1014
|
-
matmulPass.end();
|
|
1015
|
-
const softmaxPass = encoder.beginComputePass();
|
|
1016
|
-
softmaxPass.setPipeline(softmaxPipeline);
|
|
1017
|
-
softmaxPass.setBindGroup(0, softmaxBindGroup);
|
|
1018
|
-
softmaxPass.dispatchWorkgroups(1);
|
|
1019
|
-
softmaxPass.end();
|
|
1020
|
-
encoder.copyBufferToBuffer(outputBuffer, 0, readBuffer, 0, outputByteSize);
|
|
1021
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1022
|
-
await readBuffer.mapAsync(GPUMapMode.READ);
|
|
1023
|
-
const probs = new Float32Array(readBuffer.getMappedRange().slice(0));
|
|
1024
|
-
readBuffer.unmap();
|
|
1025
|
-
const nextToken = this.sampleToken(probs, temperature);
|
|
1026
|
-
generatedTokens.push(nextToken);
|
|
1027
|
-
if (nextToken === 0 || nextToken === 2) break;
|
|
1028
|
-
const newInput = new Float32Array([nextToken / vocabSize]);
|
|
1029
|
-
this.device.queue.writeBuffer(inputBuffer, 0, newInput);
|
|
1030
|
-
paramsData[0] = 1;
|
|
1031
|
-
this.device.queue.writeBuffer(paramsBuffer, 0, paramsData);
|
|
1032
|
-
}
|
|
1033
|
-
inputBuffer.destroy();
|
|
1034
|
-
outputBuffer.destroy();
|
|
1035
|
-
paramsBuffer.destroy();
|
|
1036
|
-
readBuffer.destroy();
|
|
1037
|
-
sizeBuf.destroy();
|
|
1038
|
-
return this.detokenize(generatedTokens, model);
|
|
1039
|
-
}
|
|
1040
|
-
sampleToken(probs, temperature) {
|
|
1041
|
-
if (temperature <= 0) {
|
|
1042
|
-
let maxIdx = 0;
|
|
1043
|
-
let maxVal = probs[0];
|
|
1044
|
-
for (let i = 1; i < probs.length; i++) {
|
|
1045
|
-
if (probs[i] > maxVal) {
|
|
1046
|
-
maxVal = probs[i];
|
|
1047
|
-
maxIdx = i;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
return maxIdx;
|
|
1051
|
-
}
|
|
1052
|
-
const scaled = new Float32Array(probs.length);
|
|
1053
|
-
let sum = 0;
|
|
1054
|
-
for (let i = 0; i < probs.length; i++) {
|
|
1055
|
-
scaled[i] = Math.exp(Math.log(Math.max(probs[i], 1e-10)) / temperature);
|
|
1056
|
-
sum += scaled[i];
|
|
1057
|
-
}
|
|
1058
|
-
for (let i = 0; i < scaled.length; i++) {
|
|
1059
|
-
scaled[i] /= sum;
|
|
1060
|
-
}
|
|
1061
|
-
const r = Math.random();
|
|
1062
|
-
let cumulative = 0;
|
|
1063
|
-
for (let i = 0; i < scaled.length; i++) {
|
|
1064
|
-
cumulative += scaled[i];
|
|
1065
|
-
if (r < cumulative) return i;
|
|
1066
|
-
}
|
|
1067
|
-
return scaled.length - 1;
|
|
1068
|
-
}
|
|
1069
|
-
// ---------------------------------------------------------------------------
|
|
1070
|
-
// Embedding computation (for DOM element analysis)
|
|
1071
|
-
// ---------------------------------------------------------------------------
|
|
1072
|
-
async computeEmbedding(modelName, text) {
|
|
1073
|
-
const model = this.models.get(modelName);
|
|
1074
|
-
if (!model || !this.device) {
|
|
1075
|
-
throw new Error(`Model ${modelName} not loaded or GPU unavailable`);
|
|
1076
|
-
}
|
|
1077
|
-
const tokens = this.tokenize(text, model);
|
|
1078
|
-
const inputSize = Math.min(tokens.length, 512);
|
|
1079
|
-
const embeddingDim = 256;
|
|
1080
|
-
const inputData = new Float32Array(inputSize);
|
|
1081
|
-
for (let i = 0; i < inputSize; i++) {
|
|
1082
|
-
inputData[i] = tokens[i];
|
|
1083
|
-
}
|
|
1084
|
-
const inputBuffer = this.device.createBuffer({
|
|
1085
|
-
size: inputData.byteLength,
|
|
1086
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
1087
|
-
});
|
|
1088
|
-
this.device.queue.writeBuffer(inputBuffer, 0, inputData);
|
|
1089
|
-
const embeddingBuffer = this.device.createBuffer({
|
|
1090
|
-
size: embeddingDim * 4,
|
|
1091
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
|
|
1092
|
-
});
|
|
1093
|
-
const paramData = new Uint32Array([inputSize, embeddingDim, 0]);
|
|
1094
|
-
const paramBuffer = this.device.createBuffer({
|
|
1095
|
-
size: paramData.byteLength,
|
|
1096
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
1097
|
-
});
|
|
1098
|
-
this.device.queue.writeBuffer(paramBuffer, 0, paramData);
|
|
1099
|
-
const shaderModule = this.device.createShaderModule({ code: this.matmulShader });
|
|
1100
|
-
const layout = this.device.createBindGroupLayout({
|
|
1101
|
-
entries: [
|
|
1102
|
-
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
1103
|
-
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } },
|
|
1104
|
-
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } },
|
|
1105
|
-
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "read-only-storage" } }
|
|
1106
|
-
]
|
|
1107
|
-
});
|
|
1108
|
-
const pipeline = this.device.createComputePipeline({
|
|
1109
|
-
layout: this.device.createPipelineLayout({ bindGroupLayouts: [layout] }),
|
|
1110
|
-
compute: { module: shaderModule, entryPoint: "main" }
|
|
1111
|
-
});
|
|
1112
|
-
const bindGroup = this.device.createBindGroup({
|
|
1113
|
-
layout,
|
|
1114
|
-
entries: [
|
|
1115
|
-
{ binding: 0, resource: { buffer: model.buffer } },
|
|
1116
|
-
{ binding: 1, resource: { buffer: inputBuffer } },
|
|
1117
|
-
{ binding: 2, resource: { buffer: embeddingBuffer } },
|
|
1118
|
-
{ binding: 3, resource: { buffer: paramBuffer } }
|
|
1119
|
-
]
|
|
1120
|
-
});
|
|
1121
|
-
const readBuffer = this.device.createBuffer({
|
|
1122
|
-
size: embeddingDim * 4,
|
|
1123
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
1124
|
-
});
|
|
1125
|
-
const encoder = this.device.createCommandEncoder();
|
|
1126
|
-
const pass = encoder.beginComputePass();
|
|
1127
|
-
pass.setPipeline(pipeline);
|
|
1128
|
-
pass.setBindGroup(0, bindGroup);
|
|
1129
|
-
pass.dispatchWorkgroups(Math.ceil(embeddingDim / 256));
|
|
1130
|
-
pass.end();
|
|
1131
|
-
encoder.copyBufferToBuffer(embeddingBuffer, 0, readBuffer, 0, embeddingDim * 4);
|
|
1132
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1133
|
-
await readBuffer.mapAsync(GPUMapMode.READ);
|
|
1134
|
-
const embedding = new Float32Array(readBuffer.getMappedRange().slice(0));
|
|
1135
|
-
readBuffer.unmap();
|
|
1136
|
-
let norm = 0;
|
|
1137
|
-
for (let i = 0; i < embedding.length; i++) {
|
|
1138
|
-
norm += embedding[i] * embedding[i];
|
|
1139
|
-
}
|
|
1140
|
-
norm = Math.sqrt(norm);
|
|
1141
|
-
if (norm > 0) {
|
|
1142
|
-
for (let i = 0; i < embedding.length; i++) {
|
|
1143
|
-
embedding[i] /= norm;
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
inputBuffer.destroy();
|
|
1147
|
-
embeddingBuffer.destroy();
|
|
1148
|
-
paramBuffer.destroy();
|
|
1149
|
-
readBuffer.destroy();
|
|
1150
|
-
return embedding;
|
|
1151
|
-
}
|
|
1152
|
-
// ---------------------------------------------------------------------------
|
|
1153
|
-
// Status / Cleanup
|
|
1154
|
-
// ---------------------------------------------------------------------------
|
|
1155
|
-
getStatus() {
|
|
1156
|
-
return {
|
|
1157
|
-
initialized: !!this.device,
|
|
1158
|
-
models: Array.from(this.models.keys())
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1161
|
-
async unloadModel(name) {
|
|
1162
|
-
const model = this.models.get(name);
|
|
1163
|
-
if (model) {
|
|
1164
|
-
model.buffer.destroy();
|
|
1165
|
-
this.models.delete(name);
|
|
1166
|
-
console.log(`[Hanzo AI] Model ${name} unloaded`);
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
destroy() {
|
|
1170
|
-
for (const [name, model] of this.models) {
|
|
1171
|
-
model.buffer.destroy();
|
|
1172
|
-
}
|
|
1173
|
-
this.models.clear();
|
|
1174
|
-
this.device = null;
|
|
1175
|
-
}
|
|
1176
|
-
};
|
|
1177
|
-
|
|
1178
|
-
// src/cdp-bridge.ts
|
|
1179
|
-
var CDPBridge = class {
|
|
1180
|
-
constructor() {
|
|
1181
|
-
this.sessions = /* @__PURE__ */ new Map();
|
|
1182
|
-
this.messageHandlers = /* @__PURE__ */ new Map();
|
|
1183
|
-
this.commandId = 0;
|
|
1184
|
-
this.wsServer = null;
|
|
1185
|
-
this.wsClients = /* @__PURE__ */ new Set();
|
|
1186
|
-
this.setupDebuggerListener();
|
|
1187
|
-
}
|
|
1188
|
-
setupDebuggerListener() {
|
|
1189
|
-
if (typeof chrome !== "undefined" && chrome.debugger) {
|
|
1190
|
-
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
1191
|
-
this.broadcastEvent(source.tabId, method, params);
|
|
1192
|
-
});
|
|
1193
|
-
chrome.debugger.onDetach.addListener((source, reason) => {
|
|
1194
|
-
console.log(`[CDP] Detached from tab ${source.tabId}: ${reason}`);
|
|
1195
|
-
if (source.tabId) {
|
|
1196
|
-
this.sessions.delete(source.tabId);
|
|
1197
|
-
}
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
// Attach debugger to a tab
|
|
1202
|
-
async attach(tabId) {
|
|
1203
|
-
if (this.sessions.has(tabId)) {
|
|
1204
|
-
return true;
|
|
1205
|
-
}
|
|
1206
|
-
return new Promise((resolve) => {
|
|
1207
|
-
const debuggee = { tabId };
|
|
1208
|
-
chrome.debugger.attach(debuggee, "1.3", () => {
|
|
1209
|
-
if (chrome.runtime.lastError) {
|
|
1210
|
-
console.error(`[CDP] Failed to attach: ${chrome.runtime.lastError.message}`);
|
|
1211
|
-
resolve(false);
|
|
1212
|
-
} else {
|
|
1213
|
-
this.sessions.set(tabId, {
|
|
1214
|
-
tabId,
|
|
1215
|
-
debuggee,
|
|
1216
|
-
connected: true
|
|
1217
|
-
});
|
|
1218
|
-
console.log(`[CDP] Attached to tab ${tabId}`);
|
|
1219
|
-
resolve(true);
|
|
1220
|
-
}
|
|
1221
|
-
});
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
// Detach from a tab
|
|
1225
|
-
async detach(tabId) {
|
|
1226
|
-
const session = this.sessions.get(tabId);
|
|
1227
|
-
if (!session) return;
|
|
1228
|
-
return new Promise((resolve) => {
|
|
1229
|
-
chrome.debugger.detach(session.debuggee, () => {
|
|
1230
|
-
this.sessions.delete(tabId);
|
|
1231
|
-
resolve();
|
|
1232
|
-
});
|
|
1233
|
-
});
|
|
1234
|
-
}
|
|
1235
|
-
// Send CDP command
|
|
1236
|
-
async send(tabId, method, params) {
|
|
1237
|
-
const session = this.sessions.get(tabId);
|
|
1238
|
-
if (!session) {
|
|
1239
|
-
const attached = await this.attach(tabId);
|
|
1240
|
-
if (!attached) {
|
|
1241
|
-
throw new Error(`Cannot attach to tab ${tabId}`);
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
return new Promise((resolve, reject) => {
|
|
1245
|
-
chrome.debugger.sendCommand(
|
|
1246
|
-
{ tabId },
|
|
1247
|
-
method,
|
|
1248
|
-
params || {},
|
|
1249
|
-
(result) => {
|
|
1250
|
-
if (chrome.runtime.lastError) {
|
|
1251
|
-
reject(new Error(chrome.runtime.lastError.message));
|
|
1252
|
-
} else {
|
|
1253
|
-
resolve(result);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
);
|
|
1257
|
-
});
|
|
1258
|
-
}
|
|
1259
|
-
// High-level browser control methods for hanzo-mcp integration
|
|
1260
|
-
async navigate(tabId, url) {
|
|
1261
|
-
await this.send(tabId, "Page.navigate", { url });
|
|
1262
|
-
}
|
|
1263
|
-
async screenshot(tabId, options) {
|
|
1264
|
-
await this.send(tabId, "Page.enable");
|
|
1265
|
-
const params = {
|
|
1266
|
-
format: (options == null ? void 0 : options.format) || "png"
|
|
1267
|
-
};
|
|
1268
|
-
if (options == null ? void 0 : options.quality) {
|
|
1269
|
-
params.quality = options.quality;
|
|
1270
|
-
}
|
|
1271
|
-
if (options == null ? void 0 : options.fullPage) {
|
|
1272
|
-
const metrics = await this.send(tabId, "Page.getLayoutMetrics");
|
|
1273
|
-
params.clip = {
|
|
1274
|
-
x: 0,
|
|
1275
|
-
y: 0,
|
|
1276
|
-
width: metrics.contentSize.width,
|
|
1277
|
-
height: metrics.contentSize.height,
|
|
1278
|
-
scale: 1
|
|
1279
|
-
};
|
|
1280
|
-
params.captureBeyondViewport = true;
|
|
1281
|
-
} else if (options == null ? void 0 : options.clip) {
|
|
1282
|
-
params.clip = { ...options.clip, scale: 1 };
|
|
1283
|
-
}
|
|
1284
|
-
const result = await this.send(tabId, "Page.captureScreenshot", params);
|
|
1285
|
-
return result.data;
|
|
1286
|
-
}
|
|
1287
|
-
async click(tabId, x, y) {
|
|
1288
|
-
await this.send(tabId, "Input.dispatchMouseEvent", {
|
|
1289
|
-
type: "mousePressed",
|
|
1290
|
-
x,
|
|
1291
|
-
y,
|
|
1292
|
-
button: "left",
|
|
1293
|
-
clickCount: 1
|
|
1294
|
-
});
|
|
1295
|
-
await this.send(tabId, "Input.dispatchMouseEvent", {
|
|
1296
|
-
type: "mouseReleased",
|
|
1297
|
-
x,
|
|
1298
|
-
y,
|
|
1299
|
-
button: "left",
|
|
1300
|
-
clickCount: 1
|
|
1301
|
-
});
|
|
1302
|
-
}
|
|
1303
|
-
async type(tabId, text) {
|
|
1304
|
-
for (const char of text) {
|
|
1305
|
-
await this.send(tabId, "Input.dispatchKeyEvent", {
|
|
1306
|
-
type: "keyDown",
|
|
1307
|
-
text: char
|
|
1308
|
-
});
|
|
1309
|
-
await this.send(tabId, "Input.dispatchKeyEvent", {
|
|
1310
|
-
type: "keyUp",
|
|
1311
|
-
text: char
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
async evaluate(tabId, expression) {
|
|
1316
|
-
var _a;
|
|
1317
|
-
await this.send(tabId, "Runtime.enable");
|
|
1318
|
-
const result = await this.send(tabId, "Runtime.evaluate", {
|
|
1319
|
-
expression,
|
|
1320
|
-
returnByValue: true
|
|
1321
|
-
});
|
|
1322
|
-
return (_a = result.result) == null ? void 0 : _a.value;
|
|
1323
|
-
}
|
|
1324
|
-
async getDocument(tabId) {
|
|
1325
|
-
await this.send(tabId, "DOM.enable");
|
|
1326
|
-
return this.send(tabId, "DOM.getDocument");
|
|
1327
|
-
}
|
|
1328
|
-
async querySelector(tabId, selector) {
|
|
1329
|
-
const doc = await this.getDocument(tabId);
|
|
1330
|
-
try {
|
|
1331
|
-
const result = await this.send(tabId, "DOM.querySelector", {
|
|
1332
|
-
nodeId: doc.root.nodeId,
|
|
1333
|
-
selector
|
|
1334
|
-
});
|
|
1335
|
-
return result.nodeId || null;
|
|
1336
|
-
} catch {
|
|
1337
|
-
return null;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
async getBoxModel(tabId, nodeId) {
|
|
1341
|
-
return this.send(tabId, "DOM.getBoxModel", { nodeId });
|
|
1342
|
-
}
|
|
1343
|
-
async clickSelector(tabId, selector) {
|
|
1344
|
-
var _a;
|
|
1345
|
-
const nodeId = await this.querySelector(tabId, selector);
|
|
1346
|
-
if (!nodeId) {
|
|
1347
|
-
return false;
|
|
1348
|
-
}
|
|
1349
|
-
const box = await this.getBoxModel(tabId, nodeId);
|
|
1350
|
-
if (!((_a = box == null ? void 0 : box.model) == null ? void 0 : _a.content)) {
|
|
1351
|
-
return false;
|
|
1352
|
-
}
|
|
1353
|
-
const quad = box.model.content;
|
|
1354
|
-
const x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
1355
|
-
const y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
1356
|
-
await this.click(tabId, x, y);
|
|
1357
|
-
return true;
|
|
1358
|
-
}
|
|
1359
|
-
async fillSelector(tabId, selector, value) {
|
|
1360
|
-
const nodeId = await this.querySelector(tabId, selector);
|
|
1361
|
-
if (!nodeId) {
|
|
1362
|
-
return false;
|
|
1363
|
-
}
|
|
1364
|
-
await this.send(tabId, "DOM.focus", { nodeId });
|
|
1365
|
-
await this.send(tabId, "Input.dispatchKeyEvent", {
|
|
1366
|
-
type: "keyDown",
|
|
1367
|
-
key: "a",
|
|
1368
|
-
modifiers: 2
|
|
1369
|
-
// Ctrl/Cmd
|
|
1370
|
-
});
|
|
1371
|
-
await this.send(tabId, "Input.dispatchKeyEvent", {
|
|
1372
|
-
type: "keyUp",
|
|
1373
|
-
key: "a",
|
|
1374
|
-
modifiers: 2
|
|
1375
|
-
});
|
|
1376
|
-
await this.type(tabId, value);
|
|
1377
|
-
return true;
|
|
1378
|
-
}
|
|
1379
|
-
// WebSocket server for external connections (hanzo-mcp)
|
|
1380
|
-
startWebSocketServer(port = 9223) {
|
|
1381
|
-
this.connectToBridge(`ws://localhost:${port}/cdp`);
|
|
1382
|
-
}
|
|
1383
|
-
connectToBridge(url) {
|
|
1384
|
-
try {
|
|
1385
|
-
this.wsServer = new WebSocket(url);
|
|
1386
|
-
this.wsServer.onopen = () => {
|
|
1387
|
-
var _a;
|
|
1388
|
-
console.log("[CDP] Connected to bridge server");
|
|
1389
|
-
const browser = typeof navigator !== "undefined" ? navigator.userAgent.includes("Firefox") ? "firefox" : navigator.userAgent.includes("Edg/") ? "edge" : navigator.userAgent.includes("Chrome") ? "chrome" : navigator.userAgent.includes("Safari") ? "safari" : "unknown" : "unknown";
|
|
1390
|
-
(_a = this.wsServer) == null ? void 0 : _a.send(JSON.stringify({
|
|
1391
|
-
type: "register",
|
|
1392
|
-
role: "cdp-provider",
|
|
1393
|
-
browser,
|
|
1394
|
-
capabilities: ["navigate", "screenshot", "click", "type", "evaluate", "takeover"]
|
|
1395
|
-
}));
|
|
1396
|
-
};
|
|
1397
|
-
this.wsServer.onmessage = async (event) => {
|
|
1398
|
-
var _a, _b;
|
|
1399
|
-
try {
|
|
1400
|
-
const message = JSON.parse(event.data);
|
|
1401
|
-
const response = await this.handleBridgeMessage(message);
|
|
1402
|
-
(_a = this.wsServer) == null ? void 0 : _a.send(JSON.stringify(response));
|
|
1403
|
-
} catch (error) {
|
|
1404
|
-
(_b = this.wsServer) == null ? void 0 : _b.send(JSON.stringify({
|
|
1405
|
-
id: 0,
|
|
1406
|
-
error: { code: -32603, message: error.message }
|
|
1407
|
-
}));
|
|
1408
|
-
}
|
|
1409
|
-
};
|
|
1410
|
-
this.wsServer.onerror = (error) => {
|
|
1411
|
-
console.error("[CDP] Bridge connection error:", error);
|
|
1412
|
-
};
|
|
1413
|
-
this.wsServer.onclose = () => {
|
|
1414
|
-
console.log("[CDP] Bridge disconnected, reconnecting in 5s...");
|
|
1415
|
-
setTimeout(() => this.connectToBridge(url), 5e3);
|
|
1416
|
-
};
|
|
1417
|
-
} catch (error) {
|
|
1418
|
-
console.error("[CDP] Failed to connect to bridge:", error);
|
|
1419
|
-
setTimeout(() => this.connectToBridge(url), 5e3);
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
async handleBridgeMessage(message) {
|
|
1423
|
-
const { id, method, params } = message;
|
|
1424
|
-
try {
|
|
1425
|
-
let result;
|
|
1426
|
-
const tabId = (params == null ? void 0 : params.tabId) || await this.getActiveTabId();
|
|
1427
|
-
switch (method) {
|
|
1428
|
-
case "Browser.getVersion":
|
|
1429
|
-
result = {
|
|
1430
|
-
protocolVersion: "1.3",
|
|
1431
|
-
product: "Hanzo Browser Extension",
|
|
1432
|
-
userAgent: navigator.userAgent
|
|
1433
|
-
};
|
|
1434
|
-
break;
|
|
1435
|
-
case "Target.getTargets":
|
|
1436
|
-
result = await this.getTargets();
|
|
1437
|
-
break;
|
|
1438
|
-
case "Page.navigate":
|
|
1439
|
-
this.notifyControlOverlay(tabId, "ai.control.cursor", { x: 24, y: 24 });
|
|
1440
|
-
this.notifyControlOverlay(tabId, "ai.control.status", { text: `Navigating to ${params.url}` });
|
|
1441
|
-
await this.navigate(tabId, params.url);
|
|
1442
|
-
result = { frameId: "main" };
|
|
1443
|
-
break;
|
|
1444
|
-
case "Page.captureScreenshot":
|
|
1445
|
-
const screenshot = await this.screenshot(tabId, params);
|
|
1446
|
-
result = { data: screenshot };
|
|
1447
|
-
break;
|
|
1448
|
-
case "Input.dispatchMouseEvent":
|
|
1449
|
-
if ((params == null ? void 0 : params.x) !== void 0 && (params == null ? void 0 : params.y) !== void 0) {
|
|
1450
|
-
this.notifyControlOverlay(tabId, "ai.control.cursor", { x: params.x, y: params.y });
|
|
1451
|
-
}
|
|
1452
|
-
await this.send(tabId, method, params);
|
|
1453
|
-
result = {};
|
|
1454
|
-
break;
|
|
1455
|
-
case "Input.dispatchKeyEvent":
|
|
1456
|
-
await this.send(tabId, method, params);
|
|
1457
|
-
result = {};
|
|
1458
|
-
break;
|
|
1459
|
-
case "Runtime.evaluate":
|
|
1460
|
-
result = await this.send(tabId, method, params);
|
|
1461
|
-
break;
|
|
1462
|
-
case "DOM.getDocument":
|
|
1463
|
-
result = await this.getDocument(tabId);
|
|
1464
|
-
break;
|
|
1465
|
-
case "DOM.querySelector":
|
|
1466
|
-
const nodeId = await this.querySelector(tabId, params.selector);
|
|
1467
|
-
result = { nodeId };
|
|
1468
|
-
break;
|
|
1469
|
-
// High-level commands for hanzo-mcp
|
|
1470
|
-
case "hanzo.click":
|
|
1471
|
-
this.notifyControlOverlay(tabId, "ai.control.highlight", { selector: params.selector });
|
|
1472
|
-
this.notifyControlOverlay(tabId, "ai.control.status", { text: `Clicking ${params.selector}` });
|
|
1473
|
-
const clicked = await this.clickSelector(tabId, params.selector);
|
|
1474
|
-
result = { success: clicked };
|
|
1475
|
-
break;
|
|
1476
|
-
case "hanzo.fill":
|
|
1477
|
-
this.notifyControlOverlay(tabId, "ai.control.highlight", { selector: params.selector });
|
|
1478
|
-
this.notifyControlOverlay(tabId, "ai.control.status", { text: `Filling ${params.selector}` });
|
|
1479
|
-
const filled = await this.fillSelector(tabId, params.selector, params.value);
|
|
1480
|
-
result = { success: filled };
|
|
1481
|
-
break;
|
|
1482
|
-
case "hanzo.screenshot":
|
|
1483
|
-
const screenshotData = await this.screenshot(tabId, params);
|
|
1484
|
-
result = { data: screenshotData };
|
|
1485
|
-
break;
|
|
1486
|
-
// Control overlay management
|
|
1487
|
-
case "hanzo.control.start":
|
|
1488
|
-
case "hanzo.takeover.start":
|
|
1489
|
-
this.notifyControlOverlay(tabId, "ai.control.start", { task: params.task || "Hanzo AI is controlling this tab" });
|
|
1490
|
-
result = { success: true };
|
|
1491
|
-
break;
|
|
1492
|
-
case "hanzo.control.stop":
|
|
1493
|
-
case "hanzo.takeover.end":
|
|
1494
|
-
this.notifyControlOverlay(tabId, "ai.control.stop", {});
|
|
1495
|
-
result = { success: true };
|
|
1496
|
-
break;
|
|
1497
|
-
case "hanzo.takeover.cursor":
|
|
1498
|
-
if ((params == null ? void 0 : params.x) !== void 0 && (params == null ? void 0 : params.y) !== void 0) {
|
|
1499
|
-
this.notifyControlOverlay(tabId, "ai.control.cursor", { x: params.x, y: params.y });
|
|
1500
|
-
}
|
|
1501
|
-
result = { success: true };
|
|
1502
|
-
break;
|
|
1503
|
-
default:
|
|
1504
|
-
result = await this.send(tabId, method, params);
|
|
1505
|
-
}
|
|
1506
|
-
return { id, result };
|
|
1507
|
-
} catch (error) {
|
|
1508
|
-
return {
|
|
1509
|
-
id,
|
|
1510
|
-
error: { code: -32603, message: error.message }
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
async getActiveTabId() {
|
|
1515
|
-
return new Promise((resolve, reject) => {
|
|
1516
|
-
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
|
1517
|
-
var _a;
|
|
1518
|
-
if ((_a = tabs[0]) == null ? void 0 : _a.id) {
|
|
1519
|
-
resolve(tabs[0].id);
|
|
1520
|
-
} else {
|
|
1521
|
-
reject(new Error("No active tab"));
|
|
1522
|
-
}
|
|
1523
|
-
});
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
async getTargets() {
|
|
1527
|
-
return new Promise((resolve) => {
|
|
1528
|
-
chrome.tabs.query({}, (tabs) => {
|
|
1529
|
-
const targetInfos = tabs.map((tab) => ({
|
|
1530
|
-
targetId: `tab-${tab.id}`,
|
|
1531
|
-
type: "page",
|
|
1532
|
-
title: tab.title,
|
|
1533
|
-
url: tab.url,
|
|
1534
|
-
attached: this.sessions.has(tab.id),
|
|
1535
|
-
browserContextId: "default"
|
|
1536
|
-
}));
|
|
1537
|
-
resolve({ targetInfos });
|
|
1538
|
-
});
|
|
1539
|
-
});
|
|
1540
|
-
}
|
|
1541
|
-
notifyControlOverlay(tabId, action, data) {
|
|
1542
|
-
try {
|
|
1543
|
-
chrome.tabs.sendMessage(tabId, { action, ...data }, () => {
|
|
1544
|
-
if (chrome.runtime.lastError) {
|
|
1545
|
-
}
|
|
1546
|
-
});
|
|
1547
|
-
} catch {
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
/** Check whether the bridge WebSocket is connected. */
|
|
1551
|
-
isBridgeConnected() {
|
|
1552
|
-
return this.wsServer !== null && this.wsServer.readyState === WebSocket.OPEN;
|
|
1553
|
-
}
|
|
1554
|
-
/** Send a config key/value to the bridge server for persistence. */
|
|
1555
|
-
sendConfig(key, value) {
|
|
1556
|
-
if (!this.isBridgeConnected()) return;
|
|
1557
|
-
this.wsServer.send(JSON.stringify({
|
|
1558
|
-
type: "config",
|
|
1559
|
-
key,
|
|
1560
|
-
value
|
|
1561
|
-
}));
|
|
1562
|
-
}
|
|
1563
|
-
broadcastEvent(tabId, method, params) {
|
|
1564
|
-
var _a;
|
|
1565
|
-
const event = JSON.stringify({
|
|
1566
|
-
type: "event",
|
|
1567
|
-
tabId,
|
|
1568
|
-
method,
|
|
1569
|
-
params
|
|
1570
|
-
});
|
|
1571
|
-
if (((_a = this.wsServer) == null ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
|
1572
|
-
this.wsServer.send(event);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
};
|
|
1576
|
-
var cdpBridgeInstance = null;
|
|
1577
|
-
function getCDPBridge() {
|
|
1578
|
-
if (!cdpBridgeInstance) {
|
|
1579
|
-
cdpBridgeInstance = new CDPBridge();
|
|
1580
|
-
}
|
|
1581
|
-
return cdpBridgeInstance;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
// src/auth.ts
|
|
1585
|
-
function generateRandomString(length) {
|
|
1586
|
-
const array = new Uint8Array(length);
|
|
1587
|
-
crypto.getRandomValues(array);
|
|
1588
|
-
return Array.from(array, (b) => b.toString(36).padStart(2, "0")).join("").slice(0, length);
|
|
1589
|
-
}
|
|
1590
|
-
async function sha256(plain) {
|
|
1591
|
-
const encoder = new TextEncoder();
|
|
1592
|
-
return crypto.subtle.digest("SHA-256", encoder.encode(plain));
|
|
1593
|
-
}
|
|
1594
|
-
function base64UrlEncode(buffer) {
|
|
1595
|
-
const bytes = new Uint8Array(buffer);
|
|
1596
|
-
let binary = "";
|
|
1597
|
-
for (const byte of bytes) {
|
|
1598
|
-
binary += String.fromCharCode(byte);
|
|
1599
|
-
}
|
|
1600
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1601
|
-
}
|
|
1602
|
-
async function generatePkceChallenge() {
|
|
1603
|
-
const codeVerifier = generateRandomString(64);
|
|
1604
|
-
const hash = await sha256(codeVerifier);
|
|
1605
|
-
const codeChallenge = base64UrlEncode(hash);
|
|
1606
|
-
return { codeVerifier, codeChallenge };
|
|
1607
|
-
}
|
|
1608
|
-
function generateState() {
|
|
1609
|
-
return generateRandomString(32);
|
|
1610
|
-
}
|
|
1611
|
-
var IAM_LOGIN = "https://hanzo.id";
|
|
1612
|
-
var IAM_API = "https://iam.hanzo.ai";
|
|
1613
|
-
var CLIENT_ID = "app-hanzo";
|
|
1614
|
-
var SCOPES = "openid profile email";
|
|
1615
|
-
function getRedirectUri() {
|
|
1616
|
-
return "https://hanzo.ai/callback";
|
|
1617
|
-
}
|
|
1618
|
-
var STORAGE_KEYS = {
|
|
1619
|
-
accessToken: "hanzo_iam_access_token",
|
|
1620
|
-
refreshToken: "hanzo_iam_refresh_token",
|
|
1621
|
-
idToken: "hanzo_iam_id_token",
|
|
1622
|
-
expiresAt: "hanzo_iam_expires_at",
|
|
1623
|
-
user: "hanzo_iam_user"
|
|
1624
|
-
};
|
|
1625
|
-
async function storeTokens(tokens) {
|
|
1626
|
-
const data = {
|
|
1627
|
-
[STORAGE_KEYS.accessToken]: tokens.access_token
|
|
1628
|
-
};
|
|
1629
|
-
if (tokens.refresh_token) data[STORAGE_KEYS.refreshToken] = tokens.refresh_token;
|
|
1630
|
-
if (tokens.id_token) data[STORAGE_KEYS.idToken] = tokens.id_token;
|
|
1631
|
-
if (tokens.expires_in) {
|
|
1632
|
-
data[STORAGE_KEYS.expiresAt] = Date.now() + tokens.expires_in * 1e3;
|
|
1633
|
-
}
|
|
1634
|
-
return chrome.storage.local.set(data);
|
|
1635
|
-
}
|
|
1636
|
-
async function getStoredTokens() {
|
|
1637
|
-
return new Promise((resolve) => {
|
|
1638
|
-
chrome.storage.local.get(
|
|
1639
|
-
[STORAGE_KEYS.accessToken, STORAGE_KEYS.refreshToken, STORAGE_KEYS.expiresAt],
|
|
1640
|
-
(result) => {
|
|
1641
|
-
resolve({
|
|
1642
|
-
accessToken: result[STORAGE_KEYS.accessToken] || null,
|
|
1643
|
-
refreshToken: result[STORAGE_KEYS.refreshToken] || null,
|
|
1644
|
-
expiresAt: result[STORAGE_KEYS.expiresAt] || null
|
|
1645
|
-
});
|
|
1646
|
-
}
|
|
1647
|
-
);
|
|
1648
|
-
});
|
|
1649
|
-
}
|
|
1650
|
-
async function login() {
|
|
1651
|
-
const { codeVerifier, codeChallenge } = await generatePkceChallenge();
|
|
1652
|
-
const state = generateState();
|
|
1653
|
-
const redirectUri = getRedirectUri();
|
|
1654
|
-
const authorizeUrl = new URL(`${IAM_LOGIN}/login/oauth/authorize`);
|
|
1655
|
-
authorizeUrl.searchParams.set("client_id", CLIENT_ID);
|
|
1656
|
-
authorizeUrl.searchParams.set("response_type", "code");
|
|
1657
|
-
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
1658
|
-
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
1659
|
-
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
1660
|
-
authorizeUrl.searchParams.set("scope", SCOPES);
|
|
1661
|
-
authorizeUrl.searchParams.set("state", state);
|
|
1662
|
-
const callbackUrl = await openAuthTab(authorizeUrl.toString(), redirectUri);
|
|
1663
|
-
const url = new URL(callbackUrl);
|
|
1664
|
-
const returnedState = url.searchParams.get("state");
|
|
1665
|
-
if (returnedState !== state) throw new Error("State mismatch \u2014 possible CSRF");
|
|
1666
|
-
const code = url.searchParams.get("code");
|
|
1667
|
-
const directToken = url.searchParams.get("access_token");
|
|
1668
|
-
if (directToken) {
|
|
1669
|
-
const tokens = {
|
|
1670
|
-
access_token: directToken,
|
|
1671
|
-
refresh_token: url.searchParams.get("refresh_token") || void 0,
|
|
1672
|
-
token_type: "Bearer"
|
|
1673
|
-
};
|
|
1674
|
-
await storeTokens(tokens);
|
|
1675
|
-
} else if (code) {
|
|
1676
|
-
const tokenResponse = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
|
|
1677
|
-
method: "POST",
|
|
1678
|
-
headers: { "Content-Type": "application/json" },
|
|
1679
|
-
body: JSON.stringify({
|
|
1680
|
-
grant_type: "authorization_code",
|
|
1681
|
-
client_id: CLIENT_ID,
|
|
1682
|
-
code,
|
|
1683
|
-
redirect_uri: redirectUri,
|
|
1684
|
-
code_verifier: codeVerifier
|
|
1685
|
-
})
|
|
1686
|
-
});
|
|
1687
|
-
if (!tokenResponse.ok) {
|
|
1688
|
-
const errText = await tokenResponse.text();
|
|
1689
|
-
throw new Error(`Token exchange failed: ${errText}`);
|
|
1690
|
-
}
|
|
1691
|
-
const tokens = await tokenResponse.json();
|
|
1692
|
-
await storeTokens(tokens);
|
|
1693
|
-
} else {
|
|
1694
|
-
const error = url.searchParams.get("error_description") || url.searchParams.get("error") || "No authorization code or token";
|
|
1695
|
-
throw new Error(error);
|
|
1696
|
-
}
|
|
1697
|
-
const { accessToken } = await getStoredTokens();
|
|
1698
|
-
if (!accessToken) throw new Error("Login succeeded but no token was stored");
|
|
1699
|
-
const user = await fetchUserInfo(accessToken);
|
|
1700
|
-
await chrome.storage.local.set({ [STORAGE_KEYS.user]: user });
|
|
1701
|
-
return user;
|
|
1702
|
-
}
|
|
1703
|
-
function openAuthTab(authorizeUrl, redirectUriPrefix) {
|
|
1704
|
-
return new Promise((resolve, reject) => {
|
|
1705
|
-
let authTabId;
|
|
1706
|
-
const timeout = setTimeout(() => {
|
|
1707
|
-
cleanup();
|
|
1708
|
-
reject(new Error("Login timed out"));
|
|
1709
|
-
}, 3e5);
|
|
1710
|
-
function cleanup() {
|
|
1711
|
-
clearTimeout(timeout);
|
|
1712
|
-
chrome.tabs.onUpdated.removeListener(onTabUpdated);
|
|
1713
|
-
chrome.tabs.onRemoved.removeListener(onTabRemoved);
|
|
1714
|
-
}
|
|
1715
|
-
function onTabUpdated(tabId, changeInfo) {
|
|
1716
|
-
if (tabId !== authTabId || !changeInfo.url) return;
|
|
1717
|
-
if (changeInfo.url.startsWith(redirectUriPrefix)) {
|
|
1718
|
-
const callbackUrl = changeInfo.url;
|
|
1719
|
-
cleanup();
|
|
1720
|
-
chrome.tabs.remove(tabId).catch(() => {
|
|
1721
|
-
});
|
|
1722
|
-
resolve(callbackUrl);
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
function onTabRemoved(tabId) {
|
|
1726
|
-
if (tabId !== authTabId) return;
|
|
1727
|
-
cleanup();
|
|
1728
|
-
reject(new Error("Login cancelled"));
|
|
1729
|
-
}
|
|
1730
|
-
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
|
1731
|
-
chrome.tabs.onRemoved.addListener(onTabRemoved);
|
|
1732
|
-
chrome.tabs.create({ url: authorizeUrl }, (tab) => {
|
|
1733
|
-
var _a;
|
|
1734
|
-
if (chrome.runtime.lastError || !(tab == null ? void 0 : tab.id)) {
|
|
1735
|
-
cleanup();
|
|
1736
|
-
reject(new Error(((_a = chrome.runtime.lastError) == null ? void 0 : _a.message) || "Failed to open login tab"));
|
|
1737
|
-
return;
|
|
1738
|
-
}
|
|
1739
|
-
authTabId = tab.id;
|
|
1740
|
-
});
|
|
1741
|
-
});
|
|
1742
|
-
}
|
|
1743
|
-
async function logout() {
|
|
1744
|
-
await chrome.storage.local.remove(Object.values(STORAGE_KEYS));
|
|
1745
|
-
}
|
|
1746
|
-
async function getValidAccessToken() {
|
|
1747
|
-
const { accessToken, refreshToken, expiresAt } = await getStoredTokens();
|
|
1748
|
-
if (!accessToken) return null;
|
|
1749
|
-
if (expiresAt && Date.now() < expiresAt - 6e4) {
|
|
1750
|
-
return accessToken;
|
|
1751
|
-
}
|
|
1752
|
-
if (refreshToken) {
|
|
1753
|
-
try {
|
|
1754
|
-
const refreshed = await refreshAccessToken(refreshToken);
|
|
1755
|
-
return refreshed;
|
|
1756
|
-
} catch {
|
|
1757
|
-
await logout();
|
|
1758
|
-
return null;
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
return accessToken;
|
|
1762
|
-
}
|
|
1763
|
-
async function refreshAccessToken(refreshToken) {
|
|
1764
|
-
const response = await fetch(`${IAM_API}/api/login/oauth/access_token`, {
|
|
1765
|
-
method: "POST",
|
|
1766
|
-
headers: { "Content-Type": "application/json" },
|
|
1767
|
-
body: JSON.stringify({
|
|
1768
|
-
grant_type: "refresh_token",
|
|
1769
|
-
client_id: CLIENT_ID,
|
|
1770
|
-
refresh_token: refreshToken
|
|
1771
|
-
})
|
|
1772
|
-
});
|
|
1773
|
-
if (!response.ok) throw new Error("Token refresh failed");
|
|
1774
|
-
const tokens = await response.json();
|
|
1775
|
-
await storeTokens(tokens);
|
|
1776
|
-
return tokens.access_token;
|
|
1777
|
-
}
|
|
1778
|
-
async function fetchUserInfo(accessToken) {
|
|
1779
|
-
const response = await fetch(`${IAM_API}/api/userinfo`, {
|
|
1780
|
-
headers: { Authorization: `Bearer ${accessToken}` }
|
|
1781
|
-
});
|
|
1782
|
-
if (!response.ok) throw new Error("Failed to fetch user info");
|
|
1783
|
-
return response.json();
|
|
1784
|
-
}
|
|
1785
|
-
async function getUserInfo() {
|
|
1786
|
-
return new Promise((resolve) => {
|
|
1787
|
-
chrome.storage.local.get([STORAGE_KEYS.user], (result) => {
|
|
1788
|
-
resolve(result[STORAGE_KEYS.user] || null);
|
|
1789
|
-
});
|
|
1790
|
-
});
|
|
1791
|
-
}
|
|
1792
|
-
async function isAuthenticated() {
|
|
1793
|
-
const { accessToken } = await getStoredTokens();
|
|
1794
|
-
return !!accessToken;
|
|
1795
|
-
}
|
|
1796
|
-
async function getAuthStatus() {
|
|
1797
|
-
const [authenticated, user] = await Promise.all([
|
|
1798
|
-
isAuthenticated(),
|
|
1799
|
-
getUserInfo()
|
|
1800
|
-
]);
|
|
1801
|
-
return { authenticated, user };
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
// src/chat-client.ts
|
|
1805
|
-
var API_BASE = "https://api.hanzo.ai";
|
|
1806
|
-
async function listModels(token) {
|
|
1807
|
-
const response = await fetch(`${API_BASE}/v1/models`, {
|
|
1808
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
1809
|
-
});
|
|
1810
|
-
if (!response.ok) {
|
|
1811
|
-
throw new Error(`Failed to list models: ${response.status}`);
|
|
1812
|
-
}
|
|
1813
|
-
const data = await response.json();
|
|
1814
|
-
return data.data || data.models || data || [];
|
|
1815
|
-
}
|
|
1816
|
-
async function chatCompletion(token, options) {
|
|
1817
|
-
var _a, _b, _c;
|
|
1818
|
-
const response = await fetch(`${API_BASE}/v1/chat/completions`, {
|
|
1819
|
-
method: "POST",
|
|
1820
|
-
headers: {
|
|
1821
|
-
"Content-Type": "application/json",
|
|
1822
|
-
Authorization: `Bearer ${token}`
|
|
1823
|
-
},
|
|
1824
|
-
body: JSON.stringify(options)
|
|
1825
|
-
});
|
|
1826
|
-
if (!response.ok) {
|
|
1827
|
-
const text = await response.text();
|
|
1828
|
-
throw new Error(`Chat API error ${response.status}: ${text}`);
|
|
1829
|
-
}
|
|
1830
|
-
const data = await response.json();
|
|
1831
|
-
return ((_c = (_b = (_a = data.choices) == null ? void 0 : _a[0]) == null ? void 0 : _b.message) == null ? void 0 : _c.content) || "";
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
// src/background.ts
|
|
1835
|
-
var browserControl = new BrowserControl();
|
|
1836
|
-
var webgpuAI = new WebGPUAI();
|
|
1837
|
-
var cdpBridge = getCDPBridge();
|
|
1838
|
-
var zapState = {
|
|
1839
|
-
connected: false,
|
|
1840
|
-
mcps: [],
|
|
1841
|
-
extensionId: `hanzo-ext-${Date.now().toString(36)}`
|
|
1842
|
-
};
|
|
1843
|
-
var controlSession = {
|
|
1844
|
-
active: false,
|
|
1845
|
-
tabId: null,
|
|
1846
|
-
task: null,
|
|
1847
|
-
startedAt: null
|
|
1848
|
-
};
|
|
1849
|
-
var DEFAULT_ZAP_PORTS = [9999, 9998, 9997, 9996, 9995];
|
|
1850
|
-
var DEFAULT_MCP_PORT = 3001;
|
|
1851
|
-
var DEFAULT_CDP_PORT = 9223;
|
|
1852
|
-
var ZAP_RECONNECT_DELAY = 3e3;
|
|
1853
|
-
var ZAP_DISCOVERY_TIMEOUT = 2e3;
|
|
1854
|
-
var DEFAULT_RAG_TOP_K = 5;
|
|
1855
|
-
async function getPortConfig() {
|
|
1856
|
-
return new Promise((resolve) => {
|
|
1857
|
-
chrome.storage.local.get(["zapPorts", "mcpPort", "cdpPort"], (result) => {
|
|
1858
|
-
resolve({
|
|
1859
|
-
zapPorts: result.zapPorts || DEFAULT_ZAP_PORTS,
|
|
1860
|
-
mcpPort: result.mcpPort || DEFAULT_MCP_PORT,
|
|
1861
|
-
cdpPort: result.cdpPort || DEFAULT_CDP_PORT
|
|
1862
|
-
});
|
|
1863
|
-
});
|
|
1864
|
-
});
|
|
1865
|
-
}
|
|
1866
|
-
async function getRagConfig() {
|
|
1867
|
-
return new Promise((resolve) => {
|
|
1868
|
-
chrome.storage.local.get([
|
|
1869
|
-
"hanzo_rag_endpoint",
|
|
1870
|
-
"hanzo_rag_api_key",
|
|
1871
|
-
"hanzo_rag_kb",
|
|
1872
|
-
"hanzo_rag_top_k",
|
|
1873
|
-
"hanzo_rag_include_tab_context",
|
|
1874
|
-
"hanzo_rag_use_zap"
|
|
1875
|
-
], (result) => {
|
|
1876
|
-
const parsedTopK = Number.parseInt(String(result.hanzo_rag_top_k ?? DEFAULT_RAG_TOP_K), 10);
|
|
1877
|
-
resolve({
|
|
1878
|
-
endpoint: String(result.hanzo_rag_endpoint || "").trim(),
|
|
1879
|
-
apiKey: String(result.hanzo_rag_api_key || "").trim(),
|
|
1880
|
-
knowledgeBase: String(result.hanzo_rag_kb || "").trim(),
|
|
1881
|
-
topK: Number.isFinite(parsedTopK) ? Math.max(1, Math.min(parsedTopK, 20)) : DEFAULT_RAG_TOP_K,
|
|
1882
|
-
includeTabContext: result.hanzo_rag_include_tab_context !== false,
|
|
1883
|
-
useZapMemory: result.hanzo_rag_use_zap !== false
|
|
1884
|
-
});
|
|
1885
|
-
});
|
|
1886
|
-
});
|
|
1887
|
-
}
|
|
1888
|
-
var zapConnections = /* @__PURE__ */ new Map();
|
|
1889
|
-
var ZAP_MAGIC = new Uint8Array([90, 65, 80, 1]);
|
|
1890
|
-
var MSG_HANDSHAKE = 1;
|
|
1891
|
-
var MSG_HANDSHAKE_OK = 2;
|
|
1892
|
-
var MSG_REQUEST = 16;
|
|
1893
|
-
var MSG_RESPONSE = 17;
|
|
1894
|
-
var MSG_PING = 254;
|
|
1895
|
-
var MSG_PONG = 255;
|
|
1896
|
-
function encodeZapMessage(type, payload) {
|
|
1897
|
-
const json = JSON.stringify(payload);
|
|
1898
|
-
const encoder = new TextEncoder();
|
|
1899
|
-
const data = encoder.encode(json);
|
|
1900
|
-
const buf = new ArrayBuffer(4 + 1 + 4 + data.length);
|
|
1901
|
-
const view = new DataView(buf);
|
|
1902
|
-
new Uint8Array(buf, 0, 4).set(ZAP_MAGIC);
|
|
1903
|
-
view.setUint8(4, type);
|
|
1904
|
-
view.setUint32(5, data.length, false);
|
|
1905
|
-
new Uint8Array(buf, 9).set(data);
|
|
1906
|
-
return buf;
|
|
1907
|
-
}
|
|
1908
|
-
function decodeZapMessage(buf) {
|
|
1909
|
-
if (buf.byteLength < 9) return null;
|
|
1910
|
-
const view = new DataView(buf);
|
|
1911
|
-
const magic = new Uint8Array(buf, 0, 4);
|
|
1912
|
-
if (magic[0] !== 90 || magic[1] !== 65 || magic[2] !== 80 || magic[3] !== 1) {
|
|
1913
|
-
return null;
|
|
1914
|
-
}
|
|
1915
|
-
const type = view.getUint8(4);
|
|
1916
|
-
const length = view.getUint32(5, false);
|
|
1917
|
-
const decoder = new TextDecoder();
|
|
1918
|
-
const json = decoder.decode(new Uint8Array(buf, 9, length));
|
|
1919
|
-
return { type, payload: JSON.parse(json) };
|
|
1920
|
-
}
|
|
1921
|
-
function probeZapServer(port) {
|
|
1922
|
-
return new Promise((resolve) => {
|
|
1923
|
-
const url = `ws://localhost:${port}`;
|
|
1924
|
-
const ws2 = new WebSocket(url);
|
|
1925
|
-
const timer = setTimeout(() => {
|
|
1926
|
-
ws2.close();
|
|
1927
|
-
resolve(null);
|
|
1928
|
-
}, ZAP_DISCOVERY_TIMEOUT);
|
|
1929
|
-
ws2.binaryType = "arraybuffer";
|
|
1930
|
-
ws2.onopen = () => {
|
|
1931
|
-
clearTimeout(timer);
|
|
1932
|
-
ws2.send(encodeZapMessage(MSG_HANDSHAKE, {
|
|
1933
|
-
clientId: zapState.extensionId,
|
|
1934
|
-
clientType: "browser_extension",
|
|
1935
|
-
browser: detectBrowser(),
|
|
1936
|
-
version: chrome.runtime.getManifest().version,
|
|
1937
|
-
capabilities: ["tabs", "navigate", "screenshot", "evaluate", "cookies", "storage"]
|
|
1938
|
-
}));
|
|
1939
|
-
const hsTimer = setTimeout(() => {
|
|
1940
|
-
ws2.close();
|
|
1941
|
-
resolve(null);
|
|
1942
|
-
}, ZAP_DISCOVERY_TIMEOUT);
|
|
1943
|
-
ws2.onmessage = (ev) => {
|
|
1944
|
-
clearTimeout(hsTimer);
|
|
1945
|
-
ws2.close();
|
|
1946
|
-
resolve(url);
|
|
1947
|
-
};
|
|
1948
|
-
};
|
|
1949
|
-
ws2.onerror = () => {
|
|
1950
|
-
clearTimeout(timer);
|
|
1951
|
-
resolve(null);
|
|
1952
|
-
};
|
|
1953
|
-
});
|
|
1954
|
-
}
|
|
1955
|
-
async function connectZap(url) {
|
|
1956
|
-
return new Promise((resolve) => {
|
|
1957
|
-
const ws2 = new WebSocket(url);
|
|
1958
|
-
ws2.binaryType = "arraybuffer";
|
|
1959
|
-
const connTimer = setTimeout(() => {
|
|
1960
|
-
ws2.close();
|
|
1961
|
-
resolve(null);
|
|
1962
|
-
}, 1e4);
|
|
1963
|
-
ws2.onopen = () => {
|
|
1964
|
-
ws2.send(encodeZapMessage(MSG_HANDSHAKE, {
|
|
1965
|
-
clientId: zapState.extensionId,
|
|
1966
|
-
clientType: "browser_extension",
|
|
1967
|
-
browser: detectBrowser(),
|
|
1968
|
-
version: chrome.runtime.getManifest().version,
|
|
1969
|
-
capabilities: ["tabs", "navigate", "screenshot", "evaluate", "cookies", "storage"]
|
|
1970
|
-
}));
|
|
1971
|
-
};
|
|
1972
|
-
ws2.onmessage = (ev) => {
|
|
1973
|
-
let msg = null;
|
|
1974
|
-
if (ev.data instanceof ArrayBuffer) {
|
|
1975
|
-
msg = decodeZapMessage(ev.data);
|
|
1976
|
-
}
|
|
1977
|
-
if (!msg && typeof ev.data === "string") {
|
|
1978
|
-
try {
|
|
1979
|
-
const json = JSON.parse(ev.data);
|
|
1980
|
-
handleMCPMessage(json);
|
|
1981
|
-
return;
|
|
1982
|
-
} catch {
|
|
1983
|
-
return;
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
if (!msg) return;
|
|
1987
|
-
switch (msg.type) {
|
|
1988
|
-
case MSG_HANDSHAKE_OK: {
|
|
1989
|
-
clearTimeout(connTimer);
|
|
1990
|
-
const info = msg.payload;
|
|
1991
|
-
const mcpId = info.serverId || `mcp-${Date.now().toString(36)}`;
|
|
1992
|
-
zapConnections.set(mcpId, ws2);
|
|
1993
|
-
zapState.connected = true;
|
|
1994
|
-
zapState.mcps.push({
|
|
1995
|
-
id: mcpId,
|
|
1996
|
-
name: info.name || `MCP@${url}`,
|
|
1997
|
-
url,
|
|
1998
|
-
tools: (info.tools || []).map((t) => t.name)
|
|
1999
|
-
});
|
|
2000
|
-
console.log(`[Hanzo/ZAP] Connected to ${info.name || url} (${(info.tools || []).length} tools)`);
|
|
2001
|
-
resolve(mcpId);
|
|
2002
|
-
break;
|
|
2003
|
-
}
|
|
2004
|
-
case MSG_RESPONSE: {
|
|
2005
|
-
const { id, result, error } = msg.payload;
|
|
2006
|
-
const pending = pendingRequests.get(id);
|
|
2007
|
-
if (pending) {
|
|
2008
|
-
pendingRequests.delete(id);
|
|
2009
|
-
if (error) pending.reject(new Error(error.message || error));
|
|
2010
|
-
else pending.resolve(result);
|
|
2011
|
-
}
|
|
2012
|
-
break;
|
|
2013
|
-
}
|
|
2014
|
-
case MSG_PING:
|
|
2015
|
-
ws2.send(encodeZapMessage(MSG_PONG, {}));
|
|
2016
|
-
break;
|
|
2017
|
-
}
|
|
2018
|
-
};
|
|
2019
|
-
ws2.onclose = () => {
|
|
2020
|
-
for (const [id, conn] of zapConnections) {
|
|
2021
|
-
if (conn === ws2) {
|
|
2022
|
-
zapConnections.delete(id);
|
|
2023
|
-
zapState.mcps = zapState.mcps.filter((m) => m.id !== id);
|
|
2024
|
-
break;
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
zapState.connected = zapConnections.size > 0;
|
|
2028
|
-
console.log(`[Hanzo/ZAP] Disconnected from ${url}, reconnecting in ${ZAP_RECONNECT_DELAY}ms...`);
|
|
2029
|
-
setTimeout(() => connectZap(url), ZAP_RECONNECT_DELAY);
|
|
2030
|
-
};
|
|
2031
|
-
ws2.onerror = () => {
|
|
2032
|
-
clearTimeout(connTimer);
|
|
2033
|
-
resolve(null);
|
|
2034
|
-
};
|
|
2035
|
-
});
|
|
2036
|
-
}
|
|
2037
|
-
var pendingRequests = /* @__PURE__ */ new Map();
|
|
2038
|
-
var requestIdCounter = 0;
|
|
2039
|
-
function zapRequest(mcpId, method, params = {}) {
|
|
2040
|
-
const ws2 = zapConnections.get(mcpId);
|
|
2041
|
-
if (!ws2 || ws2.readyState !== WebSocket.OPEN) {
|
|
2042
|
-
return Promise.reject(new Error(`MCP ${mcpId} not connected`));
|
|
2043
|
-
}
|
|
2044
|
-
const id = `req-${++requestIdCounter}`;
|
|
2045
|
-
return new Promise((resolve, reject) => {
|
|
2046
|
-
const timer = setTimeout(() => {
|
|
2047
|
-
pendingRequests.delete(id);
|
|
2048
|
-
reject(new Error(`ZAP request timeout: ${method}`));
|
|
2049
|
-
}, 3e4);
|
|
2050
|
-
pendingRequests.set(id, {
|
|
2051
|
-
resolve: (r) => {
|
|
2052
|
-
clearTimeout(timer);
|
|
2053
|
-
resolve(r);
|
|
2054
|
-
},
|
|
2055
|
-
reject: (e) => {
|
|
2056
|
-
clearTimeout(timer);
|
|
2057
|
-
reject(e);
|
|
2058
|
-
}
|
|
2059
|
-
});
|
|
2060
|
-
ws2.send(encodeZapMessage(MSG_REQUEST, { id, method, params }));
|
|
2061
|
-
});
|
|
2062
|
-
}
|
|
2063
|
-
async function zapCallTool(name, args = {}, targetMcpId) {
|
|
2064
|
-
if (targetMcpId) {
|
|
2065
|
-
return zapRequest(targetMcpId, "tools/call", { name, arguments: args });
|
|
2066
|
-
}
|
|
2067
|
-
for (const mcp of zapState.mcps) {
|
|
2068
|
-
if (mcp.tools.includes(name)) {
|
|
2069
|
-
return zapRequest(mcp.id, "tools/call", { name, arguments: args });
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
throw new Error(`Tool not found on any ZAP-connected MCP: ${name}`);
|
|
2073
|
-
}
|
|
2074
|
-
function hasZapTool(name) {
|
|
2075
|
-
return zapState.mcps.some((mcp) => mcp.tools.includes(name));
|
|
2076
|
-
}
|
|
2077
|
-
function normalizeRagSnippets(raw) {
|
|
2078
|
-
if (!raw) return [];
|
|
2079
|
-
const candidates = [];
|
|
2080
|
-
if (Array.isArray(raw)) candidates.push(...raw);
|
|
2081
|
-
if (Array.isArray(raw == null ? void 0 : raw.snippets)) candidates.push(...raw.snippets);
|
|
2082
|
-
if (Array.isArray(raw == null ? void 0 : raw.documents)) candidates.push(...raw.documents);
|
|
2083
|
-
if (Array.isArray(raw == null ? void 0 : raw.items)) candidates.push(...raw.items);
|
|
2084
|
-
if (Array.isArray(raw == null ? void 0 : raw.results)) candidates.push(...raw.results);
|
|
2085
|
-
if (Array.isArray(raw == null ? void 0 : raw.memories)) candidates.push(...raw.memories);
|
|
2086
|
-
if (Array.isArray(raw == null ? void 0 : raw.matches)) candidates.push(...raw.matches);
|
|
2087
|
-
if (!candidates.length && typeof (raw == null ? void 0 : raw.content) === "string") {
|
|
2088
|
-
candidates.push({ content: raw.content, source: raw.source || "memory" });
|
|
2089
|
-
}
|
|
2090
|
-
const snippets = candidates.map((item) => {
|
|
2091
|
-
const content = String(
|
|
2092
|
-
(item == null ? void 0 : item.content) ?? (item == null ? void 0 : item.text) ?? (item == null ? void 0 : item.snippet) ?? (item == null ? void 0 : item.body) ?? (item == null ? void 0 : item.value) ?? ""
|
|
2093
|
-
).trim();
|
|
2094
|
-
if (!content) return null;
|
|
2095
|
-
return {
|
|
2096
|
-
content,
|
|
2097
|
-
title: (item == null ? void 0 : item.title) ? String(item.title) : void 0,
|
|
2098
|
-
source: (item == null ? void 0 : item.source) ? String(item.source) : void 0,
|
|
2099
|
-
score: typeof (item == null ? void 0 : item.score) === "number" ? item.score : void 0,
|
|
2100
|
-
url: (item == null ? void 0 : item.url) ? String(item.url) : void 0
|
|
2101
|
-
};
|
|
2102
|
-
}).filter((item) => !!item);
|
|
2103
|
-
return snippets.slice(0, 20);
|
|
2104
|
-
}
|
|
2105
|
-
async function queryRagFromZap(params) {
|
|
2106
|
-
if (!params.useZapMemory || !zapState.connected || !hasZapTool("memory")) {
|
|
2107
|
-
return [];
|
|
2108
|
-
}
|
|
2109
|
-
const payloads = [
|
|
2110
|
-
{
|
|
2111
|
-
action: "query",
|
|
2112
|
-
query: params.query,
|
|
2113
|
-
top_k: params.topK,
|
|
2114
|
-
limit: params.topK,
|
|
2115
|
-
kb: params.knowledgeBase || void 0,
|
|
2116
|
-
page_context: params.pageContext
|
|
2117
|
-
},
|
|
2118
|
-
{
|
|
2119
|
-
query: params.query,
|
|
2120
|
-
topK: params.topK,
|
|
2121
|
-
limit: params.topK,
|
|
2122
|
-
knowledge_base: params.knowledgeBase || void 0,
|
|
2123
|
-
context: params.pageContext
|
|
2124
|
-
}
|
|
2125
|
-
];
|
|
2126
|
-
for (const payload of payloads) {
|
|
2127
|
-
try {
|
|
2128
|
-
const raw = await zapCallTool("memory", payload, params.mcpId);
|
|
2129
|
-
const snippets = normalizeRagSnippets(raw);
|
|
2130
|
-
if (snippets.length) {
|
|
2131
|
-
return snippets.map((snippet) => ({
|
|
2132
|
-
...snippet,
|
|
2133
|
-
source: snippet.source || "zap-memory"
|
|
2134
|
-
}));
|
|
2135
|
-
}
|
|
2136
|
-
} catch {
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
return [];
|
|
2140
|
-
}
|
|
2141
|
-
async function queryRagFromEndpoint(params) {
|
|
2142
|
-
if (!params.endpoint) return [];
|
|
2143
|
-
const headers = {
|
|
2144
|
-
"Content-Type": "application/json"
|
|
2145
|
-
};
|
|
2146
|
-
if (params.apiKey) {
|
|
2147
|
-
headers.Authorization = `Bearer ${params.apiKey}`;
|
|
2148
|
-
}
|
|
2149
|
-
const response = await fetch(params.endpoint, {
|
|
2150
|
-
method: "POST",
|
|
2151
|
-
headers,
|
|
2152
|
-
body: JSON.stringify({
|
|
2153
|
-
query: params.query,
|
|
2154
|
-
top_k: params.topK,
|
|
2155
|
-
knowledge_base: params.knowledgeBase || void 0,
|
|
2156
|
-
page_context: params.includeTabContext ? params.pageContext : void 0,
|
|
2157
|
-
source: "hanzo-browser-extension"
|
|
2158
|
-
})
|
|
2159
|
-
});
|
|
2160
|
-
if (!response.ok) {
|
|
2161
|
-
const text = await response.text();
|
|
2162
|
-
throw new Error(`RAG endpoint error ${response.status}: ${text || "Unknown error"}`);
|
|
2163
|
-
}
|
|
2164
|
-
const raw = await response.json();
|
|
2165
|
-
return normalizeRagSnippets(raw).map((snippet) => ({
|
|
2166
|
-
...snippet,
|
|
2167
|
-
source: snippet.source || "rag-endpoint"
|
|
2168
|
-
}));
|
|
2169
|
-
}
|
|
2170
|
-
async function discoverZapServers() {
|
|
2171
|
-
console.log("[Hanzo/ZAP] Discovering MCP servers...");
|
|
2172
|
-
const { zapPorts } = await getPortConfig();
|
|
2173
|
-
const results = await Promise.all(zapPorts.map((p) => probeZapServer(p)));
|
|
2174
|
-
const available = results.filter(Boolean);
|
|
2175
|
-
if (available.length === 0) {
|
|
2176
|
-
console.log("[Hanzo/ZAP] No servers found, retrying in 10s...");
|
|
2177
|
-
setTimeout(discoverZapServers, 1e4);
|
|
2178
|
-
return;
|
|
2179
|
-
}
|
|
2180
|
-
console.log(`[Hanzo/ZAP] Found ${available.length} server(s): ${available.join(", ")}`);
|
|
2181
|
-
await Promise.all(available.map((url) => connectZap(url)));
|
|
2182
|
-
}
|
|
2183
|
-
function detectBrowser() {
|
|
2184
|
-
if (typeof navigator === "undefined") return "unknown";
|
|
2185
|
-
const ua = navigator.userAgent;
|
|
2186
|
-
if (ua.includes("Firefox")) return "firefox";
|
|
2187
|
-
if (ua.includes("Edg/")) return "edge";
|
|
2188
|
-
if (ua.includes("Chrome")) return "chrome";
|
|
2189
|
-
if (ua.includes("Safari")) return "safari";
|
|
2190
|
-
return "unknown";
|
|
2191
|
-
}
|
|
2192
|
-
function sendControlMessageToTab(tabId, message) {
|
|
2193
|
-
chrome.tabs.sendMessage(tabId, message, () => {
|
|
2194
|
-
if (chrome.runtime.lastError) {
|
|
2195
|
-
}
|
|
2196
|
-
});
|
|
2197
|
-
}
|
|
2198
|
-
function sendMessageToTab(tabId, message) {
|
|
2199
|
-
return new Promise((resolve, reject) => {
|
|
2200
|
-
chrome.tabs.sendMessage(tabId, message, (response) => {
|
|
2201
|
-
if (chrome.runtime.lastError) {
|
|
2202
|
-
reject(new Error(chrome.runtime.lastError.message || "Failed to contact page content script"));
|
|
2203
|
-
return;
|
|
2204
|
-
}
|
|
2205
|
-
resolve(response);
|
|
2206
|
-
});
|
|
2207
|
-
});
|
|
2208
|
-
}
|
|
2209
|
-
function getActiveTabId() {
|
|
2210
|
-
return new Promise((resolve) => {
|
|
2211
|
-
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
|
2212
|
-
var _a;
|
|
2213
|
-
resolve(((_a = tabs[0]) == null ? void 0 : _a.id) ?? null);
|
|
2214
|
-
});
|
|
2215
|
-
});
|
|
2216
|
-
}
|
|
2217
|
-
async function resolveControlTabId(tabId) {
|
|
2218
|
-
if (typeof tabId === "number") return tabId;
|
|
2219
|
-
if (controlSession.active && controlSession.tabId !== null) return controlSession.tabId;
|
|
2220
|
-
return getActiveTabId();
|
|
2221
|
-
}
|
|
2222
|
-
async function startControlSession(tabId, task) {
|
|
2223
|
-
if (controlSession.active && controlSession.tabId !== null && controlSession.tabId !== tabId) {
|
|
2224
|
-
sendControlMessageToTab(controlSession.tabId, { action: "ai.control.stop" });
|
|
2225
|
-
}
|
|
2226
|
-
controlSession.active = true;
|
|
2227
|
-
controlSession.tabId = tabId;
|
|
2228
|
-
controlSession.task = task || "AI is controlling this page";
|
|
2229
|
-
controlSession.startedAt = Date.now();
|
|
2230
|
-
sendControlMessageToTab(tabId, {
|
|
2231
|
-
action: "ai.control.start",
|
|
2232
|
-
task: controlSession.task
|
|
2233
|
-
});
|
|
2234
|
-
}
|
|
2235
|
-
async function stopControlSession() {
|
|
2236
|
-
if (controlSession.active && controlSession.tabId !== null) {
|
|
2237
|
-
sendControlMessageToTab(controlSession.tabId, { action: "ai.control.stop" });
|
|
2238
|
-
}
|
|
2239
|
-
controlSession.active = false;
|
|
2240
|
-
controlSession.tabId = null;
|
|
2241
|
-
controlSession.task = null;
|
|
2242
|
-
controlSession.startedAt = null;
|
|
2243
|
-
}
|
|
2244
|
-
chrome.runtime.onInstalled.addListener(async () => {
|
|
2245
|
-
console.log("[Hanzo] Extension installed, initializing...");
|
|
2246
|
-
const gpuAvailable = await webgpuAI.initialize();
|
|
2247
|
-
if (gpuAvailable) {
|
|
2248
|
-
console.log("[Hanzo] WebGPU available, loading local models...");
|
|
2249
|
-
try {
|
|
2250
|
-
await webgpuAI.loadModel({
|
|
2251
|
-
name: "hanzo-browser-control",
|
|
2252
|
-
url: chrome.runtime.getURL("models/browser-control-4bit.bin"),
|
|
2253
|
-
quantization: "4bit",
|
|
2254
|
-
maxTokens: 512
|
|
2255
|
-
});
|
|
2256
|
-
} catch (error) {
|
|
2257
|
-
console.error("[Hanzo] Failed to load local model:", error);
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
});
|
|
2261
|
-
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
2262
|
-
handleMessage(request, sender, sendResponse);
|
|
2263
|
-
return true;
|
|
2264
|
-
});
|
|
2265
|
-
async function handleMessage(request, sender, sendResponse) {
|
|
2266
|
-
var _a, _b;
|
|
2267
|
-
switch (request.action) {
|
|
2268
|
-
// --- ZAP Protocol actions ---
|
|
2269
|
-
case "zap.status":
|
|
2270
|
-
sendResponse({ success: true, zap: zapState });
|
|
2271
|
-
break;
|
|
2272
|
-
case "zap.discover":
|
|
2273
|
-
discoverZapServers().then(() => sendResponse({ success: true, mcps: zapState.mcps }));
|
|
2274
|
-
break;
|
|
2275
|
-
case "zap.connect":
|
|
2276
|
-
try {
|
|
2277
|
-
const mcpId = await connectZap(request.url);
|
|
2278
|
-
sendResponse({ success: !!mcpId, mcpId });
|
|
2279
|
-
} catch (e) {
|
|
2280
|
-
sendResponse({ success: false, error: e.message });
|
|
2281
|
-
}
|
|
2282
|
-
break;
|
|
2283
|
-
case "zap.callTool":
|
|
2284
|
-
try {
|
|
2285
|
-
const result = await zapCallTool(request.name, request.args, request.mcpId);
|
|
2286
|
-
sendResponse({ success: true, result });
|
|
2287
|
-
} catch (e) {
|
|
2288
|
-
sendResponse({ success: false, error: e.message });
|
|
2289
|
-
}
|
|
2290
|
-
break;
|
|
2291
|
-
case "zap.listTools": {
|
|
2292
|
-
const tools = zapState.mcps.flatMap((m) => m.tools.map((t) => ({ name: t, mcpId: m.id, mcpName: m.name })));
|
|
2293
|
-
sendResponse({ success: true, tools });
|
|
2294
|
-
break;
|
|
2295
|
-
}
|
|
2296
|
-
// --- Local AI ---
|
|
2297
|
-
case "runLocalAI":
|
|
2298
|
-
try {
|
|
2299
|
-
const result = await webgpuAI.runInference(request.model || "hanzo-browser-control", request.prompt);
|
|
2300
|
-
sendResponse({ success: true, result });
|
|
2301
|
-
} catch (error) {
|
|
2302
|
-
sendResponse({ success: false, error: error.message });
|
|
2303
|
-
}
|
|
2304
|
-
break;
|
|
2305
|
-
case "checkWebGPU": {
|
|
2306
|
-
try {
|
|
2307
|
-
if (!navigator.gpu) {
|
|
2308
|
-
sendResponse({ available: false });
|
|
2309
|
-
break;
|
|
2310
|
-
}
|
|
2311
|
-
const adapter = await navigator.gpu.requestAdapter();
|
|
2312
|
-
sendResponse({
|
|
2313
|
-
available: !!adapter,
|
|
2314
|
-
adapter: (adapter == null ? void 0 : adapter.name) || "Unknown GPU",
|
|
2315
|
-
models: webgpuAI.getStatus().models
|
|
2316
|
-
});
|
|
2317
|
-
} catch {
|
|
2318
|
-
sendResponse({ available: false });
|
|
2319
|
-
}
|
|
2320
|
-
break;
|
|
2321
|
-
}
|
|
2322
|
-
case "listAgents":
|
|
2323
|
-
sendResponse({ success: true, agents: browserControl.getAgents() });
|
|
2324
|
-
break;
|
|
2325
|
-
case "stopAgent":
|
|
2326
|
-
sendResponse({ success: browserControl.stopAgent(request.agentId) });
|
|
2327
|
-
break;
|
|
2328
|
-
case "launchAIWorker":
|
|
2329
|
-
try {
|
|
2330
|
-
const targetTabId = request.tabId || ((_a = sender.tab) == null ? void 0 : _a.id);
|
|
2331
|
-
if (!targetTabId) {
|
|
2332
|
-
sendResponse({ success: false, error: "No target tab found" });
|
|
2333
|
-
break;
|
|
2334
|
-
}
|
|
2335
|
-
const agentId = await browserControl.launchAIWorker(targetTabId, request.model || "browser-control");
|
|
2336
|
-
sendResponse({ success: true, agentId });
|
|
2337
|
-
} catch (error) {
|
|
2338
|
-
sendResponse({ success: false, error: error.message });
|
|
2339
|
-
}
|
|
2340
|
-
break;
|
|
2341
|
-
// --- Tab Filesystem ---
|
|
2342
|
-
case "readTabFS":
|
|
2343
|
-
try {
|
|
2344
|
-
const content = await browserControl.readTab(request.path);
|
|
2345
|
-
sendResponse({ success: true, content });
|
|
2346
|
-
} catch (error) {
|
|
2347
|
-
sendResponse({ success: false, error: error.message });
|
|
2348
|
-
}
|
|
2349
|
-
break;
|
|
2350
|
-
case "writeTabFS":
|
|
2351
|
-
try {
|
|
2352
|
-
await browserControl.writeTab(request.path, request.content);
|
|
2353
|
-
sendResponse({ success: true });
|
|
2354
|
-
} catch (error) {
|
|
2355
|
-
sendResponse({ success: false, error: error.message });
|
|
2356
|
-
}
|
|
2357
|
-
break;
|
|
2358
|
-
case "listTabFS":
|
|
2359
|
-
const tabs = await browserControl.listTabs();
|
|
2360
|
-
sendResponse({ success: true, tabs });
|
|
2361
|
-
break;
|
|
2362
|
-
// --- CDP bridge commands ---
|
|
2363
|
-
case "cdp.attach":
|
|
2364
|
-
try {
|
|
2365
|
-
const attached = await cdpBridge.attach(request.tabId);
|
|
2366
|
-
sendResponse({ success: attached });
|
|
2367
|
-
} catch (error) {
|
|
2368
|
-
sendResponse({ success: false, error: error.message });
|
|
2369
|
-
}
|
|
2370
|
-
break;
|
|
2371
|
-
case "cdp.detach":
|
|
2372
|
-
try {
|
|
2373
|
-
await cdpBridge.detach(request.tabId);
|
|
2374
|
-
sendResponse({ success: true });
|
|
2375
|
-
} catch (error) {
|
|
2376
|
-
sendResponse({ success: false, error: error.message });
|
|
2377
|
-
}
|
|
2378
|
-
break;
|
|
2379
|
-
case "cdp.send":
|
|
2380
|
-
try {
|
|
2381
|
-
const result = await cdpBridge.send(request.tabId, request.method, request.params);
|
|
2382
|
-
sendResponse({ success: true, result });
|
|
2383
|
-
} catch (error) {
|
|
2384
|
-
sendResponse({ success: false, error: error.message });
|
|
2385
|
-
}
|
|
2386
|
-
break;
|
|
2387
|
-
case "cdp.screenshot":
|
|
2388
|
-
try {
|
|
2389
|
-
const screenshot = await cdpBridge.screenshot(request.tabId, request.options);
|
|
2390
|
-
sendResponse({ success: true, data: screenshot });
|
|
2391
|
-
} catch (error) {
|
|
2392
|
-
sendResponse({ success: false, error: error.message });
|
|
2393
|
-
}
|
|
2394
|
-
break;
|
|
2395
|
-
case "cdp.click":
|
|
2396
|
-
try {
|
|
2397
|
-
const clicked = await cdpBridge.clickSelector(request.tabId, request.selector);
|
|
2398
|
-
sendResponse({ success: clicked });
|
|
2399
|
-
} catch (error) {
|
|
2400
|
-
sendResponse({ success: false, error: error.message });
|
|
2401
|
-
}
|
|
2402
|
-
break;
|
|
2403
|
-
case "cdp.fill":
|
|
2404
|
-
try {
|
|
2405
|
-
const filled = await cdpBridge.fillSelector(request.tabId, request.selector, request.value);
|
|
2406
|
-
sendResponse({ success: filled });
|
|
2407
|
-
} catch (error) {
|
|
2408
|
-
sendResponse({ success: false, error: error.message });
|
|
2409
|
-
}
|
|
2410
|
-
break;
|
|
2411
|
-
case "cdp.navigate":
|
|
2412
|
-
try {
|
|
2413
|
-
await cdpBridge.navigate(request.tabId, request.url);
|
|
2414
|
-
sendResponse({ success: true });
|
|
2415
|
-
} catch (error) {
|
|
2416
|
-
sendResponse({ success: false, error: error.message });
|
|
2417
|
-
}
|
|
2418
|
-
break;
|
|
2419
|
-
case "cdp.evaluate":
|
|
2420
|
-
try {
|
|
2421
|
-
const evalResult = await cdpBridge.evaluate(request.tabId, request.expression);
|
|
2422
|
-
sendResponse({ success: true, result: evalResult });
|
|
2423
|
-
} catch (error) {
|
|
2424
|
-
sendResponse({ success: false, error: error.message });
|
|
2425
|
-
}
|
|
2426
|
-
break;
|
|
2427
|
-
// --- AI Control Overlay ---
|
|
2428
|
-
case "ai.control.start": {
|
|
2429
|
-
const tabId = await resolveControlTabId(request.tabId);
|
|
2430
|
-
if (tabId === null) {
|
|
2431
|
-
sendResponse({ success: false, error: "No target tab found" });
|
|
2432
|
-
break;
|
|
2433
|
-
}
|
|
2434
|
-
await startControlSession(tabId, request.task);
|
|
2435
|
-
sendResponse({
|
|
2436
|
-
success: true,
|
|
2437
|
-
session: {
|
|
2438
|
-
active: controlSession.active,
|
|
2439
|
-
tabId: controlSession.tabId,
|
|
2440
|
-
task: controlSession.task,
|
|
2441
|
-
startedAt: controlSession.startedAt
|
|
2442
|
-
}
|
|
2443
|
-
});
|
|
2444
|
-
break;
|
|
2445
|
-
}
|
|
2446
|
-
case "ai.control.stop": {
|
|
2447
|
-
await stopControlSession();
|
|
2448
|
-
sendResponse({ success: true });
|
|
2449
|
-
break;
|
|
2450
|
-
}
|
|
2451
|
-
case "ai.control.cursor":
|
|
2452
|
-
case "ai.control.highlight":
|
|
2453
|
-
case "ai.control.status": {
|
|
2454
|
-
const tabId = await resolveControlTabId(request.tabId);
|
|
2455
|
-
if (tabId === null) {
|
|
2456
|
-
sendResponse({ success: false, error: "No target tab found" });
|
|
2457
|
-
break;
|
|
2458
|
-
}
|
|
2459
|
-
if (controlSession.active && controlSession.tabId !== null && tabId !== controlSession.tabId) {
|
|
2460
|
-
sendResponse({
|
|
2461
|
-
success: false,
|
|
2462
|
-
error: `Control locked to tab ${controlSession.tabId}`
|
|
2463
|
-
});
|
|
2464
|
-
break;
|
|
2465
|
-
}
|
|
2466
|
-
sendControlMessageToTab(tabId, request);
|
|
2467
|
-
sendResponse({ success: true, tabId });
|
|
2468
|
-
break;
|
|
2469
|
-
}
|
|
2470
|
-
case "ai.control.cancel":
|
|
2471
|
-
await stopControlSession();
|
|
2472
|
-
if (zapState.connected) {
|
|
2473
|
-
for (const [, conn] of zapConnections) {
|
|
2474
|
-
if (conn.readyState === WebSocket.OPEN) {
|
|
2475
|
-
conn.send(encodeZapMessage(MSG_REQUEST, {
|
|
2476
|
-
id: `cancel-${++requestIdCounter}`,
|
|
2477
|
-
method: "notifications/controlCancelled",
|
|
2478
|
-
params: {}
|
|
2479
|
-
}));
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
sendResponse({ success: true });
|
|
2484
|
-
break;
|
|
2485
|
-
// --- In-page overlay (content script) ---
|
|
2486
|
-
case "page.overlay.toggle":
|
|
2487
|
-
case "page.overlay.show":
|
|
2488
|
-
case "page.overlay.hide":
|
|
2489
|
-
case "page.overlay.status": {
|
|
2490
|
-
try {
|
|
2491
|
-
const tabId = typeof request.tabId === "number" ? request.tabId : ((_b = sender.tab) == null ? void 0 : _b.id) ?? await getActiveTabId();
|
|
2492
|
-
if (tabId === null) {
|
|
2493
|
-
sendResponse({ success: false, error: "No target tab found" });
|
|
2494
|
-
break;
|
|
2495
|
-
}
|
|
2496
|
-
const response = await sendMessageToTab(tabId, { action: request.action });
|
|
2497
|
-
sendResponse({ success: true, tabId, ...response || {} });
|
|
2498
|
-
} catch (error) {
|
|
2499
|
-
sendResponse({ success: false, error: error.message });
|
|
2500
|
-
}
|
|
2501
|
-
break;
|
|
2502
|
-
}
|
|
2503
|
-
// --- Auth (Hanzo IAM OAuth2 + PKCE) ---
|
|
2504
|
-
case "auth.login":
|
|
2505
|
-
try {
|
|
2506
|
-
const user = await login();
|
|
2507
|
-
sendResponse({ success: true, user });
|
|
2508
|
-
} catch (error) {
|
|
2509
|
-
sendResponse({ success: false, error: error.message });
|
|
2510
|
-
}
|
|
2511
|
-
break;
|
|
2512
|
-
case "auth.logout":
|
|
2513
|
-
try {
|
|
2514
|
-
await logout();
|
|
2515
|
-
sendResponse({ success: true });
|
|
2516
|
-
} catch (error) {
|
|
2517
|
-
sendResponse({ success: false, error: error.message });
|
|
2518
|
-
}
|
|
2519
|
-
break;
|
|
2520
|
-
case "auth.status":
|
|
2521
|
-
try {
|
|
2522
|
-
const status = await getAuthStatus();
|
|
2523
|
-
sendResponse({ success: true, ...status });
|
|
2524
|
-
} catch (error) {
|
|
2525
|
-
sendResponse({ success: false, error: error.message });
|
|
2526
|
-
}
|
|
2527
|
-
break;
|
|
2528
|
-
case "auth.getToken":
|
|
2529
|
-
try {
|
|
2530
|
-
const token = await getValidAccessToken();
|
|
2531
|
-
sendResponse({ success: !!token, token });
|
|
2532
|
-
} catch (error) {
|
|
2533
|
-
sendResponse({ success: false, error: error.message });
|
|
2534
|
-
}
|
|
2535
|
-
break;
|
|
2536
|
-
case "rag.query":
|
|
2537
|
-
try {
|
|
2538
|
-
const config = await getRagConfig();
|
|
2539
|
-
const topKInput = Number.parseInt(String(request.topK ?? config.topK), 10);
|
|
2540
|
-
const params = {
|
|
2541
|
-
query: String(request.query || "").trim(),
|
|
2542
|
-
topK: Number.isFinite(topKInput) ? Math.max(1, Math.min(topKInput, 20)) : DEFAULT_RAG_TOP_K,
|
|
2543
|
-
knowledgeBase: String(request.knowledgeBase ?? config.knowledgeBase ?? "").trim(),
|
|
2544
|
-
includeTabContext: request.includeTabContext !== void 0 ? !!request.includeTabContext : config.includeTabContext,
|
|
2545
|
-
useZapMemory: request.useZapMemory !== void 0 ? !!request.useZapMemory : config.useZapMemory,
|
|
2546
|
-
endpoint: String(request.endpoint ?? config.endpoint ?? "").trim(),
|
|
2547
|
-
apiKey: String(request.apiKey ?? config.apiKey ?? "").trim(),
|
|
2548
|
-
mcpId: request.mcpId ? String(request.mcpId) : void 0,
|
|
2549
|
-
pageContext: request.pageContext && typeof request.pageContext === "object" ? {
|
|
2550
|
-
url: request.pageContext.url ? String(request.pageContext.url) : void 0,
|
|
2551
|
-
title: request.pageContext.title ? String(request.pageContext.title) : void 0
|
|
2552
|
-
} : void 0
|
|
2553
|
-
};
|
|
2554
|
-
if (!params.query) {
|
|
2555
|
-
sendResponse({ success: false, error: "Query is required" });
|
|
2556
|
-
break;
|
|
2557
|
-
}
|
|
2558
|
-
let snippets = await queryRagFromZap(params);
|
|
2559
|
-
let source = snippets.length ? "zap-memory" : "none";
|
|
2560
|
-
if (!snippets.length && params.endpoint) {
|
|
2561
|
-
snippets = await queryRagFromEndpoint(params);
|
|
2562
|
-
source = snippets.length ? "rag-endpoint" : source;
|
|
2563
|
-
}
|
|
2564
|
-
sendResponse({
|
|
2565
|
-
success: true,
|
|
2566
|
-
source,
|
|
2567
|
-
snippets: snippets.slice(0, params.topK)
|
|
2568
|
-
});
|
|
2569
|
-
} catch (error) {
|
|
2570
|
-
sendResponse({ success: false, error: error.message });
|
|
2571
|
-
}
|
|
2572
|
-
break;
|
|
2573
|
-
// --- Cloud Chat ---
|
|
2574
|
-
case "chat.listModels":
|
|
2575
|
-
try {
|
|
2576
|
-
const token = await getValidAccessToken();
|
|
2577
|
-
if (!token) {
|
|
2578
|
-
sendResponse({ success: false, error: "Not authenticated" });
|
|
2579
|
-
break;
|
|
2580
|
-
}
|
|
2581
|
-
const models = await listModels(token);
|
|
2582
|
-
sendResponse({ success: true, models });
|
|
2583
|
-
} catch (error) {
|
|
2584
|
-
sendResponse({ success: false, error: error.message });
|
|
2585
|
-
}
|
|
2586
|
-
break;
|
|
2587
|
-
case "chat.complete":
|
|
2588
|
-
try {
|
|
2589
|
-
const token = await getValidAccessToken();
|
|
2590
|
-
if (!token) {
|
|
2591
|
-
sendResponse({ success: false, error: "Not authenticated" });
|
|
2592
|
-
break;
|
|
2593
|
-
}
|
|
2594
|
-
const model = String(request.model || "gpt-4o");
|
|
2595
|
-
const messagesInput = Array.isArray(request.messages) ? request.messages : [];
|
|
2596
|
-
const messages = messagesInput.filter((msg) => msg && typeof msg.content === "string" && typeof msg.role === "string").map((msg) => ({
|
|
2597
|
-
role: msg.role === "system" || msg.role === "assistant" ? msg.role : "user",
|
|
2598
|
-
content: String(msg.content)
|
|
2599
|
-
})).slice(-24);
|
|
2600
|
-
if (!messages.length) {
|
|
2601
|
-
sendResponse({ success: false, error: "messages are required" });
|
|
2602
|
-
break;
|
|
2603
|
-
}
|
|
2604
|
-
const content = await chatCompletion(token, {
|
|
2605
|
-
model,
|
|
2606
|
-
messages,
|
|
2607
|
-
temperature: typeof request.temperature === "number" ? request.temperature : void 0,
|
|
2608
|
-
max_tokens: typeof request.max_tokens === "number" ? request.max_tokens : void 0
|
|
2609
|
-
});
|
|
2610
|
-
sendResponse({ success: true, content });
|
|
2611
|
-
} catch (error) {
|
|
2612
|
-
sendResponse({ success: false, error: error.message });
|
|
2613
|
-
}
|
|
2614
|
-
break;
|
|
2615
|
-
// --- Bridge status ---
|
|
2616
|
-
case "bridge.status": {
|
|
2617
|
-
const bridgeConnected = cdpBridge.isBridgeConnected();
|
|
2618
|
-
sendResponse({
|
|
2619
|
-
success: true,
|
|
2620
|
-
connected: bridgeConnected,
|
|
2621
|
-
browsers: bridgeConnected ? [detectBrowser()] : []
|
|
2622
|
-
});
|
|
2623
|
-
break;
|
|
2624
|
-
}
|
|
2625
|
-
// --- Config persistence ---
|
|
2626
|
-
case "config.save": {
|
|
2627
|
-
const { key, value } = request;
|
|
2628
|
-
cdpBridge.sendConfig(key, value);
|
|
2629
|
-
sendResponse({ success: true });
|
|
2630
|
-
break;
|
|
2631
|
-
}
|
|
2632
|
-
// --- Takeover messages (forwarded from CDP bridge to content script) ---
|
|
2633
|
-
case "hanzo.takeover.start": {
|
|
2634
|
-
const takeoverTabId = await resolveControlTabId(request.tabId);
|
|
2635
|
-
if (takeoverTabId === null) {
|
|
2636
|
-
sendResponse({ success: false, error: "No target tab found" });
|
|
2637
|
-
break;
|
|
2638
|
-
}
|
|
2639
|
-
await startControlSession(takeoverTabId, request.task);
|
|
2640
|
-
sendResponse({ success: true });
|
|
2641
|
-
break;
|
|
2642
|
-
}
|
|
2643
|
-
case "hanzo.takeover.end": {
|
|
2644
|
-
await stopControlSession();
|
|
2645
|
-
sendResponse({ success: true });
|
|
2646
|
-
break;
|
|
2647
|
-
}
|
|
2648
|
-
case "hanzo.takeover.cursor": {
|
|
2649
|
-
const cursorTabId = await resolveControlTabId(request.tabId);
|
|
2650
|
-
if (cursorTabId !== null) {
|
|
2651
|
-
sendControlMessageToTab(cursorTabId, {
|
|
2652
|
-
action: "ai.control.cursor",
|
|
2653
|
-
x: request.x,
|
|
2654
|
-
y: request.y
|
|
2655
|
-
});
|
|
2656
|
-
}
|
|
2657
|
-
sendResponse({ success: true });
|
|
2658
|
-
break;
|
|
2659
|
-
}
|
|
2660
|
-
// --- Content script element selection (routed from content script) ---
|
|
2661
|
-
case "elementSelected":
|
|
2662
|
-
if (zapState.connected) {
|
|
2663
|
-
for (const [, conn] of zapConnections) {
|
|
2664
|
-
if (conn.readyState === WebSocket.OPEN) {
|
|
2665
|
-
conn.send(encodeZapMessage(MSG_REQUEST, {
|
|
2666
|
-
id: `evt-${++requestIdCounter}`,
|
|
2667
|
-
method: "notifications/elementSelected",
|
|
2668
|
-
params: request.data
|
|
2669
|
-
}));
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
2674
|
-
ws.send(JSON.stringify(request.data));
|
|
2675
|
-
}
|
|
2676
|
-
sendResponse({ success: true });
|
|
2677
|
-
break;
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
var ws = null;
|
|
2681
|
-
async function connectToMCP() {
|
|
2682
|
-
const { mcpPort } = await getPortConfig();
|
|
2683
|
-
ws = new WebSocket(`ws://localhost:${mcpPort}/browser-extension`);
|
|
2684
|
-
ws.onopen = () => {
|
|
2685
|
-
console.log("[Hanzo] Connected to legacy MCP server");
|
|
2686
|
-
};
|
|
2687
|
-
ws.onmessage = (event) => {
|
|
2688
|
-
const data = JSON.parse(event.data);
|
|
2689
|
-
handleMCPMessage(data);
|
|
2690
|
-
};
|
|
2691
|
-
ws.onerror = () => {
|
|
2692
|
-
};
|
|
2693
|
-
ws.onclose = () => {
|
|
2694
|
-
setTimeout(connectToMCP, 5e3);
|
|
2695
|
-
};
|
|
2696
|
-
}
|
|
2697
|
-
function handleMCPMessage(data) {
|
|
2698
|
-
switch (data.type) {
|
|
2699
|
-
case "browserControl":
|
|
2700
|
-
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
|
2701
|
-
var _a;
|
|
2702
|
-
if ((_a = tabs[0]) == null ? void 0 : _a.id) {
|
|
2703
|
-
browserControl.launchAIWorker(tabs[0].id, data.model);
|
|
2704
|
-
}
|
|
2705
|
-
});
|
|
2706
|
-
break;
|
|
2707
|
-
}
|
|
2708
|
-
}
|
|
2709
|
-
if (chrome.sidePanel) {
|
|
2710
|
-
chrome.sidePanel.setOptions({ path: "sidebar.html", enabled: true });
|
|
2711
|
-
}
|
|
2712
|
-
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
2713
|
-
if (controlSession.active && controlSession.tabId === tabId) {
|
|
2714
|
-
void stopControlSession();
|
|
2715
|
-
}
|
|
2716
|
-
});
|
|
2717
|
-
discoverZapServers();
|
|
2718
|
-
connectToMCP();
|
|
2719
|
-
getPortConfig().then(({ cdpPort }) => {
|
|
2720
|
-
try {
|
|
2721
|
-
cdpBridge.startWebSocketServer(cdpPort);
|
|
2722
|
-
console.log(`[Hanzo] CDP bridge connecting to ws://localhost:${cdpPort}/cdp`);
|
|
2723
|
-
} catch (e) {
|
|
2724
|
-
console.error("[Hanzo] Failed to start CDP bridge:", e);
|
|
2725
|
-
}
|
|
2726
|
-
});
|
|
2727
|
-
export {
|
|
2728
|
-
browserControl,
|
|
2729
|
-
webgpuAI,
|
|
2730
|
-
zapCallTool,
|
|
2731
|
-
zapState
|
|
2732
|
-
};
|