@quanta-intellect/vessel-browser 0.1.137 → 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);
16771
17055
  }
16772
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);
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
+ });
16773
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
+ };
17158
+ }
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(
@@ -22177,6 +22709,13 @@ function broadcastState(tabManager) {
22177
22709
  stateListener(getDevToolsPanelState(tabId));
22178
22710
  }
22179
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
+ );
22718
+ }
22180
22719
  const activityEntry = {
22181
22720
  id: ++activityCounter,
22182
22721
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -23034,7 +23573,7 @@ async function getPostActionState(tabManager, name) {
23034
23573
  if (navActions.includes(name)) {
23035
23574
  let warning = "";
23036
23575
  try {
23037
- const page = await extractContent$1(wc);
23576
+ const page = await extractContent(wc);
23038
23577
  const issue = getRecoverableAccessIssue(page);
23039
23578
  if (issue) {
23040
23579
  const blockedUrl = wc.getURL();
@@ -23900,7 +24439,7 @@ function registerMemoryTools(server, tabManager, runtime2) {
23900
24439
  "memory_page_capture",
23901
24440
  { title, folder, tags },
23902
24441
  async () => {
23903
- const page = await extractContent$1(tab.view.webContents);
24442
+ const page = await extractContent(tab.view.webContents);
23904
24443
  const saved = await capturePageToVault({
23905
24444
  page,
23906
24445
  title,
@@ -24667,7 +25206,7 @@ function registerTools(server, tabManager, runtime2) {
24667
25206
  const wc = activeTab.view.webContents;
24668
25207
  pageUrl = wc.getURL();
24669
25208
  pageTitle = wc.getTitle();
24670
- const page = await extractContent$1(wc);
25209
+ const page = await extractContent(wc);
24671
25210
  pageType = detectPageType(page);
24672
25211
  } catch (err) {
24673
25212
  logger$b.warn("Failed to detect page type for tool scoring, falling back to GENERAL:", err);
@@ -24833,7 +25372,7 @@ ${buildScopedContext(pageContent, mode)}`;
24833
25372
  const tab = tabManager.getActiveTab();
24834
25373
  if (!tab) return asNoActiveTabResponse();
24835
25374
  try {
24836
- const pageContent = await extractContent$1(tab.view.webContents);
25375
+ const pageContent = await extractContent(tab.view.webContents);
24837
25376
  const effectiveMode = mode || "full";
24838
25377
  return asTextResponse(
24839
25378
  await buildExtractResponse(
@@ -24865,7 +25404,7 @@ ${buildScopedContext(pageContent, mode)}`;
24865
25404
  const tab = tabManager.getActiveTab();
24866
25405
  if (!tab) return asNoActiveTabResponse();
24867
25406
  try {
24868
- const pageContent = await extractContent$1(tab.view.webContents);
25407
+ const pageContent = await extractContent(tab.view.webContents);
24869
25408
  const effectiveMode = mode || "full";
24870
25409
  return asTextResponse(
24871
25410
  await buildExtractResponse(
@@ -25000,7 +25539,7 @@ ${buildScopedContext(pageContent, mode)}`;
25000
25539
  const tab = tabManager.getActiveTab();
25001
25540
  if (!tab) return asNoActiveTabResponse();
25002
25541
  try {
25003
- const pageContent = await extractContent$1(tab.view.webContents);
25542
+ const pageContent = await extractContent(tab.view.webContents);
25004
25543
  const requestedType = typeof type === "string" && type.trim() ? type.trim().toLowerCase() : "";
25005
25544
  const entities = (pageContent.structuredData ?? []).filter(
25006
25545
  (entity) => requestedType ? entity.types.some(
@@ -26082,7 +26621,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
26082
26621
  const wc = tab.view.webContents;
26083
26622
  let page;
26084
26623
  try {
26085
- page = await extractContent$1(wc);
26624
+ page = await extractContent(wc);
26086
26625
  } catch (err) {
26087
26626
  logger$b.warn("Failed to extract page while generating suggestions:", err);
26088
26627
  return asTextResponse(
@@ -27643,11 +28182,7 @@ const VALID_KIT_CATEGORIES = /* @__PURE__ */ new Set([
27643
28182
  "productivity",
27644
28183
  "forms"
27645
28184
  ]);
27646
- const BUNDLED_KIT_IDS = /* @__PURE__ */ new Set([
27647
- "research-collect",
27648
- "price-scout",
27649
- "form-filler"
27650
- ]);
28185
+ const BUNDLED_KIT_IDS = /* @__PURE__ */ new Set();
27651
28186
  const KIT_ID_UNSAFE_CHAR_PATTERN = /[/\\\0]/;
27652
28187
  function isSafeAutomationKitId(id) {
27653
28188
  return id.length > 0 && !KIT_ID_UNSAFE_CHAR_PATTERN.test(id);
@@ -27697,18 +28232,18 @@ async function getInstalledKits() {
27697
28232
  if (isValidKit(parsed)) {
27698
28233
  kits.push(parsed);
27699
28234
  } else {
27700
- logger$9.warn(`Skipping invalid kit file: ${file}`);
28235
+ logger$9.warn(`Skipping invalid skill file: ${file}`);
27701
28236
  }
27702
28237
  } catch (err) {
27703
- logger$9.warn(`Failed to read kit file: ${file}`, err);
28238
+ logger$9.warn(`Failed to read skill file: ${file}`, err);
27704
28239
  }
27705
28240
  }
27706
28241
  return kits;
27707
28242
  }
27708
28243
  async function installKitFromFile() {
27709
28244
  const { canceled, filePaths } = await electron.dialog.showOpenDialog({
27710
- title: "Install Automation Kit",
27711
- filters: [{ name: "Automation Kit", extensions: ["kit.json", "json"] }],
28245
+ title: "Import Skill",
28246
+ filters: [{ name: "Skills", extensions: ["skill.json", "json"] }],
27712
28247
  properties: ["openFile"]
27713
28248
  });
27714
28249
  if (canceled || filePaths.length === 0) {
@@ -27718,62 +28253,128 @@ async function installKitFromFile() {
27718
28253
  try {
27719
28254
  raw = await readFile(filePaths[0], "utf-8");
27720
28255
  } catch (err) {
27721
- logger$9.warn("Failed to read selected kit file:", err);
28256
+ logger$9.warn("Failed to read selected skill file:", err);
27722
28257
  return errorResult("Could not read the selected file.");
27723
28258
  }
27724
28259
  let parsed;
27725
28260
  try {
27726
28261
  parsed = JSON.parse(raw);
27727
28262
  } catch (err) {
27728
- logger$9.warn("Selected kit file is not valid JSON:", err);
28263
+ logger$9.warn("Selected skill file is not valid JSON:", err);
27729
28264
  return errorResult("File is not valid JSON.");
27730
28265
  }
27731
28266
  if (!isValidKit(parsed)) {
27732
28267
  return errorResult(
27733
- "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."
27734
28300
  );
27735
28301
  }
27736
28302
  if (BUNDLED_KIT_IDS.has(parsed.id)) {
27737
28303
  return errorResult(
27738
- `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.`
27739
28305
  );
27740
28306
  }
27741
28307
  await ensureKitsDir();
27742
28308
  const dest = getKitFilePath(parsed.id);
27743
28309
  if (!dest) {
27744
- return errorResult("Kit id contains unsupported characters.");
28310
+ return errorResult("Skill id contains unsupported characters.");
27745
28311
  }
27746
28312
  try {
27747
28313
  await writeFile(dest, JSON.stringify(parsed, null, 2), "utf-8");
27748
28314
  } catch (err) {
27749
- logger$9.warn("Failed to save kit file:", err);
27750
- return errorResult("Failed to save the kit file.");
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.");
28346
+ }
28347
+ try {
28348
+ await writeFile(target, JSON.stringify(parsed, null, 2), "utf-8");
28349
+ } catch (err) {
28350
+ logger$9.warn("Failed to update skill:", err);
28351
+ return errorResult("Failed to update the skill.");
27751
28352
  }
27752
28353
  return okResult({ kit: parsed });
27753
28354
  }
27754
28355
  async function uninstallKit(id, scheduledKitIds) {
27755
28356
  if (BUNDLED_KIT_IDS.has(id)) {
27756
- return errorResult("Built-in kits cannot be removed.");
28357
+ return errorResult("Built-in skills cannot be removed.");
27757
28358
  }
27758
28359
  if (scheduledKitIds?.has(id)) {
27759
28360
  return errorResult(
27760
- "This kit has active scheduled jobs. Delete or reassign them first."
28361
+ "This skill has active scheduled jobs. Delete or reassign them first."
27761
28362
  );
27762
28363
  }
27763
28364
  await ensureKitsDir();
27764
28365
  const target = getKitFilePath(id);
27765
28366
  if (!target) {
27766
- return errorResult("Kit id contains unsupported characters.");
28367
+ return errorResult("Skill id contains unsupported characters.");
27767
28368
  }
27768
28369
  if (!await pathExists(target)) {
27769
- return errorResult("Kit not found.");
28370
+ return errorResult("Skill not found.");
27770
28371
  }
27771
28372
  try {
27772
28373
  await unlink(target);
27773
28374
  return okResult();
27774
28375
  } catch (err) {
27775
- logger$9.warn("Failed to remove kit file:", err);
27776
- 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.");
27777
28378
  }
27778
28379
  }
27779
28380
  const logger$8 = createLogger("Scheduler");
@@ -27958,7 +28559,7 @@ async function fireJob(job, windowState2, runtime2) {
27958
28559
  } catch (err) {
27959
28560
  const msg = err instanceof Error ? err.message : "Unknown error";
27960
28561
  appendActivity(`
27961
- [Scheduled Kit Error: ${msg}]`);
28562
+ [Scheduled Skill Error: ${msg}]`);
27962
28563
  finishActivity("failed");
27963
28564
  }
27964
28565
  }
@@ -28016,10 +28617,12 @@ function registerScheduleHandlers(windowState2, runtime2, sendToAll) {
28016
28617
  }, msToNextMinute);
28017
28618
  electron.ipcMain.handle(Channels.SCHEDULE_GET_ALL, (event) => {
28018
28619
  assertTrustedIpcSender(event);
28620
+ assertFeatureUnlocked("automation_kits", "Skills");
28019
28621
  return jobs;
28020
28622
  });
28021
28623
  electron.ipcMain.handle(Channels.SCHEDULE_CREATE, (event, rawJob) => {
28022
28624
  assertTrustedIpcSender(event);
28625
+ assertFeatureUnlocked("automation_kits", "Skills");
28023
28626
  if (!isValidJobData(rawJob)) {
28024
28627
  throw new Error(
28025
28628
  "Invalid job data. Required: kitId, kitName, kitIcon, renderedPrompt, schedule, enabled."
@@ -28038,6 +28641,7 @@ function registerScheduleHandlers(windowState2, runtime2, sendToAll) {
28038
28641
  });
28039
28642
  electron.ipcMain.handle(Channels.SCHEDULE_UPDATE, (event, id, updates) => {
28040
28643
  assertTrustedIpcSender(event);
28644
+ assertFeatureUnlocked("automation_kits", "Skills");
28041
28645
  if (typeof id !== "string") throw new Error("id must be a string");
28042
28646
  const job = jobs.find((j) => j.id === id);
28043
28647
  if (!job) return null;
@@ -28065,6 +28669,7 @@ function registerScheduleHandlers(windowState2, runtime2, sendToAll) {
28065
28669
  });
28066
28670
  electron.ipcMain.handle(Channels.SCHEDULE_DELETE, (event, id) => {
28067
28671
  assertTrustedIpcSender(event);
28672
+ assertFeatureUnlocked("automation_kits", "Skills");
28068
28673
  if (typeof id !== "string") throw new Error("id must be a string");
28069
28674
  const before = jobs.length;
28070
28675
  jobs = jobs.filter((j) => j.id !== id);
@@ -28089,6 +28694,7 @@ function stopScheduler() {
28089
28694
  }
28090
28695
  }
28091
28696
  const KitIdSchema = zod.z.string().min(1);
28697
+ const SkillSourceSchema = zod.z.string().min(1).max(1e5);
28092
28698
  const OriginSchema = zod.z.string().min(1);
28093
28699
  function registerSystemHandlers(windowState2, sendToRendererViews) {
28094
28700
  const { tabManager } = windowState2;
@@ -28107,14 +28713,32 @@ function registerSystemHandlers(windowState2, sendToRendererViews) {
28107
28713
  });
28108
28714
  electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, async (event) => {
28109
28715
  assertTrustedIpcSender(event);
28716
+ assertFeatureUnlocked("automation_kits", "Skills");
28110
28717
  return await getInstalledKits();
28111
28718
  });
28112
28719
  electron.ipcMain.handle(Channels.AUTOMATION_INSTALL_FROM_FILE, async (event) => {
28113
28720
  assertTrustedIpcSender(event);
28721
+ assertFeatureUnlocked("automation_kits", "Skills");
28114
28722
  return await installKitFromFile();
28115
28723
  });
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) => {
28732
+ assertTrustedIpcSender(event);
28733
+ assertFeatureUnlocked("automation_kits", "Skills");
28734
+ return await updateKitFromText(
28735
+ parseIpc(KitIdSchema, id, "id"),
28736
+ parseIpc(SkillSourceSchema, source, "source")
28737
+ );
28738
+ });
28116
28739
  electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, async (event, id) => {
28117
28740
  assertTrustedIpcSender(event);
28741
+ assertFeatureUnlocked("automation_kits", "Skills");
28118
28742
  return await uninstallKit(
28119
28743
  parseIpc(KitIdSchema, id, "id"),
28120
28744
  getScheduledKitIds()
@@ -29577,7 +30201,7 @@ function registerAutofillHandlers(windowState2) {
29577
30201
  const activeTab = windowState2.tabManager.getActiveTab();
29578
30202
  const wc = activeTab?.view.webContents;
29579
30203
  if (!wc) throw new Error("No active tab");
29580
- const content = await extractContent$1(wc);
30204
+ const content = await extractContent(wc);
29581
30205
  const elements = content.interactiveElements || [];
29582
30206
  const matches = matchFields(elements, profile);
29583
30207
  if (matches.length === 0) {