@@ -7,2 +7,3 @@ "use strict"; | ||
| exports.extractPageMeta = extractPageMeta; | ||
| exports.extractPageMarkdown = extractPageMarkdown; | ||
| exports.extractSelectorSignal = extractSelectorSignal; | ||
@@ -65,2 +66,9 @@ function delay(ms) { | ||
| } | ||
| async function extractPageMarkdown(tabId, timeoutMs, maxHtmlChars) { | ||
| const result = await executeWithTimeout(tabId, timeoutMs, (cap) => { | ||
| const raw = document.documentElement?.outerHTML || ""; | ||
| return raw.length > cap ? raw.slice(0, cap) : raw; | ||
| }, [maxHtmlChars]); | ||
| return typeof result === "string" ? result : ""; | ||
| } | ||
| async function extractSelectorSignal(tabId, specs, timeoutMs, selectorValueMaxLength) { | ||
@@ -75,2 +83,5 @@ if (!specs.length) { | ||
| const hints = {}; | ||
| const stringCap = typeof maxLen === "number" && maxLen > 0 ? maxLen : 500; | ||
| const htmlCap = typeof maxLen === "number" && maxLen > 0 ? maxLen : 4096; | ||
| const normalizeStringValue = (value, cap) => value.replace(/\s+/g, " ").trim().slice(0, cap); | ||
| for (const raw of rawSpecs) { | ||
@@ -88,2 +99,5 @@ const selector = typeof raw.selector === "string" ? raw.selector : ""; | ||
| const textModes = new Set(["", "contains", "exact", "starts-with"]); | ||
| const styleProps = Array.isArray(raw.styleProps) | ||
| ? raw.styleProps.filter((prop) => typeof prop === "string").map((prop) => prop.trim()).filter(Boolean) | ||
| : []; | ||
| if (!textModes.has(normalizedTextMode)) { | ||
@@ -96,12 +110,2 @@ errors[name] = `Unsupported textMode: ${textMode || "unknown"}`; | ||
| const elements = Array.from(document.querySelectorAll(selector)); | ||
| if (!elements.length) { | ||
| missing.push(name); | ||
| if (selector.includes(":contains(")) { | ||
| hints[name] = "CSS :contains() is not supported; use selector text filters or a different selector."; | ||
| } | ||
| else { | ||
| hints[name] = "No matches found; capture a screenshot for context or adjust the selector."; | ||
| } | ||
| continue; | ||
| } | ||
| const matchesText = (el) => { | ||
@@ -121,2 +125,16 @@ if (!text) { | ||
| const filtered = text ? elements.filter(matchesText) : elements; | ||
| if (attr === "count") { | ||
| values[name] = filtered.length; | ||
| continue; | ||
| } | ||
| if (!elements.length) { | ||
| missing.push(name); | ||
| if (selector.includes(":contains(")) { | ||
| hints[name] = "CSS :contains() is not supported; use selector text filters or a different selector."; | ||
| } | ||
| else { | ||
| hints[name] = "No matches found; capture a screenshot for context or adjust the selector."; | ||
| } | ||
| continue; | ||
| } | ||
| if (!filtered.length) { | ||
@@ -128,33 +146,79 @@ missing.push(name); | ||
| const getValue = (el) => { | ||
| let value = ""; | ||
| if (attr === "text") { | ||
| value = el.textContent || ""; | ||
| return normalizeStringValue(el.textContent || "", stringCap); | ||
| } | ||
| else if (attr === "href-url" || attr === "src-url") { | ||
| if (attr === "html") { | ||
| return normalizeStringValue(el.outerHTML || "", htmlCap); | ||
| } | ||
| if (attr === "href-url" || attr === "src-url") { | ||
| const rawValue = el.getAttribute(attr === "href-url" ? "href" : "src") || ""; | ||
| if (!rawValue) { | ||
| value = ""; | ||
| return ""; | ||
| } | ||
| else { | ||
| try { | ||
| const resolved = new URL(rawValue, document.baseURI); | ||
| if (resolved.protocol === "http:" || resolved.protocol === "https:") { | ||
| value = resolved.toString(); | ||
| } | ||
| else { | ||
| value = ""; | ||
| } | ||
| try { | ||
| const resolved = new URL(rawValue, document.baseURI); | ||
| if (resolved.protocol === "http:" || resolved.protocol === "https:") { | ||
| return normalizeStringValue(resolved.toString(), stringCap); | ||
| } | ||
| catch { | ||
| value = ""; | ||
| } | ||
| return ""; | ||
| } | ||
| catch { | ||
| return ""; | ||
| } | ||
| } | ||
| else { | ||
| value = el.getAttribute(attr) || ""; | ||
| if (attr === "value") { | ||
| if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) { | ||
| return el.value; | ||
| } | ||
| return null; | ||
| } | ||
| return value.replace(/\s+/g, " ").trim().slice(0, maxLen); | ||
| if (attr === "box") { | ||
| const rect = el.getBoundingClientRect(); | ||
| return { | ||
| x: rect.x, | ||
| y: rect.y, | ||
| width: rect.width, | ||
| height: rect.height, | ||
| top: rect.top, | ||
| right: rect.right, | ||
| bottom: rect.bottom, | ||
| left: rect.left, | ||
| }; | ||
| } | ||
| if (attr === "styles") { | ||
| if (!styleProps.length) { | ||
| return {}; | ||
| } | ||
| const computed = window.getComputedStyle(el); | ||
| const selected = {}; | ||
| for (const prop of styleProps) { | ||
| selected[prop] = computed.getPropertyValue(prop); | ||
| } | ||
| return selected; | ||
| } | ||
| if (attr === "visible") { | ||
| const computed = window.getComputedStyle(el); | ||
| const rect = el.getBoundingClientRect(); | ||
| const styleVisible = computed.display !== "none" && computed.visibility !== "hidden" && computed.opacity !== "0"; | ||
| const hasRenderedBox = rect.width > 0 || rect.height > 0; | ||
| return styleVisible && hasRenderedBox; | ||
| } | ||
| if (attr === "enabled") { | ||
| const candidate = el; | ||
| return candidate.disabled !== true && el.getAttribute("aria-disabled") !== "true"; | ||
| } | ||
| if (attr === "checked") { | ||
| if (el instanceof HTMLInputElement) { | ||
| return el.checked; | ||
| } | ||
| return el.getAttribute("aria-checked") === "true"; | ||
| } | ||
| return normalizeStringValue(el.getAttribute(attr) || "", stringCap); | ||
| }; | ||
| if (attr === "box") { | ||
| values[name] = getValue(filtered[0]); | ||
| continue; | ||
| } | ||
| if (all) { | ||
| values[name] = filtered.map(getValue).filter((val) => val.length > 0); | ||
| values[name] = filtered.map(getValue).filter((val) => typeof val !== "string" || val.length > 0); | ||
| } | ||
@@ -161,0 +225,0 @@ else { |
@@ -22,3 +22,3 @@ { | ||
| }, | ||
| "version_name": "0.6.0-rc.5" | ||
| "version_name": "0.6.0-rc.6" | ||
| } |
+4
-2
| { | ||
| "name": "tabctl", | ||
| "version": "0.6.0-rc.5", | ||
| "version": "0.6.0-rc.6", | ||
| "description": "CLI tool to manage and analyze browser tabs", | ||
@@ -45,2 +45,3 @@ "license": "MIT", | ||
| "test:integration": "cargo test --manifest-path rust/Cargo.toml --test browser_integration --test browser_coverage --test browser_primitives --test local_commands -- --ignored --nocapture --test-threads=1", | ||
| "test:smoke": "node scripts/smoke-test.js", | ||
| "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" ", | ||
@@ -56,7 +57,8 @@ "prepare": "git rev-parse --git-dir >/dev/null 2>&1 && git config core.hooksPath .githooks || true" | ||
| "optionalDependencies": { | ||
| "tabctl-win32-x64": "0.6.0-rc.5" | ||
| "tabctl-win32-x64": "0.6.0-rc.6" | ||
| }, | ||
| "dependencies": { | ||
| "markdown-for-agents": "^1.3.4", | ||
| "normalize-url": "^8.1.1" | ||
| } | ||
| } |
+13
-1
@@ -150,2 +150,7 @@ # tabctl | ||
| Read-only browser features include: | ||
| - `inspectTabs` for page metadata and selector-based reads | ||
| - `readTabs` for page HTML → Markdown conversion with best-effort extraction | ||
| - `reportTabs`, `captureScreenshots`, and browser-state history queries for summaries and context | ||
| See [CLI.md](CLI.md) for the full command reference, options, and examples. | ||
@@ -165,2 +170,8 @@ | ||
| # Inspect selectors with typed attrs and filters | ||
| tabctl query 'query { inspectTabs(windowId: 123, selectors: [{ name: "prices", selector: ".price", attr: "text", all: true }, { name: "buy_now_visible", selector: "button.buy-now", attr: "visible" }, { name: "buy_now_style", selector: "button.buy-now", attr: "styles", styleProps: ["color", "background-color"] }, { name: "review_count", selector: ".review", attr: "count" }, { name: "email_value", selector: "input[type=email]", attr: "value" }]) { entries { tabId url signals { name valueJson } } } }' | ||
| # Read tab content as Markdown (best-effort extraction by default) | ||
| tabctl query 'query { readTabs(windowId: 123, extract: true, maxChars: 30000) { entries { tabId title url markdown chars truncated extracted error } } }' | ||
| # Generate reports | ||
@@ -420,5 +431,6 @@ tabctl query '{ reportTabs(windowId: 123) { entries { tabId title url description } } }' | ||
| - Reports include short descriptions from page metadata and a fallback snippet. | ||
| - `inspectTabs` supports `page-meta` and `selector` signals. | ||
| - `inspectTabs` supports `page-meta` plus selector reads with `all`, `text`, `textMode`, `styleProps`, and attrs such as `html`, `value`, `count`, `box`, `styles`, `visible`, `enabled`, and `checked`. | ||
| - `readTabs` converts main-frame page HTML to Markdown, with best-effort content extraction enabled by default and status surfaced through `extracted`/`error` fields. | ||
| - `captureScreenshots` returns tile metadata and image data from GraphQL. | ||
| - `undoAction` accepts either an explicit `txid` or `latest: true`. | ||
| - `tabctl history --json` returns a top-level JSON array. |
Sorry, the diff of this file is too big to display
Obfuscated code
Supply chain riskObfuscated files are intentionally packed to hide their behavior. This could be a sign of malware.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
306322
289.47%5364
264.4%434
2.84%3
50%1
Infinity%3
200%7
16.67%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed