@quanta-intellect/vessel-browser 0.1.17 → 0.1.19
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 +983 -19
- package/out/preload/index.js +32 -0
- package/out/renderer/assets/{index-iB-6qrfG.js → index-CKOT_IZt.js} +1185 -177
- 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");
|
|
@@ -26,7 +26,15 @@ const defaults = {
|
|
|
26
26
|
chatProvider: null,
|
|
27
27
|
maxToolIterations: 200,
|
|
28
28
|
domainPolicy: { allowedDomains: [], blockedDomains: [] },
|
|
29
|
-
downloadPath: ""
|
|
29
|
+
downloadPath: "",
|
|
30
|
+
telemetryEnabled: true,
|
|
31
|
+
premium: {
|
|
32
|
+
status: "free",
|
|
33
|
+
customerId: "",
|
|
34
|
+
email: "",
|
|
35
|
+
validatedAt: "",
|
|
36
|
+
expiresAt: ""
|
|
37
|
+
}
|
|
30
38
|
};
|
|
31
39
|
const SETTABLE_KEYS = new Set(Object.keys(defaults));
|
|
32
40
|
let settings = null;
|
|
@@ -1220,7 +1228,7 @@ function subscribe$1(listener) {
|
|
|
1220
1228
|
listeners$1.delete(listener);
|
|
1221
1229
|
};
|
|
1222
1230
|
}
|
|
1223
|
-
function addEntry(url, title) {
|
|
1231
|
+
function addEntry$1(url, title) {
|
|
1224
1232
|
if (!url || url === "about:blank") return;
|
|
1225
1233
|
load$1();
|
|
1226
1234
|
const last = state$2.entries[0];
|
|
@@ -1983,7 +1991,7 @@ class TabManager {
|
|
|
1983
1991
|
},
|
|
1984
1992
|
onPageLoad: (pageUrl, wc) => {
|
|
1985
1993
|
this.reapplyHighlights(pageUrl, wc);
|
|
1986
|
-
addEntry(pageUrl, wc.getTitle());
|
|
1994
|
+
addEntry$1(pageUrl, wc.getTitle());
|
|
1987
1995
|
},
|
|
1988
1996
|
onHighlightSelection: (wc) => this.captureHighlightFromPage(wc),
|
|
1989
1997
|
onHighlightRemove: (url2, text) => this.removeHighlightByText(url2, text),
|
|
@@ -2334,6 +2342,19 @@ const Channels = {
|
|
|
2334
2342
|
DOWNLOAD_STARTED: "download:started",
|
|
2335
2343
|
DOWNLOAD_PROGRESS: "download:progress",
|
|
2336
2344
|
DOWNLOAD_DONE: "download:done",
|
|
2345
|
+
// Premium
|
|
2346
|
+
PREMIUM_GET_STATE: "premium:get-state",
|
|
2347
|
+
PREMIUM_ACTIVATE: "premium:activate",
|
|
2348
|
+
PREMIUM_CHECKOUT: "premium:checkout",
|
|
2349
|
+
PREMIUM_PORTAL: "premium:portal",
|
|
2350
|
+
PREMIUM_RESET: "premium:reset",
|
|
2351
|
+
PREMIUM_UPDATE: "premium:update",
|
|
2352
|
+
// Agent Credential Vault
|
|
2353
|
+
VAULT_LIST: "vault:list",
|
|
2354
|
+
VAULT_ADD: "vault:add",
|
|
2355
|
+
VAULT_UPDATE: "vault:update",
|
|
2356
|
+
VAULT_REMOVE: "vault:remove",
|
|
2357
|
+
VAULT_AUDIT_LOG: "vault:audit-log",
|
|
2337
2358
|
// Window controls
|
|
2338
2359
|
WINDOW_MINIMIZE: "window:minimize",
|
|
2339
2360
|
WINDOW_MAXIMIZE: "window:maximize",
|
|
@@ -3169,6 +3190,309 @@ function addIfPresent(target, key, value) {
|
|
|
3169
3190
|
target[key] = value;
|
|
3170
3191
|
}
|
|
3171
3192
|
}
|
|
3193
|
+
const VERIFICATION_API = process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
|
|
3194
|
+
const FREE_TOOL_ITERATION_LIMIT = 50;
|
|
3195
|
+
const REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3196
|
+
const OFFLINE_GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
3197
|
+
const PREMIUM_TOOLS = /* @__PURE__ */ new Set([
|
|
3198
|
+
"screenshot",
|
|
3199
|
+
"save_session",
|
|
3200
|
+
"load_session",
|
|
3201
|
+
"list_sessions",
|
|
3202
|
+
"delete_session",
|
|
3203
|
+
"flow_start",
|
|
3204
|
+
"flow_advance",
|
|
3205
|
+
"flow_status",
|
|
3206
|
+
"flow_end",
|
|
3207
|
+
"metrics",
|
|
3208
|
+
"extract_table",
|
|
3209
|
+
"vault_login",
|
|
3210
|
+
"vault_status",
|
|
3211
|
+
"vault_totp"
|
|
3212
|
+
]);
|
|
3213
|
+
function isPremium() {
|
|
3214
|
+
const { premium } = loadSettings();
|
|
3215
|
+
if (premium.status === "active" || premium.status === "trialing") {
|
|
3216
|
+
return true;
|
|
3217
|
+
}
|
|
3218
|
+
if (premium.validatedAt && premium.status !== "free") {
|
|
3219
|
+
const lastValidated = new Date(premium.validatedAt).getTime();
|
|
3220
|
+
if (Date.now() - lastValidated < OFFLINE_GRACE_PERIOD_MS) {
|
|
3221
|
+
return true;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return false;
|
|
3225
|
+
}
|
|
3226
|
+
function getPremiumState() {
|
|
3227
|
+
return { ...loadSettings().premium };
|
|
3228
|
+
}
|
|
3229
|
+
function getEffectiveMaxIterations() {
|
|
3230
|
+
if (isPremium()) {
|
|
3231
|
+
return loadSettings().maxToolIterations || 200;
|
|
3232
|
+
}
|
|
3233
|
+
return FREE_TOOL_ITERATION_LIMIT;
|
|
3234
|
+
}
|
|
3235
|
+
function resetPremium() {
|
|
3236
|
+
const fresh = {
|
|
3237
|
+
status: "free",
|
|
3238
|
+
customerId: "",
|
|
3239
|
+
email: "",
|
|
3240
|
+
validatedAt: "",
|
|
3241
|
+
expiresAt: ""
|
|
3242
|
+
};
|
|
3243
|
+
setSetting("premium", fresh);
|
|
3244
|
+
return fresh;
|
|
3245
|
+
}
|
|
3246
|
+
function isToolGated(toolName) {
|
|
3247
|
+
return PREMIUM_TOOLS.has(toolName) && !isPremium();
|
|
3248
|
+
}
|
|
3249
|
+
async function getCheckoutUrl(email) {
|
|
3250
|
+
try {
|
|
3251
|
+
const params = new URLSearchParams();
|
|
3252
|
+
if (email) params.set("email", email);
|
|
3253
|
+
const res = await fetch(`${VERIFICATION_API}/checkout?${params}`, {
|
|
3254
|
+
method: "POST",
|
|
3255
|
+
headers: { "Content-Type": "application/json" }
|
|
3256
|
+
});
|
|
3257
|
+
if (!res.ok) {
|
|
3258
|
+
const body = await res.text();
|
|
3259
|
+
return { ok: false, error: body || `HTTP ${res.status}` };
|
|
3260
|
+
}
|
|
3261
|
+
const { url } = await res.json();
|
|
3262
|
+
return { ok: true, url };
|
|
3263
|
+
} catch (err) {
|
|
3264
|
+
return {
|
|
3265
|
+
ok: false,
|
|
3266
|
+
error: err instanceof Error ? err.message : "Failed to create checkout"
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
async function getPortalUrl() {
|
|
3271
|
+
const { premium } = loadSettings();
|
|
3272
|
+
if (!premium.customerId) {
|
|
3273
|
+
return { ok: false, error: "No active subscription" };
|
|
3274
|
+
}
|
|
3275
|
+
try {
|
|
3276
|
+
const res = await fetch(`${VERIFICATION_API}/portal`, {
|
|
3277
|
+
method: "POST",
|
|
3278
|
+
headers: { "Content-Type": "application/json" },
|
|
3279
|
+
body: JSON.stringify({ customerId: premium.customerId })
|
|
3280
|
+
});
|
|
3281
|
+
if (!res.ok) {
|
|
3282
|
+
return { ok: false, error: `HTTP ${res.status}` };
|
|
3283
|
+
}
|
|
3284
|
+
const { url } = await res.json();
|
|
3285
|
+
return { ok: true, url };
|
|
3286
|
+
} catch (err) {
|
|
3287
|
+
return {
|
|
3288
|
+
ok: false,
|
|
3289
|
+
error: err instanceof Error ? err.message : "Failed to get portal URL"
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
async function verifySubscription(emailOrCustomerId) {
|
|
3294
|
+
const current = loadSettings().premium;
|
|
3295
|
+
const identifier = emailOrCustomerId || current.customerId || current.email;
|
|
3296
|
+
if (!identifier) {
|
|
3297
|
+
return current;
|
|
3298
|
+
}
|
|
3299
|
+
try {
|
|
3300
|
+
const res = await fetch(`${VERIFICATION_API}/verify`, {
|
|
3301
|
+
method: "POST",
|
|
3302
|
+
headers: { "Content-Type": "application/json" },
|
|
3303
|
+
body: JSON.stringify({ identifier })
|
|
3304
|
+
});
|
|
3305
|
+
if (!res.ok) {
|
|
3306
|
+
console.warn("[Vessel Premium] Verification API returned", res.status);
|
|
3307
|
+
return current;
|
|
3308
|
+
}
|
|
3309
|
+
const data = await res.json();
|
|
3310
|
+
const updated = {
|
|
3311
|
+
status: data.status,
|
|
3312
|
+
customerId: data.customerId,
|
|
3313
|
+
email: data.email,
|
|
3314
|
+
validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3315
|
+
expiresAt: data.expiresAt
|
|
3316
|
+
};
|
|
3317
|
+
setSetting("premium", updated);
|
|
3318
|
+
return updated;
|
|
3319
|
+
} catch (err) {
|
|
3320
|
+
console.warn("[Vessel Premium] Verification failed:", err);
|
|
3321
|
+
return current;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
async function activateWithEmail(email) {
|
|
3325
|
+
if (!email.trim()) {
|
|
3326
|
+
return { ok: false, state: getPremiumState(), error: "Email is required" };
|
|
3327
|
+
}
|
|
3328
|
+
const state2 = await verifySubscription(email.trim());
|
|
3329
|
+
if (state2.status === "active" || state2.status === "trialing") {
|
|
3330
|
+
return { ok: true, state: state2 };
|
|
3331
|
+
}
|
|
3332
|
+
return {
|
|
3333
|
+
ok: false,
|
|
3334
|
+
state: state2,
|
|
3335
|
+
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."
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
let revalidationTimer = null;
|
|
3339
|
+
function startBackgroundRevalidation() {
|
|
3340
|
+
if (revalidationTimer) return;
|
|
3341
|
+
const { premium } = loadSettings();
|
|
3342
|
+
if (premium.customerId || premium.email) {
|
|
3343
|
+
const lastValidated = premium.validatedAt ? new Date(premium.validatedAt).getTime() : 0;
|
|
3344
|
+
if (Date.now() - lastValidated > REVALIDATION_INTERVAL_MS) {
|
|
3345
|
+
void verifySubscription();
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
revalidationTimer = setInterval(() => {
|
|
3349
|
+
const { premium: p } = loadSettings();
|
|
3350
|
+
if (p.customerId || p.email) {
|
|
3351
|
+
void verifySubscription();
|
|
3352
|
+
}
|
|
3353
|
+
}, REVALIDATION_INTERVAL_MS);
|
|
3354
|
+
}
|
|
3355
|
+
function stopBackgroundRevalidation() {
|
|
3356
|
+
if (revalidationTimer) {
|
|
3357
|
+
clearInterval(revalidationTimer);
|
|
3358
|
+
revalidationTimer = null;
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || "phc_OMeM3P5cxJwl14lOKxYad0Yre52xvjNfkLEFnPtXyM";
|
|
3362
|
+
const POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
|
|
3363
|
+
const BATCH_INTERVAL_MS = 6e4;
|
|
3364
|
+
const MAX_BATCH_SIZE = 50;
|
|
3365
|
+
function getDeviceIdPath() {
|
|
3366
|
+
return path.join(electron.app.getPath("userData"), ".vessel-device-id");
|
|
3367
|
+
}
|
|
3368
|
+
let deviceId = null;
|
|
3369
|
+
function getDeviceId() {
|
|
3370
|
+
if (deviceId) return deviceId;
|
|
3371
|
+
const idPath = getDeviceIdPath();
|
|
3372
|
+
try {
|
|
3373
|
+
deviceId = fs.readFileSync(idPath, "utf-8").trim();
|
|
3374
|
+
if (deviceId) return deviceId;
|
|
3375
|
+
} catch {
|
|
3376
|
+
}
|
|
3377
|
+
deviceId = crypto.randomUUID();
|
|
3378
|
+
try {
|
|
3379
|
+
fs.mkdirSync(path.dirname(idPath), { recursive: true });
|
|
3380
|
+
fs.writeFileSync(idPath, deviceId, "utf-8");
|
|
3381
|
+
} catch {
|
|
3382
|
+
}
|
|
3383
|
+
return deviceId;
|
|
3384
|
+
}
|
|
3385
|
+
let eventQueue = [];
|
|
3386
|
+
let flushTimer = null;
|
|
3387
|
+
let sessionStartedAt = null;
|
|
3388
|
+
function isEnabled() {
|
|
3389
|
+
if (POSTHOG_API_KEY === "YOUR_POSTHOG_KEY_HERE") return false;
|
|
3390
|
+
return loadSettings().telemetryEnabled !== false;
|
|
3391
|
+
}
|
|
3392
|
+
function trackEvent(event, properties = {}) {
|
|
3393
|
+
if (!isEnabled()) return;
|
|
3394
|
+
eventQueue.push({
|
|
3395
|
+
event,
|
|
3396
|
+
properties: {
|
|
3397
|
+
...properties,
|
|
3398
|
+
premium_status: isPremium() ? "premium" : "free",
|
|
3399
|
+
app_version: electron.app.getVersion(),
|
|
3400
|
+
platform: process.platform,
|
|
3401
|
+
arch: process.arch
|
|
3402
|
+
},
|
|
3403
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3404
|
+
});
|
|
3405
|
+
if (eventQueue.length >= MAX_BATCH_SIZE) {
|
|
3406
|
+
void flush();
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
function trackToolCall(toolName, pageType) {
|
|
3410
|
+
trackEvent("tool_called", {
|
|
3411
|
+
tool_name: toolName,
|
|
3412
|
+
page_type: "unknown"
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
function trackProviderConfigured(providerId) {
|
|
3416
|
+
trackEvent("provider_configured", {
|
|
3417
|
+
provider_id: providerId
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
function trackSettingChanged(key) {
|
|
3421
|
+
trackEvent("setting_changed", { setting_key: key });
|
|
3422
|
+
}
|
|
3423
|
+
function trackApprovalModeChanged(mode) {
|
|
3424
|
+
trackEvent("approval_mode_changed", { mode });
|
|
3425
|
+
}
|
|
3426
|
+
function trackBookmarkAction(action) {
|
|
3427
|
+
trackEvent("bookmark_action", { action });
|
|
3428
|
+
}
|
|
3429
|
+
function trackVaultAction(action) {
|
|
3430
|
+
trackEvent("vault_action", { action });
|
|
3431
|
+
}
|
|
3432
|
+
function trackExtractionFailed(domain, reason) {
|
|
3433
|
+
trackEvent("extraction_failed", { domain, reason });
|
|
3434
|
+
}
|
|
3435
|
+
function trackPremiumFunnel(step, context) {
|
|
3436
|
+
trackEvent("premium_funnel", { step, ...context });
|
|
3437
|
+
}
|
|
3438
|
+
function startTelemetry() {
|
|
3439
|
+
if (!isEnabled()) return;
|
|
3440
|
+
sessionStartedAt = Date.now();
|
|
3441
|
+
trackEvent("app_launched", {
|
|
3442
|
+
electron_version: process.versions.electron,
|
|
3443
|
+
chrome_version: process.versions.chrome
|
|
3444
|
+
});
|
|
3445
|
+
flushTimer = setInterval(() => {
|
|
3446
|
+
void flush();
|
|
3447
|
+
}, BATCH_INTERVAL_MS);
|
|
3448
|
+
}
|
|
3449
|
+
function stopTelemetry() {
|
|
3450
|
+
if (sessionStartedAt) {
|
|
3451
|
+
const durationMinutes = Math.round(
|
|
3452
|
+
(Date.now() - sessionStartedAt) / 6e4
|
|
3453
|
+
);
|
|
3454
|
+
trackEvent("app_session_ended", {
|
|
3455
|
+
duration_minutes: durationMinutes
|
|
3456
|
+
});
|
|
3457
|
+
sessionStartedAt = null;
|
|
3458
|
+
}
|
|
3459
|
+
if (flushTimer) {
|
|
3460
|
+
clearInterval(flushTimer);
|
|
3461
|
+
flushTimer = null;
|
|
3462
|
+
}
|
|
3463
|
+
void flush();
|
|
3464
|
+
}
|
|
3465
|
+
async function flush() {
|
|
3466
|
+
if (eventQueue.length === 0) return;
|
|
3467
|
+
if (!isEnabled()) {
|
|
3468
|
+
eventQueue = [];
|
|
3469
|
+
return;
|
|
3470
|
+
}
|
|
3471
|
+
const batch = eventQueue.splice(0);
|
|
3472
|
+
const distinctId = getDeviceId();
|
|
3473
|
+
const payload = {
|
|
3474
|
+
api_key: POSTHOG_API_KEY,
|
|
3475
|
+
batch: batch.map((e) => ({
|
|
3476
|
+
event: e.event,
|
|
3477
|
+
properties: {
|
|
3478
|
+
distinct_id: distinctId,
|
|
3479
|
+
...e.properties
|
|
3480
|
+
},
|
|
3481
|
+
timestamp: e.timestamp
|
|
3482
|
+
}))
|
|
3483
|
+
};
|
|
3484
|
+
try {
|
|
3485
|
+
await fetch(`${POSTHOG_HOST}/batch`, {
|
|
3486
|
+
method: "POST",
|
|
3487
|
+
headers: { "Content-Type": "application/json" },
|
|
3488
|
+
body: JSON.stringify(payload)
|
|
3489
|
+
});
|
|
3490
|
+
} catch {
|
|
3491
|
+
if (eventQueue.length < MAX_BATCH_SIZE * 2) {
|
|
3492
|
+
eventQueue.unshift(...batch);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3172
3496
|
const EMPTY_PAGE_CONTENT = {
|
|
3173
3497
|
title: "",
|
|
3174
3498
|
content: "",
|
|
@@ -3214,7 +3538,7 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
|
|
|
3214
3538
|
(function() {
|
|
3215
3539
|
// Time budget: stop expensive DOM traversals after this many ms so heavy
|
|
3216
3540
|
// pages (Newegg, Wikipedia, etc.) don't stall the agent for 30-60s+.
|
|
3217
|
-
var BUDGET_MS =
|
|
3541
|
+
var BUDGET_MS = 8000;
|
|
3218
3542
|
var _budgetStart = performance.now();
|
|
3219
3543
|
function withinBudget() {
|
|
3220
3544
|
return (performance.now() - _budgetStart) < BUDGET_MS;
|
|
@@ -4073,7 +4397,7 @@ const SAFE_EXTRACTION_SCRIPT = String.raw`
|
|
|
4073
4397
|
function delay(ms) {
|
|
4074
4398
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4075
4399
|
}
|
|
4076
|
-
const EXECUTE_SCRIPT_TIMEOUT_MS =
|
|
4400
|
+
const EXECUTE_SCRIPT_TIMEOUT_MS = 3e3;
|
|
4077
4401
|
async function waitForDomReady(webContents, timeoutMs = 1500) {
|
|
4078
4402
|
const deadline = Date.now() + timeoutMs;
|
|
4079
4403
|
while (Date.now() < deadline) {
|
|
@@ -4189,7 +4513,25 @@ function mergePageContent(candidates, webContents) {
|
|
|
4189
4513
|
url: mergedBase.url || webContents.getURL() || ""
|
|
4190
4514
|
};
|
|
4191
4515
|
}
|
|
4192
|
-
const
|
|
4516
|
+
const EXTRACT_TIMEOUT_BASE_MS = 12e3;
|
|
4517
|
+
const EXTRACT_TIMEOUT_MAX_MS = 2e4;
|
|
4518
|
+
async function estimateExtractionTimeout(webContents) {
|
|
4519
|
+
try {
|
|
4520
|
+
const elementCount = await executeScript(
|
|
4521
|
+
webContents,
|
|
4522
|
+
`(function() { try { return document.querySelectorAll('*').length; } catch { return 0; } })()`
|
|
4523
|
+
);
|
|
4524
|
+
if (typeof elementCount === "number" && elementCount > 5e3) {
|
|
4525
|
+
const extra = Math.min(
|
|
4526
|
+
EXTRACT_TIMEOUT_MAX_MS - EXTRACT_TIMEOUT_BASE_MS,
|
|
4527
|
+
Math.ceil((elementCount - 5e3) / 2e3) * 1e3
|
|
4528
|
+
);
|
|
4529
|
+
return EXTRACT_TIMEOUT_BASE_MS + extra;
|
|
4530
|
+
}
|
|
4531
|
+
} catch {
|
|
4532
|
+
}
|
|
4533
|
+
return EXTRACT_TIMEOUT_BASE_MS;
|
|
4534
|
+
}
|
|
4193
4535
|
async function extractContentInner(webContents) {
|
|
4194
4536
|
await waitForDomReady(webContents);
|
|
4195
4537
|
const [preloadResult, directResult, safeResult] = await Promise.all([
|
|
@@ -4204,20 +4546,29 @@ async function extractContentInner(webContents) {
|
|
|
4204
4546
|
}
|
|
4205
4547
|
async function extractContent(webContents) {
|
|
4206
4548
|
try {
|
|
4549
|
+
const timeoutMs = await estimateExtractionTimeout(webContents);
|
|
4207
4550
|
return await Promise.race([
|
|
4208
4551
|
extractContentInner(webContents),
|
|
4209
4552
|
new Promise(
|
|
4210
4553
|
(_, reject) => setTimeout(
|
|
4211
4554
|
() => reject(new Error("extractContent timeout")),
|
|
4212
|
-
|
|
4555
|
+
timeoutMs
|
|
4213
4556
|
)
|
|
4214
4557
|
)
|
|
4215
4558
|
]);
|
|
4216
|
-
} catch {
|
|
4559
|
+
} catch (err) {
|
|
4560
|
+
const url = webContents.getURL() || "";
|
|
4561
|
+
let domain = "unknown";
|
|
4562
|
+
try {
|
|
4563
|
+
domain = new URL(url).hostname;
|
|
4564
|
+
} catch {
|
|
4565
|
+
}
|
|
4566
|
+
const reason = err instanceof Error ? err.message : "unknown";
|
|
4567
|
+
trackExtractionFailed(domain, reason);
|
|
4217
4568
|
return {
|
|
4218
4569
|
...EMPTY_PAGE_CONTENT,
|
|
4219
4570
|
title: webContents.getTitle() || "",
|
|
4220
|
-
url
|
|
4571
|
+
url
|
|
4221
4572
|
};
|
|
4222
4573
|
}
|
|
4223
4574
|
}
|
|
@@ -4396,6 +4747,207 @@ function setMcpHealth(update) {
|
|
|
4396
4747
|
}
|
|
4397
4748
|
}
|
|
4398
4749
|
}
|
|
4750
|
+
const VAULT_FILENAME = "vessel-vault.enc";
|
|
4751
|
+
const KEY_FILENAME = "vessel-vault.key";
|
|
4752
|
+
const ALGORITHM = "aes-256-gcm";
|
|
4753
|
+
const IV_LENGTH = 12;
|
|
4754
|
+
const AUTH_TAG_LENGTH = 16;
|
|
4755
|
+
let cachedEntries = null;
|
|
4756
|
+
function getVaultDir() {
|
|
4757
|
+
return electron.app.getPath("userData");
|
|
4758
|
+
}
|
|
4759
|
+
function getVaultPath() {
|
|
4760
|
+
return path$1.join(getVaultDir(), VAULT_FILENAME);
|
|
4761
|
+
}
|
|
4762
|
+
function getKeyPath() {
|
|
4763
|
+
return path$1.join(getVaultDir(), KEY_FILENAME);
|
|
4764
|
+
}
|
|
4765
|
+
function getOrCreateEncryptionKey() {
|
|
4766
|
+
const keyPath = getKeyPath();
|
|
4767
|
+
if (fs$1.existsSync(keyPath)) {
|
|
4768
|
+
const encryptedKey = fs$1.readFileSync(keyPath);
|
|
4769
|
+
if (electron.safeStorage.isEncryptionAvailable()) {
|
|
4770
|
+
return electron.safeStorage.decryptString ? Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8") : encryptedKey;
|
|
4771
|
+
}
|
|
4772
|
+
return encryptedKey;
|
|
4773
|
+
}
|
|
4774
|
+
const key = crypto$1.randomBytes(32);
|
|
4775
|
+
fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
|
|
4776
|
+
if (electron.safeStorage.isEncryptionAvailable()) {
|
|
4777
|
+
const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
|
|
4778
|
+
fs$1.writeFileSync(keyPath, encrypted);
|
|
4779
|
+
} else {
|
|
4780
|
+
fs$1.writeFileSync(keyPath, key);
|
|
4781
|
+
fs$1.chmodSync(keyPath, 384);
|
|
4782
|
+
}
|
|
4783
|
+
return key;
|
|
4784
|
+
}
|
|
4785
|
+
function encrypt(plaintext) {
|
|
4786
|
+
const key = getOrCreateEncryptionKey();
|
|
4787
|
+
const iv = crypto$1.randomBytes(IV_LENGTH);
|
|
4788
|
+
const cipher = crypto$1.createCipheriv(ALGORITHM, key, iv, {
|
|
4789
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
4790
|
+
});
|
|
4791
|
+
const encrypted = Buffer.concat([
|
|
4792
|
+
cipher.update(plaintext, "utf-8"),
|
|
4793
|
+
cipher.final()
|
|
4794
|
+
]);
|
|
4795
|
+
const authTag = cipher.getAuthTag();
|
|
4796
|
+
return Buffer.concat([iv, authTag, encrypted]);
|
|
4797
|
+
}
|
|
4798
|
+
function decrypt(data) {
|
|
4799
|
+
const key = getOrCreateEncryptionKey();
|
|
4800
|
+
const iv = data.subarray(0, IV_LENGTH);
|
|
4801
|
+
const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
|
|
4802
|
+
const ciphertext = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
|
|
4803
|
+
const decipher = crypto$1.createDecipheriv(ALGORITHM, key, iv, {
|
|
4804
|
+
authTagLength: AUTH_TAG_LENGTH
|
|
4805
|
+
});
|
|
4806
|
+
decipher.setAuthTag(authTag);
|
|
4807
|
+
return decipher.update(ciphertext, void 0, "utf-8") + decipher.final("utf-8");
|
|
4808
|
+
}
|
|
4809
|
+
function loadVault() {
|
|
4810
|
+
if (cachedEntries) return cachedEntries;
|
|
4811
|
+
const vaultPath = getVaultPath();
|
|
4812
|
+
if (!fs$1.existsSync(vaultPath)) {
|
|
4813
|
+
cachedEntries = [];
|
|
4814
|
+
return cachedEntries;
|
|
4815
|
+
}
|
|
4816
|
+
try {
|
|
4817
|
+
const raw = fs$1.readFileSync(vaultPath);
|
|
4818
|
+
const json = decrypt(raw);
|
|
4819
|
+
cachedEntries = JSON.parse(json);
|
|
4820
|
+
return cachedEntries;
|
|
4821
|
+
} catch (err) {
|
|
4822
|
+
console.error("[Vessel Vault] Failed to load vault:", err);
|
|
4823
|
+
cachedEntries = [];
|
|
4824
|
+
return cachedEntries;
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
function saveVault(entries) {
|
|
4828
|
+
const json = JSON.stringify(entries, null, 2);
|
|
4829
|
+
const encrypted = encrypt(json);
|
|
4830
|
+
const vaultPath = getVaultPath();
|
|
4831
|
+
fs$1.mkdirSync(path$1.dirname(vaultPath), { recursive: true });
|
|
4832
|
+
fs$1.writeFileSync(vaultPath, encrypted);
|
|
4833
|
+
fs$1.chmodSync(vaultPath, 384);
|
|
4834
|
+
cachedEntries = entries;
|
|
4835
|
+
}
|
|
4836
|
+
function domainMatches(pattern, hostname) {
|
|
4837
|
+
const p = pattern.toLowerCase().trim();
|
|
4838
|
+
const h = hostname.toLowerCase().trim();
|
|
4839
|
+
if (p === h) return true;
|
|
4840
|
+
if (p.startsWith("*.")) {
|
|
4841
|
+
const suffix = p.slice(2);
|
|
4842
|
+
return h === suffix || h.endsWith("." + suffix);
|
|
4843
|
+
}
|
|
4844
|
+
return false;
|
|
4845
|
+
}
|
|
4846
|
+
function listEntries() {
|
|
4847
|
+
return loadVault().map(({ password, totpSecret, ...rest }) => rest);
|
|
4848
|
+
}
|
|
4849
|
+
function findEntriesForDomain(url) {
|
|
4850
|
+
let hostname;
|
|
4851
|
+
try {
|
|
4852
|
+
hostname = new URL(url).hostname;
|
|
4853
|
+
} catch {
|
|
4854
|
+
return [];
|
|
4855
|
+
}
|
|
4856
|
+
return loadVault().filter((e) => domainMatches(e.domainPattern, hostname)).map(({ password, totpSecret, ...rest }) => rest);
|
|
4857
|
+
}
|
|
4858
|
+
function addEntry(entry) {
|
|
4859
|
+
const entries = loadVault();
|
|
4860
|
+
const newEntry = {
|
|
4861
|
+
...entry,
|
|
4862
|
+
id: crypto$1.randomUUID(),
|
|
4863
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4864
|
+
useCount: 0
|
|
4865
|
+
};
|
|
4866
|
+
entries.push(newEntry);
|
|
4867
|
+
saveVault(entries);
|
|
4868
|
+
return newEntry;
|
|
4869
|
+
}
|
|
4870
|
+
function updateEntry(id, updates) {
|
|
4871
|
+
const entries = loadVault();
|
|
4872
|
+
const idx = entries.findIndex((e) => e.id === id);
|
|
4873
|
+
if (idx === -1) return null;
|
|
4874
|
+
entries[idx] = { ...entries[idx], ...updates };
|
|
4875
|
+
saveVault(entries);
|
|
4876
|
+
return entries[idx];
|
|
4877
|
+
}
|
|
4878
|
+
function removeEntry(id) {
|
|
4879
|
+
const entries = loadVault();
|
|
4880
|
+
const idx = entries.findIndex((e) => e.id === id);
|
|
4881
|
+
if (idx === -1) return false;
|
|
4882
|
+
entries.splice(idx, 1);
|
|
4883
|
+
saveVault(entries);
|
|
4884
|
+
return true;
|
|
4885
|
+
}
|
|
4886
|
+
function recordUsage(id) {
|
|
4887
|
+
const entries = loadVault();
|
|
4888
|
+
const entry = entries.find((e) => e.id === id);
|
|
4889
|
+
if (!entry) return;
|
|
4890
|
+
entry.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4891
|
+
entry.useCount += 1;
|
|
4892
|
+
saveVault(entries);
|
|
4893
|
+
}
|
|
4894
|
+
function getCredential(id) {
|
|
4895
|
+
const entry = loadVault().find((e) => e.id === id);
|
|
4896
|
+
if (!entry) return null;
|
|
4897
|
+
return { username: entry.username, password: entry.password };
|
|
4898
|
+
}
|
|
4899
|
+
function getTotpSecret(id) {
|
|
4900
|
+
const entry = loadVault().find((e) => e.id === id);
|
|
4901
|
+
return entry?.totpSecret ?? null;
|
|
4902
|
+
}
|
|
4903
|
+
function generateTotpCode(secret) {
|
|
4904
|
+
const epoch = Math.floor(Date.now() / 1e3);
|
|
4905
|
+
const counter = Math.floor(epoch / 30);
|
|
4906
|
+
const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
4907
|
+
const cleanSecret = secret.replace(/[\s=-]/g, "").toUpperCase();
|
|
4908
|
+
let bits = "";
|
|
4909
|
+
for (const ch of cleanSecret) {
|
|
4910
|
+
const val = base32Chars.indexOf(ch);
|
|
4911
|
+
if (val === -1) continue;
|
|
4912
|
+
bits += val.toString(2).padStart(5, "0");
|
|
4913
|
+
}
|
|
4914
|
+
const keyBytes = Buffer.alloc(Math.floor(bits.length / 8));
|
|
4915
|
+
for (let i = 0; i < keyBytes.length; i++) {
|
|
4916
|
+
keyBytes[i] = parseInt(bits.slice(i * 8, i * 8 + 8), 2);
|
|
4917
|
+
}
|
|
4918
|
+
const counterBuf = Buffer.alloc(8);
|
|
4919
|
+
counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
|
|
4920
|
+
counterBuf.writeUInt32BE(counter & 4294967295, 4);
|
|
4921
|
+
const hmac = crypto$1.createHmac("sha1", keyBytes).update(counterBuf).digest();
|
|
4922
|
+
const offset = hmac[hmac.length - 1] & 15;
|
|
4923
|
+
const code = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
|
|
4924
|
+
return (code % 1e6).toString().padStart(6, "0");
|
|
4925
|
+
}
|
|
4926
|
+
const AUDIT_FILENAME = "vessel-vault-audit.jsonl";
|
|
4927
|
+
const MAX_ENTRIES = 1e3;
|
|
4928
|
+
function getAuditPath() {
|
|
4929
|
+
return path$1.join(electron.app.getPath("userData"), AUDIT_FILENAME);
|
|
4930
|
+
}
|
|
4931
|
+
function appendAuditEntry(entry) {
|
|
4932
|
+
try {
|
|
4933
|
+
const auditPath = getAuditPath();
|
|
4934
|
+
fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
|
|
4935
|
+
fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
|
|
4936
|
+
} catch (err) {
|
|
4937
|
+
console.error("[Vessel Vault] Failed to write audit log:", err);
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
function readAuditLog(limit = 100) {
|
|
4941
|
+
try {
|
|
4942
|
+
const auditPath = getAuditPath();
|
|
4943
|
+
if (!fs$1.existsSync(auditPath)) return [];
|
|
4944
|
+
const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
|
|
4945
|
+
return lines.slice(-Math.min(limit, MAX_ENTRIES)).map((line) => JSON.parse(line)).reverse();
|
|
4946
|
+
} catch (err) {
|
|
4947
|
+
console.error("[Vessel Vault] Failed to read audit log:", err);
|
|
4948
|
+
return [];
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4399
4951
|
function isRichToolResult(value) {
|
|
4400
4952
|
return typeof value === "object" && value !== null && value.__richResult === true;
|
|
4401
4953
|
}
|
|
@@ -4409,7 +4961,6 @@ function makeImageResult(base64, description, mediaType = "image/png") {
|
|
|
4409
4961
|
};
|
|
4410
4962
|
return JSON.stringify(result);
|
|
4411
4963
|
}
|
|
4412
|
-
const DEFAULT_MAX_ITERATIONS$1 = 200;
|
|
4413
4964
|
class AnthropicProvider {
|
|
4414
4965
|
client;
|
|
4415
4966
|
model;
|
|
@@ -4457,7 +5008,7 @@ class AnthropicProvider {
|
|
|
4457
5008
|
{ role: "user", content: userMessage }
|
|
4458
5009
|
];
|
|
4459
5010
|
try {
|
|
4460
|
-
const maxIterations =
|
|
5011
|
+
const maxIterations = getEffectiveMaxIterations();
|
|
4461
5012
|
let iterationsUsed = 0;
|
|
4462
5013
|
for (let i = 0; i < maxIterations; i++) {
|
|
4463
5014
|
iterationsUsed = i + 1;
|
|
@@ -4515,7 +5066,8 @@ class AnthropicProvider {
|
|
|
4515
5066
|
toolUseBlocks.push({
|
|
4516
5067
|
id: currentToolUse.id,
|
|
4517
5068
|
name: currentToolUse.name,
|
|
4518
|
-
input: {}
|
|
5069
|
+
input: {},
|
|
5070
|
+
_malformedArgs: currentToolUse.inputJson
|
|
4519
5071
|
});
|
|
4520
5072
|
}
|
|
4521
5073
|
currentToolUse = null;
|
|
@@ -4543,6 +5095,18 @@ class AnthropicProvider {
|
|
|
4543
5095
|
}
|
|
4544
5096
|
const toolResults = [];
|
|
4545
5097
|
for (const tb of toolUseBlocks) {
|
|
5098
|
+
if (tb._malformedArgs !== void 0) {
|
|
5099
|
+
onChunk(`
|
|
5100
|
+
<<tool:${tb.name}:⚠ invalid args>>
|
|
5101
|
+
`);
|
|
5102
|
+
toolResults.push({
|
|
5103
|
+
type: "tool_result",
|
|
5104
|
+
tool_use_id: tb.id,
|
|
5105
|
+
content: `Error: Invalid JSON in tool arguments — could not parse. Please retry with valid JSON. Raw input: ${tb._malformedArgs.slice(0, 200)}`,
|
|
5106
|
+
is_error: true
|
|
5107
|
+
});
|
|
5108
|
+
continue;
|
|
5109
|
+
}
|
|
4546
5110
|
const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
|
|
4547
5111
|
onChunk(`
|
|
4548
5112
|
<<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
|
|
@@ -4698,7 +5262,6 @@ const PROVIDERS = {
|
|
|
4698
5262
|
apiKeyHint: "Any OpenAI-compatible API endpoint"
|
|
4699
5263
|
}
|
|
4700
5264
|
};
|
|
4701
|
-
const DEFAULT_MAX_ITERATIONS = 200;
|
|
4702
5265
|
function toOpenAITools(tools) {
|
|
4703
5266
|
return tools.map((t) => ({
|
|
4704
5267
|
type: "function",
|
|
@@ -4764,7 +5327,7 @@ class OpenAICompatProvider {
|
|
|
4764
5327
|
{ role: "user", content: userMessage }
|
|
4765
5328
|
];
|
|
4766
5329
|
try {
|
|
4767
|
-
const maxIterations =
|
|
5330
|
+
const maxIterations = getEffectiveMaxIterations();
|
|
4768
5331
|
let iterationsUsed = 0;
|
|
4769
5332
|
for (let i = 0; i < maxIterations; i++) {
|
|
4770
5333
|
iterationsUsed = i + 1;
|
|
@@ -4808,10 +5371,12 @@ class OpenAICompatProvider {
|
|
|
4808
5371
|
for (const tc of Object.values(toolCallAccums)) {
|
|
4809
5372
|
if (!tc.id) tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
4810
5373
|
}
|
|
5374
|
+
const malformedToolCalls = /* @__PURE__ */ new Set();
|
|
4811
5375
|
for (const tc of toolCalls) {
|
|
4812
5376
|
try {
|
|
4813
5377
|
JSON.parse(tc.argsJson || "{}");
|
|
4814
5378
|
} catch {
|
|
5379
|
+
malformedToolCalls.add(tc.id);
|
|
4815
5380
|
tc.argsJson = "{}";
|
|
4816
5381
|
}
|
|
4817
5382
|
}
|
|
@@ -4829,6 +5394,17 @@ class OpenAICompatProvider {
|
|
|
4829
5394
|
messages.push(assistantMsg);
|
|
4830
5395
|
if (toolCalls.length === 0) break;
|
|
4831
5396
|
for (const tc of toolCalls) {
|
|
5397
|
+
if (malformedToolCalls.has(tc.id)) {
|
|
5398
|
+
onChunk(`
|
|
5399
|
+
<<tool:${tc.name}:⚠ invalid args>>
|
|
5400
|
+
`);
|
|
5401
|
+
messages.push({
|
|
5402
|
+
role: "tool",
|
|
5403
|
+
tool_call_id: tc.id,
|
|
5404
|
+
content: `Error: Invalid JSON in tool arguments. The arguments could not be parsed. Please retry with valid JSON.`
|
|
5405
|
+
});
|
|
5406
|
+
continue;
|
|
5407
|
+
}
|
|
4832
5408
|
let args = {};
|
|
4833
5409
|
try {
|
|
4834
5410
|
args = JSON.parse(tc.argsJson || "{}");
|
|
@@ -4839,7 +5415,7 @@ class OpenAICompatProvider {
|
|
|
4839
5415
|
messages.push({
|
|
4840
5416
|
role: "tool",
|
|
4841
5417
|
tool_call_id: tc.id,
|
|
4842
|
-
content: `Error: Invalid JSON in tool arguments. Please retry with valid JSON
|
|
5418
|
+
content: `Error: Invalid JSON in tool arguments. Please retry with valid JSON.`
|
|
4843
5419
|
});
|
|
4844
5420
|
continue;
|
|
4845
5421
|
}
|
|
@@ -7275,7 +7851,7 @@ function pruneToolsForContext(tools, pageType, query = "") {
|
|
|
7275
7851
|
const ctx = pageType ?? "GENERAL";
|
|
7276
7852
|
const hints = CONTEXT_HINTS[ctx] ?? {};
|
|
7277
7853
|
const intents = inferIntent(query);
|
|
7278
|
-
const scored = tools.filter((tool) => shouldIncludeTool(tool.name, ctx, intents)).map((tool) => ({
|
|
7854
|
+
const scored = tools.filter((tool) => !isToolGated(tool.name)).filter((tool) => shouldIncludeTool(tool.name, ctx, intents)).map((tool) => ({
|
|
7279
7855
|
tool,
|
|
7280
7856
|
score: scoreForContext(tool.name, ctx)
|
|
7281
7857
|
}));
|
|
@@ -10773,6 +11349,10 @@ async function executeAction(name, args, ctx) {
|
|
|
10773
11349
|
].includes(name)) {
|
|
10774
11350
|
return "Error: No active tab";
|
|
10775
11351
|
}
|
|
11352
|
+
trackToolCall(name);
|
|
11353
|
+
if (isToolGated(name)) {
|
|
11354
|
+
return `This tool (${name}) requires Vessel Premium. Upgrade at Settings > Premium to unlock screenshot, session management, workflow tracking, and more.`;
|
|
11355
|
+
}
|
|
10776
11356
|
const wc = tab?.view.webContents;
|
|
10777
11357
|
const result = await ctx.runtime.runControlledAction({
|
|
10778
11358
|
source: "ai",
|
|
@@ -12884,6 +13464,41 @@ Exception: ${result.exceptionDetails}`);
|
|
|
12884
13464
|
}
|
|
12885
13465
|
);
|
|
12886
13466
|
}
|
|
13467
|
+
const sessionTrustedDomains = /* @__PURE__ */ new Set();
|
|
13468
|
+
async function requestConsent(request) {
|
|
13469
|
+
const domain = request.domain.toLowerCase();
|
|
13470
|
+
if (sessionTrustedDomains.has(domain)) {
|
|
13471
|
+
return { approved: true, trustForSession: true };
|
|
13472
|
+
}
|
|
13473
|
+
const focusedWindow = electron.BrowserWindow.getFocusedWindow();
|
|
13474
|
+
const { response } = await electron.dialog.showMessageBox(
|
|
13475
|
+
focusedWindow ?? (electron.BrowserWindow.getAllWindows()[0] || null),
|
|
13476
|
+
{
|
|
13477
|
+
type: "question",
|
|
13478
|
+
title: "Agent Credential Access",
|
|
13479
|
+
message: `Agent wants to sign in to ${request.domain}`,
|
|
13480
|
+
detail: [
|
|
13481
|
+
`Credential: ${request.credentialLabel}`,
|
|
13482
|
+
`Username: ${request.username}`,
|
|
13483
|
+
"",
|
|
13484
|
+
"The agent is requesting to fill a login form with stored credentials.",
|
|
13485
|
+
"Credential values will NOT be sent to the AI provider."
|
|
13486
|
+
].join("\n"),
|
|
13487
|
+
buttons: ["Deny", "Allow Once", "Allow for Session"],
|
|
13488
|
+
defaultId: 1,
|
|
13489
|
+
cancelId: 0,
|
|
13490
|
+
noLink: true
|
|
13491
|
+
}
|
|
13492
|
+
);
|
|
13493
|
+
if (response === 0) {
|
|
13494
|
+
return { approved: false, trustForSession: false };
|
|
13495
|
+
}
|
|
13496
|
+
const trustForSession = response === 2;
|
|
13497
|
+
if (trustForSession) {
|
|
13498
|
+
sessionTrustedDomains.add(domain);
|
|
13499
|
+
}
|
|
13500
|
+
return { approved: true, trustForSession };
|
|
13501
|
+
}
|
|
12887
13502
|
let httpServer = null;
|
|
12888
13503
|
let mcpAuthToken = null;
|
|
12889
13504
|
function asTextResponse(text) {
|
|
@@ -17033,6 +17648,244 @@ ${JSON.stringify(tableJson, null, 2)}`;
|
|
|
17033
17648
|
);
|
|
17034
17649
|
}
|
|
17035
17650
|
);
|
|
17651
|
+
server.registerTool(
|
|
17652
|
+
"vessel_vault_status",
|
|
17653
|
+
{
|
|
17654
|
+
title: "Check Vault Credentials",
|
|
17655
|
+
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.",
|
|
17656
|
+
inputSchema: {
|
|
17657
|
+
domain: zod.z.string().describe(
|
|
17658
|
+
"The domain to check credentials for (e.g. 'github.com'). If omitted, checks the active tab's domain."
|
|
17659
|
+
).optional()
|
|
17660
|
+
}
|
|
17661
|
+
},
|
|
17662
|
+
async ({ domain }) => {
|
|
17663
|
+
let targetDomain = domain;
|
|
17664
|
+
if (!targetDomain) {
|
|
17665
|
+
const tab = tabManager.getActiveTab();
|
|
17666
|
+
if (!tab) return asTextResponse("Error: No active tab and no domain specified");
|
|
17667
|
+
try {
|
|
17668
|
+
targetDomain = new URL(tab.state.url).hostname;
|
|
17669
|
+
} catch {
|
|
17670
|
+
return asTextResponse("Error: Could not parse active tab URL");
|
|
17671
|
+
}
|
|
17672
|
+
}
|
|
17673
|
+
const matches = findEntriesForDomain(
|
|
17674
|
+
targetDomain.includes("://") ? targetDomain : `https://${targetDomain}`
|
|
17675
|
+
);
|
|
17676
|
+
if (matches.length === 0) {
|
|
17677
|
+
return asTextResponse(
|
|
17678
|
+
`No stored credentials found for ${targetDomain}. The user needs to add credentials in Settings > Agent Credential Vault before the agent can log in.`
|
|
17679
|
+
);
|
|
17680
|
+
}
|
|
17681
|
+
appendAuditEntry({
|
|
17682
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17683
|
+
credentialId: matches[0].id,
|
|
17684
|
+
credentialLabel: matches[0].label,
|
|
17685
|
+
domain: targetDomain,
|
|
17686
|
+
action: "status_check",
|
|
17687
|
+
approved: true
|
|
17688
|
+
});
|
|
17689
|
+
const summary = matches.map((m) => ` - "${m.label}" (${m.username})`).join("\n");
|
|
17690
|
+
return asTextResponse(
|
|
17691
|
+
`Found ${matches.length} credential(s) for ${targetDomain}:
|
|
17692
|
+
${summary}
|
|
17693
|
+
|
|
17694
|
+
Use vessel_vault_login to fill the login form. Credentials are filled directly — you will NOT see the password values.`
|
|
17695
|
+
);
|
|
17696
|
+
}
|
|
17697
|
+
);
|
|
17698
|
+
server.registerTool(
|
|
17699
|
+
"vessel_vault_login",
|
|
17700
|
+
{
|
|
17701
|
+
title: "Fill Login with Vault Credentials",
|
|
17702
|
+
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.",
|
|
17703
|
+
inputSchema: {
|
|
17704
|
+
credential_label: zod.z.string().optional().describe(
|
|
17705
|
+
"Label of the credential to use. If omitted, uses the first matching credential for the current domain."
|
|
17706
|
+
),
|
|
17707
|
+
username_index: zod.z.number().optional().describe(
|
|
17708
|
+
"Element index of the username/email input field from read_page."
|
|
17709
|
+
),
|
|
17710
|
+
password_index: zod.z.number().optional().describe(
|
|
17711
|
+
"Element index of the password input field from read_page."
|
|
17712
|
+
),
|
|
17713
|
+
submit_after: zod.z.boolean().optional().describe(
|
|
17714
|
+
"Whether to click the submit button after filling credentials. Defaults to false."
|
|
17715
|
+
),
|
|
17716
|
+
submit_index: zod.z.number().optional().describe(
|
|
17717
|
+
"Element index of the submit button. Required if submit_after is true."
|
|
17718
|
+
)
|
|
17719
|
+
}
|
|
17720
|
+
},
|
|
17721
|
+
async ({
|
|
17722
|
+
credential_label,
|
|
17723
|
+
username_index,
|
|
17724
|
+
password_index,
|
|
17725
|
+
submit_after,
|
|
17726
|
+
submit_index
|
|
17727
|
+
}) => {
|
|
17728
|
+
const tab = tabManager.getActiveTab();
|
|
17729
|
+
if (!tab) return asTextResponse("Error: No active tab");
|
|
17730
|
+
const wc = tab.view.webContents;
|
|
17731
|
+
let hostname;
|
|
17732
|
+
try {
|
|
17733
|
+
hostname = new URL(tab.state.url).hostname;
|
|
17734
|
+
} catch {
|
|
17735
|
+
return asTextResponse("Error: Could not parse active tab URL");
|
|
17736
|
+
}
|
|
17737
|
+
const matches = findEntriesForDomain(`https://${hostname}`);
|
|
17738
|
+
if (matches.length === 0) {
|
|
17739
|
+
return asTextResponse(
|
|
17740
|
+
`No stored credentials for ${hostname}. The user needs to add credentials in Settings > Agent Credential Vault.`
|
|
17741
|
+
);
|
|
17742
|
+
}
|
|
17743
|
+
const match = credential_label ? matches.find(
|
|
17744
|
+
(m) => m.label.toLowerCase() === credential_label.toLowerCase()
|
|
17745
|
+
) : matches[0];
|
|
17746
|
+
if (!match) {
|
|
17747
|
+
return asTextResponse(
|
|
17748
|
+
`No credential named "${credential_label}" found for ${hostname}. Available: ${matches.map((m) => m.label).join(", ")}`
|
|
17749
|
+
);
|
|
17750
|
+
}
|
|
17751
|
+
const consent = await requestConsent({
|
|
17752
|
+
credentialLabel: match.label,
|
|
17753
|
+
username: match.username,
|
|
17754
|
+
domain: hostname
|
|
17755
|
+
});
|
|
17756
|
+
appendAuditEntry({
|
|
17757
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17758
|
+
credentialId: match.id,
|
|
17759
|
+
credentialLabel: match.label,
|
|
17760
|
+
domain: hostname,
|
|
17761
|
+
action: "login_fill",
|
|
17762
|
+
approved: consent.approved
|
|
17763
|
+
});
|
|
17764
|
+
if (!consent.approved) {
|
|
17765
|
+
return asTextResponse(
|
|
17766
|
+
`User denied credential access for ${hostname}. The agent should not retry without being asked.`
|
|
17767
|
+
);
|
|
17768
|
+
}
|
|
17769
|
+
const creds = getCredential(match.id);
|
|
17770
|
+
if (!creds) {
|
|
17771
|
+
return asTextResponse("Error: Credential not found in vault");
|
|
17772
|
+
}
|
|
17773
|
+
const results = [];
|
|
17774
|
+
if (username_index != null) {
|
|
17775
|
+
const usernameResult = await wc.executeJavaScript(
|
|
17776
|
+
`window.__vessel?.interactByIndex?.(${username_index}, "value", ${JSON.stringify(creds.username)}) || "Error: interactByIndex not available"`
|
|
17777
|
+
);
|
|
17778
|
+
results.push(`Username: ${usernameResult}`);
|
|
17779
|
+
}
|
|
17780
|
+
if (password_index != null) {
|
|
17781
|
+
const passwordResult = await wc.executeJavaScript(
|
|
17782
|
+
`window.__vessel?.interactByIndex?.(${password_index}, "value", ${JSON.stringify(creds.password)}) || "Error: interactByIndex not available"`
|
|
17783
|
+
);
|
|
17784
|
+
results.push(`Password: ${passwordResult.replace(/Typed into:.*/, "Typed into: [password field]")}`);
|
|
17785
|
+
}
|
|
17786
|
+
recordUsage(match.id);
|
|
17787
|
+
trackVaultAction("login_fill");
|
|
17788
|
+
if (submit_after && submit_index != null) {
|
|
17789
|
+
const submitResult = await wc.executeJavaScript(
|
|
17790
|
+
`window.__vessel?.interactByIndex?.(${submit_index}, "click") || "Error: interactByIndex not available"`
|
|
17791
|
+
);
|
|
17792
|
+
results.push(`Submit: ${submitResult}`);
|
|
17793
|
+
}
|
|
17794
|
+
return asTextResponse(
|
|
17795
|
+
[
|
|
17796
|
+
`Login form filled for ${hostname} using credential "${match.label}".`,
|
|
17797
|
+
...results,
|
|
17798
|
+
"",
|
|
17799
|
+
"Note: Credential values were filled directly into the page. They are NOT included in this response."
|
|
17800
|
+
].join("\n")
|
|
17801
|
+
);
|
|
17802
|
+
}
|
|
17803
|
+
);
|
|
17804
|
+
server.registerTool(
|
|
17805
|
+
"vessel_vault_totp",
|
|
17806
|
+
{
|
|
17807
|
+
title: "Fill TOTP Code from Vault",
|
|
17808
|
+
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.",
|
|
17809
|
+
inputSchema: {
|
|
17810
|
+
credential_label: zod.z.string().optional().describe(
|
|
17811
|
+
"Label of the credential whose TOTP secret to use. If omitted, uses the first matching credential with a TOTP secret."
|
|
17812
|
+
),
|
|
17813
|
+
code_index: zod.z.number().describe(
|
|
17814
|
+
"Element index of the TOTP/2FA code input field from read_page."
|
|
17815
|
+
),
|
|
17816
|
+
submit_after: zod.z.boolean().optional().describe("Whether to click submit after filling the code."),
|
|
17817
|
+
submit_index: zod.z.number().optional().describe("Element index of the submit button.")
|
|
17818
|
+
}
|
|
17819
|
+
},
|
|
17820
|
+
async ({ credential_label, code_index, submit_after, submit_index }) => {
|
|
17821
|
+
const tab = tabManager.getActiveTab();
|
|
17822
|
+
if (!tab) return asTextResponse("Error: No active tab");
|
|
17823
|
+
const wc = tab.view.webContents;
|
|
17824
|
+
let hostname;
|
|
17825
|
+
try {
|
|
17826
|
+
hostname = new URL(tab.state.url).hostname;
|
|
17827
|
+
} catch {
|
|
17828
|
+
return asTextResponse("Error: Could not parse active tab URL");
|
|
17829
|
+
}
|
|
17830
|
+
const matches = findEntriesForDomain(`https://${hostname}`);
|
|
17831
|
+
const match = credential_label ? matches.find(
|
|
17832
|
+
(m) => m.label.toLowerCase() === credential_label.toLowerCase()
|
|
17833
|
+
) : matches.find((m) => {
|
|
17834
|
+
const secret2 = getTotpSecret(m.id);
|
|
17835
|
+
return secret2 != null;
|
|
17836
|
+
});
|
|
17837
|
+
if (!match) {
|
|
17838
|
+
return asTextResponse(
|
|
17839
|
+
`No credential with TOTP secret found for ${hostname}.`
|
|
17840
|
+
);
|
|
17841
|
+
}
|
|
17842
|
+
const secret = getTotpSecret(match.id);
|
|
17843
|
+
if (!secret) {
|
|
17844
|
+
return asTextResponse(
|
|
17845
|
+
`Credential "${match.label}" does not have a TOTP secret configured.`
|
|
17846
|
+
);
|
|
17847
|
+
}
|
|
17848
|
+
const consent = await requestConsent({
|
|
17849
|
+
credentialLabel: match.label,
|
|
17850
|
+
username: match.username,
|
|
17851
|
+
domain: hostname
|
|
17852
|
+
});
|
|
17853
|
+
appendAuditEntry({
|
|
17854
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17855
|
+
credentialId: match.id,
|
|
17856
|
+
credentialLabel: match.label,
|
|
17857
|
+
domain: hostname,
|
|
17858
|
+
action: "totp_generate",
|
|
17859
|
+
approved: consent.approved
|
|
17860
|
+
});
|
|
17861
|
+
if (!consent.approved) {
|
|
17862
|
+
return asTextResponse(
|
|
17863
|
+
`User denied TOTP access for ${hostname}.`
|
|
17864
|
+
);
|
|
17865
|
+
}
|
|
17866
|
+
const code = generateTotpCode(secret);
|
|
17867
|
+
const fillResult = await wc.executeJavaScript(
|
|
17868
|
+
`window.__vessel?.interactByIndex?.(${code_index}, "value", ${JSON.stringify(code)}) || "Error: interactByIndex not available"`
|
|
17869
|
+
);
|
|
17870
|
+
recordUsage(match.id);
|
|
17871
|
+
trackVaultAction("totp_fill");
|
|
17872
|
+
const results = [`2FA code filled: ${fillResult.replace(/Typed into:.*/, "Typed into: [2FA field]")}`];
|
|
17873
|
+
if (submit_after && submit_index != null) {
|
|
17874
|
+
const submitResult = await wc.executeJavaScript(
|
|
17875
|
+
`window.__vessel?.interactByIndex?.(${submit_index}, "click") || "Error: interactByIndex not available"`
|
|
17876
|
+
);
|
|
17877
|
+
results.push(`Submit: ${submitResult}`);
|
|
17878
|
+
}
|
|
17879
|
+
return asTextResponse(
|
|
17880
|
+
[
|
|
17881
|
+
`TOTP code filled for ${hostname} using credential "${match.label}".`,
|
|
17882
|
+
...results,
|
|
17883
|
+
"",
|
|
17884
|
+
"Note: The TOTP code was filled directly into the page. It is NOT included in this response."
|
|
17885
|
+
].join("\n")
|
|
17886
|
+
);
|
|
17887
|
+
}
|
|
17888
|
+
);
|
|
17036
17889
|
server.registerTool(
|
|
17037
17890
|
"vessel_metrics",
|
|
17038
17891
|
{
|
|
@@ -17349,6 +18202,16 @@ function stopMcpServer() {
|
|
|
17349
18202
|
});
|
|
17350
18203
|
}
|
|
17351
18204
|
let activeChatProvider = null;
|
|
18205
|
+
function assertString(value, name) {
|
|
18206
|
+
if (typeof value !== "string") throw new Error(`${name} must be a string`);
|
|
18207
|
+
}
|
|
18208
|
+
function assertOptionalString(value, name) {
|
|
18209
|
+
if (value !== void 0 && typeof value !== "string") throw new Error(`${name} must be a string`);
|
|
18210
|
+
}
|
|
18211
|
+
function assertNumber(value, name) {
|
|
18212
|
+
if (typeof value !== "number" || Number.isNaN(value)) throw new Error(`${name} must be a number`);
|
|
18213
|
+
}
|
|
18214
|
+
const VALID_APPROVAL_MODES = /* @__PURE__ */ new Set(["auto", "confirm-dangerous", "manual"]);
|
|
17352
18215
|
function registerIpcHandlers(windowState, runtime2) {
|
|
17353
18216
|
const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
|
|
17354
18217
|
const sendToRendererViews = (channel, ...args) => {
|
|
@@ -17373,6 +18236,8 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17373
18236
|
layoutViews(windowState);
|
|
17374
18237
|
});
|
|
17375
18238
|
electron.ipcMain.handle(Channels.TAB_NAVIGATE, (_, id, url) => {
|
|
18239
|
+
assertString(id, "tabId");
|
|
18240
|
+
assertString(url, "url");
|
|
17376
18241
|
tabManager.navigateTab(id, url);
|
|
17377
18242
|
});
|
|
17378
18243
|
electron.ipcMain.handle(Channels.TAB_BACK, (_, id) => {
|
|
@@ -17398,6 +18263,7 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17398
18263
|
}
|
|
17399
18264
|
try {
|
|
17400
18265
|
activeChatProvider = createProvider(chatConfig);
|
|
18266
|
+
trackProviderConfigured(chatConfig.id);
|
|
17401
18267
|
const activeTab = tabManager.getActiveTab();
|
|
17402
18268
|
await handleAIQuery(
|
|
17403
18269
|
query,
|
|
@@ -17423,6 +18289,9 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17423
18289
|
});
|
|
17424
18290
|
electron.ipcMain.handle(Channels.AI_FETCH_MODELS, async (_, config) => {
|
|
17425
18291
|
try {
|
|
18292
|
+
if (!config || typeof config !== "object" || !("id" in config)) {
|
|
18293
|
+
return { ok: false, models: [], error: "Invalid provider configuration" };
|
|
18294
|
+
}
|
|
17426
18295
|
const models = await fetchProviderModels(config);
|
|
17427
18296
|
return { ok: true, models };
|
|
17428
18297
|
} catch (err) {
|
|
@@ -17466,6 +18335,7 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17466
18335
|
windowState.sidebarView.setBounds({ x: 0, y: 0, width, height });
|
|
17467
18336
|
});
|
|
17468
18337
|
electron.ipcMain.handle(Channels.SIDEBAR_RESIZE, (_, width) => {
|
|
18338
|
+
assertNumber(width, "width");
|
|
17469
18339
|
const clamped = Math.max(240, Math.min(800, Math.round(width)));
|
|
17470
18340
|
windowState.uiState.sidebarWidth = clamped;
|
|
17471
18341
|
return clamped;
|
|
@@ -17489,11 +18359,13 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17489
18359
|
});
|
|
17490
18360
|
electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, () => getRuntimeHealth());
|
|
17491
18361
|
electron.ipcMain.handle(Channels.SETTINGS_SET, async (_, key, value) => {
|
|
18362
|
+
assertString(key, "key");
|
|
17492
18363
|
if (!SETTABLE_KEYS.has(key)) {
|
|
17493
18364
|
throw new Error(`Unknown setting key: ${key}`);
|
|
17494
18365
|
}
|
|
17495
18366
|
const settingsKey = key;
|
|
17496
18367
|
const updatedSettings = setSetting(settingsKey, value);
|
|
18368
|
+
trackSettingChanged(key);
|
|
17497
18369
|
if (key === "approvalMode") {
|
|
17498
18370
|
runtime2.setApprovalMode(value);
|
|
17499
18371
|
}
|
|
@@ -17510,6 +18382,11 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17510
18382
|
electron.ipcMain.handle(
|
|
17511
18383
|
Channels.AGENT_SET_APPROVAL_MODE,
|
|
17512
18384
|
(_, mode) => {
|
|
18385
|
+
assertString(mode, "mode");
|
|
18386
|
+
if (!VALID_APPROVAL_MODES.has(mode)) {
|
|
18387
|
+
throw new Error(`Invalid approval mode: ${mode}`);
|
|
18388
|
+
}
|
|
18389
|
+
trackApprovalModeChanged(mode);
|
|
17513
18390
|
setSetting("approvalMode", mode);
|
|
17514
18391
|
return runtime2.setApprovalMode(mode);
|
|
17515
18392
|
}
|
|
@@ -17540,17 +18417,23 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17540
18417
|
electron.ipcMain.handle(
|
|
17541
18418
|
Channels.FOLDER_CREATE,
|
|
17542
18419
|
(_, name, summary) => {
|
|
18420
|
+
trackBookmarkAction("folder_create");
|
|
17543
18421
|
return createFolderWithSummary(name, summary);
|
|
17544
18422
|
}
|
|
17545
18423
|
);
|
|
17546
18424
|
electron.ipcMain.handle(
|
|
17547
18425
|
Channels.BOOKMARK_SAVE,
|
|
17548
|
-
(_, url, title, folderId, note) =>
|
|
18426
|
+
(_, url, title, folderId, note) => {
|
|
18427
|
+
trackBookmarkAction("save");
|
|
18428
|
+
return saveBookmark(url, title, folderId, note);
|
|
18429
|
+
}
|
|
17549
18430
|
);
|
|
17550
18431
|
electron.ipcMain.handle(Channels.BOOKMARK_REMOVE, (_, id) => {
|
|
18432
|
+
trackBookmarkAction("remove");
|
|
17551
18433
|
return removeBookmark(id);
|
|
17552
18434
|
});
|
|
17553
18435
|
electron.ipcMain.handle(Channels.FOLDER_REMOVE, (_, id) => {
|
|
18436
|
+
trackBookmarkAction("folder_remove");
|
|
17554
18437
|
return removeFolder(id);
|
|
17555
18438
|
});
|
|
17556
18439
|
electron.ipcMain.handle(
|
|
@@ -17698,6 +18581,83 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
17698
18581
|
layoutViews(windowState);
|
|
17699
18582
|
return clamped;
|
|
17700
18583
|
});
|
|
18584
|
+
electron.ipcMain.handle(Channels.PREMIUM_GET_STATE, () => {
|
|
18585
|
+
return getPremiumState();
|
|
18586
|
+
});
|
|
18587
|
+
electron.ipcMain.handle(Channels.PREMIUM_ACTIVATE, async (_, email) => {
|
|
18588
|
+
assertString(email, "email");
|
|
18589
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
|
|
18590
|
+
return { ok: false, state: getPremiumState(), error: "Invalid email format" };
|
|
18591
|
+
}
|
|
18592
|
+
trackPremiumFunnel("activation_attempted");
|
|
18593
|
+
const result = await activateWithEmail(email);
|
|
18594
|
+
if (result.ok) {
|
|
18595
|
+
trackPremiumFunnel("activation_succeeded", { status: result.state.status });
|
|
18596
|
+
sendToRendererViews(Channels.PREMIUM_UPDATE, result.state);
|
|
18597
|
+
} else {
|
|
18598
|
+
trackPremiumFunnel("activation_failed", { status: result.state.status });
|
|
18599
|
+
}
|
|
18600
|
+
return result;
|
|
18601
|
+
});
|
|
18602
|
+
electron.ipcMain.handle(Channels.PREMIUM_CHECKOUT, async (_, email) => {
|
|
18603
|
+
trackPremiumFunnel("checkout_clicked");
|
|
18604
|
+
const result = await getCheckoutUrl(email);
|
|
18605
|
+
if (result.ok && result.url) {
|
|
18606
|
+
tabManager.createTab(result.url);
|
|
18607
|
+
}
|
|
18608
|
+
return result;
|
|
18609
|
+
});
|
|
18610
|
+
electron.ipcMain.handle(Channels.PREMIUM_RESET, () => {
|
|
18611
|
+
trackPremiumFunnel("reset");
|
|
18612
|
+
const state2 = resetPremium();
|
|
18613
|
+
sendToRendererViews(Channels.PREMIUM_UPDATE, state2);
|
|
18614
|
+
return state2;
|
|
18615
|
+
});
|
|
18616
|
+
electron.ipcMain.handle(Channels.PREMIUM_PORTAL, async () => {
|
|
18617
|
+
trackPremiumFunnel("portal_opened");
|
|
18618
|
+
const result = await getPortalUrl();
|
|
18619
|
+
if (result.ok && result.url) {
|
|
18620
|
+
tabManager.createTab(result.url);
|
|
18621
|
+
}
|
|
18622
|
+
return result;
|
|
18623
|
+
});
|
|
18624
|
+
electron.ipcMain.handle(Channels.VAULT_LIST, () => {
|
|
18625
|
+
return listEntries();
|
|
18626
|
+
});
|
|
18627
|
+
electron.ipcMain.handle(
|
|
18628
|
+
Channels.VAULT_ADD,
|
|
18629
|
+
(_, entry) => {
|
|
18630
|
+
if (!entry || typeof entry !== "object") throw new Error("Invalid vault entry");
|
|
18631
|
+
assertString(entry.label, "label");
|
|
18632
|
+
assertString(entry.domainPattern, "domainPattern");
|
|
18633
|
+
assertString(entry.username, "username");
|
|
18634
|
+
assertString(entry.password, "password");
|
|
18635
|
+
if (!entry.label.trim() || !entry.domainPattern.trim() || !entry.username.trim() || !entry.password.trim()) {
|
|
18636
|
+
throw new Error("Label, domain, username, and password are required");
|
|
18637
|
+
}
|
|
18638
|
+
assertOptionalString(entry.totpSecret, "totpSecret");
|
|
18639
|
+
assertOptionalString(entry.notes, "notes");
|
|
18640
|
+
trackVaultAction("credential_added");
|
|
18641
|
+
const created = addEntry(entry);
|
|
18642
|
+
return { id: created.id, label: created.label, domainPattern: created.domainPattern, username: created.username };
|
|
18643
|
+
}
|
|
18644
|
+
);
|
|
18645
|
+
electron.ipcMain.handle(
|
|
18646
|
+
Channels.VAULT_UPDATE,
|
|
18647
|
+
(_, id, updates) => {
|
|
18648
|
+
assertString(id, "id");
|
|
18649
|
+
if (!updates || typeof updates !== "object") throw new Error("Invalid updates");
|
|
18650
|
+
return updateEntry(id, updates) !== null;
|
|
18651
|
+
}
|
|
18652
|
+
);
|
|
18653
|
+
electron.ipcMain.handle(Channels.VAULT_REMOVE, (_, id) => {
|
|
18654
|
+
assertString(id, "id");
|
|
18655
|
+
trackVaultAction("credential_removed");
|
|
18656
|
+
return removeEntry(id);
|
|
18657
|
+
});
|
|
18658
|
+
electron.ipcMain.handle(Channels.VAULT_AUDIT_LOG, (_, limit) => {
|
|
18659
|
+
return readAuditLog(limit);
|
|
18660
|
+
});
|
|
17701
18661
|
electron.ipcMain.handle(Channels.WINDOW_MINIMIZE, () => {
|
|
17702
18662
|
mainWindow.minimize();
|
|
17703
18663
|
});
|
|
@@ -18466,6 +19426,8 @@ async function bootstrap() {
|
|
|
18466
19426
|
sidebarView.webContents.send(Channels.HISTORY_UPDATE, state2);
|
|
18467
19427
|
});
|
|
18468
19428
|
installDownloadHandler(chromeView);
|
|
19429
|
+
startBackgroundRevalidation();
|
|
19430
|
+
startTelemetry();
|
|
18469
19431
|
const chromeUrl = rendererUrlFor("chrome");
|
|
18470
19432
|
const sidebarUrl = rendererUrlFor("sidebar");
|
|
18471
19433
|
const devtoolsUrl = rendererUrlFor("devtools");
|
|
@@ -18505,6 +19467,8 @@ electron.app.whenReady().then(bootstrap).catch((error) => {
|
|
|
18505
19467
|
});
|
|
18506
19468
|
electron.app.on("window-all-closed", () => {
|
|
18507
19469
|
electron.globalShortcut.unregisterAll();
|
|
19470
|
+
stopTelemetry();
|
|
19471
|
+
stopBackgroundRevalidation();
|
|
18508
19472
|
runtime?.flushPersist();
|
|
18509
19473
|
void stopMcpServer().finally(() => {
|
|
18510
19474
|
electron.app.quit();
|