@quanta-intellect/vessel-browser 0.1.136 → 0.1.138

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
@@ -10,6 +10,8 @@ const OpenAI = require("openai");
10
10
  const crypto$2 = require("node:crypto");
11
11
  const http = require("http");
12
12
  const path$1 = require("node:path");
13
+ const readability = require("@mozilla/readability");
14
+ const linkedom = require("linkedom");
13
15
  const node_module = require("node:module");
14
16
  const http$1 = require("node:http");
15
17
  const os = require("node:os");
@@ -494,6 +496,10 @@ function assertSafeURL(url) {
494
496
  }
495
497
  function assertPermittedNavigationURL(url) {
496
498
  assertSafeURL(url);
499
+ const airGapError = getAirGapBlockReason(url);
500
+ if (airGapError) {
501
+ throw new Error(airGapError);
502
+ }
497
503
  const policyError = checkDomainPolicy(url);
498
504
  if (policyError) {
499
505
  throw new Error(policyError);
@@ -589,11 +595,11 @@ class Tab {
589
595
  return null;
590
596
  }
591
597
  try {
592
- assertSafeURL(url);
598
+ assertPermittedNavigationURL(url);
593
599
  } catch (error) {
594
600
  return error instanceof Error ? error.message : "Blocked unsafe navigation";
595
601
  }
596
- return checkDomainPolicy(url);
602
+ return null;
597
603
  }
598
604
  guardedLoadURL(url, options) {
599
605
  const blockReason = this.getNavigationBlockReason(url);
@@ -3705,6 +3711,8 @@ const AutofillChannels = {
3705
3711
  const AutomationChannels = {
3706
3712
  AUTOMATION_GET_INSTALLED: "automation:get-installed",
3707
3713
  AUTOMATION_INSTALL_FROM_FILE: "automation:install-from-file",
3714
+ AUTOMATION_CREATE_FROM_TEXT: "automation:create-from-text",
3715
+ AUTOMATION_UPDATE_FROM_TEXT: "automation:update-from-text",
3708
3716
  AUTOMATION_UNINSTALL: "automation:uninstall",
3709
3717
  AUTOMATION_ACTIVITY_START: "automation:activity-start",
3710
3718
  AUTOMATION_ACTIVITY_CHUNK: "automation:activity-chunk",
@@ -4251,10 +4259,28 @@ function buildPageSnapshotKey(rawUrl) {
4251
4259
  return normalizePageUrl(rawUrl);
4252
4260
  }
4253
4261
  }
4262
+ function isDocumentViewerUrl(rawUrl) {
4263
+ try {
4264
+ const url = new URL(rawUrl);
4265
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
4266
+ const pathname = decodeURIComponent(url.pathname).toLowerCase();
4267
+ if (/\.(pdf|epub|mobi|cbz|cbr)(?:$|[?#])/.test(pathname)) {
4268
+ return true;
4269
+ }
4270
+ const host = url.hostname.toLowerCase().replace(/^www\./, "");
4271
+ if (host === "archive.org") {
4272
+ return /^\/(details|stream|download)\//.test(pathname);
4273
+ }
4274
+ return false;
4275
+ } catch {
4276
+ return false;
4277
+ }
4278
+ }
4254
4279
  function isTrackablePageUrl(rawUrl) {
4255
4280
  try {
4256
4281
  const url = new URL(rawUrl);
4257
- return url.protocol === "http:" || url.protocol === "https:";
4282
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
4283
+ return !isDocumentViewerUrl(rawUrl);
4258
4284
  } catch {
4259
4285
  return false;
4260
4286
  }
@@ -4951,6 +4977,11 @@ const PREMIUM_TOOLS = /* @__PURE__ */ new Set([
4951
4977
  "human_vault_list",
4952
4978
  "human_vault_fill",
4953
4979
  "human_vault_remove",
4980
+ "memory_note_create",
4981
+ "memory_note_append",
4982
+ "memory_note_list",
4983
+ "memory_note_search",
4984
+ "memory_page_capture",
4954
4985
  "research_confirm_brief",
4955
4986
  "research_approve_objectives",
4956
4987
  "research_export_report"
@@ -6663,7 +6694,7 @@ async function extractContentInner(webContents) {
6663
6694
  webContents
6664
6695
  );
6665
6696
  }
6666
- async function extractContent$1(webContents) {
6697
+ async function extractContent(webContents) {
6667
6698
  const cacheKey = `${webContents.id}:${webContents.getURL() || ""}`;
6668
6699
  const cached = extractionCache.get(cacheKey);
6669
6700
  if (cached) {
@@ -6881,7 +6912,7 @@ async function capturePageSnapshot(url, wc, sendToRendererViews) {
6881
6912
  if (!shouldTrackSnapshotUrl(url)) return;
6882
6913
  const key2 = normalizeUrl(url);
6883
6914
  const oldSnap = getSnapshot(key2);
6884
- const content = await extractContent$1(wc);
6915
+ const content = await extractContent(wc);
6885
6916
  const textContent = content.content || "";
6886
6917
  const title = content.title || "";
6887
6918
  const headings = content.headings || [];
@@ -12113,7 +12144,7 @@ async function resolveSelector(wc, index, selector) {
12113
12144
  if (typeof fallbackSelector === "string" && fallbackSelector) {
12114
12145
  return fallbackSelector;
12115
12146
  }
12116
- const page = await extractContent$1(wc);
12147
+ const page = await extractContent(wc);
12117
12148
  const extractedSelector = findSelectorByIndex(page, index);
12118
12149
  if (extractedSelector) return extractedSelector;
12119
12150
  return null;
@@ -12186,6 +12217,15 @@ async function validateLinkDestination(url, timeoutMs = 3500) {
12186
12217
  detail: "Non-HTTP URL"
12187
12218
  };
12188
12219
  }
12220
+ try {
12221
+ assertPermittedNavigationURL(url);
12222
+ } catch (error) {
12223
+ return {
12224
+ status: "unknown",
12225
+ checkedUrl: url,
12226
+ detail: error instanceof Error ? error.message : "Navigation policy blocked URL"
12227
+ };
12228
+ }
12189
12229
  try {
12190
12230
  const headResponse = await requestUrl(url, "HEAD", timeoutMs);
12191
12231
  if (!HEAD_FALLBACK_STATUS_CODES.has(headResponse.status)) {
@@ -14849,6 +14889,22 @@ function getGlanceExtractScript() {
14849
14889
  };
14850
14890
  })()`;
14851
14891
  }
14892
+ function cleanArticleText(value) {
14893
+ return value.replace(/\u00a0/g, " ").replace(/[ \t]+/g, " ").replace(/\n[ \t]+/g, "\n").replace(/(\n\s*){3,}/g, "\n\n").trim();
14894
+ }
14895
+ function articleTextResultToOutput(result, mode, source, elapsedMs) {
14896
+ const sections = [
14897
+ `# ${result.title || "(untitled)"}`,
14898
+ `URL: ${result.url}`,
14899
+ "",
14900
+ `[read_page mode=${mode} — ${source}, ${elapsedMs}ms]`
14901
+ ];
14902
+ if (result.headings.length > 0) {
14903
+ sections.push("", "## Headings", ...result.headings);
14904
+ }
14905
+ sections.push("", "## Article Text", "", result.text);
14906
+ return sections.join("\n");
14907
+ }
14852
14908
  async function glanceExtract(wc) {
14853
14909
  const startMs = Date.now();
14854
14910
  const result = await executePageScript(wc, getGlanceExtractScript(), { timeoutMs: 2500, label: "glance-extract" });
@@ -14896,6 +14952,168 @@ async function glanceExtract(wc) {
14896
14952
  }
14897
14953
  return sections.join("\n");
14898
14954
  }
14955
+ async function fastArticleTextExtract(wc, mode) {
14956
+ const startMs = Date.now();
14957
+ const result = await executePageScript(
14958
+ wc,
14959
+ `(function() {
14960
+ function clean(value) {
14961
+ return String(value || '').replace(/[ \\t]+/g, ' ').replace(/(\\n\\s*){3,}/g, '\\n\\n').trim();
14962
+ }
14963
+
14964
+ var rootSelectors = [
14965
+ '#mw-content-text .mw-parser-output',
14966
+ '#mw-content-text',
14967
+ 'main article',
14968
+ 'article',
14969
+ 'main',
14970
+ '[role="main"]',
14971
+ '#content'
14972
+ ];
14973
+ var root = null;
14974
+ for (var i = 0; i < rootSelectors.length; i++) {
14975
+ var candidate = document.querySelector(rootSelectors[i]);
14976
+ if (candidate && clean(candidate.textContent).length > 300) {
14977
+ root = candidate;
14978
+ break;
14979
+ }
14980
+ }
14981
+ if (!root) return null;
14982
+
14983
+ var unwantedSelector = [
14984
+ 'script',
14985
+ 'style',
14986
+ 'noscript',
14987
+ 'nav',
14988
+ 'header',
14989
+ 'footer',
14990
+ 'aside',
14991
+ '.mw-editsection',
14992
+ '.reference',
14993
+ '.reflist',
14994
+ '.navbox',
14995
+ '.infobox',
14996
+ '.metadata',
14997
+ '.ambox',
14998
+ '.toc',
14999
+ '#toc'
15000
+ ].join(',');
15001
+
15002
+ var headings = [];
15003
+ var parts = [];
15004
+ var nodes = root.querySelectorAll('h1, h2, h3, p, li');
15005
+ for (var j = 0; j < nodes.length && parts.length < 180; j++) {
15006
+ var node = nodes[j];
15007
+ if (node.closest && node.closest(unwantedSelector)) continue;
15008
+ var tag = String(node.tagName || '').toLowerCase();
15009
+ var text = clean(node.textContent);
15010
+ if (!text) continue;
15011
+ if (/^h[1-3]$/.test(tag)) {
15012
+ if (text.length < 180) headings.push(tag + ': ' + text);
15013
+ parts.push('\\n## ' + text);
15014
+ continue;
15015
+ }
15016
+ if (text.length < 40) continue;
15017
+ parts.push(text);
15018
+ }
15019
+
15020
+ var articleText = clean(parts.join('\\n\\n'));
15021
+ if (articleText.length < 300) {
15022
+ articleText = clean(root.textContent).slice(0, 12000);
15023
+ }
15024
+ if (articleText.length < 300) return null;
15025
+
15026
+ return {
15027
+ title: document.title || '',
15028
+ url: location.href,
15029
+ headings: headings.slice(0, 18),
15030
+ text: articleText.slice(0, ${mode === "summary" ? 9e3 : 14e3}),
15031
+ };
15032
+ })()`,
15033
+ {
15034
+ timeoutMs: 1800,
15035
+ label: "fast article text"
15036
+ }
15037
+ );
15038
+ if (!result || result === PAGE_SCRIPT_TIMEOUT || !result.text.trim()) {
15039
+ return null;
15040
+ }
15041
+ return articleTextResultToOutput(
15042
+ {
15043
+ title: result.title || wc.getTitle() || "(untitled)",
15044
+ url: result.url || wc.getURL(),
15045
+ headings: result.headings,
15046
+ text: result.text
15047
+ },
15048
+ mode,
15049
+ "fast article text",
15050
+ Date.now() - startMs
15051
+ );
15052
+ }
15053
+ async function fetchArticleTextExtract(wc, mode) {
15054
+ const startMs = Date.now();
15055
+ const url = wc.getURL();
15056
+ try {
15057
+ assertSafeURL(url);
15058
+ } catch {
15059
+ return null;
15060
+ }
15061
+ const controller = new AbortController();
15062
+ const timeout = setTimeout(() => controller.abort(), 4500);
15063
+ try {
15064
+ const response = await fetch(url, {
15065
+ signal: controller.signal,
15066
+ headers: {
15067
+ Accept: "text/html,application/xhtml+xml",
15068
+ "User-Agent": "VesselBrowser/0.1 read-page-fallback"
15069
+ }
15070
+ });
15071
+ if (!response.ok) return null;
15072
+ const contentType = response.headers.get("content-type") || "";
15073
+ if (contentType && !/text\/html|application\/xhtml\+xml/i.test(contentType)) {
15074
+ return null;
15075
+ }
15076
+ const contentLength = Number(response.headers.get("content-length") || "0");
15077
+ if (contentLength > 5e6) return null;
15078
+ const html = await response.text();
15079
+ if (html.trim().length < 300) return null;
15080
+ const { document } = linkedom.parseHTML(html);
15081
+ const readable = new readability.Readability(document, {
15082
+ charThreshold: 300
15083
+ }).parse();
15084
+ const title = cleanArticleText(readable?.title || document.title || wc.getTitle()) || wc.getTitle() || "(untitled)";
15085
+ const headings = Array.from(document.querySelectorAll("h1, h2, h3")).map((node) => {
15086
+ const tag = String(node.tagName || "").toLowerCase();
15087
+ const text2 = cleanArticleText(node.textContent || "");
15088
+ return text2.length > 0 && text2.length < 180 ? `${tag}: ${text2}` : "";
15089
+ }).filter(Boolean).slice(0, 18);
15090
+ const fallbackRoot = document.querySelector("article") || document.querySelector("main") || document.querySelector('[role="main"]') || document.body;
15091
+ const text = cleanArticleText(
15092
+ readable?.textContent || fallbackRoot?.textContent || ""
15093
+ );
15094
+ if (text.length < 300) return null;
15095
+ return articleTextResultToOutput(
15096
+ {
15097
+ title,
15098
+ url,
15099
+ headings,
15100
+ text: text.slice(0, mode === "summary" ? 9e3 : 14e3)
15101
+ },
15102
+ mode,
15103
+ "network article fallback",
15104
+ Date.now() - startMs
15105
+ );
15106
+ } catch (err) {
15107
+ if (err instanceof Error && err.name === "AbortError") {
15108
+ logger$k.warn("Network article fallback timed out:", url);
15109
+ } else {
15110
+ logger$k.warn("Network article fallback failed:", err);
15111
+ }
15112
+ return null;
15113
+ } finally {
15114
+ clearTimeout(timeout);
15115
+ }
15116
+ }
14899
15117
  function normalizeReadPageMode(mode, pageContent) {
14900
15118
  if (typeof mode === "string") {
14901
15119
  const normalized = mode.trim().toLowerCase();
@@ -14967,7 +15185,7 @@ async function getPostSearchSummary(wc) {
14967
15185
  await waitForLoad(wc, 2e3);
14968
15186
  try {
14969
15187
  const content = await Promise.race([
14970
- extractContent$1(wc),
15188
+ extractContent(wc),
14971
15189
  new Promise((resolve) => setTimeout(() => resolve(null), 2500))
14972
15190
  ]);
14973
15191
  if (content && content.content.length > 0) {
@@ -14989,7 +15207,7 @@ Search results snapshot unavailable. Use read_page(mode="results_only") if neede
14989
15207
  async function getPostClickNavSummary(wc, toolProfile) {
14990
15208
  try {
14991
15209
  const content = await Promise.race([
14992
- extractContent$1(wc),
15210
+ extractContent(wc),
14993
15211
  new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
14994
15212
  ]);
14995
15213
  if (content && content.content.length > 0) {
@@ -16715,8 +16933,11 @@ async function tryAcceptCookiesQuickly(wc) {
16715
16933
  const dismissed = await executePageScript(
16716
16934
  wc,
16717
16935
  `
16718
- (function() {
16719
- var selectors = [
16936
+ (async function() {
16937
+ var delay = function(ms) {
16938
+ return new Promise(function(resolve) { setTimeout(resolve, ms); });
16939
+ };
16940
+ var selectorTargets = [
16720
16941
  '#onetrust-accept-btn-handler',
16721
16942
  '#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll',
16722
16943
  '[data-cookiefirst-action="accept"]',
@@ -16736,60 +16957,237 @@ async function tryAcceptCookiesQuickly(wc) {
16736
16957
  '.message-component.message-button.no-children.focusable.sp_choice_type_11',
16737
16958
  '[class*="truste"] [class*="accept"]',
16738
16959
  '[id*="consent-accept"]',
16739
- '[class*="cmp-accept"]',
16960
+ '[class*="cmp-accept"]'
16740
16961
  ];
16741
- var textPatterns = [
16742
- 'accept all',
16743
- 'accept cookies',
16744
- 'allow all',
16745
- 'allow cookies',
16746
- 'agree',
16747
- 'got it',
16748
- 'ok',
16749
- 'i agree',
16750
- 'i accept',
16751
- 'consent',
16752
- 'continue',
16753
- 'accept and continue',
16754
- 'accept & continue'
16962
+ var surfaceSelectors = [
16963
+ '#onetrust-consent-sdk',
16964
+ '#CybotCookiebotDialog',
16965
+ '[id*="cookie" i]',
16966
+ '[id*="consent" i]',
16967
+ '[id*="cmp" i]',
16968
+ '[id*="sp_message" i]',
16969
+ '[class*="cookie" i]',
16970
+ '[class*="consent" i]',
16971
+ '[class*="cmp" i]',
16972
+ '[class*="sp_message" i]',
16973
+ '[class*="truste" i]',
16974
+ '[class*="didomi" i]',
16975
+ '[data-testid*="cookie" i]',
16976
+ '[data-testid*="consent" i]',
16977
+ '[aria-label*="cookie" i]',
16978
+ '[aria-label*="consent" i]',
16979
+ '.fc-consent-root'
16755
16980
  ];
16756
- for (var i = 0; i < selectors.length; i++) {
16757
- var el = document.querySelector(selectors[i]);
16758
- if (el && el instanceof HTMLElement) {
16759
- el.click();
16760
- return "Dismissed cookie banner via: " + selectors[i];
16981
+ var actionSelector = [
16982
+ 'button',
16983
+ '[role="button"]',
16984
+ 'a[role="button"]',
16985
+ 'a.message-component',
16986
+ 'input[type="button"]',
16987
+ 'input[type="submit"]'
16988
+ ].join(',');
16989
+ var seen = [];
16990
+
16991
+ function normalize(text) {
16992
+ return String(text || '').replace(/\\s+/g, ' ').trim();
16993
+ }
16994
+
16995
+ function lower(text) {
16996
+ return normalize(text).toLowerCase();
16997
+ }
16998
+
16999
+ function isElementVisible(el) {
17000
+ if (!(el instanceof HTMLElement)) return false;
17001
+ var style = window.getComputedStyle(el);
17002
+ if (style.display === 'none' || style.visibility === 'hidden') return false;
17003
+ if (Number(style.opacity || '1') < 0.05) return false;
17004
+ var rect = el.getBoundingClientRect();
17005
+ return rect.width > 2 &&
17006
+ rect.height > 2 &&
17007
+ rect.bottom > 0 &&
17008
+ rect.right > 0 &&
17009
+ rect.top < window.innerHeight &&
17010
+ rect.left < window.innerWidth;
17011
+ }
17012
+
17013
+ function elementText(el) {
17014
+ if (!(el instanceof HTMLElement)) return '';
17015
+ return normalize([
17016
+ el.getAttribute('aria-label'),
17017
+ el.getAttribute('title'),
17018
+ el.getAttribute('value'),
17019
+ el.textContent
17020
+ ].filter(Boolean).join(' '));
17021
+ }
17022
+
17023
+ function looksLikeCookieSurface(el) {
17024
+ if (!isElementVisible(el)) return false;
17025
+ var text = lower([
17026
+ el.getAttribute('aria-label'),
17027
+ el.getAttribute('id'),
17028
+ el.getAttribute('class'),
17029
+ el.textContent
17030
+ ].filter(Boolean).join(' ')).slice(0, 1600);
17031
+ if (!/(cookie|consent|privacy|tracking|personalise|personalize|advertis|data choices|your choices|cmp|onetrust|truste|didomi)/.test(text)) {
17032
+ return false;
16761
17033
  }
17034
+
17035
+ var rect = el.getBoundingClientRect();
17036
+ var style = window.getComputedStyle(el);
17037
+ var fixedLike = style.position === 'fixed' || style.position === 'sticky';
17038
+ var sizeable = rect.width >= Math.min(window.innerWidth * 0.35, 360) && rect.height >= 48;
17039
+ var bottomBanner = fixedLike && rect.bottom > window.innerHeight * 0.58 && rect.height >= 64;
17040
+ var dialog = el.getAttribute('role') === 'dialog' || el.getAttribute('aria-modal') === 'true';
17041
+ var namedSurface = /(cookie|consent|cmp|onetrust|truste|didomi|sp_message)/.test(lower([
17042
+ el.getAttribute('id'),
17043
+ el.getAttribute('class'),
17044
+ el.getAttribute('data-testid')
17045
+ ].filter(Boolean).join(' ')));
17046
+
17047
+ return sizeable && (namedSurface || bottomBanner || dialog);
16762
17048
  }
16763
- var buttons = document.querySelectorAll('button, a[role="button"], [type="submit"]');
16764
- for (var j = 0; j < buttons.length; j++) {
16765
- var btn = buttons[j];
16766
- var text = (btn.textContent || '').trim().toLowerCase();
16767
- for (var k = 0; k < textPatterns.length; k++) {
16768
- if (text === textPatterns[k] || text.startsWith(textPatterns[k])) {
16769
- btn.click();
16770
- return "Dismissed cookie banner via text match: " + text;
17049
+
17050
+ function cookieSurfaces() {
17051
+ var surfaces = [];
17052
+ function addSurface(el) {
17053
+ if (surfaces.indexOf(el) === -1 && looksLikeCookieSurface(el)) {
17054
+ surfaces.push(el);
17055
+ }
17056
+ }
17057
+
17058
+ for (var i = 0; i < surfaceSelectors.length; i++) {
17059
+ try {
17060
+ document.querySelectorAll(surfaceSelectors[i]).forEach(addSurface);
17061
+ } catch {
17062
+ // Ignore unsupported selectors in older page engines.
17063
+ }
17064
+ }
17065
+
17066
+ document.querySelectorAll('div, section, aside, footer, form, [role="dialog"]').forEach(function(el) {
17067
+ if (!(el instanceof HTMLElement)) return;
17068
+ var style = window.getComputedStyle(el);
17069
+ if (style.position === 'fixed' || style.position === 'sticky' || el.getAttribute('role') === 'dialog') {
17070
+ addSurface(el);
16771
17071
  }
17072
+ });
17073
+
17074
+ return surfaces;
17075
+ }
17076
+
17077
+ function hasCookieSurface() {
17078
+ return cookieSurfaces().length > 0;
17079
+ }
17080
+
17081
+ function labelScore(label) {
17082
+ var text = lower(label);
17083
+ if (!text) return 0;
17084
+ if (/^(accept all|accept cookies|allow all|allow cookies|accept and continue|accept & continue|agree and continue)$/.test(text)) {
17085
+ return 220;
17086
+ }
17087
+ if (/^(i agree|i accept|agree|accept|allow|ok|okay|got it|continue|yes)$/.test(text)) {
17088
+ return 190;
17089
+ }
17090
+ if (/^(reject all|decline|deny|necessary only|essential only|save preferences|confirm choices|submit preferences)$/.test(text)) {
17091
+ return 170;
17092
+ }
17093
+ if (/\\b(accept all|allow all|accept cookies|allow cookies|accept and continue|accept & continue|agree and continue)\\b/.test(text)) {
17094
+ return 160;
17095
+ }
17096
+ if (/\\b(reject all|save preferences|confirm choices|necessary only|essential only)\\b/.test(text)) {
17097
+ return 145;
17098
+ }
17099
+ if (/^(consent|cookie consent|cookies|privacy|privacy policy|cookie policy|learn more|more information|settings|preferences|manage options|customize|customise)$/.test(text)) {
17100
+ return 0;
17101
+ }
17102
+ return 0;
17103
+ }
17104
+
17105
+ function addCandidate(el, source, baseScore) {
17106
+ if (!(el instanceof HTMLElement) || seen.indexOf(el) !== -1 || !isElementVisible(el)) return;
17107
+ var label = elementText(el);
17108
+ var score = labelScore(label);
17109
+ if (score <= 0 && baseScore < 160) return;
17110
+ seen.push(el);
17111
+ candidates.push({
17112
+ el: el,
17113
+ label: label || source,
17114
+ source: source,
17115
+ score: baseScore + score
17116
+ });
17117
+ }
17118
+
17119
+ var beforeHadSurface = hasCookieSurface();
17120
+ var candidates = [];
17121
+
17122
+ for (var i = 0; i < selectorTargets.length; i++) {
17123
+ try {
17124
+ document.querySelectorAll(selectorTargets[i]).forEach(function(el) {
17125
+ addCandidate(el, selectorTargets[i], 160);
17126
+ });
17127
+ } catch {
17128
+ // Ignore unsupported selectors in older page engines.
17129
+ }
17130
+ }
17131
+
17132
+ var surfaces = cookieSurfaces();
17133
+ surfaces.forEach(function(surface) {
17134
+ surface.querySelectorAll(actionSelector).forEach(function(el) {
17135
+ addCandidate(el, 'cookie surface', 120);
17136
+ });
17137
+ });
17138
+
17139
+ if (beforeHadSurface) {
17140
+ document.querySelectorAll(actionSelector).forEach(function(el) {
17141
+ addCandidate(el, 'page action', 40);
17142
+ });
17143
+ }
17144
+
17145
+ candidates.sort(function(a, b) { return b.score - a.score; });
17146
+
17147
+ var tried = 0;
17148
+ for (var j = 0; j < Math.min(candidates.length, 8); j++) {
17149
+ var candidate = candidates[j];
17150
+ tried += 1;
17151
+ candidate.el.click();
17152
+ await delay(220);
17153
+ if (!hasCookieSurface()) {
17154
+ return {
17155
+ status: 'dismissed',
17156
+ message: 'Dismissed cookie banner via: ' + candidate.label.slice(0, 80)
17157
+ };
16772
17158
  }
16773
17159
  }
17160
+
17161
+ if (beforeHadSurface || tried > 0) {
17162
+ return {
17163
+ status: 'still_visible',
17164
+ message: tried > 0
17165
+ ? 'Cookie consent banner is still visible after trying ' + tried + ' candidate control(s). Try clear_overlays or dismiss_popup.'
17166
+ : 'Cookie consent banner appears visible, but no reliable accept/reject button was found. Try clear_overlays or dismiss_popup.'
17167
+ };
17168
+ }
17169
+
16774
17170
  return null;
16775
17171
  })()
16776
17172
  `,
16777
17173
  {
16778
17174
  label: "accept cookies",
16779
- timeoutMs: 1200
17175
+ timeoutMs: 2200,
17176
+ userGesture: true
16780
17177
  }
16781
17178
  );
16782
17179
  if (dismissed) return dismissed;
16783
- return tryDismissConsentIframe(wc);
17180
+ const iframeDismissed = await tryDismissConsentIframe(wc);
17181
+ return iframeDismissed ? { status: "dismissed", message: iframeDismissed } : null;
16784
17182
  }
16785
17183
  async function clearOverlays(wc, strategy = "auto") {
16786
17184
  const quickCookieResult = await tryAcceptCookiesQuickly(wc);
16787
17185
  if (quickCookieResult === PAGE_SCRIPT_TIMEOUT) {
16788
17186
  return pageBusyError("clear_overlays");
16789
17187
  }
16790
- if (quickCookieResult) {
17188
+ if (quickCookieResult?.status === "dismissed") {
16791
17189
  return [
16792
- quickCookieResult,
17190
+ quickCookieResult.message,
16793
17191
  "Stopped after a lightweight consent pass to keep the page responsive. Re-run only if the banner is still blocking the page."
16794
17192
  ].join("\n");
16795
17193
  }
@@ -17738,6 +18136,94 @@ async function locateSearchTarget(wc, explicitSelector) {
17738
18136
  }
17739
18137
  );
17740
18138
  }
18139
+ async function locateImplicitTextTarget(wc) {
18140
+ return executePageScript(
18141
+ wc,
18142
+ `
18143
+ (function() {
18144
+ function normalize(value) {
18145
+ return value == null ? "" : String(value).trim().toLowerCase();
18146
+ }
18147
+
18148
+ function isVisible(el) {
18149
+ if (!(el instanceof HTMLElement)) return true;
18150
+ const style = window.getComputedStyle(el);
18151
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
18152
+ if (el.hasAttribute("hidden") || el.getAttribute("aria-hidden") === "true") return false;
18153
+ const rect = el.getBoundingClientRect();
18154
+ return rect.width > 0 && rect.height > 0;
18155
+ }
18156
+
18157
+ function inViewport(el) {
18158
+ if (!(el instanceof HTMLElement)) return true;
18159
+ const rect = el.getBoundingClientRect();
18160
+ const vw = window.innerWidth || document.documentElement?.clientWidth || 0;
18161
+ const vh = window.innerHeight || document.documentElement?.clientHeight || 0;
18162
+ return rect.bottom > 0 && rect.right > 0 && rect.top < vh && rect.left < vw;
18163
+ }
18164
+
18165
+ function isFillable(el) {
18166
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return false;
18167
+ if (el.disabled || el.readOnly || el.getAttribute("aria-disabled") === "true") return false;
18168
+ const type = el instanceof HTMLTextAreaElement ? "text" : normalize(el.getAttribute("type") || el.type || "text");
18169
+ return ["", "search", "text", "email", "url", "tel", "number", "password"].includes(type);
18170
+ }
18171
+
18172
+ function nearestSearchScope(input) {
18173
+ return input.closest('[role="search"], form, header, nav, [class*="search" i], [id*="search" i]');
18174
+ }
18175
+
18176
+ ${selectorHelpersJS(["data-testid", "name", "form", "aria-label", "placeholder"])}
18177
+
18178
+ const active = document.activeElement;
18179
+ if (active && isFillable(active) && isVisible(active) && inViewport(active)) {
18180
+ return selectorFor(active);
18181
+ }
18182
+
18183
+ const candidates = Array.from(
18184
+ document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="image"]), textarea')
18185
+ ).filter((el) => isFillable(el) && isVisible(el));
18186
+
18187
+ let best = null;
18188
+ let bestScore = -1;
18189
+ for (const el of candidates) {
18190
+ let score = 0;
18191
+ if (inViewport(el)) score += 100;
18192
+ const rect = el.getBoundingClientRect();
18193
+ score += Math.max(0, 36 - Math.min(36, Math.floor(Math.max(0, rect.top) / 22)));
18194
+
18195
+ const type = el instanceof HTMLTextAreaElement ? "text" : normalize(el.getAttribute("type") || el.type);
18196
+ const name = normalize(el.getAttribute("name"));
18197
+ const placeholder = normalize(el.getAttribute("placeholder"));
18198
+ const aria = normalize(el.getAttribute("aria-label"));
18199
+ const role = normalize(el.getAttribute("role"));
18200
+ const id = normalize(el.getAttribute("id"));
18201
+
18202
+ if (type === "search") score += 80;
18203
+ if (role === "searchbox") score += 70;
18204
+ if (name === "q" || name === "query" || name === "search") score += 65;
18205
+ if (placeholder.includes("search")) score += 55;
18206
+ if (aria.includes("search")) score += 55;
18207
+ if (id.includes("search")) score += 35;
18208
+
18209
+ const scope = nearestSearchScope(el);
18210
+ if (scope) score += 35;
18211
+
18212
+ if (score > bestScore) {
18213
+ best = el;
18214
+ bestScore = score;
18215
+ }
18216
+ }
18217
+
18218
+ return best ? selectorFor(best) : null;
18219
+ })()
18220
+ `,
18221
+ {
18222
+ timeoutMs: 2200,
18223
+ label: "find implicit text input"
18224
+ }
18225
+ );
18226
+ }
17741
18227
  async function searchPage(wc, args) {
17742
18228
  const query = String(args.query || "");
17743
18229
  if (!query) return "Error: No search query provided.";
@@ -18227,8 +18713,19 @@ async function executeAction(name, args, ctx) {
18227
18713
  }
18228
18714
  case "type_text": {
18229
18715
  if (!wc) return "Error: No active tab";
18230
- const selector = await resolveSelector(wc, args.index, args.selector);
18231
- if (!selector) return "Error: No element index or selector provided";
18716
+ let selector = await resolveSelector(wc, args.index, args.selector);
18717
+ if (selector === PAGE_SCRIPT_TIMEOUT) {
18718
+ return pageBusyError("type_text");
18719
+ }
18720
+ if (!selector) {
18721
+ selector = await locateImplicitTextTarget(wc);
18722
+ }
18723
+ if (selector === PAGE_SCRIPT_TIMEOUT) {
18724
+ return pageBusyError("type_text");
18725
+ }
18726
+ if (!selector) {
18727
+ return "Error: No element index or selector provided, and no focused or visible text input could be found.";
18728
+ }
18232
18729
  const mode = typeof args.mode === "string" ? args.mode : "default";
18233
18730
  if (mode === "keystroke") {
18234
18731
  return typeKeystroke(wc, selector, String(args.text || ""));
@@ -18324,6 +18821,23 @@ async function executeAction(name, args, ctx) {
18324
18821
  if (requestedGlance) {
18325
18822
  return glanceExtract(wc);
18326
18823
  }
18824
+ const requestedTextMode = typeof args.mode === "string" ? args.mode.trim().toLowerCase() : "";
18825
+ if (requestedTextMode === "summary" || requestedTextMode === "text_only") {
18826
+ const fastArticleText = await fastArticleTextExtract(
18827
+ wc,
18828
+ requestedTextMode
18829
+ );
18830
+ if (fastArticleText) {
18831
+ return fastArticleText;
18832
+ }
18833
+ const fetchedArticleText = await fetchArticleTextExtract(
18834
+ wc,
18835
+ requestedTextMode
18836
+ );
18837
+ if (fetchedArticleText) {
18838
+ return fetchedArticleText;
18839
+ }
18840
+ }
18327
18841
  let content = null;
18328
18842
  try {
18329
18843
  content = await Promise.race([
@@ -19020,7 +19534,7 @@ ${steps.join("\n")}`;
19020
19534
  if (dismissed === PAGE_SCRIPT_TIMEOUT) {
19021
19535
  return pageBusyError("accept_cookies");
19022
19536
  }
19023
- if (dismissed) return dismissed;
19537
+ if (dismissed) return dismissed.message;
19024
19538
  return "No cookie consent banner detected. Try dismiss_popup for other overlays.";
19025
19539
  }
19026
19540
  case "extract_table": {
@@ -20272,13 +20786,31 @@ async function showDownloadInFolder(id) {
20272
20786
  }
20273
20787
  const defaultDownloadViews = /* @__PURE__ */ new Set();
20274
20788
  let defaultDownloadHandlerInstalled = false;
20789
+ function sanitizeDownloadFilename(filename) {
20790
+ const normalized = filename.replace(/\\/g, "/");
20791
+ const basename = path.posix.basename(normalized).trim();
20792
+ const safeName = basename.replace(/[\0-\x1f\x7f]/g, "_");
20793
+ if (!safeName || safeName === "." || safeName === "..") {
20794
+ return "download";
20795
+ }
20796
+ return safeName;
20797
+ }
20798
+ function isPathInside(parentDir, candidatePath) {
20799
+ const relative = path.relative(parentDir, candidatePath);
20800
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
20801
+ }
20275
20802
  function resolveDownloadPath(downloadDir, filename) {
20276
20803
  fs$1.mkdirSync(downloadDir, { recursive: true });
20277
- const parsed = path.parse(filename);
20804
+ const rootDir = path.resolve(downloadDir);
20805
+ const safeFilename = sanitizeDownloadFilename(filename);
20806
+ const parsed = path.parse(safeFilename);
20278
20807
  let attempt = 0;
20279
20808
  while (true) {
20280
- const candidateName = attempt === 0 ? filename : `${parsed.name} (${attempt})${parsed.ext}`;
20281
- const candidatePath = path.join(downloadDir, candidateName);
20809
+ const candidateName = attempt === 0 ? safeFilename : `${parsed.name} (${attempt})${parsed.ext}`;
20810
+ const candidatePath = path.resolve(rootDir, candidateName);
20811
+ if (!isPathInside(rootDir, candidatePath)) {
20812
+ throw new Error("Blocked unsafe download filename");
20813
+ }
20282
20814
  if (!fs$1.existsSync(candidatePath)) {
20283
20815
  return candidatePath;
20284
20816
  }
@@ -21455,7 +21987,7 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
21455
21987
  const isSummarize = lowerQuery.startsWith("summarize") || lowerQuery.startsWith("tldr") || lowerQuery === "summary";
21456
21988
  if (provider.streamAgentQuery && tabManager && activeWebContents && runtime2) {
21457
21989
  try {
21458
- const pageContent = await extractContent$1(activeWebContents);
21990
+ const pageContent = await extractContent(activeWebContents);
21459
21991
  const pageType = detectPageType(pageContent);
21460
21992
  const defaultReadMode = chooseAgentReadMode(pageContent);
21461
21993
  if (provider.agentToolProfile === "compact") {
@@ -21556,7 +22088,7 @@ ${trackerCtx}`;
21556
22088
  let prompt;
21557
22089
  if (activeWebContents) {
21558
22090
  try {
21559
- const pageContent = await extractContent$1(activeWebContents);
22091
+ const pageContent = await extractContent(activeWebContents);
21560
22092
  if (isSummarize) {
21561
22093
  prompt = buildSummarizePrompt(pageContent);
21562
22094
  } else {
@@ -21837,7 +22369,7 @@ function registerContentHandlers(windowState2) {
21837
22369
  assertTrustedIpcSender(event);
21838
22370
  const activeTab = tabManager.getActiveTab();
21839
22371
  if (!activeTab) return null;
21840
- return extractContent$1(activeTab.view.webContents);
22372
+ return extractContent(activeTab.view.webContents);
21841
22373
  });
21842
22374
  electron.ipcMain.handle(Channels.READER_MODE_TOGGLE, async (event) => {
21843
22375
  assertTrustedIpcSender(event);
@@ -21851,7 +22383,7 @@ function registerContentHandlers(windowState2) {
21851
22383
  }
21852
22384
  } else {
21853
22385
  const originalUrl = activeTab.state.url;
21854
- const content = await extractContent$1(activeTab.view.webContents);
22386
+ const content = await extractContent(activeTab.view.webContents);
21855
22387
  const html = generateReaderHTML(content);
21856
22388
  activeTab.setReaderMode(true, originalUrl);
21857
22389
  void loadInternalDataURL(
@@ -22147,532 +22679,182 @@ function registerAgentRuntimeHandlers(runtime2, chromeView, sidebarView, sendToR
22147
22679
  }
22148
22680
  );
22149
22681
  }
22150
- const DEFAULT_PAGE_FOLDER = "Vessel/Pages";
22151
- const DEFAULT_NOTE_FOLDER = "Vessel/Research";
22152
- const DEFAULT_BOOKMARK_FOLDER = "Vessel/Bookmarks";
22153
- const PAGE_CONTENT_LIMIT = 6e3;
22154
- const DEFAULT_LIST_LIMIT = 50;
22155
- const DEFAULT_SEARCH_LIMIT = 20;
22156
- function getVaultRoot() {
22157
- const configured = loadSettings().obsidianVaultPath.trim();
22158
- if (!configured) {
22159
- throw new Error(
22160
- "Obsidian not configured. Set vault path in Vessel settings to use memory capture."
22161
- );
22162
- }
22163
- return path$1.resolve(configured);
22682
+ function asTextResponse$1(text) {
22683
+ return { content: [{ type: "text", text }] };
22164
22684
  }
22165
- function assertInsideVault(targetPath, vaultRoot) {
22166
- const resolved = path$1.resolve(targetPath);
22167
- const relative = path$1.relative(vaultRoot, resolved);
22168
- if (relative.startsWith("..") || path$1.isAbsolute(relative)) {
22169
- throw new Error("Resolved note path is outside the configured vault.");
22170
- }
22171
- return resolved;
22685
+ const DANGEROUS_DEVTOOLS_ACTIONS = /* @__PURE__ */ new Set([
22686
+ "devtools_execute_js",
22687
+ "devtools_modify_dom",
22688
+ "devtools_set_storage"
22689
+ ]);
22690
+ let stateListener = null;
22691
+ const activityLog = [];
22692
+ const MAX_ACTIVITY_ENTRIES = 100;
22693
+ let activityCounter = 0;
22694
+ function setDevToolsPanelListener(listener) {
22695
+ stateListener = listener;
22172
22696
  }
22173
- function normalizeFolder(folder, fallback) {
22174
- const raw = (folder?.trim() || fallback).replace(/\\/g, "/");
22175
- if (!raw) return fallback;
22176
- if (path$1.isAbsolute(raw)) {
22177
- throw new Error("Vault note folders must be relative to the vault root.");
22178
- }
22179
- const segments = raw.split("/").filter(Boolean);
22180
- if (segments.some((segment) => segment === "." || segment === "..")) {
22181
- throw new Error("Vault note folders cannot traverse outside the vault.");
22182
- }
22183
- return segments.join(path$1.sep);
22697
+ function getDevToolsPanelState(tabId) {
22698
+ const session = tabId ? getSession(tabId) : void 0;
22699
+ return {
22700
+ console: session?.getConsoleLogs() ?? [],
22701
+ network: session?.getNetworkLog() ?? [],
22702
+ errors: session?.getErrors() ?? [],
22703
+ activity: activityLog
22704
+ };
22184
22705
  }
22185
- function normalizeNotePath(notePath) {
22186
- const raw = notePath.trim().replace(/\\/g, "/");
22187
- if (!raw) {
22188
- throw new Error("A note path is required.");
22706
+ function broadcastState(tabManager) {
22707
+ if (!stateListener) return;
22708
+ const tabId = tabManager.getActiveTabId();
22709
+ stateListener(getDevToolsPanelState(tabId));
22710
+ }
22711
+ async function withDevToolsAction(runtime2, tabManager, name, args, executor) {
22712
+ try {
22713
+ assertFeatureUnlocked("devtools", "DevTools");
22714
+ } catch (error) {
22715
+ return asTextResponse$1(
22716
+ `Error: ${error instanceof Error ? error.message : "DevTools require Vessel Premium."}`
22717
+ );
22189
22718
  }
22190
- if (path$1.isAbsolute(raw)) {
22191
- throw new Error("Note paths must be relative to the vault root.");
22719
+ const activityEntry = {
22720
+ id: ++activityCounter,
22721
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
22722
+ tool: name,
22723
+ args: JSON.stringify(args).slice(0, 200),
22724
+ result: "",
22725
+ durationMs: 0,
22726
+ status: "running"
22727
+ };
22728
+ activityLog.push(activityEntry);
22729
+ if (activityLog.length > MAX_ACTIVITY_ENTRIES) {
22730
+ activityLog.splice(0, activityLog.length - MAX_ACTIVITY_ENTRIES);
22192
22731
  }
22193
- const segments = raw.split("/").filter(Boolean);
22194
- if (segments.some((segment) => segment === "." || segment === "..")) {
22195
- throw new Error("Note paths cannot traverse outside the vault.");
22732
+ broadcastState(tabManager);
22733
+ const startTime = Date.now();
22734
+ try {
22735
+ const result = await runtime2.runControlledAction({
22736
+ source: "mcp",
22737
+ name,
22738
+ args,
22739
+ tabId: tabManager.getActiveTabId(),
22740
+ dangerous: DANGEROUS_DEVTOOLS_ACTIONS.has(name),
22741
+ executor
22742
+ });
22743
+ activityEntry.status = "completed";
22744
+ activityEntry.result = result.slice(0, 200);
22745
+ activityEntry.durationMs = Date.now() - startTime;
22746
+ broadcastState(tabManager);
22747
+ return asTextResponse$1(result);
22748
+ } catch (error) {
22749
+ const message = error instanceof Error ? error.message : "Unknown error";
22750
+ activityEntry.status = "failed";
22751
+ activityEntry.result = message.slice(0, 200);
22752
+ activityEntry.durationMs = Date.now() - startTime;
22753
+ broadcastState(tabManager);
22754
+ return asTextResponse$1(`Error: ${message}`);
22196
22755
  }
22197
- const normalized = segments.join(path$1.sep);
22198
- return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
22199
- }
22200
- function escapeYaml(value) {
22201
- return JSON.stringify(value);
22202
22756
  }
22203
- function renderFrontmatter(data) {
22204
- const lines = ["---"];
22205
- for (const [key2, value] of Object.entries(data)) {
22206
- if (value == null) continue;
22207
- if (Array.isArray(value)) {
22208
- if (value.length === 0) continue;
22209
- lines.push(`${key2}:`);
22210
- for (const item of value) {
22211
- lines.push(` - ${escapeYaml(item)}`);
22757
+ function registerDevTools(server, tabManager, runtime2) {
22758
+ server.registerTool(
22759
+ "vessel_devtools_console_logs",
22760
+ {
22761
+ title: "DevTools: Get Console Logs",
22762
+ description: "Get console log entries captured from the active tab. Returns a rolling buffer of recent console.log, console.warn, console.error, etc. calls. Automatically starts capturing on first use.",
22763
+ inputSchema: {
22764
+ level: zod.z.enum(["log", "warning", "error", "info", "debug", "verbose"]).optional().describe("Filter by log level"),
22765
+ limit: zod.z.number().optional().describe("Maximum number of entries to return (default: all)"),
22766
+ search: zod.z.string().optional().describe("Filter entries containing this text (case-insensitive)")
22212
22767
  }
22213
- continue;
22768
+ },
22769
+ async ({ level, limit, search: search2 }) => {
22770
+ return withDevToolsAction(
22771
+ runtime2,
22772
+ tabManager,
22773
+ "devtools_console_logs",
22774
+ { level, limit, search: search2 },
22775
+ async () => {
22776
+ const session = getOrCreateSession(tabManager);
22777
+ await session.ensureConsoleDomain();
22778
+ const entries = session.getConsoleLogs({ level, limit, search: search2 });
22779
+ if (entries.length === 0) {
22780
+ return "No console entries captured yet. Console monitoring is now active — new entries will be captured as they occur.";
22781
+ }
22782
+ return JSON.stringify(entries, null, 2);
22783
+ }
22784
+ );
22214
22785
  }
22215
- lines.push(`${key2}: ${escapeYaml(value)}`);
22216
- }
22217
- lines.push("---", "");
22218
- return lines.join("\n");
22219
- }
22220
- function slugify(value) {
22221
- const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
22222
- return normalized || "note";
22223
- }
22224
- function buildUniqueNotePath(dir, title) {
22225
- const datePrefix = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
22226
- const slug = slugify(title);
22227
- const base = `${datePrefix}-${slug}`;
22228
- let candidate = `${base}.md`;
22229
- let counter = 2;
22230
- while (fs$1.existsSync(path$1.join(dir, candidate))) {
22231
- candidate = `${base}-${counter}.md`;
22232
- counter += 1;
22233
- }
22234
- return path$1.join(dir, candidate);
22235
- }
22236
- function trimContent(content, limit = PAGE_CONTENT_LIMIT) {
22237
- const cleaned = content.trim();
22238
- if (cleaned.length <= limit) return cleaned;
22239
- return `${cleaned.slice(0, limit)}
22240
-
22241
- [Truncated]`;
22242
- }
22243
- function parseFrontmatter(content) {
22244
- if (!content.startsWith("---\n")) {
22245
- return { body: content, tags: [] };
22246
- }
22247
- const closingIndex = content.indexOf("\n---\n", 4);
22248
- if (closingIndex === -1) {
22249
- return { body: content, tags: [] };
22250
- }
22251
- const raw = content.slice(4, closingIndex);
22252
- const body = content.slice(closingIndex + 5);
22253
- const lines = raw.split("\n");
22254
- const result = { tags: [] };
22255
- let activeArrayKey = "";
22256
- for (const line of lines) {
22257
- const trimmed = line.trim();
22258
- if (!trimmed) continue;
22259
- if (trimmed.startsWith("- ") && activeArrayKey === "tags") {
22260
- result.tags.push(
22261
- trimmed.slice(2).trim().replace(/^["']|["']$/g, "")
22786
+ );
22787
+ server.registerTool(
22788
+ "vessel_devtools_console_clear",
22789
+ {
22790
+ title: "DevTools: Clear Console Logs",
22791
+ description: "Clear the captured console log buffer for the active tab."
22792
+ },
22793
+ async () => {
22794
+ return withDevToolsAction(
22795
+ runtime2,
22796
+ tabManager,
22797
+ "devtools_console_clear",
22798
+ {},
22799
+ async () => {
22800
+ const session = getOrCreateSession(tabManager);
22801
+ const count = session.clearConsoleLogs();
22802
+ return `Cleared ${count} console entries.`;
22803
+ }
22262
22804
  );
22263
- continue;
22264
22805
  }
22265
- activeArrayKey = "";
22266
- const separatorIndex = trimmed.indexOf(":");
22267
- if (separatorIndex === -1) continue;
22268
- const key2 = trimmed.slice(0, separatorIndex).trim();
22269
- const value = trimmed.slice(separatorIndex + 1).trim();
22270
- if (key2 === "title" && value) {
22271
- result.title = value.replace(/^["']|["']$/g, "");
22272
- } else if (key2 === "tags") {
22273
- activeArrayKey = "tags";
22274
- if (value.startsWith("[") && value.endsWith("]")) {
22275
- const inline = value.slice(1, -1).split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
22276
- result.tags.push(...inline);
22277
- activeArrayKey = "";
22806
+ );
22807
+ server.registerTool(
22808
+ "vessel_devtools_network_log",
22809
+ {
22810
+ title: "DevTools: Get Network Log",
22811
+ description: "Get captured network requests/responses from the active tab. Returns method, URL, status, timing, headers, and size. Automatically starts capturing on first use.",
22812
+ inputSchema: {
22813
+ url_pattern: zod.z.string().optional().describe("Filter by URL pattern (regex or substring match)"),
22814
+ method: zod.z.string().optional().describe("Filter by HTTP method (GET, POST, etc.)"),
22815
+ status_min: zod.z.number().optional().describe("Minimum HTTP status code (e.g., 400 for errors)"),
22816
+ status_max: zod.z.number().optional().describe("Maximum HTTP status code"),
22817
+ limit: zod.z.number().optional().describe("Maximum number of entries to return (default: all)")
22278
22818
  }
22819
+ },
22820
+ async ({ url_pattern, method, status_min, status_max, limit }) => {
22821
+ return withDevToolsAction(
22822
+ runtime2,
22823
+ tabManager,
22824
+ "devtools_network_log",
22825
+ { url_pattern, method, status_min, status_max, limit },
22826
+ async () => {
22827
+ const session = getOrCreateSession(tabManager);
22828
+ await session.ensureNetworkDomain();
22829
+ const entries = session.getNetworkLog({
22830
+ urlPattern: url_pattern,
22831
+ method,
22832
+ statusRange: status_min != null || status_max != null ? { min: status_min, max: status_max } : void 0,
22833
+ limit
22834
+ });
22835
+ if (entries.length === 0) {
22836
+ return "No network requests captured yet. Network monitoring is now active — new requests will be captured as they occur.";
22837
+ }
22838
+ return JSON.stringify(entries, null, 2);
22839
+ }
22840
+ );
22279
22841
  }
22280
- }
22281
- return { body, title: result.title, tags: result.tags };
22282
- }
22283
- function collectMarkdownFiles(dir) {
22284
- const entries = fs$1.readdirSync(dir, { withFileTypes: true });
22285
- const files = [];
22286
- for (const entry of entries) {
22287
- const absolutePath = path$1.join(dir, entry.name);
22288
- if (entry.isDirectory()) {
22289
- files.push(...collectMarkdownFiles(absolutePath));
22290
- continue;
22291
- }
22292
- if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
22293
- files.push(absolutePath);
22294
- }
22295
- }
22296
- return files;
22297
- }
22298
- function toSummary(absolutePath, vaultRoot) {
22299
- const stats = fs$1.statSync(absolutePath);
22300
- const relativePath = path$1.relative(vaultRoot, absolutePath).split(path$1.sep).join("/");
22301
- const raw = fs$1.readFileSync(absolutePath, "utf-8");
22302
- const parsed = parseFrontmatter(raw);
22303
- const headingMatch = parsed.body.match(/^#\s+(.+)$/m);
22304
- const title = parsed.title || headingMatch?.[1]?.trim() || path$1.basename(absolutePath, ".md");
22305
- return {
22306
- title,
22307
- absolutePath,
22308
- relativePath,
22309
- modifiedAt: stats.mtime.toISOString(),
22310
- tags: parsed.tags
22311
- };
22312
- }
22313
- function renderBookmarkLinkBlock(bookmark, note) {
22314
- const lines = [
22315
- "## Linked Bookmark",
22316
- "",
22317
- `- Bookmark ID: \`${bookmark.id}\``,
22318
- `- Title: ${bookmark.title || bookmark.url}`,
22319
- `- URL: [${bookmark.url}](${bookmark.url})`,
22320
- `- Saved At: ${bookmark.savedAt}`
22321
- ];
22322
- if (note?.trim()) {
22323
- lines.push("", "### Context", "", note.trim());
22324
- }
22325
- return `${lines.join("\n")}
22326
- `;
22327
- }
22328
- function writeMemoryNote({
22329
- title,
22330
- body,
22331
- folder,
22332
- tags = [],
22333
- frontmatter = {}
22334
- }) {
22335
- const vaultRoot = getVaultRoot();
22336
- const relativeFolder = normalizeFolder(folder, DEFAULT_NOTE_FOLDER);
22337
- const targetDir = path$1.join(vaultRoot, relativeFolder);
22338
- fs$1.mkdirSync(targetDir, { recursive: true });
22339
- const absolutePath = buildUniqueNotePath(targetDir, title);
22340
- const relativePath = path$1.relative(vaultRoot, absolutePath);
22341
- const content = [
22342
- renderFrontmatter({
22343
- title,
22344
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
22345
- tags,
22346
- ...frontmatter
22347
- }),
22348
- body.trim(),
22349
- ""
22350
- ].join("\n");
22351
- fs$1.writeFileSync(absolutePath, content, "utf-8");
22352
- return {
22353
- title,
22354
- absolutePath,
22355
- relativePath: relativePath.split(path$1.sep).join("/")
22356
- };
22357
- }
22358
- function appendToMemoryNote({
22359
- notePath,
22360
- content,
22361
- heading
22362
- }) {
22363
- const vaultRoot = getVaultRoot();
22364
- const relativePath = normalizeNotePath(notePath);
22365
- const absolutePath = assertInsideVault(
22366
- path$1.join(vaultRoot, relativePath),
22367
- vaultRoot
22368
22842
  );
22369
- if (!fs$1.existsSync(absolutePath)) {
22370
- throw new Error(
22371
- `Memory note not found: ${relativePath.split(path$1.sep).join("/")}`
22372
- );
22373
- }
22374
- const current = fs$1.readFileSync(absolutePath, "utf-8").trimEnd();
22375
- const nextParts = [current, ""];
22376
- if (heading?.trim()) {
22377
- nextParts.push(`## ${heading.trim()}`, "");
22378
- }
22379
- nextParts.push(content.trim(), "");
22380
- fs$1.writeFileSync(absolutePath, nextParts.join("\n"), "utf-8");
22381
- return {
22382
- title: path$1.basename(absolutePath, ".md"),
22383
- absolutePath,
22384
- relativePath: relativePath.split(path$1.sep).join("/")
22385
- };
22386
- }
22387
- function listMemoryNotes({
22388
- folder,
22389
- limit = DEFAULT_LIST_LIMIT
22390
- } = {}) {
22391
- const vaultRoot = getVaultRoot();
22392
- const relativeFolder = normalizeFolder(folder, "");
22393
- const targetDir = relativeFolder ? path$1.join(vaultRoot, relativeFolder) : vaultRoot;
22394
- if (!fs$1.existsSync(targetDir)) {
22395
- return [];
22396
- }
22397
- return collectMarkdownFiles(targetDir).map((absolutePath) => toSummary(absolutePath, vaultRoot)).sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt)).slice(0, Math.max(1, limit));
22398
- }
22399
- function searchMemoryNotes({
22400
- query,
22401
- folder,
22402
- tags = [],
22403
- limit = DEFAULT_SEARCH_LIMIT
22404
- }) {
22405
- const loweredQuery = query.trim().toLowerCase();
22406
- if (!loweredQuery) {
22407
- throw new Error("A non-empty memory search query is required.");
22408
- }
22409
- const vaultRoot = getVaultRoot();
22410
- const relativeFolder = normalizeFolder(folder, "");
22411
- const targetDir = relativeFolder ? path$1.join(vaultRoot, relativeFolder) : vaultRoot;
22412
- if (!fs$1.existsSync(targetDir)) {
22413
- return [];
22414
- }
22415
- const loweredTags = tags.map((tag) => tag.trim().toLowerCase()).filter(Boolean);
22416
- return collectMarkdownFiles(targetDir).map((absolutePath) => {
22417
- const raw = fs$1.readFileSync(absolutePath, "utf-8");
22418
- const parsed = parseFrontmatter(raw);
22419
- const summary = toSummary(absolutePath, vaultRoot);
22420
- const haystack = `${summary.title}
22421
- ${summary.relativePath}
22422
- ${parsed.body}`.toLowerCase();
22423
- const hasQuery = haystack.includes(loweredQuery);
22424
- const hasTags = loweredTags.length === 0 || loweredTags.every(
22425
- (tag) => summary.tags.some((noteTag) => noteTag.toLowerCase() === tag)
22426
- );
22427
- return hasQuery && hasTags ? summary : null;
22428
- }).filter((item) => item !== null).sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt)).slice(0, Math.max(1, limit));
22429
- }
22430
- function capturePageToVault({
22431
- page,
22432
- title,
22433
- folder,
22434
- summary,
22435
- note,
22436
- tags = []
22437
- }) {
22438
- const noteTitle = title?.trim() || page.title.trim() || page.url;
22439
- const bodyLines = [
22440
- `# ${noteTitle}`,
22441
- "",
22442
- `Source: [${page.title || page.url}](${page.url})`,
22443
- `Captured: ${(/* @__PURE__ */ new Date()).toISOString()}`
22444
- ];
22445
- if (page.byline) {
22446
- bodyLines.push(`Byline: ${page.byline}`);
22447
- }
22448
- bodyLines.push("");
22449
- if (summary?.trim()) {
22450
- bodyLines.push("## Summary", "", summary.trim(), "");
22451
- }
22452
- if (note?.trim()) {
22453
- bodyLines.push("## Research Note", "", note.trim(), "");
22454
- }
22455
- if (page.excerpt.trim()) {
22456
- bodyLines.push("## Excerpt", "", page.excerpt.trim(), "");
22457
- }
22458
- const snapshot2 = trimContent(page.content);
22459
- if (snapshot2) {
22460
- bodyLines.push("## Page Snapshot", "", snapshot2, "");
22461
- }
22462
- return writeMemoryNote({
22463
- title: noteTitle,
22464
- body: bodyLines.join("\n"),
22465
- folder: folder || DEFAULT_PAGE_FOLDER,
22466
- tags,
22467
- frontmatter: {
22468
- source_url: page.url,
22469
- source_title: page.title || page.url
22470
- }
22471
- });
22472
- }
22473
- function linkBookmarkToMemory({
22474
- bookmark,
22475
- notePath,
22476
- title,
22477
- folder,
22478
- note,
22479
- tags = []
22480
- }) {
22481
- if (notePath?.trim()) {
22482
- return appendToMemoryNote({
22483
- notePath,
22484
- heading: "Linked Bookmark",
22485
- content: [
22486
- `- Bookmark ID: \`${bookmark.id}\``,
22487
- `- Title: ${bookmark.title || bookmark.url}`,
22488
- `- URL: [${bookmark.url}](${bookmark.url})`,
22489
- `- Saved At: ${bookmark.savedAt}`,
22490
- note?.trim() ? `- Note: ${note.trim()}` : ""
22491
- ].filter(Boolean).join("\n")
22492
- });
22493
- }
22494
- const noteTitle = title?.trim() || bookmark.title || bookmark.url;
22495
- return writeMemoryNote({
22496
- title: noteTitle,
22497
- body: renderBookmarkLinkBlock(bookmark, note),
22498
- folder: folder || DEFAULT_BOOKMARK_FOLDER,
22499
- tags,
22500
- frontmatter: {
22501
- bookmark_id: bookmark.id,
22502
- source_url: bookmark.url,
22503
- source_title: bookmark.title || bookmark.url
22504
- }
22505
- });
22506
- }
22507
- function asTextResponse$1(text) {
22508
- return { content: [{ type: "text", text }] };
22509
- }
22510
- const DANGEROUS_DEVTOOLS_ACTIONS = /* @__PURE__ */ new Set([
22511
- "devtools_execute_js",
22512
- "devtools_modify_dom",
22513
- "devtools_set_storage"
22514
- ]);
22515
- let stateListener = null;
22516
- const activityLog = [];
22517
- const MAX_ACTIVITY_ENTRIES = 100;
22518
- let activityCounter = 0;
22519
- function setDevToolsPanelListener(listener) {
22520
- stateListener = listener;
22521
- }
22522
- function getDevToolsPanelState(tabId) {
22523
- const session = tabId ? getSession(tabId) : void 0;
22524
- return {
22525
- console: session?.getConsoleLogs() ?? [],
22526
- network: session?.getNetworkLog() ?? [],
22527
- errors: session?.getErrors() ?? [],
22528
- activity: activityLog
22529
- };
22530
- }
22531
- function broadcastState(tabManager) {
22532
- if (!stateListener) return;
22533
- const tabId = tabManager.getActiveTabId();
22534
- stateListener(getDevToolsPanelState(tabId));
22535
- }
22536
- async function withDevToolsAction(runtime2, tabManager, name, args, executor) {
22537
- const activityEntry = {
22538
- id: ++activityCounter,
22539
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
22540
- tool: name,
22541
- args: JSON.stringify(args).slice(0, 200),
22542
- result: "",
22543
- durationMs: 0,
22544
- status: "running"
22545
- };
22546
- activityLog.push(activityEntry);
22547
- if (activityLog.length > MAX_ACTIVITY_ENTRIES) {
22548
- activityLog.splice(0, activityLog.length - MAX_ACTIVITY_ENTRIES);
22549
- }
22550
- broadcastState(tabManager);
22551
- const startTime = Date.now();
22552
- try {
22553
- const result = await runtime2.runControlledAction({
22554
- source: "mcp",
22555
- name,
22556
- args,
22557
- tabId: tabManager.getActiveTabId(),
22558
- dangerous: DANGEROUS_DEVTOOLS_ACTIONS.has(name),
22559
- executor
22560
- });
22561
- activityEntry.status = "completed";
22562
- activityEntry.result = result.slice(0, 200);
22563
- activityEntry.durationMs = Date.now() - startTime;
22564
- broadcastState(tabManager);
22565
- return asTextResponse$1(result);
22566
- } catch (error) {
22567
- const message = error instanceof Error ? error.message : "Unknown error";
22568
- activityEntry.status = "failed";
22569
- activityEntry.result = message.slice(0, 200);
22570
- activityEntry.durationMs = Date.now() - startTime;
22571
- broadcastState(tabManager);
22572
- return asTextResponse$1(`Error: ${message}`);
22573
- }
22574
- }
22575
- function registerDevTools(server, tabManager, runtime2) {
22576
22843
  server.registerTool(
22577
- "vessel_devtools_console_logs",
22844
+ "vessel_devtools_network_response_body",
22578
22845
  {
22579
- title: "DevTools: Get Console Logs",
22580
- description: "Get console log entries captured from the active tab. Returns a rolling buffer of recent console.log, console.warn, console.error, etc. calls. Automatically starts capturing on first use.",
22846
+ title: "DevTools: Get Network Response Body",
22847
+ 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.",
22581
22848
  inputSchema: {
22582
- level: zod.z.enum(["log", "warning", "error", "info", "debug", "verbose"]).optional().describe("Filter by log level"),
22583
- limit: zod.z.number().optional().describe("Maximum number of entries to return (default: all)"),
22584
- search: zod.z.string().optional().describe("Filter entries containing this text (case-insensitive)")
22849
+ request_id: zod.z.string().describe("The requestId from a network log entry")
22585
22850
  }
22586
22851
  },
22587
- async ({ level, limit, search: search2 }) => {
22852
+ async ({ request_id }) => {
22588
22853
  return withDevToolsAction(
22589
22854
  runtime2,
22590
22855
  tabManager,
22591
- "devtools_console_logs",
22592
- { level, limit, search: search2 },
22593
- async () => {
22594
- const session = getOrCreateSession(tabManager);
22595
- await session.ensureConsoleDomain();
22596
- const entries = session.getConsoleLogs({ level, limit, search: search2 });
22597
- if (entries.length === 0) {
22598
- return "No console entries captured yet. Console monitoring is now active — new entries will be captured as they occur.";
22599
- }
22600
- return JSON.stringify(entries, null, 2);
22601
- }
22602
- );
22603
- }
22604
- );
22605
- server.registerTool(
22606
- "vessel_devtools_console_clear",
22607
- {
22608
- title: "DevTools: Clear Console Logs",
22609
- description: "Clear the captured console log buffer for the active tab."
22610
- },
22611
- async () => {
22612
- return withDevToolsAction(
22613
- runtime2,
22614
- tabManager,
22615
- "devtools_console_clear",
22616
- {},
22617
- async () => {
22618
- const session = getOrCreateSession(tabManager);
22619
- const count = session.clearConsoleLogs();
22620
- return `Cleared ${count} console entries.`;
22621
- }
22622
- );
22623
- }
22624
- );
22625
- server.registerTool(
22626
- "vessel_devtools_network_log",
22627
- {
22628
- title: "DevTools: Get Network Log",
22629
- description: "Get captured network requests/responses from the active tab. Returns method, URL, status, timing, headers, and size. Automatically starts capturing on first use.",
22630
- inputSchema: {
22631
- url_pattern: zod.z.string().optional().describe("Filter by URL pattern (regex or substring match)"),
22632
- method: zod.z.string().optional().describe("Filter by HTTP method (GET, POST, etc.)"),
22633
- status_min: zod.z.number().optional().describe("Minimum HTTP status code (e.g., 400 for errors)"),
22634
- status_max: zod.z.number().optional().describe("Maximum HTTP status code"),
22635
- limit: zod.z.number().optional().describe("Maximum number of entries to return (default: all)")
22636
- }
22637
- },
22638
- async ({ url_pattern, method, status_min, status_max, limit }) => {
22639
- return withDevToolsAction(
22640
- runtime2,
22641
- tabManager,
22642
- "devtools_network_log",
22643
- { url_pattern, method, status_min, status_max, limit },
22644
- async () => {
22645
- const session = getOrCreateSession(tabManager);
22646
- await session.ensureNetworkDomain();
22647
- const entries = session.getNetworkLog({
22648
- urlPattern: url_pattern,
22649
- method,
22650
- statusRange: status_min != null || status_max != null ? { min: status_min, max: status_max } : void 0,
22651
- limit
22652
- });
22653
- if (entries.length === 0) {
22654
- return "No network requests captured yet. Network monitoring is now active — new requests will be captured as they occur.";
22655
- }
22656
- return JSON.stringify(entries, null, 2);
22657
- }
22658
- );
22659
- }
22660
- );
22661
- server.registerTool(
22662
- "vessel_devtools_network_response_body",
22663
- {
22664
- title: "DevTools: Get Network Response Body",
22665
- 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.",
22666
- inputSchema: {
22667
- request_id: zod.z.string().describe("The requestId from a network log entry")
22668
- }
22669
- },
22670
- async ({ request_id }) => {
22671
- return withDevToolsAction(
22672
- runtime2,
22673
- tabManager,
22674
- "devtools_network_response_body",
22675
- { request_id },
22856
+ "devtools_network_response_body",
22857
+ { request_id },
22676
22858
  async () => {
22677
22859
  const session = getOrCreateSession(tabManager);
22678
22860
  const result = await session.getNetworkResponseBody(request_id);
@@ -22914,28 +23096,403 @@ Exception: ${result.exceptionDetails}`);
22914
23096
  return JSON.stringify(entries, null, 2);
22915
23097
  }
22916
23098
  );
22917
- }
23099
+ }
23100
+ );
23101
+ server.registerTool(
23102
+ "vessel_devtools_clear_errors",
23103
+ {
23104
+ title: "DevTools: Clear Errors",
23105
+ description: "Clear the captured error buffer for the active tab."
23106
+ },
23107
+ async () => {
23108
+ return withDevToolsAction(
23109
+ runtime2,
23110
+ tabManager,
23111
+ "devtools_clear_errors",
23112
+ {},
23113
+ async () => {
23114
+ const session = getOrCreateSession(tabManager);
23115
+ const count = session.clearErrors();
23116
+ return `Cleared ${count} error entries.`;
23117
+ }
23118
+ );
23119
+ }
23120
+ );
23121
+ }
23122
+ const DEFAULT_PAGE_FOLDER = "Vessel/Pages";
23123
+ const DEFAULT_NOTE_FOLDER = "Vessel/Research";
23124
+ const DEFAULT_BOOKMARK_FOLDER = "Vessel/Bookmarks";
23125
+ const PAGE_CONTENT_LIMIT = 6e3;
23126
+ const DEFAULT_LIST_LIMIT = 50;
23127
+ const DEFAULT_SEARCH_LIMIT = 20;
23128
+ const { access: access$1, mkdir: mkdir$1, readFile: readFile$1, readdir: readdir$1, stat, writeFile: writeFile$1 } = fs$1.promises;
23129
+ function getVaultRoot() {
23130
+ const configured = loadSettings().obsidianVaultPath.trim();
23131
+ if (!configured) {
23132
+ throw new Error(
23133
+ "Obsidian not configured. Set vault path in Vessel settings to use memory capture."
23134
+ );
23135
+ }
23136
+ return path$1.resolve(configured);
23137
+ }
23138
+ function assertInsideVault(targetPath, vaultRoot) {
23139
+ const resolved = path$1.resolve(targetPath);
23140
+ const relative = path$1.relative(vaultRoot, resolved);
23141
+ if (relative.startsWith("..") || path$1.isAbsolute(relative)) {
23142
+ throw new Error("Resolved note path is outside the configured vault.");
23143
+ }
23144
+ return resolved;
23145
+ }
23146
+ function normalizeFolder(folder, fallback) {
23147
+ const raw = (folder?.trim() || fallback).replace(/\\/g, "/");
23148
+ if (!raw) return fallback;
23149
+ if (path$1.isAbsolute(raw)) {
23150
+ throw new Error("Vault note folders must be relative to the vault root.");
23151
+ }
23152
+ const segments = raw.split("/").filter(Boolean);
23153
+ if (segments.some((segment) => segment === "." || segment === "..")) {
23154
+ throw new Error("Vault note folders cannot traverse outside the vault.");
23155
+ }
23156
+ return segments.join(path$1.sep);
23157
+ }
23158
+ function normalizeNotePath(notePath) {
23159
+ const raw = notePath.trim().replace(/\\/g, "/");
23160
+ if (!raw) {
23161
+ throw new Error("A note path is required.");
23162
+ }
23163
+ if (path$1.isAbsolute(raw)) {
23164
+ throw new Error("Note paths must be relative to the vault root.");
23165
+ }
23166
+ const segments = raw.split("/").filter(Boolean);
23167
+ if (segments.some((segment) => segment === "." || segment === "..")) {
23168
+ throw new Error("Note paths cannot traverse outside the vault.");
23169
+ }
23170
+ const normalized = segments.join(path$1.sep);
23171
+ return normalized.endsWith(".md") ? normalized : `${normalized}.md`;
23172
+ }
23173
+ function escapeYaml(value) {
23174
+ return JSON.stringify(value);
23175
+ }
23176
+ function renderFrontmatter(data) {
23177
+ const lines = ["---"];
23178
+ for (const [key2, value] of Object.entries(data)) {
23179
+ if (value == null) continue;
23180
+ if (Array.isArray(value)) {
23181
+ if (value.length === 0) continue;
23182
+ lines.push(`${key2}:`);
23183
+ for (const item of value) {
23184
+ lines.push(` - ${escapeYaml(item)}`);
23185
+ }
23186
+ continue;
23187
+ }
23188
+ lines.push(`${key2}: ${escapeYaml(value)}`);
23189
+ }
23190
+ lines.push("---", "");
23191
+ return lines.join("\n");
23192
+ }
23193
+ function slugify(value) {
23194
+ const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
23195
+ return normalized || "note";
23196
+ }
23197
+ async function pathExists$1(filePath2) {
23198
+ try {
23199
+ await access$1(filePath2);
23200
+ return true;
23201
+ } catch {
23202
+ return false;
23203
+ }
23204
+ }
23205
+ async function buildUniqueNotePath(dir, title) {
23206
+ const datePrefix = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
23207
+ const slug = slugify(title);
23208
+ const base = `${datePrefix}-${slug}`;
23209
+ let candidate = `${base}.md`;
23210
+ let counter = 2;
23211
+ while (await pathExists$1(path$1.join(dir, candidate))) {
23212
+ candidate = `${base}-${counter}.md`;
23213
+ counter += 1;
23214
+ }
23215
+ return path$1.join(dir, candidate);
23216
+ }
23217
+ function trimContent(content, limit = PAGE_CONTENT_LIMIT) {
23218
+ const cleaned = content.trim();
23219
+ if (cleaned.length <= limit) return cleaned;
23220
+ return `${cleaned.slice(0, limit)}
23221
+
23222
+ [Truncated]`;
23223
+ }
23224
+ function parseFrontmatter(content) {
23225
+ if (!content.startsWith("---\n")) {
23226
+ return { body: content, tags: [] };
23227
+ }
23228
+ const closingIndex = content.indexOf("\n---\n", 4);
23229
+ if (closingIndex === -1) {
23230
+ return { body: content, tags: [] };
23231
+ }
23232
+ const raw = content.slice(4, closingIndex);
23233
+ const body = content.slice(closingIndex + 5);
23234
+ const lines = raw.split("\n");
23235
+ const result = { tags: [] };
23236
+ let activeArrayKey = "";
23237
+ for (const line of lines) {
23238
+ const trimmed = line.trim();
23239
+ if (!trimmed) continue;
23240
+ if (trimmed.startsWith("- ") && activeArrayKey === "tags") {
23241
+ result.tags.push(
23242
+ trimmed.slice(2).trim().replace(/^["']|["']$/g, "")
23243
+ );
23244
+ continue;
23245
+ }
23246
+ activeArrayKey = "";
23247
+ const separatorIndex = trimmed.indexOf(":");
23248
+ if (separatorIndex === -1) continue;
23249
+ const key2 = trimmed.slice(0, separatorIndex).trim();
23250
+ const value = trimmed.slice(separatorIndex + 1).trim();
23251
+ if (key2 === "title" && value) {
23252
+ result.title = value.replace(/^["']|["']$/g, "");
23253
+ } else if (key2 === "tags") {
23254
+ activeArrayKey = "tags";
23255
+ if (value.startsWith("[") && value.endsWith("]")) {
23256
+ const inline = value.slice(1, -1).split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
23257
+ result.tags.push(...inline);
23258
+ activeArrayKey = "";
23259
+ }
23260
+ }
23261
+ }
23262
+ return { body, title: result.title, tags: result.tags };
23263
+ }
23264
+ async function collectMarkdownFiles(dir) {
23265
+ const entries = await readdir$1(dir, { withFileTypes: true });
23266
+ const nestedFiles = await Promise.all(
23267
+ entries.map(async (entry) => {
23268
+ const absolutePath = path$1.join(dir, entry.name);
23269
+ if (entry.isDirectory()) {
23270
+ return collectMarkdownFiles(absolutePath);
23271
+ }
23272
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
23273
+ return [absolutePath];
23274
+ }
23275
+ return [];
23276
+ })
23277
+ );
23278
+ return nestedFiles.flat();
23279
+ }
23280
+ async function toSummary(absolutePath, vaultRoot, raw) {
23281
+ const stats = await stat(absolutePath);
23282
+ const relativePath = path$1.relative(vaultRoot, absolutePath).split(path$1.sep).join("/");
23283
+ const noteContent = raw ?? await readFile$1(absolutePath, "utf-8");
23284
+ const parsed = parseFrontmatter(noteContent);
23285
+ const headingMatch = parsed.body.match(/^#\s+(.+)$/m);
23286
+ const title = parsed.title || headingMatch?.[1]?.trim() || path$1.basename(absolutePath, ".md");
23287
+ return {
23288
+ title,
23289
+ absolutePath,
23290
+ relativePath,
23291
+ modifiedAt: stats.mtime.toISOString(),
23292
+ tags: parsed.tags
23293
+ };
23294
+ }
23295
+ function renderBookmarkLinkBlock(bookmark, note) {
23296
+ const lines = [
23297
+ "## Linked Bookmark",
23298
+ "",
23299
+ `- Bookmark ID: \`${bookmark.id}\``,
23300
+ `- Title: ${bookmark.title || bookmark.url}`,
23301
+ `- URL: [${bookmark.url}](${bookmark.url})`,
23302
+ `- Saved At: ${bookmark.savedAt}`
23303
+ ];
23304
+ if (note?.trim()) {
23305
+ lines.push("", "### Context", "", note.trim());
23306
+ }
23307
+ return `${lines.join("\n")}
23308
+ `;
23309
+ }
23310
+ async function writeMemoryNote({
23311
+ title,
23312
+ body,
23313
+ folder,
23314
+ tags = [],
23315
+ frontmatter = {}
23316
+ }) {
23317
+ const vaultRoot = getVaultRoot();
23318
+ const relativeFolder = normalizeFolder(folder, DEFAULT_NOTE_FOLDER);
23319
+ const targetDir = path$1.join(vaultRoot, relativeFolder);
23320
+ await mkdir$1(targetDir, { recursive: true });
23321
+ const absolutePath = await buildUniqueNotePath(targetDir, title);
23322
+ const relativePath = path$1.relative(vaultRoot, absolutePath);
23323
+ const content = [
23324
+ renderFrontmatter({
23325
+ title,
23326
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
23327
+ tags,
23328
+ ...frontmatter
23329
+ }),
23330
+ body.trim(),
23331
+ ""
23332
+ ].join("\n");
23333
+ await writeFile$1(absolutePath, content, "utf-8");
23334
+ return {
23335
+ title,
23336
+ absolutePath,
23337
+ relativePath: relativePath.split(path$1.sep).join("/")
23338
+ };
23339
+ }
23340
+ async function appendToMemoryNote({
23341
+ notePath,
23342
+ content,
23343
+ heading
23344
+ }) {
23345
+ const vaultRoot = getVaultRoot();
23346
+ const relativePath = normalizeNotePath(notePath);
23347
+ const absolutePath = assertInsideVault(
23348
+ path$1.join(vaultRoot, relativePath),
23349
+ vaultRoot
23350
+ );
23351
+ if (!await pathExists$1(absolutePath)) {
23352
+ throw new Error(
23353
+ `Memory note not found: ${relativePath.split(path$1.sep).join("/")}`
23354
+ );
23355
+ }
23356
+ const current = (await readFile$1(absolutePath, "utf-8")).trimEnd();
23357
+ const nextParts = [current, ""];
23358
+ if (heading?.trim()) {
23359
+ nextParts.push(`## ${heading.trim()}`, "");
23360
+ }
23361
+ nextParts.push(content.trim(), "");
23362
+ await writeFile$1(absolutePath, nextParts.join("\n"), "utf-8");
23363
+ return {
23364
+ title: path$1.basename(absolutePath, ".md"),
23365
+ absolutePath,
23366
+ relativePath: relativePath.split(path$1.sep).join("/")
23367
+ };
23368
+ }
23369
+ async function listMemoryNotes({
23370
+ folder,
23371
+ limit = DEFAULT_LIST_LIMIT
23372
+ } = {}) {
23373
+ const vaultRoot = getVaultRoot();
23374
+ const relativeFolder = normalizeFolder(folder, "");
23375
+ const targetDir = relativeFolder ? path$1.join(vaultRoot, relativeFolder) : vaultRoot;
23376
+ if (!await pathExists$1(targetDir)) {
23377
+ return [];
23378
+ }
23379
+ const notes = await Promise.all(
23380
+ (await collectMarkdownFiles(targetDir)).map(
23381
+ (absolutePath) => toSummary(absolutePath, vaultRoot)
23382
+ )
23383
+ );
23384
+ return notes.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt)).slice(0, Math.max(1, limit));
23385
+ }
23386
+ async function searchMemoryNotes({
23387
+ query,
23388
+ folder,
23389
+ tags = [],
23390
+ limit = DEFAULT_SEARCH_LIMIT
23391
+ }) {
23392
+ const loweredQuery = query.trim().toLowerCase();
23393
+ if (!loweredQuery) {
23394
+ throw new Error("A non-empty memory search query is required.");
23395
+ }
23396
+ const vaultRoot = getVaultRoot();
23397
+ const relativeFolder = normalizeFolder(folder, "");
23398
+ const targetDir = relativeFolder ? path$1.join(vaultRoot, relativeFolder) : vaultRoot;
23399
+ if (!await pathExists$1(targetDir)) {
23400
+ return [];
23401
+ }
23402
+ const loweredTags = tags.map((tag) => tag.trim().toLowerCase()).filter(Boolean);
23403
+ const matches = await Promise.all(
23404
+ (await collectMarkdownFiles(targetDir)).map(async (absolutePath) => {
23405
+ const raw = await readFile$1(absolutePath, "utf-8");
23406
+ const parsed = parseFrontmatter(raw);
23407
+ const summary = await toSummary(absolutePath, vaultRoot, raw);
23408
+ const haystack = `${summary.title}
23409
+ ${summary.relativePath}
23410
+ ${parsed.body}`.toLowerCase();
23411
+ const hasQuery = haystack.includes(loweredQuery);
23412
+ const hasTags = loweredTags.length === 0 || loweredTags.every(
23413
+ (tag) => summary.tags.some((noteTag) => noteTag.toLowerCase() === tag)
23414
+ );
23415
+ return hasQuery && hasTags ? summary : null;
23416
+ })
22918
23417
  );
22919
- server.registerTool(
22920
- "vessel_devtools_clear_errors",
22921
- {
22922
- title: "DevTools: Clear Errors",
22923
- description: "Clear the captured error buffer for the active tab."
22924
- },
22925
- async () => {
22926
- return withDevToolsAction(
22927
- runtime2,
22928
- tabManager,
22929
- "devtools_clear_errors",
22930
- {},
22931
- async () => {
22932
- const session = getOrCreateSession(tabManager);
22933
- const count = session.clearErrors();
22934
- return `Cleared ${count} error entries.`;
22935
- }
22936
- );
23418
+ return matches.filter((item) => item !== null).sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt)).slice(0, Math.max(1, limit));
23419
+ }
23420
+ async function capturePageToVault({
23421
+ page,
23422
+ title,
23423
+ folder,
23424
+ summary,
23425
+ note,
23426
+ tags = []
23427
+ }) {
23428
+ const noteTitle = title?.trim() || page.title.trim() || page.url;
23429
+ const bodyLines = [
23430
+ `# ${noteTitle}`,
23431
+ "",
23432
+ `Source: [${page.title || page.url}](${page.url})`,
23433
+ `Captured: ${(/* @__PURE__ */ new Date()).toISOString()}`
23434
+ ];
23435
+ if (page.byline) {
23436
+ bodyLines.push(`Byline: ${page.byline}`);
23437
+ }
23438
+ bodyLines.push("");
23439
+ if (summary?.trim()) {
23440
+ bodyLines.push("## Summary", "", summary.trim(), "");
23441
+ }
23442
+ if (note?.trim()) {
23443
+ bodyLines.push("## Research Note", "", note.trim(), "");
23444
+ }
23445
+ if (page.excerpt.trim()) {
23446
+ bodyLines.push("## Excerpt", "", page.excerpt.trim(), "");
23447
+ }
23448
+ const snapshot2 = trimContent(page.content);
23449
+ if (snapshot2) {
23450
+ bodyLines.push("## Page Snapshot", "", snapshot2, "");
23451
+ }
23452
+ return await writeMemoryNote({
23453
+ title: noteTitle,
23454
+ body: bodyLines.join("\n"),
23455
+ folder: folder || DEFAULT_PAGE_FOLDER,
23456
+ tags,
23457
+ frontmatter: {
23458
+ source_url: page.url,
23459
+ source_title: page.title || page.url
22937
23460
  }
22938
- );
23461
+ });
23462
+ }
23463
+ async function linkBookmarkToMemory({
23464
+ bookmark,
23465
+ notePath,
23466
+ title,
23467
+ folder,
23468
+ note,
23469
+ tags = []
23470
+ }) {
23471
+ if (notePath?.trim()) {
23472
+ return await appendToMemoryNote({
23473
+ notePath,
23474
+ heading: "Linked Bookmark",
23475
+ content: [
23476
+ `- Bookmark ID: \`${bookmark.id}\``,
23477
+ `- Title: ${bookmark.title || bookmark.url}`,
23478
+ `- URL: [${bookmark.url}](${bookmark.url})`,
23479
+ `- Saved At: ${bookmark.savedAt}`,
23480
+ note?.trim() ? `- Note: ${note.trim()}` : ""
23481
+ ].filter(Boolean).join("\n")
23482
+ });
23483
+ }
23484
+ const noteTitle = title?.trim() || bookmark.title || bookmark.url;
23485
+ return await writeMemoryNote({
23486
+ title: noteTitle,
23487
+ body: renderBookmarkLinkBlock(bookmark, note),
23488
+ folder: folder || DEFAULT_BOOKMARK_FOLDER,
23489
+ tags,
23490
+ frontmatter: {
23491
+ bookmark_id: bookmark.id,
23492
+ source_url: bookmark.url,
23493
+ source_title: bookmark.title || bookmark.url
23494
+ }
23495
+ });
22939
23496
  }
22940
23497
  const logger$f = createLogger("MCP");
22941
23498
  function asTextResponse(text) {
@@ -23016,7 +23573,7 @@ async function getPostActionState(tabManager, name) {
23016
23573
  if (navActions.includes(name)) {
23017
23574
  let warning = "";
23018
23575
  try {
23019
- const page = await extractContent$1(wc);
23576
+ const page = await extractContent(wc);
23020
23577
  const issue = getRecoverableAccessIssue(page);
23021
23578
  if (issue) {
23022
23579
  const blockedUrl = wc.getURL();
@@ -23682,65 +24239,216 @@ function registerBookmarkTools(server, tabManager, runtime2) {
23682
24239
  summary: zod.z.string().optional().describe("Optional one-sentence summary for the folder")
23683
24240
  }
23684
24241
  },
23685
- async ({ folder_id, new_name, summary }) => {
24242
+ async ({ folder_id, new_name, summary }) => {
24243
+ return withAction(
24244
+ runtime2,
24245
+ tabManager,
24246
+ "rename_bookmark_folder",
24247
+ { folder_id, new_name, summary },
24248
+ async () => {
24249
+ const existing = findFolderByName(new_name);
24250
+ if (existing && existing.id !== folder_id) {
24251
+ return composeFolderAwareResponse$1(
24252
+ `Folder "${existing.name}" already exists (id=${existing.id})`
24253
+ );
24254
+ }
24255
+ const folder = renameFolder(
24256
+ folder_id,
24257
+ new_name,
24258
+ summary
24259
+ );
24260
+ return folder ? composeFolderAwareResponse$1(`Renamed folder to "${folder.name}"`) : `Folder ${folder_id} not found`;
24261
+ }
24262
+ );
24263
+ }
24264
+ );
24265
+ server.registerTool(
24266
+ "memory_link_bookmark",
24267
+ {
24268
+ title: "Link Bookmark To Memory",
24269
+ description: "Create a note for a bookmark or append bookmark details into an existing memory note.",
24270
+ inputSchema: {
24271
+ bookmark_id: zod.z.string().describe("Bookmark ID to link"),
24272
+ note_path: zod.z.string().optional().describe("Existing relative note path to append into"),
24273
+ title: zod.z.string().optional().describe("Optional title when creating a new note"),
24274
+ folder: zod.z.string().optional().describe("Relative folder when creating a new note"),
24275
+ note: zod.z.string().optional().describe(
24276
+ "Optional rationale or breadcrumb to store with the bookmark"
24277
+ ),
24278
+ tags: zod.z.array(zod.z.string()).optional().describe("Optional tags when creating a new note")
24279
+ }
24280
+ },
24281
+ async ({ bookmark_id, note_path, title, folder, note, tags }) => {
24282
+ return withAction(
24283
+ runtime2,
24284
+ tabManager,
24285
+ "memory_link_bookmark",
24286
+ { bookmark_id, note_path, title, folder, tags },
24287
+ async () => {
24288
+ const bookmark = getBookmark(bookmark_id);
24289
+ if (!bookmark) {
24290
+ return `Bookmark ${bookmark_id} not found`;
24291
+ }
24292
+ const saved = await linkBookmarkToMemory({
24293
+ bookmark,
24294
+ notePath: note_path,
24295
+ title,
24296
+ folder,
24297
+ note,
24298
+ tags
24299
+ });
24300
+ return `Linked bookmark "${bookmark.title}" to memory note ${saved.relativePath}`;
24301
+ }
24302
+ );
24303
+ }
24304
+ );
24305
+ }
24306
+ function registerMemoryTools(server, tabManager, runtime2) {
24307
+ server.registerTool(
24308
+ "memory_note_create",
24309
+ {
24310
+ title: "Create Memory Note",
24311
+ description: "Write a markdown note into the configured Obsidian vault for research notes, breadcrumbs, or synthesis.",
24312
+ inputSchema: {
24313
+ title: zod.z.string().describe("Title of the note"),
24314
+ body: zod.z.string().describe("Markdown body for the note"),
24315
+ folder: zod.z.string().optional().describe(
24316
+ "Relative folder inside the vault (default: Vessel/Research)"
24317
+ ),
24318
+ tags: zod.z.array(zod.z.string()).optional().describe("Optional tags to store in frontmatter")
24319
+ }
24320
+ },
24321
+ async ({ title, body, folder, tags }) => {
24322
+ return withAction(
24323
+ runtime2,
24324
+ tabManager,
24325
+ "memory_note_create",
24326
+ { title, folder, tags },
24327
+ async () => {
24328
+ const saved = await writeMemoryNote({ title, body, folder, tags });
24329
+ return `Saved memory note "${saved.title}" to ${saved.relativePath}`;
24330
+ }
24331
+ );
24332
+ }
24333
+ );
24334
+ server.registerTool(
24335
+ "memory_append",
24336
+ {
24337
+ title: "Append Memory Note",
24338
+ description: "Append markdown content to an existing note in the configured Obsidian vault.",
24339
+ inputSchema: {
24340
+ note_path: zod.z.string().describe("Relative path to an existing note inside the vault"),
24341
+ content: zod.z.string().describe("Markdown content to append"),
24342
+ heading: zod.z.string().optional().describe("Optional section heading to add before the content")
24343
+ }
24344
+ },
24345
+ async ({ note_path, content, heading }) => {
24346
+ return withAction(
24347
+ runtime2,
24348
+ tabManager,
24349
+ "memory_note_append",
24350
+ { note_path, heading },
24351
+ async () => {
24352
+ const saved = await appendToMemoryNote({
24353
+ notePath: note_path,
24354
+ content,
24355
+ heading
24356
+ });
24357
+ return `Appended memory note at ${saved.relativePath}`;
24358
+ }
24359
+ );
24360
+ }
24361
+ );
24362
+ server.registerTool(
24363
+ "memory_list",
24364
+ {
24365
+ title: "List Memory Notes",
24366
+ description: "List recent markdown notes in the configured Obsidian vault.",
24367
+ inputSchema: {
24368
+ folder: zod.z.string().optional().describe("Optional relative folder inside the vault"),
24369
+ limit: zod.z.number().int().positive().max(200).optional().describe("Maximum number of notes to return")
24370
+ }
24371
+ },
24372
+ async ({ folder, limit }) => {
24373
+ return withAction(
24374
+ runtime2,
24375
+ tabManager,
24376
+ "memory_note_list",
24377
+ { folder, limit },
24378
+ async () => {
24379
+ const notes = await listMemoryNotes({ folder, limit });
24380
+ if (notes.length === 0) {
24381
+ return "No memory notes found.";
24382
+ }
24383
+ return notes.map(
24384
+ (note) => `- ${note.title} | path=${note.relativePath} | modified=${note.modifiedAt}${note.tags.length ? ` | tags=${note.tags.join(",")}` : ""}`
24385
+ ).join("\n");
24386
+ }
24387
+ );
24388
+ }
24389
+ );
24390
+ server.registerTool(
24391
+ "memory_search",
24392
+ {
24393
+ title: "Search Memory Notes",
24394
+ description: "Search markdown notes in the configured Obsidian vault by title, path, body, and optional tags.",
24395
+ inputSchema: {
24396
+ query: zod.z.string().describe("Search query"),
24397
+ folder: zod.z.string().optional().describe("Optional relative folder inside the vault"),
24398
+ tags: zod.z.array(zod.z.string()).optional().describe("Optional tags that matching notes must contain"),
24399
+ limit: zod.z.number().int().positive().max(100).optional().describe("Maximum number of matching notes to return")
24400
+ }
24401
+ },
24402
+ async ({ query, folder, tags, limit }) => {
23686
24403
  return withAction(
23687
24404
  runtime2,
23688
24405
  tabManager,
23689
- "rename_bookmark_folder",
23690
- { folder_id, new_name, summary },
24406
+ "memory_note_search",
24407
+ { query, folder, tags, limit },
23691
24408
  async () => {
23692
- const existing = findFolderByName(new_name);
23693
- if (existing && existing.id !== folder_id) {
23694
- return composeFolderAwareResponse$1(
23695
- `Folder "${existing.name}" already exists (id=${existing.id})`
23696
- );
24409
+ const notes = await searchMemoryNotes({ query, folder, tags, limit });
24410
+ if (notes.length === 0) {
24411
+ return `No memory notes matched "${query}".`;
23697
24412
  }
23698
- const folder = renameFolder(
23699
- folder_id,
23700
- new_name,
23701
- summary
23702
- );
23703
- return folder ? composeFolderAwareResponse$1(`Renamed folder to "${folder.name}"`) : `Folder ${folder_id} not found`;
24413
+ return notes.map(
24414
+ (note) => `- ${note.title} | path=${note.relativePath} | modified=${note.modifiedAt}${note.tags.length ? ` | tags=${note.tags.join(",")}` : ""}`
24415
+ ).join("\n");
23704
24416
  }
23705
24417
  );
23706
24418
  }
23707
24419
  );
23708
24420
  server.registerTool(
23709
- "memory_link_bookmark",
24421
+ "memory_page_capture",
23710
24422
  {
23711
- title: "Link Bookmark To Memory",
23712
- description: "Create a note for a bookmark or append bookmark details into an existing memory note.",
24423
+ title: "Capture Page To Memory",
24424
+ description: "Capture the current page into the configured Obsidian vault as a markdown note with URL, excerpt, and content snapshot.",
23713
24425
  inputSchema: {
23714
- bookmark_id: zod.z.string().describe("Bookmark ID to link"),
23715
- note_path: zod.z.string().optional().describe("Existing relative note path to append into"),
23716
- title: zod.z.string().optional().describe("Optional title when creating a new note"),
23717
- folder: zod.z.string().optional().describe("Relative folder when creating a new note"),
23718
- note: zod.z.string().optional().describe(
23719
- "Optional rationale or breadcrumb to store with the bookmark"
23720
- ),
23721
- tags: zod.z.array(zod.z.string()).optional().describe("Optional tags when creating a new note")
24426
+ title: zod.z.string().optional().describe("Optional note title override"),
24427
+ folder: zod.z.string().optional().describe("Relative folder inside the vault (default: Vessel/Pages)"),
24428
+ summary: zod.z.string().optional().describe("Optional summary written into the note"),
24429
+ note: zod.z.string().optional().describe("Optional research note or breadcrumb"),
24430
+ tags: zod.z.array(zod.z.string()).optional().describe("Optional tags to store in frontmatter")
23722
24431
  }
23723
24432
  },
23724
- async ({ bookmark_id, note_path, title, folder, note, tags }) => {
24433
+ async ({ title, folder, summary, note, tags }) => {
24434
+ const tab = tabManager.getActiveTab();
24435
+ if (!tab) return asNoActiveTabResponse();
23725
24436
  return withAction(
23726
24437
  runtime2,
23727
24438
  tabManager,
23728
- "memory_link_bookmark",
23729
- { bookmark_id, note_path, title, folder, tags },
24439
+ "memory_page_capture",
24440
+ { title, folder, tags },
23730
24441
  async () => {
23731
- const bookmark = getBookmark(bookmark_id);
23732
- if (!bookmark) {
23733
- return `Bookmark ${bookmark_id} not found`;
23734
- }
23735
- const saved = linkBookmarkToMemory({
23736
- bookmark,
23737
- notePath: note_path,
24442
+ const page = await extractContent(tab.view.webContents);
24443
+ const saved = await capturePageToVault({
24444
+ page,
23738
24445
  title,
23739
24446
  folder,
24447
+ summary,
23740
24448
  note,
23741
24449
  tags
23742
24450
  });
23743
- return `Linked bookmark "${bookmark.title}" to memory note ${saved.relativePath}`;
24451
+ return `Captured page "${saved.title}" to ${saved.relativePath}`;
23744
24452
  }
23745
24453
  );
23746
24454
  }
@@ -24498,7 +25206,7 @@ function registerTools(server, tabManager, runtime2) {
24498
25206
  const wc = activeTab.view.webContents;
24499
25207
  pageUrl = wc.getURL();
24500
25208
  pageTitle = wc.getTitle();
24501
- const page = await extractContent$1(wc);
25209
+ const page = await extractContent(wc);
24502
25210
  pageType = detectPageType(page);
24503
25211
  } catch (err) {
24504
25212
  logger$b.warn("Failed to detect page type for tool scoring, falling back to GENERAL:", err);
@@ -24664,7 +25372,7 @@ ${buildScopedContext(pageContent, mode)}`;
24664
25372
  const tab = tabManager.getActiveTab();
24665
25373
  if (!tab) return asNoActiveTabResponse();
24666
25374
  try {
24667
- const pageContent = await extractContent$1(tab.view.webContents);
25375
+ const pageContent = await extractContent(tab.view.webContents);
24668
25376
  const effectiveMode = mode || "full";
24669
25377
  return asTextResponse(
24670
25378
  await buildExtractResponse(
@@ -24696,7 +25404,7 @@ ${buildScopedContext(pageContent, mode)}`;
24696
25404
  const tab = tabManager.getActiveTab();
24697
25405
  if (!tab) return asNoActiveTabResponse();
24698
25406
  try {
24699
- const pageContent = await extractContent$1(tab.view.webContents);
25407
+ const pageContent = await extractContent(tab.view.webContents);
24700
25408
  const effectiveMode = mode || "full";
24701
25409
  return asTextResponse(
24702
25410
  await buildExtractResponse(
@@ -24831,7 +25539,7 @@ ${buildScopedContext(pageContent, mode)}`;
24831
25539
  const tab = tabManager.getActiveTab();
24832
25540
  if (!tab) return asNoActiveTabResponse();
24833
25541
  try {
24834
- const pageContent = await extractContent$1(tab.view.webContents);
25542
+ const pageContent = await extractContent(tab.view.webContents);
24835
25543
  const requestedType = typeof type === "string" && type.trim() ? type.trim().toLowerCase() : "";
24836
25544
  const entities = (pageContent.structuredData ?? []).filter(
24837
25545
  (entity) => requestedType ? entity.types.some(
@@ -25805,155 +26513,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
25805
26513
  );
25806
26514
  registerBookmarkTools(server, tabManager, runtime2);
25807
26515
  registerSessionTools(server, tabManager, runtime2);
25808
- server.registerTool(
25809
- "memory_note_create",
25810
- {
25811
- title: "Create Memory Note",
25812
- description: "Write a markdown note into the configured Obsidian vault for research notes, breadcrumbs, or synthesis.",
25813
- inputSchema: {
25814
- title: zod.z.string().describe("Title of the note"),
25815
- body: zod.z.string().describe("Markdown body for the note"),
25816
- folder: zod.z.string().optional().describe(
25817
- "Relative folder inside the vault (default: Vessel/Research)"
25818
- ),
25819
- tags: zod.z.array(zod.z.string()).optional().describe("Optional tags to store in frontmatter")
25820
- }
25821
- },
25822
- async ({ title, body, folder, tags }) => {
25823
- return withAction(
25824
- runtime2,
25825
- tabManager,
25826
- "memory_note_create",
25827
- { title, folder, tags },
25828
- async () => {
25829
- const saved = writeMemoryNote({ title, body, folder, tags });
25830
- return `Saved memory note "${saved.title}" to ${saved.relativePath}`;
25831
- }
25832
- );
25833
- }
25834
- );
25835
- server.registerTool(
25836
- "memory_append",
25837
- {
25838
- title: "Append Memory Note",
25839
- description: "Append markdown content to an existing note in the configured Obsidian vault.",
25840
- inputSchema: {
25841
- note_path: zod.z.string().describe("Relative path to an existing note inside the vault"),
25842
- content: zod.z.string().describe("Markdown content to append"),
25843
- heading: zod.z.string().optional().describe("Optional section heading to add before the content")
25844
- }
25845
- },
25846
- async ({ note_path, content, heading }) => {
25847
- return withAction(
25848
- runtime2,
25849
- tabManager,
25850
- "memory_note_append",
25851
- { note_path, heading },
25852
- async () => {
25853
- const saved = appendToMemoryNote({
25854
- notePath: note_path,
25855
- content,
25856
- heading
25857
- });
25858
- return `Appended memory note at ${saved.relativePath}`;
25859
- }
25860
- );
25861
- }
25862
- );
25863
- server.registerTool(
25864
- "memory_list",
25865
- {
25866
- title: "List Memory Notes",
25867
- description: "List recent markdown notes in the configured Obsidian vault.",
25868
- inputSchema: {
25869
- folder: zod.z.string().optional().describe("Optional relative folder inside the vault"),
25870
- limit: zod.z.number().int().positive().max(200).optional().describe("Maximum number of notes to return")
25871
- }
25872
- },
25873
- async ({ folder, limit }) => {
25874
- return withAction(
25875
- runtime2,
25876
- tabManager,
25877
- "memory_note_list",
25878
- { folder, limit },
25879
- async () => {
25880
- const notes = listMemoryNotes({ folder, limit });
25881
- if (notes.length === 0) {
25882
- return "No memory notes found.";
25883
- }
25884
- return notes.map(
25885
- (note) => `- ${note.title} | path=${note.relativePath} | modified=${note.modifiedAt}${note.tags.length ? ` | tags=${note.tags.join(",")}` : ""}`
25886
- ).join("\n");
25887
- }
25888
- );
25889
- }
25890
- );
25891
- server.registerTool(
25892
- "memory_search",
25893
- {
25894
- title: "Search Memory Notes",
25895
- description: "Search markdown notes in the configured Obsidian vault by title, path, body, and optional tags.",
25896
- inputSchema: {
25897
- query: zod.z.string().describe("Search query"),
25898
- folder: zod.z.string().optional().describe("Optional relative folder inside the vault"),
25899
- tags: zod.z.array(zod.z.string()).optional().describe("Optional tags that matching notes must contain"),
25900
- limit: zod.z.number().int().positive().max(100).optional().describe("Maximum number of matching notes to return")
25901
- }
25902
- },
25903
- async ({ query, folder, tags, limit }) => {
25904
- return withAction(
25905
- runtime2,
25906
- tabManager,
25907
- "memory_note_search",
25908
- { query, folder, tags, limit },
25909
- async () => {
25910
- const notes = searchMemoryNotes({ query, folder, tags, limit });
25911
- if (notes.length === 0) {
25912
- return `No memory notes matched "${query}".`;
25913
- }
25914
- return notes.map(
25915
- (note) => `- ${note.title} | path=${note.relativePath} | modified=${note.modifiedAt}${note.tags.length ? ` | tags=${note.tags.join(",")}` : ""}`
25916
- ).join("\n");
25917
- }
25918
- );
25919
- }
25920
- );
25921
- server.registerTool(
25922
- "memory_page_capture",
25923
- {
25924
- title: "Capture Page To Memory",
25925
- description: "Capture the current page into the configured Obsidian vault as a markdown note with URL, excerpt, and content snapshot.",
25926
- inputSchema: {
25927
- title: zod.z.string().optional().describe("Optional note title override"),
25928
- folder: zod.z.string().optional().describe("Relative folder inside the vault (default: Vessel/Pages)"),
25929
- summary: zod.z.string().optional().describe("Optional summary written into the note"),
25930
- note: zod.z.string().optional().describe("Optional research note or breadcrumb"),
25931
- tags: zod.z.array(zod.z.string()).optional().describe("Optional tags to store in frontmatter")
25932
- }
25933
- },
25934
- async ({ title, folder, summary, note, tags }) => {
25935
- const tab = tabManager.getActiveTab();
25936
- if (!tab) return asNoActiveTabResponse();
25937
- return withAction(
25938
- runtime2,
25939
- tabManager,
25940
- "memory_page_capture",
25941
- { title, folder, tags },
25942
- async () => {
25943
- const page = await extractContent$1(tab.view.webContents);
25944
- const saved = capturePageToVault({
25945
- page,
25946
- title,
25947
- folder,
25948
- summary,
25949
- note,
25950
- tags
25951
- });
25952
- return `Captured page "${saved.title}" to ${saved.relativePath}`;
25953
- }
25954
- );
25955
- }
25956
- );
26516
+ registerMemoryTools(server, tabManager, runtime2);
25957
26517
  server.registerTool(
25958
26518
  "flow_start",
25959
26519
  {
@@ -26061,7 +26621,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
26061
26621
  const wc = tab.view.webContents;
26062
26622
  let page;
26063
26623
  try {
26064
- page = await extractContent$1(wc);
26624
+ page = await extractContent(wc);
26065
26625
  } catch (err) {
26066
26626
  logger$b.warn("Failed to extract page while generating suggestions:", err);
26067
26627
  return asTextResponse(
@@ -27622,25 +28182,27 @@ const VALID_KIT_CATEGORIES = /* @__PURE__ */ new Set([
27622
28182
  "productivity",
27623
28183
  "forms"
27624
28184
  ]);
27625
- const BUNDLED_KIT_IDS = /* @__PURE__ */ new Set([
27626
- "research-collect",
27627
- "price-scout",
27628
- "form-filler"
27629
- ]);
28185
+ const BUNDLED_KIT_IDS = /* @__PURE__ */ new Set();
27630
28186
  const KIT_ID_UNSAFE_CHAR_PATTERN = /[/\\\0]/;
27631
28187
  function isSafeAutomationKitId(id) {
27632
28188
  return id.length > 0 && !KIT_ID_UNSAFE_CHAR_PATTERN.test(id);
27633
28189
  }
27634
28190
  const logger$9 = createLogger("KitRegistry");
28191
+ const { access, mkdir, readFile, readdir, unlink, writeFile } = fs$1.promises;
27635
28192
  function getUserKitsDir() {
27636
28193
  return path$1.join(electron.app.getPath("userData"), "kits");
27637
28194
  }
27638
- function ensureKitsDir() {
27639
- const dir = getUserKitsDir();
27640
- if (!fs$1.existsSync(dir)) {
27641
- fs$1.mkdirSync(dir, { recursive: true });
28195
+ async function pathExists(filePath2) {
28196
+ try {
28197
+ await access(filePath2);
28198
+ return true;
28199
+ } catch {
28200
+ return false;
27642
28201
  }
27643
28202
  }
28203
+ async function ensureKitsDir() {
28204
+ await mkdir(getUserKitsDir(), { recursive: true });
28205
+ }
27644
28206
  function getKitFilePath(id) {
27645
28207
  if (!isSafeAutomationKitId(id)) return null;
27646
28208
  const kitsDir = path$1.resolve(getUserKitsDir());
@@ -27652,12 +28214,12 @@ function isValidKit(value) {
27652
28214
  const k = value;
27653
28215
  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);
27654
28216
  }
27655
- function getInstalledKits() {
27656
- ensureKitsDir();
28217
+ async function getInstalledKits() {
28218
+ await ensureKitsDir();
27657
28219
  const dir = getUserKitsDir();
27658
28220
  let files;
27659
28221
  try {
27660
- files = fs$1.readdirSync(dir).filter((f) => f.endsWith(".kit.json"));
28222
+ files = (await readdir(dir)).filter((f) => f.endsWith(".kit.json"));
27661
28223
  } catch (err) {
27662
28224
  logger$9.warn("Failed to read kit directory:", err);
27663
28225
  return [];
@@ -27665,23 +28227,23 @@ function getInstalledKits() {
27665
28227
  const kits = [];
27666
28228
  for (const file of files) {
27667
28229
  try {
27668
- const raw = fs$1.readFileSync(path$1.join(dir, file), "utf-8");
28230
+ const raw = await readFile(path$1.join(dir, file), "utf-8");
27669
28231
  const parsed = JSON.parse(raw);
27670
28232
  if (isValidKit(parsed)) {
27671
28233
  kits.push(parsed);
27672
28234
  } else {
27673
- logger$9.warn(`Skipping invalid kit file: ${file}`);
28235
+ logger$9.warn(`Skipping invalid skill file: ${file}`);
27674
28236
  }
27675
28237
  } catch (err) {
27676
- logger$9.warn(`Failed to read kit file: ${file}`, err);
28238
+ logger$9.warn(`Failed to read skill file: ${file}`, err);
27677
28239
  }
27678
28240
  }
27679
28241
  return kits;
27680
28242
  }
27681
28243
  async function installKitFromFile() {
27682
28244
  const { canceled, filePaths } = await electron.dialog.showOpenDialog({
27683
- title: "Install Automation Kit",
27684
- filters: [{ name: "Automation Kit", extensions: ["kit.json", "json"] }],
28245
+ title: "Import Skill",
28246
+ filters: [{ name: "Skills", extensions: ["skill.json", "json"] }],
27685
28247
  properties: ["openFile"]
27686
28248
  });
27687
28249
  if (canceled || filePaths.length === 0) {
@@ -27689,64 +28251,130 @@ async function installKitFromFile() {
27689
28251
  }
27690
28252
  let raw;
27691
28253
  try {
27692
- raw = fs$1.readFileSync(filePaths[0], "utf-8");
28254
+ raw = await readFile(filePaths[0], "utf-8");
27693
28255
  } catch (err) {
27694
- logger$9.warn("Failed to read selected kit file:", err);
28256
+ logger$9.warn("Failed to read selected skill file:", err);
27695
28257
  return errorResult("Could not read the selected file.");
27696
28258
  }
27697
28259
  let parsed;
27698
28260
  try {
27699
28261
  parsed = JSON.parse(raw);
27700
28262
  } catch (err) {
27701
- logger$9.warn("Selected kit file is not valid JSON:", err);
28263
+ logger$9.warn("Selected skill file is not valid JSON:", err);
27702
28264
  return errorResult("File is not valid JSON.");
27703
28265
  }
27704
28266
  if (!isValidKit(parsed)) {
27705
28267
  return errorResult(
27706
- "File is not a valid automation kit. Required fields: id, name, description, icon, inputs, promptTemplate."
28268
+ "File is not a valid skill. Required fields: id, name, description, icon, inputs, promptTemplate."
28269
+ );
28270
+ }
28271
+ if (BUNDLED_KIT_IDS.has(parsed.id)) {
28272
+ return errorResult(
28273
+ `Skill id "${parsed.id}" conflicts with a built-in skill and cannot be overwritten.`
28274
+ );
28275
+ }
28276
+ await ensureKitsDir();
28277
+ const dest = getKitFilePath(parsed.id);
28278
+ if (!dest) {
28279
+ return errorResult("Skill id contains unsupported characters.");
28280
+ }
28281
+ try {
28282
+ await writeFile(dest, JSON.stringify(parsed, null, 2), "utf-8");
28283
+ } catch (err) {
28284
+ logger$9.warn("Failed to save skill file:", err);
28285
+ return errorResult("Failed to save the skill file.");
28286
+ }
28287
+ return okResult({ kit: parsed });
28288
+ }
28289
+ async function createKitFromText(source) {
28290
+ let parsed;
28291
+ try {
28292
+ parsed = JSON.parse(source);
28293
+ } catch (err) {
28294
+ logger$9.warn("Created skill text is not valid JSON:", err);
28295
+ return errorResult("Skill text is not valid JSON.");
28296
+ }
28297
+ if (!isValidKit(parsed)) {
28298
+ return errorResult(
28299
+ "Text is not a valid skill. Required fields: id, name, description, icon, inputs, promptTemplate."
27707
28300
  );
27708
28301
  }
27709
28302
  if (BUNDLED_KIT_IDS.has(parsed.id)) {
27710
28303
  return errorResult(
27711
- `Kit id "${parsed.id}" conflicts with a built-in kit and cannot be overwritten.`
28304
+ `Skill id "${parsed.id}" conflicts with a built-in skill and cannot be overwritten.`
27712
28305
  );
27713
28306
  }
27714
- ensureKitsDir();
28307
+ await ensureKitsDir();
27715
28308
  const dest = getKitFilePath(parsed.id);
27716
28309
  if (!dest) {
27717
- return errorResult("Kit id contains unsupported characters.");
28310
+ return errorResult("Skill id contains unsupported characters.");
28311
+ }
28312
+ try {
28313
+ await writeFile(dest, JSON.stringify(parsed, null, 2), "utf-8");
28314
+ } catch (err) {
28315
+ logger$9.warn("Failed to save created skill:", err);
28316
+ return errorResult("Failed to save the skill.");
28317
+ }
28318
+ return okResult({ kit: parsed });
28319
+ }
28320
+ async function updateKitFromText(id, source) {
28321
+ if (BUNDLED_KIT_IDS.has(id)) {
28322
+ return errorResult("Built-in skills cannot be edited.");
28323
+ }
28324
+ const target = getKitFilePath(id);
28325
+ if (!target) {
28326
+ return errorResult("Skill id contains unsupported characters.");
28327
+ }
28328
+ let parsed;
28329
+ try {
28330
+ parsed = JSON.parse(source);
28331
+ } catch (err) {
28332
+ logger$9.warn("Updated skill text is not valid JSON:", err);
28333
+ return errorResult("Skill text is not valid JSON.");
28334
+ }
28335
+ if (!isValidKit(parsed)) {
28336
+ return errorResult(
28337
+ "Text is not a valid skill. Required fields: id, name, description, icon, inputs, promptTemplate."
28338
+ );
28339
+ }
28340
+ if (parsed.id !== id) {
28341
+ return errorResult("Skill id cannot be changed while editing.");
28342
+ }
28343
+ await ensureKitsDir();
28344
+ if (!await pathExists(target)) {
28345
+ return errorResult("Skill not found.");
27718
28346
  }
27719
28347
  try {
27720
- fs$1.writeFileSync(dest, JSON.stringify(parsed, null, 2), "utf-8");
28348
+ await writeFile(target, JSON.stringify(parsed, null, 2), "utf-8");
27721
28349
  } catch (err) {
27722
- logger$9.warn("Failed to save kit file:", err);
27723
- return errorResult("Failed to save the kit file.");
28350
+ logger$9.warn("Failed to update skill:", err);
28351
+ return errorResult("Failed to update the skill.");
27724
28352
  }
27725
28353
  return okResult({ kit: parsed });
27726
28354
  }
27727
- function uninstallKit(id, scheduledKitIds) {
28355
+ async function uninstallKit(id, scheduledKitIds) {
27728
28356
  if (BUNDLED_KIT_IDS.has(id)) {
27729
- return errorResult("Built-in kits cannot be removed.");
28357
+ return errorResult("Built-in skills cannot be removed.");
27730
28358
  }
27731
28359
  if (scheduledKitIds?.has(id)) {
27732
28360
  return errorResult(
27733
- "This kit has active scheduled jobs. Delete or reassign them first."
28361
+ "This skill has active scheduled jobs. Delete or reassign them first."
27734
28362
  );
27735
28363
  }
27736
- ensureKitsDir();
28364
+ await ensureKitsDir();
27737
28365
  const target = getKitFilePath(id);
27738
28366
  if (!target) {
27739
- return errorResult("Kit id contains unsupported characters.");
28367
+ return errorResult("Skill id contains unsupported characters.");
27740
28368
  }
27741
- if (!fs$1.existsSync(target)) {
27742
- return errorResult("Kit not found.");
28369
+ if (!await pathExists(target)) {
28370
+ return errorResult("Skill not found.");
27743
28371
  }
27744
28372
  try {
27745
- fs$1.unlinkSync(target);
28373
+ await unlink(target);
27746
28374
  return okResult();
27747
28375
  } catch (err) {
27748
- logger$9.warn("Failed to remove kit file:", err);
27749
- return errorResult("Failed to remove the kit file.");
28376
+ logger$9.warn("Failed to remove skill file:", err);
28377
+ return errorResult("Failed to remove the skill file.");
27750
28378
  }
27751
28379
  }
27752
28380
  const logger$8 = createLogger("Scheduler");
@@ -27931,7 +28559,7 @@ async function fireJob(job, windowState2, runtime2) {
27931
28559
  } catch (err) {
27932
28560
  const msg = err instanceof Error ? err.message : "Unknown error";
27933
28561
  appendActivity(`
27934
- [Scheduled Kit Error: ${msg}]`);
28562
+ [Scheduled Skill Error: ${msg}]`);
27935
28563
  finishActivity("failed");
27936
28564
  }
27937
28565
  }
@@ -27989,10 +28617,12 @@ function registerScheduleHandlers(windowState2, runtime2, sendToAll) {
27989
28617
  }, msToNextMinute);
27990
28618
  electron.ipcMain.handle(Channels.SCHEDULE_GET_ALL, (event) => {
27991
28619
  assertTrustedIpcSender(event);
28620
+ assertFeatureUnlocked("automation_kits", "Skills");
27992
28621
  return jobs;
27993
28622
  });
27994
28623
  electron.ipcMain.handle(Channels.SCHEDULE_CREATE, (event, rawJob) => {
27995
28624
  assertTrustedIpcSender(event);
28625
+ assertFeatureUnlocked("automation_kits", "Skills");
27996
28626
  if (!isValidJobData(rawJob)) {
27997
28627
  throw new Error(
27998
28628
  "Invalid job data. Required: kitId, kitName, kitIcon, renderedPrompt, schedule, enabled."
@@ -28011,6 +28641,7 @@ function registerScheduleHandlers(windowState2, runtime2, sendToAll) {
28011
28641
  });
28012
28642
  electron.ipcMain.handle(Channels.SCHEDULE_UPDATE, (event, id, updates) => {
28013
28643
  assertTrustedIpcSender(event);
28644
+ assertFeatureUnlocked("automation_kits", "Skills");
28014
28645
  if (typeof id !== "string") throw new Error("id must be a string");
28015
28646
  const job = jobs.find((j) => j.id === id);
28016
28647
  if (!job) return null;
@@ -28038,6 +28669,7 @@ function registerScheduleHandlers(windowState2, runtime2, sendToAll) {
28038
28669
  });
28039
28670
  electron.ipcMain.handle(Channels.SCHEDULE_DELETE, (event, id) => {
28040
28671
  assertTrustedIpcSender(event);
28672
+ assertFeatureUnlocked("automation_kits", "Skills");
28041
28673
  if (typeof id !== "string") throw new Error("id must be a string");
28042
28674
  const before = jobs.length;
28043
28675
  jobs = jobs.filter((j) => j.id !== id);
@@ -28062,6 +28694,7 @@ function stopScheduler() {
28062
28694
  }
28063
28695
  }
28064
28696
  const KitIdSchema = zod.z.string().min(1);
28697
+ const SkillSourceSchema = zod.z.string().min(1).max(1e5);
28065
28698
  const OriginSchema = zod.z.string().min(1);
28066
28699
  function registerSystemHandlers(windowState2, sendToRendererViews) {
28067
28700
  const { tabManager } = windowState2;
@@ -28078,17 +28711,38 @@ function registerSystemHandlers(windowState2, sendToRendererViews) {
28078
28711
  layoutViews(windowState2);
28079
28712
  return clamped;
28080
28713
  });
28081
- electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, (event) => {
28714
+ electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, async (event) => {
28082
28715
  assertTrustedIpcSender(event);
28083
- return getInstalledKits();
28716
+ assertFeatureUnlocked("automation_kits", "Skills");
28717
+ return await getInstalledKits();
28084
28718
  });
28085
28719
  electron.ipcMain.handle(Channels.AUTOMATION_INSTALL_FROM_FILE, async (event) => {
28086
28720
  assertTrustedIpcSender(event);
28721
+ assertFeatureUnlocked("automation_kits", "Skills");
28087
28722
  return await installKitFromFile();
28088
28723
  });
28089
- electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, (event, id) => {
28724
+ electron.ipcMain.handle(Channels.AUTOMATION_CREATE_FROM_TEXT, async (event, source) => {
28725
+ assertTrustedIpcSender(event);
28726
+ assertFeatureUnlocked("automation_kits", "Skills");
28727
+ return await createKitFromText(
28728
+ parseIpc(SkillSourceSchema, source, "source")
28729
+ );
28730
+ });
28731
+ electron.ipcMain.handle(Channels.AUTOMATION_UPDATE_FROM_TEXT, async (event, id, source) => {
28090
28732
  assertTrustedIpcSender(event);
28091
- return uninstallKit(parseIpc(KitIdSchema, id, "id"), getScheduledKitIds());
28733
+ assertFeatureUnlocked("automation_kits", "Skills");
28734
+ return await updateKitFromText(
28735
+ parseIpc(KitIdSchema, id, "id"),
28736
+ parseIpc(SkillSourceSchema, source, "source")
28737
+ );
28738
+ });
28739
+ electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, async (event, id) => {
28740
+ assertTrustedIpcSender(event);
28741
+ assertFeatureUnlocked("automation_kits", "Skills");
28742
+ return await uninstallKit(
28743
+ parseIpc(KitIdSchema, id, "id"),
28744
+ getScheduledKitIds()
28745
+ );
28092
28746
  });
28093
28747
  electron.ipcMain.handle(Channels.CLEAR_BROWSING_DATA, async (event, options) => {
28094
28748
  assertTrustedIpcSender(event);
@@ -29547,7 +30201,7 @@ function registerAutofillHandlers(windowState2) {
29547
30201
  const activeTab = windowState2.tabManager.getActiveTab();
29548
30202
  const wc = activeTab?.view.webContents;
29549
30203
  if (!wc) throw new Error("No active tab");
29550
- const content = await extractContent$1(wc);
30204
+ const content = await extractContent(wc);
29551
30205
  const elements = content.interactiveElements || [];
29552
30206
  const matches = matchFields(elements, profile);
29553
30207
  if (matches.length === 0) {