← Commit history
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"