@quanta-intellect/vessel-browser 0.1.18 → 0.1.20
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/README.md +17 -0
- package/out/main/index.js +1112 -427
- package/out/preload/index.js +14 -1
- package/out/renderer/assets/{index-DAaJOss-.js → index-CKOT_IZt.js} +947 -209
- package/out/renderer/assets/{index-DMd-y6tm.css → index-DwRZftNk.css} +110 -0
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
package/out/main/index.js
CHANGED
|
@@ -4,11 +4,11 @@ const fs$1 = require("node:fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const crypto = require("crypto");
|
|
7
|
+
const crypto$1 = require("node:crypto");
|
|
8
|
+
const path$1 = require("node:path");
|
|
7
9
|
const Anthropic = require("@anthropic-ai/sdk");
|
|
8
10
|
const OpenAI = require("openai");
|
|
9
11
|
const zod = require("zod");
|
|
10
|
-
const path$1 = require("node:path");
|
|
11
|
-
const crypto$1 = require("node:crypto");
|
|
12
12
|
const http = require("node:http");
|
|
13
13
|
const os = require("node:os");
|
|
14
14
|
const mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
@@ -362,7 +362,7 @@ class Tab {
|
|
|
362
362
|
get state() {
|
|
363
363
|
return { ...this._state };
|
|
364
364
|
}
|
|
365
|
-
navigate(url) {
|
|
365
|
+
navigate(url, postBody) {
|
|
366
366
|
if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
|
|
367
367
|
if (url.includes(".") && !url.includes(" ")) {
|
|
368
368
|
url = "https://" + url;
|
|
@@ -375,7 +375,24 @@ class Tab {
|
|
|
375
375
|
}
|
|
376
376
|
const policyError = checkDomainPolicy(url);
|
|
377
377
|
if (policyError) return policyError;
|
|
378
|
-
|
|
378
|
+
if (postBody) {
|
|
379
|
+
const params = new URLSearchParams();
|
|
380
|
+
for (const [key, value] of Object.entries(postBody)) {
|
|
381
|
+
params.set(key, value);
|
|
382
|
+
}
|
|
383
|
+
this.view.webContents.loadURL(url, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
extraHeaders: "Content-Type: application/x-www-form-urlencoded\r\n",
|
|
386
|
+
postData: [
|
|
387
|
+
{
|
|
388
|
+
type: "rawData",
|
|
389
|
+
bytes: Buffer.from(params.toString())
|
|
390
|
+
}
|
|
391
|
+
]
|
|
392
|
+
});
|
|
393
|
+
} else {
|
|
394
|
+
this.view.webContents.loadURL(url);
|
|
395
|
+
}
|
|
379
396
|
return null;
|
|
380
397
|
}
|
|
381
398
|
goBack() {
|
|
@@ -1228,7 +1245,7 @@ function subscribe$1(listener) {
|
|
|
1228
1245
|
listeners$1.delete(listener);
|
|
1229
1246
|
};
|
|
1230
1247
|
}
|
|
1231
|
-
function addEntry(url, title) {
|
|
1248
|
+
function addEntry$1(url, title) {
|
|
1232
1249
|
if (!url || url === "about:blank") return;
|
|
1233
1250
|
load$1();
|
|
1234
1251
|
const last = state$2.entries[0];
|
|
@@ -1991,7 +2008,7 @@ class TabManager {
|
|
|
1991
2008
|
},
|
|
1992
2009
|
onPageLoad: (pageUrl, wc) => {
|
|
1993
2010
|
this.reapplyHighlights(pageUrl, wc);
|
|
1994
|
-
addEntry(pageUrl, wc.getTitle());
|
|
2011
|
+
addEntry$1(pageUrl, wc.getTitle());
|
|
1995
2012
|
},
|
|
1996
2013
|
onHighlightSelection: (wc) => this.captureHighlightFromPage(wc),
|
|
1997
2014
|
onHighlightRemove: (url2, text) => this.removeHighlightByText(url2, text),
|
|
@@ -2037,9 +2054,10 @@ class TabManager {
|
|
|
2037
2054
|
this.broadcastState();
|
|
2038
2055
|
}
|
|
2039
2056
|
}
|
|
2040
|
-
navigateTab(id, url) {
|
|
2057
|
+
navigateTab(id, url, postBody) {
|
|
2041
2058
|
const tab = this.tabs.get(id);
|
|
2042
|
-
if (tab) tab
|
|
2059
|
+
if (!tab) return `No tab with id ${id}`;
|
|
2060
|
+
return tab.navigate(url, postBody);
|
|
2043
2061
|
}
|
|
2044
2062
|
goBack(id) {
|
|
2045
2063
|
return this.tabs.get(id)?.goBack() ?? false;
|
|
@@ -2349,6 +2367,12 @@ const Channels = {
|
|
|
2349
2367
|
PREMIUM_PORTAL: "premium:portal",
|
|
2350
2368
|
PREMIUM_RESET: "premium:reset",
|
|
2351
2369
|
PREMIUM_UPDATE: "premium:update",
|
|
2370
|
+
// Agent Credential Vault
|
|
2371
|
+
VAULT_LIST: "vault:list",
|
|
2372
|
+
VAULT_ADD: "vault:add",
|
|
2373
|
+
VAULT_UPDATE: "vault:update",
|
|
2374
|
+
VAULT_REMOVE: "vault:remove",
|
|
2375
|
+
VAULT_AUDIT_LOG: "vault:audit-log",
|
|
2352
2376
|
// Window controls
|
|
2353
2377
|
WINDOW_MINIMIZE: "window:minimize",
|
|
2354
2378
|
WINDOW_MAXIMIZE: "window:maximize",
|
|
@@ -3184,105 +3208,408 @@ function addIfPresent(target, key, value) {
|
|
|
3184
3208
|
target[key] = value;
|
|
3185
3209
|
}
|
|
3186
3210
|
}
|
|
3187
|
-
const
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
try {
|
|
3217
|
-
if (window.__vessel && typeof window.__vessel.extractContent === "function") {
|
|
3218
|
-
const structured = window.__vessel.extractContent();
|
|
3219
|
-
if (structured && typeof structured === "object") {
|
|
3220
|
-
return structured;
|
|
3221
|
-
}
|
|
3222
|
-
}
|
|
3223
|
-
} catch (_error) {
|
|
3224
|
-
}
|
|
3225
|
-
return null;
|
|
3226
|
-
})()
|
|
3227
|
-
`;
|
|
3228
|
-
const DIRECT_EXTRACTION_SCRIPT = String.raw`
|
|
3229
|
-
(function() {
|
|
3230
|
-
// Time budget: stop expensive DOM traversals after this many ms so heavy
|
|
3231
|
-
// pages (Newegg, Wikipedia, etc.) don't stall the agent for 30-60s+.
|
|
3232
|
-
var BUDGET_MS = 5000;
|
|
3233
|
-
var _budgetStart = performance.now();
|
|
3234
|
-
function withinBudget() {
|
|
3235
|
-
return (performance.now() - _budgetStart) < BUDGET_MS;
|
|
3211
|
+
const VERIFICATION_API = process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
|
|
3212
|
+
const FREE_TOOL_ITERATION_LIMIT = 50;
|
|
3213
|
+
const REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3214
|
+
const OFFLINE_GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
3215
|
+
const PREMIUM_TOOLS = /* @__PURE__ */ new Set([
|
|
3216
|
+
"screenshot",
|
|
3217
|
+
"save_session",
|
|
3218
|
+
"load_session",
|
|
3219
|
+
"list_sessions",
|
|
3220
|
+
"delete_session",
|
|
3221
|
+
"flow_start",
|
|
3222
|
+
"flow_advance",
|
|
3223
|
+
"flow_status",
|
|
3224
|
+
"flow_end",
|
|
3225
|
+
"metrics",
|
|
3226
|
+
"extract_table",
|
|
3227
|
+
"vault_login",
|
|
3228
|
+
"vault_status",
|
|
3229
|
+
"vault_totp"
|
|
3230
|
+
]);
|
|
3231
|
+
function isPremium() {
|
|
3232
|
+
const { premium } = loadSettings();
|
|
3233
|
+
if (premium.status === "active" || premium.status === "trialing") {
|
|
3234
|
+
return true;
|
|
3235
|
+
}
|
|
3236
|
+
if (premium.validatedAt && premium.status !== "free") {
|
|
3237
|
+
const lastValidated = new Date(premium.validatedAt).getTime();
|
|
3238
|
+
if (Date.now() - lastValidated < OFFLINE_GRACE_PERIOD_MS) {
|
|
3239
|
+
return true;
|
|
3236
3240
|
}
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3241
|
+
}
|
|
3242
|
+
return false;
|
|
3243
|
+
}
|
|
3244
|
+
function getPremiumState() {
|
|
3245
|
+
return { ...loadSettings().premium };
|
|
3246
|
+
}
|
|
3247
|
+
function getEffectiveMaxIterations() {
|
|
3248
|
+
if (isPremium()) {
|
|
3249
|
+
return loadSettings().maxToolIterations || 200;
|
|
3250
|
+
}
|
|
3251
|
+
return FREE_TOOL_ITERATION_LIMIT;
|
|
3252
|
+
}
|
|
3253
|
+
function resetPremium() {
|
|
3254
|
+
const fresh = {
|
|
3255
|
+
status: "free",
|
|
3256
|
+
customerId: "",
|
|
3257
|
+
email: "",
|
|
3258
|
+
validatedAt: "",
|
|
3259
|
+
expiresAt: ""
|
|
3260
|
+
};
|
|
3261
|
+
setSetting("premium", fresh);
|
|
3262
|
+
return fresh;
|
|
3263
|
+
}
|
|
3264
|
+
function isToolGated(toolName) {
|
|
3265
|
+
return PREMIUM_TOOLS.has(toolName) && !isPremium();
|
|
3266
|
+
}
|
|
3267
|
+
async function getCheckoutUrl(email) {
|
|
3268
|
+
try {
|
|
3269
|
+
const params = new URLSearchParams();
|
|
3270
|
+
if (email) params.set("email", email);
|
|
3271
|
+
const res = await fetch(`${VERIFICATION_API}/checkout?${params}`, {
|
|
3272
|
+
method: "POST",
|
|
3273
|
+
headers: { "Content-Type": "application/json" }
|
|
3274
|
+
});
|
|
3275
|
+
if (!res.ok) {
|
|
3276
|
+
const body = await res.text();
|
|
3277
|
+
return { ok: false, error: body || `HTTP ${res.status}` };
|
|
3256
3278
|
}
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3279
|
+
const { url } = await res.json();
|
|
3280
|
+
return { ok: true, url };
|
|
3281
|
+
} catch (err) {
|
|
3282
|
+
return {
|
|
3283
|
+
ok: false,
|
|
3284
|
+
error: err instanceof Error ? err.message : "Failed to create checkout"
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
async function getPortalUrl() {
|
|
3289
|
+
const { premium } = loadSettings();
|
|
3290
|
+
if (!premium.customerId) {
|
|
3291
|
+
return { ok: false, error: "No active subscription" };
|
|
3292
|
+
}
|
|
3293
|
+
try {
|
|
3294
|
+
const res = await fetch(`${VERIFICATION_API}/portal`, {
|
|
3295
|
+
method: "POST",
|
|
3296
|
+
headers: { "Content-Type": "application/json" },
|
|
3297
|
+
body: JSON.stringify({ customerId: premium.customerId })
|
|
3298
|
+
});
|
|
3299
|
+
if (!res.ok) {
|
|
3300
|
+
return { ok: false, error: `HTTP ${res.status}` };
|
|
3261
3301
|
}
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3302
|
+
const { url } = await res.json();
|
|
3303
|
+
return { ok: true, url };
|
|
3304
|
+
} catch (err) {
|
|
3305
|
+
return {
|
|
3306
|
+
ok: false,
|
|
3307
|
+
error: err instanceof Error ? err.message : "Failed to get portal URL"
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
async function verifySubscription(emailOrCustomerId) {
|
|
3312
|
+
const current = loadSettings().premium;
|
|
3313
|
+
const identifier = emailOrCustomerId || current.customerId || current.email;
|
|
3314
|
+
if (!identifier) {
|
|
3315
|
+
return current;
|
|
3316
|
+
}
|
|
3317
|
+
try {
|
|
3318
|
+
const res = await fetch(`${VERIFICATION_API}/verify`, {
|
|
3319
|
+
method: "POST",
|
|
3320
|
+
headers: { "Content-Type": "application/json" },
|
|
3321
|
+
body: JSON.stringify({ identifier })
|
|
3322
|
+
});
|
|
3323
|
+
if (!res.ok) {
|
|
3324
|
+
console.warn("[Vessel Premium] Verification API returned", res.status);
|
|
3325
|
+
return current;
|
|
3268
3326
|
}
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3327
|
+
const data = await res.json();
|
|
3328
|
+
const updated = {
|
|
3329
|
+
status: data.status,
|
|
3330
|
+
customerId: data.customerId,
|
|
3331
|
+
email: data.email,
|
|
3332
|
+
validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3333
|
+
expiresAt: data.expiresAt
|
|
3334
|
+
};
|
|
3335
|
+
setSetting("premium", updated);
|
|
3336
|
+
return updated;
|
|
3337
|
+
} catch (err) {
|
|
3338
|
+
console.warn("[Vessel Premium] Verification failed:", err);
|
|
3339
|
+
return current;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
async function activateWithEmail(email) {
|
|
3343
|
+
if (!email.trim()) {
|
|
3344
|
+
return { ok: false, state: getPremiumState(), error: "Email is required" };
|
|
3345
|
+
}
|
|
3346
|
+
const state2 = await verifySubscription(email.trim());
|
|
3347
|
+
if (state2.status === "active" || state2.status === "trialing") {
|
|
3348
|
+
return { ok: true, state: state2 };
|
|
3349
|
+
}
|
|
3350
|
+
return {
|
|
3351
|
+
ok: false,
|
|
3352
|
+
state: state2,
|
|
3353
|
+
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."
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
let revalidationTimer = null;
|
|
3357
|
+
function startBackgroundRevalidation() {
|
|
3358
|
+
if (revalidationTimer) return;
|
|
3359
|
+
const { premium } = loadSettings();
|
|
3360
|
+
if (premium.customerId || premium.email) {
|
|
3361
|
+
const lastValidated = premium.validatedAt ? new Date(premium.validatedAt).getTime() : 0;
|
|
3362
|
+
if (Date.now() - lastValidated > REVALIDATION_INTERVAL_MS) {
|
|
3363
|
+
void verifySubscription();
|
|
3277
3364
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
return uniqueSelector(candidate);
|
|
3365
|
+
}
|
|
3366
|
+
revalidationTimer = setInterval(() => {
|
|
3367
|
+
const { premium: p } = loadSettings();
|
|
3368
|
+
if (p.customerId || p.email) {
|
|
3369
|
+
void verifySubscription();
|
|
3284
3370
|
}
|
|
3285
|
-
|
|
3371
|
+
}, REVALIDATION_INTERVAL_MS);
|
|
3372
|
+
}
|
|
3373
|
+
function stopBackgroundRevalidation() {
|
|
3374
|
+
if (revalidationTimer) {
|
|
3375
|
+
clearInterval(revalidationTimer);
|
|
3376
|
+
revalidationTimer = null;
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || "phc_OMeM3P5cxJwl14lOKxYad0Yre52xvjNfkLEFnPtXyM";
|
|
3380
|
+
const POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
|
|
3381
|
+
const BATCH_INTERVAL_MS = 6e4;
|
|
3382
|
+
const MAX_BATCH_SIZE = 50;
|
|
3383
|
+
function getDeviceIdPath() {
|
|
3384
|
+
return path.join(electron.app.getPath("userData"), ".vessel-device-id");
|
|
3385
|
+
}
|
|
3386
|
+
let deviceId = null;
|
|
3387
|
+
function getDeviceId() {
|
|
3388
|
+
if (deviceId) return deviceId;
|
|
3389
|
+
const idPath = getDeviceIdPath();
|
|
3390
|
+
try {
|
|
3391
|
+
deviceId = fs.readFileSync(idPath, "utf-8").trim();
|
|
3392
|
+
if (deviceId) return deviceId;
|
|
3393
|
+
} catch {
|
|
3394
|
+
}
|
|
3395
|
+
deviceId = crypto.randomUUID();
|
|
3396
|
+
try {
|
|
3397
|
+
fs.mkdirSync(path.dirname(idPath), { recursive: true });
|
|
3398
|
+
fs.writeFileSync(idPath, deviceId, "utf-8");
|
|
3399
|
+
} catch {
|
|
3400
|
+
}
|
|
3401
|
+
return deviceId;
|
|
3402
|
+
}
|
|
3403
|
+
let eventQueue = [];
|
|
3404
|
+
let flushTimer = null;
|
|
3405
|
+
let sessionStartedAt = null;
|
|
3406
|
+
function isEnabled() {
|
|
3407
|
+
if (POSTHOG_API_KEY === "YOUR_POSTHOG_KEY_HERE") return false;
|
|
3408
|
+
return loadSettings().telemetryEnabled !== false;
|
|
3409
|
+
}
|
|
3410
|
+
function trackEvent(event, properties = {}) {
|
|
3411
|
+
if (!isEnabled()) return;
|
|
3412
|
+
eventQueue.push({
|
|
3413
|
+
event,
|
|
3414
|
+
properties: {
|
|
3415
|
+
...properties,
|
|
3416
|
+
premium_status: isPremium() ? "premium" : "free",
|
|
3417
|
+
app_version: electron.app.getVersion(),
|
|
3418
|
+
platform: process.platform,
|
|
3419
|
+
arch: process.arch
|
|
3420
|
+
},
|
|
3421
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3422
|
+
});
|
|
3423
|
+
if (eventQueue.length >= MAX_BATCH_SIZE) {
|
|
3424
|
+
void flush();
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
function trackToolCall(toolName, pageType) {
|
|
3428
|
+
trackEvent("tool_called", {
|
|
3429
|
+
tool_name: toolName,
|
|
3430
|
+
page_type: "unknown"
|
|
3431
|
+
});
|
|
3432
|
+
}
|
|
3433
|
+
function trackProviderConfigured(providerId) {
|
|
3434
|
+
trackEvent("provider_configured", {
|
|
3435
|
+
provider_id: providerId
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
function trackSettingChanged(key) {
|
|
3439
|
+
trackEvent("setting_changed", { setting_key: key });
|
|
3440
|
+
}
|
|
3441
|
+
function trackApprovalModeChanged(mode) {
|
|
3442
|
+
trackEvent("approval_mode_changed", { mode });
|
|
3443
|
+
}
|
|
3444
|
+
function trackBookmarkAction(action) {
|
|
3445
|
+
trackEvent("bookmark_action", { action });
|
|
3446
|
+
}
|
|
3447
|
+
function trackVaultAction(action) {
|
|
3448
|
+
trackEvent("vault_action", { action });
|
|
3449
|
+
}
|
|
3450
|
+
function trackExtractionFailed(domain, reason) {
|
|
3451
|
+
trackEvent("extraction_failed", { domain, reason });
|
|
3452
|
+
}
|
|
3453
|
+
function trackPremiumFunnel(step, context) {
|
|
3454
|
+
trackEvent("premium_funnel", { step, ...context });
|
|
3455
|
+
}
|
|
3456
|
+
function startTelemetry() {
|
|
3457
|
+
if (!isEnabled()) return;
|
|
3458
|
+
sessionStartedAt = Date.now();
|
|
3459
|
+
trackEvent("app_launched", {
|
|
3460
|
+
electron_version: process.versions.electron,
|
|
3461
|
+
chrome_version: process.versions.chrome
|
|
3462
|
+
});
|
|
3463
|
+
flushTimer = setInterval(() => {
|
|
3464
|
+
void flush();
|
|
3465
|
+
}, BATCH_INTERVAL_MS);
|
|
3466
|
+
}
|
|
3467
|
+
function stopTelemetry() {
|
|
3468
|
+
if (sessionStartedAt) {
|
|
3469
|
+
const durationMinutes = Math.round(
|
|
3470
|
+
(Date.now() - sessionStartedAt) / 6e4
|
|
3471
|
+
);
|
|
3472
|
+
trackEvent("app_session_ended", {
|
|
3473
|
+
duration_minutes: durationMinutes
|
|
3474
|
+
});
|
|
3475
|
+
sessionStartedAt = null;
|
|
3476
|
+
}
|
|
3477
|
+
if (flushTimer) {
|
|
3478
|
+
clearInterval(flushTimer);
|
|
3479
|
+
flushTimer = null;
|
|
3480
|
+
}
|
|
3481
|
+
void flush();
|
|
3482
|
+
}
|
|
3483
|
+
async function flush() {
|
|
3484
|
+
if (eventQueue.length === 0) return;
|
|
3485
|
+
if (!isEnabled()) {
|
|
3486
|
+
eventQueue = [];
|
|
3487
|
+
return;
|
|
3488
|
+
}
|
|
3489
|
+
const batch = eventQueue.splice(0);
|
|
3490
|
+
const distinctId = getDeviceId();
|
|
3491
|
+
const payload = {
|
|
3492
|
+
api_key: POSTHOG_API_KEY,
|
|
3493
|
+
batch: batch.map((e) => ({
|
|
3494
|
+
event: e.event,
|
|
3495
|
+
properties: {
|
|
3496
|
+
distinct_id: distinctId,
|
|
3497
|
+
...e.properties
|
|
3498
|
+
},
|
|
3499
|
+
timestamp: e.timestamp
|
|
3500
|
+
}))
|
|
3501
|
+
};
|
|
3502
|
+
try {
|
|
3503
|
+
await fetch(`${POSTHOG_HOST}/batch`, {
|
|
3504
|
+
method: "POST",
|
|
3505
|
+
headers: { "Content-Type": "application/json" },
|
|
3506
|
+
body: JSON.stringify(payload)
|
|
3507
|
+
});
|
|
3508
|
+
} catch {
|
|
3509
|
+
if (eventQueue.length < MAX_BATCH_SIZE * 2) {
|
|
3510
|
+
eventQueue.unshift(...batch);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
const EMPTY_PAGE_CONTENT = {
|
|
3515
|
+
title: "",
|
|
3516
|
+
content: "",
|
|
3517
|
+
htmlContent: "",
|
|
3518
|
+
byline: "",
|
|
3519
|
+
excerpt: "",
|
|
3520
|
+
url: "",
|
|
3521
|
+
headings: [],
|
|
3522
|
+
navigation: [],
|
|
3523
|
+
interactiveElements: [],
|
|
3524
|
+
forms: [],
|
|
3525
|
+
viewport: {
|
|
3526
|
+
width: 0,
|
|
3527
|
+
height: 0,
|
|
3528
|
+
scrollX: 0,
|
|
3529
|
+
scrollY: 0
|
|
3530
|
+
},
|
|
3531
|
+
overlays: [],
|
|
3532
|
+
dormantOverlays: [],
|
|
3533
|
+
landmarks: [],
|
|
3534
|
+
jsonLd: [],
|
|
3535
|
+
microdata: [],
|
|
3536
|
+
rdfa: [],
|
|
3537
|
+
metaTags: {},
|
|
3538
|
+
structuredData: [],
|
|
3539
|
+
pageIssues: []
|
|
3540
|
+
};
|
|
3541
|
+
const PRELOAD_EXTRACTION_SCRIPT = String.raw`
|
|
3542
|
+
(function() {
|
|
3543
|
+
try {
|
|
3544
|
+
if (window.__vessel && typeof window.__vessel.extractContent === "function") {
|
|
3545
|
+
const structured = window.__vessel.extractContent();
|
|
3546
|
+
if (structured && typeof structured === "object") {
|
|
3547
|
+
return structured;
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
} catch (_error) {
|
|
3551
|
+
}
|
|
3552
|
+
return null;
|
|
3553
|
+
})()
|
|
3554
|
+
`;
|
|
3555
|
+
const DIRECT_EXTRACTION_SCRIPT = String.raw`
|
|
3556
|
+
(function() {
|
|
3557
|
+
// Time budget: stop expensive DOM traversals after this many ms so heavy
|
|
3558
|
+
// pages (Newegg, Wikipedia, etc.) don't stall the agent for 30-60s+.
|
|
3559
|
+
var BUDGET_MS = 8000;
|
|
3560
|
+
var _budgetStart = performance.now();
|
|
3561
|
+
function withinBudget() {
|
|
3562
|
+
return (performance.now() - _budgetStart) < BUDGET_MS;
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
function getCleanBodyText() {
|
|
3566
|
+
var removed = [];
|
|
3567
|
+
document
|
|
3568
|
+
.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]')
|
|
3569
|
+
.forEach(function(label) {
|
|
3570
|
+
var parent = label.parentNode;
|
|
3571
|
+
if (!parent) return;
|
|
3572
|
+
removed.push({ label: label, parent: parent, nextSibling: label.nextSibling });
|
|
3573
|
+
parent.removeChild(label);
|
|
3574
|
+
});
|
|
3575
|
+
try {
|
|
3576
|
+
return document.body?.innerText || document.documentElement?.innerText || "";
|
|
3577
|
+
} finally {
|
|
3578
|
+
for (var i = removed.length - 1; i >= 0; i--) {
|
|
3579
|
+
var entry = removed[i];
|
|
3580
|
+
entry.parent.insertBefore(entry.label, entry.nextSibling);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
function text(value) {
|
|
3586
|
+
const trimmed = value == null ? "" : String(value).trim();
|
|
3587
|
+
return trimmed || undefined;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
function escapeSelectorValue(value) {
|
|
3591
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
|
|
3592
|
+
return CSS.escape(value);
|
|
3593
|
+
}
|
|
3594
|
+
return String(value).replace(/["\\]/g, "\\$&");
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
function uniqueSelector(candidate) {
|
|
3598
|
+
if (!candidate) return null;
|
|
3599
|
+
try {
|
|
3600
|
+
return document.querySelectorAll(candidate).length === 1 ? candidate : null;
|
|
3601
|
+
} catch {
|
|
3602
|
+
return null;
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
function uniqueAttributeSelector(el, attribute) {
|
|
3607
|
+
const value = text(el.getAttribute && el.getAttribute(attribute));
|
|
3608
|
+
if (!value) return null;
|
|
3609
|
+
const candidate = el.tagName.toLowerCase() + "[" + attribute + "=\"" + escapeSelectorValue(value) + "\"]";
|
|
3610
|
+
return uniqueSelector(candidate);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3286
3613
|
function selectorFor(el) {
|
|
3287
3614
|
if (!el) return "";
|
|
3288
3615
|
if (el.id) return "#" + escapeSelectorValue(el.id);
|
|
@@ -4088,7 +4415,7 @@ const SAFE_EXTRACTION_SCRIPT = String.raw`
|
|
|
4088
4415
|
function delay(ms) {
|
|
4089
4416
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4090
4417
|
}
|
|
4091
|
-
const EXECUTE_SCRIPT_TIMEOUT_MS =
|
|
4418
|
+
const EXECUTE_SCRIPT_TIMEOUT_MS = 3e3;
|
|
4092
4419
|
async function waitForDomReady(webContents, timeoutMs = 1500) {
|
|
4093
4420
|
const deadline = Date.now() + timeoutMs;
|
|
4094
4421
|
while (Date.now() < deadline) {
|
|
@@ -4204,7 +4531,25 @@ function mergePageContent(candidates, webContents) {
|
|
|
4204
4531
|
url: mergedBase.url || webContents.getURL() || ""
|
|
4205
4532
|
};
|
|
4206
4533
|
}
|
|
4207
|
-
const
|
|
4534
|
+
const EXTRACT_TIMEOUT_BASE_MS = 12e3;
|
|
4535
|
+
const EXTRACT_TIMEOUT_MAX_MS = 2e4;
|
|
4536
|
+
async function estimateExtractionTimeout(webContents) {
|
|
4537
|
+
try {
|
|
4538
|
+
const elementCount = await executeScript(
|
|
4539
|
+
webContents,
|
|
4540
|
+
`(function() { try { return document.querySelectorAll('*').length; } catch { return 0; } })()`
|
|
4541
|
+
);
|
|
4542
|
+
if (typeof elementCount === "number" && elementCount > 5e3) {
|
|
4543
|
+
const extra = Math.min(
|
|
4544
|
+
EXTRACT_TIMEOUT_MAX_MS - EXTRACT_TIMEOUT_BASE_MS,
|
|
4545
|
+
Math.ceil((elementCount - 5e3) / 2e3) * 1e3
|
|
4546
|
+
);
|
|
4547
|
+
return EXTRACT_TIMEOUT_BASE_MS + extra;
|
|
4548
|
+
}
|
|
4549
|
+
} catch {
|
|
4550
|
+
}
|
|
4551
|
+
return EXTRACT_TIMEOUT_BASE_MS;
|
|
4552
|
+
}
|
|
4208
4553
|
async function extractContentInner(webContents) {
|
|
4209
4554
|
await waitForDomReady(webContents);
|
|
4210
4555
|
const [preloadResult, directResult, safeResult] = await Promise.all([
|
|
@@ -4219,20 +4564,29 @@ async function extractContentInner(webContents) {
|
|
|
4219
4564
|
}
|
|
4220
4565
|
async function extractContent(webContents) {
|
|
4221
4566
|
try {
|
|
4567
|
+
const timeoutMs = await estimateExtractionTimeout(webContents);
|
|
4222
4568
|
return await Promise.race([
|
|
4223
4569
|
extractContentInner(webContents),
|
|
4224
4570
|
new Promise(
|
|
4225
4571
|
(_, reject) => setTimeout(
|
|
4226
4572
|
() => reject(new Error("extractContent timeout")),
|
|
4227
|
-
|
|
4573
|
+
timeoutMs
|
|
4228
4574
|
)
|
|
4229
4575
|
)
|
|
4230
4576
|
]);
|
|
4231
|
-
} catch {
|
|
4577
|
+
} catch (err) {
|
|
4578
|
+
const url = webContents.getURL() || "";
|
|
4579
|
+
let domain = "unknown";
|
|
4580
|
+
try {
|
|
4581
|
+
domain = new URL(url).hostname;
|
|
4582
|
+
} catch {
|
|
4583
|
+
}
|
|
4584
|
+
const reason = err instanceof Error ? err.message : "unknown";
|
|
4585
|
+
trackExtractionFailed(domain, reason);
|
|
4232
4586
|
return {
|
|
4233
4587
|
...EMPTY_PAGE_CONTENT,
|
|
4234
4588
|
title: webContents.getTitle() || "",
|
|
4235
|
-
url
|
|
4589
|
+
url
|
|
4236
4590
|
};
|
|
4237
4591
|
}
|
|
4238
4592
|
}
|
|
@@ -4411,286 +4765,205 @@ function setMcpHealth(update) {
|
|
|
4411
4765
|
}
|
|
4412
4766
|
}
|
|
4413
4767
|
}
|
|
4414
|
-
const
|
|
4415
|
-
const
|
|
4416
|
-
const
|
|
4417
|
-
const
|
|
4418
|
-
const
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
"
|
|
4422
|
-
"list_sessions",
|
|
4423
|
-
"delete_session",
|
|
4424
|
-
"flow_start",
|
|
4425
|
-
"flow_advance",
|
|
4426
|
-
"flow_status",
|
|
4427
|
-
"flow_end",
|
|
4428
|
-
"metrics",
|
|
4429
|
-
"extract_table"
|
|
4430
|
-
]);
|
|
4431
|
-
function isPremium() {
|
|
4432
|
-
const { premium } = loadSettings();
|
|
4433
|
-
if (premium.status === "active" || premium.status === "trialing") {
|
|
4434
|
-
return true;
|
|
4435
|
-
}
|
|
4436
|
-
if (premium.validatedAt && premium.status !== "free") {
|
|
4437
|
-
const lastValidated = new Date(premium.validatedAt).getTime();
|
|
4438
|
-
if (Date.now() - lastValidated < OFFLINE_GRACE_PERIOD_MS) {
|
|
4439
|
-
return true;
|
|
4440
|
-
}
|
|
4441
|
-
}
|
|
4442
|
-
return false;
|
|
4443
|
-
}
|
|
4444
|
-
function getPremiumState() {
|
|
4445
|
-
return { ...loadSettings().premium };
|
|
4768
|
+
const VAULT_FILENAME = "vessel-vault.enc";
|
|
4769
|
+
const KEY_FILENAME = "vessel-vault.key";
|
|
4770
|
+
const ALGORITHM = "aes-256-gcm";
|
|
4771
|
+
const IV_LENGTH = 12;
|
|
4772
|
+
const AUTH_TAG_LENGTH = 16;
|
|
4773
|
+
let cachedEntries = null;
|
|
4774
|
+
function getVaultDir() {
|
|
4775
|
+
return electron.app.getPath("userData");
|
|
4446
4776
|
}
|
|
4447
|
-
function
|
|
4448
|
-
|
|
4449
|
-
return loadSettings().maxToolIterations || 200;
|
|
4450
|
-
}
|
|
4451
|
-
return FREE_TOOL_ITERATION_LIMIT;
|
|
4777
|
+
function getVaultPath() {
|
|
4778
|
+
return path$1.join(getVaultDir(), VAULT_FILENAME);
|
|
4452
4779
|
}
|
|
4453
|
-
function
|
|
4454
|
-
|
|
4455
|
-
status: "free",
|
|
4456
|
-
customerId: "",
|
|
4457
|
-
email: "",
|
|
4458
|
-
validatedAt: "",
|
|
4459
|
-
expiresAt: ""
|
|
4460
|
-
};
|
|
4461
|
-
setSetting("premium", fresh);
|
|
4462
|
-
return fresh;
|
|
4463
|
-
}
|
|
4464
|
-
function isToolGated(toolName) {
|
|
4465
|
-
return PREMIUM_TOOLS.has(toolName) && !isPremium();
|
|
4780
|
+
function getKeyPath() {
|
|
4781
|
+
return path$1.join(getVaultDir(), KEY_FILENAME);
|
|
4466
4782
|
}
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
headers: { "Content-Type": "application/json" }
|
|
4474
|
-
});
|
|
4475
|
-
if (!res.ok) {
|
|
4476
|
-
const body = await res.text();
|
|
4477
|
-
return { ok: false, error: body || `HTTP ${res.status}` };
|
|
4783
|
+
function getOrCreateEncryptionKey() {
|
|
4784
|
+
const keyPath = getKeyPath();
|
|
4785
|
+
if (fs$1.existsSync(keyPath)) {
|
|
4786
|
+
const encryptedKey = fs$1.readFileSync(keyPath);
|
|
4787
|
+
if (electron.safeStorage.isEncryptionAvailable()) {
|
|
4788
|
+
return electron.safeStorage.decryptString ? Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8") : encryptedKey;
|
|
4478
4789
|
}
|
|
4479
|
-
|
|
4480
|
-
return { ok: true, url };
|
|
4481
|
-
} catch (err) {
|
|
4482
|
-
return {
|
|
4483
|
-
ok: false,
|
|
4484
|
-
error: err instanceof Error ? err.message : "Failed to create checkout"
|
|
4485
|
-
};
|
|
4486
|
-
}
|
|
4487
|
-
}
|
|
4488
|
-
async function getPortalUrl() {
|
|
4489
|
-
const { premium } = loadSettings();
|
|
4490
|
-
if (!premium.customerId) {
|
|
4491
|
-
return { ok: false, error: "No active subscription" };
|
|
4790
|
+
return encryptedKey;
|
|
4492
4791
|
}
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
}
|
|
4502
|
-
const { url } = await res.json();
|
|
4503
|
-
return { ok: true, url };
|
|
4504
|
-
} catch (err) {
|
|
4505
|
-
return {
|
|
4506
|
-
ok: false,
|
|
4507
|
-
error: err instanceof Error ? err.message : "Failed to get portal URL"
|
|
4508
|
-
};
|
|
4792
|
+
const key = crypto$1.randomBytes(32);
|
|
4793
|
+
fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
|
|
4794
|
+
if (electron.safeStorage.isEncryptionAvailable()) {
|
|
4795
|
+
const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
|
|
4796
|
+
fs$1.writeFileSync(keyPath, encrypted);
|
|
4797
|
+
} else {
|
|
4798
|
+
fs$1.writeFileSync(keyPath, key);
|
|
4799
|
+
fs$1.chmodSync(keyPath, 384);
|
|
4509
4800
|
}
|
|
4801
|
+
return key;
|
|
4510
4802
|
}
|
|
4511
|
-
|
|
4512
|
-
const
|
|
4513
|
-
const
|
|
4514
|
-
|
|
4515
|
-
|
|
4803
|
+
function encrypt(plaintext) {
|
|
4804
|
+
const key = getOrCreateEncryptionKey();
|
|
4805
|
+
const iv = crypto$1.randomBytes(IV_LENGTH);
|
|
4806
|
+
const cipher = crypto$1.createCipheriv(ALGORITHM, key, iv, {
|
|
4807
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
4808
|
+
});
|
|
4809
|
+
const encrypted = Buffer.concat([
|
|
4810
|
+
cipher.update(plaintext, "utf-8"),
|
|
4811
|
+
cipher.final()
|
|
4812
|
+
]);
|
|
4813
|
+
const authTag = cipher.getAuthTag();
|
|
4814
|
+
return Buffer.concat([iv, authTag, encrypted]);
|
|
4815
|
+
}
|
|
4816
|
+
function decrypt(data) {
|
|
4817
|
+
const key = getOrCreateEncryptionKey();
|
|
4818
|
+
const iv = data.subarray(0, IV_LENGTH);
|
|
4819
|
+
const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
|
|
4820
|
+
const ciphertext = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
|
|
4821
|
+
const decipher = crypto$1.createDecipheriv(ALGORITHM, key, iv, {
|
|
4822
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
4823
|
+
});
|
|
4824
|
+
decipher.setAuthTag(authTag);
|
|
4825
|
+
return decipher.update(ciphertext, void 0, "utf-8") + decipher.final("utf-8");
|
|
4826
|
+
}
|
|
4827
|
+
function loadVault() {
|
|
4828
|
+
if (cachedEntries) return cachedEntries;
|
|
4829
|
+
const vaultPath = getVaultPath();
|
|
4830
|
+
if (!fs$1.existsSync(vaultPath)) {
|
|
4831
|
+
cachedEntries = [];
|
|
4832
|
+
return cachedEntries;
|
|
4516
4833
|
}
|
|
4517
4834
|
try {
|
|
4518
|
-
const
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
});
|
|
4523
|
-
if (!res.ok) {
|
|
4524
|
-
console.warn("[Vessel Premium] Verification API returned", res.status);
|
|
4525
|
-
return current;
|
|
4526
|
-
}
|
|
4527
|
-
const data = await res.json();
|
|
4528
|
-
const updated = {
|
|
4529
|
-
status: data.status,
|
|
4530
|
-
customerId: data.customerId,
|
|
4531
|
-
email: data.email,
|
|
4532
|
-
validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4533
|
-
expiresAt: data.expiresAt
|
|
4534
|
-
};
|
|
4535
|
-
setSetting("premium", updated);
|
|
4536
|
-
return updated;
|
|
4835
|
+
const raw = fs$1.readFileSync(vaultPath);
|
|
4836
|
+
const json = decrypt(raw);
|
|
4837
|
+
cachedEntries = JSON.parse(json);
|
|
4838
|
+
return cachedEntries;
|
|
4537
4839
|
} catch (err) {
|
|
4538
|
-
console.
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
}
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
const
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
const { premium } = loadSettings();
|
|
4560
|
-
if (premium.customerId || premium.email) {
|
|
4561
|
-
const lastValidated = premium.validatedAt ? new Date(premium.validatedAt).getTime() : 0;
|
|
4562
|
-
if (Date.now() - lastValidated > REVALIDATION_INTERVAL_MS) {
|
|
4563
|
-
void verifySubscription();
|
|
4564
|
-
}
|
|
4565
|
-
}
|
|
4566
|
-
revalidationTimer = setInterval(() => {
|
|
4567
|
-
const { premium: p } = loadSettings();
|
|
4568
|
-
if (p.customerId || p.email) {
|
|
4569
|
-
void verifySubscription();
|
|
4570
|
-
}
|
|
4571
|
-
}, REVALIDATION_INTERVAL_MS);
|
|
4572
|
-
}
|
|
4573
|
-
function stopBackgroundRevalidation() {
|
|
4574
|
-
if (revalidationTimer) {
|
|
4575
|
-
clearInterval(revalidationTimer);
|
|
4576
|
-
revalidationTimer = null;
|
|
4840
|
+
console.error("[Vessel Vault] Failed to load vault:", err);
|
|
4841
|
+
cachedEntries = [];
|
|
4842
|
+
return cachedEntries;
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
function saveVault(entries) {
|
|
4846
|
+
const json = JSON.stringify(entries, null, 2);
|
|
4847
|
+
const encrypted = encrypt(json);
|
|
4848
|
+
const vaultPath = getVaultPath();
|
|
4849
|
+
fs$1.mkdirSync(path$1.dirname(vaultPath), { recursive: true });
|
|
4850
|
+
fs$1.writeFileSync(vaultPath, encrypted);
|
|
4851
|
+
fs$1.chmodSync(vaultPath, 384);
|
|
4852
|
+
cachedEntries = entries;
|
|
4853
|
+
}
|
|
4854
|
+
function domainMatches(pattern, hostname) {
|
|
4855
|
+
const p = pattern.toLowerCase().trim();
|
|
4856
|
+
const h = hostname.toLowerCase().trim();
|
|
4857
|
+
if (p === h) return true;
|
|
4858
|
+
if (p.startsWith("*.")) {
|
|
4859
|
+
const suffix = p.slice(2);
|
|
4860
|
+
return h === suffix || h.endsWith("." + suffix);
|
|
4577
4861
|
}
|
|
4862
|
+
return false;
|
|
4578
4863
|
}
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
const BATCH_INTERVAL_MS = 6e4;
|
|
4582
|
-
const MAX_BATCH_SIZE = 50;
|
|
4583
|
-
function getDeviceIdPath() {
|
|
4584
|
-
return path.join(electron.app.getPath("userData"), ".vessel-device-id");
|
|
4864
|
+
function listEntries() {
|
|
4865
|
+
return loadVault().map(({ password, totpSecret, ...rest }) => rest);
|
|
4585
4866
|
}
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
if (deviceId) return deviceId;
|
|
4589
|
-
const idPath = getDeviceIdPath();
|
|
4590
|
-
try {
|
|
4591
|
-
deviceId = fs.readFileSync(idPath, "utf-8").trim();
|
|
4592
|
-
if (deviceId) return deviceId;
|
|
4593
|
-
} catch {
|
|
4594
|
-
}
|
|
4595
|
-
deviceId = crypto.randomUUID();
|
|
4867
|
+
function findEntriesForDomain(url) {
|
|
4868
|
+
let hostname;
|
|
4596
4869
|
try {
|
|
4597
|
-
|
|
4598
|
-
fs.writeFileSync(idPath, deviceId, "utf-8");
|
|
4870
|
+
hostname = new URL(url).hostname;
|
|
4599
4871
|
} catch {
|
|
4872
|
+
return [];
|
|
4600
4873
|
}
|
|
4601
|
-
return
|
|
4602
|
-
}
|
|
4603
|
-
let eventQueue = [];
|
|
4604
|
-
let flushTimer = null;
|
|
4605
|
-
let sessionStartedAt = null;
|
|
4606
|
-
function isEnabled() {
|
|
4607
|
-
if (POSTHOG_API_KEY === "YOUR_POSTHOG_KEY_HERE") return false;
|
|
4608
|
-
return loadSettings().telemetryEnabled !== false;
|
|
4609
|
-
}
|
|
4610
|
-
function trackEvent(event, properties = {}) {
|
|
4611
|
-
if (!isEnabled()) return;
|
|
4612
|
-
eventQueue.push({
|
|
4613
|
-
event,
|
|
4614
|
-
properties: {
|
|
4615
|
-
...properties,
|
|
4616
|
-
premium_status: isPremium() ? "premium" : "free",
|
|
4617
|
-
app_version: electron.app.getVersion(),
|
|
4618
|
-
platform: process.platform,
|
|
4619
|
-
arch: process.arch
|
|
4620
|
-
},
|
|
4621
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4622
|
-
});
|
|
4623
|
-
if (eventQueue.length >= MAX_BATCH_SIZE) {
|
|
4624
|
-
void flush();
|
|
4625
|
-
}
|
|
4626
|
-
}
|
|
4627
|
-
function trackToolCall(toolName, pageType) {
|
|
4628
|
-
trackEvent("tool_called", {
|
|
4629
|
-
tool_name: toolName,
|
|
4630
|
-
page_type: "unknown"
|
|
4631
|
-
});
|
|
4632
|
-
}
|
|
4633
|
-
function trackProviderConfigured(providerId) {
|
|
4634
|
-
trackEvent("provider_configured", {
|
|
4635
|
-
provider_id: providerId
|
|
4636
|
-
});
|
|
4874
|
+
return loadVault().filter((e) => domainMatches(e.domainPattern, hostname)).map(({ password, totpSecret, ...rest }) => rest);
|
|
4637
4875
|
}
|
|
4638
|
-
function
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4876
|
+
function addEntry(entry) {
|
|
4877
|
+
const entries = loadVault();
|
|
4878
|
+
const newEntry = {
|
|
4879
|
+
...entry,
|
|
4880
|
+
id: crypto$1.randomUUID(),
|
|
4881
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4882
|
+
useCount: 0
|
|
4883
|
+
};
|
|
4884
|
+
entries.push(newEntry);
|
|
4885
|
+
saveVault(entries);
|
|
4886
|
+
return newEntry;
|
|
4887
|
+
}
|
|
4888
|
+
function updateEntry(id, updates) {
|
|
4889
|
+
const entries = loadVault();
|
|
4890
|
+
const idx = entries.findIndex((e) => e.id === id);
|
|
4891
|
+
if (idx === -1) return null;
|
|
4892
|
+
entries[idx] = { ...entries[idx], ...updates };
|
|
4893
|
+
saveVault(entries);
|
|
4894
|
+
return entries[idx];
|
|
4895
|
+
}
|
|
4896
|
+
function removeEntry(id) {
|
|
4897
|
+
const entries = loadVault();
|
|
4898
|
+
const idx = entries.findIndex((e) => e.id === id);
|
|
4899
|
+
if (idx === -1) return false;
|
|
4900
|
+
entries.splice(idx, 1);
|
|
4901
|
+
saveVault(entries);
|
|
4902
|
+
return true;
|
|
4648
4903
|
}
|
|
4649
|
-
function
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
if (
|
|
4660
|
-
|
|
4661
|
-
|
|
4904
|
+
function recordUsage(id) {
|
|
4905
|
+
const entries = loadVault();
|
|
4906
|
+
const entry = entries.find((e) => e.id === id);
|
|
4907
|
+
if (!entry) return;
|
|
4908
|
+
entry.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4909
|
+
entry.useCount += 1;
|
|
4910
|
+
saveVault(entries);
|
|
4911
|
+
}
|
|
4912
|
+
function getCredential(id) {
|
|
4913
|
+
const entry = loadVault().find((e) => e.id === id);
|
|
4914
|
+
if (!entry) return null;
|
|
4915
|
+
return { username: entry.username, password: entry.password };
|
|
4916
|
+
}
|
|
4917
|
+
function getTotpSecret(id) {
|
|
4918
|
+
const entry = loadVault().find((e) => e.id === id);
|
|
4919
|
+
return entry?.totpSecret ?? null;
|
|
4920
|
+
}
|
|
4921
|
+
function generateTotpCode(secret) {
|
|
4922
|
+
const epoch = Math.floor(Date.now() / 1e3);
|
|
4923
|
+
const counter = Math.floor(epoch / 30);
|
|
4924
|
+
const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
4925
|
+
const cleanSecret = secret.replace(/[\s=-]/g, "").toUpperCase();
|
|
4926
|
+
let bits = "";
|
|
4927
|
+
for (const ch of cleanSecret) {
|
|
4928
|
+
const val = base32Chars.indexOf(ch);
|
|
4929
|
+
if (val === -1) continue;
|
|
4930
|
+
bits += val.toString(2).padStart(5, "0");
|
|
4931
|
+
}
|
|
4932
|
+
const keyBytes = Buffer.alloc(Math.floor(bits.length / 8));
|
|
4933
|
+
for (let i = 0; i < keyBytes.length; i++) {
|
|
4934
|
+
keyBytes[i] = parseInt(bits.slice(i * 8, i * 8 + 8), 2);
|
|
4935
|
+
}
|
|
4936
|
+
const counterBuf = Buffer.alloc(8);
|
|
4937
|
+
counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
|
|
4938
|
+
counterBuf.writeUInt32BE(counter & 4294967295, 4);
|
|
4939
|
+
const hmac = crypto$1.createHmac("sha1", keyBytes).update(counterBuf).digest();
|
|
4940
|
+
const offset = hmac[hmac.length - 1] & 15;
|
|
4941
|
+
const code = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
|
|
4942
|
+
return (code % 1e6).toString().padStart(6, "0");
|
|
4943
|
+
}
|
|
4944
|
+
const AUDIT_FILENAME = "vessel-vault-audit.jsonl";
|
|
4945
|
+
const MAX_ENTRIES = 1e3;
|
|
4946
|
+
function getAuditPath() {
|
|
4947
|
+
return path$1.join(electron.app.getPath("userData"), AUDIT_FILENAME);
|
|
4948
|
+
}
|
|
4949
|
+
function appendAuditEntry(entry) {
|
|
4950
|
+
try {
|
|
4951
|
+
const auditPath = getAuditPath();
|
|
4952
|
+
fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
|
|
4953
|
+
fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
|
|
4954
|
+
} catch (err) {
|
|
4955
|
+
console.error("[Vessel Vault] Failed to write audit log:", err);
|
|
4662
4956
|
}
|
|
4663
|
-
void flush();
|
|
4664
4957
|
}
|
|
4665
|
-
|
|
4666
|
-
if (eventQueue.length === 0) return;
|
|
4667
|
-
if (!isEnabled()) {
|
|
4668
|
-
eventQueue = [];
|
|
4669
|
-
return;
|
|
4670
|
-
}
|
|
4671
|
-
const batch = eventQueue.splice(0);
|
|
4672
|
-
const distinctId = getDeviceId();
|
|
4673
|
-
const payload = {
|
|
4674
|
-
api_key: POSTHOG_API_KEY,
|
|
4675
|
-
batch: batch.map((e) => ({
|
|
4676
|
-
event: e.event,
|
|
4677
|
-
properties: {
|
|
4678
|
-
distinct_id: distinctId,
|
|
4679
|
-
...e.properties
|
|
4680
|
-
},
|
|
4681
|
-
timestamp: e.timestamp
|
|
4682
|
-
}))
|
|
4683
|
-
};
|
|
4958
|
+
function readAuditLog(limit = 100) {
|
|
4684
4959
|
try {
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
eventQueue.unshift(...batch);
|
|
4693
|
-
}
|
|
4960
|
+
const auditPath = getAuditPath();
|
|
4961
|
+
if (!fs$1.existsSync(auditPath)) return [];
|
|
4962
|
+
const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
|
|
4963
|
+
return lines.slice(-Math.min(limit, MAX_ENTRIES)).map((line) => JSON.parse(line)).reverse();
|
|
4964
|
+
} catch (err) {
|
|
4965
|
+
console.error("[Vessel Vault] Failed to read audit log:", err);
|
|
4966
|
+
return [];
|
|
4694
4967
|
}
|
|
4695
4968
|
}
|
|
4696
4969
|
function isRichToolResult(value) {
|
|
@@ -4811,7 +5084,8 @@ class AnthropicProvider {
|
|
|
4811
5084
|
toolUseBlocks.push({
|
|
4812
5085
|
id: currentToolUse.id,
|
|
4813
5086
|
name: currentToolUse.name,
|
|
4814
|
-
input: {}
|
|
5087
|
+
input: {},
|
|
5088
|
+
_malformedArgs: currentToolUse.inputJson
|
|
4815
5089
|
});
|
|
4816
5090
|
}
|
|
4817
5091
|
currentToolUse = null;
|
|
@@ -4839,6 +5113,18 @@ class AnthropicProvider {
|
|
|
4839
5113
|
}
|
|
4840
5114
|
const toolResults = [];
|
|
4841
5115
|
for (const tb of toolUseBlocks) {
|
|
5116
|
+
if (tb._malformedArgs !== void 0) {
|
|
5117
|
+
onChunk(`
|
|
5118
|
+
<<tool:${tb.name}:⚠ invalid args>>
|
|
5119
|
+
`);
|
|
5120
|
+
toolResults.push({
|
|
5121
|
+
type: "tool_result",
|
|
5122
|
+
tool_use_id: tb.id,
|
|
5123
|
+
content: `Error: Invalid JSON in tool arguments — could not parse. Please retry with valid JSON. Raw input: ${tb._malformedArgs.slice(0, 200)}`,
|
|
5124
|
+
is_error: true
|
|
5125
|
+
});
|
|
5126
|
+
continue;
|
|
5127
|
+
}
|
|
4842
5128
|
const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
|
|
4843
5129
|
onChunk(`
|
|
4844
5130
|
<<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
|
|
@@ -5103,10 +5389,12 @@ class OpenAICompatProvider {
|
|
|
5103
5389
|
for (const tc of Object.values(toolCallAccums)) {
|
|
5104
5390
|
if (!tc.id) tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5105
5391
|
}
|
|
5392
|
+
const malformedToolCalls = /* @__PURE__ */ new Set();
|
|
5106
5393
|
for (const tc of toolCalls) {
|
|
5107
5394
|
try {
|
|
5108
5395
|
JSON.parse(tc.argsJson || "{}");
|
|
5109
5396
|
} catch {
|
|
5397
|
+
malformedToolCalls.add(tc.id);
|
|
5110
5398
|
tc.argsJson = "{}";
|
|
5111
5399
|
}
|
|
5112
5400
|
}
|
|
@@ -5124,6 +5412,17 @@ class OpenAICompatProvider {
|
|
|
5124
5412
|
messages.push(assistantMsg);
|
|
5125
5413
|
if (toolCalls.length === 0) break;
|
|
5126
5414
|
for (const tc of toolCalls) {
|
|
5415
|
+
if (malformedToolCalls.has(tc.id)) {
|
|
5416
|
+
onChunk(`
|
|
5417
|
+
<<tool:${tc.name}:⚠ invalid args>>
|
|
5418
|
+
`);
|
|
5419
|
+
messages.push({
|
|
5420
|
+
role: "tool",
|
|
5421
|
+
tool_call_id: tc.id,
|
|
5422
|
+
content: `Error: Invalid JSON in tool arguments. The arguments could not be parsed. Please retry with valid JSON.`
|
|
5423
|
+
});
|
|
5424
|
+
continue;
|
|
5425
|
+
}
|
|
5127
5426
|
let args = {};
|
|
5128
5427
|
try {
|
|
5129
5428
|
args = JSON.parse(tc.argsJson || "{}");
|
|
@@ -5134,7 +5433,7 @@ class OpenAICompatProvider {
|
|
|
5134
5433
|
messages.push({
|
|
5135
5434
|
role: "tool",
|
|
5136
5435
|
tool_call_id: tc.id,
|
|
5137
|
-
content: `Error: Invalid JSON in tool arguments. Please retry with valid JSON
|
|
5436
|
+
content: `Error: Invalid JSON in tool arguments. Please retry with valid JSON.`
|
|
5138
5437
|
});
|
|
5139
5438
|
continue;
|
|
5140
5439
|
}
|
|
@@ -6856,9 +7155,12 @@ const TOOL_DEFINITIONS = [
|
|
|
6856
7155
|
{
|
|
6857
7156
|
name: "navigate",
|
|
6858
7157
|
title: "Navigate",
|
|
6859
|
-
description: "Navigate the browser to a URL.",
|
|
7158
|
+
description: "Navigate the browser to a URL. Use postBody to submit data via POST request (e.g. form submissions).",
|
|
6860
7159
|
inputSchema: {
|
|
6861
|
-
url: zod.z.string().describe("The URL to navigate to")
|
|
7160
|
+
url: zod.z.string().describe("The URL to navigate to"),
|
|
7161
|
+
postBody: zod.z.record(zod.z.string(), zod.z.string()).optional().describe(
|
|
7162
|
+
"Optional form fields to submit via POST (application/x-www-form-urlencoded). Only supported on http/https URLs."
|
|
7163
|
+
)
|
|
6862
7164
|
},
|
|
6863
7165
|
tier: 0
|
|
6864
7166
|
},
|
|
@@ -11137,7 +11439,7 @@ async function executeAction(name, args, ctx) {
|
|
|
11137
11439
|
const createdId = ctx.tabManager.createTab(
|
|
11138
11440
|
typeof args.url === "string" && args.url.trim() ? args.url.trim() : "about:blank"
|
|
11139
11441
|
);
|
|
11140
|
-
const created = ctx.tabManager.
|
|
11442
|
+
const created = ctx.tabManager.getTab(createdId);
|
|
11141
11443
|
if (created) {
|
|
11142
11444
|
await waitForLoad$1(created.view.webContents);
|
|
11143
11445
|
return `Created tab ${createdId}${await getPostNavSummary(created.view.webContents)}`;
|
|
@@ -11150,7 +11452,8 @@ async function executeAction(name, args, ctx) {
|
|
|
11150
11452
|
if (navValidation.status === "dead") {
|
|
11151
11453
|
return `Navigation blocked: ${args.url} returned ${navValidation.detail || "dead link"}. Try a different URL or go back and choose another link.`;
|
|
11152
11454
|
}
|
|
11153
|
-
ctx.tabManager.navigateTab(tabId, args.url);
|
|
11455
|
+
const navError = ctx.tabManager.navigateTab(tabId, args.url, args.postBody);
|
|
11456
|
+
if (navError) return navError;
|
|
11154
11457
|
await waitForLoad$1(wc);
|
|
11155
11458
|
return `Navigated to ${wc.getURL()}${await getPostNavSummary(wc)}`;
|
|
11156
11459
|
}
|
|
@@ -13183,6 +13486,41 @@ Exception: ${result.exceptionDetails}`);
|
|
|
13183
13486
|
}
|
|
13184
13487
|
);
|
|
13185
13488
|
}
|
|
13489
|
+
const sessionTrustedDomains = /* @__PURE__ */ new Set();
|
|
13490
|
+
async function requestConsent(request) {
|
|
13491
|
+
const domain = request.domain.toLowerCase();
|
|
13492
|
+
if (sessionTrustedDomains.has(domain)) {
|
|
13493
|
+
return { approved: true, trustForSession: true };
|
|
13494
|
+
}
|
|
13495
|
+
const focusedWindow = electron.BrowserWindow.getFocusedWindow();
|
|
13496
|
+
const { response } = await electron.dialog.showMessageBox(
|
|
13497
|
+
focusedWindow ?? (electron.BrowserWindow.getAllWindows()[0] || null),
|
|
13498
|
+
{
|
|
13499
|
+
type: "question",
|
|
13500
|
+
title: "Agent Credential Access",
|
|
13501
|
+
message: `Agent wants to sign in to ${request.domain}`,
|
|
13502
|
+
detail: [
|
|
13503
|
+
`Credential: ${request.credentialLabel}`,
|
|
13504
|
+
`Username: ${request.username}`,
|
|
13505
|
+
"",
|
|
13506
|
+
"The agent is requesting to fill a login form with stored credentials.",
|
|
13507
|
+
"Credential values will NOT be sent to the AI provider."
|
|
13508
|
+
].join("\n"),
|
|
13509
|
+
buttons: ["Deny", "Allow Once", "Allow for Session"],
|
|
13510
|
+
defaultId: 1,
|
|
13511
|
+
cancelId: 0,
|
|
13512
|
+
noLink: true
|
|
13513
|
+
}
|
|
13514
|
+
);
|
|
13515
|
+
if (response === 0) {
|
|
13516
|
+
return { approved: false, trustForSession: false };
|
|
13517
|
+
}
|
|
13518
|
+
const trustForSession = response === 2;
|
|
13519
|
+
if (trustForSession) {
|
|
13520
|
+
sessionTrustedDomains.add(domain);
|
|
13521
|
+
}
|
|
13522
|
+
return { approved: true, trustForSession };
|
|
13523
|
+
}
|
|
13186
13524
|
let httpServer = null;
|
|
13187
13525
|
let mcpAuthToken = null;
|
|
13188
13526
|
function asTextResponse(text) {
|
|
@@ -14859,10 +15197,15 @@ ${buildScopedContext(pageContent, mode)}`;
|
|
|
14859
15197
|
"vessel_navigate",
|
|
14860
15198
|
{
|
|
14861
15199
|
title: "Navigate",
|
|
14862
|
-
description: "Navigate the active browser tab to a URL.",
|
|
14863
|
-
inputSchema: {
|
|
15200
|
+
description: "Navigate the active browser tab to a URL. Use postBody to submit data via POST request (e.g. form submissions).",
|
|
15201
|
+
inputSchema: {
|
|
15202
|
+
url: zod.z.string().describe("The URL to navigate to"),
|
|
15203
|
+
postBody: zod.z.record(zod.z.string(), zod.z.string()).optional().describe(
|
|
15204
|
+
"Optional form fields to submit via POST (application/x-www-form-urlencoded). Only supported on http/https URLs."
|
|
15205
|
+
)
|
|
15206
|
+
}
|
|
14864
15207
|
},
|
|
14865
|
-
async ({ url }) => {
|
|
15208
|
+
async ({ url, postBody }) => {
|
|
14866
15209
|
const tab = tabManager.getActiveTab();
|
|
14867
15210
|
if (!tab) return asTextResponse("Error: No active tab");
|
|
14868
15211
|
const preCheck = await validateLinkDestination(url);
|
|
@@ -14873,7 +15216,8 @@ ${buildScopedContext(pageContent, mode)}`;
|
|
|
14873
15216
|
}
|
|
14874
15217
|
return withAction(runtime2, tabManager, "navigate", { url }, async () => {
|
|
14875
15218
|
const id = tabManager.getActiveTabId();
|
|
14876
|
-
tabManager.navigateTab(id, url);
|
|
15219
|
+
const navError = tabManager.navigateTab(id, url, postBody);
|
|
15220
|
+
if (navError) return navError;
|
|
14877
15221
|
const { httpStatus } = await waitForLoadWithStatus(
|
|
14878
15222
|
tab.view.webContents
|
|
14879
15223
|
);
|
|
@@ -17332,6 +17676,244 @@ ${JSON.stringify(tableJson, null, 2)}`;
|
|
|
17332
17676
|
);
|
|
17333
17677
|
}
|
|
17334
17678
|
);
|
|
17679
|
+
server.registerTool(
|
|
17680
|
+
"vessel_vault_status",
|
|
17681
|
+
{
|
|
17682
|
+
title: "Check Vault Credentials",
|
|
17683
|
+
description: "Check whether stored credentials exist for a domain. Returns credential labels and usernames but NEVER password values. Use this before vault_login to verify credentials are available.",
|
|
17684
|
+
inputSchema: {
|
|
17685
|
+
domain: zod.z.string().describe(
|
|
17686
|
+
"The domain to check credentials for (e.g. 'github.com'). If omitted, checks the active tab's domain."
|
|
17687
|
+
).optional()
|
|
17688
|
+
}
|
|
17689
|
+
},
|
|
17690
|
+
async ({ domain }) => {
|
|
17691
|
+
let targetDomain = domain;
|
|
17692
|
+
if (!targetDomain) {
|
|
17693
|
+
const tab = tabManager.getActiveTab();
|
|
17694
|
+
if (!tab) return asTextResponse("Error: No active tab and no domain specified");
|
|
17695
|
+
try {
|
|
17696
|
+
targetDomain = new URL(tab.state.url).hostname;
|
|
17697
|
+
} catch {
|
|
17698
|
+
return asTextResponse("Error: Could not parse active tab URL");
|
|
17699
|
+
}
|
|
17700
|
+
}
|
|
17701
|
+
const matches = findEntriesForDomain(
|
|
17702
|
+
targetDomain.includes("://") ? targetDomain : `https://${targetDomain}`
|
|
17703
|
+
);
|
|
17704
|
+
if (matches.length === 0) {
|
|
17705
|
+
return asTextResponse(
|
|
17706
|
+
`No stored credentials found for ${targetDomain}. The user needs to add credentials in Settings > Agent Credential Vault before the agent can log in.`
|
|
17707
|
+
);
|
|
17708
|
+
}
|
|
17709
|
+
appendAuditEntry({
|
|
17710
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17711
|
+
credentialId: matches[0].id,
|
|
17712
|
+
credentialLabel: matches[0].label,
|
|
17713
|
+
domain: targetDomain,
|
|
17714
|
+
action: "status_check",
|
|
17715
|
+
approved: true
|
|
17716
|
+
});
|
|
17717
|
+
const summary = matches.map((m) => ` - "${m.label}" (${m.username})`).join("\n");
|
|
17718
|
+
return asTextResponse(
|
|
17719
|
+
`Found ${matches.length} credential(s) for ${targetDomain}:
|
|
17720
|
+
${summary}
|
|
17721
|
+
|
|
17722
|
+
Use vessel_vault_login to fill the login form. Credentials are filled directly — you will NOT see the password values.`
|
|
17723
|
+
);
|
|
17724
|
+
}
|
|
17725
|
+
);
|
|
17726
|
+
server.registerTool(
|
|
17727
|
+
"vessel_vault_login",
|
|
17728
|
+
{
|
|
17729
|
+
title: "Fill Login with Vault Credentials",
|
|
17730
|
+
description: "Fill a login form on the current page using stored credentials from the Agent Credential Vault. The credential values are filled directly into the page — they are NEVER returned in this response. The user will see a consent dialog before credentials are used.",
|
|
17731
|
+
inputSchema: {
|
|
17732
|
+
credential_label: zod.z.string().optional().describe(
|
|
17733
|
+
"Label of the credential to use. If omitted, uses the first matching credential for the current domain."
|
|
17734
|
+
),
|
|
17735
|
+
username_index: zod.z.number().optional().describe(
|
|
17736
|
+
"Element index of the username/email input field from read_page."
|
|
17737
|
+
),
|
|
17738
|
+
password_index: zod.z.number().optional().describe(
|
|
17739
|
+
"Element index of the password input field from read_page."
|
|
17740
|
+
),
|
|
17741
|
+
submit_after: zod.z.boolean().optional().describe(
|
|
17742
|
+
"Whether to click the submit button after filling credentials. Defaults to false."
|
|
17743
|
+
),
|
|
17744
|
+
submit_index: zod.z.number().optional().describe(
|
|
17745
|
+
"Element index of the submit button. Required if submit_after is true."
|
|
17746
|
+
)
|
|
17747
|
+
}
|
|
17748
|
+
},
|
|
17749
|
+
async ({
|
|
17750
|
+
credential_label,
|
|
17751
|
+
username_index,
|
|
17752
|
+
password_index,
|
|
17753
|
+
submit_after,
|
|
17754
|
+
submit_index
|
|
17755
|
+
}) => {
|
|
17756
|
+
const tab = tabManager.getActiveTab();
|
|
17757
|
+
if (!tab) return asTextResponse("Error: No active tab");
|
|
17758
|
+
const wc = tab.view.webContents;
|
|
17759
|
+
let hostname;
|
|
17760
|
+
try {
|
|
17761
|
+
hostname = new URL(tab.state.url).hostname;
|
|
17762
|
+
} catch {
|
|
17763
|
+
return asTextResponse("Error: Could not parse active tab URL");
|
|
17764
|
+
}
|
|
17765
|
+
const matches = findEntriesForDomain(`https://${hostname}`);
|
|
17766
|
+
if (matches.length === 0) {
|
|
17767
|
+
return asTextResponse(
|
|
17768
|
+
`No stored credentials for ${hostname}. The user needs to add credentials in Settings > Agent Credential Vault.`
|
|
17769
|
+
);
|
|
17770
|
+
}
|
|
17771
|
+
const match = credential_label ? matches.find(
|
|
17772
|
+
(m) => m.label.toLowerCase() === credential_label.toLowerCase()
|
|
17773
|
+
) : matches[0];
|
|
17774
|
+
if (!match) {
|
|
17775
|
+
return asTextResponse(
|
|
17776
|
+
`No credential named "${credential_label}" found for ${hostname}. Available: ${matches.map((m) => m.label).join(", ")}`
|
|
17777
|
+
);
|
|
17778
|
+
}
|
|
17779
|
+
const consent = await requestConsent({
|
|
17780
|
+
credentialLabel: match.label,
|
|
17781
|
+
username: match.username,
|
|
17782
|
+
domain: hostname
|
|
17783
|
+
});
|
|
17784
|
+
appendAuditEntry({
|
|
17785
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17786
|
+
credentialId: match.id,
|
|
17787
|
+
credentialLabel: match.label,
|
|
17788
|
+
domain: hostname,
|
|
17789
|
+
action: "login_fill",
|
|
17790
|
+
approved: consent.approved
|
|
17791
|
+
});
|
|
17792
|
+
if (!consent.approved) {
|
|
17793
|
+
return asTextResponse(
|
|
17794
|
+
`User denied credential access for ${hostname}. The agent should not retry without being asked.`
|
|
17795
|
+
);
|
|
17796
|
+
}
|
|
17797
|
+
const creds = getCredential(match.id);
|
|
17798
|
+
if (!creds) {
|
|
17799
|
+
return asTextResponse("Error: Credential not found in vault");
|
|
17800
|
+
}
|
|
17801
|
+
const results = [];
|
|
17802
|
+
if (username_index != null) {
|
|
17803
|
+
const usernameResult = await wc.executeJavaScript(
|
|
17804
|
+
`window.__vessel?.interactByIndex?.(${username_index}, "value", ${JSON.stringify(creds.username)}) || "Error: interactByIndex not available"`
|
|
17805
|
+
);
|
|
17806
|
+
results.push(`Username: ${usernameResult}`);
|
|
17807
|
+
}
|
|
17808
|
+
if (password_index != null) {
|
|
17809
|
+
const passwordResult = await wc.executeJavaScript(
|
|
17810
|
+
`window.__vessel?.interactByIndex?.(${password_index}, "value", ${JSON.stringify(creds.password)}) || "Error: interactByIndex not available"`
|
|
17811
|
+
);
|
|
17812
|
+
results.push(`Password: ${passwordResult.replace(/Typed into:.*/, "Typed into: [password field]")}`);
|
|
17813
|
+
}
|
|
17814
|
+
recordUsage(match.id);
|
|
17815
|
+
trackVaultAction("login_fill");
|
|
17816
|
+
if (submit_after && submit_index != null) {
|
|
17817
|
+
const submitResult = await wc.executeJavaScript(
|
|
17818
|
+
`window.__vessel?.interactByIndex?.(${submit_index}, "click") || "Error: interactByIndex not available"`
|
|
17819
|
+
);
|
|
17820
|
+
results.push(`Submit: ${submitResult}`);
|
|
17821
|
+
}
|
|
17822
|
+
return asTextResponse(
|
|
17823
|
+
[
|
|
17824
|
+
`Login form filled for ${hostname} using credential "${match.label}".`,
|
|
17825
|
+
...results,
|
|
17826
|
+
"",
|
|
17827
|
+
"Note: Credential values were filled directly into the page. They are NOT included in this response."
|
|
17828
|
+
].join("\n")
|
|
17829
|
+
);
|
|
17830
|
+
}
|
|
17831
|
+
);
|
|
17832
|
+
server.registerTool(
|
|
17833
|
+
"vessel_vault_totp",
|
|
17834
|
+
{
|
|
17835
|
+
title: "Fill TOTP Code from Vault",
|
|
17836
|
+
description: "Generate a TOTP 2FA code from a stored secret and fill it into a code input field. The TOTP secret and generated code are NEVER returned — only filled directly into the page.",
|
|
17837
|
+
inputSchema: {
|
|
17838
|
+
credential_label: zod.z.string().optional().describe(
|
|
17839
|
+
"Label of the credential whose TOTP secret to use. If omitted, uses the first matching credential with a TOTP secret."
|
|
17840
|
+
),
|
|
17841
|
+
code_index: zod.z.number().describe(
|
|
17842
|
+
"Element index of the TOTP/2FA code input field from read_page."
|
|
17843
|
+
),
|
|
17844
|
+
submit_after: zod.z.boolean().optional().describe("Whether to click submit after filling the code."),
|
|
17845
|
+
submit_index: zod.z.number().optional().describe("Element index of the submit button.")
|
|
17846
|
+
}
|
|
17847
|
+
},
|
|
17848
|
+
async ({ credential_label, code_index, submit_after, submit_index }) => {
|
|
17849
|
+
const tab = tabManager.getActiveTab();
|
|
17850
|
+
if (!tab) return asTextResponse("Error: No active tab");
|
|
17851
|
+
const wc = tab.view.webContents;
|
|
17852
|
+
let hostname;
|
|
17853
|
+
try {
|
|
17854
|
+
hostname = new URL(tab.state.url).hostname;
|
|
17855
|
+
} catch {
|
|
17856
|
+
return asTextResponse("Error: Could not parse active tab URL");
|
|
17857
|
+
}
|
|
17858
|
+
const matches = findEntriesForDomain(`https://${hostname}`);
|
|
17859
|
+
const match = credential_label ? matches.find(
|
|
17860
|
+
(m) => m.label.toLowerCase() === credential_label.toLowerCase()
|
|
17861
|
+
) : matches.find((m) => {
|
|
17862
|
+
const secret2 = getTotpSecret(m.id);
|
|
17863
|
+
return secret2 != null;
|
|
17864
|
+
});
|
|
17865
|
+
if (!match) {
|
|
17866
|
+
return asTextResponse(
|
|
17867
|
+
`No credential with TOTP secret found for ${hostname}.`
|
|
17868
|
+
);
|
|
17869
|
+
}
|
|
17870
|
+
const secret = getTotpSecret(match.id);
|
|
17871
|
+
if (!secret) {
|
|
17872
|
+
return asTextResponse(
|
|
17873
|
+
`Credential "${match.label}" does not have a TOTP secret configured.`
|
|
17874
|
+
);
|
|
17875
|
+
}
|
|
17876
|
+
const consent = await requestConsent({
|
|
17877
|
+
credentialLabel: match.label,
|
|
17878
|
+
username: match.username,
|
|
17879
|
+
domain: hostname
|
|
17880
|
+
});
|
|
17881
|
+
appendAuditEntry({
|
|
17882
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17883
|
+
credentialId: match.id,
|
|
17884
|
+
credentialLabel: match.label,
|
|
17885
|
+
domain: hostname,
|
|
17886
|
+
action: "totp_generate",
|
|
17887
|
+
approved: consent.approved
|
|
17888
|
+
});
|
|
17889
|
+
if (!consent.approved) {
|
|
17890
|
+
return asTextResponse(
|
|
17891
|
+
`User denied TOTP access for ${hostname}.`
|
|
17892
|
+
);
|
|
17893
|
+
}
|
|
17894
|
+
const code = generateTotpCode(secret);
|
|
17895
|
+
const fillResult = await wc.executeJavaScript(
|
|
17896
|
+
`window.__vessel?.interactByIndex?.(${code_index}, "value", ${JSON.stringify(code)}) || "Error: interactByIndex not available"`
|
|
17897
|
+
);
|
|
17898
|
+
recordUsage(match.id);
|
|
17899
|
+
trackVaultAction("totp_fill");
|
|
17900
|
+
const results = [`2FA code filled: ${fillResult.replace(/Typed into:.*/, "Typed into: [2FA field]")}`];
|
|
17901
|
+
if (submit_after && submit_index != null) {
|
|
17902
|
+
const submitResult = await wc.executeJavaScript(
|
|
17903
|
+
`window.__vessel?.interactByIndex?.(${submit_index}, "click") || "Error: interactByIndex not available"`
|
|
17904
|
+
);
|
|
17905
|
+
results.push(`Submit: ${submitResult}`);
|
|
17906
|
+
}
|
|
17907
|
+
return asTextResponse(
|
|
17908
|
+
[
|
|
17909
|
+
`TOTP code filled for ${hostname} using credential "${match.label}".`,
|
|
17910
|
+
...results,
|
|
17911
|
+
"",
|
|
17912
|
+
"Note: The TOTP code was filled directly into the page. It is NOT included in this response."
|
|
17913
|
+
].join("\n")
|
|
17914
|
+
);
|
|
17915
|
+
}
|
|
17916
|
+
);
|
|
17335
17917
|
server.registerTool(
|
|
17336
17918
|
"vessel_metrics",
|
|
17337
17919
|
{
|
|
@@ -17648,6 +18230,16 @@ function stopMcpServer() {
|
|
|
17648
18230
|
});
|
|
17649
18231
|
}
|
|
17650
18232
|
let activeChatProvider = null;
|
|
18233
|
+
function assertString(value, name) {
|
|
18234
|
+
if (typeof value !== "string") throw new Error(`${name} must be a string`);
|
|
18235
|
+
}
|
|
18236
|
+
function assertOptionalString(value, name) {
|
|
18237
|
+
if (value !== void 0 && typeof value !== "string") throw new Error(`${name} must be a string`);
|
|
18238
|
+
}
|
|
18239
|
+
function assertNumber(value, name) {
|
|
18240
|
+
if (typeof value !== "number" || Number.isNaN(value)) throw new Error(`${name} must be a number`);
|
|
18241
|
+
}
|
|
18242
|
+
const VALID_APPROVAL_MODES = /* @__PURE__ */ new Set(["auto", "confirm-dangerous", "manual"]);
|
|
17651
18243
|
function registerIpcHandlers(windowState, runtime2) {
|
|
17652
18244
|
const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
|
|
17653
18245
|
const sendToRendererViews = (channel, ...args) => {
|
|
@@ -17671,9 +18263,14 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17671
18263
|
tabManager.switchTab(id);
|
|
17672
18264
|
layoutViews(windowState);
|
|
17673
18265
|
});
|
|
17674
|
-
electron.ipcMain.handle(
|
|
17675
|
-
|
|
17676
|
-
|
|
18266
|
+
electron.ipcMain.handle(
|
|
18267
|
+
Channels.TAB_NAVIGATE,
|
|
18268
|
+
(_, id, url, postBody) => {
|
|
18269
|
+
assertString(id, "tabId");
|
|
18270
|
+
assertString(url, "url");
|
|
18271
|
+
return tabManager.navigateTab(id, url, postBody);
|
|
18272
|
+
}
|
|
18273
|
+
);
|
|
17677
18274
|
electron.ipcMain.handle(Channels.TAB_BACK, (_, id) => {
|
|
17678
18275
|
tabManager.goBack(id);
|
|
17679
18276
|
});
|
|
@@ -17723,6 +18320,9 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17723
18320
|
});
|
|
17724
18321
|
electron.ipcMain.handle(Channels.AI_FETCH_MODELS, async (_, config) => {
|
|
17725
18322
|
try {
|
|
18323
|
+
if (!config || typeof config !== "object" || !("id" in config)) {
|
|
18324
|
+
return { ok: false, models: [], error: "Invalid provider configuration" };
|
|
18325
|
+
}
|
|
17726
18326
|
const models = await fetchProviderModels(config);
|
|
17727
18327
|
return { ok: true, models };
|
|
17728
18328
|
} catch (err) {
|
|
@@ -17766,6 +18366,7 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17766
18366
|
windowState.sidebarView.setBounds({ x: 0, y: 0, width, height });
|
|
17767
18367
|
});
|
|
17768
18368
|
electron.ipcMain.handle(Channels.SIDEBAR_RESIZE, (_, width) => {
|
|
18369
|
+
assertNumber(width, "width");
|
|
17769
18370
|
const clamped = Math.max(240, Math.min(800, Math.round(width)));
|
|
17770
18371
|
windowState.uiState.sidebarWidth = clamped;
|
|
17771
18372
|
return clamped;
|
|
@@ -17789,11 +18390,13 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17789
18390
|
});
|
|
17790
18391
|
electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, () => getRuntimeHealth());
|
|
17791
18392
|
electron.ipcMain.handle(Channels.SETTINGS_SET, async (_, key, value) => {
|
|
18393
|
+
assertString(key, "key");
|
|
17792
18394
|
if (!SETTABLE_KEYS.has(key)) {
|
|
17793
18395
|
throw new Error(`Unknown setting key: ${key}`);
|
|
17794
18396
|
}
|
|
17795
18397
|
const settingsKey = key;
|
|
17796
18398
|
const updatedSettings = setSetting(settingsKey, value);
|
|
18399
|
+
trackSettingChanged(key);
|
|
17797
18400
|
if (key === "approvalMode") {
|
|
17798
18401
|
runtime2.setApprovalMode(value);
|
|
17799
18402
|
}
|
|
@@ -17810,6 +18413,11 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17810
18413
|
electron.ipcMain.handle(
|
|
17811
18414
|
Channels.AGENT_SET_APPROVAL_MODE,
|
|
17812
18415
|
(_, mode) => {
|
|
18416
|
+
assertString(mode, "mode");
|
|
18417
|
+
if (!VALID_APPROVAL_MODES.has(mode)) {
|
|
18418
|
+
throw new Error(`Invalid approval mode: ${mode}`);
|
|
18419
|
+
}
|
|
18420
|
+
trackApprovalModeChanged(mode);
|
|
17813
18421
|
setSetting("approvalMode", mode);
|
|
17814
18422
|
return runtime2.setApprovalMode(mode);
|
|
17815
18423
|
}
|
|
@@ -17840,17 +18448,23 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17840
18448
|
electron.ipcMain.handle(
|
|
17841
18449
|
Channels.FOLDER_CREATE,
|
|
17842
18450
|
(_, name, summary) => {
|
|
18451
|
+
trackBookmarkAction("folder_create");
|
|
17843
18452
|
return createFolderWithSummary(name, summary);
|
|
17844
18453
|
}
|
|
17845
18454
|
);
|
|
17846
18455
|
electron.ipcMain.handle(
|
|
17847
18456
|
Channels.BOOKMARK_SAVE,
|
|
17848
|
-
(_, url, title, folderId, note) =>
|
|
18457
|
+
(_, url, title, folderId, note) => {
|
|
18458
|
+
trackBookmarkAction("save");
|
|
18459
|
+
return saveBookmark(url, title, folderId, note);
|
|
18460
|
+
}
|
|
17849
18461
|
);
|
|
17850
18462
|
electron.ipcMain.handle(Channels.BOOKMARK_REMOVE, (_, id) => {
|
|
18463
|
+
trackBookmarkAction("remove");
|
|
17851
18464
|
return removeBookmark(id);
|
|
17852
18465
|
});
|
|
17853
18466
|
electron.ipcMain.handle(Channels.FOLDER_REMOVE, (_, id) => {
|
|
18467
|
+
trackBookmarkAction("folder_remove");
|
|
17854
18468
|
return removeFolder(id);
|
|
17855
18469
|
});
|
|
17856
18470
|
electron.ipcMain.handle(
|
|
@@ -18002,13 +18616,22 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18002
18616
|
return getPremiumState();
|
|
18003
18617
|
});
|
|
18004
18618
|
electron.ipcMain.handle(Channels.PREMIUM_ACTIVATE, async (_, email) => {
|
|
18619
|
+
assertString(email, "email");
|
|
18620
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
|
|
18621
|
+
return { ok: false, state: getPremiumState(), error: "Invalid email format" };
|
|
18622
|
+
}
|
|
18623
|
+
trackPremiumFunnel("activation_attempted");
|
|
18005
18624
|
const result = await activateWithEmail(email);
|
|
18006
18625
|
if (result.ok) {
|
|
18626
|
+
trackPremiumFunnel("activation_succeeded", { status: result.state.status });
|
|
18007
18627
|
sendToRendererViews(Channels.PREMIUM_UPDATE, result.state);
|
|
18628
|
+
} else {
|
|
18629
|
+
trackPremiumFunnel("activation_failed", { status: result.state.status });
|
|
18008
18630
|
}
|
|
18009
18631
|
return result;
|
|
18010
18632
|
});
|
|
18011
18633
|
electron.ipcMain.handle(Channels.PREMIUM_CHECKOUT, async (_, email) => {
|
|
18634
|
+
trackPremiumFunnel("checkout_clicked");
|
|
18012
18635
|
const result = await getCheckoutUrl(email);
|
|
18013
18636
|
if (result.ok && result.url) {
|
|
18014
18637
|
tabManager.createTab(result.url);
|
|
@@ -18016,17 +18639,56 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
18016
18639
|
return result;
|
|
18017
18640
|
});
|
|
18018
18641
|
electron.ipcMain.handle(Channels.PREMIUM_RESET, () => {
|
|
18642
|
+
trackPremiumFunnel("reset");
|
|
18019
18643
|
const state2 = resetPremium();
|
|
18020
18644
|
sendToRendererViews(Channels.PREMIUM_UPDATE, state2);
|
|
18021
18645
|
return state2;
|
|
18022
18646
|
});
|
|
18023
18647
|
electron.ipcMain.handle(Channels.PREMIUM_PORTAL, async () => {
|
|
18648
|
+
trackPremiumFunnel("portal_opened");
|
|
18024
18649
|
const result = await getPortalUrl();
|
|
18025
18650
|
if (result.ok && result.url) {
|
|
18026
18651
|
tabManager.createTab(result.url);
|
|
18027
18652
|
}
|
|
18028
18653
|
return result;
|
|
18029
18654
|
});
|
|
18655
|
+
electron.ipcMain.handle(Channels.VAULT_LIST, () => {
|
|
18656
|
+
return listEntries();
|
|
18657
|
+
});
|
|
18658
|
+
electron.ipcMain.handle(
|
|
18659
|
+
Channels.VAULT_ADD,
|
|
18660
|
+
(_, entry) => {
|
|
18661
|
+
if (!entry || typeof entry !== "object") throw new Error("Invalid vault entry");
|
|
18662
|
+
assertString(entry.label, "label");
|
|
18663
|
+
assertString(entry.domainPattern, "domainPattern");
|
|
18664
|
+
assertString(entry.username, "username");
|
|
18665
|
+
assertString(entry.password, "password");
|
|
18666
|
+
if (!entry.label.trim() || !entry.domainPattern.trim() || !entry.username.trim() || !entry.password.trim()) {
|
|
18667
|
+
throw new Error("Label, domain, username, and password are required");
|
|
18668
|
+
}
|
|
18669
|
+
assertOptionalString(entry.totpSecret, "totpSecret");
|
|
18670
|
+
assertOptionalString(entry.notes, "notes");
|
|
18671
|
+
trackVaultAction("credential_added");
|
|
18672
|
+
const created = addEntry(entry);
|
|
18673
|
+
return { id: created.id, label: created.label, domainPattern: created.domainPattern, username: created.username };
|
|
18674
|
+
}
|
|
18675
|
+
);
|
|
18676
|
+
electron.ipcMain.handle(
|
|
18677
|
+
Channels.VAULT_UPDATE,
|
|
18678
|
+
(_, id, updates) => {
|
|
18679
|
+
assertString(id, "id");
|
|
18680
|
+
if (!updates || typeof updates !== "object") throw new Error("Invalid updates");
|
|
18681
|
+
return updateEntry(id, updates) !== null;
|
|
18682
|
+
}
|
|
18683
|
+
);
|
|
18684
|
+
electron.ipcMain.handle(Channels.VAULT_REMOVE, (_, id) => {
|
|
18685
|
+
assertString(id, "id");
|
|
18686
|
+
trackVaultAction("credential_removed");
|
|
18687
|
+
return removeEntry(id);
|
|
18688
|
+
});
|
|
18689
|
+
electron.ipcMain.handle(Channels.VAULT_AUDIT_LOG, (_, limit) => {
|
|
18690
|
+
return readAuditLog(limit);
|
|
18691
|
+
});
|
|
18030
18692
|
electron.ipcMain.handle(Channels.WINDOW_MINIMIZE, () => {
|
|
18031
18693
|
mainWindow.minimize();
|
|
18032
18694
|
});
|
|
@@ -18659,13 +19321,70 @@ function installDownloadHandler(chromeView) {
|
|
|
18659
19321
|
});
|
|
18660
19322
|
});
|
|
18661
19323
|
}
|
|
18662
|
-
|
|
19324
|
+
function registerHighlightShortcut(mainWindow, tabManager) {
|
|
19325
|
+
const register = () => {
|
|
19326
|
+
electron.globalShortcut.unregister("CommandOrControl+H");
|
|
19327
|
+
const success = electron.globalShortcut.register("CommandOrControl+H", () => {
|
|
19328
|
+
const activeTab = tabManager.getActiveTab();
|
|
19329
|
+
if (!activeTab) return;
|
|
19330
|
+
tabManager.captureHighlightFromActiveTab();
|
|
19331
|
+
});
|
|
19332
|
+
if (!success) {
|
|
19333
|
+
console.warn("[Vessel] Failed to register Ctrl+H shortcut");
|
|
19334
|
+
}
|
|
19335
|
+
};
|
|
19336
|
+
register();
|
|
19337
|
+
mainWindow.on("focus", register);
|
|
19338
|
+
return () => {
|
|
19339
|
+
electron.globalShortcut.unregister("CommandOrControl+H");
|
|
19340
|
+
mainWindow.removeListener("focus", register);
|
|
19341
|
+
};
|
|
19342
|
+
}
|
|
19343
|
+
function setupAppMenu() {
|
|
19344
|
+
const appMenu = electron.Menu.buildFromTemplate([
|
|
19345
|
+
{
|
|
19346
|
+
label: "Edit",
|
|
19347
|
+
submenu: [
|
|
19348
|
+
{ role: "undo" },
|
|
19349
|
+
{ role: "redo" },
|
|
19350
|
+
{ type: "separator" },
|
|
19351
|
+
{ role: "cut" },
|
|
19352
|
+
{ role: "copy" },
|
|
19353
|
+
{ role: "paste" },
|
|
19354
|
+
{ role: "selectAll" }
|
|
19355
|
+
]
|
|
19356
|
+
}
|
|
19357
|
+
]);
|
|
19358
|
+
electron.Menu.setApplicationMenu(appMenu);
|
|
19359
|
+
}
|
|
18663
19360
|
function rendererUrlFor(view) {
|
|
18664
19361
|
if (!process.env.ELECTRON_RENDERER_URL) return null;
|
|
18665
19362
|
const url = new URL(process.env.ELECTRON_RENDERER_URL);
|
|
18666
19363
|
url.searchParams.set("view", view);
|
|
18667
19364
|
return url.toString();
|
|
18668
19365
|
}
|
|
19366
|
+
function loadRenderers(chromeView, sidebarView, devtoolsPanelView) {
|
|
19367
|
+
const chromeUrl = rendererUrlFor("chrome");
|
|
19368
|
+
const sidebarUrl = rendererUrlFor("sidebar");
|
|
19369
|
+
const devtoolsUrl = rendererUrlFor("devtools");
|
|
19370
|
+
if (chromeUrl && sidebarUrl && devtoolsUrl) {
|
|
19371
|
+
chromeView.webContents.loadURL(chromeUrl);
|
|
19372
|
+
sidebarView.webContents.loadURL(sidebarUrl);
|
|
19373
|
+
devtoolsPanelView.webContents.loadURL(devtoolsUrl);
|
|
19374
|
+
} else {
|
|
19375
|
+
const rendererFile = path$1.join(__dirname, "../../renderer/index.html");
|
|
19376
|
+
chromeView.webContents.loadFile(rendererFile, {
|
|
19377
|
+
query: { view: "chrome" }
|
|
19378
|
+
});
|
|
19379
|
+
sidebarView.webContents.loadFile(rendererFile, {
|
|
19380
|
+
query: { view: "sidebar" }
|
|
19381
|
+
});
|
|
19382
|
+
devtoolsPanelView.webContents.loadFile(rendererFile, {
|
|
19383
|
+
query: { view: "devtools" }
|
|
19384
|
+
});
|
|
19385
|
+
}
|
|
19386
|
+
}
|
|
19387
|
+
let runtime = null;
|
|
18669
19388
|
function checkWritableUserData(userDataPath) {
|
|
18670
19389
|
const issues = [];
|
|
18671
19390
|
try {
|
|
@@ -18758,34 +19477,8 @@ async function bootstrap() {
|
|
|
18758
19477
|
}
|
|
18759
19478
|
});
|
|
18760
19479
|
registerIpcHandlers(windowState, runtime);
|
|
18761
|
-
|
|
18762
|
-
|
|
18763
|
-
const success = electron.globalShortcut.register("CommandOrControl+H", () => {
|
|
18764
|
-
const activeTab = tabManager.getActiveTab();
|
|
18765
|
-
if (!activeTab) return;
|
|
18766
|
-
tabManager.captureHighlightFromActiveTab();
|
|
18767
|
-
});
|
|
18768
|
-
if (!success) {
|
|
18769
|
-
console.warn("[Vessel] Failed to register Ctrl+H shortcut");
|
|
18770
|
-
}
|
|
18771
|
-
};
|
|
18772
|
-
registerHighlightShortcut();
|
|
18773
|
-
windowState.mainWindow.on("focus", registerHighlightShortcut);
|
|
18774
|
-
const appMenu = electron.Menu.buildFromTemplate([
|
|
18775
|
-
{
|
|
18776
|
-
label: "Edit",
|
|
18777
|
-
submenu: [
|
|
18778
|
-
{ role: "undo" },
|
|
18779
|
-
{ role: "redo" },
|
|
18780
|
-
{ type: "separator" },
|
|
18781
|
-
{ role: "cut" },
|
|
18782
|
-
{ role: "copy" },
|
|
18783
|
-
{ role: "paste" },
|
|
18784
|
-
{ role: "selectAll" }
|
|
18785
|
-
]
|
|
18786
|
-
}
|
|
18787
|
-
]);
|
|
18788
|
-
electron.Menu.setApplicationMenu(appMenu);
|
|
19480
|
+
registerHighlightShortcut(windowState.mainWindow, tabManager);
|
|
19481
|
+
setupAppMenu();
|
|
18789
19482
|
subscribe((state2) => {
|
|
18790
19483
|
chromeView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
|
|
18791
19484
|
sidebarView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
|
|
@@ -18797,25 +19490,7 @@ async function bootstrap() {
|
|
|
18797
19490
|
installDownloadHandler(chromeView);
|
|
18798
19491
|
startBackgroundRevalidation();
|
|
18799
19492
|
startTelemetry();
|
|
18800
|
-
|
|
18801
|
-
const sidebarUrl = rendererUrlFor("sidebar");
|
|
18802
|
-
const devtoolsUrl = rendererUrlFor("devtools");
|
|
18803
|
-
if (chromeUrl && sidebarUrl && devtoolsUrl) {
|
|
18804
|
-
chromeView.webContents.loadURL(chromeUrl);
|
|
18805
|
-
sidebarView.webContents.loadURL(sidebarUrl);
|
|
18806
|
-
devtoolsPanelView.webContents.loadURL(devtoolsUrl);
|
|
18807
|
-
} else {
|
|
18808
|
-
const rendererFile = path.join(__dirname, "../renderer/index.html");
|
|
18809
|
-
chromeView.webContents.loadFile(rendererFile, {
|
|
18810
|
-
query: { view: "chrome" }
|
|
18811
|
-
});
|
|
18812
|
-
sidebarView.webContents.loadFile(rendererFile, {
|
|
18813
|
-
query: { view: "sidebar" }
|
|
18814
|
-
});
|
|
18815
|
-
devtoolsPanelView.webContents.loadFile(rendererFile, {
|
|
18816
|
-
query: { view: "devtools" }
|
|
18817
|
-
});
|
|
18818
|
-
}
|
|
19493
|
+
loadRenderers(chromeView, sidebarView, devtoolsPanelView);
|
|
18819
19494
|
await startMcpServer(tabManager, runtime, settings2.mcpPort);
|
|
18820
19495
|
chromeView.webContents.once("did-finish-load", () => {
|
|
18821
19496
|
const savedSession = runtime.getState().session;
|
|
@@ -18830,6 +19505,16 @@ async function bootstrap() {
|
|
|
18830
19505
|
void maybeShowStartupHealthDialog(windowState);
|
|
18831
19506
|
});
|
|
18832
19507
|
}
|
|
19508
|
+
process.on("uncaughtException", (error) => {
|
|
19509
|
+
console.error("[Vessel] Uncaught exception:", error.message, error.stack);
|
|
19510
|
+
electron.app.quit();
|
|
19511
|
+
});
|
|
19512
|
+
process.on("unhandledRejection", (reason) => {
|
|
19513
|
+
console.error(
|
|
19514
|
+
"[Vessel] Unhandled rejection:",
|
|
19515
|
+
reason instanceof Error ? reason.message : reason
|
|
19516
|
+
);
|
|
19517
|
+
});
|
|
18833
19518
|
electron.app.whenReady().then(bootstrap).catch((error) => {
|
|
18834
19519
|
console.error("[Vessel] Failed to bootstrap application:", error);
|
|
18835
19520
|
electron.app.quit();
|