@quanta-intellect/vessel-browser 0.1.31 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  </div>
12
12
 
13
13
 
14
- Open source chromium-based browser for persistent web agents on Linux (Mac/Windows support to come).
14
+ Open source chromium-based browser for persistent web agents. Linux is the most mature install target today, and macOS release packaging is available from source.
15
15
 
16
16
  Vessel gives external agent harnesses a real browser with durable state, MCP control, and a human-visible supervisory UI. It is built for long-running workflows where the agent drives and the human audits, intervenes, and redirects when needed.
17
17
 
@@ -200,12 +200,25 @@ npm run dist:dir
200
200
 
201
201
  # Package a Linux AppImage
202
202
  npm run dist
203
+
204
+ # Package an unpacked macOS app bundle (run on macOS)
205
+ npm run dist:mac:dir
206
+
207
+ # Package macOS DMG + ZIP artifacts (run on macOS)
208
+ npm run dist:mac
209
+
210
+ # Package signed macOS DMG + ZIP artifacts (run on macOS with signing set up)
211
+ npm run dist:mac:signed
203
212
  ```
204
213
 
205
214
  Notes:
206
215
 
207
216
  - `npm run dev` still launches the stock Electron binary, so Linux may continue showing the default Electron gear icon in development
208
217
  - packaged builds created with `npm run dist` / `npm run dist:dir` use the Vessel app icon
218
+ - `npm run build:icon:mac` regenerates `resources/vessel-icon.icns` from `resources/vessel-icon.png` for macOS packaging
219
+ - `npm run dist:mac` and `npm run dist:mac:dir` intentionally disable auto-signing so local packaging works on any Mac without keychain setup
220
+ - `npm run dist:mac:signed` and `npm run dist:mac:dir:signed` use normal `electron-builder` signing discovery; if your login keychain has duplicate Apple certs, clean those up or use a dedicated keychain before running the signed path
221
+ - signed builds are still not notarized by this repo out of the box, so Gatekeeper warnings remain until notarization is added for release publishing
209
222
  - the tracked smoke test runs typecheck, build, the MCP stdio proxy regression check, and the Electron navigation regression harness
210
223
  - for headless CI, run the smoke test under `xvfb-run -a npm run smoke:test`
211
224
 
package/out/main/index.js CHANGED
@@ -11509,6 +11509,325 @@ async function submitForm$1(wc, args) {
11509
11509
  async function clickElementBySelector(wc, selector) {
11510
11510
  return clickResolvedSelector$1(wc, selector);
11511
11511
  }
11512
+ const HUGGING_FACE_HUB_HOSTS = /* @__PURE__ */ new Set(["huggingface.co", "www.huggingface.co"]);
11513
+ const HUGGING_FACE_MODEL_TASKS = [
11514
+ {
11515
+ value: "automatic-speech-recognition",
11516
+ label: "automatic speech recognition",
11517
+ phrases: ["automatic speech recognition", "speech recognition", "asr"]
11518
+ },
11519
+ {
11520
+ value: "text-classification",
11521
+ label: "text classification",
11522
+ phrases: ["text classification", "classification"]
11523
+ },
11524
+ {
11525
+ value: "token-classification",
11526
+ label: "token classification",
11527
+ phrases: ["token classification", "named entity recognition", "ner"]
11528
+ },
11529
+ {
11530
+ value: "question-answering",
11531
+ label: "question answering",
11532
+ phrases: ["question answering", "qa"]
11533
+ },
11534
+ {
11535
+ value: "sentence-similarity",
11536
+ label: "sentence similarity",
11537
+ phrases: ["sentence similarity", "semantic similarity"]
11538
+ },
11539
+ {
11540
+ value: "feature-extraction",
11541
+ label: "feature extraction",
11542
+ phrases: ["feature extraction", "embeddings", "embedding"]
11543
+ },
11544
+ {
11545
+ value: "image-classification",
11546
+ label: "image classification",
11547
+ phrases: ["image classification"]
11548
+ },
11549
+ {
11550
+ value: "object-detection",
11551
+ label: "object detection",
11552
+ phrases: ["object detection"]
11553
+ },
11554
+ {
11555
+ value: "image-to-text",
11556
+ label: "image to text",
11557
+ phrases: ["image to text", "image-to-text", "ocr"]
11558
+ },
11559
+ {
11560
+ value: "text-to-image",
11561
+ label: "text to image",
11562
+ phrases: ["text to image", "text-to-image"]
11563
+ },
11564
+ {
11565
+ value: "text-to-speech",
11566
+ label: "text to speech",
11567
+ phrases: ["text to speech", "text-to-speech", "tts"]
11568
+ },
11569
+ {
11570
+ value: "text-generation",
11571
+ label: "text generation",
11572
+ phrases: [
11573
+ "text generation",
11574
+ "text-generation",
11575
+ "llm",
11576
+ "chat model",
11577
+ "chat models",
11578
+ "instruct model",
11579
+ "instruct models"
11580
+ ]
11581
+ },
11582
+ {
11583
+ value: "text2text-generation",
11584
+ label: "text2text generation",
11585
+ phrases: [
11586
+ "text2text generation",
11587
+ "text-to-text generation",
11588
+ "text to text generation"
11589
+ ]
11590
+ },
11591
+ {
11592
+ value: "summarization",
11593
+ label: "summarization",
11594
+ phrases: ["summarization", "summarize", "summarizer"]
11595
+ },
11596
+ {
11597
+ value: "translation",
11598
+ label: "translation",
11599
+ phrases: ["translation", "translate", "translator"]
11600
+ }
11601
+ ];
11602
+ const HUGGING_FACE_MODEL_LIBRARIES = [
11603
+ { value: "sentence-transformers", label: "sentence-transformers", phrases: ["sentence-transformers"] },
11604
+ { value: "transformers.js", label: "transformers.js", phrases: ["transformers.js"] },
11605
+ { value: "transformers", label: "transformers", phrases: ["transformers"] },
11606
+ { value: "diffusers", label: "diffusers", phrases: ["diffusers"] },
11607
+ { value: "safetensors", label: "safetensors", phrases: ["safetensors"] },
11608
+ { value: "pytorch", label: "pytorch", phrases: ["pytorch"] },
11609
+ { value: "tf", label: "tensorflow", phrases: ["tensorflow", "tf"] },
11610
+ { value: "jax", label: "jax", phrases: ["jax"] },
11611
+ { value: "onnx", label: "onnx", phrases: ["onnx"] },
11612
+ { value: "gguf", label: "gguf", phrases: ["gguf"] },
11613
+ { value: "mlx", label: "mlx", phrases: ["mlx"] }
11614
+ ];
11615
+ function escapeRegex(value) {
11616
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11617
+ }
11618
+ function stripSearchPhrase(query, phrase) {
11619
+ return query.replace(
11620
+ new RegExp(`(^|[^\\w])${escapeRegex(phrase)}(?=[^\\w]|$)`, "gi"),
11621
+ " "
11622
+ );
11623
+ }
11624
+ function collapseSearchTerms(query) {
11625
+ return query.replace(/\b(on|in|for|with|without|from|by|to|of|the|a|an|and|or)\b/gi, " ").replace(/\b(find|show|search|browse|look(?:ing)?(?:\s+for)?|need|want)\b/gi, " ").replace(/\bhugging\s*face\b/gi, " ").replace(/\s+/g, " ").trim();
11626
+ }
11627
+ function normalizeSearchQuery(query) {
11628
+ return query.replace(/\s+/g, " ").trim();
11629
+ }
11630
+ function extractFirstMatchingFilter(query, filters) {
11631
+ for (const filter of filters) {
11632
+ for (const phrase of filter.phrases) {
11633
+ const pattern = new RegExp(
11634
+ `(^|[^\\w])${escapeRegex(phrase)}(?=[^\\w]|$)`,
11635
+ "i"
11636
+ );
11637
+ if (!pattern.test(query)) continue;
11638
+ return {
11639
+ match: { value: filter.value, label: filter.label },
11640
+ remainingQuery: stripSearchPhrase(query, phrase)
11641
+ };
11642
+ }
11643
+ }
11644
+ return { match: null, remainingQuery: query };
11645
+ }
11646
+ function mapHuggingFaceParameterBucket(query) {
11647
+ const match = query.match(/\b(\d+(?:\.\d+)?)\s*(b|bn|billion|t|tn|trillion)\b/i);
11648
+ if (!match) {
11649
+ return { value: null, label: null, remainingQuery: query };
11650
+ }
11651
+ const amount = Number(match[1]);
11652
+ if (!Number.isFinite(amount) || amount <= 0) {
11653
+ return { value: null, label: null, remainingQuery: query };
11654
+ }
11655
+ const unit = match[2].toLowerCase();
11656
+ const billions = unit === "t" || unit === "tn" || unit === "trillion" ? amount * 1e3 : amount;
11657
+ let value;
11658
+ if (billions < 1) value = "n<1B";
11659
+ else if (billions < 3) value = "1B<n<3B";
11660
+ else if (billions < 6) value = "3B<n<6B";
11661
+ else if (billions < 9) value = "6B<n<9B";
11662
+ else if (billions < 12) value = "9B<n<12B";
11663
+ else if (billions < 24) value = "12B<n<24B";
11664
+ else if (billions < 32) value = "24B<n<32B";
11665
+ else if (billions < 64) value = "32B<n<64B";
11666
+ else if (billions < 128) value = "64B<n<128B";
11667
+ else if (billions < 256) value = "128B<n<256B";
11668
+ else if (billions < 500) value = "256B<n<500B";
11669
+ else if (billions < 1e3) value = "500B<n<1T";
11670
+ else value = "n>1T";
11671
+ return {
11672
+ value,
11673
+ label: `${amount}${unit.startsWith("t") ? "T" : "B"} size bucket`,
11674
+ remainingQuery: query.replace(match[0], " ")
11675
+ };
11676
+ }
11677
+ function chooseHuggingFaceSection(url, query) {
11678
+ const pathname = url.pathname.toLowerCase();
11679
+ const normalized = query.toLowerCase();
11680
+ const mentionsDatasets = /\b(dataset|datasets|corpus|benchmark|benchmarks)\b/.test(
11681
+ normalized
11682
+ );
11683
+ const mentionsSpaces = /\b(space|spaces|app|apps|demo|demos|gradio|streamlit)\b/.test(
11684
+ normalized
11685
+ );
11686
+ const mentionsModels = /\b(model|models|checkpoint|checkpoints|lora|loras|weights)\b/.test(
11687
+ normalized
11688
+ );
11689
+ if (mentionsDatasets && !mentionsModels && !mentionsSpaces) return "datasets";
11690
+ if (mentionsSpaces && !mentionsModels && !mentionsDatasets) return "spaces";
11691
+ if (mentionsModels && !mentionsDatasets && !mentionsSpaces) return "models";
11692
+ if (pathname.startsWith("/models")) return "models";
11693
+ if (pathname.startsWith("/datasets")) return "datasets";
11694
+ if (pathname.startsWith("/spaces")) return "spaces";
11695
+ if (HUGGING_FACE_MODEL_TASKS.some(
11696
+ (entry) => entry.phrases.some(
11697
+ (phrase) => new RegExp(`(^|[^\\w])${escapeRegex(phrase)}(?=[^\\w]|$)`, "i").test(
11698
+ normalized
11699
+ )
11700
+ )
11701
+ ) || HUGGING_FACE_MODEL_LIBRARIES.some(
11702
+ (entry) => entry.phrases.some(
11703
+ (phrase) => new RegExp(`(^|[^\\w])${escapeRegex(phrase)}(?=[^\\w]|$)`, "i").test(
11704
+ normalized
11705
+ )
11706
+ )
11707
+ ) || /\b\d+(?:\.\d+)?\s*(b|bn|billion|t|tn|trillion)\b/i.test(normalized)) {
11708
+ return "models";
11709
+ }
11710
+ return null;
11711
+ }
11712
+ function buildHuggingFaceSearchShortcut(currentUrl, rawQuery) {
11713
+ let url;
11714
+ try {
11715
+ url = new URL(currentUrl);
11716
+ } catch {
11717
+ return null;
11718
+ }
11719
+ const hostname = url.hostname.toLowerCase();
11720
+ if (!HUGGING_FACE_HUB_HOSTS.has(hostname)) {
11721
+ return null;
11722
+ }
11723
+ const query = rawQuery.trim();
11724
+ if (!query) return null;
11725
+ const section = chooseHuggingFaceSection(url, query);
11726
+ if (!section) return null;
11727
+ let remainingQuery = query;
11728
+ const target = new URL(`https://huggingface.co/${section}`);
11729
+ const appliedFilters = [];
11730
+ if (section === "models") {
11731
+ const taskResult = extractFirstMatchingFilter(
11732
+ remainingQuery,
11733
+ HUGGING_FACE_MODEL_TASKS
11734
+ );
11735
+ remainingQuery = taskResult.remainingQuery;
11736
+ if (taskResult.match) {
11737
+ target.searchParams.append("pipeline_tag", taskResult.match.value);
11738
+ appliedFilters.push(`task: ${taskResult.match.label}`);
11739
+ }
11740
+ const libraryResult = extractFirstMatchingFilter(
11741
+ remainingQuery,
11742
+ HUGGING_FACE_MODEL_LIBRARIES
11743
+ );
11744
+ remainingQuery = libraryResult.remainingQuery;
11745
+ if (libraryResult.match) {
11746
+ target.searchParams.append("library", libraryResult.match.value);
11747
+ appliedFilters.push(`library: ${libraryResult.match.label}`);
11748
+ }
11749
+ const parameterResult = mapHuggingFaceParameterBucket(remainingQuery);
11750
+ remainingQuery = parameterResult.remainingQuery;
11751
+ if (parameterResult.value && parameterResult.label) {
11752
+ target.searchParams.append("num_parameters", parameterResult.value);
11753
+ appliedFilters.push(parameterResult.label);
11754
+ }
11755
+ }
11756
+ remainingQuery = collapseSearchTerms(
11757
+ remainingQuery.replace(/\bmodels?\b/gi, " ").replace(/\bdatasets?\b/gi, " ").replace(/\bspaces?\b/gi, " ").replace(/\bapps?\b/gi, " ")
11758
+ );
11759
+ if (remainingQuery) {
11760
+ target.searchParams.set("search", remainingQuery);
11761
+ }
11762
+ if (!remainingQuery && appliedFilters.length === 0) {
11763
+ return null;
11764
+ }
11765
+ if (section === "spaces" && remainingQuery) {
11766
+ target.searchParams.set("includeNonRunning", "true");
11767
+ }
11768
+ return {
11769
+ url: target.toString(),
11770
+ source: "Hugging Face",
11771
+ section,
11772
+ appliedFilters
11773
+ };
11774
+ }
11775
+ const COMMON_SEARCH_QUERY_PARAMS = [
11776
+ "search",
11777
+ "q",
11778
+ "query",
11779
+ "keyword",
11780
+ "keywords",
11781
+ "term",
11782
+ "text"
11783
+ ];
11784
+ const COMMON_PAGINATION_PARAMS = [
11785
+ "p",
11786
+ "page",
11787
+ "offset",
11788
+ "start",
11789
+ "cursor",
11790
+ "skip"
11791
+ ];
11792
+ function looksLikeSearchResultsPath(pathname) {
11793
+ return /\/(search|results|browse|discover|find)(\/|$)/i.test(pathname);
11794
+ }
11795
+ function buildCommonSearchUrlShortcut(currentUrl, rawQuery) {
11796
+ let url;
11797
+ try {
11798
+ url = new URL(currentUrl);
11799
+ } catch {
11800
+ return null;
11801
+ }
11802
+ if (!/^https?:$/i.test(url.protocol)) {
11803
+ return null;
11804
+ }
11805
+ const query = normalizeSearchQuery(rawQuery);
11806
+ if (!query) return null;
11807
+ const existingParam = COMMON_SEARCH_QUERY_PARAMS.find(
11808
+ (param) => url.searchParams.has(param)
11809
+ );
11810
+ if (!existingParam && !looksLikeSearchResultsPath(url.pathname)) {
11811
+ return null;
11812
+ }
11813
+ const target = new URL(url.toString());
11814
+ const searchParam = existingParam ?? "q";
11815
+ target.searchParams.set(searchParam, query);
11816
+ for (const param of COMMON_PAGINATION_PARAMS) {
11817
+ target.searchParams.delete(param);
11818
+ }
11819
+ if (target.toString() === url.toString()) {
11820
+ return null;
11821
+ }
11822
+ return {
11823
+ url: target.toString(),
11824
+ source: "page URL",
11825
+ appliedFilters: existingParam ? [`updated ${existingParam} query`] : []
11826
+ };
11827
+ }
11828
+ function buildSearchShortcut(currentUrl, rawQuery) {
11829
+ return buildHuggingFaceSearchShortcut(currentUrl, rawQuery) ?? buildCommonSearchUrlShortcut(currentUrl, rawQuery);
11830
+ }
11512
11831
  async function locateSearchTarget(wc, explicitSelector) {
11513
11832
  if (explicitSelector) {
11514
11833
  return { selector: explicitSelector, submitSelector: null };
@@ -11775,6 +12094,19 @@ async function searchPage(wc, args) {
11775
12094
  if (buttonLikePatterns.some((p) => queryLower.includes(p))) {
11776
12095
  return `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`;
11777
12096
  }
12097
+ if (typeof args.selector !== "string") {
12098
+ const shortcut = buildSearchShortcut(wc.getURL(), query);
12099
+ if (shortcut) {
12100
+ const beforeUrl2 = wc.getURL();
12101
+ assertSafeURL(shortcut.url);
12102
+ wc.loadURL(shortcut.url);
12103
+ await waitForPotentialNavigation$1(wc, beforeUrl2, 4e3);
12104
+ const afterUrl2 = wc.getURL();
12105
+ const applied = shortcut.appliedFilters.length > 0 ? ` (${shortcut.appliedFilters.join(", ")})` : "";
12106
+ const destination = shortcut.section ? ` ${shortcut.section}` : "";
12107
+ return `Searched "${query}" via ${shortcut.source}${destination} shortcut${applied} → ${afterUrl2}`;
12108
+ }
12109
+ }
11778
12110
  const searchInfo = await locateSearchTarget(
11779
12111
  wc,
11780
12112
  typeof args.selector === "string" ? args.selector : void 0
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@quanta-intellect/vessel-browser",
3
3
  "mcpName": "io.github.unmodeled-tyler/vessel-browser",
4
- "version": "0.1.31",
5
- "description": "AI-native web browser for Linux — persistent browser runtime for autonomous agents with human supervision",
4
+ "version": "0.1.32",
5
+ "description": "AI-native web browser runtime for autonomous agents with human supervision",
6
6
  "main": "./out/main/index.js",
7
7
  "bin": {
8
8
  "vessel-browser": "./bin/vessel-browser.js"
@@ -18,8 +18,13 @@
18
18
  "scripts": {
19
19
  "dev": "ELECTRON_DISABLE_SANDBOX=1 electron-vite dev",
20
20
  "build": "electron-vite build",
21
+ "build:icon:mac": "scripts/build-macos-icon.sh",
21
22
  "dist": "npm run build && electron-builder --linux --publish never",
22
23
  "dist:dir": "npm run build && electron-builder --linux dir --publish never",
24
+ "dist:mac": "npm run build:icon:mac && npm run build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac dmg zip --publish never",
25
+ "dist:mac:dir": "npm run build:icon:mac && npm run build && CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac dir --publish never",
26
+ "dist:mac:signed": "npm run build:icon:mac && npm run build && electron-builder --mac dmg zip --publish never",
27
+ "dist:mac:dir:signed": "npm run build:icon:mac && npm run build && electron-builder --mac dir --publish never",
23
28
  "preview": "electron-vite preview",
24
29
  "typecheck": "tsc --noEmit",
25
30
  "prepublishOnly": "npm run build",