← Commit history
Publish 1.0.0
9 files changed
+573
SKILL.mdadded+74@@ -0,0 +1,74 @@+---+name: wiki-light-mode+description: Screenshot any Adom Wiki page (wiki.adom.inc) in synthesized LIGHT MODE and show it to the human via adom-shotlog. The wiki ships dark-only, so this logs a headless Chrome in as the user (single-use adom-wiki magic link, private pages included), applies an invert overlay that keeps images true-color, pixel-verifies the result is really light, and injects the shots into shotlog. Trigger words - wiki light mode, show me the wiki page in light mode, light version of a wiki page, screenshot a wiki page, capture wiki page, grab a wiki page and show it to me, render wiki page as image, wiki page in shotlog, white background wiki screenshot, invert the wiki.+user-invocable: true+---++# wiki-light-mode++Capture an Adom Wiki page as a light-mode screenshot and put it in front of the+human. One command does everything:++```bash+wiki-light-shot adom/puma-bridge+```++That logs a headless Chrome into wiki.adom.inc as the current user (so private+pages work), renders the page, applies the light-mode overlay, saves a top-of-+page shot (1920x1200) and a full-page shot, verifies at the pixel level that the+result is genuinely light, and injects both into the adom-shotlog channel named+after the page slug. The broker surfaces the channel to the human; do not open+or foreground anything yourself.++## Why "synthesized" light mode++wiki.adom.inc is dark-only by design: `color-scheme: dark`, no theme toggle, no+light palette. There is nothing to switch on. This tool inverts the page+(`invert(1) hue-rotate(180deg)`) and re-inverts raster media (`img`, `video`,+`canvas`, `iframe`) so heroes and screenshots keep their true colors. The output+reads as a real light theme: white chrome, dark text, accents intact.++## Usage++```bash+wiki-light-shot <owner/slug | https://wiki.adom.inc/...> [options]++ --out DIR output directory (default ./wiki-light-shots)+ --channel NAME shotlog channel (default: the page slug)+ --no-inject save PNGs only, skip shotlog+ --no-auth skip login (public pages only; private pages 404)+ --keep-dark capture the page as-is, no overlay+ --viewport WxH default 1920x1200+ --full-max PX cap full-page height (default 8000)+ --json { status, data, hints[] } envelope+```++Read the OK/WARN/ERROR + Hint lines: they tell you whether login worked, whether+the shot verified light (mean luminance out of 255), and whether a human viewer+is on-screen in shotlog.++## What it refuses and why++- Any host other than wiki.adom.inc. The browser is logged in as the USER; the+ tool will not carry that session anywhere else, and it blocks non-wiki+ subresource requests too.+- Pages that 404. With auth that means a wrong slug (check with+ `adom-wiki page stats <owner>/<slug>`); with `--no-auth` it usually means the+ page is private - drop the flag.++## Failure modes it self-heals++- puppeteer-core missing: auto-installed into the skill package on first run+ (`--no-save`, so the wiki package.json is never touched).+- Login link expired (they are single-use, 60s): a fresh link is minted per+ attempt, with one retry.+- No local Chrome: it searches the puppeteer cache and system paths; override+ with `WIKI_LIGHT_CHROME=/path/to/chrome`. Last resort:+ `npx @puppeteer/browsers install chrome@stable`.+- Result still dark (wiki markup changed): a pixel-luminance check WARNs loudly+ instead of silently handing the human a dark shot.++## Remember++You cannot show images inline in a text thread. The shotlog inject IS the+delivery; point the human at the shotlog viewer, never at "the image above".
bin/wiki-light-shot.mjsadded+391@@ -0,0 +1,391 @@+#!/usr/bin/env node+// wiki-light-shot — render an Adom Wiki page in synthesized LIGHT MODE and+// screenshot it (top viewport + full page), optionally injecting the shots+// into adom-shotlog so the human actually sees them.+//+// The wiki ships dark-only (color-scheme: dark, no light palette). This tool+// logs a headless Chrome in via a single-use `adom-wiki auth login` magic link+// (so private pages work), applies an invert+hue-rotate overlay with raster+// media re-inverted (hero/screenshots keep true colors), verifies at the pixel+// level that the result is genuinely light, and saves/injects the shots.+//+// Contract: OK:/WARN:/ERROR: + Hint: lines. Exit 0 = shots saved, 1 = failure.++import { execFileSync, spawnSync } from 'node:child_process';+import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';+import { createRequire } from 'node:module';+import { homedir } from 'node:os';+import { dirname, join, resolve } from 'node:path';+import { fileURLToPath } from 'node:url';+import zlib from 'node:zlib';++const PKG_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..');+const WIKI_HOST = 'wiki.adom.inc';+const NAV_TIMEOUT_MS = 45000;+const SETTLE_MS = 2500;++// ---------- output helpers ----------+const jsonMode = process.argv.includes('--json');+const lines = [];+function say(line) { if (jsonMode) lines.push(line); else console.log(line); }+const ok = (m) => say(`OK: ${m}`);+const warn = (m) => say(`WARN: ${m}`);+const hint = (m) => say(`Hint: ${m}`);+function die(msg, ...hints) {+ if (jsonMode) {+ console.log(JSON.stringify({ status: 'error', error: msg, hints, log: lines }, null, 2));+ } else {+ console.error(`ERROR: ${msg}`);+ for (const h of hints) console.error(`Hint: ${h}`);+ }+ process.exit(1);+}++// ---------- args ----------+function usage() {+ say('wiki-light-shot — screenshot an Adom Wiki page in synthesized light mode');+ say('');+ say('Usage: wiki-light-shot <owner/slug | https://wiki.adom.inc/...> [options]');+ say('');+ say('Options:');+ say(' --out DIR output directory (default: ./wiki-light-shots)');+ say(' --channel NAME shotlog channel (default: the page slug)');+ say(' --no-inject save PNGs only, skip adom-shotlog');+ say(' --no-auth skip login (public pages only; private pages 404)');+ say(' --keep-dark capture as-is without the light-mode overlay');+ say(' --viewport WxH viewport size (default 1920x1200)');+ say(' --full-max PX cap full-page capture height (default 8000)');+ say(' --json machine-readable { status, data, hints[] } envelope');+ process.exit(process.argv.length <= 2 ? 1 : 0);+}++const VALUE_FLAGS = new Set(['--out', '--channel', '--viewport', '--full-max']);+const BOOL_FLAGS = new Set(['--no-inject', '--no-auth', '--keep-dark', '--json', '--help', '-h']);+const flags = {};+const positionals = [];+const rawArgs = process.argv.slice(2);+for (let i = 0; i < rawArgs.length; i++) {+ const a = rawArgs[i];+ if (VALUE_FLAGS.has(a)) {+ const v = rawArgs[++i];+ if (v === undefined || v.startsWith('--')) die(`${a} needs a value`);+ flags[a] = v;+ } else if (BOOL_FLAGS.has(a)) flags[a] = true;+ else if (a.startsWith('--')) die(`unknown flag ${a}`, 'run with --help for usage');+ else positionals.push(a);+}+if (rawArgs.length === 0 || flags['--help'] || flags['-h']) usage();++const flagVal = (name, dflt) => flags[name] ?? dflt;+const hasFlag = (name) => !!flags[name];++if (positionals.length !== 1) {+ die(positionals.length === 0 ? 'no page reference given' : `expected one page reference, got: ${positionals.join(', ')}`,+ 'pass owner/slug (e.g. adom/puma-bridge) or a full wiki.adom.inc URL');+}+const ref = positionals[0];++const outDir = resolve(flagVal('--out', './wiki-light-shots'));+const noInject = hasFlag('--no-inject');+const noAuth = hasFlag('--no-auth');+const keepDark = hasFlag('--keep-dark');+const vpRaw = flagVal('--viewport', '1920x1200');+const vpMatch = /^(\d{3,4})x(\d{3,4})$/.exec(vpRaw);+if (!vpMatch) die(`bad --viewport '${vpRaw}'`, 'use WxH, e.g. 1920x1200');+const viewport = { width: +vpMatch[1], height: +vpMatch[2] };+const fullMax = Math.max(viewport.height, parseInt(flagVal('--full-max', '8000'), 10) || 8000);++// ---------- resolve + validate the target URL (never leak the session elsewhere) ----------+let pageUrl, slug;+if (/^https?:\/\//i.test(ref)) {+ let u;+ try { u = new URL(ref); } catch { die(`'${ref}' is not a valid URL`); }+ if (u.hostname !== WIKI_HOST) {+ die(`refusing to open ${u.hostname} — this tool logs the browser in as YOU and only navigates ${WIKI_HOST}`,+ 'pass an owner/slug or a https://wiki.adom.inc/... URL');+ }+ pageUrl = u.href;+ slug = u.pathname.split('/').filter(Boolean).slice(0, 2).join('-') || 'wiki-page';+} else {+ const m = /^([A-Za-z0-9][\w.-]*)\/([A-Za-z0-9][\w.-]*)$/.exec(ref);+ if (!m) die(`'${ref}' is neither owner/slug nor a wiki URL`, 'examples: adom/puma-bridge, https://wiki.adom.inc/adom/core');+ pageUrl = `https://${WIKI_HOST}/${m[1]}/${m[2]}`;+ slug = m[2];+}+const channel = flagVal('--channel', slug);++// ---------- find Chrome ----------+function findChrome() {+ const envPath = process.env.WIKI_LIGHT_CHROME;+ if (envPath) {+ if (existsSync(envPath)) return envPath;+ die(`WIKI_LIGHT_CHROME points at '${envPath}' which does not exist`, 'unset it or fix the path');+ }+ for (const flavor of ['chrome', 'chrome-headless-shell']) {+ const base = join(homedir(), '.cache', 'puppeteer', flavor);+ if (!existsSync(base)) continue;+ const versions = readdirSync(base).filter((d) => d.startsWith('linux-'))+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));+ for (const v of versions) {+ for (const sub of [`chrome-linux64/chrome`, `chrome-headless-shell-linux64/chrome-headless-shell`]) {+ const p = join(base, v, sub);+ if (existsSync(p)) return p;+ }+ }+ }+ for (const name of ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser']) {+ const r = spawnSync('which', [name], { encoding: 'utf8' });+ if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();+ }+ return null;+}++// ---------- puppeteer-core, self-healing ----------+async function loadPuppeteer() {+ const req = createRequire(join(PKG_DIR, 'package.json'));+ try { return req('puppeteer-core'); } catch {}+ try { return (await import('puppeteer-core')).default; } catch {}+ warn('puppeteer-core not found — installing it into the skill package (one-time)');+ // --no-save is load-bearing: this package.json's "dependencies" holds WIKI+ // deps (owner/slug refs) — letting npm write into it would corrupt them.+ const r = spawnSync('npm', ['install', 'puppeteer-core@^22', '--no-save', '--no-package-lock', '--no-audit', '--no-fund', '--silent'],+ { cwd: PKG_DIR, encoding: 'utf8', timeout: 180000 });+ if (r.status !== 0) {+ die('could not install puppeteer-core', `run manually: cd ${PKG_DIR} && npm i puppeteer-core@^22`,+ r.stderr ? `npm said: ${String(r.stderr).trim().slice(0, 300)}` : 'is npm on PATH?');+ }+ try { return req('puppeteer-core'); }+ catch (e) { die(`puppeteer-core installed but still not loadable: ${e.message}`); }+}++// ---------- auth ----------+function mintLoginUrl() {+ try {+ const out = execFileSync('adom-wiki', ['auth', 'login', '--json'], { encoding: 'utf8', timeout: 20000 });+ const parsed = JSON.parse(out);+ const url = parsed?.data?.logged_in_url;+ if (url && new URL(url).hostname === WIKI_HOST) return url;+ warn('adom-wiki auth login returned no usable link — continuing anonymously');+ } catch (e) {+ warn(`could not mint a wiki login link (${String(e.message).split('\n')[0]}) — continuing anonymously`);+ hint("private pages will 404 without auth; check `adom-wiki auth token` / `adom-wiki whoami`");+ }+ return null;+}++// ---------- tiny PNG luminance check (proof the shot is really light) ----------+// Supports what Chrome emits: 8-bit RGB/RGBA, non-interlaced.+function pngMeanLuminance(buf) {+ try {+ if (buf.readUInt32BE(0) !== 0x89504e47) return null;+ let pos = 8, width = 0, height = 0, colorType = -1, bitDepth = 0, interlace = 0;+ const idat = [];+ while (pos < buf.length) {+ const len = buf.readUInt32BE(pos);+ const type = buf.toString('ascii', pos + 4, pos + 8);+ const data = buf.subarray(pos + 8, pos + 8 + len);+ if (type === 'IHDR') {+ width = data.readUInt32BE(0); height = data.readUInt32BE(4);+ bitDepth = data[8]; colorType = data[9]; interlace = data[12];+ } else if (type === 'IDAT') idat.push(data);+ else if (type === 'IEND') break;+ pos += 12 + len;+ }+ if (bitDepth !== 8 || interlace !== 0 || (colorType !== 2 && colorType !== 6)) return null;+ const bpp = colorType === 6 ? 4 : 3;+ const raw = zlib.inflateSync(Buffer.concat(idat));+ const stride = width * bpp;+ let sum = 0, n = 0;+ const prev = Buffer.alloc(stride);+ const cur = Buffer.alloc(stride);+ const rowStep = Math.max(1, Math.floor(height / 240)); // sample ~240 rows+ const pxStep = Math.max(1, Math.floor(width / 320)) * bpp;+ for (let y = 0; y < height; y++) {+ const filter = raw[y * (stride + 1)];+ raw.copy(cur, 0, y * (stride + 1) + 1, (y + 1) * (stride + 1));+ for (let i = 0; i < stride; i++) {+ const a = i >= bpp ? cur[i - bpp] : 0, b = prev[i], c = i >= bpp ? prev[i - bpp] : 0;+ let v = cur[i];+ if (filter === 1) v = (v + a) & 255;+ else if (filter === 2) v = (v + b) & 255;+ else if (filter === 3) v = (v + ((a + b) >> 1)) & 255;+ else if (filter === 4) {+ const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);+ v = (v + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c)) & 255;+ }+ cur[i] = v;+ }+ if (y % rowStep === 0) {+ for (let i = 0; i + 2 < stride; i += pxStep) {+ sum += 0.2126 * cur[i] + 0.7152 * cur[i + 1] + 0.0722 * cur[i + 2];+ n++;+ }+ }+ cur.copy(prev);+ }+ return n ? sum / n : null;+ } catch { return null; }+}++// ---------- shotlog ----------+function shotlogAvailable() {+ const r = spawnSync('adom-shotlog', ['health'], { encoding: 'utf8', timeout: 8000 });+ return r.status === 0;+}+function inject(file, desc) {+ const r = spawnSync('adom-shotlog',+ ['inject', '-c', channel, '-d', desc, '-s', 'headless-chrome', '--thread', 'wiki light mode', file],+ { encoding: 'utf8', timeout: 30000 });+ const out = (r.stdout || '') + (r.stderr || '');+ if (r.status !== 0) { warn(`shotlog inject failed for ${file}: ${out.trim().slice(0, 200)}`); return false; }+ const seen = out.split('\n').find((l) => l.includes('viewer(s)'));+ if (seen) say(seen.trim());+ return true;+}++// ---------- main ----------+const chromePath = findChrome();+if (!chromePath) {+ die('no Chrome found', 'set WIKI_LIGHT_CHROME=/path/to/chrome, or install one: npx @puppeteer/browsers install chrome@stable');+}+const puppeteer = await loadPuppeteer();+mkdirSync(outDir, { recursive: true });++let browser;+const cleanup = async () => { try { await browser?.close(); } catch {} };+process.on('SIGINT', async () => { await cleanup(); process.exit(1); });+process.on('SIGTERM', async () => { await cleanup(); process.exit(1); });++try {+ browser = await puppeteer.launch({+ executablePath: chromePath,+ headless: 'new',+ args: ['--no-sandbox', '--disable-dev-shm-usage', '--hide-scrollbars'],+ });+} catch (e) {+ die(`Chrome failed to launch (${chromePath}): ${String(e.message).split('\n')[0]}`,+ 'try a different binary via WIKI_LIGHT_CHROME');+}++try {+ const page = await browser.newPage();+ await page.setViewport({ ...viewport, deviceScaleFactor: 1 });+ await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'light' }]);++ // Belt-and-braces: block requests to any non-wiki host except same-page assets.+ await page.setRequestInterception(true);+ page.on('request', (r) => {+ try {+ const h = new URL(r.url()).hostname;+ if (h === WIKI_HOST || h.endsWith('.r2.cloudflarestorage.com') || r.url().startsWith('data:')) r.continue();+ else r.abort('blockedbyclient');+ } catch { r.abort('blockedbyclient'); }+ });++ // Log in (single-use link, fresh every run). One retry with a fresh link.+ let authed = false;+ if (!noAuth) {+ for (let attempt = 0; attempt < 2 && !authed; attempt++) {+ const loginUrl = mintLoginUrl();+ if (!loginUrl) break;+ try {+ const r = await page.goto(loginUrl, { waitUntil: 'networkidle2', timeout: NAV_TIMEOUT_MS });+ // The session cookie is HttpOnly, so a clean 2xx/3xx landing is the signal.+ authed = !!r && r.status() < 400;+ if (!authed) warn(`login link landed with HTTP ${r?.status?.()} (link expired? links are single-use, 60s)`);+ } catch (e) {+ warn(`login navigation failed (attempt ${attempt + 1}): ${String(e.message).split('\n')[0]}`);+ }+ }+ if (authed) ok('logged in via single-use adom-wiki magic link');+ else warn('proceeding anonymously — private pages will 404');+ }++ // Navigate to the page, one retry (the session cookie survives retries).+ let resp = null;+ for (let attempt = 0; attempt < 2; attempt++) {+ try {+ resp = await page.goto(pageUrl, { waitUntil: 'networkidle2', timeout: NAV_TIMEOUT_MS });+ break;+ } catch (e) {+ if (attempt === 1) die(`could not load ${pageUrl}: ${String(e.message).split('\n')[0]}`,+ 'is wiki.adom.inc reachable from this container?');+ warn('page load timed out, retrying once');+ }+ }+ const status = resp?.status?.() ?? 0;+ if (status === 404) {+ await cleanup();+ die(`the wiki returned 404 for ${pageUrl}`,+ authed ? 'the page does not exist (check the slug with `adom-wiki page stats <owner>/<slug>`) or your account lacks access'+ : 'the page may be private — retry without --no-auth so the tool can log in');+ }+ if (status >= 400) { await cleanup(); die(`the wiki returned HTTP ${status} for ${pageUrl}`); }+ await new Promise((r) => setTimeout(r, SETTLE_MS));++ // Synthesized light mode: invert the page, re-invert raster media so the+ // hero and screenshots keep their true colors.+ if (!keepDark) {+ await page.addStyleTag({ content: `+ html { filter: invert(1) hue-rotate(180deg) !important; color-scheme: light !important; background: #ffffff !important; }+ img, video, iframe, canvas, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; }+ ` });+ await new Promise((r) => setTimeout(r, 600));+ }++ const meta = await page.evaluate(() => ({+ title: document.title,+ scrollHeight: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight || 0),+ }));++ const mode = keepDark ? 'dark' : 'light';+ const topFile = join(outDir, `${slug}-${mode}-top.png`);+ const fullFile = join(outDir, `${slug}-${mode}-full.png`);+ await page.screenshot({ path: topFile });++ const fullH = Math.min(meta.scrollHeight, fullMax);+ if (meta.scrollHeight > fullMax) {+ warn(`page is ${meta.scrollHeight}px tall — full capture clipped to ${fullMax}px (raise with --full-max)`);+ }+ await page.screenshot({ path: fullFile, clip: { x: 0, y: 0, width: viewport.width, height: fullH } });+ await cleanup();++ // Pixel-level proof: is the shot actually light?+ const lum = pngMeanLuminance(readFileSync(topFile));+ if (!keepDark) {+ if (lum === null) warn('could not verify luminance (unexpected PNG format) — eyeball the shot');+ else if (lum < 100) warn(`the capture still reads DARK (mean luminance ${lum.toFixed(0)}/255) — the wiki markup may have changed; inspect ${topFile}`);+ else ok(`light mode verified: mean luminance ${lum.toFixed(0)}/255`);+ } else if (lum !== null) {+ ok(`captured as-is (mean luminance ${lum.toFixed(0)}/255)`);+ }++ ok(`saved ${topFile} (${viewport.width}x${viewport.height}) and ${fullFile} (${viewport.width}x${fullH})`);++ // Show the human, via shotlog.+ let injected = false;+ if (!noInject) {+ if (shotlogAvailable()) {+ const label = keepDark ? 'as-is (dark)' : 'in light mode (wiki is dark-only; light synthesized via CSS invert)';+ injected = inject(topFile, `wiki ${ref} ${label} - top of page`);+ injected = inject(fullFile, `wiki ${ref} ${label} - full page`) || injected;+ if (injected) ok(`injected into shotlog channel '${channel}' — the human sees it in the shotlog viewer`);+ } else {+ warn('adom-shotlog is not available — shots saved to disk only');+ hint('install it (`adom-wiki pkg install adom/adom-shotlog`) so the human can actually SEE the shots; you cannot show images in a text thread');+ }+ }++ if (jsonMode) {+ console.log(JSON.stringify({+ status: 'ok',+ data: { page: pageUrl, title: meta.title, mode, top: topFile, full: fullFile,+ meanLuminance: lum, channel: injected ? channel : null, authed },+ hints: [], log: lines,+ }, null, 2));+ }+ process.exit(0);+} catch (e) {+ await cleanup();+ die(`capture failed: ${String(e.stack || e.message).split('\n').slice(0, 3).join(' | ')}`);+}
docs/HERO.mdadded+8@@ -0,0 +1,8 @@+Generate `hero.png` for this folder in Hero Studio. It is the page-header image, reused on the homepage and the screensaver; the page overlays live stats on its lower third, so the image carries the title and artwork only.++New app/skill heroes MUST come from Hero Studio (the server rejects anything else with HERO_STUDIO_REQUIRED; the studio stamps provenance into the PNG and exports at 2000x1250):++1. `adom-wiki pkg install adom/hero-studio`+2. start it (`hero-studio`, port 8846) and open the webview so the human sees it+3. pose the hero (the UI, or POST /state), then Generate+4. copy `generated/latest.png` here as `docs/hero.png`
docs/example-dark.pngadded⋯ 1 unchanged line ⋯
docs/example-light.pngadded⋯ 1 unchanged line ⋯
docs/hero.pngadded⋯ 1 unchanged line ⋯
install.shadded+39@@ -0,0 +1,39 @@+#!/bin/bash+# install hook for adom/wiki-light-mode: expose the CLI, deploy the skill,+# and best-effort pre-warm the node dependency (runtime self-heals if this+# step fails, so nothing here is fatal except a missing node).+set -e+HERE="$(cd "$(dirname "$0")" && pwd)"++if ! command -v node >/dev/null 2>&1; then+ echo "ERROR: node is required (18+) and was not found on PATH"+ exit 1+fi+NODE_MAJOR="$(node -e 'console.log(process.versions.node.split(".")[0])')"+if [ "$NODE_MAJOR" -lt 18 ]; then+ echo "ERROR: node >= 18 required, found $(node --version)"+ exit 1+fi++# CLI onto PATH (symlink back into the module dir so pkg updates propagate)+mkdir -p "$HOME/.local/bin"+chmod +x "$HERE/bin/wiki-light-shot.mjs"+ln -sfn "$HERE/bin/wiki-light-shot.mjs" "$HOME/.local/bin/wiki-light-shot"+echo "OK: wiki-light-shot on PATH (~/.local/bin/wiki-light-shot)"++# Skill for the agent (symlinked so pkg updates propagate without reinstall)+mkdir -p "$HOME/.claude/skills/wiki-light-mode"+ln -sfn "$HERE/SKILL.md" "$HOME/.claude/skills/wiki-light-mode/SKILL.md"+echo "OK: skill installed at ~/.claude/skills/wiki-light-mode"++# Pre-warm puppeteer-core so the first run is fast. --no-save is load-bearing:+# this package.json's "dependencies" holds WIKI refs, npm must not write to it.+if command -v npm >/dev/null 2>&1; then+ (cd "$HERE" && npm install puppeteer-core@^22 --no-save --no-package-lock --no-audit --no-fund --silent) \+ && echo "OK: puppeteer-core ready" \+ || echo "NOTE: puppeteer-core pre-install failed; the CLI installs it on first run"+else+ echo "NOTE: npm not found; the CLI installs puppeteer-core on first run"+fi++echo "OK: wiki-light-mode installed. Try: wiki-light-shot adom/core"
package.jsonadded+56@@ -0,0 +1,56 @@+{+ "slug": "wiki-light-mode",+ "title": "Wiki Light Mode",+ "type": "skill",+ "version": "1.0.0",+ "description": "Screenshot any Adom Wiki page in synthesized light mode and show it to the human via adom-shotlog. Logs a headless Chrome in as the user (single-use magic link, private pages included), applies an invert overlay that keeps images true-color, pixel-verifies the shot is really light, and injects top + full-page captures into shotlog.",+ "license": "MIT",+ "tags": [+ "skill",+ "wiki",+ "screenshot",+ "light-mode",+ "shotlog",+ "headless-chrome"+ ],+ "dependencies": {},+ "discovery_triggers": [+ "wiki light mode",+ "show me the wiki page in light mode",+ "light version of a wiki page",+ "screenshot a wiki page",+ "grab a wiki page and show it to me",+ "wiki page in shotlog"+ ],+ "discovery_pitch": "Capture any wiki.adom.inc page (private included) as a verified light-mode screenshot and surface it to the human through adom-shotlog.",+ "sample_prompts": [+ {+ "label": "Light mode",+ "prompt": "grab wiki.adom.inc/adom/puma-bridge and show it to me in shotlog as light mode"+ },+ {+ "label": "Compare",+ "prompt": "capture the adom/core wiki page in both light and dark and put them in shotlog"+ },+ {+ "label": "Just the file",+ "prompt": "save a light-mode screenshot of the adom-shotlog wiki page to disk"+ }+ ],+ "files": [+ "SKILL.md",+ "README.md",+ "package.json",+ "install.sh",+ "uninstall.sh",+ "bin/**",+ "docs/**"+ ],+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "hero": {+ "path": "docs/hero.png"+ }+}
uninstall.shadded+5@@ -0,0 +1,5 @@+#!/bin/bash+set -e+rm -f "$HOME/.local/bin/wiki-light-shot"+rm -rf "$HOME/.claude/skills/wiki-light-mode"+echo "OK: wiki-light-mode uninstalled"