Initial commit: userscripts collection with oxlint tooling

This commit is contained in:
2026-07-08 11:59:50 -04:00
commit 672d3d51e1
17 changed files with 2602 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules/
_scratch/
.direnv/
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
"env": {
"browser": true,
"es2024": true,
"greasemonkey": true
},
"globals": {
"GM": "readonly",
"GM_getValue": "readonly",
"GM_setValue": "readonly",
"GM_deleteValue": "readonly",
"GM_addStyle": "readonly",
"GM_openInTab": "readonly",
"GM_xmlhttpRequest": "readonly",
"GM_registerMenuCommand": "readonly",
"GM_unregisterMenuCommand": "readonly",
"unsafeWindow": "readonly"
},
"rules": {
"no-unused-vars": ["warn", { "caughtErrors": "none", "args": "none" }],
"no-debugger": "warn"
},
"ignorePatterns": ["_scratch/**"]
}
+29
View File
@@ -0,0 +1,29 @@
# Userscripts
A flat collection of standalone browser userscripts (Tampermonkey / Violentmonkey / Greasemonkey).
## Core Rule: One Self-Contained File Per Script
Each userscript is a single `*.user.js` file at the repo root. A script owns everything it needs — metadata header, styles, and logic — in that one file.
- No shared modules, no build step, no bundler. A file must install and run as-is by pasting it into a userscript manager.
- New script → new `name.user.js` file. Do not factor common code into helpers across files; duplication across scripts is acceptable and expected here.
- Every file starts with a complete `==UserScript==` metadata block (`@name`, `@namespace`, `@version`, `@description`, `@match`, `@run-at`, and any `@grant`/`@connect`).
## Linting
[oxlint](https://oxc.rs) lints all scripts. It's a pnpm devDependency (pinned in `package.json` / `pnpm-lock.yaml`), not a Nix package. Config: `.oxlintrc.json` (browser + greasemonkey env, GM globals declared, `_scratch/` ignored).
```bash
pnpm lint # lint every *.user.js
```
Warnings on third-party/imported scripts are acceptable signal, not blockers. Keep your own scripts warning-clean.
## Dev Environment
The Nix flake provides `nodejs` + `pnpm`; its `shellHook` runs `pnpm install` and puts `node_modules/.bin` on `PATH`, so `oxlint` is ready automatically.
```bash
nix develop # or: direnv allow (uses .envrc → `use flake`)
```
+48
View File
@@ -0,0 +1,48 @@
// ==UserScript==
// @name Auto Archive Redirect
// @namespace Evan Reichard
// @version 0.0.3
// @match *://*/*
// @grant GM.xmlhttpRequest
// @grant GM.openInTab
// @noframes
// @run-at document-start
// ==/UserScript==
GM.xmlhttpRequest({
url: 'https://archive.is/' + document.location.href,
method: 'GET',
responseType: 'document',
onloadend: processArchiveResponse
});
function processArchiveResponse(response){
const { response: archiveDocument } = response;
const archiveURL = archiveDocument.querySelector('#CONTENT #row0 a')?.href;
if (archiveURL) createArchiveButton(archiveURL);
}
function createArchiveButton(archiveURL){
let myDiv = document.createElement('div')
myDiv.innerHTML = `<span href="#" style="all: initial; display: flex; justify-content: center; cursor: pointer; position: fixed; opacity: 0; transition: 1s; width: 60px; height: 60px; bottom: 40px; right: 40px; background-color: #0C9; color: #FFF; border-radius: 50px; text-align: center; box-shadow: 2px 2px 3px #999;">
<h2 style="all: initial; align-self: center; font-size: 25px; color: white; cursor: pointer;">A</h2>
</span>`;
let spanEl = myDiv.children[0];
document.body.append(spanEl)
spanEl.onclick = () => {
document.location.href = archiveURL;
}
setTimeout(() => {
spanEl.style.opacity = '1';
}, 0);
setTimeout(() => {
spanEl.style.opacity = '0';
}, 5000);
setTimeout(() => {
spanEl.remove();
}, 6000);
}
+108
View File
@@ -0,0 +1,108 @@
// ==UserScript==
// @name DarkReader
// @version 0.0.7
// @match *://*/*
// @downloadURL https://gist.github.com/evanreichard/f983ca26836d4af6ca0f6fb602b9dcaf/raw/darkreader.user.js
// @require https://unpkg.com/darkreader@4.9.46/darkreader.js
// @grant GM.xmlhttpRequest
// @grant GM.registerMenuCommand
// @grant GM.unregisterMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @inject-into content
// @noframes
// @run-at document-start
// ==/UserScript==
/* globals DarkReader */
let options = {
brightness: 70,
contrast: 110,
sepia: 50,
};
let prefersDark =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches;
function toggleHost() {
let disabledHosts = GM_getValue("disabledHosts", []);
let isDisabled = disabledHosts.includes(window.location.hostname);
if (isDisabled) {
disabledHosts = disabledHosts.filter(
(item) => item !== window.location.hostname,
);
} else {
disabledHosts.push(window.location.hostname);
}
GM_setValue("disabledHosts", disabledHosts);
applySettings();
}
function toggleSystem() {
let useSystem = GM_getValue("useSystem", true);
GM_setValue("useSystem", !useSystem);
applySettings();
}
/**
* This is necessary for CORS related requests. GM.xmlhttpRequest
* bypasses that restriction.
**/
async function extensionFetch(url) {
let responseData = await new Promise((resolve, reject) => {
GM.xmlhttpRequest({
url,
responseType: "blob",
onerror: reject,
onload: resolve,
});
});
return new Response(responseData.response);
}
DarkReader.setFetchMethod(extensionFetch);
function applySettings() {
GM.unregisterMenuCommand("System Mode - Enable");
GM.unregisterMenuCommand("System Mode - Disable");
GM.unregisterMenuCommand("DarkReader - Disable");
GM.unregisterMenuCommand("DarkReader - Enable");
let disabledHosts = GM_getValue("disabledHosts", []);
let useSystem = GM_getValue("useSystem", true);
if (useSystem) {
GM.registerMenuCommand("System Mode - Disable", toggleSystem);
} else {
GM.registerMenuCommand("System Mode - Enable", toggleSystem);
}
let isDisabled = disabledHosts.includes(window.location.hostname);
if (isDisabled) {
GM.registerMenuCommand("DarkReader - Enable", toggleHost);
} else {
GM.registerMenuCommand("DarkReader - Disable", toggleHost);
}
if (useSystem && prefersDark && !isDisabled)
return DarkReader.enable(options);
if (!useSystem && !isDisabled) return DarkReader.enable(options);
DarkReader.disable();
}
// System Change
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (event) => {
prefersDark = event.matches;
applySettings();
});
// Initial Load
applySettings();
+250
View File
@@ -0,0 +1,250 @@
// ==UserScript==
// @name Em Dash Highlighter
// @namespace Reichard
// @version 1.0.0
// @description Detects em dashes on the page, highlights them, and shows a popup on hover/tap
// @author Evan Reichard
// @match *://*/*
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ─── Constants ────────────────────────────────────────────────────────────
const EM_DASH = '\u2014';
const EMDASH_REGEX = /\u2014/g;
const PROCESSED_ATTR = 'data-emdash-processed';
const POPUP_ID = 'emdash-popup';
// Tags whose text content we should never touch
const SKIP_TAGS = new Set([
'SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT',
'SELECT', 'BUTTON', 'PRE', 'SVG', 'MATH',
]);
// ─── Styles ───────────────────────────────────────────────────────────────
GM_addStyle(`
.emdash-highlight {
display: inline;
padding: 0 1px;
border-radius: 2px;
cursor: help;
animation: emdash-hue 4s linear infinite;
background-color: hsl(0, 80%, 70%);
position: relative;
}
@keyframes emdash-hue {
0% { background-color: hsl(0, 80%, 70%); }
25% { background-color: hsl(90, 80%, 70%); }
50% { background-color: hsl(180, 80%, 70%); }
75% { background-color: hsl(270, 80%, 70%); }
100% { background-color: hsl(360, 80%, 70%); }
}
#${POPUP_ID} {
position: fixed;
z-index: 2147483647;
background: #1e1e2e;
color: #cdd6f4;
border: 1px solid #89b4fa;
border-radius: 6px;
padding: 6px 10px;
font: 13px/1.4 monospace;
pointer-events: none;
white-space: nowrap;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
opacity: 0;
transition: opacity 0.15s ease;
max-width: 280px;
white-space: normal;
}
#${POPUP_ID}.visible {
opacity: 1;
}
`);
// ─── Popup singleton ──────────────────────────────────────────────────────
const popup = document.createElement('div');
popup.id = POPUP_ID;
popup.innerHTML = `
<strong style="color:#89b4fa">Em Dash</strong> &mdash; U+2014
`;
document.documentElement.appendChild(popup);
let hideTimer = null;
function showPopup(anchorEl) {
clearTimeout(hideTimer);
const rect = anchorEl.getBoundingClientRect();
const popupHeight = popup.offsetHeight || 80;
const popupWidth = popup.offsetWidth || 220;
let top = rect.top - popupHeight - 8;
let left = rect.left + rect.width / 2 - popupWidth / 2;
// Flip below if not enough room above
if (top < 4) top = rect.bottom + 8;
// Clamp horizontally
left = Math.max(6, Math.min(left, window.innerWidth - popupWidth - 6));
popup.style.top = `${top}px`;
popup.style.left = `${left}px`;
popup.classList.add('visible');
}
function hidePopup(delay = 120) {
hideTimer = setTimeout(() => popup.classList.remove('visible'), delay);
}
// ─── Span factory ─────────────────────────────────────────────────────────
function createSpan() {
const span = document.createElement('span');
span.className = 'emdash-highlight';
span.setAttribute('aria-label', 'Em dash character');
span.textContent = EM_DASH;
// Desktop hover
span.addEventListener('mouseenter', () => showPopup(span));
span.addEventListener('mouseleave', () => hidePopup());
// Mobile tap — show/hide without bubbling
span.addEventListener('touchend', (e) => {
e.stopPropagation();
e.preventDefault();
if (popup.classList.contains('visible')) {
hidePopup(0);
} else {
showPopup(span);
// Auto-hide after 3 s on mobile
hideTimer = setTimeout(() => popup.classList.remove('visible'), 3000);
}
}, { passive: false });
// Prevent click bubbling (e.g. accordion toggles, links)
span.addEventListener('click', (e) => {
e.stopPropagation();
});
return span;
}
// ─── Core processor ───────────────────────────────────────────────────────
/**
* Walk a DOM subtree and wrap every em dash in a text node with a span.
* We operate on Text nodes directly to avoid serialising/deserialising HTML.
*/
function processNode(root) {
// TreeWalker is faster than querySelectorAll for text nodes
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
const parent = node.parentElement;
if (!parent) return NodeFilter.FILTER_REJECT;
if (SKIP_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
if (parent.isContentEditable) return NodeFilter.FILTER_REJECT;
if (parent.classList.contains('emdash-highlight')) return NodeFilter.FILTER_REJECT;
if (!node.nodeValue.includes(EM_DASH)) return NodeFilter.FILTER_SKIP;
return NodeFilter.FILTER_ACCEPT;
},
}
);
// Collect first — mutating the DOM while walking breaks the walker
const textNodes = [];
let node;
while ((node = walker.nextNode())) textNodes.push(node);
for (const textNode of textNodes) {
splitAndWrap(textNode);
}
}
/**
* Split a text node around em dashes and replace it with a fragment
* containing plain text nodes and highlight spans.
*/
function splitAndWrap(textNode) {
const text = textNode.nodeValue;
const parts = text.split(EMDASH_REGEX);
if (parts.length <= 1) return; // no em dash found
const fragment = document.createDocumentFragment();
parts.forEach((part, i) => {
if (part) fragment.appendChild(document.createTextNode(part));
if (i < parts.length - 1) fragment.appendChild(createSpan());
});
textNode.parentNode.replaceChild(fragment, textNode);
}
// ─── Mutation observer ────────────────────────────────────────────────────
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'characterData') {
// Text node changed in place
const node = mutation.target;
if (
node.nodeValue?.includes(EM_DASH) &&
node.parentElement &&
!SKIP_TAGS.has(node.parentElement.tagName) &&
!node.parentElement.classList.contains('emdash-highlight')
) {
splitAndWrap(node);
}
} else if (mutation.type === 'childList') {
for (const added of mutation.addedNodes) {
if (added.nodeType === Node.TEXT_NODE) {
if (
added.nodeValue?.includes(EM_DASH) &&
added.parentElement &&
!SKIP_TAGS.has(added.parentElement.tagName) &&
!added.parentElement.classList.contains('emdash-highlight')
) {
splitAndWrap(added);
}
} else if (added.nodeType === Node.ELEMENT_NODE) {
processNode(added);
}
}
}
}
});
// ─── Global tap-outside to dismiss popup ──────────────────────────────────
document.addEventListener('touchstart', () => hidePopup(0), { passive: true });
// ─── Init ─────────────────────────────────────────────────────────────────
function init() {
processNode(document.body);
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
});
}
// document-idle fires after DOMContentLoaded, but guard anyway
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
+34
View File
@@ -0,0 +1,34 @@
// ==UserScript==
// @name Paste Enabler
// @version 0.0.1
// @description Re-enables paste functionality on websites
// @author Evan Reichard
// @match *://*/*
// @grant GM.registerMenuCommand
// @noframes
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const enablePasteFunction = function() {
const events = ['paste', 'copy', 'cut'];
events.forEach(function(event) {
document.addEventListener(event, function(e) {
e.stopImmediatePropagation();
return true;
}, true);
});
document.querySelectorAll('input, textarea').forEach(function(el) {
el.setAttribute('oncopy', 'return true');
el.setAttribute('onpaste', 'return true');
el.setAttribute('oncut', 'return true');
el.removeAttribute('readonly');
});
};
// Register the menu command
GM.registerMenuCommand("Enable Paste", enablePasteFunction, "p");
})();
+34
View File
@@ -0,0 +1,34 @@
{
description = "Userscripts dev environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{ self
, nixpkgs
, flake-utils
,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { system = system; };
in
{
devShells.default = pkgs.mkShell {
packages = with pkgs; [
nodejs
pnpm
];
shellHook = ''
pnpm install --silent
export PATH="$PWD/node_modules/.bin:$PATH"
'';
};
}
);
}
+139
View File
@@ -0,0 +1,139 @@
// ==UserScript==
// @name Hacker News Tweaks
// @version 0.0.4
// @downloadURL https://gist.githubusercontent.com/evanreichard/96d89512cbfe23e5d7a81479c482e6da/raw/hn-tweaks.user.js
// @match *://news.ycombinator.com/*
// @grant GM.addStyle
// @grant GM.registerMenuCommand
// @grant GM.unregisterMenuCommand
// @grant GM_getValue
// @grant GM.setValue
// @noframes
// @run-at document-idle
// ==/UserScript==
/**
* This is a hacked together UserScript to add comment collapsing like old reddit.
* It also adds the following CSS tweak: https://news.ycombinator.com/item?id=29494475
**/
let activeStyleElement;
async function toggleStyle() {
let isEnabled = GM_getValue("enabled", true);
if (isEnabled === true) {
GM.unregisterMenuCommand("Disable Style");
GM.registerMenuCommand("Enable Style", toggleStyle);
activeStyleElement?.remove();
} else {
GM.unregisterMenuCommand("Enable Style");
GM.registerMenuCommand("Disable Style", toggleStyle);
applyStyle();
}
await GM.setValue("enabled", !isEnabled);
}
function applyStyle() {
activeStyleElement = GM.addStyle(`.comhead {
font-size: 12pt;
line-height: 20pt;
}
@media (prefers-color-scheme: dark) { body { background-color: #1F2430; }
#hnmain { background-color: #232834; }
a:link.storylink,
#hnmain > tbody > tr:first-child > td a,
.commtext, .commtext a:link,
td {
color: #fafafa;
}
#hnmain > tbody > tr:first-child > td { background-color: #333a4a; }
a:link,
.sitebit, .subtext, .sitestr, .subtext a:link,
.rank, a.morelink,
.pagetop, .yclinks a,
.comhead a:link, .comhead,
.hnmore a:link, .reply a:link {
color: #8c96ac;
}
a:visited {
color: #979cf4;
}
.votearrow {
overflow: visible;
position: relative;
}
.votearrow::before {
content: "";
width: 0;
height: 0;
left: 1px;
top: 3px;
display: block;
position: absolute;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 7px solid #bebfd1;
}
input[type=text],
textarea {
background-color: #333a4a;
color: #fafafa;
border: 1px solid #1F2430;
}
}`);
}
function findParent(el, index) {
let previousSibling = el.previousElementSibling;
let foundElem = previousSibling.querySelector(`[indent="${index}"]`);
return foundElem
? foundElem.closest(".athing")
: findParent(previousSibling, index);
}
let isEnabled = GM_getValue("enabled", true);
if (isEnabled === true) {
GM.registerMenuCommand("Disable Style", toggleStyle);
applyStyle();
} else {
GM.registerMenuCommand("Enable Style", toggleStyle);
}
Array.from(document.querySelectorAll(".ind")).forEach((indent) => {
let imgEl = indent.querySelector("img");
if (!imgEl) return;
let colWidth = imgEl.offsetWidth;
indent.style.position = "relative";
let indCount = parseInt(indent.getAttribute("indent"));
let voteOffset =
indent.nextElementSibling.querySelector("center").offsetHeight;
for (let i = 0; i <= indCount; i += 1) {
let marginLeft = i === 0 ? "10px" : (colWidth / indCount) * i + 10 + "px";
let topOffset = i === indCount ? voteOffset + 15 : -10;
let newElem = document.createElement("div");
newElem.setAttribute(
"style",
`cursor: pointer; height: calc(100% - ${topOffset + 2}px); width: 0px; position: absolute; margin-left: ${marginLeft}; top: ${topOffset}px; border-left: 2px solid green; padding: 0px min(${colWidth / 2}px, 5px);`,
);
newElem.onclick = () => {
let commentBlock = newElem.closest(".athing");
if (i === indCount) {
commentBlock.querySelector(".togg.clicky").click();
} else {
let parentComment = findParent(commentBlock, i);
parentComment.querySelector(".togg.clicky").click();
}
};
indent.appendChild(newElem);
}
});
+45
View File
@@ -0,0 +1,45 @@
// ==UserScript==
// @name Auto Kagi LLM
// @match https://kagi.com/assistant?mode=4*
// @grant none
// @version 1.0
// @author Evan Reichard
// ==/UserScript==
function doAutoLLM() {
let hashLocation = window.location.hash;
if (!hashLocation.startsWith("#query=")) return;
let query = decodeURIComponent(hashLocation.replace(/^#query=/, ''));
let inputEl = document.querySelector("textarea");
inputEl.value = query;
const keyboardEvent = new KeyboardEvent('keydown', {
code: 'Enter',
key: 'Enter',
charCode: 13,
keyCode: 13,
view: window,
bubbles: true
});
setTimeout(() => {
inputEl.dispatchEvent(keyboardEvent);
}, 100);
}
doAutoLLM()
// Example Usage:
// https://kagi.com/assistant?mode=4&sub_mode=1#query=%s
// LLMs:
// GPT35Turbo - https://kagi.com/assistant?mode=4&sub_mode=1
// GPT4 - https://kagi.com/assistant?mode=4&sub_mode=2
// GPT4Turbo - https://kagi.com/assistant?mode=4&sub_mode=7
// Claude3Haiku - https://kagi.com/assistant?mode=4&sub_mode=13
// Claude3Sonnet - https://kagi.com/assistant?mode=4&sub_mode=14
// Claude3Opus - https://kagi.com/assistant?mode=4&sub_mode=12
// MistralSmall - https://kagi.com/assistant?mode=4&sub_mode=10
// MistralLarge - https://kagi.com/assistant?mode=4&sub_mode=11
// Gemini15Pro - https://kagi.com/assistant?mode=4&sub_mode=8
+854
View File
@@ -0,0 +1,854 @@
// ==UserScript==
// @name Reddit Tweaks (Apollo)
// @version 0.3.0
// @match *://old.reddit.com/*
// @match *://www.reddit.com/*
// @downloadURL https://gist.githubusercontent.com/evanreichard/968eee1cf83da38c81c155fb775cf3c6/raw/old-reddit-mobile.user.js
// @grant GM.addStyle
// @grant GM.registerMenuCommand
// @grant GM.unregisterMenuCommand
// @grant GM.getValue
// @grant GM.setValue
// @grant GM_getValue
// @noframes
// @run-at document-start
// ==/UserScript==
/* ============================================================================
* REDIRECT: www.reddit.com -> old.reddit.com
* ----------------------------------------------------------------------------
* Run before anything else so we don't waste cycles styling the new design.
* The /media path is a special case: Reddit's image viewer wraps the image
* in heavy chrome, so we strip everything down to just the <img> tag.
* ========================================================================== */
if (document.location.host === "www.reddit.com") {
if (document.location.pathname === "/media") {
document.addEventListener("DOMContentLoaded", () => {
const img = document.querySelector("img");
if (img) document.body.innerHTML = img.outerHTML;
});
} else {
window.location.href =
"https://old.reddit.com" +
window.location.pathname +
window.location.search +
window.location.hash;
}
}
/* ============================================================================
* STATE
* ----------------------------------------------------------------------------
* `isEnabled` is read synchronously at script start so we can decide whether
* to inject CSS at document-start (before paint). Toggling the script ALWAYS
* reloads the page — see toggleStyle() for the reasoning.
* ========================================================================== */
let isEnabled = GM_getValue("enabled", true);
/* ============================================================================
* STYLES
* ----------------------------------------------------------------------------
* All visual styling lives here. Uses CSS custom properties for theming so
* dark mode is just a media query override on the :root tokens.
* ========================================================================== */
function applyStyle() {
GM.addStyle(`
/* --------------------------------------------------------------
DESIGN TOKENS
-------------------------------------------------------------- */
:root {
--bg: #ffffff;
--bg-elev: #f7f8fa;
--bg-hover: #eef0f3;
--surface: #ffffff;
--surface-2: #f4f5f7;
--border: #e3e6ea;
--border-soft: #ecedef;
--fg: #1c1c1e;
--fg-2: #3c3c43;
--fg-dim: #6e6e73;
--fg-faint: #b0b3b8;
--accent: #ff4500;
--accent-2: #ff6a33;
--link: #2f6feb;
--visited: #8a8d96;
--upvote: #ff4500;
--downvote: #5b8def;
/* Apollo-style thread depth colors (cycle every 6) */
--depth-1: #4f9cf9; /* blue */
--depth-2: #34c759; /* green */
--depth-3: #ffcc00; /* yellow */
--depth-4: #ff9f0a; /* orange */
--depth-5: #ff453a; /* red */
--depth-6: #bf5af2; /* purple */
--radius-sm: 6px;
--radius: 10px;
--radius-lg: 14px;
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
"Segoe UI", "Inter", Roboto, system-ui, sans-serif;
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
--shadow: 0 2px 8px rgba(0,0,0,0.06);
--tap: 44px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0e0f12;
--bg-elev: #16181d;
--bg-hover: #1e2128;
--surface: #16181d;
--surface-2: #1c1f25;
--border: #26292f;
--border-soft: #1e2128;
--fg: #f2f2f7;
--fg-2: #d1d1d6;
--fg-dim: #9b9ba1;
--fg-faint: #5a5d63;
--link: #5b8def;
--visited: #6e6f78;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
--shadow: 0 4px 12px rgba(0,0,0,0.4);
}
}
/* --------------------------------------------------------------
GLOBAL RESET / BASE
-------------------------------------------------------------- */
html, body {
background: var(--bg) !important;
color: var(--fg) !important;
font-family: var(--font-sans) !important;
font-size: 15px !important;
line-height: 1.4 !important;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: 100%;
}
a { color: var(--link); text-decoration: none; }
a:visited { color: var(--visited); }
a:hover { color: var(--fg); }
/* Hide all the cruft old.reddit ships with by default */
.side,
.footer-parent,
.rank,
.rank-spacer,
#sr-header-area,
.mobile-web-redirect-bar,
.link.promotedlink,
.infobar.listingsignupbar,
.infobar.welcome,
.listingsignupbar,
.promoted,
[data-promoted="true"],
.res-floatingSideBar,
.bottommenu,
.sr-list,
.sr-bar,
.sr-interest-bar,
.leavemoderator,
.titlebox,
.login-form-side,
.icon-menu .gold-buy,
.reportform,
.panestack-title .res-comments-sortMenu,
.link .flat-list.buttons li.share,
.link .flat-list.buttons .post-sharing-button,
.link .flat-list.buttons .reportbtn {
display: none !important;
}
/* --------------------------------------------------------------
HEADER
-------------------------------------------------------------- */
#header {
background: var(--bg-elev) !important;
border-bottom: 1px solid var(--border) !important;
box-shadow: var(--shadow-sm);
padding: 0 12px !important;
min-height: 48px !important;
height: auto !important;
position: sticky;
top: 0;
z-index: 1000;
backdrop-filter: saturate(180%) blur(12px);
-webkit-backdrop-filter: saturate(180%) blur(12px);
}
#header-bottom-left {
display: flex !important;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 6px 14px;
padding: 8px 0 !important;
}
#header-img, .pagename a {
color: var(--fg) !important;
font-weight: 700 !important;
font-size: 17px !important;
letter-spacing: -0.01em;
}
.pagename { padding: 0 !important; }
.tabmenu {
width: 100%;
display: flex !important;
gap: 4px;
margin: 0 !important;
overflow-x: auto;
scrollbar-width: none;
}
.tabmenu::-webkit-scrollbar { display: none; }
.tabmenu li { margin: 0 !important; }
.tabmenu li a {
display: inline-block;
padding: 6px 12px !important;
border-radius: 999px;
font-size: 13px !important;
font-weight: 500;
color: var(--fg-dim) !important;
background: transparent !important;
border: 1px solid transparent !important;
transition: background 0.15s ease, color 0.15s ease;
}
.tabmenu li.selected a {
background: var(--accent) !important;
color: #fff !important;
}
.tabmenu li:not(.selected) a:hover {
background: var(--bg-hover) !important;
color: var(--fg) !important;
}
#header-bottom-right {
top: 0;
bottom: unset;
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
background: var(--bg-elev) !important;
color: var(--fg-dim) !important;
font-size: 12px !important;
padding: 6px 10px !important;
border-left: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
#header-bottom-right a { color: var(--fg-dim) !important; }
/* --------------------------------------------------------------
CONTENT WRAPPER
-------------------------------------------------------------- */
.content[role="main"] {
margin: 0 !important;
padding: 0 !important;
max-width: 100% !important;
background: var(--bg) !important;
color: var(--fg) !important;
}
#siteTable { margin: 0 !important; }
/* --------------------------------------------------------------
POSTS
----------------------------------------------------------------
Flex layout that preserves Reddit's native expando behavior.
Order: thumbnail | entry | (votes hidden) | (wrap) expando
-------------------------------------------------------------- */
.link {
position: relative;
display: flex !important;
flex-wrap: wrap;
align-items: flex-start;
column-gap: 12px;
row-gap: 4px;
padding: 12px 14px !important;
margin: 0 !important;
background: var(--surface) !important;
color: var(--fg) !important;
border: 0 !important;
border-bottom: 1px solid var(--border-soft) !important;
transition: background 0.12s ease;
}
.link:active { background: var(--bg-hover) !important; }
.link.last-clicked {
background: var(--bg-elev) !important;
border-bottom: 1px solid var(--border-soft) !important;
}
.link > .thumbnail { order: 1; flex: 0 0 auto; }
.link > .entry { order: 2; flex: 1 1 0; min-width: 0; }
.link > .midcol { order: 3; flex: 0 0 auto; }
.link > .child,
.link > .clearleft { display: none !important; }
/* Hide post-level vote arrows and native expando button (we tap thumbnail instead) */
.link > .midcol { display: none !important; }
.link .expando-button { display: none !important; }
/* Thumbnail container — shared by real and synthetic thumbs */
.link > .thumbnail {
width: 64px !important;
height: 64px !important;
margin: 0 !important;
border-radius: var(--radius);
overflow: hidden;
background: var(--bg-elev);
border: 1px solid var(--border-soft);
display: flex !important;
align-items: center;
justify-content: center;
cursor: pointer;
}
.link > .thumbnail img {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
display: block;
}
/* Placeholder labels when there's no real image */
.link > .thumbnail.self::after,
.link > .thumbnail.default::after,
.link > .thumbnail.spoiler::after,
.link > .thumbnail.nsfw::after {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.05em;
color: var(--fg-dim);
}
.link > .thumbnail.self::after { content: "TEXT"; }
.link > .thumbnail.default::after { content: "LINK"; }
.link > .thumbnail.spoiler::after { content: "⚠"; font-size: 18px; }
.link.over18 > .thumbnail::after { content: "NSFW"; color: #ff453a; }
/* Entry / title / meta */
.link .entry { margin: 0 !important; color: var(--fg) !important; }
.link .top-matter { padding: 0 !important; margin: 0 !important; }
.link .title {
margin: 0 0 4px 0 !important;
padding: 0 !important;
font-size: 15px !important;
line-height: 1.3 !important;
font-weight: 500;
letter-spacing: -0.01em;
}
.link .title a.title { color: var(--fg) !important; }
.link.visited .title a.title,
.link .title a.title:visited { color: var(--fg-dim) !important; }
.link .title .domain {
display: inline-block;
font-size: 11px !important;
color: var(--fg-dim) !important;
font-weight: 400;
margin-left: 4px;
vertical-align: middle;
}
.link .title .domain a { color: var(--fg-dim) !important; }
.link .linkflairlabel {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 10.5px !important;
font-weight: 600;
padding: 2px 7px !important;
border-radius: 999px;
margin-right: 6px;
vertical-align: middle;
line-height: 1.4;
border: 0 !important;
}
.link .flairemoji { width: 12px !important; height: 12px !important; }
.link .tagline {
font-size: 12px !important;
color: var(--fg-dim) !important;
margin: 0 !important;
line-height: 1.3 !important;
}
.link .tagline a,
.link .tagline time { color: var(--fg-dim) !important; }
.link .tagline .author { color: var(--fg-2) !important; font-weight: 500; }
.link .flat-list.buttons {
display: flex;
gap: 12px;
margin: 4px 0 0 0 !important;
padding: 0 !important;
font-size: 12px !important;
list-style: none;
}
.link .flat-list.buttons li { display: inline-block; margin: 0; padding: 0; }
.link .flat-list.buttons a { color: var(--fg-dim) !important; font-weight: 500; }
.link .flat-list.buttons .comments {
color: var(--accent) !important;
font-weight: 600;
}
/* --------------------------------------------------------------
EXPANDO (full-width row beneath the post)
-------------------------------------------------------------- */
.link > .expando {
order: 4;
flex: 0 0 100%;
width: 100% !important;
max-width: 100% !important;
margin: 8px 0 0 0 !important;
padding: 0 !important;
background: transparent !important;
border: 0 !important;
color: var(--fg) !important;
box-sizing: border-box;
}
.link > .expando .media-preview,
.link > .expando .media-preview-content,
.link > .expando .res-expando-box,
.link > .expando .res-expando-box div {
max-width: 100% !important;
width: 100% !important;
}
.link .expando img,
.link .expando video,
.link .expando iframe,
.link .expando .preview,
.link .expando .media-preview-content > * {
max-width: 100% !important;
height: auto !important;
border-radius: var(--radius);
display: block;
}
/* Self-text body */
.link .expando .usertext-body,
.link .usertext-body,
.commentarea .link .usertext-body {
background: var(--surface-2) !important;
color: var(--fg) !important;
border: 1px solid var(--border-soft) !important;
border-radius: var(--radius);
padding: 10px 14px !important;
margin: 0 !important;
}
.link .expando .md,
.link .usertext-body .md {
background: transparent !important;
color: var(--fg) !important;
max-width: unset !important;
font-size: 14px !important;
line-height: 1.55 !important;
}
.link .expando .md p,
.link .expando .md li,
.link .expando .md h1,
.link .expando .md h2,
.link .expando .md h3,
.link .expando .md blockquote {
color: var(--fg) !important;
}
/* --------------------------------------------------------------
COMMENTS — bubble-in-bubble with depth-colored left border
-------------------------------------------------------------- */
.commentarea {
background: var(--bg) !important;
color: var(--fg) !important;
padding: 8px 10px !important;
margin: 0 !important;
}
.commentarea > .menuarea {
background: transparent !important;
border: 0 !important;
padding: 8px 4px !important;
margin: 0 0 4px 0 !important;
font-size: 12px;
color: var(--fg-dim) !important;
}
.comment {
background: var(--surface) !important;
color: var(--fg) !important;
border: 1px solid var(--border-soft) !important;
border-left: 3px solid var(--depth-1) !important;
border-radius: var(--radius);
padding: 10px 12px !important;
margin: 6px 0 !important;
box-shadow: var(--shadow-sm);
}
.comment .child,
.comment .showreplies {
border-left: 0 !important;
margin: 6px 0 0 6px !important;
padding: 0 !important;
}
/* Cycle border colors by nesting depth */
.comment .comment { border-left-color: var(--depth-2) !important; }
.comment .comment .comment { border-left-color: var(--depth-3) !important; }
.comment .comment .comment .comment { border-left-color: var(--depth-4) !important; }
.comment .comment .comment .comment .comment { border-left-color: var(--depth-5) !important; }
.comment .comment .comment .comment .comment .comment { border-left-color: var(--depth-6) !important; }
.comment .comment .comment .comment .comment .comment .comment { border-left-color: var(--depth-1) !important; }
.comment .comment .comment .comment .comment .comment .comment .comment { border-left-color: var(--depth-2) !important; }
.comment .comment .comment .comment .comment .comment .comment .comment .comment { border-left-color: var(--depth-3) !important; }
/* Alternate bg shades to emphasize nesting */
.comment .comment { background: var(--surface-2) !important; }
.comment .comment .comment { background: var(--surface) !important; }
.comment .entry { padding: 0 !important; }
.comment .tagline {
font-size: 12px !important;
color: var(--fg-dim) !important;
margin-bottom: 4px;
}
.comment .tagline .author { color: var(--fg) !important; font-weight: 600; }
.comment .tagline .submitter { color: var(--link) !important; font-weight: 600; }
.comment .tagline .score { color: var(--fg-dim) !important; font-weight: 500; }
.comment .usertext-body,
.comment .usertext-body .md,
.comment .md {
font-size: 14px !important;
line-height: 1.5 !important;
color: var(--fg) !important;
background: transparent !important;
}
.comment .md p { margin: 4px 0 !important; color: var(--fg) !important; }
.comment .md a { color: var(--link) !important; }
.comment .md blockquote {
border-left: 3px solid var(--border);
padding-left: 10px;
color: var(--fg-2) !important;
margin: 6px 0;
}
.comment .md code {
background: var(--bg-elev) !important;
color: var(--fg) !important;
border-radius: 4px;
padding: 1px 5px;
font-family: var(--font-mono);
font-size: 12.5px;
}
.comment .md pre {
background: var(--bg-elev) !important;
color: var(--fg) !important;
padding: 8px 10px;
border-radius: var(--radius-sm);
overflow-x: auto;
}
.comment .midcol {
width: 16px !important;
margin-right: 4px !important;
}
.comment .arrow {
width: 18px !important;
height: 18px !important;
background-size: 14px !important;
border-radius: 4px;
}
.comment .arrow:hover { background-color: var(--bg-hover); }
.comment .flat-list.buttons {
font-size: 12px !important;
margin-top: 4px !important;
}
.comment .flat-list.buttons a { color: var(--fg-dim) !important; font-weight: 500; }
.comment .flat-list.buttons .reply-button a { color: var(--link) !important; }
.comment .expand {
color: var(--fg-dim) !important;
font-weight: 600;
padding: 0 4px;
}
.comment.collapsed {
opacity: 0.7;
padding: 6px 12px !important;
}
/* "load more" buttons */
.morechildren a, .morecomments a {
display: inline-block;
padding: 6px 12px;
border-radius: 999px;
background: var(--bg-elev) !important;
color: var(--link) !important;
font-weight: 600;
font-size: 13px;
border: 1px solid var(--border) !important;
}
/* --------------------------------------------------------------
PAGINATION
-------------------------------------------------------------- */
.nextprev {
display: flex;
justify-content: space-between;
align-items: stretch;
gap: 10px;
padding: 16px 14px 28px;
margin: 0;
}
.nextprev .separator { display: none; }
.nextprev a {
flex: 1 1 0;
min-height: var(--tap);
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
border-radius: var(--radius);
font-weight: 600;
font-size: 15px;
text-decoration: none;
background: var(--bg-elev) !important;
color: var(--fg) !important;
border: 1px solid var(--border);
transition: transform 0.06s ease, background 0.15s ease;
}
.nextprev a:hover { background: var(--bg-hover) !important; }
.nextprev a:active { transform: scale(0.98); }
.nextprev a[rel*="next"] {
background: var(--accent) !important;
color: #fff !important;
border-color: transparent;
}
.nextprev a[rel*="next"]:hover { background: var(--accent-2) !important; }
/* --------------------------------------------------------------
FORMS / MENUS / MISC
-------------------------------------------------------------- */
input[type=text],
input[type=password],
input[type=email],
textarea {
background: var(--bg-elev) !important;
color: var(--fg) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius-sm) !important;
padding: 8px 10px !important;
font-family: var(--font-sans) !important;
font-size: 14px !important;
}
.btn, button, .morelink a {
background: var(--bg-elev) !important;
color: var(--fg) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius-sm) !important;
padding: 6px 12px !important;
font-weight: 600;
font-family: var(--font-sans) !important;
}
.menuarea, .panestack-title {
background: transparent !important;
border: 0 !important;
color: var(--fg-dim) !important;
padding: 8px 12px !important;
}
.dropdown, .drop-choices {
background: var(--surface) !important;
color: var(--fg) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius-sm) !important;
box-shadow: var(--shadow);
}
table, th, td {
background: var(--surface) !important;
color: var(--fg) !important;
border-color: var(--border) !important;
}
pre, code {
background: var(--bg-elev) !important;
color: var(--fg) !important;
font-family: var(--font-mono) !important;
}
/* Dim Reddit's default vote arrows in dark mode (they're black PNGs) */
@media (prefers-color-scheme: dark) {
.arrow:not(.upmod):not(.downmod) { filter: brightness(0.85) invert(0.9); }
}
a, button { touch-action: manipulation; }
body > .content .link .midcol { width: auto !important; }
body > .content .link .rank, .rank-spacer { width: 0 !important; }
`);
// Mobile viewport — Reddit's default doesn't include viewport-fit=cover,
// which we want for safe-area handling on notched devices.
if (document.head) {
const meta = document.createElement("meta");
meta.setAttribute("name", "viewport");
meta.setAttribute(
"content",
"width=device-width, initial-scale=1, viewport-fit=cover",
);
document.head.appendChild(meta);
}
// Nuke subreddit-specific stylesheet ASAP so it can't fight ours
document.head?.querySelector("[ref=applied_subreddit_stylesheet]")?.remove();
}
/* ============================================================================
* DOM TWEAKS
* ----------------------------------------------------------------------------
* Things CSS alone can't do: rewriting nodes, injecting placeholders,
* relocating elements within the post layout.
* ========================================================================== */
function applyDOMTweaks() {
// Belt-and-suspenders subreddit stylesheet removal (in case it loaded after us)
document.querySelector('link[title="applied_subreddit_stylesheet"]')?.remove();
// Replace the subreddit logo/banner anchor with a plain "reddit" home link
const headerAnchor = document.querySelector("#header-bottom-left a");
if (headerAnchor) {
headerAnchor.outerHTML =
`<a href="/" id="header-img" class="default-header" title="">reddit</a>`;
}
// Process every post on the page
document.querySelectorAll(".link").forEach(processLink);
// Pagination polish — shorten "view more" wording and add arrows
const nextprev = document.querySelector(".nextprev");
if (nextprev) {
// The first text node is often "view more:" — drop it
const first = nextprev.childNodes[0];
if (first && first.textContent.trim().toLowerCase().startsWith("view more")) {
first.remove();
}
nextprev.querySelectorAll("a").forEach((a) => {
const rel = (a.getAttribute("rel") || "").toLowerCase();
if (rel.includes("next")) a.textContent = "Next ";
else if (rel.includes("prev")) a.textContent = " Prev";
});
// Center the lone button if there's only one direction available
if (nextprev.querySelectorAll("a").length === 1) {
nextprev.style.justifyContent = "center";
nextprev.querySelector("a").style.flex = "0 1 280px";
}
}
}
/**
* Per-post tweaks:
* 1. Move .expando out of .entry so it can become a full-width flex row
* (flex order: thumbnail | entry | expando-on-new-line).
* 2. Inject a synthetic .thumbnail <div> when the subreddit suppresses
* thumbnails server-side (e.g. r/cars). This guarantees every post
* has a tappable thumb area showing "TEXT", "LINK", "NSFW", etc.
*/
function processLink(link) {
// --- 1. Promote .expando to be a direct child of .link ---
const expando = link.querySelector(":scope > .entry > .expando");
if (expando) link.appendChild(expando);
// --- 2. Inject synthetic thumbnail if missing ---
if (!link.querySelector(":scope > .thumbnail")) {
const thumb = document.createElement("div");
// Pick the right placeholder class so CSS ::after shows the right label
if (link.classList.contains("over18")) {
thumb.className = "thumbnail nsfw";
} else if (link.classList.contains("spoiler")) {
thumb.className = "thumbnail spoiler";
} else if (link.classList.contains("self")) {
thumb.className = "thumbnail self";
} else {
thumb.className = "thumbnail default";
}
link.prepend(thumb);
}
}
/* ============================================================================
* INTERACTION HANDLERS
* ----------------------------------------------------------------------------
* Delegated click handlers — attached once at script start, gated on
* `isEnabled` so they no-op when the user disables the script live.
* ========================================================================== */
// Tap a comment body to collapse/expand it (Apollo-style)
document.addEventListener("click", (evt) => {
if (!isEnabled) return;
// Ignore clicks on interactive descendants
if (evt.target.closest("a")) return;
if (evt.target.classList.contains("expand")) return;
if (evt.target.closest(".flat-list.buttons")) return;
if (evt.target.closest(".usertext-edit")) return;
if (evt.target.closest(".midcol")) return;
const comment = evt.target.closest(".comment");
if (!comment) return;
const expander = comment.querySelector(":scope > .entry .expand");
if (!expander) return;
expander.click();
});
// Tap a thumbnail to toggle the post's expando.
// On synthetic thumbnails (link posts with no expando) this is a no-op.
document.addEventListener("click", (evt) => {
if (!isEnabled) return;
let thumb;
if (evt.target.classList?.contains("thumbnail")) {
thumb = evt.target;
} else if (evt.target.parentElement?.classList?.contains("thumbnail")) {
thumb = evt.target.parentElement;
}
if (!thumb) return;
const link = thumb.closest(".link");
if (!link) return;
const btn = link.querySelector(".expando-button");
evt.preventDefault();
evt.stopPropagation();
if (!btn) return; // synthetic thumb, nothing to expand
btn.click();
});
/* ============================================================================
* MENU / TOGGLE
* ----------------------------------------------------------------------------
* Toggling always reloads the page. Why:
* - GM.addStyle injects a <style> tag that can't be cleanly removed without
* tracking references and tearing them down.
* - DOM tweaks (moved expandos, injected thumbnails, replaced header anchor)
* can't be reversed without remembering the original state.
* - A reload gives a clean, predictable state for both directions.
* ========================================================================== */
async function toggleStyle() {
const currentlyEnabled = await GM.getValue("enabled", true);
await GM.setValue("enabled", !currentlyEnabled);
window.location.reload();
}
function registerMenu() {
const label = isEnabled ? "Style - Disable" : "Style - Enable";
GM.registerMenuCommand(label, toggleStyle);
}
/* ============================================================================
* BOOT
* ========================================================================== */
if (isEnabled) {
applyStyle(); // inject CSS at document-start
window.addEventListener("load", applyDOMTweaks); // DOM tweaks once page is ready
}
registerMenu();
+10
View File
@@ -0,0 +1,10 @@
{
"name": "userscripts",
"private": true,
"scripts": {
"lint": "oxlint"
},
"devDependencies": {
"oxlint": "^1.65.0"
}
}
+516
View File
@@ -0,0 +1,516 @@
// ==UserScript==
// @name Page Agent — LLM in the page
// @namespace https://github.com/example/page-agent
// @version 0.1.0
// @description Turn any web page into an LLM agent that can run JS in the page to inspect, extract, fetch and act — then bake what it learned into a standalone, LLM-free userscript.
// @match *://*/*
// @run-at document-idle
// @grant GM.xmlHttpRequest
// @grant GM.setValue
// @grant GM.getValue
// @connect api.openai.com
// @connect *
// ==/UserScript==
// SECURITY NOTE: `@connect *` allows GM.xmlHttpRequest to POST to ANY host. That is
// handy while you experiment with self-hosted / proxied endpoints, but it is broad.
// Once you know your endpoint's host, DELETE the `@connect *` line above and keep
// only the specific host(s) you use (e.g. just `@connect api.openai.com`).
(async () => {
'use strict';
// Built at runtime so this file contains no literal triple-backticks and stays paste-safe.
const BT3 = '`'.repeat(3);
// ============================================================
// Configuration
// ============================================================
const DEFAULTS = {
endpoint: 'https://api.openai.com/v1/chat/completions',
apiKey: '',
model: 'gpt-4o-mini',
};
const CONFIG = {
endpoint: await GM.getValue('endpoint', DEFAULTS.endpoint),
apiKey: await GM.getValue('apiKey', DEFAULTS.apiKey),
model: await GM.getValue('model', DEFAULTS.model),
};
const maxSteps = 8; // upper bound on the agent's tool-use loop
// The panel starts fully hidden. Reveal it with:
// Desktop — Ctrl+Shift+Space (see HOTKEY)
// Mobile — three-finger tap anywhere on the page
const HOTKEY = { ctrl: true, shift: true, alt: false, code: 'Space', label: 'Ctrl+Shift+Space' };
// ============================================================
// safeStringify — serialize arbitrary values without blowing up
// ============================================================
function safeStringify(value, maxLen = 20000) {
const seen = new WeakSet();
let out;
try {
out = JSON.stringify(value, function (key, val) {
// DOM nodes -> "[NODENAME] <first 200 chars of textContent>"
if (typeof Node !== 'undefined' && val instanceof Node) {
return '[' + val.nodeName + '] ' + String(val.textContent || '').slice(0, 200);
}
if (typeof val === 'function') return '[Function]';
if (val !== null && typeof val === 'object') {
if (seen.has(val)) return String(val); // circular / repeated ref
seen.add(val);
}
return val;
});
} catch (e) {
out = String(value);
}
if (out === undefined) out = String(value); // undefined coercion
if (out.length > maxLen) {
const total = out.length;
out = out.slice(0, maxLen) + '…[truncated, ' + total + ' chars]';
}
return out;
}
// ============================================================
// The single tool: run_js
// ============================================================
const TOOLS = [{
type: 'function',
function: {
name: 'run_js',
description:
'Execute JavaScript in the context of the current web page. Your code runs ' +
'as the BODY of an async function, so you can use `await` and you MUST `return` ' +
'a value to see any result. The returned value is JSON-stringified and sent back ' +
'to you, so keep it SMALL and RELEVANT — return only the specific fields/text you ' +
'need, never whole DOM trees or huge arrays. Use it to query selectors, extract ' +
'content, call fetch(), read/modify the page, click, fill and navigate. Take ' +
'several small targeted steps rather than one giant snippet.',
parameters: {
type: 'object',
properties: {
code: {
type: 'string',
description: 'JavaScript run as the body of an async function. `return` the result you want back.',
},
},
required: ['code'],
},
},
}];
async function runJs(code) {
try {
// Wrapped so the model can `await` and `return` naturally.
const fn = new Function('return (async () => { ' + code + ' })()');
const result = await fn();
return safeStringify(result);
} catch (e) {
return 'ERROR: ' + (e && e.stack ? e.stack : String(e));
}
}
// ============================================================
// OpenAI-compatible request via GM.xmlHttpRequest
// ============================================================
function chat(messages) {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'POST',
url: CONFIG.endpoint,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + CONFIG.apiKey,
},
data: JSON.stringify({
model: CONFIG.model,
messages: messages,
tools: TOOLS,
tool_choice: 'auto',
temperature: 0.2,
}),
timeout: 60000,
onload: (res) => {
if (res.status < 200 || res.status >= 300) {
reject(new Error('HTTP ' + res.status + ': ' + res.responseText));
return;
}
try {
resolve(JSON.parse(res.responseText));
} catch (e) {
reject(new Error('Bad JSON from endpoint: ' + res.responseText));
}
},
onerror: () => reject(new Error('Network error contacting ' + CONFIG.endpoint)),
ontimeout: () => reject(new Error('Request timed out after 60s')),
});
});
}
// ============================================================
// Agentic loop
// ============================================================
const SYSTEM_PROMPT =
'You are an agent embedded directly in a live web page in the user\'s browser. ' +
'You have exactly one tool, run_js, which executes JavaScript in this page and ' +
'returns its JSON-stringified return value. Use it to inspect the DOM, query ' +
'selectors, extract content, call fetch(), and act on the page (click, fill, ' +
'navigate) to satisfy the request. Prefer small, targeted snippets that return ' +
'only the specific data you need; do not dump whole DOM trees or large arrays. ' +
'Take several small steps rather than one giant one. When the request is complete, ' +
'stop calling tools and reply to the user in plain text.';
// Returns { answer, messages } so the conversation can be reused for later turns.
async function agent(userRequest, log = console.log, history = null) {
const messages = history ? history : [{ role: 'system', content: SYSTEM_PROMPT }];
if (userRequest) messages.push({ role: 'user', content: userRequest });
for (let step = 0; step < maxSteps; step++) {
const data = await chat(messages);
const msg = data && data.choices && data.choices[0] && data.choices[0].message;
if (!msg) {
return { answer: 'ERROR: no message in response: ' + safeStringify(data), messages };
}
messages.push(msg);
const toolCalls = msg.tool_calls;
if (!toolCalls || toolCalls.length === 0) {
return { answer: msg.content || '', messages }; // final plain-text answer
}
for (const tc of toolCalls) {
let args = {};
try {
args = JSON.parse(tc.function.arguments || '{}');
} catch (e) {
args = { code: '' };
}
const code = args.code || '';
log('» run_js:\n' + code);
const result = await runJs(code);
log('« result: ' + result.slice(0, 500));
messages.push({ role: 'tool', tool_call_id: tc.id, content: result });
}
}
return { answer: '(Stopped after ' + maxSteps + ' steps without a final answer.)', messages };
}
// ============================================================
// UI — floating panel (bottom-right, dark), hidden by default
// ============================================================
const PANEL_ID = 'llm-page-agent-panel';
if (document.getElementById(PANEL_ID)) return; // avoid double-inject
const style = document.createElement('style');
style.textContent = `
#${PANEL_ID}{position:fixed;bottom:16px;right:16px;z-index:2147483647;width:340px;
font-family:-apple-system,system-ui,Segoe UI,Roboto,sans-serif;font-size:13px;
background:#1e1e22;color:#e6e6e6;border:1px solid #3a3a40;border-radius:10px;
box-shadow:0 8px 30px rgba(0,0,0,.5);overflow:hidden;
transition:opacity .12s ease, transform .12s ease;}
#${PANEL_ID}.pa-hidden{opacity:0;transform:translateY(8px) scale(.98);pointer-events:none;
visibility:hidden;}
#${PANEL_ID} .pa-header{display:flex;align-items:center;gap:8px;padding:8px 10px;
background:#27272c;user-select:none;}
#${PANEL_ID} .pa-title{font-weight:600;flex:1;}
#${PANEL_ID} .pa-iconbtn{background:transparent;border:none;color:#bdbdbd;cursor:pointer;
font-size:14px;line-height:1;padding:3px 7px;border-radius:6px;}
#${PANEL_ID} .pa-iconbtn:hover{background:#3a3a40;color:#fff;}
#${PANEL_ID} .pa-body{padding:8px 10px;}
#${PANEL_ID} .pa-log{max-height:240px;overflow-y:auto;white-space:pre-wrap;
word-break:break-word;background:#131316;border:1px solid #2c2c31;border-radius:6px;
padding:8px;line-height:1.35;margin-bottom:8px;font-family:ui-monospace,Menlo,Consolas,monospace;
font-size:12px;}
#${PANEL_ID} .pa-row{display:flex;gap:6px;margin-bottom:6px;}
#${PANEL_ID} input.pa-input{flex:1;min-width:0;background:#131316;color:#e6e6e6;
border:1px solid #3a3a40;border-radius:6px;padding:6px 8px;outline:none;}
#${PANEL_ID} input.pa-input:disabled{opacity:.5;}
#${PANEL_ID} button.pa-btn{background:#3b82f6;color:#fff;border:none;border-radius:6px;
padding:6px 10px;cursor:pointer;font-size:12px;white-space:nowrap;}
#${PANEL_ID} button.pa-btn.pa-secondary{background:#3a3a40;}
#${PANEL_ID} button.pa-btn:disabled{opacity:.5;cursor:not-allowed;}
#${PANEL_ID} .pa-spinner{display:inline-block;width:12px;height:12px;
border:2px solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;
animation:pa-rotation .7s linear infinite;vertical-align:middle;}
#${PANEL_ID}.pa-collapsed .pa-body{display:none;}
@keyframes pa-rotation{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}
#pa-agent-toast{position:fixed;bottom:16px;right:16px;z-index:2147483647;
font-family:-apple-system,system-ui,Segoe UI,Roboto,sans-serif;font-size:12px;
background:#1e1e22;color:#e6e6e6;border:1px solid #3a3a40;border-radius:8px;
padding:8px 12px;box-shadow:0 6px 20px rgba(0,0,0,.45);max-width:280px;
transition:opacity .3s ease;pointer-events:none;}
#pa-agent-toast.pa-fade{opacity:0;}
`;
document.head.appendChild(style);
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.className = 'pa-hidden'; // hidden until revealed
panel.innerHTML =
'<div class="pa-header">' +
'<span class="pa-title">🤖 Page Agent</span>' +
'<button class="pa-iconbtn" data-act="settings" title="Settings">⚙</button>' +
'<button class="pa-iconbtn" data-act="collapse" title="Collapse">—</button>' +
'<button class="pa-iconbtn" data-act="hide" title="Hide (reopen with ' + HOTKEY.label + ' or a three-finger tap)">✕</button>' +
'</div>' +
'<div class="pa-body">' +
'<div class="pa-log"></div>' +
'<div class="pa-row">' +
'<input class="pa-input" type="text" placeholder="Ask the agent to do something…" />' +
'<button class="pa-btn" data-act="send">Send</button>' +
'</div>' +
'<div class="pa-row">' +
'<button class="pa-btn pa-secondary" data-act="convert">Convert to userscript</button>' +
'<button class="pa-btn pa-secondary" data-act="download">Download .user.js</button>' +
'</div>' +
'</div>';
document.body.appendChild(panel);
const logEl = panel.querySelector('.pa-log');
const inputEl = panel.querySelector('.pa-input');
const sendBtn = panel.querySelector('[data-act="send"]');
const convertBtn = panel.querySelector('[data-act="convert"]');
const downloadBtn = panel.querySelector('[data-act="download"]');
const settingsBtn = panel.querySelector('[data-act="settings"]');
const collapseBtn = panel.querySelector('[data-act="collapse"]');
const hideBtn = panel.querySelector('[data-act="hide"]');
const allButtons = [sendBtn, convertBtn, downloadBtn];
function append(text) {
const line = document.createElement('div');
line.textContent = text;
logEl.appendChild(line);
logEl.scrollTop = logEl.scrollHeight; // autoscroll
}
// ============================================================
// Visibility — hidden by default; toggle via hotkey / gesture
// ============================================================
let hintShown = false;
function isHidden() { return panel.classList.contains('pa-hidden'); }
function showPanel() {
panel.classList.remove('pa-hidden');
if (!hintShown) {
append('Reveal/hide me anytime: ' + HOTKEY.label + ' (desktop) or a three-finger tap (mobile).');
hintShown = true;
}
setTimeout(() => inputEl.focus(), 0);
}
function hidePanel() { panel.classList.add('pa-hidden'); }
function togglePanel() { if (isHidden()) showPanel(); else hidePanel(); }
// Transient toast so the reveal shortcut is discoverable on page load.
function showToast(text, ms = 4000) {
const existing = document.getElementById('pa-agent-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.id = 'pa-agent-toast';
toast.textContent = text;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('pa-fade');
setTimeout(() => toast.remove(), 320);
}, ms);
}
// ============================================================
// Busy / loading state
// ============================================================
let busy = false;
let statusEl = null;
function setBusy(on, label) {
busy = on;
inputEl.disabled = on;
allButtons.forEach((b) => { b.disabled = on; });
if (on) {
sendBtn.dataset.label = sendBtn.textContent;
sendBtn.innerHTML = '<span class="pa-spinner"></span>';
statusEl = document.createElement('div');
statusEl.innerHTML = '<span class="pa-spinner"></span> ' + (label || 'thinking…');
logEl.appendChild(statusEl);
logEl.scrollTop = logEl.scrollHeight;
} else {
if (sendBtn.dataset.label) sendBtn.textContent = sendBtn.dataset.label;
if (statusEl && statusEl.parentNode) statusEl.parentNode.removeChild(statusEl);
statusEl = null;
}
}
// ============================================================
// Conversation state + runners
// ============================================================
let conversation = null; // running messages array, reused across turns
let lastCode = ''; // last generated userscript
async function run(request) {
if (busy) return;
if (!CONFIG.apiKey) { append('⚠ No API key set. Click ⚙ to configure.'); return; }
append(' ' + request);
setBusy(true, 'thinking…');
try {
const { answer, messages } = await agent(request, append, conversation);
conversation = messages; // persist for later turns
append('🤖 ' + answer);
} catch (e) {
append('ERROR: ' + (e && e.message ? e.message : String(e)));
} finally {
setBusy(false); // always reset
}
}
function send() {
const v = inputEl.value.trim();
if (!v) return;
inputEl.value = '';
run(v);
}
// ============================================================
// Convert / Download — bake the session into a standalone userscript
// ============================================================
const CONVERT_PROMPT =
'Now convert EVERYTHING you learned and did in this session into a single, ' +
'standalone, repeatable Tampermonkey/Violentmonkey userscript that performs the ' +
'same task automatically with NO LLM calls and no dependence on this agent. Bake ' +
'in the concrete selectors, fetch() URLs, and data transforms you discovered. ' +
'Include a complete ==UserScript== metadata header (@name, @namespace, @version, ' +
'@description, an appropriate @match, @run-at and any @grant/@connect you need). ' +
'The script must be self-contained and idempotent. Output ONLY the finished ' +
'userscript inside a single ' + BT3 + 'javascript code block — no explanation ' +
'before or after.';
function extractCode(text) {
if (!text) return '';
const re = new RegExp(BT3 + 'javascript\\s*([\\s\\S]*?)' + BT3, 'i');
const m = text.match(re);
if (m) return m[1].trim();
return text.trim(); // fallback: raw text
}
async function copyToClipboard(text) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return;
}
} catch (e) { /* fall through to fallback */ }
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.focus();
ta.select();
try { document.execCommand('copy'); } catch (e) { /* ignore */ }
document.body.removeChild(ta);
}
function saveFile(text, filename) {
const blob = new Blob([text], { type: 'text/javascript' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Runs the convert prompt against a COPY of the conversation so the live one stays clean.
async function generateUserscript() {
const copy = JSON.parse(JSON.stringify(conversation));
const { answer } = await agent(CONVERT_PROMPT, append, copy);
lastCode = extractCode(answer);
return lastCode;
}
async function convert() {
if (busy) return;
if (!conversation) { append('⚠ Nothing to convert yet — run the agent first.'); return; }
setBusy(true, 'converting…');
try {
const code = await generateUserscript();
await copyToClipboard(code);
append('✅ Userscript generated & copied to clipboard (' + code.length + ' chars). Use “Download .user.js” to save it.');
} catch (e) {
append('ERROR: ' + (e && e.message ? e.message : String(e)));
} finally {
setBusy(false);
}
}
async function download() {
if (busy) return;
if (!conversation) { append('⚠ Nothing to convert yet — run the agent first.'); return; }
setBusy(true, 'preparing download…');
try {
const code = lastCode || await generateUserscript();
saveFile(code, 'page-agent-generated.user.js');
append('⬇ Saved page-agent-generated.user.js (' + code.length + ' chars).');
} catch (e) {
append('ERROR: ' + (e && e.message ? e.message : String(e)));
} finally {
setBusy(false);
}
}
// ============================================================
// Settings
// ============================================================
async function openSettings() {
const ep = prompt('Endpoint (chat-completions URL):', CONFIG.endpoint);
if (ep !== null) { CONFIG.endpoint = ep.trim() || DEFAULTS.endpoint; await GM.setValue('endpoint', CONFIG.endpoint); }
const key = prompt('API key (Bearer token):', CONFIG.apiKey);
if (key !== null) { CONFIG.apiKey = key.trim(); await GM.setValue('apiKey', CONFIG.apiKey); }
const mdl = prompt('Model:', CONFIG.model);
if (mdl !== null) { CONFIG.model = mdl.trim() || DEFAULTS.model; await GM.setValue('model', CONFIG.model); }
append('⚙ Settings saved — endpoint=' + CONFIG.endpoint + ' model=' + CONFIG.model + ' key=' + (CONFIG.apiKey ? '(set)' : '(empty)'));
}
// ============================================================
// Wiring
// ============================================================
sendBtn.addEventListener('click', send);
inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } });
convertBtn.addEventListener('click', convert);
downloadBtn.addEventListener('click', download);
settingsBtn.addEventListener('click', openSettings);
hideBtn.addEventListener('click', hidePanel);
collapseBtn.addEventListener('click', () => {
const collapsed = panel.classList.toggle('pa-collapsed');
collapseBtn.textContent = collapsed ? '▢' : '—';
collapseBtn.title = collapsed ? 'Expand' : 'Collapse';
});
// Desktop reveal/hide — global hotkey (Ctrl+Shift+Space by default).
window.addEventListener('keydown', (e) => {
if (!!e.ctrlKey === HOTKEY.ctrl && !!e.shiftKey === HOTKEY.shift &&
!!e.altKey === HOTKEY.alt && e.code === HOTKEY.code) {
e.preventDefault();
togglePanel();
} else if (e.key === 'Escape' && !isHidden() && document.activeElement !== inputEl) {
hidePanel();
}
}, true);
// Mobile reveal/hide — three-finger tap anywhere on the page.
window.addEventListener('touchstart', (e) => {
if (e.touches && e.touches.length === 3) {
e.preventDefault();
togglePanel();
}
}, { capture: true, passive: false });
append('Ready. ' + (CONFIG.apiKey ? '' : 'Set your API key via ⚙ first. ') + 'Ask me to inspect or act on this page.');
showToast('🤖 Page Agent loaded — ' + HOTKEY.label + ' (desktop) or three-finger tap (mobile) to open.');
})();
+231
View File
@@ -0,0 +1,231 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
oxlint:
specifier: ^1.65.0
version: 1.73.0
packages:
'@oxlint/binding-android-arm-eabi@1.73.0':
resolution: {integrity: sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@oxlint/binding-android-arm64@1.73.0':
resolution: {integrity: sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@oxlint/binding-darwin-arm64@1.73.0':
resolution: {integrity: sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@oxlint/binding-darwin-x64@1.73.0':
resolution: {integrity: sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@oxlint/binding-freebsd-x64@1.73.0':
resolution: {integrity: sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@oxlint/binding-linux-arm-gnueabihf@1.73.0':
resolution: {integrity: sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxlint/binding-linux-arm-musleabihf@1.73.0':
resolution: {integrity: sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxlint/binding-linux-arm64-gnu@1.73.0':
resolution: {integrity: sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-arm64-musl@1.73.0':
resolution: {integrity: sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-ppc64-gnu@1.73.0':
resolution: {integrity: sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-gnu@1.73.0':
resolution: {integrity: sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-musl@1.73.0':
resolution: {integrity: sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-s390x-gnu@1.73.0':
resolution: {integrity: sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-gnu@1.73.0':
resolution: {integrity: sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-musl@1.73.0':
resolution: {integrity: sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxlint/binding-openharmony-arm64@1.73.0':
resolution: {integrity: sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@oxlint/binding-win32-arm64-msvc@1.73.0':
resolution: {integrity: sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@oxlint/binding-win32-ia32-msvc@1.73.0':
resolution: {integrity: sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
'@oxlint/binding-win32-x64-msvc@1.73.0':
resolution: {integrity: sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
oxlint@1.73.0:
resolution: {integrity: sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
oxlint-tsgolint: '>=0.24.0'
vite-plus: '*'
peerDependenciesMeta:
oxlint-tsgolint:
optional: true
vite-plus:
optional: true
snapshots:
'@oxlint/binding-android-arm-eabi@1.73.0':
optional: true
'@oxlint/binding-android-arm64@1.73.0':
optional: true
'@oxlint/binding-darwin-arm64@1.73.0':
optional: true
'@oxlint/binding-darwin-x64@1.73.0':
optional: true
'@oxlint/binding-freebsd-x64@1.73.0':
optional: true
'@oxlint/binding-linux-arm-gnueabihf@1.73.0':
optional: true
'@oxlint/binding-linux-arm-musleabihf@1.73.0':
optional: true
'@oxlint/binding-linux-arm64-gnu@1.73.0':
optional: true
'@oxlint/binding-linux-arm64-musl@1.73.0':
optional: true
'@oxlint/binding-linux-ppc64-gnu@1.73.0':
optional: true
'@oxlint/binding-linux-riscv64-gnu@1.73.0':
optional: true
'@oxlint/binding-linux-riscv64-musl@1.73.0':
optional: true
'@oxlint/binding-linux-s390x-gnu@1.73.0':
optional: true
'@oxlint/binding-linux-x64-gnu@1.73.0':
optional: true
'@oxlint/binding-linux-x64-musl@1.73.0':
optional: true
'@oxlint/binding-openharmony-arm64@1.73.0':
optional: true
'@oxlint/binding-win32-arm64-msvc@1.73.0':
optional: true
'@oxlint/binding-win32-ia32-msvc@1.73.0':
optional: true
'@oxlint/binding-win32-x64-msvc@1.73.0':
optional: true
oxlint@1.73.0:
optionalDependencies:
'@oxlint/binding-android-arm-eabi': 1.73.0
'@oxlint/binding-android-arm64': 1.73.0
'@oxlint/binding-darwin-arm64': 1.73.0
'@oxlint/binding-darwin-x64': 1.73.0
'@oxlint/binding-freebsd-x64': 1.73.0
'@oxlint/binding-linux-arm-gnueabihf': 1.73.0
'@oxlint/binding-linux-arm-musleabihf': 1.73.0
'@oxlint/binding-linux-arm64-gnu': 1.73.0
'@oxlint/binding-linux-arm64-musl': 1.73.0
'@oxlint/binding-linux-ppc64-gnu': 1.73.0
'@oxlint/binding-linux-riscv64-gnu': 1.73.0
'@oxlint/binding-linux-riscv64-musl': 1.73.0
'@oxlint/binding-linux-s390x-gnu': 1.73.0
'@oxlint/binding-linux-x64-gnu': 1.73.0
'@oxlint/binding-linux-x64-musl': 1.73.0
'@oxlint/binding-openharmony-arm64': 1.73.0
'@oxlint/binding-win32-arm64-msvc': 1.73.0
'@oxlint/binding-win32-ia32-msvc': 1.73.0
'@oxlint/binding-win32-x64-msvc': 1.73.0
+22
View File
@@ -0,0 +1,22 @@
// ==UserScript==
// @name PWA Everything
// @author Evan Reichard
// @version 0.0.1
// @match *://*/*
// @grant none
// @run-at document-idle
// @noframes
// ==/UserScript==
let webManifest = {
"name": "",
"short_name": "",
"theme_color": "#ff0000",
"background_color": "#ff0000",
"display": "standalone"
};
let manifestElem = document.createElement('link');
manifestElem.setAttribute('rel', 'manifest');
manifestElem.setAttribute('href', 'data:application/manifest+json;base64,' + btoa(JSON.stringify(webManifest)));
document.head.prepend(manifestElem);
+229
View File
@@ -0,0 +1,229 @@
// ==UserScript==
// @name XenOrchestra NG Theme
// @match https://10.0.50.100/*
// @grant none
// @version 0.1
// @author Evan Reichard
// ==/UserScript==
(function() {
'use strict';
const css = `
/* ------------------------------------- */
/* ---------- Menu & Sub-Menu ---------- */
/* ------------------------------------- */
.xo-menu, .xo-sub-menu {
background-color: #111225;
padding: 5px;
border: 1px solid #363647;
}
.xo-menu + div > div {
overflow: unset !important;
overflow-x: auto !important;
}
.nav-tabs {
text-transform: uppercase;
border-top: 1px solid #363647;
border-bottom: 0px;
float: left;
min-width: max-content;
font-size: 1rem;
width: 100%;
}
.nav-tabs a.nav-link,
.nav-tabs a.nav-link:focus,
.nav-tabs a.nav-link:hover,
.nav-tabs a.nav-link.active,
.nav-tabs a.nav-link.active:focus,
.nav-tabs a.nav-link.active:hover {
color: #f8f8f8;
border-bottom: 2px solid #8f84ff;
}
.nav-tabs a.nav-link:hover {
background-color: #393566;
}
.nav-tabs a.nav-link {
border-radius: 0px;
border: 0px;
}
.nav-tabs .nav-item + .nav-item {
margin-left: 0px;
}
.xo-menu-item:hover, .nav-link.active {
background-color: #2b284d !important;
}
.xo-menu-item, .nav-link.active {
border-radius: 15px;
}
.nav-link.active:hover {
background-color: #393566 !important;
}
/* ------------------------------------- */
/* ------------ Global Style ----------- */
/* ------------------------------------- */
/* HTML Text Color */
table, .form-group, .ct-label, .ct-point, h2, #newSrForm, .table, h3 {
color: white;
}
/* SVG Text Color */
text {
fill: white;
}
.table thead th {
border-bottom: 2px solid #363647;
}
.thead-default th {
color: #e0e0e0;
background-color: #1e1e2d;
}
.table td, .table th {
border-top: 1px solid #363647;
}
.card-header {
color: white;
background-color: #0a0a0a;
}
.card, .list-group-item, tr {
background-color: black;
color: white;
border-radius: 10px;
}
/* ------------------------------------- */
/* ------------ Page Header ------------ */
/* ------------------------------------- */
.page-header {
padding: 0px;
border-radius: 0px;
}
.page-header .container-fluid {
padding: 0px !important;
border-radius: 0px;
padding: 0px;
}
.page-header > div > div:first-child::after {
content: none;
}
.header-title + div {
text-align: unset !important;
width: unset !important;
}
/* Header Tabs Horizontal Scroll */
.page-header > div > div:first-child > div:has(.pull-right) {
overflow-y: hidden;
}
.page-header > .container-fluid > div:not([class*="_itemRowHeader"]):first-child > div:first-child {
margin: 10px !important;
}
/* ------------------------------------- */
/* -------------- VM Page -------------- */
/* ------------------------------------- */
/* VM List Buttons */
.page-header > .container-fluid > [class*="_itemRowHeader"].row {
display: flex;
width: 100%;
padding: 10px;
align-items: center;
}
.page-header > .container-fluid > [class*="_itemRowHeader"].row:nth-child(2) > .hidden-sm-down {
display: flex;
justify-content: center;
}
/* Expanded VM Graphs */
.row > [class*="_itemExpanded"]:has(.row > [class*="_itemExpanded"]) {
/* width: 100%; */
float: right;
}
/* VM Page Start / Stop Buttons */
.row:has(.header-title) > div:nth-child(2):has(div > .btn-group) {
margin: 20px;
float: right;
}
.row > div {
padding: 0px;
}
div:has(> .nav.nav-tabs) {
width: 100%;
}
.row {
margin: 0px !important;
}
nav + div {
overflow-y: unset !important;
background-color: black;
padding: 10px !important;
border: 1px solid #363647;
}
.container-fluid, [class$="_itemContainer"], #vmCreation, #networkCreation {
background-color: #111225;
color: white;
padding: 20px !important;
border-radius: 10px;
border: 1px solid #363647;
}
.container-fluid > .container-fluid {
background-color: transparent;
padding: 0;
border-radius: 0;
border: none;
}
[class$="_itemContainer"] [class$="_item"] {
border-bottom: 1px solid #363647;
}
[class$="_itemContainer"] [class$="_item"]:hover {
background-color: #393566 !important;
}
/* ------------------------------------- */
/* ------------- VM Console ------------ */
/* ------------------------------------- */
div:has(input[value="1"]) ~ .console canvas {
width: 100% !important;
height: unset !important;
}
`;
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
})();
+25
View File
@@ -0,0 +1,25 @@
// ==UserScript==
// @name Yattee Redirect
// @version 0.0.3
// @updateURL https://gist.github.com/evanreichard/8db561c7be7a6c308db18ce3e983c8d6/raw/yattee-redirect.user.js
// @match https://www.youtube.com/*
// @match https://m.youtube.com/*
// @grant none
// @noframes
// @run-at document-start
// ==/UserScript==
function attemptRedirect(){
if (document.location.pathname == "/watch") {
let newPath = document.location.href.split("https://" + document.location.host + "/")[1];
document.location.href = "yattee://" + newPath
}
}
var pushState = history.pushState;
history.pushState = function () {
pushState.apply(history, arguments);
attemptRedirect();
};
attemptRedirect();