@quanta-intellect/vessel-browser 0.1.44 → 0.1.46
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/out/main/index.js
CHANGED
|
@@ -32,12 +32,14 @@ const defaults = {
|
|
|
32
32
|
premium: {
|
|
33
33
|
status: "free",
|
|
34
34
|
customerId: "",
|
|
35
|
+
verificationToken: "",
|
|
35
36
|
email: "",
|
|
36
37
|
validatedAt: "",
|
|
37
38
|
expiresAt: ""
|
|
38
39
|
}
|
|
39
40
|
};
|
|
40
41
|
const SAVE_DEBOUNCE_MS$3 = 150;
|
|
42
|
+
const CHAT_PROVIDER_SECRET_FILENAME = "vessel-chat-provider-secret";
|
|
41
43
|
const SETTABLE_KEYS = new Set(Object.keys(defaults));
|
|
42
44
|
let settings = null;
|
|
43
45
|
let settingsIssues = [];
|
|
@@ -52,6 +54,80 @@ function getUserDataPath() {
|
|
|
52
54
|
function getSettingsPath() {
|
|
53
55
|
return path.join(getUserDataPath(), "vessel-settings.json");
|
|
54
56
|
}
|
|
57
|
+
function getChatProviderSecretPath() {
|
|
58
|
+
return path.join(getUserDataPath(), CHAT_PROVIDER_SECRET_FILENAME);
|
|
59
|
+
}
|
|
60
|
+
function canUseSafeStorage() {
|
|
61
|
+
try {
|
|
62
|
+
return electron.safeStorage.isEncryptionAvailable();
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function readStoredProviderSecret() {
|
|
68
|
+
try {
|
|
69
|
+
const raw = fs.readFileSync(getChatProviderSecretPath());
|
|
70
|
+
const decoded = canUseSafeStorage() && electron.safeStorage.decryptString ? electron.safeStorage.decryptString(raw) : raw.toString("utf-8");
|
|
71
|
+
const parsed = JSON.parse(decoded);
|
|
72
|
+
if (parsed && typeof parsed === "object" && typeof parsed.providerId === "string" && typeof parsed.apiKey === "string") {
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function writeStoredProviderSecret(secret) {
|
|
80
|
+
const filePath = getChatProviderSecretPath();
|
|
81
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
82
|
+
const payload = JSON.stringify(secret);
|
|
83
|
+
if (canUseSafeStorage()) {
|
|
84
|
+
const encrypted = electron.safeStorage.encryptString(payload);
|
|
85
|
+
fs.writeFileSync(filePath, encrypted, { mode: 384 });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
fs.writeFileSync(filePath, payload, { mode: 384 });
|
|
89
|
+
}
|
|
90
|
+
function clearStoredProviderSecret() {
|
|
91
|
+
try {
|
|
92
|
+
fs.unlinkSync(getChatProviderSecretPath());
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function mergeChatProviderSecret(provider) {
|
|
97
|
+
if (!provider) return null;
|
|
98
|
+
const stored = readStoredProviderSecret();
|
|
99
|
+
const legacyApiKey = provider.apiKey?.trim() || "";
|
|
100
|
+
const apiKey = stored?.providerId === provider.id ? stored.apiKey : legacyApiKey;
|
|
101
|
+
if (legacyApiKey && stored?.providerId !== provider.id) {
|
|
102
|
+
writeStoredProviderSecret({ providerId: provider.id, apiKey: legacyApiKey });
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
...provider,
|
|
106
|
+
apiKey,
|
|
107
|
+
hasApiKey: Boolean(apiKey)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function buildPersistedSettings(source) {
|
|
111
|
+
return {
|
|
112
|
+
...source,
|
|
113
|
+
chatProvider: source.chatProvider ? {
|
|
114
|
+
...source.chatProvider,
|
|
115
|
+
apiKey: "",
|
|
116
|
+
hasApiKey: source.chatProvider.hasApiKey || Boolean(source.chatProvider.apiKey)
|
|
117
|
+
} : null
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function getRendererSettings() {
|
|
121
|
+
const current = loadSettings();
|
|
122
|
+
return {
|
|
123
|
+
...current,
|
|
124
|
+
chatProvider: current.chatProvider ? {
|
|
125
|
+
...current.chatProvider,
|
|
126
|
+
apiKey: "",
|
|
127
|
+
hasApiKey: Boolean(current.chatProvider.apiKey)
|
|
128
|
+
} : null
|
|
129
|
+
};
|
|
130
|
+
}
|
|
55
131
|
function getSettingsLoadIssues() {
|
|
56
132
|
return settingsIssues.map((issue) => ({ ...issue }));
|
|
57
133
|
}
|
|
@@ -80,6 +156,7 @@ function loadSettings() {
|
|
|
80
156
|
settings = {
|
|
81
157
|
...defaults,
|
|
82
158
|
...parsed,
|
|
159
|
+
chatProvider: mergeChatProviderSecret(parsed.chatProvider ?? null),
|
|
83
160
|
mcpPort: sanitizePort(parsed.mcpPort ?? defaults.mcpPort),
|
|
84
161
|
agentTranscriptMode: parsed.agentTranscriptMode === "off" || parsed.agentTranscriptMode === "summary" || parsed.agentTranscriptMode === "full" ? parsed.agentTranscriptMode : parsed.showAgentTranscript === false ? "off" : defaults.agentTranscriptMode
|
|
85
162
|
};
|
|
@@ -104,7 +181,10 @@ function persistNow$3() {
|
|
|
104
181
|
saveTimer$3 = null;
|
|
105
182
|
}
|
|
106
183
|
return fs.promises.mkdir(path.dirname(getSettingsPath()), { recursive: true }).then(
|
|
107
|
-
() => fs.promises.writeFile(
|
|
184
|
+
() => fs.promises.writeFile(
|
|
185
|
+
getSettingsPath(),
|
|
186
|
+
JSON.stringify(buildPersistedSettings(settings), null, 2)
|
|
187
|
+
)
|
|
108
188
|
).catch((err) => console.error("[Vessel] Failed to save settings:", err));
|
|
109
189
|
}
|
|
110
190
|
function saveSettings() {
|
|
@@ -121,6 +201,30 @@ function setSetting(key, value) {
|
|
|
121
201
|
loadSettings();
|
|
122
202
|
if (key === "mcpPort") {
|
|
123
203
|
settings.mcpPort = sanitizePort(value);
|
|
204
|
+
} else if (key === "chatProvider") {
|
|
205
|
+
const nextProvider = value;
|
|
206
|
+
if (!nextProvider) {
|
|
207
|
+
clearStoredProviderSecret();
|
|
208
|
+
settings.chatProvider = null;
|
|
209
|
+
} else {
|
|
210
|
+
const existingSecret = readStoredProviderSecret();
|
|
211
|
+
const incomingApiKey = nextProvider.apiKey.trim();
|
|
212
|
+
const preserveExisting = !incomingApiKey && nextProvider.hasApiKey === true && existingSecret?.providerId === nextProvider.id;
|
|
213
|
+
const resolvedApiKey = preserveExisting ? existingSecret?.apiKey || "" : incomingApiKey;
|
|
214
|
+
if (resolvedApiKey) {
|
|
215
|
+
writeStoredProviderSecret({
|
|
216
|
+
providerId: nextProvider.id,
|
|
217
|
+
apiKey: resolvedApiKey
|
|
218
|
+
});
|
|
219
|
+
} else {
|
|
220
|
+
clearStoredProviderSecret();
|
|
221
|
+
}
|
|
222
|
+
settings.chatProvider = {
|
|
223
|
+
...nextProvider,
|
|
224
|
+
apiKey: resolvedApiKey,
|
|
225
|
+
hasApiKey: Boolean(resolvedApiKey)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
124
228
|
} else {
|
|
125
229
|
settings[key] = value;
|
|
126
230
|
}
|
|
@@ -160,7 +264,31 @@ function checkDomainPolicy(url) {
|
|
|
160
264
|
function matchesDomain(hostname, policyDomain) {
|
|
161
265
|
return hostname === policyDomain || hostname.endsWith("." + policyDomain);
|
|
162
266
|
}
|
|
267
|
+
const ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
268
|
+
function isSafeNavigationURL(url) {
|
|
269
|
+
try {
|
|
270
|
+
const parsed = new URL(url);
|
|
271
|
+
return ALLOWED_SCHEMES.has(parsed.protocol);
|
|
272
|
+
} catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function assertSafeURL(url) {
|
|
277
|
+
if (!isSafeNavigationURL(url)) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function assertPermittedNavigationURL(url) {
|
|
284
|
+
assertSafeURL(url);
|
|
285
|
+
const policyError = checkDomainPolicy(url);
|
|
286
|
+
if (policyError) {
|
|
287
|
+
throw new Error(policyError);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
163
290
|
const MAX_CUSTOM_HISTORY = 50;
|
|
291
|
+
const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
|
|
164
292
|
class Tab {
|
|
165
293
|
id;
|
|
166
294
|
view;
|
|
@@ -181,6 +309,32 @@ class Tab {
|
|
|
181
309
|
urlForwardStack = [];
|
|
182
310
|
lastCommittedUrl = "";
|
|
183
311
|
navigatingViaHistory = false;
|
|
312
|
+
isReaderModeDataUrl(url) {
|
|
313
|
+
return this._state.isReaderMode && url.startsWith(READER_MODE_DATA_URL_PREFIX);
|
|
314
|
+
}
|
|
315
|
+
getNavigationBlockReason(url) {
|
|
316
|
+
if (!url) {
|
|
317
|
+
return "Blocked navigation to empty URL";
|
|
318
|
+
}
|
|
319
|
+
if (url.startsWith("about:") || this.isReaderModeDataUrl(url)) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
assertSafeURL(url);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return error instanceof Error ? error.message : "Blocked unsafe navigation";
|
|
326
|
+
}
|
|
327
|
+
return checkDomainPolicy(url);
|
|
328
|
+
}
|
|
329
|
+
guardedLoadURL(url, options) {
|
|
330
|
+
const blockReason = this.getNavigationBlockReason(url);
|
|
331
|
+
if (blockReason) {
|
|
332
|
+
console.warn(`[Tab] ${blockReason}`);
|
|
333
|
+
return blockReason;
|
|
334
|
+
}
|
|
335
|
+
void this.view.webContents.loadURL(url, options);
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
184
338
|
constructor(id, url, onChange, options) {
|
|
185
339
|
this.id = id;
|
|
186
340
|
this.parentWindow = options?.parentWindow;
|
|
@@ -198,10 +352,11 @@ class Tab {
|
|
|
198
352
|
nodeIntegration: false
|
|
199
353
|
}
|
|
200
354
|
});
|
|
355
|
+
const initialUrl = url || "about:blank";
|
|
201
356
|
this._state = {
|
|
202
357
|
id,
|
|
203
358
|
title: "New Tab",
|
|
204
|
-
url:
|
|
359
|
+
url: initialUrl,
|
|
205
360
|
favicon: "",
|
|
206
361
|
isLoading: false,
|
|
207
362
|
canGoBack: false,
|
|
@@ -222,13 +377,22 @@ class Tab {
|
|
|
222
377
|
});
|
|
223
378
|
this.setupListeners();
|
|
224
379
|
if (url) {
|
|
225
|
-
|
|
226
|
-
|
|
380
|
+
const error = this.guardedLoadURL(url);
|
|
381
|
+
if (error) {
|
|
382
|
+
this._state.url = "about:blank";
|
|
383
|
+
} else {
|
|
384
|
+
this.lastCommittedUrl = url;
|
|
385
|
+
}
|
|
227
386
|
}
|
|
228
387
|
}
|
|
229
388
|
setupListeners() {
|
|
230
389
|
const wc = this.view.webContents;
|
|
231
390
|
wc.setWindowOpenHandler(({ url, disposition }) => {
|
|
391
|
+
const error = this.getNavigationBlockReason(url);
|
|
392
|
+
if (error) {
|
|
393
|
+
console.warn(`[Tab] ${error}`);
|
|
394
|
+
return { action: "deny" };
|
|
395
|
+
}
|
|
232
396
|
this.onOpenUrl?.({
|
|
233
397
|
url,
|
|
234
398
|
background: disposition === "background-tab",
|
|
@@ -236,6 +400,18 @@ class Tab {
|
|
|
236
400
|
});
|
|
237
401
|
return { action: "deny" };
|
|
238
402
|
});
|
|
403
|
+
const blockNavigation = (event, url, context) => {
|
|
404
|
+
const error = this.getNavigationBlockReason(url);
|
|
405
|
+
if (!error) return;
|
|
406
|
+
event.preventDefault();
|
|
407
|
+
console.warn(`[Tab] ${context}: ${error}`);
|
|
408
|
+
};
|
|
409
|
+
wc.on("will-navigate", (event, url) => {
|
|
410
|
+
blockNavigation(event, url, "Blocked top-level navigation");
|
|
411
|
+
});
|
|
412
|
+
wc.on("will-redirect", (event, url) => {
|
|
413
|
+
blockNavigation(event, url, "Blocked redirect");
|
|
414
|
+
});
|
|
239
415
|
const syncNavigationState = () => {
|
|
240
416
|
this._state.title = wc.getTitle() || this._state.title || "New Tab";
|
|
241
417
|
this._state.url = wc.getURL() || this._state.url;
|
|
@@ -402,17 +578,12 @@ class Tab {
|
|
|
402
578
|
url = `https://duckduckgo.com/?q=${encodeURIComponent(url)}`;
|
|
403
579
|
}
|
|
404
580
|
}
|
|
405
|
-
if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
|
|
406
|
-
return `Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`;
|
|
407
|
-
}
|
|
408
|
-
const policyError = checkDomainPolicy(url);
|
|
409
|
-
if (policyError) return policyError;
|
|
410
581
|
if (postBody) {
|
|
411
582
|
const params = new URLSearchParams();
|
|
412
583
|
for (const [key, value] of Object.entries(postBody)) {
|
|
413
584
|
params.set(key, value);
|
|
414
585
|
}
|
|
415
|
-
this.
|
|
586
|
+
return this.guardedLoadURL(url, {
|
|
416
587
|
method: "POST",
|
|
417
588
|
extraHeaders: "Content-Type: application/x-www-form-urlencoded\r\n",
|
|
418
589
|
postData: [
|
|
@@ -422,27 +593,39 @@ class Tab {
|
|
|
422
593
|
}
|
|
423
594
|
]
|
|
424
595
|
});
|
|
425
|
-
} else {
|
|
426
|
-
this.view.webContents.loadURL(url);
|
|
427
596
|
}
|
|
428
|
-
return
|
|
597
|
+
return this.guardedLoadURL(url);
|
|
429
598
|
}
|
|
430
599
|
goBack() {
|
|
431
600
|
const previousUrl = this.urlHistory.pop();
|
|
432
601
|
if (!previousUrl) return false;
|
|
602
|
+
const currentUrl = this.lastCommittedUrl;
|
|
433
603
|
this.navigatingViaHistory = true;
|
|
434
|
-
this.urlForwardStack.push(
|
|
604
|
+
this.urlForwardStack.push(currentUrl);
|
|
605
|
+
const error = this.guardedLoadURL(previousUrl);
|
|
606
|
+
if (error) {
|
|
607
|
+
this.navigatingViaHistory = false;
|
|
608
|
+
this.urlForwardStack.pop();
|
|
609
|
+
this.urlHistory.push(previousUrl);
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
435
612
|
this.lastCommittedUrl = previousUrl;
|
|
436
|
-
this.view.webContents.loadURL(previousUrl);
|
|
437
613
|
return true;
|
|
438
614
|
}
|
|
439
615
|
goForward() {
|
|
440
616
|
const nextUrl = this.urlForwardStack.pop();
|
|
441
617
|
if (!nextUrl) return false;
|
|
618
|
+
const currentUrl = this.lastCommittedUrl;
|
|
442
619
|
this.navigatingViaHistory = true;
|
|
443
|
-
this.urlHistory.push(
|
|
620
|
+
this.urlHistory.push(currentUrl);
|
|
621
|
+
const error = this.guardedLoadURL(nextUrl);
|
|
622
|
+
if (error) {
|
|
623
|
+
this.navigatingViaHistory = false;
|
|
624
|
+
this.urlHistory.pop();
|
|
625
|
+
this.urlForwardStack.push(nextUrl);
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
444
628
|
this.lastCommittedUrl = nextUrl;
|
|
445
|
-
this.view.webContents.loadURL(nextUrl);
|
|
446
629
|
return true;
|
|
447
630
|
}
|
|
448
631
|
canGoBack() {
|
|
@@ -2446,7 +2629,8 @@ const Channels = {
|
|
|
2446
2629
|
DOWNLOAD_DONE: "download:done",
|
|
2447
2630
|
// Premium
|
|
2448
2631
|
PREMIUM_GET_STATE: "premium:get-state",
|
|
2449
|
-
|
|
2632
|
+
PREMIUM_ACTIVATION_START: "premium:activation-start",
|
|
2633
|
+
PREMIUM_ACTIVATION_VERIFY: "premium:activation-verify",
|
|
2450
2634
|
PREMIUM_CHECKOUT: "premium:checkout",
|
|
2451
2635
|
PREMIUM_PORTAL: "premium:portal",
|
|
2452
2636
|
PREMIUM_RESET: "premium:reset",
|
|
@@ -3382,6 +3566,7 @@ function resetPremium() {
|
|
|
3382
3566
|
const fresh = {
|
|
3383
3567
|
status: "free",
|
|
3384
3568
|
customerId: "",
|
|
3569
|
+
verificationToken: "",
|
|
3385
3570
|
email: "",
|
|
3386
3571
|
validatedAt: "",
|
|
3387
3572
|
expiresAt: ""
|
|
@@ -3414,39 +3599,22 @@ async function getCheckoutUrl(email) {
|
|
|
3414
3599
|
}
|
|
3415
3600
|
}
|
|
3416
3601
|
async function getPortalUrl() {
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
}
|
|
3421
|
-
try {
|
|
3422
|
-
const res = await fetch(`${VERIFICATION_API}/portal`, {
|
|
3423
|
-
method: "POST",
|
|
3424
|
-
headers: { "Content-Type": "application/json" },
|
|
3425
|
-
body: JSON.stringify({ customerId: premium.customerId })
|
|
3426
|
-
});
|
|
3427
|
-
if (!res.ok) {
|
|
3428
|
-
return { ok: false, error: `HTTP ${res.status}` };
|
|
3429
|
-
}
|
|
3430
|
-
const { url } = await res.json();
|
|
3431
|
-
return { ok: true, url };
|
|
3432
|
-
} catch (err) {
|
|
3433
|
-
return {
|
|
3434
|
-
ok: false,
|
|
3435
|
-
error: err instanceof Error ? err.message : "Failed to get portal URL"
|
|
3436
|
-
};
|
|
3437
|
-
}
|
|
3602
|
+
return {
|
|
3603
|
+
ok: false,
|
|
3604
|
+
error: "Billing portal access is temporarily disabled while we harden the premium API. Use the Stripe billing link from your subscription email for now."
|
|
3605
|
+
};
|
|
3438
3606
|
}
|
|
3439
|
-
async function verifySubscription(
|
|
3607
|
+
async function verifySubscription(identifier) {
|
|
3440
3608
|
const current = loadSettings().premium;
|
|
3441
|
-
const
|
|
3442
|
-
if (!
|
|
3609
|
+
const verificationIdentifier = identifier || current.verificationToken || current.customerId;
|
|
3610
|
+
if (!verificationIdentifier) {
|
|
3443
3611
|
return current;
|
|
3444
3612
|
}
|
|
3445
3613
|
try {
|
|
3446
3614
|
const res = await fetch(`${VERIFICATION_API}/verify`, {
|
|
3447
3615
|
method: "POST",
|
|
3448
3616
|
headers: { "Content-Type": "application/json" },
|
|
3449
|
-
body: JSON.stringify({ identifier })
|
|
3617
|
+
body: JSON.stringify({ identifier: verificationIdentifier })
|
|
3450
3618
|
});
|
|
3451
3619
|
if (!res.ok) {
|
|
3452
3620
|
console.warn("[Vessel Premium] Verification API returned", res.status);
|
|
@@ -3455,8 +3623,9 @@ async function verifySubscription(emailOrCustomerId) {
|
|
|
3455
3623
|
const data = await res.json();
|
|
3456
3624
|
const updated = {
|
|
3457
3625
|
status: data.status,
|
|
3458
|
-
customerId: data.customerId,
|
|
3459
|
-
|
|
3626
|
+
customerId: data.customerId || current.customerId,
|
|
3627
|
+
verificationToken: data.verificationToken || verificationIdentifier,
|
|
3628
|
+
email: data.email || current.email,
|
|
3460
3629
|
validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3461
3630
|
expiresAt: data.expiresAt
|
|
3462
3631
|
};
|
|
@@ -3467,34 +3636,104 @@ async function verifySubscription(emailOrCustomerId) {
|
|
|
3467
3636
|
return current;
|
|
3468
3637
|
}
|
|
3469
3638
|
}
|
|
3470
|
-
async function
|
|
3471
|
-
|
|
3639
|
+
async function requestActivationCode(email) {
|
|
3640
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
3641
|
+
if (!normalizedEmail) {
|
|
3642
|
+
return { ok: false, error: "Email is required" };
|
|
3643
|
+
}
|
|
3644
|
+
try {
|
|
3645
|
+
const res = await fetch(`${VERIFICATION_API}/activate/start`, {
|
|
3646
|
+
method: "POST",
|
|
3647
|
+
headers: { "Content-Type": "application/json" },
|
|
3648
|
+
body: JSON.stringify({ email: normalizedEmail })
|
|
3649
|
+
});
|
|
3650
|
+
const data = await res.json().catch(() => ({}));
|
|
3651
|
+
if (!res.ok || !data.challengeToken) {
|
|
3652
|
+
return {
|
|
3653
|
+
ok: false,
|
|
3654
|
+
error: data.error || `HTTP ${res.status}`
|
|
3655
|
+
};
|
|
3656
|
+
}
|
|
3657
|
+
return {
|
|
3658
|
+
ok: true,
|
|
3659
|
+
email: normalizedEmail,
|
|
3660
|
+
challengeToken: data.challengeToken
|
|
3661
|
+
};
|
|
3662
|
+
} catch (err) {
|
|
3663
|
+
return {
|
|
3664
|
+
ok: false,
|
|
3665
|
+
error: err instanceof Error ? err.message : "Failed to send code"
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
async function verifyActivationCode(email, code, challengeToken) {
|
|
3670
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
3671
|
+
const trimmedCode = code.trim();
|
|
3672
|
+
if (!normalizedEmail) {
|
|
3472
3673
|
return { ok: false, state: getPremiumState(), error: "Email is required" };
|
|
3473
3674
|
}
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3675
|
+
if (!trimmedCode) {
|
|
3676
|
+
return { ok: false, state: getPremiumState(), error: "Code is required" };
|
|
3677
|
+
}
|
|
3678
|
+
if (!challengeToken.trim()) {
|
|
3679
|
+
return {
|
|
3680
|
+
ok: false,
|
|
3681
|
+
state: getPremiumState(),
|
|
3682
|
+
error: "Request a new activation code and try again."
|
|
3683
|
+
};
|
|
3684
|
+
}
|
|
3685
|
+
try {
|
|
3686
|
+
const res = await fetch(`${VERIFICATION_API}/activate/verify`, {
|
|
3687
|
+
method: "POST",
|
|
3688
|
+
headers: { "Content-Type": "application/json" },
|
|
3689
|
+
body: JSON.stringify({
|
|
3690
|
+
email: normalizedEmail,
|
|
3691
|
+
code: trimmedCode,
|
|
3692
|
+
challengeToken: challengeToken.trim()
|
|
3693
|
+
})
|
|
3694
|
+
});
|
|
3695
|
+
const data = await res.json().catch(() => ({}));
|
|
3696
|
+
if (!res.ok) {
|
|
3697
|
+
return {
|
|
3698
|
+
ok: false,
|
|
3699
|
+
state: getPremiumState(),
|
|
3700
|
+
error: data.error || `HTTP ${res.status}`
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
const updated = {
|
|
3704
|
+
status: data.status ?? "free",
|
|
3705
|
+
customerId: data.customerId || "",
|
|
3706
|
+
verificationToken: data.verificationToken || "",
|
|
3707
|
+
email: data.email || normalizedEmail,
|
|
3708
|
+
validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3709
|
+
expiresAt: data.expiresAt || ""
|
|
3710
|
+
};
|
|
3711
|
+
setSetting("premium", updated);
|
|
3712
|
+
return { ok: isPremiumActiveState(updated), state: updated };
|
|
3713
|
+
} catch (err) {
|
|
3714
|
+
return {
|
|
3715
|
+
ok: false,
|
|
3716
|
+
state: getPremiumState(),
|
|
3717
|
+
error: err instanceof Error ? err.message : "Failed to verify code"
|
|
3718
|
+
};
|
|
3477
3719
|
}
|
|
3478
|
-
return {
|
|
3479
|
-
ok: false,
|
|
3480
|
-
state: state2,
|
|
3481
|
-
error: state2.status === "canceled" ? "Subscription is canceled. Resubscribe to continue." : state2.status === "past_due" ? "Subscription payment is past due. Update your payment method." : "No active subscription found for this email."
|
|
3482
|
-
};
|
|
3483
3720
|
}
|
|
3484
3721
|
let revalidationTimer = null;
|
|
3485
3722
|
function startBackgroundRevalidation() {
|
|
3486
3723
|
if (revalidationTimer) return;
|
|
3487
3724
|
const { premium } = loadSettings();
|
|
3488
|
-
|
|
3725
|
+
const identifier = premium.verificationToken || premium.customerId;
|
|
3726
|
+
if (identifier) {
|
|
3489
3727
|
const lastValidated = premium.validatedAt ? new Date(premium.validatedAt).getTime() : 0;
|
|
3490
3728
|
if (Date.now() - lastValidated > REVALIDATION_INTERVAL_MS) {
|
|
3491
|
-
void verifySubscription();
|
|
3729
|
+
void verifySubscription(identifier);
|
|
3492
3730
|
}
|
|
3493
3731
|
}
|
|
3494
3732
|
revalidationTimer = setInterval(() => {
|
|
3495
3733
|
const { premium: p } = loadSettings();
|
|
3496
|
-
|
|
3497
|
-
|
|
3734
|
+
const currentIdentifier = p.verificationToken || p.customerId;
|
|
3735
|
+
if (currentIdentifier) {
|
|
3736
|
+
void verifySubscription(currentIdentifier);
|
|
3498
3737
|
}
|
|
3499
3738
|
}, REVALIDATION_INTERVAL_MS);
|
|
3500
3739
|
}
|
|
@@ -4185,6 +4424,20 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
|
|
|
4185
4424
|
return meta;
|
|
4186
4425
|
}
|
|
4187
4426
|
|
|
4427
|
+
function shouldExposeFieldValue(el) {
|
|
4428
|
+
if (!(el instanceof HTMLInputElement)) return false;
|
|
4429
|
+
var elType = (el.type || "").toLowerCase();
|
|
4430
|
+
if (elType !== "number") return false;
|
|
4431
|
+
var signals = [
|
|
4432
|
+
el.name,
|
|
4433
|
+
el.id,
|
|
4434
|
+
el.getAttribute("placeholder"),
|
|
4435
|
+
el.getAttribute("aria-label"),
|
|
4436
|
+
labelFor(el),
|
|
4437
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
4438
|
+
return /\b(qty|quantity|count|items?)\b/.test(signals);
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4188
4441
|
function serializeInteractive(el, kind) {
|
|
4189
4442
|
var base = {
|
|
4190
4443
|
type: kind,
|
|
@@ -4219,7 +4472,6 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
|
|
|
4219
4472
|
return {
|
|
4220
4473
|
...base,
|
|
4221
4474
|
label: labelFor(el)?.slice(0, 100),
|
|
4222
|
-
value: text(el.value),
|
|
4223
4475
|
options: Array.from(el.options || []).map(function(option) { return { label: text(option.textContent || option.value) || option.value, value: option.value }; }).filter(function(o) { return o.label || o.value; }).slice(0, 25),
|
|
4224
4476
|
required: el.hasAttribute("required") || undefined,
|
|
4225
4477
|
...fieldMeta(el),
|
|
@@ -4231,7 +4483,6 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
|
|
|
4231
4483
|
...base,
|
|
4232
4484
|
label: labelFor(el)?.slice(0, 100),
|
|
4233
4485
|
placeholder: text(el.getAttribute("placeholder")),
|
|
4234
|
-
value: text(el.value),
|
|
4235
4486
|
required: el.hasAttribute("required") || undefined,
|
|
4236
4487
|
...fieldMeta(el),
|
|
4237
4488
|
};
|
|
@@ -4243,7 +4494,7 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
|
|
|
4243
4494
|
label: labelFor(el)?.slice(0, 100),
|
|
4244
4495
|
inputType: text(el.getAttribute("type")),
|
|
4245
4496
|
placeholder: text(el.getAttribute("placeholder")),
|
|
4246
|
-
value: (
|
|
4497
|
+
value: shouldExposeFieldValue(el) ? text(el.value) : undefined,
|
|
4247
4498
|
required: el.hasAttribute("required") || undefined,
|
|
4248
4499
|
...fieldMeta(el),
|
|
4249
4500
|
};
|
|
@@ -4499,7 +4750,7 @@ const SAFE_EXTRACTION_SCRIPT = String.raw`
|
|
|
4499
4750
|
label: labelFor(el)?.slice(0, 100),
|
|
4500
4751
|
inputType: text(el.getAttribute && el.getAttribute("type")),
|
|
4501
4752
|
placeholder: text(el.getAttribute && el.getAttribute("placeholder")),
|
|
4502
|
-
value:
|
|
4753
|
+
value: shouldExposeFieldValue(el) ? text(el.value) : undefined,
|
|
4503
4754
|
options: tag === "select"
|
|
4504
4755
|
? Array.from(el.options || []).map(function(option) { return { label: text(option.textContent || option.value) || option.value, value: option.value }; }).filter(function(o) { return o.label || o.value; }).slice(0, 25)
|
|
4505
4756
|
: undefined,
|
|
@@ -4752,11 +5003,13 @@ function normalizePageContent(value) {
|
|
|
4752
5003
|
function generateReaderHTML(page) {
|
|
4753
5004
|
const escapedTitle = escapeHtml(page.title);
|
|
4754
5005
|
const escapedByline = escapeHtml(page.byline);
|
|
5006
|
+
const renderedContent = renderReaderContent(page);
|
|
4755
5007
|
return `<!DOCTYPE html>
|
|
4756
5008
|
<html lang="en">
|
|
4757
5009
|
<head>
|
|
4758
5010
|
<meta charset="utf-8">
|
|
4759
5011
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5012
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'">
|
|
4760
5013
|
<title>${escapedTitle}</title>
|
|
4761
5014
|
<style>
|
|
4762
5015
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
@@ -4827,13 +5080,20 @@ function generateReaderHTML(page) {
|
|
|
4827
5080
|
<div class="reader-container">
|
|
4828
5081
|
<h1>${escapedTitle}</h1>
|
|
4829
5082
|
${escapedByline ? `<div class="byline">${escapedByline}</div>` : ""}
|
|
4830
|
-
<div class="reader-content">${
|
|
5083
|
+
<div class="reader-content">${renderedContent}</div>
|
|
4831
5084
|
</div>
|
|
4832
5085
|
</body>
|
|
4833
5086
|
</html>`;
|
|
4834
5087
|
}
|
|
5088
|
+
function renderReaderContent(page) {
|
|
5089
|
+
const source = (page.content || page.excerpt || "").trim();
|
|
5090
|
+
if (!source) {
|
|
5091
|
+
return "<p>No readable content was available for this page.</p>";
|
|
5092
|
+
}
|
|
5093
|
+
return source.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean).map((block) => `<p>${escapeHtml(block).replace(/\n/g, "<br>")}</p>`).join("\n");
|
|
5094
|
+
}
|
|
4835
5095
|
function escapeHtml(str) {
|
|
4836
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5096
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4837
5097
|
}
|
|
4838
5098
|
const mcpStatusChangeListeners = /* @__PURE__ */ new Set();
|
|
4839
5099
|
const runtimeHealthChangeListeners = /* @__PURE__ */ new Set();
|
|
@@ -4927,24 +5187,24 @@ function getVaultPath() {
|
|
|
4927
5187
|
function getKeyPath() {
|
|
4928
5188
|
return path$1.join(getVaultDir(), KEY_FILENAME);
|
|
4929
5189
|
}
|
|
5190
|
+
function assertVaultSecretStorageAvailable() {
|
|
5191
|
+
if (!electron.safeStorage.isEncryptionAvailable()) {
|
|
5192
|
+
throw new Error(
|
|
5193
|
+
"Agent Credential Vault requires OS-backed secret storage. Enable Keychain, DPAPI, or libsecret support and restart Vessel."
|
|
5194
|
+
);
|
|
5195
|
+
}
|
|
5196
|
+
}
|
|
4930
5197
|
function getOrCreateEncryptionKey() {
|
|
5198
|
+
assertVaultSecretStorageAvailable();
|
|
4931
5199
|
const keyPath = getKeyPath();
|
|
4932
5200
|
if (fs$1.existsSync(keyPath)) {
|
|
4933
5201
|
const encryptedKey = fs$1.readFileSync(keyPath);
|
|
4934
|
-
|
|
4935
|
-
return electron.safeStorage.decryptString ? Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8") : encryptedKey;
|
|
4936
|
-
}
|
|
4937
|
-
return encryptedKey;
|
|
5202
|
+
return Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8");
|
|
4938
5203
|
}
|
|
4939
5204
|
const key = crypto$2.randomBytes(32);
|
|
4940
5205
|
fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
fs$1.writeFileSync(keyPath, encrypted);
|
|
4944
|
-
} else {
|
|
4945
|
-
fs$1.writeFileSync(keyPath, key);
|
|
4946
|
-
fs$1.chmodSync(keyPath, 384);
|
|
4947
|
-
}
|
|
5206
|
+
const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
|
|
5207
|
+
fs$1.writeFileSync(keyPath, encrypted, { mode: 384 });
|
|
4948
5208
|
return key;
|
|
4949
5209
|
}
|
|
4950
5210
|
function encrypt(plaintext) {
|
|
@@ -4985,8 +5245,9 @@ function loadVault() {
|
|
|
4985
5245
|
return cachedEntries;
|
|
4986
5246
|
} catch (err) {
|
|
4987
5247
|
console.error("[Vessel Vault] Failed to load vault:", err);
|
|
4988
|
-
|
|
4989
|
-
|
|
5248
|
+
throw new Error(
|
|
5249
|
+
"Could not unlock the Agent Credential Vault. Check that OS secret storage is available and that the stored vault key can be decrypted."
|
|
5250
|
+
);
|
|
4990
5251
|
}
|
|
4991
5252
|
}
|
|
4992
5253
|
function saveVault(entries) {
|
|
@@ -5380,7 +5641,7 @@ function buildPhaseReminder(userMessage, assistantText) {
|
|
|
5380
5641
|
const multiClickSelectionSignals = /i(?:'| a)?ll start by clicking on the following books|i will start by clicking on the following books|i will click on the following books|clicked on five different book titles|clicked on \d+ different book titles|clicking through the selected titles|click each of the selected titles/.test(
|
|
5381
5642
|
text
|
|
5382
5643
|
);
|
|
5383
|
-
const staleSelectionSignals = /cannot locate the elements to click|page structure is not being reliably captured|specific titles failed|page may have changed|stale-index/.test(
|
|
5644
|
+
const staleSelectionSignals = /cannot locate the elements to click|page structure is not being reliably captured|specific titles failed|page may have changed|stale-index|no visible area|not visible/.test(
|
|
5384
5645
|
text
|
|
5385
5646
|
);
|
|
5386
5647
|
const intermediateCartDialogSignals = /(added to cart|has been added to the cart|cart confirmation)/.test(text) && /(continue shopping|search results page|return to the search results page|back button|go back)/.test(
|
|
@@ -6728,6 +6989,10 @@ function limitItems(items, max = MAX_STRUCTURED_ITEMS) {
|
|
|
6728
6989
|
if (items.length <= max) return items;
|
|
6729
6990
|
return items.slice(0, max);
|
|
6730
6991
|
}
|
|
6992
|
+
function shouldRenderFieldValue(el) {
|
|
6993
|
+
const value = typeof el.value === "string" && el.value.trim() ? el.value.trim() : "";
|
|
6994
|
+
return Boolean(value) && isQuantityLike(el);
|
|
6995
|
+
}
|
|
6731
6996
|
function formatElementMeta(el) {
|
|
6732
6997
|
const meta = [];
|
|
6733
6998
|
if (el.context && el.context !== "content") {
|
|
@@ -6795,7 +7060,7 @@ function formatElementMeta(el) {
|
|
|
6795
7060
|
if (el.description) {
|
|
6796
7061
|
meta.push(`desc="${el.description.slice(0, 80)}"`);
|
|
6797
7062
|
}
|
|
6798
|
-
if (el
|
|
7063
|
+
if (shouldRenderFieldValue(el)) {
|
|
6799
7064
|
meta.push(`value="${el.value.slice(0, 60)}"`);
|
|
6800
7065
|
}
|
|
6801
7066
|
if (el.selector) {
|
|
@@ -6806,7 +7071,7 @@ function formatElementMeta(el) {
|
|
|
6806
7071
|
}
|
|
6807
7072
|
function summarizeElementValue(el) {
|
|
6808
7073
|
const value = typeof el.value === "string" && el.value.trim() ? el.value.trim() : "";
|
|
6809
|
-
if (!value) return null;
|
|
7074
|
+
if (!value || !shouldRenderFieldValue(el)) return null;
|
|
6810
7075
|
if (el.type === "select") {
|
|
6811
7076
|
return { label: "selected", value: value.slice(0, 60) };
|
|
6812
7077
|
}
|
|
@@ -8212,6 +8477,8 @@ const COMPACT_FOCUS_INSTRUCTIONS = [
|
|
|
8212
8477
|
"When read_page or inspect_element gives you an element index, prefer click(index=N) over selector-based clicks.",
|
|
8213
8478
|
'If a product page has no visible purchase control, scroll and call read_page(mode="visible_only") again. Do not loop on generic inspect_element calls against navigation or unrelated regions.',
|
|
8214
8479
|
"After adding an item to cart and going back, ALWAYS call read_page to see the current results. The system shows which products are already in your cart — do NOT click those again. Pick a DIFFERENT product from the list. If all visible results are already in cart, scroll down for more.",
|
|
8480
|
+
'On search results pages, always call read_page(mode="results_only") first. Click products by their [#N] index from the Results section. Never click filter or sort links (e.g. Used, New, Format, Price).',
|
|
8481
|
+
"After go_back, always call read_page before clicking. The page may have changed.",
|
|
8215
8482
|
"Keep your reasoning short. Prefer taking the next tool action over writing a long plan."
|
|
8216
8483
|
];
|
|
8217
8484
|
function buildInstructionBlock(instructions) {
|
|
@@ -8563,11 +8830,11 @@ function buildCompactScopedContext(page, mode, pageType = detectPageType(page))
|
|
|
8563
8830
|
const primaryResultElements = getPrimaryResultLinks(page);
|
|
8564
8831
|
const primaryResults = primaryResultElements.map(formatElement);
|
|
8565
8832
|
if (primaryResults.length > 0) {
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
);
|
|
8833
|
+
lines.push("");
|
|
8834
|
+
lines.push("### Results — click one of these to open a product");
|
|
8835
|
+
lines.push(...primaryResults.map((item) => `- ${item}`));
|
|
8836
|
+
lines.push("");
|
|
8837
|
+
lines.push("IMPORTANT: Use click(index=N) on a result above. Do NOT click filter or sort links.");
|
|
8571
8838
|
}
|
|
8572
8839
|
if (pageType === "FORM" || pageType === "LOGIN" || mode === "forms_only") {
|
|
8573
8840
|
pushSection(
|
|
@@ -8583,7 +8850,7 @@ function buildCompactScopedContext(page, mode, pageType = detectPageType(page))
|
|
|
8583
8850
|
).map(formatElement);
|
|
8584
8851
|
pushSection(
|
|
8585
8852
|
lines,
|
|
8586
|
-
"###
|
|
8853
|
+
"### Page Controls (filters, sorts — avoid when selecting products)",
|
|
8587
8854
|
visibleControls
|
|
8588
8855
|
);
|
|
8589
8856
|
}
|
|
@@ -10136,22 +10403,6 @@ function formatDeadLinkMessage(label, result) {
|
|
|
10136
10403
|
const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
|
|
10137
10404
|
return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
|
|
10138
10405
|
}
|
|
10139
|
-
const ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
10140
|
-
function isSafeNavigationURL(url) {
|
|
10141
|
-
try {
|
|
10142
|
-
const parsed = new URL(url);
|
|
10143
|
-
return ALLOWED_SCHEMES.has(parsed.protocol);
|
|
10144
|
-
} catch {
|
|
10145
|
-
return false;
|
|
10146
|
-
}
|
|
10147
|
-
}
|
|
10148
|
-
function assertSafeURL(url) {
|
|
10149
|
-
if (!isSafeNavigationURL(url)) {
|
|
10150
|
-
throw new Error(
|
|
10151
|
-
`Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`
|
|
10152
|
-
);
|
|
10153
|
-
}
|
|
10154
|
-
}
|
|
10155
10406
|
async function captureScreenshot(wc) {
|
|
10156
10407
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
10157
10408
|
await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
|
|
@@ -10661,6 +10912,17 @@ function resolveTextTargetInDocument(doc, rawQuery, mode) {
|
|
|
10661
10912
|
score += 30;
|
|
10662
10913
|
}
|
|
10663
10914
|
if (inViewport(el)) score += 25;
|
|
10915
|
+
if (tag === "a") {
|
|
10916
|
+
const href = htmlEl.href || "";
|
|
10917
|
+
const filterParams = /\b(condition|binding|format|availability|sort|filter|price|category_id)\b=[^&]/i;
|
|
10918
|
+
const filterPath = /\/(condition|binding|format|availability|sort|filter|price|category)\/[^/?#]+/i;
|
|
10919
|
+
if (filterParams.test(href) || filterPath.test(href)) {
|
|
10920
|
+
score -= 40;
|
|
10921
|
+
}
|
|
10922
|
+
if (/\b(used|new|paperback|hardcover|hardback|ebook|kindle|refine|clear all|remove filter)\b/.test(label)) {
|
|
10923
|
+
score -= 30;
|
|
10924
|
+
}
|
|
10925
|
+
}
|
|
10664
10926
|
return score;
|
|
10665
10927
|
}
|
|
10666
10928
|
function regionBonus(el) {
|
|
@@ -10865,6 +11127,10 @@ function formatCompactToolResult(name, result) {
|
|
|
10865
11127
|
const DEFAULT_PAGE_SCRIPT_TIMEOUT_MS = 1500;
|
|
10866
11128
|
const QUIET_NAVIGATION_WINDOW_MS = 1200;
|
|
10867
11129
|
const PAGE_SCRIPT_TIMEOUT = /* @__PURE__ */ Symbol("page-script-timeout");
|
|
11130
|
+
async function loadPermittedUrl$1(wc, url) {
|
|
11131
|
+
assertPermittedNavigationURL(url);
|
|
11132
|
+
await wc.loadURL(url);
|
|
11133
|
+
}
|
|
10868
11134
|
function pageBusyError(action) {
|
|
10869
11135
|
return `Error: Page is still busy; ${action} timed out waiting for page scripts. Retry in a moment.`;
|
|
10870
11136
|
}
|
|
@@ -11080,9 +11346,9 @@ function waitForLoad$1(wc, timeout = 5e3) {
|
|
|
11080
11346
|
finish();
|
|
11081
11347
|
return;
|
|
11082
11348
|
}
|
|
11083
|
-
wc.
|
|
11084
|
-
wc.
|
|
11085
|
-
wc.
|
|
11349
|
+
wc.once("did-finish-load", onLoadEvent);
|
|
11350
|
+
wc.once("did-stop-loading", onLoadEvent);
|
|
11351
|
+
wc.once("did-fail-load", onLoadEvent);
|
|
11086
11352
|
});
|
|
11087
11353
|
}
|
|
11088
11354
|
function waitForPotentialNavigation$1(wc, beforeUrl, timeout = 2500) {
|
|
@@ -11135,8 +11401,8 @@ function waitForPotentialNavigation$1(wc, beforeUrl, timeout = 2500) {
|
|
|
11135
11401
|
wc.once("did-start-loading", onStart);
|
|
11136
11402
|
wc.once("did-navigate", onNavigate);
|
|
11137
11403
|
wc.once("did-navigate-in-page", onNavigateInPage);
|
|
11138
|
-
wc.
|
|
11139
|
-
wc.
|
|
11404
|
+
wc.once("did-stop-loading", onNativeChange);
|
|
11405
|
+
wc.once("page-title-updated", onNativeChange);
|
|
11140
11406
|
});
|
|
11141
11407
|
}
|
|
11142
11408
|
async function getPostNavSummary(wc) {
|
|
@@ -11336,7 +11602,7 @@ async function clickElement$1(wc, selector) {
|
|
|
11336
11602
|
|
|
11337
11603
|
const rect = el.getBoundingClientRect();
|
|
11338
11604
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
11339
|
-
return { error: "Error[hidden]: Element has no visible area" };
|
|
11605
|
+
return { error: "Error[hidden]: Element has no visible area. It may be inside a collapsed, lazy-loaded, or virtual-scroll section. Scroll toward it (scroll or scroll_to_element) then call read_page to refresh visible elements before clicking again." };
|
|
11340
11606
|
}
|
|
11341
11607
|
|
|
11342
11608
|
const points = samplePoints(rect);
|
|
@@ -11903,6 +12169,12 @@ function getCartAddedSummary(url) {
|
|
|
11903
12169
|
Already in cart (${count} items):
|
|
11904
12170
|
${items}`;
|
|
11905
12171
|
}
|
|
12172
|
+
function clearCartState() {
|
|
12173
|
+
cartAddedProducts.clear();
|
|
12174
|
+
recentCartClicks.clear();
|
|
12175
|
+
clickStreakUrl = null;
|
|
12176
|
+
clickStreakCount = 0;
|
|
12177
|
+
}
|
|
11906
12178
|
async function buildCartSuccessSuffix(wc, productUrl, overlayHint) {
|
|
11907
12179
|
const productTitle = await getProductPageTitle(wc);
|
|
11908
12180
|
recordProductAddedToCart(productUrl, productTitle);
|
|
@@ -11968,8 +12240,7 @@ Go back and select a different product.`;
|
|
|
11968
12240
|
const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
|
|
11969
12241
|
if (hrefMatch) {
|
|
11970
12242
|
try {
|
|
11971
|
-
|
|
11972
|
-
await wc.loadURL(hrefMatch[1]);
|
|
12243
|
+
await loadPermittedUrl$1(wc, hrefMatch[1]);
|
|
11973
12244
|
await waitForLoad$1(wc, 8e3);
|
|
11974
12245
|
const hrefUrl = wc.getURL();
|
|
11975
12246
|
if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
|
|
@@ -12036,8 +12307,7 @@ Go back and select a different product.`;
|
|
|
12036
12307
|
const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
|
|
12037
12308
|
if (hrefMatch) {
|
|
12038
12309
|
try {
|
|
12039
|
-
|
|
12040
|
-
await wc.loadURL(hrefMatch[1]);
|
|
12310
|
+
await loadPermittedUrl$1(wc, hrefMatch[1]);
|
|
12041
12311
|
await waitForLoad$1(wc, 8e3);
|
|
12042
12312
|
const hrefUrl = wc.getURL();
|
|
12043
12313
|
if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
|
|
@@ -12132,8 +12402,7 @@ ${postActivationOverlayHint}`;
|
|
|
12132
12402
|
const validation = await validateLinkDestination(elInfo.href);
|
|
12133
12403
|
if (validation.status !== "dead") {
|
|
12134
12404
|
try {
|
|
12135
|
-
|
|
12136
|
-
await wc.loadURL(elInfo.href);
|
|
12405
|
+
await loadPermittedUrl$1(wc, elInfo.href);
|
|
12137
12406
|
await waitForLoad$1(wc, 8e3);
|
|
12138
12407
|
const hrefFallbackUrl = wc.getURL();
|
|
12139
12408
|
if (hrefFallbackUrl !== beforeUrl) {
|
|
@@ -13685,8 +13954,7 @@ async function submitForm$1(wc, args) {
|
|
|
13685
13954
|
if (formInfo.params) {
|
|
13686
13955
|
url.search = formInfo.params;
|
|
13687
13956
|
}
|
|
13688
|
-
|
|
13689
|
-
wc.loadURL(url.toString());
|
|
13957
|
+
await loadPermittedUrl$1(wc, url.toString());
|
|
13690
13958
|
await waitForPotentialNavigation$1(wc, beforeUrl);
|
|
13691
13959
|
const afterUrl = wc.getURL();
|
|
13692
13960
|
return afterUrl !== beforeUrl ? `Submitted form via GET -> ${afterUrl}` : "Submitted form via GET";
|
|
@@ -14323,8 +14591,7 @@ async function searchPage(wc, args) {
|
|
|
14323
14591
|
const shortcut = buildSearchShortcut(wc.getURL(), query);
|
|
14324
14592
|
if (shortcut) {
|
|
14325
14593
|
const beforeUrl2 = wc.getURL();
|
|
14326
|
-
|
|
14327
|
-
wc.loadURL(shortcut.url);
|
|
14594
|
+
await loadPermittedUrl$1(wc, shortcut.url);
|
|
14328
14595
|
await waitForPotentialNavigation$1(wc, beforeUrl2, 4e3);
|
|
14329
14596
|
const afterUrl2 = wc.getURL();
|
|
14330
14597
|
const applied = shortcut.appliedFilters.length > 0 ? ` (${shortcut.appliedFilters.join(", ")})` : "";
|
|
@@ -14480,6 +14747,18 @@ WARNING: You drifted to ${drift.targetDomain} but the task requires staying on $
|
|
|
14480
14747
|
warnings += `${cartSummary}
|
|
14481
14748
|
Select a DIFFERENT product that is not in the cart. Call read_page if needed to see available results.`;
|
|
14482
14749
|
}
|
|
14750
|
+
if (ctx.toolProfile === "compact" && name === "go_back") {
|
|
14751
|
+
warnings += `
|
|
14752
|
+
Call read_page(mode="results_only") to see available products before clicking.`;
|
|
14753
|
+
}
|
|
14754
|
+
}
|
|
14755
|
+
if (name === "click" && ctx.toolProfile === "compact") {
|
|
14756
|
+
const filterParams = /\b(condition|binding|format|availability|sort|filter|price|category_id|view)\b=[^&]/i;
|
|
14757
|
+
const filterPath = /\/(condition|binding|format|availability|sort|filter|price|category)\/[^/?#]+/i;
|
|
14758
|
+
if (filterParams.test(currentUrl) || filterPath.test(currentUrl)) {
|
|
14759
|
+
warnings += `
|
|
14760
|
+
WARNING: The clicked link appears to be a filter or sort control, not a product. If you intended to click a product, call go_back and use click(index=N) on a result from read_page(mode="results_only").`;
|
|
14761
|
+
}
|
|
14483
14762
|
}
|
|
14484
14763
|
return `
|
|
14485
14764
|
[state: url=${currentUrl}, title=${JSON.stringify(wc.getTitle() || "")}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]${warnings}`;
|
|
@@ -15713,9 +15992,14 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
|
|
|
15713
15992
|
const pageType = detectPageType(pageContent);
|
|
15714
15993
|
const defaultReadMode = chooseAgentReadMode(pageContent);
|
|
15715
15994
|
if (provider.agentToolProfile === "compact") {
|
|
15995
|
+
const prevGoal = runtime2.getState().taskTracker?.goal?.trim();
|
|
15716
15996
|
runtime2.ensureTaskTracker(query, pageContent.url || activeWebContents.getURL());
|
|
15997
|
+
if (prevGoal !== query.trim()) {
|
|
15998
|
+
clearCartState();
|
|
15999
|
+
}
|
|
15717
16000
|
} else {
|
|
15718
16001
|
runtime2.clearTaskTracker();
|
|
16002
|
+
clearCartState();
|
|
15719
16003
|
}
|
|
15720
16004
|
const structuredContext = provider.agentToolProfile === "compact" ? buildCompactScopedContext(
|
|
15721
16005
|
pageContent,
|
|
@@ -16739,6 +17023,10 @@ function clearMcpAuthFile() {
|
|
|
16739
17023
|
function asTextResponse(text) {
|
|
16740
17024
|
return { content: [{ type: "text", text }] };
|
|
16741
17025
|
}
|
|
17026
|
+
async function loadPermittedUrl(wc, url) {
|
|
17027
|
+
assertPermittedNavigationURL(url);
|
|
17028
|
+
await wc.loadURL(url);
|
|
17029
|
+
}
|
|
16742
17030
|
function asPromptResponse(text) {
|
|
16743
17031
|
return {
|
|
16744
17032
|
messages: [
|
|
@@ -16938,7 +17226,7 @@ async function clickElement(wc, selector) {
|
|
|
16938
17226
|
|
|
16939
17227
|
const rect = el.getBoundingClientRect();
|
|
16940
17228
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
16941
|
-
return { error: "Element is not visible" };
|
|
17229
|
+
return { error: "Element is not visible. It may be inside a collapsed, lazy-loaded, or virtual-scroll section. Scroll toward it (scroll or scroll_to_element) then call read_page to refresh visible elements before clicking again." };
|
|
16942
17230
|
}
|
|
16943
17231
|
|
|
16944
17232
|
const points = samplePoints(rect);
|
|
@@ -17058,7 +17346,7 @@ async function clickResolvedSelector(wc, selector) {
|
|
|
17058
17346
|
const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
|
|
17059
17347
|
if (hrefMatch) {
|
|
17060
17348
|
try {
|
|
17061
|
-
await wc
|
|
17349
|
+
await loadPermittedUrl(wc, hrefMatch[1]);
|
|
17062
17350
|
await waitForLoad(wc, 8e3);
|
|
17063
17351
|
const hrefUrl = wc.getURL();
|
|
17064
17352
|
if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
|
|
@@ -17111,7 +17399,7 @@ ${overlayHint2}${actionsSuffix}`;
|
|
|
17111
17399
|
const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
|
|
17112
17400
|
if (hrefMatch) {
|
|
17113
17401
|
try {
|
|
17114
|
-
await wc
|
|
17402
|
+
await loadPermittedUrl(wc, hrefMatch[1]);
|
|
17115
17403
|
await waitForLoad(wc, 8e3);
|
|
17116
17404
|
const hrefUrl = wc.getURL();
|
|
17117
17405
|
if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
|
|
@@ -17841,7 +18129,7 @@ async function hoverElement(wc, selector) {
|
|
|
17841
18129
|
el.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'center' });
|
|
17842
18130
|
}
|
|
17843
18131
|
const rect = el.getBoundingClientRect();
|
|
17844
|
-
if (rect.width <= 0 || rect.height <= 0) return { error: 'Error[hidden]: Element has no visible area' };
|
|
18132
|
+
if (rect.width <= 0 || rect.height <= 0) return { error: 'Error[hidden]: Element has no visible area. It may be inside a collapsed, lazy-loaded, or virtual-scroll section. Scroll toward it then call read_page to refresh visible elements.' };
|
|
17845
18133
|
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true }));
|
|
17846
18134
|
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false }));
|
|
17847
18135
|
const label = (el.textContent || el.tagName || 'Element').trim().slice(0, 80);
|
|
@@ -18022,8 +18310,7 @@ async function submitForm(wc, index, selector) {
|
|
|
18022
18310
|
if (formInfo.params) {
|
|
18023
18311
|
url.search = formInfo.params;
|
|
18024
18312
|
}
|
|
18025
|
-
|
|
18026
|
-
wc.loadURL(url.toString());
|
|
18313
|
+
await loadPermittedUrl(wc, url.toString());
|
|
18027
18314
|
await waitForPotentialNavigation(wc, beforeUrl);
|
|
18028
18315
|
const afterUrl = wc.getURL();
|
|
18029
18316
|
return afterUrl !== beforeUrl ? `Submitted form via GET -> ${afterUrl}` : "Submitted form via GET";
|
|
@@ -22175,7 +22462,7 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
22175
22462
|
return windowState.uiState.settingsOpen;
|
|
22176
22463
|
});
|
|
22177
22464
|
electron.ipcMain.handle(Channels.SETTINGS_GET, () => {
|
|
22178
|
-
return
|
|
22465
|
+
return getRendererSettings();
|
|
22179
22466
|
});
|
|
22180
22467
|
electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, () => getRuntimeHealth());
|
|
22181
22468
|
electron.ipcMain.handle(Channels.SETTINGS_SET, async (_, key, value) => {
|
|
@@ -22193,8 +22480,9 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
22193
22480
|
await stopMcpServer();
|
|
22194
22481
|
await startMcpServer(tabManager, runtime2, updatedSettings.mcpPort);
|
|
22195
22482
|
}
|
|
22196
|
-
|
|
22197
|
-
|
|
22483
|
+
const rendererSettings = getRendererSettings();
|
|
22484
|
+
sendToRendererViews(Channels.SETTINGS_UPDATE, rendererSettings);
|
|
22485
|
+
return rendererSettings;
|
|
22198
22486
|
});
|
|
22199
22487
|
electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, () => runtime2.getState());
|
|
22200
22488
|
electron.ipcMain.handle(Channels.AGENT_PAUSE, () => runtime2.pause());
|
|
@@ -22409,21 +22697,44 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
22409
22697
|
electron.ipcMain.handle(Channels.PREMIUM_GET_STATE, () => {
|
|
22410
22698
|
return getPremiumState();
|
|
22411
22699
|
});
|
|
22412
|
-
electron.ipcMain.handle(Channels.
|
|
22700
|
+
electron.ipcMain.handle(Channels.PREMIUM_ACTIVATION_START, async (_, email) => {
|
|
22413
22701
|
assertString(email, "email");
|
|
22414
22702
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
|
|
22415
|
-
return { ok: false,
|
|
22703
|
+
return { ok: false, error: "Invalid email format" };
|
|
22416
22704
|
}
|
|
22417
22705
|
trackPremiumFunnel("activation_attempted");
|
|
22418
|
-
const result = await
|
|
22419
|
-
if (result.ok) {
|
|
22420
|
-
trackPremiumFunnel("
|
|
22421
|
-
sendToRendererViews(Channels.PREMIUM_UPDATE, result.state);
|
|
22422
|
-
} else {
|
|
22423
|
-
trackPremiumFunnel("activation_failed", { status: result.state.status });
|
|
22706
|
+
const result = await requestActivationCode(email);
|
|
22707
|
+
if (!result.ok) {
|
|
22708
|
+
trackPremiumFunnel("activation_failed");
|
|
22424
22709
|
}
|
|
22425
22710
|
return result;
|
|
22426
22711
|
});
|
|
22712
|
+
electron.ipcMain.handle(
|
|
22713
|
+
Channels.PREMIUM_ACTIVATION_VERIFY,
|
|
22714
|
+
async (_, email, code, challengeToken) => {
|
|
22715
|
+
assertString(email, "email");
|
|
22716
|
+
assertString(code, "code");
|
|
22717
|
+
assertString(challengeToken, "challengeToken");
|
|
22718
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
|
|
22719
|
+
return {
|
|
22720
|
+
ok: false,
|
|
22721
|
+
state: getPremiumState(),
|
|
22722
|
+
error: "Invalid email format"
|
|
22723
|
+
};
|
|
22724
|
+
}
|
|
22725
|
+
trackPremiumFunnel("activation_attempted");
|
|
22726
|
+
const result = await verifyActivationCode(email, code, challengeToken);
|
|
22727
|
+
if (result.ok) {
|
|
22728
|
+
trackPremiumFunnel("activation_succeeded", {
|
|
22729
|
+
status: result.state.status
|
|
22730
|
+
});
|
|
22731
|
+
sendToRendererViews(Channels.PREMIUM_UPDATE, result.state);
|
|
22732
|
+
} else {
|
|
22733
|
+
trackPremiumFunnel("activation_failed", { status: result.state.status });
|
|
22734
|
+
}
|
|
22735
|
+
return result;
|
|
22736
|
+
}
|
|
22737
|
+
);
|
|
22427
22738
|
electron.ipcMain.handle(Channels.PREMIUM_CHECKOUT, async (_, email) => {
|
|
22428
22739
|
trackPremiumFunnel("checkout_clicked");
|
|
22429
22740
|
const result = await getCheckoutUrl(email);
|