@quanta-intellect/vessel-browser 0.1.134 → 0.1.137

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/main/index.js CHANGED
@@ -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$1 = require("crypto");
7
+ const zod = require("zod");
7
8
  const Anthropic = require("@anthropic-ai/sdk");
8
9
  const OpenAI = require("openai");
9
10
  const crypto$2 = require("node:crypto");
10
11
  const http = require("http");
11
- const zod = require("zod");
12
12
  const path$1 = require("node:path");
13
13
  const node_module = require("node:module");
14
14
  const http$1 = require("node:http");
@@ -111,10 +111,10 @@ const defaults = {
111
111
  expiresAt: ""
112
112
  }
113
113
  };
114
- const SAVE_DEBOUNCE_MS$6 = 150;
114
+ const SAVE_DEBOUNCE_MS$4 = 150;
115
115
  const CHAT_PROVIDER_SECRET_FILENAME = "vessel-chat-provider-secret";
116
116
  const CODEX_TOKENS_FILENAME = "vessel-codex-tokens";
117
- const logger$x = createLogger("Settings");
117
+ const logger$y = createLogger("Settings");
118
118
  const SETTABLE_KEYS = new Set(Object.keys(defaults));
119
119
  let settings = null;
120
120
  let settingsIssues = [];
@@ -139,7 +139,7 @@ function canUseSafeStorage$1() {
139
139
  try {
140
140
  return electron.safeStorage.isEncryptionAvailable();
141
141
  } catch (err) {
142
- logger$x.warn("safeStorage.isEncryptionAvailable() failed, assuming unavailable:", err);
142
+ logger$y.warn("safeStorage.isEncryptionAvailable() failed, assuming unavailable:", err);
143
143
  return false;
144
144
  }
145
145
  }
@@ -148,7 +148,7 @@ function writePrivateFile(filePath2, data) {
148
148
  try {
149
149
  fs.chmodSync(filePath2, 384);
150
150
  } catch (err) {
151
- logger$x.debug("Could not chmod private file (non-POSIX filesystem):", err);
151
+ logger$y.debug("Could not chmod private file (non-POSIX filesystem):", err);
152
152
  }
153
153
  }
154
154
  function assertSafeStorageAvailable() {
@@ -167,7 +167,7 @@ function readStoredProviderSecret() {
167
167
  }
168
168
  } catch (err) {
169
169
  if (!isMissingFileError(err)) {
170
- logger$x.warn("Could not read stored provider secret:", err);
170
+ logger$y.warn("Could not read stored provider secret:", err);
171
171
  }
172
172
  }
173
173
  return null;
@@ -185,7 +185,7 @@ function clearStoredProviderSecret() {
185
185
  fs.unlinkSync(getChatProviderSecretPath());
186
186
  } catch (err) {
187
187
  if (!isMissingFileError(err)) {
188
- logger$x.warn("Could not delete provider secret file:", err);
188
+ logger$y.warn("Could not delete provider secret file:", err);
189
189
  }
190
190
  }
191
191
  }
@@ -203,7 +203,7 @@ function readStoredCodexTokens() {
203
203
  }
204
204
  } catch (err) {
205
205
  if (!isMissingFileError(err)) {
206
- logger$x.warn("Could not read stored Codex tokens:", err);
206
+ logger$y.warn("Could not read stored Codex tokens:", err);
207
207
  }
208
208
  }
209
209
  return null;
@@ -221,7 +221,7 @@ function clearStoredCodexTokens() {
221
221
  fs.unlinkSync(getCodexTokensPath());
222
222
  } catch (err) {
223
223
  if (!isMissingFileError(err)) {
224
- logger$x.warn("Could not delete Codex token file:", err);
224
+ logger$y.warn("Could not delete Codex token file:", err);
225
225
  }
226
226
  }
227
227
  }
@@ -352,8 +352,8 @@ function persistNow() {
352
352
  { encoding: "utf-8", mode: 384 }
353
353
  )
354
354
  ).then(() => fs.promises.chmod(getSettingsPath(), 384).catch((err) => {
355
- logger$x.warn("Failed to chmod settings file:", err);
356
- })).catch((err) => logger$x.error("Failed to save settings:", err));
355
+ logger$y.warn("Failed to chmod settings file:", err);
356
+ })).catch((err) => logger$y.error("Failed to save settings:", err));
357
357
  }
358
358
  function saveSettings() {
359
359
  saveDirty = true;
@@ -363,7 +363,7 @@ function saveSettings() {
363
363
  if (saveDirty) {
364
364
  void persistNow();
365
365
  }
366
- }, SAVE_DEBOUNCE_MS$6);
366
+ }, SAVE_DEBOUNCE_MS$4);
367
367
  }
368
368
  function setSetting(key2, value) {
369
369
  loadSettings();
@@ -532,7 +532,7 @@ const urlSafety = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.definePro
532
532
  }, Symbol.toStringTag, { value: "Module" }));
533
533
  const MAX_CUSTOM_HISTORY = 50;
534
534
  const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
535
- const logger$w = createLogger("Tab");
535
+ const logger$x = createLogger("Tab");
536
536
  const sessionCertExceptions = /* @__PURE__ */ new WeakMap();
537
537
  const sessionsWithVerifyProc = /* @__PURE__ */ new WeakSet();
538
538
  const CERT_VERIFY_TRUST = 0;
@@ -598,7 +598,7 @@ class Tab {
598
598
  guardedLoadURL(url, options) {
599
599
  const blockReason = this.getNavigationBlockReason(url);
600
600
  if (blockReason) {
601
- logger$w.warn(blockReason);
601
+ logger$x.warn(blockReason);
602
602
  return blockReason;
603
603
  }
604
604
  void this.view.webContents.loadURL(url, options);
@@ -682,7 +682,7 @@ class Tab {
682
682
  wc.setWindowOpenHandler(({ url, disposition }) => {
683
683
  const error = this.getNavigationBlockReason(url);
684
684
  if (error) {
685
- logger$w.warn(error);
685
+ logger$x.warn(error);
686
686
  return { action: "deny" };
687
687
  }
688
688
  this.onOpenUrl?.({
@@ -696,7 +696,7 @@ class Tab {
696
696
  const error = this.getNavigationBlockReason(url);
697
697
  if (!error) return;
698
698
  event.preventDefault();
699
- logger$w.warn(`${context}: ${error}`);
699
+ logger$x.warn(`${context}: ${error}`);
700
700
  };
701
701
  wc.on("will-navigate", (event, url) => {
702
702
  blockNavigation(event, url, "Blocked top-level navigation");
@@ -780,7 +780,7 @@ class Tab {
780
780
  ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 999px; }
781
781
  ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
782
782
  ::-webkit-scrollbar-corner { background: transparent; }
783
- `).catch((err) => logger$w.warn("Failed to inject scrollbar CSS:", err));
783
+ `).catch((err) => logger$x.warn("Failed to inject scrollbar CSS:", err));
784
784
  });
785
785
  wc.on("page-favicon-updated", (_, favicons) => {
786
786
  this._state.favicon = favicons[0] || "";
@@ -816,7 +816,7 @@ class Tab {
816
816
  ).then((highlightedText) => {
817
817
  this.buildContextMenu(wc, params, highlightedText.trim());
818
818
  }).catch((err) => {
819
- logger$w.warn("Failed to inspect highlighted text for context menu:", err);
819
+ logger$x.warn("Failed to inspect highlighted text for context menu:", err);
820
820
  this.buildContextMenu(wc, params, "");
821
821
  });
822
822
  });
@@ -1017,7 +1017,7 @@ class Tab {
1017
1017
  "document.documentElement.outerHTML"
1018
1018
  );
1019
1019
  } catch (err) {
1020
- logger$w.warn("Failed to retrieve page source:", err);
1020
+ logger$x.warn("Failed to retrieve page source:", err);
1021
1021
  return;
1022
1022
  }
1023
1023
  const escaped = html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1147,7 +1147,7 @@ class Tab {
1147
1147
  document.addEventListener('mouseup', window.__vesselHighlightHandler);
1148
1148
  }
1149
1149
  })()
1150
- `).catch((err) => logger$w.warn("Failed to inject highlight listener:", err));
1150
+ `).catch((err) => logger$x.warn("Failed to inject highlight listener:", err));
1151
1151
  } else {
1152
1152
  void wc.executeJavaScript(`
1153
1153
  (function() {
@@ -1158,7 +1158,7 @@ class Tab {
1158
1158
  delete window.__vesselHighlightHandler;
1159
1159
  }
1160
1160
  })()
1161
- `).catch((err) => logger$w.warn("Failed to remove highlight listener:", err));
1161
+ `).catch((err) => logger$x.warn("Failed to remove highlight listener:", err));
1162
1162
  }
1163
1163
  }
1164
1164
  get webContentsId() {
@@ -1195,7 +1195,87 @@ const SEARCH_ENGINE_PRESETS = {
1195
1195
  ecosia: { label: "Ecosia", url: "https://www.ecosia.org/search?q=" },
1196
1196
  kagi: { label: "Kagi", url: "https://kagi.com/search?q=" }
1197
1197
  };
1198
- const logger$v = createLogger("JsonPersistence");
1198
+ const DownloadStateSchema = zod.z.enum([
1199
+ "progressing",
1200
+ "completed",
1201
+ "cancelled",
1202
+ "interrupted"
1203
+ ]);
1204
+ const DownloadRecordSchema = zod.z.object({
1205
+ id: zod.z.string(),
1206
+ filename: zod.z.string(),
1207
+ savePath: zod.z.string(),
1208
+ url: zod.z.string().optional(),
1209
+ mimeType: zod.z.string().optional(),
1210
+ totalBytes: zod.z.number(),
1211
+ receivedBytes: zod.z.number(),
1212
+ state: DownloadStateSchema,
1213
+ startedAt: zod.z.string(),
1214
+ updatedAt: zod.z.string()
1215
+ });
1216
+ zod.z.object({
1217
+ items: zod.z.array(DownloadRecordSchema)
1218
+ });
1219
+ const HistoryEntrySchema = zod.z.object({
1220
+ url: zod.z.string(),
1221
+ title: zod.z.string(),
1222
+ visitedAt: zod.z.string()
1223
+ });
1224
+ zod.z.object({
1225
+ entries: zod.z.array(HistoryEntrySchema)
1226
+ });
1227
+ const HistoryImportEntrySchema = zod.z.object({
1228
+ url: zod.z.string(),
1229
+ title: zod.z.string().optional(),
1230
+ visitedAt: zod.z.string().optional()
1231
+ });
1232
+ const HistoryImportStateSchema = zod.z.object({
1233
+ entries: zod.z.array(HistoryImportEntrySchema)
1234
+ });
1235
+ const HighlightColorSchema = zod.z.enum([
1236
+ "yellow",
1237
+ "red",
1238
+ "green",
1239
+ "blue",
1240
+ "purple",
1241
+ "orange"
1242
+ ]);
1243
+ const HighlightSourceSchema = zod.z.enum(["agent", "user"]);
1244
+ const StoredHighlightSchema = zod.z.object({
1245
+ id: zod.z.string(),
1246
+ url: zod.z.string(),
1247
+ selector: zod.z.string().optional(),
1248
+ text: zod.z.string().optional(),
1249
+ label: zod.z.string().optional(),
1250
+ color: HighlightColorSchema.optional(),
1251
+ source: HighlightSourceSchema.optional(),
1252
+ createdAt: zod.z.string()
1253
+ });
1254
+ zod.z.object({
1255
+ highlights: zod.z.array(StoredHighlightSchema)
1256
+ });
1257
+ function parseArrayStateWithFallback(itemSchema, data, field, fallback, label) {
1258
+ if (!data || typeof data !== "object") return fallback;
1259
+ const rawItems = data[field];
1260
+ if (!Array.isArray(rawItems)) return fallback;
1261
+ const items = [];
1262
+ let invalid = 0;
1263
+ for (const item of rawItems) {
1264
+ const result = itemSchema.safeParse(item);
1265
+ if (result.success) {
1266
+ items.push(result.data);
1267
+ } else {
1268
+ invalid++;
1269
+ }
1270
+ }
1271
+ if (invalid > 0) {
1272
+ console.warn(
1273
+ `[persistence] ${label} dropped ${invalid} invalid ${field} item${invalid === 1 ? "" : "s"}`
1274
+ );
1275
+ }
1276
+ return { ...fallback, [field]: items };
1277
+ }
1278
+ const logger$w = createLogger("JsonPersistence");
1199
1279
  function canUseSafeStorage() {
1200
1280
  try {
1201
1281
  return electron.safeStorage.isEncryptionAvailable();
@@ -1218,14 +1298,15 @@ function encodeStoredData(payload, secure) {
1218
1298
  function loadJsonFile({
1219
1299
  filePath: filePath2,
1220
1300
  fallback,
1221
- parse: parse2,
1301
+ parse,
1222
1302
  secure = false
1223
1303
  }) {
1224
1304
  try {
1225
1305
  const raw = fs.readFileSync(filePath2);
1226
1306
  const decoded = decodeStoredData(raw, secure);
1227
- return parse2(JSON.parse(decoded));
1228
- } catch {
1307
+ return parse(JSON.parse(decoded));
1308
+ } catch (err) {
1309
+ logger$w.warn(`Failed to load ${filePath2}, using fallback:`, err);
1229
1310
  return fallback;
1230
1311
  }
1231
1312
  }
@@ -1261,8 +1342,8 @@ function createDebouncedJsonPersistence({
1261
1342
  typeof data === "string" ? { encoding: "utf-8", mode: 384 } : { mode: 384 }
1262
1343
  )
1263
1344
  ).then(() => fs.promises.chmod(filePath2, 384).catch((err) => {
1264
- logger$v.warn(`Failed to chmod ${logLabel}:`, err);
1265
- })).catch((err) => logger$v.error(`Failed to save ${logLabel}:`, err));
1345
+ logger$w.warn(`Failed to chmod ${logLabel}:`, err);
1346
+ })).catch((err) => logger$w.error(`Failed to save ${logLabel}:`, err));
1266
1347
  };
1267
1348
  const schedule = () => {
1268
1349
  saveDirty2 = true;
@@ -1284,50 +1365,109 @@ function createDebouncedJsonPersistence({
1284
1365
  flush: flush2
1285
1366
  };
1286
1367
  }
1287
- let state$5 = null;
1288
- const listeners$2 = /* @__PURE__ */ new Set();
1289
- const SAVE_DEBOUNCE_MS$5 = 250;
1290
- function getHighlightsPath() {
1291
- return path.join(electron.app.getPath("userData"), "vessel-highlights.json");
1292
- }
1293
- function createPersistence$1() {
1294
- return createDebouncedJsonPersistence({
1295
- debounceMs: SAVE_DEBOUNCE_MS$5,
1296
- filePath: getHighlightsPath(),
1297
- getValue: () => state$5,
1298
- logLabel: "highlights",
1299
- resetOnSchedule: true
1300
- });
1301
- }
1302
- let persistence$7 = null;
1303
- function getPersistence$1() {
1304
- persistence$7 ??= createPersistence$1();
1305
- return persistence$7;
1306
- }
1307
- function load$4() {
1308
- if (state$5) return state$5;
1309
- state$5 = loadJsonFile({
1310
- filePath: getHighlightsPath(),
1311
- fallback: { highlights: [] },
1312
- parse: (raw) => {
1313
- const parsed = raw;
1314
- return {
1315
- highlights: Array.isArray(parsed.highlights) ? parsed.highlights : []
1316
- };
1368
+ class PersistentState {
1369
+ state = null;
1370
+ listeners = /* @__PURE__ */ new Set();
1371
+ persistence = null;
1372
+ config;
1373
+ constructor(config) {
1374
+ this.config = {
1375
+ debounceMs: 250,
1376
+ resetOnSchedule: false,
1377
+ secure: false,
1378
+ snapshot: (s) => s,
1379
+ ...config
1380
+ };
1381
+ }
1382
+ // --- State access ---
1383
+ /** Get the current state, loading from disk if needed. */
1384
+ getState() {
1385
+ if (this.state) return this.state;
1386
+ this.state = loadJsonFile({
1387
+ filePath: this.getFilePath(),
1388
+ fallback: this.config.fallback,
1389
+ parse: this.config.parse,
1390
+ secure: this.config.secure
1391
+ });
1392
+ return this.state;
1393
+ }
1394
+ /**
1395
+ * Update state via a mutator function.
1396
+ * The mutator receives the current state and should mutate it in place.
1397
+ * Does NOT automatically save or emit — call save() and emit() as needed.
1398
+ */
1399
+ update(mutator) {
1400
+ const s = this.getState();
1401
+ mutator(s);
1402
+ }
1403
+ /**
1404
+ * Mutate state and apply persistence/notification as one operation.
1405
+ * The mutator may return a value, such as the item it created or removed.
1406
+ */
1407
+ mutate(mutator, options = {}) {
1408
+ const result = mutator(this.getState());
1409
+ if (options.save ?? true) {
1410
+ this.save();
1317
1411
  }
1318
- });
1319
- return state$5;
1320
- }
1321
- function save$3() {
1322
- getPersistence$1().schedule();
1323
- }
1324
- function emit$4() {
1325
- if (!state$5) return;
1326
- const snapshot2 = { highlights: [...state$5.highlights] };
1327
- for (const listener of listeners$2) {
1328
- listener(snapshot2);
1412
+ if (options.emit ?? true) {
1413
+ this.emit();
1414
+ }
1415
+ return result;
1416
+ }
1417
+ // --- Persistence ---
1418
+ /** Get the file path for this state's JSON file. */
1419
+ getFilePath() {
1420
+ return path.join(electron.app.getPath("userData"), this.config.filename);
1421
+ }
1422
+ /** Schedule a debounced write to disk. */
1423
+ save() {
1424
+ this.getPersistence().schedule();
1425
+ }
1426
+ /** Flush any pending write to disk immediately. */
1427
+ flushPersist() {
1428
+ return this.getPersistence().flush();
1429
+ }
1430
+ // --- Change notification ---
1431
+ /** Subscribe to state changes. Returns an unsubscribe function. */
1432
+ subscribe(listener) {
1433
+ this.listeners.add(listener);
1434
+ return () => {
1435
+ this.listeners.delete(listener);
1436
+ };
1437
+ }
1438
+ /** Emit the current state to all subscribers. No-op if state hasn't been loaded. */
1439
+ emit() {
1440
+ if (!this.state) return;
1441
+ const snapshot2 = this.config.snapshot(this.state);
1442
+ for (const listener of this.listeners) {
1443
+ listener(snapshot2);
1444
+ }
1445
+ }
1446
+ // --- Private ---
1447
+ getPersistence() {
1448
+ if (!this.persistence) {
1449
+ this.persistence = createDebouncedJsonPersistence({
1450
+ debounceMs: this.config.debounceMs,
1451
+ filePath: this.getFilePath(),
1452
+ getValue: () => this.state,
1453
+ logLabel: this.config.logLabel,
1454
+ resetOnSchedule: this.config.resetOnSchedule,
1455
+ secure: this.config.secure
1456
+ });
1457
+ }
1458
+ return this.persistence;
1329
1459
  }
1330
1460
  }
1461
+ const HIGHLIGHTS_FALLBACK = { highlights: [] };
1462
+ const store$2 = new PersistentState({
1463
+ filename: "vessel-highlights.json",
1464
+ fallback: HIGHLIGHTS_FALLBACK,
1465
+ parse: (raw) => parseArrayStateWithFallback(StoredHighlightSchema, raw, "highlights", HIGHLIGHTS_FALLBACK, "highlights"),
1466
+ logLabel: "highlights",
1467
+ debounceMs: 250,
1468
+ resetOnSchedule: true,
1469
+ snapshot: (s) => ({ highlights: [...s.highlights] })
1470
+ });
1331
1471
  function normalizeUrl$1(rawUrl) {
1332
1472
  try {
1333
1473
  const parsed = new URL(rawUrl);
@@ -1338,16 +1478,14 @@ function normalizeUrl$1(rawUrl) {
1338
1478
  }
1339
1479
  }
1340
1480
  function getState$2() {
1341
- load$4();
1342
- return { highlights: [...state$5.highlights] };
1481
+ const s = store$2.getState();
1482
+ return { highlights: [...s.highlights] };
1343
1483
  }
1344
1484
  function getHighlightsForUrl(url) {
1345
- load$4();
1346
1485
  const normalized = normalizeUrl$1(url);
1347
- return state$5.highlights.filter((h) => h.url === normalized);
1486
+ return store$2.getState().highlights.filter((h) => h.url === normalized);
1348
1487
  }
1349
1488
  function addHighlight(url, selector, text, label, color, source) {
1350
- load$4();
1351
1489
  const highlight = {
1352
1490
  id: crypto$1.randomUUID(),
1353
1491
  url: normalizeUrl$1(url),
@@ -1358,50 +1496,66 @@ function addHighlight(url, selector, text, label, color, source) {
1358
1496
  source: source || void 0,
1359
1497
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1360
1498
  };
1361
- state$5.highlights.push(highlight);
1362
- save$3();
1363
- emit$4();
1499
+ store$2.mutate((s) => {
1500
+ s.highlights.push(highlight);
1501
+ });
1364
1502
  return highlight;
1365
1503
  }
1366
1504
  function removeHighlight(id) {
1367
- load$4();
1368
- const index = state$5.highlights.findIndex((h) => h.id === id);
1369
- if (index === -1) return null;
1370
- const [removed] = state$5.highlights.splice(index, 1);
1371
- save$3();
1372
- emit$4();
1505
+ const removed = store$2.mutate((s) => {
1506
+ const index = s.highlights.findIndex((h) => h.id === id);
1507
+ if (index === -1) return null;
1508
+ const [removed2] = s.highlights.splice(index, 1);
1509
+ return removed2;
1510
+ }, {
1511
+ save: false,
1512
+ emit: false
1513
+ });
1514
+ if (removed) {
1515
+ store$2.save();
1516
+ store$2.emit();
1517
+ }
1373
1518
  return removed;
1374
1519
  }
1375
1520
  function findHighlightByText(url, text) {
1376
- load$4();
1377
1521
  const normalized = normalizeUrl$1(url);
1378
- return state$5.highlights.find(
1522
+ return store$2.getState().highlights.find(
1379
1523
  (h) => h.url === normalized && h.text && h.text === text
1380
1524
  ) ?? null;
1381
1525
  }
1382
1526
  function updateHighlightColor(id, color) {
1383
- load$4();
1384
- const highlight = state$5.highlights.find((h) => h.id === id);
1385
- if (!highlight) return null;
1386
- highlight.color = color;
1387
- save$3();
1388
- emit$4();
1527
+ const highlight = store$2.mutate((s) => {
1528
+ const item = s.highlights.find((h) => h.id === id) ?? null;
1529
+ if (item) item.color = color;
1530
+ return item;
1531
+ }, {
1532
+ save: false,
1533
+ emit: false
1534
+ });
1535
+ if (highlight) {
1536
+ store$2.save();
1537
+ store$2.emit();
1538
+ }
1389
1539
  return highlight;
1390
1540
  }
1391
1541
  function clearHighlightsForUrl(url) {
1392
- load$4();
1393
1542
  const normalized = normalizeUrl$1(url);
1394
- const before = state$5.highlights.length;
1395
- state$5.highlights = state$5.highlights.filter((h) => h.url !== normalized);
1396
- const removed = before - state$5.highlights.length;
1543
+ const removed = store$2.mutate((s) => {
1544
+ const before = s.highlights.length;
1545
+ s.highlights = s.highlights.filter((h) => h.url !== normalized);
1546
+ return before - s.highlights.length;
1547
+ }, {
1548
+ save: false,
1549
+ emit: false
1550
+ });
1397
1551
  if (removed > 0) {
1398
- save$3();
1399
- emit$4();
1552
+ store$2.save();
1553
+ store$2.emit();
1400
1554
  }
1401
1555
  return removed;
1402
1556
  }
1403
1557
  function flushPersist$4() {
1404
- return getPersistence$1().flush();
1558
+ return store$2.flushPersist();
1405
1559
  }
1406
1560
  const SKIP_TAGS_JS = "var SKIP_TAGS = {SCRIPT:1,STYLE:1,NOSCRIPT:1,TEMPLATE:1,IFRAME:1,SVG:1};";
1407
1561
  const CONTENT_ROOTS_JS = `
@@ -2025,114 +2179,94 @@ function persistHighlight(url, text) {
2025
2179
  return { success: true, text: capped, id: highlight.id };
2026
2180
  }
2027
2181
  const MAX_HISTORY_ENTRIES = 5e3;
2028
- const SAVE_DEBOUNCE_MS$4 = 250;
2029
- let state$4 = null;
2030
- const listeners$1 = /* @__PURE__ */ new Set();
2031
- function getHistoryPath() {
2032
- return path.join(electron.app.getPath("userData"), "vessel-history.json");
2033
- }
2034
- function load$3() {
2035
- if (state$4) return state$4;
2036
- state$4 = loadJsonFile({
2037
- filePath: getHistoryPath(),
2038
- fallback: { entries: [] },
2039
- parse: (raw) => {
2040
- const parsed = raw;
2041
- return {
2042
- entries: Array.isArray(parsed.entries) ? parsed.entries : []
2043
- };
2044
- }
2045
- });
2046
- return state$4;
2047
- }
2048
- const persistence$6 = createDebouncedJsonPersistence({
2049
- debounceMs: SAVE_DEBOUNCE_MS$4,
2050
- filePath: getHistoryPath(),
2051
- getValue: () => state$4,
2052
- logLabel: "history"
2053
- });
2054
- function save$2() {
2055
- persistence$6.schedule();
2056
- }
2057
- function emit$3() {
2058
- if (!state$4) return;
2059
- const snapshot2 = listEntries$2();
2060
- for (const listener of listeners$1) {
2061
- listener(snapshot2);
2182
+ const HISTORY_FALLBACK = { entries: [] };
2183
+ const store$1 = new PersistentState({
2184
+ filename: "vessel-history.json",
2185
+ fallback: HISTORY_FALLBACK,
2186
+ parse: (raw) => parseArrayStateWithFallback(HistoryEntrySchema, raw, "entries", HISTORY_FALLBACK, "history"),
2187
+ logLabel: "history",
2188
+ debounceMs: 250,
2189
+ snapshot: (s) => {
2190
+ const entries = s.entries.slice(0, 200);
2191
+ return {
2192
+ entries,
2193
+ offset: 0,
2194
+ limit: entries.length,
2195
+ total: s.entries.length
2196
+ };
2062
2197
  }
2063
- }
2064
- function getState$1() {
2065
- load$3();
2066
- return { entries: [...state$4.entries] };
2067
- }
2198
+ });
2068
2199
  function listEntries$2(offset = 0, limit = 200) {
2069
- load$3();
2200
+ const s = store$1.getState();
2070
2201
  const safeOffset = Math.max(0, Math.floor(offset));
2071
2202
  const safeLimit = Math.max(1, Math.min(500, Math.floor(limit)));
2072
2203
  return {
2073
- entries: state$4.entries.slice(safeOffset, safeOffset + safeLimit),
2204
+ entries: s.entries.slice(safeOffset, safeOffset + safeLimit),
2074
2205
  offset: safeOffset,
2075
2206
  limit: safeLimit,
2076
- total: state$4.entries.length
2207
+ total: s.entries.length
2077
2208
  };
2078
2209
  }
2210
+ function getState$1() {
2211
+ const s = store$1.getState();
2212
+ return { entries: [...s.entries] };
2213
+ }
2079
2214
  function subscribe$1(listener) {
2080
- listeners$1.add(listener);
2081
- return () => {
2082
- listeners$1.delete(listener);
2083
- };
2215
+ return store$1.subscribe(listener);
2084
2216
  }
2085
2217
  function addEntry$1(url, title) {
2086
2218
  if (!url || url === "about:blank") return;
2087
- load$3();
2088
- const last = state$4.entries[0];
2089
- if (last && last.url === url) {
2090
- if (title && title !== last.title) {
2091
- last.title = title;
2092
- save$2();
2093
- emit$3();
2219
+ const changed = store$1.mutate((s) => {
2220
+ const last = s.entries[0];
2221
+ if (last && last.url === url) {
2222
+ if (title && title !== last.title) {
2223
+ last.title = title;
2224
+ return true;
2225
+ }
2226
+ return false;
2094
2227
  }
2095
- return;
2096
- }
2097
- const entry = {
2098
- url,
2099
- title: title || url,
2100
- visitedAt: (/* @__PURE__ */ new Date()).toISOString()
2101
- };
2102
- state$4.entries.unshift(entry);
2103
- if (state$4.entries.length > MAX_HISTORY_ENTRIES) {
2104
- state$4.entries = state$4.entries.slice(0, MAX_HISTORY_ENTRIES);
2228
+ const entry = {
2229
+ url,
2230
+ title: title || url,
2231
+ visitedAt: (/* @__PURE__ */ new Date()).toISOString()
2232
+ };
2233
+ s.entries.unshift(entry);
2234
+ if (s.entries.length > MAX_HISTORY_ENTRIES) {
2235
+ s.entries = s.entries.slice(0, MAX_HISTORY_ENTRIES);
2236
+ }
2237
+ return true;
2238
+ }, { save: false, emit: false });
2239
+ if (changed) {
2240
+ store$1.save();
2241
+ store$1.emit();
2105
2242
  }
2106
- save$2();
2107
- emit$3();
2108
2243
  }
2109
2244
  function search(query, limit = 50) {
2110
- load$3();
2111
- if (!query.trim()) return state$4.entries.slice(0, limit);
2245
+ const s = store$1.getState();
2246
+ if (!query.trim()) return s.entries.slice(0, limit);
2112
2247
  const normalized = query.toLowerCase();
2113
- return state$4.entries.filter(
2248
+ return s.entries.filter(
2114
2249
  (e) => e.url.toLowerCase().includes(normalized) || e.title.toLowerCase().includes(normalized)
2115
2250
  ).slice(0, limit);
2116
2251
  }
2117
2252
  function clearAll$1() {
2118
- state$4 = { entries: [] };
2119
- save$2();
2120
- emit$3();
2253
+ store$1.mutate((s) => {
2254
+ s.entries = [];
2255
+ });
2121
2256
  }
2122
2257
  function clearByTimeRange(timeRange) {
2123
- load$3();
2124
2258
  if (timeRange === "all") {
2125
2259
  clearAll$1();
2126
2260
  return;
2127
2261
  }
2128
2262
  const now = Date.now();
2129
2263
  const cutoff = new Date(now - timeRangeToMs(timeRange));
2130
- state$4.entries = state$4.entries.filter((entry) => {
2131
- const visitedAt = new Date(entry.visitedAt).getTime();
2132
- return Number.isNaN(visitedAt) || visitedAt < cutoff.getTime();
2264
+ store$1.mutate((s) => {
2265
+ s.entries = s.entries.filter((entry) => {
2266
+ const visitedAt = new Date(entry.visitedAt).getTime();
2267
+ return Number.isNaN(visitedAt) || visitedAt < cutoff.getTime();
2268
+ });
2133
2269
  });
2134
- save$2();
2135
- emit$3();
2136
2270
  }
2137
2271
  function timeRangeToMs(range) {
2138
2272
  switch (range) {
@@ -2183,12 +2317,13 @@ function importHistoryFromJson(content) {
2183
2317
  let skipped = 0;
2184
2318
  let errors = 0;
2185
2319
  try {
2186
- const parsed = JSON.parse(content);
2187
- const entries = Array.isArray(parsed?.entries) ? parsed.entries : [];
2188
- load$3();
2189
- const existingUrls = new Set(state$4.entries.map((e) => e.url));
2320
+ const parsed = HistoryImportStateSchema.safeParse(JSON.parse(content));
2321
+ const entries = parsed.success ? parsed.data.entries : [];
2322
+ if (!parsed.success) errors++;
2323
+ const s = store$1.getState();
2324
+ const existingUrls = new Set(s.entries.map((e) => e.url));
2190
2325
  for (const entry of entries) {
2191
- if (!entry?.url || typeof entry.url !== "string") {
2326
+ if (!entry.url) {
2192
2327
  errors++;
2193
2328
  continue;
2194
2329
  }
@@ -2196,22 +2331,29 @@ function importHistoryFromJson(content) {
2196
2331
  skipped++;
2197
2332
  continue;
2198
2333
  }
2199
- state$4.entries.push({
2200
- url: entry.url,
2201
- title: typeof entry.title === "string" ? entry.title : entry.url,
2202
- visitedAt: typeof entry.visitedAt === "string" ? entry.visitedAt : (/* @__PURE__ */ new Date()).toISOString()
2203
- });
2204
2334
  existingUrls.add(entry.url);
2205
2335
  imported++;
2206
2336
  }
2207
- state$4.entries.sort(
2208
- (a, b) => new Date(b.visitedAt).getTime() - new Date(a.visitedAt).getTime()
2209
- );
2210
- if (state$4.entries.length > MAX_HISTORY_ENTRIES) {
2211
- state$4.entries = state$4.entries.slice(0, MAX_HISTORY_ENTRIES);
2337
+ if (imported > 0) {
2338
+ store$1.mutate((state2) => {
2339
+ const urlSet = new Set(state2.entries.map((e) => e.url));
2340
+ for (const entry of entries) {
2341
+ if (!entry.url || urlSet.has(entry.url)) continue;
2342
+ state2.entries.push({
2343
+ url: entry.url,
2344
+ title: entry.title || entry.url,
2345
+ visitedAt: entry.visitedAt || (/* @__PURE__ */ new Date()).toISOString()
2346
+ });
2347
+ urlSet.add(entry.url);
2348
+ }
2349
+ state2.entries.sort(
2350
+ (a, b) => new Date(b.visitedAt).getTime() - new Date(a.visitedAt).getTime()
2351
+ );
2352
+ if (state2.entries.length > MAX_HISTORY_ENTRIES) {
2353
+ state2.entries = state2.entries.slice(0, MAX_HISTORY_ENTRIES);
2354
+ }
2355
+ });
2212
2356
  }
2213
- save$2();
2214
- emit$3();
2215
2357
  } catch {
2216
2358
  errors++;
2217
2359
  }
@@ -2221,9 +2363,10 @@ function importHistoryFromHtml(content) {
2221
2363
  let imported = 0;
2222
2364
  let skipped = 0;
2223
2365
  let errors = 0;
2224
- load$3();
2225
- const existingUrls = new Set(state$4.entries.map((e) => e.url));
2366
+ const s = store$1.getState();
2367
+ const existingUrls = new Set(s.entries.map((e) => e.url));
2226
2368
  const hrefRegex = /<A\s+[^>]*HREF="([^"]+)"[^>]*>([^<]*)<\/A>/gi;
2369
+ const newEntries = [];
2227
2370
  let match;
2228
2371
  while ((match = hrefRegex.exec(content)) !== null) {
2229
2372
  const url = match[1];
@@ -2233,26 +2376,25 @@ function importHistoryFromHtml(content) {
2233
2376
  else errors++;
2234
2377
  continue;
2235
2378
  }
2236
- state$4.entries.push({
2237
- url,
2238
- title,
2239
- visitedAt: (/* @__PURE__ */ new Date()).toISOString()
2240
- });
2379
+ newEntries.push({ url, title, visitedAt: (/* @__PURE__ */ new Date()).toISOString() });
2241
2380
  existingUrls.add(url);
2242
2381
  imported++;
2243
2382
  }
2244
- state$4.entries.sort(
2245
- (a, b) => new Date(b.visitedAt).getTime() - new Date(a.visitedAt).getTime()
2246
- );
2247
- if (state$4.entries.length > MAX_HISTORY_ENTRIES) {
2248
- state$4.entries = state$4.entries.slice(0, MAX_HISTORY_ENTRIES);
2383
+ if (newEntries.length > 0) {
2384
+ store$1.mutate((state2) => {
2385
+ state2.entries.push(...newEntries);
2386
+ state2.entries.sort(
2387
+ (a, b) => new Date(b.visitedAt).getTime() - new Date(a.visitedAt).getTime()
2388
+ );
2389
+ if (state2.entries.length > MAX_HISTORY_ENTRIES) {
2390
+ state2.entries = state2.entries.slice(0, MAX_HISTORY_ENTRIES);
2391
+ }
2392
+ });
2249
2393
  }
2250
- save$2();
2251
- emit$3();
2252
2394
  return { imported, skipped, errors };
2253
2395
  }
2254
2396
  function flushPersist$3() {
2255
- return persistence$6.flush();
2397
+ return store$1.flushPersist();
2256
2398
  }
2257
2399
  const MAX_CONSOLE_ENTRIES = 500;
2258
2400
  const MAX_NETWORK_ENTRIES = 200;
@@ -2954,7 +3096,7 @@ function destroySession(tabId) {
2954
3096
  sessions.delete(tabId);
2955
3097
  }
2956
3098
  }
2957
- const logger$u = createLogger("TabManager");
3099
+ const logger$v = createLogger("TabManager");
2958
3100
  function sanitizeFilename(title, ext) {
2959
3101
  const clean = title.replace(/[<>:"/\\|?*\x00-\x1f]/g, " ").replace(/\s+/g, " ").trim();
2960
3102
  const escapedExt = ext.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3373,7 +3515,7 @@ class TabManager {
3373
3515
  }));
3374
3516
  if (entries.length > 0) {
3375
3517
  void highlightBatchOnPage(wc, entries).catch(
3376
- (err) => logger$u.warn("Failed to batch highlight:", err)
3518
+ (err) => logger$v.warn("Failed to batch highlight:", err)
3377
3519
  );
3378
3520
  }
3379
3521
  }
@@ -3395,12 +3537,12 @@ class TabManager {
3395
3537
  const result = await captureSelectionHighlight(wc);
3396
3538
  if (result.success && result.text) {
3397
3539
  await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(
3398
- (err) => logger$u.warn("Failed to capture highlight:", err)
3540
+ (err) => logger$v.warn("Failed to capture highlight:", err)
3399
3541
  );
3400
3542
  }
3401
3543
  this.highlightCaptureCallback?.(result);
3402
3544
  } catch (err) {
3403
- logger$u.warn("Failed to capture highlight from page:", err);
3545
+ logger$v.warn("Failed to capture highlight from page:", err);
3404
3546
  this.highlightCaptureCallback?.({
3405
3547
  success: false,
3406
3548
  message: "Could not capture selection"
@@ -3425,7 +3567,7 @@ class TabManager {
3425
3567
  void this.removeHighlightMarksForText(wc, text);
3426
3568
  }
3427
3569
  } catch (err) {
3428
- logger$u.warn("Failed to remove highlight from matching tab:", err);
3570
+ logger$v.warn("Failed to remove highlight from matching tab:", err);
3429
3571
  }
3430
3572
  }
3431
3573
  this.highlightCaptureCallback?.({
@@ -3456,12 +3598,12 @@ class TabManager {
3456
3598
  void 0,
3457
3599
  color
3458
3600
  ).catch(
3459
- (err) => logger$u.warn("Failed to update highlight color:", err)
3601
+ (err) => logger$v.warn("Failed to update highlight color:", err)
3460
3602
  );
3461
3603
  });
3462
3604
  }
3463
3605
  } catch (err) {
3464
- logger$u.warn("Failed to iterate highlights for color change:", err);
3606
+ logger$v.warn("Failed to iterate highlights for color change:", err);
3465
3607
  }
3466
3608
  }
3467
3609
  this.highlightCaptureCallback?.({
@@ -3516,7 +3658,7 @@ class TabManager {
3516
3658
  });
3517
3659
  })()`
3518
3660
  ).catch(
3519
- (err) => logger$u.warn("Failed to remove highlight marks:", err)
3661
+ (err) => logger$v.warn("Failed to remove highlight marks:", err)
3520
3662
  );
3521
3663
  }
3522
3664
  broadcastState(meta = { persistSession: false }) {
@@ -4155,7 +4297,7 @@ function load$2() {
4155
4297
  });
4156
4298
  return snapshots;
4157
4299
  }
4158
- const persistence$5 = createDebouncedJsonPersistence({
4300
+ const persistence$4 = createDebouncedJsonPersistence({
4159
4301
  debounceMs: SAVE_DEBOUNCE_MS$3,
4160
4302
  filePath: getFilePath$1(),
4161
4303
  getValue: () => snapshots,
@@ -4184,11 +4326,11 @@ function saveSnapshot(rawUrl, title, textContent, headings) {
4184
4326
  };
4185
4327
  s.delete(key2);
4186
4328
  s.set(key2, snapshot2);
4187
- persistence$5.schedule();
4329
+ persistence$4.schedule();
4188
4330
  return snapshot2;
4189
4331
  }
4190
4332
  function flushPersist$2() {
4191
- return persistence$5.flush();
4333
+ return persistence$4.flush();
4192
4334
  }
4193
4335
  const SEARCH_ENGINE_HOSTS = [
4194
4336
  "google.",
@@ -4768,7 +4910,7 @@ async function readJsonResponse(response, fallback, onError) {
4768
4910
  return fallback;
4769
4911
  }
4770
4912
  }
4771
- const logger$t = createLogger("Premium");
4913
+ const logger$u = createLogger("Premium");
4772
4914
  const VERIFICATION_API = process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
4773
4915
  const FREE_TOOL_ITERATION_LIMIT = 50;
4774
4916
  const REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
@@ -4944,7 +5086,7 @@ async function verifySubscription$1(identifier) {
4944
5086
  });
4945
5087
  if (!res.ok) {
4946
5088
  const detail = await readApiErrorDetail(res);
4947
- logger$t.warn(
5089
+ logger$u.warn(
4948
5090
  "Verification API returned a non-OK status:",
4949
5091
  res.status,
4950
5092
  detail
@@ -4963,7 +5105,7 @@ async function verifySubscription$1(identifier) {
4963
5105
  setSetting("premium", updated);
4964
5106
  return updated;
4965
5107
  } catch (err) {
4966
- logger$t.warn("Verification failed:", err);
5108
+ logger$u.warn("Verification failed:", err);
4967
5109
  return current;
4968
5110
  }
4969
5111
  }
@@ -4984,7 +5126,7 @@ async function requestActivationCode(email) {
4984
5126
  const data = await readJsonResponse(
4985
5127
  res,
4986
5128
  {},
4987
- (msg) => logger$t.warn("Failed to parse premium activation response:", msg)
5129
+ (msg) => logger$u.warn("Failed to parse premium activation response:", msg)
4988
5130
  );
4989
5131
  if (!res.ok || !data.challengeToken) {
4990
5132
  return errorResult(data.error || `HTTP ${res.status}`);
@@ -5029,7 +5171,7 @@ async function verifyActivationCode(email, code, challengeToken) {
5029
5171
  const data = await readJsonResponse(
5030
5172
  res,
5031
5173
  {},
5032
- (msg) => logger$t.warn("Failed to parse premium verification response:", msg)
5174
+ (msg) => logger$u.warn("Failed to parse premium verification response:", msg)
5033
5175
  );
5034
5176
  if (!res.ok) {
5035
5177
  return errorResult(data.error || `HTTP ${res.status}`, {
@@ -5626,7 +5768,7 @@ const EXTRACT_TIMEOUT_MAX_MS = 2e4;
5626
5768
  const MUTATION_CAPTURE_INTERVAL_MS = 5e3;
5627
5769
  const MUTATION_SETTLE_AFTER_MS = 1500;
5628
5770
  const AGENT_STREAM_IDLE_TIMEOUT_MS = 3e4;
5629
- const logger$s = createLogger("Extractor");
5771
+ const logger$t = createLogger("Extractor");
5630
5772
  const EXTRACTION_CACHE_TTL_MS = 1500;
5631
5773
  const MAX_EXTRACTION_CACHE_ENTRIES = 50;
5632
5774
  const extractionCache = new BoundedCache(
@@ -6391,9 +6533,9 @@ async function executeScript(webContents, script, options = {}) {
6391
6533
  const message = err instanceof Error ? err.message : String(err);
6392
6534
  const detail = `Failed to execute page script${label} on ${url}: ${message}`;
6393
6535
  if (options.warnOnFailure) {
6394
- logger$s.warn(detail);
6536
+ logger$t.warn(detail);
6395
6537
  } else {
6396
- logger$s.debug(detail);
6538
+ logger$t.debug(detail);
6397
6539
  }
6398
6540
  return null;
6399
6541
  } finally {
@@ -6502,7 +6644,7 @@ async function estimateExtractionTimeout(webContents) {
6502
6644
  return EXTRACT_TIMEOUT_BASE_MS + extra;
6503
6645
  }
6504
6646
  } catch (err) {
6505
- logger$s.warn("Failed to estimate extraction timeout, using base timeout:", err);
6647
+ logger$t.warn("Failed to estimate extraction timeout, using base timeout:", err);
6506
6648
  }
6507
6649
  return EXTRACT_TIMEOUT_BASE_MS;
6508
6650
  }
@@ -6673,7 +6815,7 @@ function loadHistory() {
6673
6815
  }
6674
6816
  return recentPageDiffBursts;
6675
6817
  }
6676
- const persistence$4 = createDebouncedJsonPersistence({
6818
+ const persistence$3 = createDebouncedJsonPersistence({
6677
6819
  debounceMs: SAVE_DEBOUNCE_MS$2,
6678
6820
  filePath: getHistoryFilePath(),
6679
6821
  getValue: () => recentPageDiffBursts,
@@ -6703,7 +6845,7 @@ function getPageDiffBursts(rawUrl) {
6703
6845
  } else {
6704
6846
  history.delete(key2);
6705
6847
  }
6706
- persistence$4.schedule();
6848
+ persistence$3.schedule();
6707
6849
  }
6708
6850
  return bursts.slice().reverse();
6709
6851
  }
@@ -6724,7 +6866,7 @@ function enrichWithBurstHistory(key2, diff) {
6724
6866
  now: Date.parse(detectedAt)
6725
6867
  });
6726
6868
  history.set(key2, bursts);
6727
- persistence$4.schedule();
6869
+ persistence$3.schedule();
6728
6870
  const recentBursts = bursts.slice(-5);
6729
6871
  return {
6730
6872
  ...diff,
@@ -7470,7 +7612,7 @@ function isClickReadLoop(names) {
7470
7612
  return clickReadPairs >= 2;
7471
7613
  }
7472
7614
  const TERMINAL_TOOL_RESULT = "__VESSEL_TERMINAL_TOOL_RESULT__";
7473
- const logger$r = createLogger("PromptCache");
7615
+ const logger$s = createLogger("PromptCache");
7474
7616
  function shortHash(value) {
7475
7617
  return crypto$2.createHash("sha256").update(value).digest("hex").slice(0, 12);
7476
7618
  }
@@ -7522,7 +7664,7 @@ function logOpenAIPromptCacheUsage(usage, context) {
7522
7664
  const details = record.prompt_tokens_details;
7523
7665
  const cachedTokens = details && typeof details === "object" ? numericField(details, "cached_tokens") : null;
7524
7666
  if (promptTokens === null && cachedTokens === null) return;
7525
- logger$r.debug("OpenAI prompt cache usage", {
7667
+ logger$s.debug("OpenAI prompt cache usage", {
7526
7668
  model: context.model,
7527
7669
  mode: context.mode,
7528
7670
  promptTokens,
@@ -7538,7 +7680,7 @@ function logAnthropicPromptCacheUsage(usage, context) {
7538
7680
  if (inputTokens === null && cacheCreationTokens === null && cacheReadTokens === null) {
7539
7681
  return;
7540
7682
  }
7541
- logger$r.debug("Anthropic prompt cache usage", {
7683
+ logger$s.debug("Anthropic prompt cache usage", {
7542
7684
  model: context.model,
7543
7685
  mode: context.mode,
7544
7686
  inputTokens,
@@ -8431,7 +8573,7 @@ function recoverNarratedActionToolCalls(text, availableToolNames) {
8431
8573
  }
8432
8574
  return recovered;
8433
8575
  }
8434
- const logger$q = createLogger("OpenAIProvider");
8576
+ const logger$r = createLogger("OpenAIProvider");
8435
8577
  function shouldDebugAgentLoop() {
8436
8578
  const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
8437
8579
  return value === "1" || value === "true";
@@ -8699,9 +8841,9 @@ function shouldRetryCompactToolLoop(profile, text, hasToolHistory, userMessage)
8699
8841
  function logAgentLoopDebug(payload) {
8700
8842
  if (!shouldDebugAgentLoop()) return;
8701
8843
  try {
8702
- logger$q.info(`[agent-debug] ${JSON.stringify(payload)}`);
8844
+ logger$r.info(`[agent-debug] ${JSON.stringify(payload)}`);
8703
8845
  } catch (err) {
8704
- logger$q.warn("Failed to serialize debug payload:", err);
8846
+ logger$r.warn("Failed to serialize debug payload:", err);
8705
8847
  }
8706
8848
  }
8707
8849
  function formatOpenAICompatErrorMessage(providerId, message) {
@@ -9328,7 +9470,7 @@ function createLocalPkceOAuthFlow(config) {
9328
9470
  isInProgress: () => activeFlow !== null
9329
9471
  };
9330
9472
  }
9331
- const logger$p = createLogger("CodexOAuth");
9473
+ const logger$q = createLogger("CodexOAuth");
9332
9474
  const ISSUER = "https://auth.openai.com";
9333
9475
  const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
9334
9476
  const SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
@@ -9456,7 +9598,7 @@ async function refreshAccessToken(tokens) {
9456
9598
  }
9457
9599
  const codexOAuth = createLocalPkceOAuthFlow({
9458
9600
  name: "Codex",
9459
- logger: logger$p,
9601
+ logger: logger$q,
9460
9602
  preferredPorts: [PREFERRED_PORT$1, FALLBACK_PORT$1],
9461
9603
  timeoutMs: AUTH_TIMEOUT_MS$1,
9462
9604
  callbackPath: () => "/auth/callback",
@@ -9480,7 +9622,7 @@ async function startCodexOAuth(onStatus) {
9480
9622
  function cancelCodexOAuth() {
9481
9623
  codexOAuth.cancel();
9482
9624
  }
9483
- const logger$o = createLogger("CodexProvider");
9625
+ const logger$p = createLogger("CodexProvider");
9484
9626
  const REFRESH_WINDOW_MS = 5 * 60 * 1e3;
9485
9627
  const CODEX_BACKEND_BASE_URL = "https://chatgpt.com/backend-api/codex";
9486
9628
  const CODEX_CLIENT_VERSION = "0.129.0";
@@ -9545,7 +9687,7 @@ class CodexProvider {
9545
9687
  async ensureFreshTokens() {
9546
9688
  if (Date.now() < this.tokens.expiresAt - REFRESH_WINDOW_MS) return;
9547
9689
  try {
9548
- logger$o.info("Refreshing Codex access token");
9690
+ logger$p.info("Refreshing Codex access token");
9549
9691
  const fresh = await refreshAccessToken(this.tokens);
9550
9692
  this.tokens = fresh;
9551
9693
  writeStoredCodexTokens(fresh);
@@ -9693,7 +9835,7 @@ class CodexProvider {
9693
9835
  } catch (err) {
9694
9836
  if (err.name !== "AbortError") {
9695
9837
  const msg = err instanceof Error ? err.message : String(err);
9696
- logger$o.error("Codex streamQuery error:", err);
9838
+ logger$p.error("Codex streamQuery error:", err);
9697
9839
  onChunk(`
9698
9840
 
9699
9841
  [Error: ${msg}]`);
@@ -9761,7 +9903,7 @@ class CodexProvider {
9761
9903
  } catch (err) {
9762
9904
  if (err.name !== "AbortError") {
9763
9905
  const msg = err instanceof Error ? err.message : String(err);
9764
- logger$o.error("Codex streamAgentQuery error:", err);
9906
+ logger$p.error("Codex streamAgentQuery error:", err);
9765
9907
  onChunk(`
9766
9908
 
9767
9909
  [Error: ${msg}]`);
@@ -10995,14 +11137,17 @@ function normalizeBookmarkMetadataUpdate(input) {
10995
11137
  }
10996
11138
  return normalized;
10997
11139
  }
10998
- const logger$n = createLogger("BookmarkManager");
11140
+ function unixNow() {
11141
+ return Math.floor(Date.now() / 1e3);
11142
+ }
11143
+ const logger$o = createLogger("BookmarkManager");
10999
11144
  const UNSORTED_ID = "unsorted";
11000
11145
  const ARCHIVE_FOLDER_NAME = "Archive";
11001
11146
  const NETSCAPE_BOOKMARKS_DOCTYPE = "<!DOCTYPE NETSCAPE-Bookmark-file-1>";
11002
11147
  const SAVE_DEBOUNCE_MS$1 = 250;
11003
11148
  const DEFAULT_BOOKMARK_SEARCH_LIMIT = 50;
11004
11149
  const MAX_BOOKMARK_SEARCH_LIMIT = 200;
11005
- let state$3 = null;
11150
+ let state$2 = null;
11006
11151
  const listeners = /* @__PURE__ */ new Set();
11007
11152
  function cloneState(current) {
11008
11153
  return {
@@ -11012,7 +11157,7 @@ function cloneState(current) {
11012
11157
  }
11013
11158
  function getFolderMap() {
11014
11159
  load$1();
11015
- return new Map(state$3.folders.map((folder) => [folder.id, folder]));
11160
+ return new Map(state$2.folders.map((folder) => [folder.id, folder]));
11016
11161
  }
11017
11162
  function getBookmarksPath() {
11018
11163
  return path.join(electron.app.getPath("userData"), "vessel-bookmarks.json");
@@ -11021,18 +11166,18 @@ function createPersistence() {
11021
11166
  return createDebouncedJsonPersistence({
11022
11167
  debounceMs: SAVE_DEBOUNCE_MS$1,
11023
11168
  filePath: getBookmarksPath(),
11024
- getValue: () => state$3,
11169
+ getValue: () => state$2,
11025
11170
  logLabel: "bookmarks"
11026
11171
  });
11027
11172
  }
11028
- let persistence$3 = null;
11173
+ let persistence$2 = null;
11029
11174
  function getPersistence() {
11030
- persistence$3 ??= createPersistence();
11031
- return persistence$3;
11175
+ persistence$2 ??= createPersistence();
11176
+ return persistence$2;
11032
11177
  }
11033
11178
  function load$1() {
11034
- if (state$3) return state$3;
11035
- state$3 = loadJsonFile({
11179
+ if (state$2) return state$2;
11180
+ state$2 = loadJsonFile({
11036
11181
  filePath: getBookmarksPath(),
11037
11182
  fallback: { folders: [], bookmarks: [] },
11038
11183
  parse: (raw) => {
@@ -11043,7 +11188,7 @@ function load$1() {
11043
11188
  };
11044
11189
  }
11045
11190
  });
11046
- return state$3;
11191
+ return state$2;
11047
11192
  }
11048
11193
  function save$1() {
11049
11194
  getPersistence().schedule();
@@ -11056,8 +11201,8 @@ function assignDefinedBookmarkFields(bookmark, fields) {
11056
11201
  }
11057
11202
  }
11058
11203
  function emit$2() {
11059
- if (!state$3) return;
11060
- const snapshot2 = cloneState(state$3);
11204
+ if (!state$2) return;
11205
+ const snapshot2 = cloneState(state$2);
11061
11206
  for (const listener of listeners) {
11062
11207
  listener(snapshot2);
11063
11208
  }
@@ -11066,9 +11211,9 @@ function escapeBookmarkHtml(value) {
11066
11211
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
11067
11212
  }
11068
11213
  function toNetscapeTimestamp(value) {
11069
- if (!value) return Math.floor(Date.now() / 1e3);
11214
+ if (!value) return unixNow();
11070
11215
  const time = Date.parse(value);
11071
- return Number.isNaN(time) ? Math.floor(Date.now() / 1e3) : Math.floor(time / 1e3);
11216
+ return Number.isNaN(time) ? unixNow() : Math.floor(time / 1e3);
11072
11217
  }
11073
11218
  function getBookmarkDescription(bookmark) {
11074
11219
  const lines = [
@@ -11099,7 +11244,7 @@ function exportBookmarksHtml(options = {}) {
11099
11244
  const resolvedOptions = {
11100
11245
  includeNotes: options.includeNotes ?? false
11101
11246
  };
11102
- const now = Math.floor(Date.now() / 1e3);
11247
+ const now = unixNow();
11103
11248
  const folders = [
11104
11249
  { id: UNSORTED_ID, name: "Vessel Bookmarks", createdAt: "", summary: "" },
11105
11250
  ...current.folders
@@ -11140,7 +11285,7 @@ function exportBookmarkFolderHtml(folderId, options = {}) {
11140
11285
  const resolvedOptions = {
11141
11286
  includeNotes: options.includeNotes ?? false
11142
11287
  };
11143
- const now = Math.floor(Date.now() / 1e3);
11288
+ const now = unixNow();
11144
11289
  const items = current.bookmarks.filter(
11145
11290
  (bookmark) => bookmark.folderId === folder.id
11146
11291
  );
@@ -11179,28 +11324,28 @@ function subscribe(listener) {
11179
11324
  };
11180
11325
  }
11181
11326
  function clearAll() {
11182
- state$3 = { folders: [], bookmarks: [] };
11327
+ state$2 = { folders: [], bookmarks: [] };
11183
11328
  save$1();
11184
11329
  emit$2();
11185
11330
  }
11186
11331
  function getBookmark(id) {
11187
11332
  load$1();
11188
- const bookmark = state$3.bookmarks.find((item) => item.id === id);
11333
+ const bookmark = state$2.bookmarks.find((item) => item.id === id);
11189
11334
  return bookmark ? { ...bookmark } : null;
11190
11335
  }
11191
11336
  function getBookmarkByUrl(url) {
11192
11337
  load$1();
11193
11338
  const normalized = url.trim();
11194
11339
  if (!normalized) return null;
11195
- const bookmark = [...state$3.bookmarks].reverse().find((item) => item.url === normalized);
11340
+ const bookmark = [...state$2.bookmarks].reverse().find((item) => item.url === normalized);
11196
11341
  return bookmark ? { ...bookmark } : null;
11197
11342
  }
11198
11343
  function getBookmarkByUrlInFolder(url, folderId) {
11199
11344
  load$1();
11200
11345
  const normalizedUrl = url.trim();
11201
11346
  if (!normalizedUrl) return null;
11202
- const targetFolderId = folderId && folderId !== UNSORTED_ID ? state$3.folders.find((f) => f.id === folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
11203
- const bookmark = [...state$3.bookmarks].reverse().find(
11347
+ const targetFolderId = folderId && folderId !== UNSORTED_ID ? state$2.folders.find((f) => f.id === folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
11348
+ const bookmark = [...state$2.bookmarks].reverse().find(
11204
11349
  (item) => item.url === normalizedUrl && item.folderId === targetFolderId
11205
11350
  );
11206
11351
  return bookmark ? { ...bookmark } : null;
@@ -11208,14 +11353,14 @@ function getBookmarkByUrlInFolder(url, folderId) {
11208
11353
  function getFolder(id) {
11209
11354
  load$1();
11210
11355
  if (!id || id === UNSORTED_ID) return null;
11211
- const folder = state$3.folders.find((item) => item.id === id);
11356
+ const folder = state$2.folders.find((item) => item.id === id);
11212
11357
  return folder ? { ...folder } : null;
11213
11358
  }
11214
11359
  function findFolderByName(name) {
11215
11360
  load$1();
11216
11361
  const normalized = name.trim().toLowerCase();
11217
11362
  if (!normalized || normalized === "unsorted") return null;
11218
- const folder = state$3.folders.find(
11363
+ const folder = state$2.folders.find(
11219
11364
  (item) => item.name.trim().toLowerCase() === normalized
11220
11365
  );
11221
11366
  return folder ? { ...folder } : null;
@@ -11223,7 +11368,7 @@ function findFolderByName(name) {
11223
11368
  function listFolderOverviews() {
11224
11369
  load$1();
11225
11370
  const counts = /* @__PURE__ */ new Map();
11226
- for (const bookmark of state$3.bookmarks) {
11371
+ for (const bookmark of state$2.bookmarks) {
11227
11372
  counts.set(bookmark.folderId, (counts.get(bookmark.folderId) ?? 0) + 1);
11228
11373
  }
11229
11374
  return [
@@ -11232,7 +11377,7 @@ function listFolderOverviews() {
11232
11377
  name: "Unsorted",
11233
11378
  count: counts.get(UNSORTED_ID) ?? 0
11234
11379
  },
11235
- ...state$3.folders.map((folder) => ({
11380
+ ...state$2.folders.map((folder) => ({
11236
11381
  id: folder.id,
11237
11382
  name: folder.name,
11238
11383
  summary: folder.summary,
@@ -11248,7 +11393,7 @@ function searchBookmarks(query, limit = DEFAULT_BOOKMARK_SEARCH_LIMIT) {
11248
11393
  1,
11249
11394
  Math.min(MAX_BOOKMARK_SEARCH_LIMIT, Math.floor(limit))
11250
11395
  );
11251
- return state$3.bookmarks.map((bookmark) => {
11396
+ return state$2.bookmarks.map((bookmark) => {
11252
11397
  const folder = foldersById.get(bookmark.folderId);
11253
11398
  const { matchedFields, score } = getBookmarkSearchMatch({
11254
11399
  query,
@@ -11285,7 +11430,7 @@ function createFolderWithSummary(name, summary) {
11285
11430
  summary: summary?.trim() || void 0,
11286
11431
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
11287
11432
  };
11288
- state$3.folders.push(folder);
11433
+ state$2.folders.push(folder);
11289
11434
  save$1();
11290
11435
  emit$2();
11291
11436
  return folder;
@@ -11316,7 +11461,7 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
11316
11461
  throw new Error("Bookmark URL cannot be empty");
11317
11462
  }
11318
11463
  const normalizedTitle = title.trim() || normalizedUrl;
11319
- const targetId = folderId && folderId !== UNSORTED_ID ? state$3.folders.find((f) => f.id === folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
11464
+ const targetId = folderId && folderId !== UNSORTED_ID ? state$2.folders.find((f) => f.id === folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
11320
11465
  const duplicatePolicy = options?.onDuplicate ?? "ask";
11321
11466
  const existing = getBookmarkByUrlInFolder(normalizedUrl, targetId);
11322
11467
  if (existing) {
@@ -11327,7 +11472,7 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
11327
11472
  };
11328
11473
  }
11329
11474
  if (duplicatePolicy === "update") {
11330
- const bookmark2 = state$3.bookmarks.find((item) => item.id === existing.id);
11475
+ const bookmark2 = state$2.bookmarks.find((item) => item.id === existing.id);
11331
11476
  if (!bookmark2) {
11332
11477
  return {
11333
11478
  status: "conflict",
@@ -11357,7 +11502,7 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
11357
11502
  savedAt: (/* @__PURE__ */ new Date()).toISOString(),
11358
11503
  ...options?.extra
11359
11504
  };
11360
- state$3.bookmarks.push(bookmark);
11505
+ state$2.bookmarks.push(bookmark);
11361
11506
  save$1();
11362
11507
  emit$2();
11363
11508
  return {
@@ -11367,9 +11512,9 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
11367
11512
  }
11368
11513
  function removeBookmark(id) {
11369
11514
  load$1();
11370
- const before = state$3.bookmarks.length;
11371
- state$3.bookmarks = state$3.bookmarks.filter((b) => b.id !== id);
11372
- if (state$3.bookmarks.length !== before) {
11515
+ const before = state$2.bookmarks.length;
11516
+ state$2.bookmarks = state$2.bookmarks.filter((b) => b.id !== id);
11517
+ if (state$2.bookmarks.length !== before) {
11373
11518
  save$1();
11374
11519
  emit$2();
11375
11520
  return true;
@@ -11378,7 +11523,7 @@ function removeBookmark(id) {
11378
11523
  }
11379
11524
  function updateBookmark(id, updates) {
11380
11525
  load$1();
11381
- const bookmark = state$3.bookmarks.find((item) => item.id === id);
11526
+ const bookmark = state$2.bookmarks.find((item) => item.id === id);
11382
11527
  if (!bookmark) return null;
11383
11528
  const metadataUpdates = normalizeBookmarkMetadataUpdate({
11384
11529
  intent: updates.intent,
@@ -11395,7 +11540,7 @@ function updateBookmark(id, updates) {
11395
11540
  bookmark.note = trimmed || void 0;
11396
11541
  }
11397
11542
  if (typeof updates.folderId === "string") {
11398
- bookmark.folderId = updates.folderId && updates.folderId !== UNSORTED_ID ? state$3.folders.find((item) => item.id === updates.folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
11543
+ bookmark.folderId = updates.folderId && updates.folderId !== UNSORTED_ID ? state$2.folders.find((item) => item.id === updates.folderId)?.id ?? UNSORTED_ID : UNSORTED_ID;
11399
11544
  }
11400
11545
  if ("intent" in metadataUpdates) {
11401
11546
  bookmark.intent = metadataUpdates.intent;
@@ -11418,23 +11563,23 @@ function updateBookmark(id, updates) {
11418
11563
  }
11419
11564
  function removeFolder(id, deleteContents = false) {
11420
11565
  load$1();
11421
- const exists = state$3.folders.some((f) => f.id === id);
11566
+ const exists = state$2.folders.some((f) => f.id === id);
11422
11567
  if (!exists) return false;
11423
11568
  if (deleteContents) {
11424
- state$3.bookmarks = state$3.bookmarks.filter((b) => b.folderId !== id);
11569
+ state$2.bookmarks = state$2.bookmarks.filter((b) => b.folderId !== id);
11425
11570
  } else {
11426
- state$3.bookmarks = state$3.bookmarks.map(
11571
+ state$2.bookmarks = state$2.bookmarks.map(
11427
11572
  (b) => b.folderId === id ? { ...b, folderId: UNSORTED_ID } : b
11428
11573
  );
11429
11574
  }
11430
- state$3.folders = state$3.folders.filter((f) => f.id !== id);
11575
+ state$2.folders = state$2.folders.filter((f) => f.id !== id);
11431
11576
  save$1();
11432
11577
  emit$2();
11433
11578
  return true;
11434
11579
  }
11435
11580
  function renameFolder(id, newName, summary) {
11436
11581
  load$1();
11437
- const folder = state$3.folders.find((f) => f.id === id);
11582
+ const folder = state$2.folders.find((f) => f.id === id);
11438
11583
  if (!folder) return null;
11439
11584
  const trimmed = newName.trim();
11440
11585
  if (!trimmed) return null;
@@ -11452,7 +11597,7 @@ function importBookmarksFromHtml(content) {
11452
11597
  let skipped = 0;
11453
11598
  let errors = 0;
11454
11599
  load$1();
11455
- const existingUrls = new Set(state$3.bookmarks.map((b) => b.url));
11600
+ const existingUrls = new Set(state$2.bookmarks.map((b) => b.url));
11456
11601
  let currentFolderId = UNSORTED_ID;
11457
11602
  let currentFolderName = "Imported";
11458
11603
  const lines = content.split("\n");
@@ -11461,7 +11606,7 @@ function importBookmarksFromHtml(content) {
11461
11606
  const folderMatch = line.match(/<DT><H3[^>]*>([^<]+)<\/H3>/i);
11462
11607
  if (folderMatch) {
11463
11608
  currentFolderName = folderMatch[1].trim();
11464
- const existing = state$3.folders.find(
11609
+ const existing = state$2.folders.find(
11465
11610
  (f) => f.name.toLowerCase() === currentFolderName.toLowerCase()
11466
11611
  );
11467
11612
  if (existing) {
@@ -11472,7 +11617,7 @@ function importBookmarksFromHtml(content) {
11472
11617
  name: currentFolderName,
11473
11618
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
11474
11619
  };
11475
- state$3.folders.push(folder);
11620
+ state$2.folders.push(folder);
11476
11621
  currentFolderId = folder.id;
11477
11622
  }
11478
11623
  continue;
@@ -11493,7 +11638,7 @@ function importBookmarksFromHtml(content) {
11493
11638
  else errors++;
11494
11639
  continue;
11495
11640
  }
11496
- state$3.bookmarks.push({
11641
+ state$2.bookmarks.push({
11497
11642
  id: crypto$1.randomUUID(),
11498
11643
  url,
11499
11644
  title,
@@ -11519,14 +11664,14 @@ function importBookmarksFromJson(content) {
11519
11664
  const incomingFolders = Array.isArray(parsed?.folders) ? parsed.folders : [];
11520
11665
  const incomingBookmarks = Array.isArray(parsed?.bookmarks) ? parsed.bookmarks : [];
11521
11666
  load$1();
11522
- const existingUrls = new Set(state$3.bookmarks.map((b) => b.url));
11667
+ const existingUrls = new Set(state$2.bookmarks.map((b) => b.url));
11523
11668
  const folderIdMap = /* @__PURE__ */ new Map();
11524
11669
  for (const folder of incomingFolders) {
11525
11670
  if (!folder?.id || !folder?.name) {
11526
11671
  errors++;
11527
11672
  continue;
11528
11673
  }
11529
- const existing = state$3.folders.find(
11674
+ const existing = state$2.folders.find(
11530
11675
  (f) => f.name.toLowerCase() === folder.name.toLowerCase()
11531
11676
  );
11532
11677
  if (existing) {
@@ -11538,7 +11683,7 @@ function importBookmarksFromJson(content) {
11538
11683
  summary: folder.summary?.trim() || void 0,
11539
11684
  createdAt: folder.createdAt || (/* @__PURE__ */ new Date()).toISOString()
11540
11685
  };
11541
- state$3.folders.push(newFolder);
11686
+ state$2.folders.push(newFolder);
11542
11687
  folderIdMap.set(folder.id, newFolder.id);
11543
11688
  }
11544
11689
  }
@@ -11552,7 +11697,7 @@ function importBookmarksFromJson(content) {
11552
11697
  continue;
11553
11698
  }
11554
11699
  const mappedFolderId = bookmark.folderId ? folderIdMap.get(bookmark.folderId) ?? UNSORTED_ID : UNSORTED_ID;
11555
- state$3.bookmarks.push({
11700
+ state$2.bookmarks.push({
11556
11701
  id: crypto$1.randomUUID(),
11557
11702
  url: bookmark.url.trim(),
11558
11703
  title: typeof bookmark.title === "string" ? bookmark.title.trim() : bookmark.url,
@@ -11568,7 +11713,7 @@ function importBookmarksFromJson(content) {
11568
11713
  emit$2();
11569
11714
  }
11570
11715
  } catch (err) {
11571
- logger$n.warn("Failed to import bookmarks from JSON:", err);
11716
+ logger$o.warn("Failed to import bookmarks from JSON:", err);
11572
11717
  errors++;
11573
11718
  }
11574
11719
  return { imported, skipped, errors };
@@ -11973,7 +12118,7 @@ async function resolveSelector(wc, index, selector) {
11973
12118
  if (extractedSelector) return extractedSelector;
11974
12119
  return null;
11975
12120
  }
11976
- const logger$m = createLogger("LinkValidation");
12121
+ const logger$n = createLogger("LinkValidation");
11977
12122
  const DEAD_STATUS_CODES = /* @__PURE__ */ new Set([404, 410, 451]);
11978
12123
  const HEAD_FALLBACK_STATUS_CODES = /* @__PURE__ */ new Set([400, 403, 404, 405, 406, 500, 501]);
11979
12124
  function isHttpUrl(value) {
@@ -11997,7 +12142,7 @@ async function requestUrl(url, method, timeoutMs) {
11997
12142
  }
11998
12143
  });
11999
12144
  await response.body?.cancel().catch((err) => {
12000
- logger$m.debug("Failed to cancel response body:", err);
12145
+ logger$n.debug("Failed to cancel response body:", err);
12001
12146
  });
12002
12147
  return response;
12003
12148
  } finally {
@@ -12061,7 +12206,7 @@ function formatDeadLinkMessage(label, result) {
12061
12206
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
12062
12207
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
12063
12208
  }
12064
- const logger$l = createLogger("Screenshot");
12209
+ const logger$m = createLogger("Screenshot");
12065
12210
  const SCREENSHOT_RETRY_COUNT = 3;
12066
12211
  const SCREENSHOT_RETRY_BASE_DELAY_MS = 120;
12067
12212
  async function captureScreenshot(wc) {
@@ -12083,7 +12228,7 @@ async function captureScreenshot(wc) {
12083
12228
  }
12084
12229
  }
12085
12230
  } catch (err) {
12086
- logger$l.debug(
12231
+ logger$m.debug(
12087
12232
  `capturePage attempt ${attempt + 1} failed; retrying if attempts remain.`,
12088
12233
  getErrorMessage(err)
12089
12234
  );
@@ -12091,6 +12236,7 @@ async function captureScreenshot(wc) {
12091
12236
  }
12092
12237
  return errorResult("Page image was empty after 3 attempts");
12093
12238
  }
12239
+ const logger$l = createLogger("Sessions");
12094
12240
  const SESSION_VERSION = 1;
12095
12241
  function getSessionsDir() {
12096
12242
  return path$1.join(electron.app.getPath("userData"), "named-sessions");
@@ -12155,7 +12301,8 @@ function readSessionFile(filePath2) {
12155
12301
  capturedAt: (/* @__PURE__ */ new Date()).toISOString()
12156
12302
  }
12157
12303
  };
12158
- } catch {
12304
+ } catch (err) {
12305
+ logger$l.warn(`Failed to read session file ${filePath2}:`, err);
12159
12306
  return null;
12160
12307
  }
12161
12308
  }
@@ -12237,7 +12384,8 @@ async function captureLocalStorageForOrigin(tabManager, origin) {
12237
12384
  )
12238
12385
  );
12239
12386
  }
12240
- } catch {
12387
+ } catch (err) {
12388
+ logger$l.debug(`Failed to capture localStorage for origin ${origin}:`, err);
12241
12389
  return {};
12242
12390
  }
12243
12391
  return {};
@@ -12327,7 +12475,8 @@ async function loadNamedSession(tabManager, name) {
12327
12475
  for (const cookie of saved.cookies) {
12328
12476
  try {
12329
12477
  await electron.session.defaultSession.cookies.set(cookieSetDetails(cookie));
12330
- } catch {
12478
+ } catch (err) {
12479
+ logger$l.debug(`Skipping cookie ${cookie.name} for ${cookie.domain}:`, err);
12331
12480
  continue;
12332
12481
  }
12333
12482
  }
@@ -20031,7 +20180,6 @@ function installAdBlockingForSession(ses, tabManager) {
20031
20180
  );
20032
20181
  });
20033
20182
  }
20034
- const filePath$1 = () => path$1.join(electron.app.getPath("userData"), "vessel-downloads.json");
20035
20183
  const EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
20036
20184
  ".appimage",
20037
20185
  ".bat",
@@ -20044,6 +20192,7 @@ const EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
20044
20192
  ".scr",
20045
20193
  ".sh"
20046
20194
  ]);
20195
+ const DOWNLOADS_FALLBACK = { items: [] };
20047
20196
  function hasMisleadingDoubleExtension(filename) {
20048
20197
  return /\.(pdf|docx?|xlsx?|pptx?|png|jpe?g|gif|txt|zip)\.(exe|msi|bat|cmd|ps1|sh|scr|appimage)$/i.test(filename);
20049
20198
  }
@@ -20058,23 +20207,14 @@ function executableWarningDetail(item) {
20058
20207
  hasMisleadingDoubleExtension(item.filename) ? "Warning: this filename uses a misleading double extension." : null
20059
20208
  ].filter(Boolean).join("\n");
20060
20209
  }
20061
- function parse(raw) {
20062
- if (!raw || typeof raw !== "object") return { items: [] };
20063
- const items = Array.isArray(raw.items) ? raw.items : [];
20064
- return { items };
20065
- }
20066
- const state$2 = loadJsonFile({ filePath: filePath$1(), fallback: { items: [] }, parse });
20067
- const persistence$2 = createDebouncedJsonPersistence({
20068
- debounceMs: 250,
20069
- filePath: filePath$1(),
20070
- getValue: () => state$2,
20071
- logLabel: "downloads"
20210
+ const store = new PersistentState({
20211
+ filename: "vessel-downloads.json",
20212
+ fallback: DOWNLOADS_FALLBACK,
20213
+ parse: (raw) => parseArrayStateWithFallback(DownloadRecordSchema, raw, "items", DOWNLOADS_FALLBACK, "downloads"),
20214
+ logLabel: "downloads",
20215
+ debounceMs: 250
20072
20216
  });
20073
20217
  let broadcaster$1 = null;
20074
- function persist() {
20075
- state$2.items = state$2.items.slice(0, 200);
20076
- persistence$2.schedule();
20077
- }
20078
20218
  function emit$1() {
20079
20219
  broadcaster$1?.(Channels.DOWNLOADS_UPDATE, listDownloads());
20080
20220
  }
@@ -20082,30 +20222,33 @@ function setDownloadBroadcaster(fn) {
20082
20222
  broadcaster$1 = fn;
20083
20223
  }
20084
20224
  function listDownloads() {
20085
- return state$2.items.map((item) => ({ ...item }));
20225
+ return store.getState().items.map((item) => ({ ...item }));
20086
20226
  }
20087
20227
  function upsertDownload(input) {
20088
20228
  const now = (/* @__PURE__ */ new Date()).toISOString();
20089
- const existing = state$2.items.find((item) => item.savePath === input.savePath);
20090
- if (existing) {
20091
- Object.assign(existing, input, { updatedAt: now });
20092
- persist();
20093
- emit$1();
20094
- return existing;
20095
- }
20096
- const record = { id: crypto$2.randomUUID(), ...input, startedAt: now, updatedAt: now };
20097
- state$2.items = [record, ...state$2.items];
20098
- persist();
20229
+ const result = store.mutate((s) => {
20230
+ const existing = s.items.find((item) => item.savePath === input.savePath);
20231
+ if (existing) {
20232
+ Object.assign(existing, input, { updatedAt: now });
20233
+ return existing;
20234
+ } else {
20235
+ const record = { id: crypto$2.randomUUID(), ...input, startedAt: now, updatedAt: now };
20236
+ s.items = [record, ...s.items];
20237
+ s.items = s.items.slice(0, 200);
20238
+ return record;
20239
+ }
20240
+ });
20099
20241
  emit$1();
20100
- return record;
20242
+ return result;
20101
20243
  }
20102
20244
  function clearDownloads() {
20103
- state$2.items = [];
20104
- persist();
20245
+ store.mutate((s) => {
20246
+ s.items = [];
20247
+ });
20105
20248
  emit$1();
20106
20249
  }
20107
20250
  async function openDownload(id) {
20108
- const item = state$2.items.find((d) => d.id === id);
20251
+ const item = store.getState().items.find((d) => d.id === id);
20109
20252
  if (!item || item.state !== "completed" || !fs$1.existsSync(item.savePath)) return false;
20110
20253
  if (isExecutableDownload(item.savePath)) {
20111
20254
  const result = electron.dialog.showMessageBoxSync({
@@ -20122,7 +20265,7 @@ async function openDownload(id) {
20122
20265
  return await electron.shell.openPath(item.savePath) === "";
20123
20266
  }
20124
20267
  async function showDownloadInFolder(id) {
20125
- const item = state$2.items.find((d) => d.id === id);
20268
+ const item = store.getState().items.find((d) => d.id === id);
20126
20269
  if (!item || !fs$1.existsSync(item.savePath)) return false;
20127
20270
  electron.shell.showItemInFolder(item.savePath);
20128
20271
  return true;
@@ -21505,6 +21648,32 @@ function compactProviderHistory(history = []) {
21505
21648
  const omitted = normalized.slice(0, normalized.length - recent.length);
21506
21649
  return omitted.length > 0 ? [summarizeOmittedHistory(omitted), ...recent] : recent;
21507
21650
  }
21651
+ const AIQuerySchema = zod.z.string().min(1);
21652
+ const AIMessageSchema = zod.z.object({
21653
+ role: zod.z.enum(["user", "assistant"]),
21654
+ content: zod.z.string()
21655
+ });
21656
+ const AIHistorySchema = zod.z.array(AIMessageSchema).optional();
21657
+ const ReasoningEffortSchema = zod.z.enum(["off", "low", "medium", "high", "max"]);
21658
+ const ProviderConfigSchema = zod.z.object({
21659
+ id: zod.z.enum([
21660
+ "anthropic",
21661
+ "openai",
21662
+ "openai_codex",
21663
+ "openrouter",
21664
+ "ollama",
21665
+ "llama_cpp",
21666
+ "mistral",
21667
+ "xai",
21668
+ "google",
21669
+ "custom"
21670
+ ]),
21671
+ apiKey: zod.z.string(),
21672
+ hasApiKey: zod.z.boolean().optional(),
21673
+ model: zod.z.string(),
21674
+ baseUrl: zod.z.string().optional(),
21675
+ reasoningEffort: ReasoningEffortSchema.optional()
21676
+ });
21508
21677
  let activeChatProvider = null;
21509
21678
  function registerAIHandlers(tabManager, runtime2, sendToRendererViews, getResearchOrchestrator) {
21510
21679
  onAIStreamIdle(() => {
@@ -21512,10 +21681,12 @@ function registerAIHandlers(tabManager, runtime2, sendToRendererViews, getResear
21512
21681
  });
21513
21682
  electron.ipcMain.handle(Channels.AI_QUERY, async (event, query, history) => {
21514
21683
  assertTrustedIpcSender(event);
21684
+ const validatedQuery = parseIpc(AIQuerySchema, query, "query");
21685
+ const validatedHistory = history !== void 0 ? parseIpc(AIHistorySchema, history, "history") : void 0;
21515
21686
  const settings2 = loadSettings();
21516
21687
  const chatConfig = settings2.chatProvider;
21517
21688
  if (!chatConfig) {
21518
- sendToRendererViews(Channels.AI_STREAM_START, query);
21689
+ sendToRendererViews(Channels.AI_STREAM_START, validatedQuery);
21519
21690
  sendToRendererViews(
21520
21691
  Channels.AI_STREAM_CHUNK,
21521
21692
  "Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
@@ -21526,20 +21697,20 @@ function registerAIHandlers(tabManager, runtime2, sendToRendererViews, getResear
21526
21697
  if (!tryBeginAIStream("manual")) {
21527
21698
  return { accepted: false, reason: "busy" };
21528
21699
  }
21529
- sendToRendererViews(Channels.AI_STREAM_START, query);
21700
+ sendToRendererViews(Channels.AI_STREAM_START, validatedQuery);
21530
21701
  (async () => {
21531
21702
  try {
21532
21703
  activeChatProvider = createProvider(chatConfig);
21533
21704
  const activeTab = tabManager.getActiveTab();
21534
21705
  await handleAIQuery(
21535
- query,
21706
+ validatedQuery,
21536
21707
  activeChatProvider,
21537
21708
  activeTab?.view.webContents,
21538
21709
  (chunk) => sendToRendererViews(Channels.AI_STREAM_CHUNK, chunk),
21539
21710
  () => sendToRendererViews(Channels.AI_STREAM_END, "completed"),
21540
21711
  tabManager,
21541
21712
  runtime2,
21542
- compactProviderHistory(history),
21713
+ compactProviderHistory(validatedHistory),
21543
21714
  getResearchOrchestrator()
21544
21715
  );
21545
21716
  } catch (err) {
@@ -21561,12 +21732,8 @@ function registerAIHandlers(tabManager, runtime2, sendToRendererViews, getResear
21561
21732
  electron.ipcMain.handle(Channels.AI_FETCH_MODELS, async (event, config) => {
21562
21733
  assertTrustedIpcSender(event);
21563
21734
  try {
21564
- if (!config || typeof config !== "object" || !("id" in config)) {
21565
- return errorResult("Invalid provider configuration", { models: [] });
21566
- }
21567
- return await fetchProviderModels(
21568
- config
21569
- );
21735
+ const validatedConfig = parseIpc(ProviderConfigSchema, config, "providerConfig");
21736
+ return await fetchProviderModels(validatedConfig);
21570
21737
  } catch (err) {
21571
21738
  return errorResult(getErrorMessage(err), { models: [] });
21572
21739
  }
@@ -21980,429 +22147,72 @@ function registerAgentRuntimeHandlers(runtime2, chromeView, sidebarView, sendToR
21980
22147
  }
21981
22148
  );
21982
22149
  }
21983
- const DEFAULT_PAGE_FOLDER = "Vessel/Pages";
21984
- const DEFAULT_NOTE_FOLDER = "Vessel/Research";
21985
- const DEFAULT_BOOKMARK_FOLDER = "Vessel/Bookmarks";
21986
- const PAGE_CONTENT_LIMIT = 6e3;
21987
- const DEFAULT_LIST_LIMIT = 50;
21988
- const DEFAULT_SEARCH_LIMIT = 20;
21989
- function getVaultRoot() {
21990
- const configured = loadSettings().obsidianVaultPath.trim();
21991
- if (!configured) {
21992
- throw new Error(
21993
- "Obsidian not configured. Set vault path in Vessel settings to use memory capture."
21994
- );
21995
- }
21996
- return path$1.resolve(configured);
22150
+ function asTextResponse$1(text) {
22151
+ return { content: [{ type: "text", text }] };
21997
22152
  }
21998
- function assertInsideVault(targetPath, vaultRoot) {
21999
- const resolved = path$1.resolve(targetPath);
22000
- const relative = path$1.relative(vaultRoot, resolved);
22001
- if (relative.startsWith("..") || path$1.isAbsolute(relative)) {
22002
- throw new Error("Resolved note path is outside the configured vault.");
22003
- }
22004
- return resolved;
22153
+ const DANGEROUS_DEVTOOLS_ACTIONS = /* @__PURE__ */ new Set([
22154
+ "devtools_execute_js",
22155
+ "devtools_modify_dom",
22156
+ "devtools_set_storage"
22157
+ ]);
22158
+ let stateListener = null;
22159
+ const activityLog = [];
22160
+ const MAX_ACTIVITY_ENTRIES = 100;
22161
+ let activityCounter = 0;
22162
+ function setDevToolsPanelListener(listener) {
22163
+ stateListener = listener;
22005
22164
  }
22006
- function normalizeFolder(folder, fallback) {
22007
- const raw = (folder?.trim() || fallback).replace(/\\/g, "/");
22008
- if (!raw) return fallback;
22009
- if (path$1.isAbsolute(raw)) {
22010
- throw new Error("Vault note folders must be relative to the vault root.");
22011
- }
22012
- const segments = raw.split("/").filter(Boolean);
22013
- if (segments.some((segment) => segment === "." || segment === "..")) {
22014
- throw new Error("Vault note folders cannot traverse outside the vault.");
22015
- }
22016
- return segments.join(path$1.sep);
22165
+ function getDevToolsPanelState(tabId) {
22166
+ const session = tabId ? getSession(tabId) : void 0;
22167
+ return {
22168
+ console: session?.getConsoleLogs() ?? [],
22169
+ network: session?.getNetworkLog() ?? [],
22170
+ errors: session?.getErrors() ?? [],
22171
+ activity: activityLog
22172
+ };
22017
22173
  }
22018
- function normalizeNotePath(notePath) {
22019
- const raw = notePath.trim().replace(/\\/g, "/");
22020
- if (!raw) {
22021
- throw new Error("A note path is required.");
22022
- }
22023
- if (path$1.isAbsolute(raw)) {
22024
- throw new Error("Note paths must be relative to the vault root.");
22174
+ function broadcastState(tabManager) {
22175
+ if (!stateListener) return;
22176
+ const tabId = tabManager.getActiveTabId();
22177
+ stateListener(getDevToolsPanelState(tabId));
22178
+ }
22179
+ async function withDevToolsAction(runtime2, tabManager, name, args, executor) {
22180
+ const activityEntry = {
22181
+ id: ++activityCounter,
22182
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
22183
+ tool: name,
22184
+ args: JSON.stringify(args).slice(0, 200),
22185
+ result: "",
22186
+ durationMs: 0,
22187
+ status: "running"
22188
+ };
22189
+ activityLog.push(activityEntry);
22190
+ if (activityLog.length > MAX_ACTIVITY_ENTRIES) {
22191
+ activityLog.splice(0, activityLog.length - MAX_ACTIVITY_ENTRIES);
22025
22192
  }
22026
- const segments = raw.split("/").filter(Boolean);
22027
- if (segments.some((segment) => segment === "." || segment === "..")) {
22028
- throw new Error("Note paths cannot traverse outside the vault.");
22029
- }
22030
- const normalized = segments.join(path$1.sep);
22031
- return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
22032
- }
22033
- function escapeYaml(value) {
22034
- return JSON.stringify(value);
22035
- }
22036
- function renderFrontmatter(data) {
22037
- const lines = ["---"];
22038
- for (const [key2, value] of Object.entries(data)) {
22039
- if (value == null) continue;
22040
- if (Array.isArray(value)) {
22041
- if (value.length === 0) continue;
22042
- lines.push(`${key2}:`);
22043
- for (const item of value) {
22044
- lines.push(` - ${escapeYaml(item)}`);
22045
- }
22046
- continue;
22047
- }
22048
- lines.push(`${key2}: ${escapeYaml(value)}`);
22049
- }
22050
- lines.push("---", "");
22051
- return lines.join("\n");
22052
- }
22053
- function slugify(value) {
22054
- const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
22055
- return normalized || "note";
22056
- }
22057
- function buildUniqueNotePath(dir, title) {
22058
- const datePrefix = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
22059
- const slug = slugify(title);
22060
- const base = `${datePrefix}-${slug}`;
22061
- let candidate = `${base}.md`;
22062
- let counter = 2;
22063
- while (fs$1.existsSync(path$1.join(dir, candidate))) {
22064
- candidate = `${base}-${counter}.md`;
22065
- counter += 1;
22066
- }
22067
- return path$1.join(dir, candidate);
22068
- }
22069
- function trimContent(content, limit = PAGE_CONTENT_LIMIT) {
22070
- const cleaned = content.trim();
22071
- if (cleaned.length <= limit) return cleaned;
22072
- return `${cleaned.slice(0, limit)}
22073
-
22074
- [Truncated]`;
22075
- }
22076
- function parseFrontmatter(content) {
22077
- if (!content.startsWith("---\n")) {
22078
- return { body: content, tags: [] };
22079
- }
22080
- const closingIndex = content.indexOf("\n---\n", 4);
22081
- if (closingIndex === -1) {
22082
- return { body: content, tags: [] };
22083
- }
22084
- const raw = content.slice(4, closingIndex);
22085
- const body = content.slice(closingIndex + 5);
22086
- const lines = raw.split("\n");
22087
- const result = { tags: [] };
22088
- let activeArrayKey = "";
22089
- for (const line of lines) {
22090
- const trimmed = line.trim();
22091
- if (!trimmed) continue;
22092
- if (trimmed.startsWith("- ") && activeArrayKey === "tags") {
22093
- result.tags.push(
22094
- trimmed.slice(2).trim().replace(/^["']|["']$/g, "")
22095
- );
22096
- continue;
22097
- }
22098
- activeArrayKey = "";
22099
- const separatorIndex = trimmed.indexOf(":");
22100
- if (separatorIndex === -1) continue;
22101
- const key2 = trimmed.slice(0, separatorIndex).trim();
22102
- const value = trimmed.slice(separatorIndex + 1).trim();
22103
- if (key2 === "title" && value) {
22104
- result.title = value.replace(/^["']|["']$/g, "");
22105
- } else if (key2 === "tags") {
22106
- activeArrayKey = "tags";
22107
- if (value.startsWith("[") && value.endsWith("]")) {
22108
- const inline = value.slice(1, -1).split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
22109
- result.tags.push(...inline);
22110
- activeArrayKey = "";
22111
- }
22112
- }
22113
- }
22114
- return { body, title: result.title, tags: result.tags };
22115
- }
22116
- function collectMarkdownFiles(dir) {
22117
- const entries = fs$1.readdirSync(dir, { withFileTypes: true });
22118
- const files = [];
22119
- for (const entry of entries) {
22120
- const absolutePath = path$1.join(dir, entry.name);
22121
- if (entry.isDirectory()) {
22122
- files.push(...collectMarkdownFiles(absolutePath));
22123
- continue;
22124
- }
22125
- if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
22126
- files.push(absolutePath);
22127
- }
22128
- }
22129
- return files;
22130
- }
22131
- function toSummary(absolutePath, vaultRoot) {
22132
- const stats = fs$1.statSync(absolutePath);
22133
- const relativePath = path$1.relative(vaultRoot, absolutePath).split(path$1.sep).join("/");
22134
- const raw = fs$1.readFileSync(absolutePath, "utf-8");
22135
- const parsed = parseFrontmatter(raw);
22136
- const headingMatch = parsed.body.match(/^#\s+(.+)$/m);
22137
- const title = parsed.title || headingMatch?.[1]?.trim() || path$1.basename(absolutePath, ".md");
22138
- return {
22139
- title,
22140
- absolutePath,
22141
- relativePath,
22142
- modifiedAt: stats.mtime.toISOString(),
22143
- tags: parsed.tags
22144
- };
22145
- }
22146
- function renderBookmarkLinkBlock(bookmark, note) {
22147
- const lines = [
22148
- "## Linked Bookmark",
22149
- "",
22150
- `- Bookmark ID: \`${bookmark.id}\``,
22151
- `- Title: ${bookmark.title || bookmark.url}`,
22152
- `- URL: [${bookmark.url}](${bookmark.url})`,
22153
- `- Saved At: ${bookmark.savedAt}`
22154
- ];
22155
- if (note?.trim()) {
22156
- lines.push("", "### Context", "", note.trim());
22157
- }
22158
- return `${lines.join("\n")}
22159
- `;
22160
- }
22161
- function writeMemoryNote({
22162
- title,
22163
- body,
22164
- folder,
22165
- tags = [],
22166
- frontmatter = {}
22167
- }) {
22168
- const vaultRoot = getVaultRoot();
22169
- const relativeFolder = normalizeFolder(folder, DEFAULT_NOTE_FOLDER);
22170
- const targetDir = path$1.join(vaultRoot, relativeFolder);
22171
- fs$1.mkdirSync(targetDir, { recursive: true });
22172
- const absolutePath = buildUniqueNotePath(targetDir, title);
22173
- const relativePath = path$1.relative(vaultRoot, absolutePath);
22174
- const content = [
22175
- renderFrontmatter({
22176
- title,
22177
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
22178
- tags,
22179
- ...frontmatter
22180
- }),
22181
- body.trim(),
22182
- ""
22183
- ].join("\n");
22184
- fs$1.writeFileSync(absolutePath, content, "utf-8");
22185
- return {
22186
- title,
22187
- absolutePath,
22188
- relativePath: relativePath.split(path$1.sep).join("/")
22189
- };
22190
- }
22191
- function appendToMemoryNote({
22192
- notePath,
22193
- content,
22194
- heading
22195
- }) {
22196
- const vaultRoot = getVaultRoot();
22197
- const relativePath = normalizeNotePath(notePath);
22198
- const absolutePath = assertInsideVault(
22199
- path$1.join(vaultRoot, relativePath),
22200
- vaultRoot
22201
- );
22202
- if (!fs$1.existsSync(absolutePath)) {
22203
- throw new Error(
22204
- `Memory note not found: ${relativePath.split(path$1.sep).join("/")}`
22205
- );
22206
- }
22207
- const current = fs$1.readFileSync(absolutePath, "utf-8").trimEnd();
22208
- const nextParts = [current, ""];
22209
- if (heading?.trim()) {
22210
- nextParts.push(`## ${heading.trim()}`, "");
22211
- }
22212
- nextParts.push(content.trim(), "");
22213
- fs$1.writeFileSync(absolutePath, nextParts.join("\n"), "utf-8");
22214
- return {
22215
- title: path$1.basename(absolutePath, ".md"),
22216
- absolutePath,
22217
- relativePath: relativePath.split(path$1.sep).join("/")
22218
- };
22219
- }
22220
- function listMemoryNotes({
22221
- folder,
22222
- limit = DEFAULT_LIST_LIMIT
22223
- } = {}) {
22224
- const vaultRoot = getVaultRoot();
22225
- const relativeFolder = normalizeFolder(folder, "");
22226
- const targetDir = relativeFolder ? path$1.join(vaultRoot, relativeFolder) : vaultRoot;
22227
- if (!fs$1.existsSync(targetDir)) {
22228
- return [];
22229
- }
22230
- return collectMarkdownFiles(targetDir).map((absolutePath) => toSummary(absolutePath, vaultRoot)).sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt)).slice(0, Math.max(1, limit));
22231
- }
22232
- function searchMemoryNotes({
22233
- query,
22234
- folder,
22235
- tags = [],
22236
- limit = DEFAULT_SEARCH_LIMIT
22237
- }) {
22238
- const loweredQuery = query.trim().toLowerCase();
22239
- if (!loweredQuery) {
22240
- throw new Error("A non-empty memory search query is required.");
22241
- }
22242
- const vaultRoot = getVaultRoot();
22243
- const relativeFolder = normalizeFolder(folder, "");
22244
- const targetDir = relativeFolder ? path$1.join(vaultRoot, relativeFolder) : vaultRoot;
22245
- if (!fs$1.existsSync(targetDir)) {
22246
- return [];
22247
- }
22248
- const loweredTags = tags.map((tag) => tag.trim().toLowerCase()).filter(Boolean);
22249
- return collectMarkdownFiles(targetDir).map((absolutePath) => {
22250
- const raw = fs$1.readFileSync(absolutePath, "utf-8");
22251
- const parsed = parseFrontmatter(raw);
22252
- const summary = toSummary(absolutePath, vaultRoot);
22253
- const haystack = `${summary.title}
22254
- ${summary.relativePath}
22255
- ${parsed.body}`.toLowerCase();
22256
- const hasQuery = haystack.includes(loweredQuery);
22257
- const hasTags = loweredTags.length === 0 || loweredTags.every(
22258
- (tag) => summary.tags.some((noteTag) => noteTag.toLowerCase() === tag)
22259
- );
22260
- return hasQuery && hasTags ? summary : null;
22261
- }).filter((item) => item !== null).sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt)).slice(0, Math.max(1, limit));
22262
- }
22263
- function capturePageToVault({
22264
- page,
22265
- title,
22266
- folder,
22267
- summary,
22268
- note,
22269
- tags = []
22270
- }) {
22271
- const noteTitle = title?.trim() || page.title.trim() || page.url;
22272
- const bodyLines = [
22273
- `# ${noteTitle}`,
22274
- "",
22275
- `Source: [${page.title || page.url}](${page.url})`,
22276
- `Captured: ${(/* @__PURE__ */ new Date()).toISOString()}`
22277
- ];
22278
- if (page.byline) {
22279
- bodyLines.push(`Byline: ${page.byline}`);
22280
- }
22281
- bodyLines.push("");
22282
- if (summary?.trim()) {
22283
- bodyLines.push("## Summary", "", summary.trim(), "");
22284
- }
22285
- if (note?.trim()) {
22286
- bodyLines.push("## Research Note", "", note.trim(), "");
22287
- }
22288
- if (page.excerpt.trim()) {
22289
- bodyLines.push("## Excerpt", "", page.excerpt.trim(), "");
22290
- }
22291
- const snapshot2 = trimContent(page.content);
22292
- if (snapshot2) {
22293
- bodyLines.push("## Page Snapshot", "", snapshot2, "");
22294
- }
22295
- return writeMemoryNote({
22296
- title: noteTitle,
22297
- body: bodyLines.join("\n"),
22298
- folder: folder || DEFAULT_PAGE_FOLDER,
22299
- tags,
22300
- frontmatter: {
22301
- source_url: page.url,
22302
- source_title: page.title || page.url
22303
- }
22304
- });
22305
- }
22306
- function linkBookmarkToMemory({
22307
- bookmark,
22308
- notePath,
22309
- title,
22310
- folder,
22311
- note,
22312
- tags = []
22313
- }) {
22314
- if (notePath?.trim()) {
22315
- return appendToMemoryNote({
22316
- notePath,
22317
- heading: "Linked Bookmark",
22318
- content: [
22319
- `- Bookmark ID: \`${bookmark.id}\``,
22320
- `- Title: ${bookmark.title || bookmark.url}`,
22321
- `- URL: [${bookmark.url}](${bookmark.url})`,
22322
- `- Saved At: ${bookmark.savedAt}`,
22323
- note?.trim() ? `- Note: ${note.trim()}` : ""
22324
- ].filter(Boolean).join("\n")
22325
- });
22326
- }
22327
- const noteTitle = title?.trim() || bookmark.title || bookmark.url;
22328
- return writeMemoryNote({
22329
- title: noteTitle,
22330
- body: renderBookmarkLinkBlock(bookmark, note),
22331
- folder: folder || DEFAULT_BOOKMARK_FOLDER,
22332
- tags,
22333
- frontmatter: {
22334
- bookmark_id: bookmark.id,
22335
- source_url: bookmark.url,
22336
- source_title: bookmark.title || bookmark.url
22337
- }
22338
- });
22339
- }
22340
- function asTextResponse$1(text) {
22341
- return { content: [{ type: "text", text }] };
22342
- }
22343
- const DANGEROUS_DEVTOOLS_ACTIONS = /* @__PURE__ */ new Set([
22344
- "devtools_execute_js",
22345
- "devtools_modify_dom",
22346
- "devtools_set_storage"
22347
- ]);
22348
- let stateListener = null;
22349
- const activityLog = [];
22350
- const MAX_ACTIVITY_ENTRIES = 100;
22351
- let activityCounter = 0;
22352
- function setDevToolsPanelListener(listener) {
22353
- stateListener = listener;
22354
- }
22355
- function getDevToolsPanelState(tabId) {
22356
- const session = tabId ? getSession(tabId) : void 0;
22357
- return {
22358
- console: session?.getConsoleLogs() ?? [],
22359
- network: session?.getNetworkLog() ?? [],
22360
- errors: session?.getErrors() ?? [],
22361
- activity: activityLog
22362
- };
22363
- }
22364
- function broadcastState(tabManager) {
22365
- if (!stateListener) return;
22366
- const tabId = tabManager.getActiveTabId();
22367
- stateListener(getDevToolsPanelState(tabId));
22368
- }
22369
- async function withDevToolsAction(runtime2, tabManager, name, args, executor) {
22370
- const activityEntry = {
22371
- id: ++activityCounter,
22372
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
22373
- tool: name,
22374
- args: JSON.stringify(args).slice(0, 200),
22375
- result: "",
22376
- durationMs: 0,
22377
- status: "running"
22378
- };
22379
- activityLog.push(activityEntry);
22380
- if (activityLog.length > MAX_ACTIVITY_ENTRIES) {
22381
- activityLog.splice(0, activityLog.length - MAX_ACTIVITY_ENTRIES);
22382
- }
22383
- broadcastState(tabManager);
22384
- const startTime = Date.now();
22385
- try {
22386
- const result = await runtime2.runControlledAction({
22387
- source: "mcp",
22388
- name,
22389
- args,
22390
- tabId: tabManager.getActiveTabId(),
22391
- dangerous: DANGEROUS_DEVTOOLS_ACTIONS.has(name),
22392
- executor
22393
- });
22394
- activityEntry.status = "completed";
22395
- activityEntry.result = result.slice(0, 200);
22396
- activityEntry.durationMs = Date.now() - startTime;
22397
- broadcastState(tabManager);
22398
- return asTextResponse$1(result);
22399
- } catch (error) {
22400
- const message = error instanceof Error ? error.message : "Unknown error";
22401
- activityEntry.status = "failed";
22402
- activityEntry.result = message.slice(0, 200);
22403
- activityEntry.durationMs = Date.now() - startTime;
22404
- broadcastState(tabManager);
22405
- return asTextResponse$1(`Error: ${message}`);
22193
+ broadcastState(tabManager);
22194
+ const startTime = Date.now();
22195
+ try {
22196
+ const result = await runtime2.runControlledAction({
22197
+ source: "mcp",
22198
+ name,
22199
+ args,
22200
+ tabId: tabManager.getActiveTabId(),
22201
+ dangerous: DANGEROUS_DEVTOOLS_ACTIONS.has(name),
22202
+ executor
22203
+ });
22204
+ activityEntry.status = "completed";
22205
+ activityEntry.result = result.slice(0, 200);
22206
+ activityEntry.durationMs = Date.now() - startTime;
22207
+ broadcastState(tabManager);
22208
+ return asTextResponse$1(result);
22209
+ } catch (error) {
22210
+ const message = error instanceof Error ? error.message : "Unknown error";
22211
+ activityEntry.status = "failed";
22212
+ activityEntry.result = message.slice(0, 200);
22213
+ activityEntry.durationMs = Date.now() - startTime;
22214
+ broadcastState(tabManager);
22215
+ return asTextResponse$1(`Error: ${message}`);
22406
22216
  }
22407
22217
  }
22408
22218
  function registerDevTools(server, tabManager, runtime2) {
@@ -22441,334 +22251,709 @@ function registerDevTools(server, tabManager, runtime2) {
22441
22251
  title: "DevTools: Clear Console Logs",
22442
22252
  description: "Clear the captured console log buffer for the active tab."
22443
22253
  },
22444
- async () => {
22254
+ async () => {
22255
+ return withDevToolsAction(
22256
+ runtime2,
22257
+ tabManager,
22258
+ "devtools_console_clear",
22259
+ {},
22260
+ async () => {
22261
+ const session = getOrCreateSession(tabManager);
22262
+ const count = session.clearConsoleLogs();
22263
+ return `Cleared ${count} console entries.`;
22264
+ }
22265
+ );
22266
+ }
22267
+ );
22268
+ server.registerTool(
22269
+ "vessel_devtools_network_log",
22270
+ {
22271
+ title: "DevTools: Get Network Log",
22272
+ description: "Get captured network requests/responses from the active tab. Returns method, URL, status, timing, headers, and size. Automatically starts capturing on first use.",
22273
+ inputSchema: {
22274
+ url_pattern: zod.z.string().optional().describe("Filter by URL pattern (regex or substring match)"),
22275
+ method: zod.z.string().optional().describe("Filter by HTTP method (GET, POST, etc.)"),
22276
+ status_min: zod.z.number().optional().describe("Minimum HTTP status code (e.g., 400 for errors)"),
22277
+ status_max: zod.z.number().optional().describe("Maximum HTTP status code"),
22278
+ limit: zod.z.number().optional().describe("Maximum number of entries to return (default: all)")
22279
+ }
22280
+ },
22281
+ async ({ url_pattern, method, status_min, status_max, limit }) => {
22282
+ return withDevToolsAction(
22283
+ runtime2,
22284
+ tabManager,
22285
+ "devtools_network_log",
22286
+ { url_pattern, method, status_min, status_max, limit },
22287
+ async () => {
22288
+ const session = getOrCreateSession(tabManager);
22289
+ await session.ensureNetworkDomain();
22290
+ const entries = session.getNetworkLog({
22291
+ urlPattern: url_pattern,
22292
+ method,
22293
+ statusRange: status_min != null || status_max != null ? { min: status_min, max: status_max } : void 0,
22294
+ limit
22295
+ });
22296
+ if (entries.length === 0) {
22297
+ return "No network requests captured yet. Network monitoring is now active — new requests will be captured as they occur.";
22298
+ }
22299
+ return JSON.stringify(entries, null, 2);
22300
+ }
22301
+ );
22302
+ }
22303
+ );
22304
+ server.registerTool(
22305
+ "vessel_devtools_network_response_body",
22306
+ {
22307
+ title: "DevTools: Get Network Response Body",
22308
+ description: "Get the response body for a specific network request by its request ID. Use vessel_devtools_network_log first to find the request ID.",
22309
+ inputSchema: {
22310
+ request_id: zod.z.string().describe("The requestId from a network log entry")
22311
+ }
22312
+ },
22313
+ async ({ request_id }) => {
22314
+ return withDevToolsAction(
22315
+ runtime2,
22316
+ tabManager,
22317
+ "devtools_network_response_body",
22318
+ { request_id },
22319
+ async () => {
22320
+ const session = getOrCreateSession(tabManager);
22321
+ const result = await session.getNetworkResponseBody(request_id);
22322
+ if ("error" in result) return `Error: ${result.error}`;
22323
+ if (result.base64Encoded) {
22324
+ return `[Base64-encoded body, ${result.body.length} chars. Likely binary content.]`;
22325
+ }
22326
+ const body = result.body;
22327
+ return body.length > 2e4 ? body.slice(0, 2e4) + `
22328
+ ... [truncated, total ${body.length} chars]` : body;
22329
+ }
22330
+ );
22331
+ }
22332
+ );
22333
+ server.registerTool(
22334
+ "vessel_devtools_network_clear",
22335
+ {
22336
+ title: "DevTools: Clear Network Log",
22337
+ description: "Clear the captured network request buffer for the active tab."
22338
+ },
22339
+ async () => {
22340
+ return withDevToolsAction(
22341
+ runtime2,
22342
+ tabManager,
22343
+ "devtools_network_clear",
22344
+ {},
22345
+ async () => {
22346
+ const session = getOrCreateSession(tabManager);
22347
+ const count = session.clearNetworkLog();
22348
+ return `Cleared ${count} network entries.`;
22349
+ }
22350
+ );
22351
+ }
22352
+ );
22353
+ server.registerTool(
22354
+ "vessel_devtools_query_dom",
22355
+ {
22356
+ title: "DevTools: Query DOM",
22357
+ description: "Query the DOM of the active tab using a CSS selector. Returns matching elements with their attributes, node type, and optionally their HTML content. Limited to 50 results.",
22358
+ inputSchema: {
22359
+ selector: zod.z.string().describe("CSS selector to query"),
22360
+ include_html: zod.z.boolean().optional().describe("Include outerHTML of matched elements (default: false)")
22361
+ }
22362
+ },
22363
+ async ({ selector, include_html }) => {
22364
+ return withDevToolsAction(
22365
+ runtime2,
22366
+ tabManager,
22367
+ "devtools_query_dom",
22368
+ { selector, include_html },
22369
+ async () => {
22370
+ const session = getOrCreateSession(tabManager);
22371
+ const nodes = await session.queryDom(selector, {
22372
+ includeHtml: include_html
22373
+ });
22374
+ if (nodes.length === 0) {
22375
+ return `No elements found matching "${selector}"`;
22376
+ }
22377
+ return JSON.stringify(nodes, null, 2);
22378
+ }
22379
+ );
22380
+ }
22381
+ );
22382
+ server.registerTool(
22383
+ "vessel_devtools_get_styles",
22384
+ {
22385
+ title: "DevTools: Get Computed Styles",
22386
+ description: "Get computed CSS styles for an element matching a CSS selector. Optionally filter to specific properties.",
22387
+ inputSchema: {
22388
+ selector: zod.z.string().describe("CSS selector for the target element"),
22389
+ properties: zod.z.array(zod.z.string()).optional().describe(
22390
+ 'Specific CSS properties to return (e.g., ["color", "font-size", "display"]). Omit for all properties.'
22391
+ )
22392
+ }
22393
+ },
22394
+ async ({ selector, properties }) => {
22395
+ return withDevToolsAction(
22396
+ runtime2,
22397
+ tabManager,
22398
+ "devtools_get_styles",
22399
+ { selector, properties },
22400
+ async () => {
22401
+ const session = getOrCreateSession(tabManager);
22402
+ const styles = await session.getComputedStyles(selector, properties);
22403
+ if (styles.length === 0) {
22404
+ return `No computed styles found for "${selector}"`;
22405
+ }
22406
+ return JSON.stringify(styles, null, 2);
22407
+ }
22408
+ );
22409
+ }
22410
+ );
22411
+ server.registerTool(
22412
+ "vessel_devtools_modify_dom",
22413
+ {
22414
+ title: "DevTools: Modify DOM Attribute",
22415
+ description: "Set or remove an HTML attribute on an element matching a CSS selector. This is a dangerous action that modifies the page.",
22416
+ inputSchema: {
22417
+ selector: zod.z.string().describe("CSS selector for the target element"),
22418
+ attribute: zod.z.string().describe("Attribute name to set or remove"),
22419
+ value: zod.z.string().nullable().describe("Attribute value to set, or null to remove the attribute")
22420
+ }
22421
+ },
22422
+ async ({ selector, attribute, value }) => {
22423
+ return withDevToolsAction(
22424
+ runtime2,
22425
+ tabManager,
22426
+ "devtools_modify_dom",
22427
+ { selector, attribute, value },
22428
+ async () => {
22429
+ const session = getOrCreateSession(tabManager);
22430
+ return session.modifyDomAttribute(selector, attribute, value);
22431
+ }
22432
+ );
22433
+ }
22434
+ );
22435
+ server.registerTool(
22436
+ "vessel_devtools_execute_js",
22437
+ {
22438
+ title: "DevTools: Execute JavaScript",
22439
+ description: "Execute a JavaScript expression in the context of the active tab's page via the Runtime.evaluate CDP method. Supports async/await. This is a dangerous action — it can modify page state.",
22440
+ inputSchema: {
22441
+ expression: zod.z.string().describe("JavaScript expression to evaluate in the page context")
22442
+ }
22443
+ },
22444
+ async ({ expression }) => {
22445
22445
  return withDevToolsAction(
22446
22446
  runtime2,
22447
22447
  tabManager,
22448
- "devtools_console_clear",
22449
- {},
22448
+ "devtools_execute_js",
22449
+ { expression: expression.slice(0, 200) },
22450
22450
  async () => {
22451
22451
  const session = getOrCreateSession(tabManager);
22452
- const count = session.clearConsoleLogs();
22453
- return `Cleared ${count} console entries.`;
22452
+ const result = await session.executeJs(expression);
22453
+ const parts = [`[${result.type}] ${result.result}`];
22454
+ if (result.exceptionDetails) {
22455
+ parts.push(`
22456
+ Exception: ${result.exceptionDetails}`);
22457
+ }
22458
+ return parts.join("");
22454
22459
  }
22455
22460
  );
22456
22461
  }
22457
22462
  );
22458
22463
  server.registerTool(
22459
- "vessel_devtools_network_log",
22464
+ "vessel_devtools_get_storage",
22460
22465
  {
22461
- title: "DevTools: Get Network Log",
22462
- description: "Get captured network requests/responses from the active tab. Returns method, URL, status, timing, headers, and size. Automatically starts capturing on first use.",
22466
+ title: "DevTools: Get Storage",
22467
+ description: "Read browser storage for the active tab's origin. Supports localStorage, sessionStorage, cookies, and IndexedDB database listing.",
22463
22468
  inputSchema: {
22464
- url_pattern: zod.z.string().optional().describe("Filter by URL pattern (regex or substring match)"),
22465
- method: zod.z.string().optional().describe("Filter by HTTP method (GET, POST, etc.)"),
22466
- status_min: zod.z.number().optional().describe("Minimum HTTP status code (e.g., 400 for errors)"),
22467
- status_max: zod.z.number().optional().describe("Maximum HTTP status code"),
22468
- limit: zod.z.number().optional().describe("Maximum number of entries to return (default: all)")
22469
+ type: zod.z.enum(["localStorage", "sessionStorage", "cookie", "indexedDB"]).describe("Storage type to read")
22469
22470
  }
22470
22471
  },
22471
- async ({ url_pattern, method, status_min, status_max, limit }) => {
22472
+ async ({ type }) => {
22472
22473
  return withDevToolsAction(
22473
22474
  runtime2,
22474
22475
  tabManager,
22475
- "devtools_network_log",
22476
- { url_pattern, method, status_min, status_max, limit },
22476
+ "devtools_get_storage",
22477
+ { type },
22477
22478
  async () => {
22478
22479
  const session = getOrCreateSession(tabManager);
22479
- await session.ensureNetworkDomain();
22480
- const entries = session.getNetworkLog({
22481
- urlPattern: url_pattern,
22482
- method,
22483
- statusRange: status_min != null || status_max != null ? { min: status_min, max: status_max } : void 0,
22484
- limit
22485
- });
22486
- if (entries.length === 0) {
22487
- return "No network requests captured yet. Network monitoring is now active — new requests will be captured as they occur.";
22480
+ const data = await session.getStorage(type);
22481
+ const count = Object.keys(data.entries).length;
22482
+ if (count === 0) {
22483
+ return `No ${type} entries found for ${data.origin}`;
22488
22484
  }
22489
- return JSON.stringify(entries, null, 2);
22485
+ return JSON.stringify(data, null, 2);
22490
22486
  }
22491
22487
  );
22492
22488
  }
22493
22489
  );
22494
22490
  server.registerTool(
22495
- "vessel_devtools_network_response_body",
22491
+ "vessel_devtools_set_storage",
22496
22492
  {
22497
- title: "DevTools: Get Network Response Body",
22498
- description: "Get the response body for a specific network request by its request ID. Use vessel_devtools_network_log first to find the request ID.",
22493
+ title: "DevTools: Set Storage",
22494
+ description: "Set or remove a key in localStorage or sessionStorage for the active tab. This is a dangerous action that modifies page state.",
22499
22495
  inputSchema: {
22500
- request_id: zod.z.string().describe("The requestId from a network log entry")
22496
+ type: zod.z.enum(["localStorage", "sessionStorage"]).describe("Storage type to modify"),
22497
+ key: zod.z.string().describe("Storage key"),
22498
+ value: zod.z.string().nullable().describe("Value to set, or null to remove the key")
22501
22499
  }
22502
22500
  },
22503
- async ({ request_id }) => {
22501
+ async ({ type, key: key2, value }) => {
22504
22502
  return withDevToolsAction(
22505
22503
  runtime2,
22506
22504
  tabManager,
22507
- "devtools_network_response_body",
22508
- { request_id },
22505
+ "devtools_set_storage",
22506
+ { type, key: key2, value: value ? value.slice(0, 100) : null },
22509
22507
  async () => {
22510
22508
  const session = getOrCreateSession(tabManager);
22511
- const result = await session.getNetworkResponseBody(request_id);
22512
- if ("error" in result) return `Error: ${result.error}`;
22513
- if (result.base64Encoded) {
22514
- return `[Base64-encoded body, ${result.body.length} chars. Likely binary content.]`;
22515
- }
22516
- const body = result.body;
22517
- return body.length > 2e4 ? body.slice(0, 2e4) + `
22518
- ... [truncated, total ${body.length} chars]` : body;
22509
+ return session.setStorage(type, key2, value);
22519
22510
  }
22520
22511
  );
22521
22512
  }
22522
22513
  );
22523
22514
  server.registerTool(
22524
- "vessel_devtools_network_clear",
22515
+ "vessel_devtools_performance",
22525
22516
  {
22526
- title: "DevTools: Clear Network Log",
22527
- description: "Clear the captured network request buffer for the active tab."
22517
+ title: "DevTools: Performance Snapshot",
22518
+ description: "Get a performance snapshot for the active tab including navigation timing, paint metrics, memory usage, and resource loading statistics."
22528
22519
  },
22529
22520
  async () => {
22530
22521
  return withDevToolsAction(
22531
22522
  runtime2,
22532
22523
  tabManager,
22533
- "devtools_network_clear",
22524
+ "devtools_performance",
22534
22525
  {},
22535
22526
  async () => {
22536
22527
  const session = getOrCreateSession(tabManager);
22537
- const count = session.clearNetworkLog();
22538
- return `Cleared ${count} network entries.`;
22528
+ const snapshot2 = await session.getPerformanceSnapshot();
22529
+ return JSON.stringify(snapshot2, null, 2);
22539
22530
  }
22540
22531
  );
22541
22532
  }
22542
22533
  );
22543
22534
  server.registerTool(
22544
- "vessel_devtools_query_dom",
22535
+ "vessel_devtools_get_errors",
22545
22536
  {
22546
- title: "DevTools: Query DOM",
22547
- description: "Query the DOM of the active tab using a CSS selector. Returns matching elements with their attributes, node type, and optionally their HTML content. Limited to 50 results.",
22537
+ title: "DevTools: Get Errors",
22538
+ description: "Get captured JavaScript errors and unhandled promise rejections from the active tab. Automatically starts capturing on first use.",
22548
22539
  inputSchema: {
22549
- selector: zod.z.string().describe("CSS selector to query"),
22550
- include_html: zod.z.boolean().optional().describe("Include outerHTML of matched elements (default: false)")
22540
+ type: zod.z.enum(["exception", "unhandled-rejection"]).optional().describe("Filter by error type"),
22541
+ limit: zod.z.number().optional().describe("Maximum number of entries to return (default: all)")
22551
22542
  }
22552
22543
  },
22553
- async ({ selector, include_html }) => {
22544
+ async ({ type, limit }) => {
22554
22545
  return withDevToolsAction(
22555
22546
  runtime2,
22556
22547
  tabManager,
22557
- "devtools_query_dom",
22558
- { selector, include_html },
22548
+ "devtools_get_errors",
22549
+ { type, limit },
22559
22550
  async () => {
22560
22551
  const session = getOrCreateSession(tabManager);
22561
- const nodes = await session.queryDom(selector, {
22562
- includeHtml: include_html
22563
- });
22564
- if (nodes.length === 0) {
22565
- return `No elements found matching "${selector}"`;
22552
+ await session.ensureErrorCapture();
22553
+ const entries = session.getErrors({ type, limit });
22554
+ if (entries.length === 0) {
22555
+ return "No errors captured yet. Error monitoring is now active — exceptions and unhandled rejections will be captured as they occur.";
22566
22556
  }
22567
- return JSON.stringify(nodes, null, 2);
22557
+ return JSON.stringify(entries, null, 2);
22568
22558
  }
22569
22559
  );
22570
22560
  }
22571
22561
  );
22572
22562
  server.registerTool(
22573
- "vessel_devtools_get_styles",
22563
+ "vessel_devtools_clear_errors",
22574
22564
  {
22575
- title: "DevTools: Get Computed Styles",
22576
- description: "Get computed CSS styles for an element matching a CSS selector. Optionally filter to specific properties.",
22577
- inputSchema: {
22578
- selector: zod.z.string().describe("CSS selector for the target element"),
22579
- properties: zod.z.array(zod.z.string()).optional().describe(
22580
- 'Specific CSS properties to return (e.g., ["color", "font-size", "display"]). Omit for all properties.'
22581
- )
22582
- }
22565
+ title: "DevTools: Clear Errors",
22566
+ description: "Clear the captured error buffer for the active tab."
22583
22567
  },
22584
- async ({ selector, properties }) => {
22568
+ async () => {
22585
22569
  return withDevToolsAction(
22586
22570
  runtime2,
22587
22571
  tabManager,
22588
- "devtools_get_styles",
22589
- { selector, properties },
22572
+ "devtools_clear_errors",
22573
+ {},
22590
22574
  async () => {
22591
22575
  const session = getOrCreateSession(tabManager);
22592
- const styles = await session.getComputedStyles(selector, properties);
22593
- if (styles.length === 0) {
22594
- return `No computed styles found for "${selector}"`;
22595
- }
22596
- return JSON.stringify(styles, null, 2);
22576
+ const count = session.clearErrors();
22577
+ return `Cleared ${count} error entries.`;
22597
22578
  }
22598
22579
  );
22599
22580
  }
22600
- );
22601
- server.registerTool(
22602
- "vessel_devtools_modify_dom",
22603
- {
22604
- title: "DevTools: Modify DOM Attribute",
22605
- description: "Set or remove an HTML attribute on an element matching a CSS selector. This is a dangerous action that modifies the page.",
22606
- inputSchema: {
22607
- selector: zod.z.string().describe("CSS selector for the target element"),
22608
- attribute: zod.z.string().describe("Attribute name to set or remove"),
22609
- value: zod.z.string().nullable().describe("Attribute value to set, or null to remove the attribute")
22581
+ );
22582
+ }
22583
+ const DEFAULT_PAGE_FOLDER = "Vessel/Pages";
22584
+ const DEFAULT_NOTE_FOLDER = "Vessel/Research";
22585
+ const DEFAULT_BOOKMARK_FOLDER = "Vessel/Bookmarks";
22586
+ const PAGE_CONTENT_LIMIT = 6e3;
22587
+ const DEFAULT_LIST_LIMIT = 50;
22588
+ const DEFAULT_SEARCH_LIMIT = 20;
22589
+ const { access: access$1, mkdir: mkdir$1, readFile: readFile$1, readdir: readdir$1, stat, writeFile: writeFile$1 } = fs$1.promises;
22590
+ function getVaultRoot() {
22591
+ const configured = loadSettings().obsidianVaultPath.trim();
22592
+ if (!configured) {
22593
+ throw new Error(
22594
+ "Obsidian not configured. Set vault path in Vessel settings to use memory capture."
22595
+ );
22596
+ }
22597
+ return path$1.resolve(configured);
22598
+ }
22599
+ function assertInsideVault(targetPath, vaultRoot) {
22600
+ const resolved = path$1.resolve(targetPath);
22601
+ const relative = path$1.relative(vaultRoot, resolved);
22602
+ if (relative.startsWith("..") || path$1.isAbsolute(relative)) {
22603
+ throw new Error("Resolved note path is outside the configured vault.");
22604
+ }
22605
+ return resolved;
22606
+ }
22607
+ function normalizeFolder(folder, fallback) {
22608
+ const raw = (folder?.trim() || fallback).replace(/\\/g, "/");
22609
+ if (!raw) return fallback;
22610
+ if (path$1.isAbsolute(raw)) {
22611
+ throw new Error("Vault note folders must be relative to the vault root.");
22612
+ }
22613
+ const segments = raw.split("/").filter(Boolean);
22614
+ if (segments.some((segment) => segment === "." || segment === "..")) {
22615
+ throw new Error("Vault note folders cannot traverse outside the vault.");
22616
+ }
22617
+ return segments.join(path$1.sep);
22618
+ }
22619
+ function normalizeNotePath(notePath) {
22620
+ const raw = notePath.trim().replace(/\\/g, "/");
22621
+ if (!raw) {
22622
+ throw new Error("A note path is required.");
22623
+ }
22624
+ if (path$1.isAbsolute(raw)) {
22625
+ throw new Error("Note paths must be relative to the vault root.");
22626
+ }
22627
+ const segments = raw.split("/").filter(Boolean);
22628
+ if (segments.some((segment) => segment === "." || segment === "..")) {
22629
+ throw new Error("Note paths cannot traverse outside the vault.");
22630
+ }
22631
+ const normalized = segments.join(path$1.sep);
22632
+ return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
22633
+ }
22634
+ function escapeYaml(value) {
22635
+ return JSON.stringify(value);
22636
+ }
22637
+ function renderFrontmatter(data) {
22638
+ const lines = ["---"];
22639
+ for (const [key2, value] of Object.entries(data)) {
22640
+ if (value == null) continue;
22641
+ if (Array.isArray(value)) {
22642
+ if (value.length === 0) continue;
22643
+ lines.push(`${key2}:`);
22644
+ for (const item of value) {
22645
+ lines.push(` - ${escapeYaml(item)}`);
22646
+ }
22647
+ continue;
22648
+ }
22649
+ lines.push(`${key2}: ${escapeYaml(value)}`);
22650
+ }
22651
+ lines.push("---", "");
22652
+ return lines.join("\n");
22653
+ }
22654
+ function slugify(value) {
22655
+ const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
22656
+ return normalized || "note";
22657
+ }
22658
+ async function pathExists$1(filePath2) {
22659
+ try {
22660
+ await access$1(filePath2);
22661
+ return true;
22662
+ } catch {
22663
+ return false;
22664
+ }
22665
+ }
22666
+ async function buildUniqueNotePath(dir, title) {
22667
+ const datePrefix = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
22668
+ const slug = slugify(title);
22669
+ const base = `${datePrefix}-${slug}`;
22670
+ let candidate = `${base}.md`;
22671
+ let counter = 2;
22672
+ while (await pathExists$1(path$1.join(dir, candidate))) {
22673
+ candidate = `${base}-${counter}.md`;
22674
+ counter += 1;
22675
+ }
22676
+ return path$1.join(dir, candidate);
22677
+ }
22678
+ function trimContent(content, limit = PAGE_CONTENT_LIMIT) {
22679
+ const cleaned = content.trim();
22680
+ if (cleaned.length <= limit) return cleaned;
22681
+ return `${cleaned.slice(0, limit)}
22682
+
22683
+ [Truncated]`;
22684
+ }
22685
+ function parseFrontmatter(content) {
22686
+ if (!content.startsWith("---\n")) {
22687
+ return { body: content, tags: [] };
22688
+ }
22689
+ const closingIndex = content.indexOf("\n---\n", 4);
22690
+ if (closingIndex === -1) {
22691
+ return { body: content, tags: [] };
22692
+ }
22693
+ const raw = content.slice(4, closingIndex);
22694
+ const body = content.slice(closingIndex + 5);
22695
+ const lines = raw.split("\n");
22696
+ const result = { tags: [] };
22697
+ let activeArrayKey = "";
22698
+ for (const line of lines) {
22699
+ const trimmed = line.trim();
22700
+ if (!trimmed) continue;
22701
+ if (trimmed.startsWith("- ") && activeArrayKey === "tags") {
22702
+ result.tags.push(
22703
+ trimmed.slice(2).trim().replace(/^["']|["']$/g, "")
22704
+ );
22705
+ continue;
22706
+ }
22707
+ activeArrayKey = "";
22708
+ const separatorIndex = trimmed.indexOf(":");
22709
+ if (separatorIndex === -1) continue;
22710
+ const key2 = trimmed.slice(0, separatorIndex).trim();
22711
+ const value = trimmed.slice(separatorIndex + 1).trim();
22712
+ if (key2 === "title" && value) {
22713
+ result.title = value.replace(/^["']|["']$/g, "");
22714
+ } else if (key2 === "tags") {
22715
+ activeArrayKey = "tags";
22716
+ if (value.startsWith("[") && value.endsWith("]")) {
22717
+ const inline = value.slice(1, -1).split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
22718
+ result.tags.push(...inline);
22719
+ activeArrayKey = "";
22610
22720
  }
22611
- },
22612
- async ({ selector, attribute, value }) => {
22613
- return withDevToolsAction(
22614
- runtime2,
22615
- tabManager,
22616
- "devtools_modify_dom",
22617
- { selector, attribute, value },
22618
- async () => {
22619
- const session = getOrCreateSession(tabManager);
22620
- return session.modifyDomAttribute(selector, attribute, value);
22621
- }
22622
- );
22623
22721
  }
22624
- );
22625
- server.registerTool(
22626
- "vessel_devtools_execute_js",
22627
- {
22628
- title: "DevTools: Execute JavaScript",
22629
- description: "Execute a JavaScript expression in the context of the active tab's page via the Runtime.evaluate CDP method. Supports async/await. This is a dangerous action — it can modify page state.",
22630
- inputSchema: {
22631
- expression: zod.z.string().describe("JavaScript expression to evaluate in the page context")
22722
+ }
22723
+ return { body, title: result.title, tags: result.tags };
22724
+ }
22725
+ async function collectMarkdownFiles(dir) {
22726
+ const entries = await readdir$1(dir, { withFileTypes: true });
22727
+ const nestedFiles = await Promise.all(
22728
+ entries.map(async (entry) => {
22729
+ const absolutePath = path$1.join(dir, entry.name);
22730
+ if (entry.isDirectory()) {
22731
+ return collectMarkdownFiles(absolutePath);
22632
22732
  }
22633
- },
22634
- async ({ expression }) => {
22635
- return withDevToolsAction(
22636
- runtime2,
22637
- tabManager,
22638
- "devtools_execute_js",
22639
- { expression: expression.slice(0, 200) },
22640
- async () => {
22641
- const session = getOrCreateSession(tabManager);
22642
- const result = await session.executeJs(expression);
22643
- const parts = [`[${result.type}] ${result.result}`];
22644
- if (result.exceptionDetails) {
22645
- parts.push(`
22646
- Exception: ${result.exceptionDetails}`);
22647
- }
22648
- return parts.join("");
22649
- }
22650
- );
22651
- }
22652
- );
22653
- server.registerTool(
22654
- "vessel_devtools_get_storage",
22655
- {
22656
- title: "DevTools: Get Storage",
22657
- description: "Read browser storage for the active tab's origin. Supports localStorage, sessionStorage, cookies, and IndexedDB database listing.",
22658
- inputSchema: {
22659
- type: zod.z.enum(["localStorage", "sessionStorage", "cookie", "indexedDB"]).describe("Storage type to read")
22733
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
22734
+ return [absolutePath];
22660
22735
  }
22661
- },
22662
- async ({ type }) => {
22663
- return withDevToolsAction(
22664
- runtime2,
22665
- tabManager,
22666
- "devtools_get_storage",
22667
- { type },
22668
- async () => {
22669
- const session = getOrCreateSession(tabManager);
22670
- const data = await session.getStorage(type);
22671
- const count = Object.keys(data.entries).length;
22672
- if (count === 0) {
22673
- return `No ${type} entries found for ${data.origin}`;
22674
- }
22675
- return JSON.stringify(data, null, 2);
22676
- }
22677
- );
22678
- }
22736
+ return [];
22737
+ })
22679
22738
  );
22680
- server.registerTool(
22681
- "vessel_devtools_set_storage",
22682
- {
22683
- title: "DevTools: Set Storage",
22684
- description: "Set or remove a key in localStorage or sessionStorage for the active tab. This is a dangerous action that modifies page state.",
22685
- inputSchema: {
22686
- type: zod.z.enum(["localStorage", "sessionStorage"]).describe("Storage type to modify"),
22687
- key: zod.z.string().describe("Storage key"),
22688
- value: zod.z.string().nullable().describe("Value to set, or null to remove the key")
22689
- }
22690
- },
22691
- async ({ type, key: key2, value }) => {
22692
- return withDevToolsAction(
22693
- runtime2,
22694
- tabManager,
22695
- "devtools_set_storage",
22696
- { type, key: key2, value: value ? value.slice(0, 100) : null },
22697
- async () => {
22698
- const session = getOrCreateSession(tabManager);
22699
- return session.setStorage(type, key2, value);
22700
- }
22701
- );
22702
- }
22739
+ return nestedFiles.flat();
22740
+ }
22741
+ async function toSummary(absolutePath, vaultRoot, raw) {
22742
+ const stats = await stat(absolutePath);
22743
+ const relativePath = path$1.relative(vaultRoot, absolutePath).split(path$1.sep).join("/");
22744
+ const noteContent = raw ?? await readFile$1(absolutePath, "utf-8");
22745
+ const parsed = parseFrontmatter(noteContent);
22746
+ const headingMatch = parsed.body.match(/^#\s+(.+)$/m);
22747
+ const title = parsed.title || headingMatch?.[1]?.trim() || path$1.basename(absolutePath, ".md");
22748
+ return {
22749
+ title,
22750
+ absolutePath,
22751
+ relativePath,
22752
+ modifiedAt: stats.mtime.toISOString(),
22753
+ tags: parsed.tags
22754
+ };
22755
+ }
22756
+ function renderBookmarkLinkBlock(bookmark, note) {
22757
+ const lines = [
22758
+ "## Linked Bookmark",
22759
+ "",
22760
+ `- Bookmark ID: \`${bookmark.id}\``,
22761
+ `- Title: ${bookmark.title || bookmark.url}`,
22762
+ `- URL: [${bookmark.url}](${bookmark.url})`,
22763
+ `- Saved At: ${bookmark.savedAt}`
22764
+ ];
22765
+ if (note?.trim()) {
22766
+ lines.push("", "### Context", "", note.trim());
22767
+ }
22768
+ return `${lines.join("\n")}
22769
+ `;
22770
+ }
22771
+ async function writeMemoryNote({
22772
+ title,
22773
+ body,
22774
+ folder,
22775
+ tags = [],
22776
+ frontmatter = {}
22777
+ }) {
22778
+ const vaultRoot = getVaultRoot();
22779
+ const relativeFolder = normalizeFolder(folder, DEFAULT_NOTE_FOLDER);
22780
+ const targetDir = path$1.join(vaultRoot, relativeFolder);
22781
+ await mkdir$1(targetDir, { recursive: true });
22782
+ const absolutePath = await buildUniqueNotePath(targetDir, title);
22783
+ const relativePath = path$1.relative(vaultRoot, absolutePath);
22784
+ const content = [
22785
+ renderFrontmatter({
22786
+ title,
22787
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
22788
+ tags,
22789
+ ...frontmatter
22790
+ }),
22791
+ body.trim(),
22792
+ ""
22793
+ ].join("\n");
22794
+ await writeFile$1(absolutePath, content, "utf-8");
22795
+ return {
22796
+ title,
22797
+ absolutePath,
22798
+ relativePath: relativePath.split(path$1.sep).join("/")
22799
+ };
22800
+ }
22801
+ async function appendToMemoryNote({
22802
+ notePath,
22803
+ content,
22804
+ heading
22805
+ }) {
22806
+ const vaultRoot = getVaultRoot();
22807
+ const relativePath = normalizeNotePath(notePath);
22808
+ const absolutePath = assertInsideVault(
22809
+ path$1.join(vaultRoot, relativePath),
22810
+ vaultRoot
22703
22811
  );
22704
- server.registerTool(
22705
- "vessel_devtools_performance",
22706
- {
22707
- title: "DevTools: Performance Snapshot",
22708
- description: "Get a performance snapshot for the active tab including navigation timing, paint metrics, memory usage, and resource loading statistics."
22709
- },
22710
- async () => {
22711
- return withDevToolsAction(
22712
- runtime2,
22713
- tabManager,
22714
- "devtools_performance",
22715
- {},
22716
- async () => {
22717
- const session = getOrCreateSession(tabManager);
22718
- const snapshot2 = await session.getPerformanceSnapshot();
22719
- return JSON.stringify(snapshot2, null, 2);
22720
- }
22721
- );
22722
- }
22812
+ if (!await pathExists$1(absolutePath)) {
22813
+ throw new Error(
22814
+ `Memory note not found: ${relativePath.split(path$1.sep).join("/")}`
22815
+ );
22816
+ }
22817
+ const current = (await readFile$1(absolutePath, "utf-8")).trimEnd();
22818
+ const nextParts = [current, ""];
22819
+ if (heading?.trim()) {
22820
+ nextParts.push(`## ${heading.trim()}`, "");
22821
+ }
22822
+ nextParts.push(content.trim(), "");
22823
+ await writeFile$1(absolutePath, nextParts.join("\n"), "utf-8");
22824
+ return {
22825
+ title: path$1.basename(absolutePath, ".md"),
22826
+ absolutePath,
22827
+ relativePath: relativePath.split(path$1.sep).join("/")
22828
+ };
22829
+ }
22830
+ async function listMemoryNotes({
22831
+ folder,
22832
+ limit = DEFAULT_LIST_LIMIT
22833
+ } = {}) {
22834
+ const vaultRoot = getVaultRoot();
22835
+ const relativeFolder = normalizeFolder(folder, "");
22836
+ const targetDir = relativeFolder ? path$1.join(vaultRoot, relativeFolder) : vaultRoot;
22837
+ if (!await pathExists$1(targetDir)) {
22838
+ return [];
22839
+ }
22840
+ const notes = await Promise.all(
22841
+ (await collectMarkdownFiles(targetDir)).map(
22842
+ (absolutePath) => toSummary(absolutePath, vaultRoot)
22843
+ )
22723
22844
  );
22724
- server.registerTool(
22725
- "vessel_devtools_get_errors",
22726
- {
22727
- title: "DevTools: Get Errors",
22728
- description: "Get captured JavaScript errors and unhandled promise rejections from the active tab. Automatically starts capturing on first use.",
22729
- inputSchema: {
22730
- type: zod.z.enum(["exception", "unhandled-rejection"]).optional().describe("Filter by error type"),
22731
- limit: zod.z.number().optional().describe("Maximum number of entries to return (default: all)")
22732
- }
22733
- },
22734
- async ({ type, limit }) => {
22735
- return withDevToolsAction(
22736
- runtime2,
22737
- tabManager,
22738
- "devtools_get_errors",
22739
- { type, limit },
22740
- async () => {
22741
- const session = getOrCreateSession(tabManager);
22742
- await session.ensureErrorCapture();
22743
- const entries = session.getErrors({ type, limit });
22744
- if (entries.length === 0) {
22745
- return "No errors captured yet. Error monitoring is now active — exceptions and unhandled rejections will be captured as they occur.";
22746
- }
22747
- return JSON.stringify(entries, null, 2);
22748
- }
22845
+ return notes.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt)).slice(0, Math.max(1, limit));
22846
+ }
22847
+ async function searchMemoryNotes({
22848
+ query,
22849
+ folder,
22850
+ tags = [],
22851
+ limit = DEFAULT_SEARCH_LIMIT
22852
+ }) {
22853
+ const loweredQuery = query.trim().toLowerCase();
22854
+ if (!loweredQuery) {
22855
+ throw new Error("A non-empty memory search query is required.");
22856
+ }
22857
+ const vaultRoot = getVaultRoot();
22858
+ const relativeFolder = normalizeFolder(folder, "");
22859
+ const targetDir = relativeFolder ? path$1.join(vaultRoot, relativeFolder) : vaultRoot;
22860
+ if (!await pathExists$1(targetDir)) {
22861
+ return [];
22862
+ }
22863
+ const loweredTags = tags.map((tag) => tag.trim().toLowerCase()).filter(Boolean);
22864
+ const matches = await Promise.all(
22865
+ (await collectMarkdownFiles(targetDir)).map(async (absolutePath) => {
22866
+ const raw = await readFile$1(absolutePath, "utf-8");
22867
+ const parsed = parseFrontmatter(raw);
22868
+ const summary = await toSummary(absolutePath, vaultRoot, raw);
22869
+ const haystack = `${summary.title}
22870
+ ${summary.relativePath}
22871
+ ${parsed.body}`.toLowerCase();
22872
+ const hasQuery = haystack.includes(loweredQuery);
22873
+ const hasTags = loweredTags.length === 0 || loweredTags.every(
22874
+ (tag) => summary.tags.some((noteTag) => noteTag.toLowerCase() === tag)
22749
22875
  );
22750
- }
22876
+ return hasQuery && hasTags ? summary : null;
22877
+ })
22751
22878
  );
22752
- server.registerTool(
22753
- "vessel_devtools_clear_errors",
22754
- {
22755
- title: "DevTools: Clear Errors",
22756
- description: "Clear the captured error buffer for the active tab."
22757
- },
22758
- async () => {
22759
- return withDevToolsAction(
22760
- runtime2,
22761
- tabManager,
22762
- "devtools_clear_errors",
22763
- {},
22764
- async () => {
22765
- const session = getOrCreateSession(tabManager);
22766
- const count = session.clearErrors();
22767
- return `Cleared ${count} error entries.`;
22768
- }
22769
- );
22879
+ return matches.filter((item) => item !== null).sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt)).slice(0, Math.max(1, limit));
22880
+ }
22881
+ async function capturePageToVault({
22882
+ page,
22883
+ title,
22884
+ folder,
22885
+ summary,
22886
+ note,
22887
+ tags = []
22888
+ }) {
22889
+ const noteTitle = title?.trim() || page.title.trim() || page.url;
22890
+ const bodyLines = [
22891
+ `# ${noteTitle}`,
22892
+ "",
22893
+ `Source: [${page.title || page.url}](${page.url})`,
22894
+ `Captured: ${(/* @__PURE__ */ new Date()).toISOString()}`
22895
+ ];
22896
+ if (page.byline) {
22897
+ bodyLines.push(`Byline: ${page.byline}`);
22898
+ }
22899
+ bodyLines.push("");
22900
+ if (summary?.trim()) {
22901
+ bodyLines.push("## Summary", "", summary.trim(), "");
22902
+ }
22903
+ if (note?.trim()) {
22904
+ bodyLines.push("## Research Note", "", note.trim(), "");
22905
+ }
22906
+ if (page.excerpt.trim()) {
22907
+ bodyLines.push("## Excerpt", "", page.excerpt.trim(), "");
22908
+ }
22909
+ const snapshot2 = trimContent(page.content);
22910
+ if (snapshot2) {
22911
+ bodyLines.push("## Page Snapshot", "", snapshot2, "");
22912
+ }
22913
+ return await writeMemoryNote({
22914
+ title: noteTitle,
22915
+ body: bodyLines.join("\n"),
22916
+ folder: folder || DEFAULT_PAGE_FOLDER,
22917
+ tags,
22918
+ frontmatter: {
22919
+ source_url: page.url,
22920
+ source_title: page.title || page.url
22770
22921
  }
22771
- );
22922
+ });
22923
+ }
22924
+ async function linkBookmarkToMemory({
22925
+ bookmark,
22926
+ notePath,
22927
+ title,
22928
+ folder,
22929
+ note,
22930
+ tags = []
22931
+ }) {
22932
+ if (notePath?.trim()) {
22933
+ return await appendToMemoryNote({
22934
+ notePath,
22935
+ heading: "Linked Bookmark",
22936
+ content: [
22937
+ `- Bookmark ID: \`${bookmark.id}\``,
22938
+ `- Title: ${bookmark.title || bookmark.url}`,
22939
+ `- URL: [${bookmark.url}](${bookmark.url})`,
22940
+ `- Saved At: ${bookmark.savedAt}`,
22941
+ note?.trim() ? `- Note: ${note.trim()}` : ""
22942
+ ].filter(Boolean).join("\n")
22943
+ });
22944
+ }
22945
+ const noteTitle = title?.trim() || bookmark.title || bookmark.url;
22946
+ return await writeMemoryNote({
22947
+ title: noteTitle,
22948
+ body: renderBookmarkLinkBlock(bookmark, note),
22949
+ folder: folder || DEFAULT_BOOKMARK_FOLDER,
22950
+ tags,
22951
+ frontmatter: {
22952
+ bookmark_id: bookmark.id,
22953
+ source_url: bookmark.url,
22954
+ source_title: bookmark.title || bookmark.url
22955
+ }
22956
+ });
22772
22957
  }
22773
22958
  const logger$f = createLogger("MCP");
22774
22959
  function asTextResponse(text) {
@@ -23485,95 +23670,246 @@ function registerBookmarkTools(server, tabManager, runtime2) {
23485
23670
  )
23486
23671
  }
23487
23672
  },
23488
- async ({ folder_id, delete_contents }) => {
23673
+ async ({ folder_id, delete_contents }) => {
23674
+ return withAction(
23675
+ runtime2,
23676
+ tabManager,
23677
+ "remove_bookmark_folder",
23678
+ { folder_id, delete_contents },
23679
+ async () => {
23680
+ const removed = removeFolder(
23681
+ folder_id,
23682
+ delete_contents
23683
+ );
23684
+ if (!removed) return `Folder ${folder_id} not found`;
23685
+ return composeFolderAwareResponse$1(
23686
+ delete_contents ? `Removed folder ${folder_id} and deleted its bookmarks.` : `Removed folder ${folder_id}. Bookmarks moved to Unsorted.`
23687
+ );
23688
+ }
23689
+ );
23690
+ }
23691
+ );
23692
+ server.registerTool(
23693
+ "folder_rename",
23694
+ {
23695
+ title: "Rename Bookmark Folder",
23696
+ description: "Rename an existing bookmark folder.",
23697
+ inputSchema: {
23698
+ folder_id: zod.z.string().describe("ID of the folder to rename"),
23699
+ new_name: zod.z.string().describe("New name for the folder"),
23700
+ summary: zod.z.string().optional().describe("Optional one-sentence summary for the folder")
23701
+ }
23702
+ },
23703
+ async ({ folder_id, new_name, summary }) => {
23704
+ return withAction(
23705
+ runtime2,
23706
+ tabManager,
23707
+ "rename_bookmark_folder",
23708
+ { folder_id, new_name, summary },
23709
+ async () => {
23710
+ const existing = findFolderByName(new_name);
23711
+ if (existing && existing.id !== folder_id) {
23712
+ return composeFolderAwareResponse$1(
23713
+ `Folder "${existing.name}" already exists (id=${existing.id})`
23714
+ );
23715
+ }
23716
+ const folder = renameFolder(
23717
+ folder_id,
23718
+ new_name,
23719
+ summary
23720
+ );
23721
+ return folder ? composeFolderAwareResponse$1(`Renamed folder to "${folder.name}"`) : `Folder ${folder_id} not found`;
23722
+ }
23723
+ );
23724
+ }
23725
+ );
23726
+ server.registerTool(
23727
+ "memory_link_bookmark",
23728
+ {
23729
+ title: "Link Bookmark To Memory",
23730
+ description: "Create a note for a bookmark or append bookmark details into an existing memory note.",
23731
+ inputSchema: {
23732
+ bookmark_id: zod.z.string().describe("Bookmark ID to link"),
23733
+ note_path: zod.z.string().optional().describe("Existing relative note path to append into"),
23734
+ title: zod.z.string().optional().describe("Optional title when creating a new note"),
23735
+ folder: zod.z.string().optional().describe("Relative folder when creating a new note"),
23736
+ note: zod.z.string().optional().describe(
23737
+ "Optional rationale or breadcrumb to store with the bookmark"
23738
+ ),
23739
+ tags: zod.z.array(zod.z.string()).optional().describe("Optional tags when creating a new note")
23740
+ }
23741
+ },
23742
+ async ({ bookmark_id, note_path, title, folder, note, tags }) => {
23743
+ return withAction(
23744
+ runtime2,
23745
+ tabManager,
23746
+ "memory_link_bookmark",
23747
+ { bookmark_id, note_path, title, folder, tags },
23748
+ async () => {
23749
+ const bookmark = getBookmark(bookmark_id);
23750
+ if (!bookmark) {
23751
+ return `Bookmark ${bookmark_id} not found`;
23752
+ }
23753
+ const saved = await linkBookmarkToMemory({
23754
+ bookmark,
23755
+ notePath: note_path,
23756
+ title,
23757
+ folder,
23758
+ note,
23759
+ tags
23760
+ });
23761
+ return `Linked bookmark "${bookmark.title}" to memory note ${saved.relativePath}`;
23762
+ }
23763
+ );
23764
+ }
23765
+ );
23766
+ }
23767
+ function registerMemoryTools(server, tabManager, runtime2) {
23768
+ server.registerTool(
23769
+ "memory_note_create",
23770
+ {
23771
+ title: "Create Memory Note",
23772
+ description: "Write a markdown note into the configured Obsidian vault for research notes, breadcrumbs, or synthesis.",
23773
+ inputSchema: {
23774
+ title: zod.z.string().describe("Title of the note"),
23775
+ body: zod.z.string().describe("Markdown body for the note"),
23776
+ folder: zod.z.string().optional().describe(
23777
+ "Relative folder inside the vault (default: Vessel/Research)"
23778
+ ),
23779
+ tags: zod.z.array(zod.z.string()).optional().describe("Optional tags to store in frontmatter")
23780
+ }
23781
+ },
23782
+ async ({ title, body, folder, tags }) => {
23783
+ return withAction(
23784
+ runtime2,
23785
+ tabManager,
23786
+ "memory_note_create",
23787
+ { title, folder, tags },
23788
+ async () => {
23789
+ const saved = await writeMemoryNote({ title, body, folder, tags });
23790
+ return `Saved memory note "${saved.title}" to ${saved.relativePath}`;
23791
+ }
23792
+ );
23793
+ }
23794
+ );
23795
+ server.registerTool(
23796
+ "memory_append",
23797
+ {
23798
+ title: "Append Memory Note",
23799
+ description: "Append markdown content to an existing note in the configured Obsidian vault.",
23800
+ inputSchema: {
23801
+ note_path: zod.z.string().describe("Relative path to an existing note inside the vault"),
23802
+ content: zod.z.string().describe("Markdown content to append"),
23803
+ heading: zod.z.string().optional().describe("Optional section heading to add before the content")
23804
+ }
23805
+ },
23806
+ async ({ note_path, content, heading }) => {
23807
+ return withAction(
23808
+ runtime2,
23809
+ tabManager,
23810
+ "memory_note_append",
23811
+ { note_path, heading },
23812
+ async () => {
23813
+ const saved = await appendToMemoryNote({
23814
+ notePath: note_path,
23815
+ content,
23816
+ heading
23817
+ });
23818
+ return `Appended memory note at ${saved.relativePath}`;
23819
+ }
23820
+ );
23821
+ }
23822
+ );
23823
+ server.registerTool(
23824
+ "memory_list",
23825
+ {
23826
+ title: "List Memory Notes",
23827
+ description: "List recent markdown notes in the configured Obsidian vault.",
23828
+ inputSchema: {
23829
+ folder: zod.z.string().optional().describe("Optional relative folder inside the vault"),
23830
+ limit: zod.z.number().int().positive().max(200).optional().describe("Maximum number of notes to return")
23831
+ }
23832
+ },
23833
+ async ({ folder, limit }) => {
23489
23834
  return withAction(
23490
23835
  runtime2,
23491
23836
  tabManager,
23492
- "remove_bookmark_folder",
23493
- { folder_id, delete_contents },
23837
+ "memory_note_list",
23838
+ { folder, limit },
23494
23839
  async () => {
23495
- const removed = removeFolder(
23496
- folder_id,
23497
- delete_contents
23498
- );
23499
- if (!removed) return `Folder ${folder_id} not found`;
23500
- return composeFolderAwareResponse$1(
23501
- delete_contents ? `Removed folder ${folder_id} and deleted its bookmarks.` : `Removed folder ${folder_id}. Bookmarks moved to Unsorted.`
23502
- );
23840
+ const notes = await listMemoryNotes({ folder, limit });
23841
+ if (notes.length === 0) {
23842
+ return "No memory notes found.";
23843
+ }
23844
+ return notes.map(
23845
+ (note) => `- ${note.title} | path=${note.relativePath} | modified=${note.modifiedAt}${note.tags.length ? ` | tags=${note.tags.join(",")}` : ""}`
23846
+ ).join("\n");
23503
23847
  }
23504
23848
  );
23505
23849
  }
23506
23850
  );
23507
23851
  server.registerTool(
23508
- "folder_rename",
23852
+ "memory_search",
23509
23853
  {
23510
- title: "Rename Bookmark Folder",
23511
- description: "Rename an existing bookmark folder.",
23854
+ title: "Search Memory Notes",
23855
+ description: "Search markdown notes in the configured Obsidian vault by title, path, body, and optional tags.",
23512
23856
  inputSchema: {
23513
- folder_id: zod.z.string().describe("ID of the folder to rename"),
23514
- new_name: zod.z.string().describe("New name for the folder"),
23515
- summary: zod.z.string().optional().describe("Optional one-sentence summary for the folder")
23857
+ query: zod.z.string().describe("Search query"),
23858
+ folder: zod.z.string().optional().describe("Optional relative folder inside the vault"),
23859
+ tags: zod.z.array(zod.z.string()).optional().describe("Optional tags that matching notes must contain"),
23860
+ limit: zod.z.number().int().positive().max(100).optional().describe("Maximum number of matching notes to return")
23516
23861
  }
23517
23862
  },
23518
- async ({ folder_id, new_name, summary }) => {
23863
+ async ({ query, folder, tags, limit }) => {
23519
23864
  return withAction(
23520
23865
  runtime2,
23521
23866
  tabManager,
23522
- "rename_bookmark_folder",
23523
- { folder_id, new_name, summary },
23867
+ "memory_note_search",
23868
+ { query, folder, tags, limit },
23524
23869
  async () => {
23525
- const existing = findFolderByName(new_name);
23526
- if (existing && existing.id !== folder_id) {
23527
- return composeFolderAwareResponse$1(
23528
- `Folder "${existing.name}" already exists (id=${existing.id})`
23529
- );
23870
+ const notes = await searchMemoryNotes({ query, folder, tags, limit });
23871
+ if (notes.length === 0) {
23872
+ return `No memory notes matched "${query}".`;
23530
23873
  }
23531
- const folder = renameFolder(
23532
- folder_id,
23533
- new_name,
23534
- summary
23535
- );
23536
- return folder ? composeFolderAwareResponse$1(`Renamed folder to "${folder.name}"`) : `Folder ${folder_id} not found`;
23874
+ return notes.map(
23875
+ (note) => `- ${note.title} | path=${note.relativePath} | modified=${note.modifiedAt}${note.tags.length ? ` | tags=${note.tags.join(",")}` : ""}`
23876
+ ).join("\n");
23537
23877
  }
23538
23878
  );
23539
23879
  }
23540
23880
  );
23541
23881
  server.registerTool(
23542
- "memory_link_bookmark",
23882
+ "memory_page_capture",
23543
23883
  {
23544
- title: "Link Bookmark To Memory",
23545
- description: "Create a note for a bookmark or append bookmark details into an existing memory note.",
23884
+ title: "Capture Page To Memory",
23885
+ description: "Capture the current page into the configured Obsidian vault as a markdown note with URL, excerpt, and content snapshot.",
23546
23886
  inputSchema: {
23547
- bookmark_id: zod.z.string().describe("Bookmark ID to link"),
23548
- note_path: zod.z.string().optional().describe("Existing relative note path to append into"),
23549
- title: zod.z.string().optional().describe("Optional title when creating a new note"),
23550
- folder: zod.z.string().optional().describe("Relative folder when creating a new note"),
23551
- note: zod.z.string().optional().describe(
23552
- "Optional rationale or breadcrumb to store with the bookmark"
23553
- ),
23554
- tags: zod.z.array(zod.z.string()).optional().describe("Optional tags when creating a new note")
23887
+ title: zod.z.string().optional().describe("Optional note title override"),
23888
+ folder: zod.z.string().optional().describe("Relative folder inside the vault (default: Vessel/Pages)"),
23889
+ summary: zod.z.string().optional().describe("Optional summary written into the note"),
23890
+ note: zod.z.string().optional().describe("Optional research note or breadcrumb"),
23891
+ tags: zod.z.array(zod.z.string()).optional().describe("Optional tags to store in frontmatter")
23555
23892
  }
23556
23893
  },
23557
- async ({ bookmark_id, note_path, title, folder, note, tags }) => {
23894
+ async ({ title, folder, summary, note, tags }) => {
23895
+ const tab = tabManager.getActiveTab();
23896
+ if (!tab) return asNoActiveTabResponse();
23558
23897
  return withAction(
23559
23898
  runtime2,
23560
23899
  tabManager,
23561
- "memory_link_bookmark",
23562
- { bookmark_id, note_path, title, folder, tags },
23900
+ "memory_page_capture",
23901
+ { title, folder, tags },
23563
23902
  async () => {
23564
- const bookmark = getBookmark(bookmark_id);
23565
- if (!bookmark) {
23566
- return `Bookmark ${bookmark_id} not found`;
23567
- }
23568
- const saved = linkBookmarkToMemory({
23569
- bookmark,
23570
- notePath: note_path,
23903
+ const page = await extractContent$1(tab.view.webContents);
23904
+ const saved = await capturePageToVault({
23905
+ page,
23571
23906
  title,
23572
23907
  folder,
23908
+ summary,
23573
23909
  note,
23574
23910
  tags
23575
23911
  });
23576
- return `Linked bookmark "${bookmark.title}" to memory note ${saved.relativePath}`;
23912
+ return `Captured page "${saved.title}" to ${saved.relativePath}`;
23577
23913
  }
23578
23914
  );
23579
23915
  }
@@ -23822,7 +24158,7 @@ function domainMatches(pattern, hostname) {
23822
24158
  return isWildcard ? h.endsWith("." + p) : p === h;
23823
24159
  }
23824
24160
  function generateTotpCode(secret) {
23825
- const epoch = Math.floor(Date.now() / 1e3);
24161
+ const epoch = unixNow();
23826
24162
  const counter = Math.floor(epoch / 30);
23827
24163
  const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
23828
24164
  const cleanSecret = secret.replace(/[\s=-]/g, "").toUpperCase();
@@ -23867,7 +24203,8 @@ function createAuditLog(filename, maxEntries) {
23867
24203
  });
23868
24204
  fs$1.chmodSync(auditPath, 384);
23869
24205
  }
23870
- } catch {
24206
+ } catch (err) {
24207
+ logger$e.warn("Failed to trim audit log:", err);
23871
24208
  }
23872
24209
  } catch (err) {
23873
24210
  logger$e.error("Failed to write audit log:", err);
@@ -25436,7 +25773,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
25436
25773
  )
25437
25774
  }
25438
25775
  },
25439
- async ({ index, selector, text, label, durationMs, persist: persist2, color }) => {
25776
+ async ({ index, selector, text, label, durationMs, persist, color }) => {
25440
25777
  const tab = tabManager.getActiveTab();
25441
25778
  if (!tab) return asNoActiveTabResponse();
25442
25779
  const normalizedText = normalizeLooseString(text);
@@ -25450,7 +25787,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
25450
25787
  text: normalizedText,
25451
25788
  label,
25452
25789
  durationMs,
25453
- persist: persist2,
25790
+ persist,
25454
25791
  color
25455
25792
  },
25456
25793
  async () => {
@@ -25464,7 +25801,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
25464
25801
  durationMs,
25465
25802
  color
25466
25803
  );
25467
- if (persist2 && !durationMs && !result.startsWith("Error") && !result.includes("not found")) {
25804
+ if (persist && !durationMs && !result.startsWith("Error") && !result.includes("not found")) {
25468
25805
  const url = normalizeUrl$1(wc.getURL());
25469
25806
  addHighlight(
25470
25807
  url,
@@ -25637,155 +25974,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
25637
25974
  );
25638
25975
  registerBookmarkTools(server, tabManager, runtime2);
25639
25976
  registerSessionTools(server, tabManager, runtime2);
25640
- server.registerTool(
25641
- "memory_note_create",
25642
- {
25643
- title: "Create Memory Note",
25644
- description: "Write a markdown note into the configured Obsidian vault for research notes, breadcrumbs, or synthesis.",
25645
- inputSchema: {
25646
- title: zod.z.string().describe("Title of the note"),
25647
- body: zod.z.string().describe("Markdown body for the note"),
25648
- folder: zod.z.string().optional().describe(
25649
- "Relative folder inside the vault (default: Vessel/Research)"
25650
- ),
25651
- tags: zod.z.array(zod.z.string()).optional().describe("Optional tags to store in frontmatter")
25652
- }
25653
- },
25654
- async ({ title, body, folder, tags }) => {
25655
- return withAction(
25656
- runtime2,
25657
- tabManager,
25658
- "memory_note_create",
25659
- { title, folder, tags },
25660
- async () => {
25661
- const saved = writeMemoryNote({ title, body, folder, tags });
25662
- return `Saved memory note "${saved.title}" to ${saved.relativePath}`;
25663
- }
25664
- );
25665
- }
25666
- );
25667
- server.registerTool(
25668
- "memory_append",
25669
- {
25670
- title: "Append Memory Note",
25671
- description: "Append markdown content to an existing note in the configured Obsidian vault.",
25672
- inputSchema: {
25673
- note_path: zod.z.string().describe("Relative path to an existing note inside the vault"),
25674
- content: zod.z.string().describe("Markdown content to append"),
25675
- heading: zod.z.string().optional().describe("Optional section heading to add before the content")
25676
- }
25677
- },
25678
- async ({ note_path, content, heading }) => {
25679
- return withAction(
25680
- runtime2,
25681
- tabManager,
25682
- "memory_note_append",
25683
- { note_path, heading },
25684
- async () => {
25685
- const saved = appendToMemoryNote({
25686
- notePath: note_path,
25687
- content,
25688
- heading
25689
- });
25690
- return `Appended memory note at ${saved.relativePath}`;
25691
- }
25692
- );
25693
- }
25694
- );
25695
- server.registerTool(
25696
- "memory_list",
25697
- {
25698
- title: "List Memory Notes",
25699
- description: "List recent markdown notes in the configured Obsidian vault.",
25700
- inputSchema: {
25701
- folder: zod.z.string().optional().describe("Optional relative folder inside the vault"),
25702
- limit: zod.z.number().int().positive().max(200).optional().describe("Maximum number of notes to return")
25703
- }
25704
- },
25705
- async ({ folder, limit }) => {
25706
- return withAction(
25707
- runtime2,
25708
- tabManager,
25709
- "memory_note_list",
25710
- { folder, limit },
25711
- async () => {
25712
- const notes = listMemoryNotes({ folder, limit });
25713
- if (notes.length === 0) {
25714
- return "No memory notes found.";
25715
- }
25716
- return notes.map(
25717
- (note) => `- ${note.title} | path=${note.relativePath} | modified=${note.modifiedAt}${note.tags.length ? ` | tags=${note.tags.join(",")}` : ""}`
25718
- ).join("\n");
25719
- }
25720
- );
25721
- }
25722
- );
25723
- server.registerTool(
25724
- "memory_search",
25725
- {
25726
- title: "Search Memory Notes",
25727
- description: "Search markdown notes in the configured Obsidian vault by title, path, body, and optional tags.",
25728
- inputSchema: {
25729
- query: zod.z.string().describe("Search query"),
25730
- folder: zod.z.string().optional().describe("Optional relative folder inside the vault"),
25731
- tags: zod.z.array(zod.z.string()).optional().describe("Optional tags that matching notes must contain"),
25732
- limit: zod.z.number().int().positive().max(100).optional().describe("Maximum number of matching notes to return")
25733
- }
25734
- },
25735
- async ({ query, folder, tags, limit }) => {
25736
- return withAction(
25737
- runtime2,
25738
- tabManager,
25739
- "memory_note_search",
25740
- { query, folder, tags, limit },
25741
- async () => {
25742
- const notes = searchMemoryNotes({ query, folder, tags, limit });
25743
- if (notes.length === 0) {
25744
- return `No memory notes matched "${query}".`;
25745
- }
25746
- return notes.map(
25747
- (note) => `- ${note.title} | path=${note.relativePath} | modified=${note.modifiedAt}${note.tags.length ? ` | tags=${note.tags.join(",")}` : ""}`
25748
- ).join("\n");
25749
- }
25750
- );
25751
- }
25752
- );
25753
- server.registerTool(
25754
- "memory_page_capture",
25755
- {
25756
- title: "Capture Page To Memory",
25757
- description: "Capture the current page into the configured Obsidian vault as a markdown note with URL, excerpt, and content snapshot.",
25758
- inputSchema: {
25759
- title: zod.z.string().optional().describe("Optional note title override"),
25760
- folder: zod.z.string().optional().describe("Relative folder inside the vault (default: Vessel/Pages)"),
25761
- summary: zod.z.string().optional().describe("Optional summary written into the note"),
25762
- note: zod.z.string().optional().describe("Optional research note or breadcrumb"),
25763
- tags: zod.z.array(zod.z.string()).optional().describe("Optional tags to store in frontmatter")
25764
- }
25765
- },
25766
- async ({ title, folder, summary, note, tags }) => {
25767
- const tab = tabManager.getActiveTab();
25768
- if (!tab) return asNoActiveTabResponse();
25769
- return withAction(
25770
- runtime2,
25771
- tabManager,
25772
- "memory_page_capture",
25773
- { title, folder, tags },
25774
- async () => {
25775
- const page = await extractContent$1(tab.view.webContents);
25776
- const saved = capturePageToVault({
25777
- page,
25778
- title,
25779
- folder,
25780
- summary,
25781
- note,
25782
- tags
25783
- });
25784
- return `Captured page "${saved.title}" to ${saved.relativePath}`;
25785
- }
25786
- );
25787
- }
25788
- );
25977
+ registerMemoryTools(server, tabManager, runtime2);
25789
25978
  server.registerTool(
25790
25979
  "flow_start",
25791
25980
  {
@@ -27464,15 +27653,21 @@ function isSafeAutomationKitId(id) {
27464
27653
  return id.length > 0 && !KIT_ID_UNSAFE_CHAR_PATTERN.test(id);
27465
27654
  }
27466
27655
  const logger$9 = createLogger("KitRegistry");
27656
+ const { access, mkdir, readFile, readdir, unlink, writeFile } = fs$1.promises;
27467
27657
  function getUserKitsDir() {
27468
27658
  return path$1.join(electron.app.getPath("userData"), "kits");
27469
27659
  }
27470
- function ensureKitsDir() {
27471
- const dir = getUserKitsDir();
27472
- if (!fs$1.existsSync(dir)) {
27473
- fs$1.mkdirSync(dir, { recursive: true });
27660
+ async function pathExists(filePath2) {
27661
+ try {
27662
+ await access(filePath2);
27663
+ return true;
27664
+ } catch {
27665
+ return false;
27474
27666
  }
27475
27667
  }
27668
+ async function ensureKitsDir() {
27669
+ await mkdir(getUserKitsDir(), { recursive: true });
27670
+ }
27476
27671
  function getKitFilePath(id) {
27477
27672
  if (!isSafeAutomationKitId(id)) return null;
27478
27673
  const kitsDir = path$1.resolve(getUserKitsDir());
@@ -27484,12 +27679,12 @@ function isValidKit(value) {
27484
27679
  const k = value;
27485
27680
  return typeof k.id === "string" && isSafeAutomationKitId(k.id) && typeof k.name === "string" && k.name.length > 0 && typeof k.description === "string" && typeof k.category === "string" && VALID_KIT_CATEGORIES.has(k.category) && typeof k.icon === "string" && typeof k.promptTemplate === "string" && k.promptTemplate.length > 0 && Array.isArray(k.inputs);
27486
27681
  }
27487
- function getInstalledKits() {
27488
- ensureKitsDir();
27682
+ async function getInstalledKits() {
27683
+ await ensureKitsDir();
27489
27684
  const dir = getUserKitsDir();
27490
27685
  let files;
27491
27686
  try {
27492
- files = fs$1.readdirSync(dir).filter((f) => f.endsWith(".kit.json"));
27687
+ files = (await readdir(dir)).filter((f) => f.endsWith(".kit.json"));
27493
27688
  } catch (err) {
27494
27689
  logger$9.warn("Failed to read kit directory:", err);
27495
27690
  return [];
@@ -27497,7 +27692,7 @@ function getInstalledKits() {
27497
27692
  const kits = [];
27498
27693
  for (const file of files) {
27499
27694
  try {
27500
- const raw = fs$1.readFileSync(path$1.join(dir, file), "utf-8");
27695
+ const raw = await readFile(path$1.join(dir, file), "utf-8");
27501
27696
  const parsed = JSON.parse(raw);
27502
27697
  if (isValidKit(parsed)) {
27503
27698
  kits.push(parsed);
@@ -27521,7 +27716,7 @@ async function installKitFromFile() {
27521
27716
  }
27522
27717
  let raw;
27523
27718
  try {
27524
- raw = fs$1.readFileSync(filePaths[0], "utf-8");
27719
+ raw = await readFile(filePaths[0], "utf-8");
27525
27720
  } catch (err) {
27526
27721
  logger$9.warn("Failed to read selected kit file:", err);
27527
27722
  return errorResult("Could not read the selected file.");
@@ -27543,20 +27738,20 @@ async function installKitFromFile() {
27543
27738
  `Kit id "${parsed.id}" conflicts with a built-in kit and cannot be overwritten.`
27544
27739
  );
27545
27740
  }
27546
- ensureKitsDir();
27741
+ await ensureKitsDir();
27547
27742
  const dest = getKitFilePath(parsed.id);
27548
27743
  if (!dest) {
27549
27744
  return errorResult("Kit id contains unsupported characters.");
27550
27745
  }
27551
27746
  try {
27552
- fs$1.writeFileSync(dest, JSON.stringify(parsed, null, 2), "utf-8");
27747
+ await writeFile(dest, JSON.stringify(parsed, null, 2), "utf-8");
27553
27748
  } catch (err) {
27554
27749
  logger$9.warn("Failed to save kit file:", err);
27555
27750
  return errorResult("Failed to save the kit file.");
27556
27751
  }
27557
27752
  return okResult({ kit: parsed });
27558
27753
  }
27559
- function uninstallKit(id, scheduledKitIds) {
27754
+ async function uninstallKit(id, scheduledKitIds) {
27560
27755
  if (BUNDLED_KIT_IDS.has(id)) {
27561
27756
  return errorResult("Built-in kits cannot be removed.");
27562
27757
  }
@@ -27565,16 +27760,16 @@ function uninstallKit(id, scheduledKitIds) {
27565
27760
  "This kit has active scheduled jobs. Delete or reassign them first."
27566
27761
  );
27567
27762
  }
27568
- ensureKitsDir();
27763
+ await ensureKitsDir();
27569
27764
  const target = getKitFilePath(id);
27570
27765
  if (!target) {
27571
27766
  return errorResult("Kit id contains unsupported characters.");
27572
27767
  }
27573
- if (!fs$1.existsSync(target)) {
27768
+ if (!await pathExists(target)) {
27574
27769
  return errorResult("Kit not found.");
27575
27770
  }
27576
27771
  try {
27577
- fs$1.unlinkSync(target);
27772
+ await unlink(target);
27578
27773
  return okResult();
27579
27774
  } catch (err) {
27580
27775
  logger$9.warn("Failed to remove kit file:", err);
@@ -27910,17 +28105,20 @@ function registerSystemHandlers(windowState2, sendToRendererViews) {
27910
28105
  layoutViews(windowState2);
27911
28106
  return clamped;
27912
28107
  });
27913
- electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, (event) => {
28108
+ electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, async (event) => {
27914
28109
  assertTrustedIpcSender(event);
27915
- return getInstalledKits();
28110
+ return await getInstalledKits();
27916
28111
  });
27917
28112
  electron.ipcMain.handle(Channels.AUTOMATION_INSTALL_FROM_FILE, async (event) => {
27918
28113
  assertTrustedIpcSender(event);
27919
28114
  return await installKitFromFile();
27920
28115
  });
27921
- electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, (event, id) => {
28116
+ electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, async (event, id) => {
27922
28117
  assertTrustedIpcSender(event);
27923
- return uninstallKit(parseIpc(KitIdSchema, id, "id"), getScheduledKitIds());
28118
+ return await uninstallKit(
28119
+ parseIpc(KitIdSchema, id, "id"),
28120
+ getScheduledKitIds()
28121
+ );
27924
28122
  });
27925
28123
  electron.ipcMain.handle(Channels.CLEAR_BROWSING_DATA, async (event, options) => {
27926
28124
  assertTrustedIpcSender(event);
@@ -27984,6 +28182,25 @@ function registerSystemHandlers(windowState2, sendToRendererViews) {
27984
28182
  return togglePictureInPicture(tabManager);
27985
28183
  });
27986
28184
  }
28185
+ const FolderNameSchema = zod.z.string().min(1);
28186
+ const BookmarkUrlSchema = zod.z.string().min(1);
28187
+ const BookmarkIdSchema = zod.z.string().min(1);
28188
+ const OptionalStringSchema = zod.z.string().optional();
28189
+ const OptionalStringArraySchema = zod.z.array(zod.z.string()).optional();
28190
+ const OptionalRecordSchema = zod.z.record(zod.z.string(), zod.z.string()).optional();
28191
+ const OptionalBooleanSchema = zod.z.boolean().optional();
28192
+ const BookmarkUpdateSchema = zod.z.object({
28193
+ title: zod.z.string().optional(),
28194
+ note: zod.z.string().optional(),
28195
+ folderId: zod.z.string().optional(),
28196
+ intent: zod.z.string().optional(),
28197
+ expectedContent: zod.z.string().optional(),
28198
+ keyFields: zod.z.array(zod.z.string()).optional(),
28199
+ agentHints: zod.z.record(zod.z.string(), zod.z.string()).optional()
28200
+ });
28201
+ const ExportOptionsSchema = zod.z.object({
28202
+ includeNotes: zod.z.boolean().optional()
28203
+ }).optional();
27987
28204
  function getSafeBookmarkExportName(name) {
27988
28205
  const safeName = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
27989
28206
  return safeName || "folder";
@@ -27997,23 +28214,33 @@ function registerBookmarkHandlers() {
27997
28214
  Channels.FOLDER_CREATE,
27998
28215
  (event, name, summary) => {
27999
28216
  assertTrustedIpcSender(event);
28217
+ const validatedName = parseIpc(FolderNameSchema, name, "name");
28218
+ const validatedSummary = parseIpc(OptionalStringSchema, summary, "summary");
28000
28219
  trackBookmarkAction("folder_create");
28001
- return createFolderWithSummary(name, summary);
28220
+ return createFolderWithSummary(validatedName, validatedSummary);
28002
28221
  }
28003
28222
  );
28004
28223
  electron.ipcMain.handle(
28005
28224
  Channels.BOOKMARK_SAVE,
28006
28225
  (event, url, title, folderId, note, intent, expectedContent, keyFields, agentHints) => {
28007
28226
  assertTrustedIpcSender(event);
28227
+ const validatedUrl = parseIpc(BookmarkUrlSchema, url, "url");
28228
+ const validatedTitle = parseIpc(zod.z.string(), title, "title");
28229
+ const validatedFolderId = parseIpc(OptionalStringSchema, folderId, "folderId");
28230
+ const validatedNote = parseIpc(OptionalStringSchema, note, "note");
28231
+ const validatedIntent = parseIpc(OptionalStringSchema, intent, "intent");
28232
+ const validatedExpectedContent = parseIpc(OptionalStringSchema, expectedContent, "expectedContent");
28233
+ const validatedKeyFields = parseIpc(OptionalStringArraySchema, keyFields, "keyFields");
28234
+ const validatedAgentHints = parseIpc(OptionalRecordSchema, agentHints, "agentHints");
28008
28235
  trackBookmarkAction("save");
28009
- const result = saveBookmarkWithPolicy(url, title, folderId, note, {
28236
+ const result = saveBookmarkWithPolicy(validatedUrl, validatedTitle, validatedFolderId, validatedNote, {
28010
28237
  onDuplicate: "update",
28011
28238
  extra: {
28012
28239
  ...normalizeBookmarkMetadata({
28013
- intent,
28014
- expectedContent,
28015
- keyFields,
28016
- agentHints
28240
+ intent: validatedIntent,
28241
+ expectedContent: validatedExpectedContent,
28242
+ keyFields: validatedKeyFields,
28243
+ agentHints: validatedAgentHints
28017
28244
  })
28018
28245
  }
28019
28246
  });
@@ -28027,19 +28254,23 @@ function registerBookmarkHandlers() {
28027
28254
  Channels.BOOKMARK_UPDATE,
28028
28255
  (event, id, updates) => {
28029
28256
  assertTrustedIpcSender(event);
28257
+ const validatedId = parseIpc(BookmarkIdSchema, id, "id");
28258
+ const validatedUpdates = parseIpc(BookmarkUpdateSchema, updates, "updates");
28030
28259
  trackBookmarkAction("save");
28031
- return updateBookmark(id, updates);
28260
+ return updateBookmark(validatedId, validatedUpdates);
28032
28261
  }
28033
28262
  );
28034
28263
  electron.ipcMain.handle(Channels.BOOKMARK_REMOVE, (event, id) => {
28035
28264
  assertTrustedIpcSender(event);
28265
+ const validatedId = parseIpc(BookmarkIdSchema, id, "id");
28036
28266
  trackBookmarkAction("remove");
28037
- return removeBookmark(id);
28267
+ return removeBookmark(validatedId);
28038
28268
  });
28039
28269
  electron.ipcMain.handle(
28040
28270
  Channels.BOOKMARKS_EXPORT_HTML,
28041
28271
  async (event, options) => {
28042
28272
  assertTrustedIpcSender(event);
28273
+ const validatedOptions = parseIpc(ExportOptionsSchema, options, "options");
28043
28274
  const { canceled, filePath: filePath2 } = await electron.dialog.showSaveDialog({
28044
28275
  title: "Export Bookmarks",
28045
28276
  defaultPath: "vessel-bookmarks.html",
@@ -28047,7 +28278,7 @@ function registerBookmarkHandlers() {
28047
28278
  });
28048
28279
  if (canceled || !filePath2) return null;
28049
28280
  const content = exportBookmarksHtml({
28050
- includeNotes: options?.includeNotes ?? false
28281
+ includeNotes: validatedOptions?.includeNotes ?? false
28051
28282
  });
28052
28283
  await fs.promises.writeFile(filePath2, content, "utf-8");
28053
28284
  trackBookmarkAction("export");
@@ -28077,7 +28308,9 @@ function registerBookmarkHandlers() {
28077
28308
  Channels.FOLDER_EXPORT_HTML,
28078
28309
  async (event, folderId, options) => {
28079
28310
  assertTrustedIpcSender(event);
28080
- const folder = getFolder(folderId);
28311
+ const validatedFolderId = parseIpc(BookmarkIdSchema, folderId, "folderId");
28312
+ const validatedOptions = parseIpc(ExportOptionsSchema, options, "options");
28313
+ const folder = getFolder(validatedFolderId);
28081
28314
  if (!folder) return null;
28082
28315
  const { canceled, filePath: filePath2 } = await electron.dialog.showSaveDialog({
28083
28316
  title: `Export ${folder.name}`,
@@ -28085,8 +28318,8 @@ function registerBookmarkHandlers() {
28085
28318
  filters: [{ name: "HTML Bookmarks", extensions: ["html"] }]
28086
28319
  });
28087
28320
  if (canceled || !filePath2) return null;
28088
- const result = exportBookmarkFolderHtml(folderId, {
28089
- includeNotes: options?.includeNotes ?? true
28321
+ const result = exportBookmarkFolderHtml(validatedFolderId, {
28322
+ includeNotes: validatedOptions?.includeNotes ?? true
28090
28323
  });
28091
28324
  if (!result) return null;
28092
28325
  await fs.promises.writeFile(filePath2, result.content, "utf-8");
@@ -28127,14 +28360,23 @@ function registerBookmarkHandlers() {
28127
28360
  });
28128
28361
  electron.ipcMain.handle(Channels.FOLDER_REMOVE, (event, id, deleteContents) => {
28129
28362
  assertTrustedIpcSender(event);
28363
+ const validatedId = parseIpc(BookmarkIdSchema, id, "id");
28364
+ const validatedDeleteContents = parseIpc(
28365
+ OptionalBooleanSchema,
28366
+ deleteContents,
28367
+ "deleteContents"
28368
+ );
28130
28369
  trackBookmarkAction("folder_remove");
28131
- return removeFolder(id, deleteContents ?? false);
28370
+ return removeFolder(validatedId, validatedDeleteContents ?? false);
28132
28371
  });
28133
28372
  electron.ipcMain.handle(
28134
28373
  Channels.FOLDER_RENAME,
28135
28374
  (event, id, newName, summary) => {
28136
28375
  assertTrustedIpcSender(event);
28137
- return renameFolder(id, newName, summary);
28376
+ const validatedId = parseIpc(BookmarkIdSchema, id, "id");
28377
+ const validatedName = parseIpc(FolderNameSchema, newName, "newName");
28378
+ const validatedSummary = parseIpc(OptionalStringSchema, summary, "summary");
28379
+ return renameFolder(validatedId, validatedName, validatedSummary);
28138
28380
  }
28139
28381
  );
28140
28382
  }
@@ -28399,6 +28641,7 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
28399
28641
  return result;
28400
28642
  });
28401
28643
  }
28644
+ const SessionNameSchema = zod.z.string().min(1);
28402
28645
  function registerSessionHandlers(tabManager) {
28403
28646
  electron.ipcMain.handle(Channels.SESSION_LIST, (event) => {
28404
28647
  assertTrustedIpcSender(event);
@@ -28406,18 +28649,18 @@ function registerSessionHandlers(tabManager) {
28406
28649
  });
28407
28650
  electron.ipcMain.handle(Channels.SESSION_SAVE, async (event, name) => {
28408
28651
  assertTrustedIpcSender(event);
28409
- assertString(name, "name");
28410
- return await saveNamedSession(tabManager, name);
28652
+ const validatedName = parseIpc(SessionNameSchema, name, "name");
28653
+ return await saveNamedSession(tabManager, validatedName);
28411
28654
  });
28412
28655
  electron.ipcMain.handle(Channels.SESSION_LOAD, async (event, name) => {
28413
28656
  assertTrustedIpcSender(event);
28414
- assertString(name, "name");
28415
- return await loadNamedSession(tabManager, name);
28657
+ const validatedName = parseIpc(SessionNameSchema, name, "name");
28658
+ return await loadNamedSession(tabManager, validatedName);
28416
28659
  });
28417
28660
  electron.ipcMain.handle(Channels.SESSION_DELETE, (event, name) => {
28418
28661
  assertTrustedIpcSender(event);
28419
- assertString(name, "name");
28420
- return deleteNamedSession(name);
28662
+ const validatedName = parseIpc(SessionNameSchema, name, "name");
28663
+ return deleteNamedSession(validatedName);
28421
28664
  });
28422
28665
  }
28423
28666
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -30507,7 +30750,8 @@ ${progress}
30507
30750
  const raw = fs$1.readFileSync(getRuntimeStatePath(), "utf-8");
30508
30751
  const parsed = JSON.parse(raw);
30509
30752
  return sanitizePersistence(parsed);
30510
- } catch {
30753
+ } catch (err) {
30754
+ logger$3.warn("Failed to load persisted runtime state, starting fresh:", err);
30511
30755
  return sanitizePersistence(null);
30512
30756
  }
30513
30757
  }
@@ -30766,7 +31010,7 @@ function findIconBase64() {
30766
31010
  return "";
30767
31011
  }
30768
31012
  function buildSplashHTML(iconSrc) {
30769
- const imgTag = iconSrc ? `<img class="logo" src="${iconSrc}" alt="" />` : `<div class="logo-fallback">V</div>`;
31013
+ const imgTag = iconSrc ? `<img class="logo" src="${escapeHtml(iconSrc)}" alt="" />` : `<div class="logo-fallback">V</div>`;
30770
31014
  return `<!DOCTYPE html>
30771
31015
  <html>
30772
31016
  <head>
@@ -30906,7 +31150,8 @@ function createSplashWindow() {
30906
31150
  splash.once("closed", () => {
30907
31151
  try {
30908
31152
  fs$1.rmSync(tmpDir, { recursive: true, force: true });
30909
- } catch {
31153
+ } catch (err) {
31154
+ logger$1.debug("Failed to clean up splash temp dir:", err);
30910
31155
  }
30911
31156
  });
30912
31157
  fs$1.writeFileSync(tmpPath, html, "utf-8");