From 991a2a9e2b84deac9c1183b67c43338f26bada0f Mon Sep 17 00:00:00 2001 From: rcholic Date: Mon, 22 Dec 2025 07:39:56 +0000 Subject: [PATCH] chore: sync extension files from sentience-chrome v0.10.3 --- sentience-chrome/background.js | 63 +++++ sentience-chrome/content.js | 22 ++ sentience-chrome/injected_api.js | 438 +++++++++++++++++++++++++++++++ sentience-chrome/manifest.json | 30 +++ sentience-chrome/release.json | 116 ++++++++ sentience-chrome/test-content.js | 4 + 6 files changed, 673 insertions(+) create mode 100644 sentience-chrome/background.js create mode 100644 sentience-chrome/content.js create mode 100644 sentience-chrome/injected_api.js create mode 100644 sentience-chrome/manifest.json create mode 100644 sentience-chrome/release.json create mode 100644 sentience-chrome/test-content.js diff --git a/sentience-chrome/background.js b/sentience-chrome/background.js new file mode 100644 index 00000000..bb5ad6fa --- /dev/null +++ b/sentience-chrome/background.js @@ -0,0 +1,63 @@ +// background.js - Service Worker for screenshot capture +// Chrome extensions can only capture screenshots from the background script +// Listen for screenshot requests from content script +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'captureScreenshot') { + handleScreenshotCapture(sender.tab.id, request.options) + .then(screenshot => { + sendResponse({ success: true, screenshot }); + }) + .catch(error => { + console.error('[Sentience] Screenshot capture failed:', error); + sendResponse({ + success: false, + error: error.message || 'Screenshot capture failed' + }); + }); + + // Return true to indicate we'll send response asynchronously + return true; + } +}); + +/** + * Capture screenshot of the active tab + * @param {number} tabId - Tab ID to capture + * @param {Object} options - Screenshot options + * @returns {Promise} Base64-encoded PNG data URL + */ +async function handleScreenshotCapture(tabId, options = {}) { + try { + const { + format = 'png', // 'png' or 'jpeg' + quality = 90 // JPEG quality (0-100), ignored for PNG + } = options; + + // Capture visible tab as data URL + const dataUrl = await chrome.tabs.captureVisibleTab(null, { + format: format, + quality: quality + }); + + console.log(`[Sentience] Screenshot captured: ${format}, size: ${dataUrl.length} bytes`); + + return dataUrl; + } catch (error) { + console.error('[Sentience] Screenshot error:', error); + throw new Error(`Failed to capture screenshot: ${error.message}`); + } +} + +/** + * Optional: Add viewport-specific capture (requires additional setup) + * This would allow capturing specific regions, not just visible area + */ +async function captureRegion(tabId, region) { + // For region capture, you'd need to: + // 1. Capture full visible tab + // 2. Use Canvas API to crop to region + // 3. Return cropped image + + // Not implemented in this basic version + throw new Error('Region capture not yet implemented'); +} diff --git a/sentience-chrome/content.js b/sentience-chrome/content.js new file mode 100644 index 00000000..de24fa5a --- /dev/null +++ b/sentience-chrome/content.js @@ -0,0 +1,22 @@ +// content.js - ISOLATED WORLD +console.log('[Sentience] Bridge loaded.'); + +// 1. Pass Extension ID to Main World (So WASM knows where to load from) +document.documentElement.dataset.sentienceExtensionId = chrome.runtime.id; + +// 2. Proxy for Screenshots (The only thing Isolated World needs to do) +window.addEventListener('message', (event) => { + // Security check: only accept messages from same window + if (event.source !== window || event.data.type !== 'SENTIENCE_SCREENSHOT_REQUEST') return; + + chrome.runtime.sendMessage( + { action: 'captureScreenshot', options: event.data.options }, + (response) => { + window.postMessage({ + type: 'SENTIENCE_SCREENSHOT_RESULT', + requestId: event.data.requestId, + screenshot: response?.success ? response.screenshot : null + }, '*'); + } + ); +}); \ No newline at end of file diff --git a/sentience-chrome/injected_api.js b/sentience-chrome/injected_api.js new file mode 100644 index 00000000..8f6eb156 --- /dev/null +++ b/sentience-chrome/injected_api.js @@ -0,0 +1,438 @@ +// injected_api.js - MAIN WORLD +(async () => { + // 1. Get Extension ID (Wait for content.js to set it) + const getExtensionId = () => document.documentElement.dataset.sentienceExtensionId; + let extId = getExtensionId(); + + // Safety poller for async loading race conditions + if (!extId) { + await new Promise(resolve => { + const check = setInterval(() => { + extId = getExtensionId(); + if (extId) { clearInterval(check); resolve(); } + }, 50); + }); + } + + const EXT_URL = `chrome-extension://${extId}/`; + console.log('[SentienceAPI.com] Initializing from:', EXT_URL); + + window.sentience_registry = []; + let wasmModule = null; + + // --- HELPER: Deep Walker --- + function getAllElements(root = document) { + const elements = []; + const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + while(walker.nextNode()) { + const node = walker.currentNode; + elements.push(node); + if (node.shadowRoot) elements.push(...getAllElements(node.shadowRoot)); + } + return elements; + } + + // --- HELPER: Smart Text Extractor --- + function getText(el) { + if (el.getAttribute('aria-label')) return el.getAttribute('aria-label'); + if (el.tagName === 'INPUT') return el.value || el.placeholder || ''; + if (el.tagName === 'IMG') return el.alt || ''; + return (el.innerText || '').replace(/\s+/g, ' ').trim().substring(0, 100); + } + + // --- HELPER: Viewport Check (NEW) --- + function isInViewport(rect) { + return ( + rect.top < window.innerHeight && rect.bottom > 0 && + rect.left < window.innerWidth && rect.right > 0 + ); + } + + // --- HELPER: Occlusion Check (NEW) --- + function isOccluded(el, rect) { + // Fast center-point check + const cx = rect.x + rect.width / 2; + const cy = rect.y + rect.height / 2; + + // If point is off-screen, elementFromPoint returns null, assume NOT occluded for safety + if (cx < 0 || cx > window.innerWidth || cy < 0 || cy > window.innerHeight) return false; + + const topEl = document.elementFromPoint(cx, cy); + if (!topEl) return false; + + // It's visible if the top element is us, or contains us, or we contain it + return !(el === topEl || el.contains(topEl) || topEl.contains(el)); + } + + // --- HELPER: Screenshot Bridge --- + function captureScreenshot(options) { + return new Promise(resolve => { + const requestId = Math.random().toString(36).substring(7); + const listener = (e) => { + if (e.data.type === 'SENTIENCE_SCREENSHOT_RESULT' && e.data.requestId === requestId) { + window.removeEventListener('message', listener); + resolve(e.data.screenshot); + } + }; + window.addEventListener('message', listener); + window.postMessage({ type: 'SENTIENCE_SCREENSHOT_REQUEST', requestId, options }, '*'); + }); + } + + // --- HELPER: Get Raw HTML for Turndown/External Processing --- + // Returns cleaned HTML that can be processed by Turndown or other Node.js libraries + function getRawHTML(root) { + const sourceRoot = root || document.body; + const clone = sourceRoot.cloneNode(true); + + // Remove unwanted elements by tag name (simple and reliable) + const unwantedTags = ['nav', 'footer', 'header', 'script', 'style', 'noscript', 'iframe', 'svg']; + unwantedTags.forEach(tag => { + const elements = clone.querySelectorAll(tag); + elements.forEach(el => { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + }); + }); + + // Remove invisible elements from original DOM and find matching ones in clone + // We'll use a simple approach: mark elements in original, then remove from clone + const invisibleSelectors = []; + const walker = document.createTreeWalker( + sourceRoot, + NodeFilter.SHOW_ELEMENT, + null, + false + ); + + let node; + while (node = walker.nextNode()) { + const tag = node.tagName.toLowerCase(); + if (tag === 'head' || tag === 'title') continue; + + const style = window.getComputedStyle(node); + if (style.display === 'none' || style.visibility === 'hidden' || + (node.offsetWidth === 0 && node.offsetHeight === 0)) { + // Build a selector for this element + let selector = tag; + if (node.id) { + selector = `#${node.id}`; + } else if (node.className && typeof node.className === 'string') { + const classes = node.className.trim().split(/\s+/).filter(c => c); + if (classes.length > 0) { + selector = `${tag}.${classes.join('.')}`; + } + } + invisibleSelectors.push(selector); + } + } + + // Remove invisible elements from clone (if we can find them) + invisibleSelectors.forEach(selector => { + try { + const elements = clone.querySelectorAll(selector); + elements.forEach(el => { + if (el.parentNode) { + el.parentNode.removeChild(el); + } + }); + } catch (e) { + // Invalid selector, skip + } + }); + + // Resolve relative URLs in links and images + const links = clone.querySelectorAll('a[href]'); + links.forEach(link => { + const href = link.getAttribute('href'); + if (href && !href.startsWith('http://') && !href.startsWith('https://') && !href.startsWith('#')) { + try { + link.setAttribute('href', new URL(href, document.baseURI).href); + } catch (e) { + // Keep original href if URL parsing fails + } + } + }); + + const images = clone.querySelectorAll('img[src]'); + images.forEach(img => { + const src = img.getAttribute('src'); + if (src && !src.startsWith('http://') && !src.startsWith('https://') && !src.startsWith('data:')) { + try { + img.setAttribute('src', new URL(src, document.baseURI).href); + } catch (e) { + // Keep original src if URL parsing fails + } + } + }); + + return clone.innerHTML; + } + + // --- HELPER: Simple Markdown Converter (Lightweight) --- + // Uses getRawHTML() and then converts to markdown for consistency + function convertToMarkdown(root) { + // Get cleaned HTML first + const rawHTML = getRawHTML(root); + + // Create a temporary container to parse the HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = rawHTML; + + let markdown = ''; + let insideLink = false; // Track if we're inside an tag + + function walk(node) { + if (node.nodeType === Node.TEXT_NODE) { + // Keep minimal whitespace to prevent words merging + // Strip newlines inside text nodes to prevent broken links + const text = node.textContent.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' '); + if (text.trim()) markdown += text; + return; + } + + if (node.nodeType !== Node.ELEMENT_NODE) return; + + const tag = node.tagName.toLowerCase(); + + // Prefix + if (tag === 'h1') markdown += '\n# '; + if (tag === 'h2') markdown += '\n## '; + if (tag === 'h3') markdown += '\n### '; + if (tag === 'li') markdown += '\n- '; + // IMPORTANT: Don't add newlines for block elements when inside a link + if (!insideLink && (tag === 'p' || tag === 'div' || tag === 'br')) markdown += '\n'; + if (tag === 'strong' || tag === 'b') markdown += '**'; + if (tag === 'em' || tag === 'i') markdown += '_'; + if (tag === 'a') { + markdown += '['; + insideLink = true; // Mark that we're entering a link + } + + // Children + if (node.shadowRoot) { + Array.from(node.shadowRoot.childNodes).forEach(walk); + } else { + node.childNodes.forEach(walk); + } + + // Suffix + if (tag === 'a') { + // Get absolute URL from href attribute (already resolved in getRawHTML) + const href = node.getAttribute('href'); + if (href) markdown += `](${href})`; + else markdown += ']'; + insideLink = false; // Mark that we're exiting the link + } + if (tag === 'strong' || tag === 'b') markdown += '**'; + if (tag === 'em' || tag === 'i') markdown += '_'; + // IMPORTANT: Don't add newlines for block elements when inside a link (suffix section too) + if (!insideLink && (tag === 'h1' || tag === 'h2' || tag === 'h3' || tag === 'p' || tag === 'div')) markdown += '\n'; + } + + walk(tempDiv); + + // Cleanup: remove excessive newlines + return markdown.replace(/\n{3,}/g, '\n\n').trim(); + } + + // --- HELPER: Raw Text Extractor --- + function convertToText(root) { + let text = ''; + function walk(node) { + if (node.nodeType === Node.TEXT_NODE) { + text += node.textContent; + return; + } + if (node.nodeType === Node.ELEMENT_NODE) { + const tag = node.tagName.toLowerCase(); + // Skip nav/footer/header/script/style/noscript/iframe/svg + if (['nav', 'footer', 'header', 'script', 'style', 'noscript', 'iframe', 'svg'].includes(tag)) return; + + const style = window.getComputedStyle(node); + if (style.display === 'none' || style.visibility === 'hidden') return; + + // Block level elements get a newline + const isBlock = style.display === 'block' || style.display === 'flex' || node.tagName === 'P' || node.tagName === 'DIV'; + if (isBlock) text += ' '; + + if (node.shadowRoot) { + Array.from(node.shadowRoot.childNodes).forEach(walk); + } else { + node.childNodes.forEach(walk); + } + + if (isBlock) text += '\n'; + } + } + walk(root || document.body); + return text.replace(/\n{3,}/g, '\n\n').trim(); + } + + // Load WASM + try { + const wasmUrl = EXT_URL + 'pkg/sentience_core.js'; + const module = await import(wasmUrl); + const imports = { + env: { + js_click_element: (id) => { + const el = window.sentience_registry[id]; + if (el) { el.click(); el.focus(); } + } + } + }; + await module.default(undefined, imports); + wasmModule = module; + + // Verify functions are available + if (!wasmModule.analyze_page) { + console.error('[SentienceAPI.com] WASM functions not available'); + } else { + console.log('[SentienceAPI.com] ✓ API Ready!'); + console.log('[SentienceAPI.com] Available functions:', Object.keys(wasmModule).filter(k => k.startsWith('analyze'))); + } + } catch (e) { + console.error('[SentienceAPI.com] WASM Load Failed:', e); + } + + // REMOVED: Headless detection - no longer needed (license system removed) + + // --- GLOBAL API --- + window.sentience = { + // 1. Geometry snapshot (existing) + snapshot: async (options = {}) => { + if (!wasmModule) return { error: "WASM not ready" }; + + const rawData = []; + // Remove textMap as we include text in rawData + window.sentience_registry = []; + + const nodes = getAllElements(); + + nodes.forEach((el, idx) => { + if (!el.getBoundingClientRect) return; + const rect = el.getBoundingClientRect(); + if (rect.width < 5 || rect.height < 5) return; + + window.sentience_registry[idx] = el; + + // Calculate properties for Fat Payload + const textVal = getText(el); + const inView = isInViewport(rect); + // Only check occlusion if visible (Optimization) + const occluded = inView ? isOccluded(el, rect) : false; + + const style = window.getComputedStyle(el); + rawData.push({ + id: idx, + tag: el.tagName.toLowerCase(), + rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + styles: { + display: style.display, + visibility: style.visibility, + opacity: style.opacity, + z_index: style.zIndex || "0", + bg_color: style.backgroundColor, + color: style.color, + cursor: style.cursor, + font_weight: style.fontWeight, + font_size: style.fontSize + }, + attributes: { + role: el.getAttribute('role'), + type_: el.getAttribute('type'), + aria_label: el.getAttribute('aria-label'), + href: el.href, + class: el.className + }, + // Pass to WASM + text: textVal || null, + in_viewport: inView, + is_occluded: occluded + }); + }); + + // FREE TIER: No license checks - extension provides basic geometry data + // Pro/Enterprise tiers will be handled server-side (future work) + + // 1. Get Geometry from WASM + let result; + try { + if (options.limit || options.filter) { + result = wasmModule.analyze_page_with_options(rawData, options); + } else { + result = wasmModule.analyze_page(rawData); + } + } catch (e) { + return { status: "error", error: e.message }; + } + + // Hydration step removed as WASM now returns populated structs + + // Capture Screenshot + let screenshot = null; + if (options.screenshot) { + screenshot = await captureScreenshot(options.screenshot); + } + + // C. Clean up null/undefined fields to save tokens (Your existing cleaner) + const cleanElement = (obj) => { + if (Array.isArray(obj)) { + return obj.map(cleanElement); + } else if (obj !== null && typeof obj === 'object') { + const cleaned = {}; + for (const [key, value] of Object.entries(obj)) { + // Keep boolean false for critical flags if desired, or remove to match Rust defaults + if (value !== null && value !== undefined) { + cleaned[key] = cleanElement(value); + } + } + return cleaned; + } + return obj; + }; + + const cleanedElements = cleanElement(result); + + return { + status: "success", + url: window.location.href, + elements: cleanedElements, + raw_elements: rawData, // Include raw data for server-side processing (safe to expose - no proprietary value) + screenshot: screenshot + }; + }, + // 2. Read Content (New) + read: (options = {}) => { + const format = options.format || 'raw'; // 'raw', 'text', or 'markdown' + let content; + + if (format === 'raw') { + // Return raw HTML suitable for Turndown or other Node.js libraries + content = getRawHTML(document.body); + } else if (format === 'markdown') { + // Return lightweight markdown conversion + content = convertToMarkdown(document.body); + } else { + // Default to text + content = convertToText(document.body); + } + + return { + status: "success", + url: window.location.href, + format: format, + content: content, + length: content.length + }; + }, + + // 3. Action + click: (id) => { + const el = window.sentience_registry[id]; + if (el) { el.click(); el.focus(); return true; } + return false; + } + }; +})(); \ No newline at end of file diff --git a/sentience-chrome/manifest.json b/sentience-chrome/manifest.json new file mode 100644 index 00000000..9d979cb1 --- /dev/null +++ b/sentience-chrome/manifest.json @@ -0,0 +1,30 @@ +{ + "manifest_version": 3, + "name": "Sentience Semantic Visual Grounding Extractor", + "version": "1.0.5", + "description": "Extract semantic visual grounding data from web pages", + "permissions": ["activeTab", "scripting"], + "host_permissions": [""], + "web_accessible_resources": [ + { + "resources": ["pkg/*"], + "matches": [""] + } + ], + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "run_at": "document_start" + }, + { + "matches": [""], + "js": ["injected_api.js"], + "run_at": "document_idle", + "world": "MAIN" + } + ], + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" + } +} \ No newline at end of file diff --git a/sentience-chrome/release.json b/sentience-chrome/release.json new file mode 100644 index 00000000..1cc6f95a --- /dev/null +++ b/sentience-chrome/release.json @@ -0,0 +1,116 @@ +{ + "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/272122068", + "assets_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/272122068/assets", + "upload_url": "https://uploads.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/272122068/assets{?name,label}", + "html_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/tag/v0.10.3", + "id": 272122068, + "author": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "node_id": "RE_kwDOQshiJ84QOEDU", + "tag_name": "v0.10.3", + "target_commitish": "main", + "name": "Release v0.10.3", + "draft": false, + "immutable": false, + "prerelease": false, + "created_at": "2025-12-22T07:06:41Z", + "updated_at": "2025-12-22T07:07:53Z", + "published_at": "2025-12-22T07:07:53Z", + "assets": [ + { + "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/331630535", + "id": 331630535, + "node_id": "RA_kwDOQshiJ84TxEfH", + "name": "extension-files.tar.gz", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/gzip", + "state": "uploaded", + "size": 61175, + "digest": "sha256:21ef5d067ffba9a1dd93630c03b39efd57dabb3d1598543c7e3519ff9d3bf387", + "download_count": 0, + "created_at": "2025-12-22T07:07:53Z", + "updated_at": "2025-12-22T07:07:53Z", + "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v0.10.3/extension-files.tar.gz" + }, + { + "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/331630534", + "id": 331630534, + "node_id": "RA_kwDOQshiJ84TxEfG", + "name": "extension-package.zip", + "label": "", + "uploader": { + "login": "github-actions[bot]", + "id": 41898282, + "node_id": "MDM6Qm90NDE4OTgyODI=", + "avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/github-actions%5Bbot%5D", + "html_url": "https://github.com/apps/github-actions", + "followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers", + "following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}", + "gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions", + "organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs", + "repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos", + "events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}", + "received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events", + "type": "Bot", + "user_view_type": "public", + "site_admin": false + }, + "content_type": "application/zip", + "state": "uploaded", + "size": 63347, + "digest": "sha256:78ca5a87ce41e249a66de63b3fa34aa34268920abe590e412ba6036ec93ceb2f", + "download_count": 0, + "created_at": "2025-12-22T07:07:53Z", + "updated_at": "2025-12-22T07:07:53Z", + "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v0.10.3/extension-package.zip" + } + ], + "tarball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/tarball/v0.10.3", + "zipball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/zipball/v0.10.3", + "body": "## What's Changed\n* release fix by @rcholic in https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/pull/8\n* transferred by @rcholic in https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/pull/9\n* fix build and release by @rcholic in https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/pull/10\n* verify loop by @rcholic in https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/pull/11\n* more robust by @rcholic in https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/pull/12\n\n\n**Full Changelog**: https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/compare/vv0.9.0...v0.10.3", + "mentions_count": 1 +} diff --git a/sentience-chrome/test-content.js b/sentience-chrome/test-content.js new file mode 100644 index 00000000..7ca4ddc9 --- /dev/null +++ b/sentience-chrome/test-content.js @@ -0,0 +1,4 @@ +// test-content.js - Simple test script +console.log('TEST: Extension content script is loading!'); +window.testExtension = true; +alert('Extension loaded!');