--- name: ac-tools description: "Drive the client-side developer tools at https://aaronchartier.com from a browser-capable agent — each tool's params, returns and a worked example, plus the run() contract." type: skill url: https://aaronchartier.com/SKILL.md --- # ac-tools — Aaron Chartier's client-side tools > This is the how-to for the tools at https://aaronchartier.com. They are deterministic functions that run entirely in your browser — nothing you pass to a tool is transmitted to the server. This file is the contract for driving them from a browser-capable agent (headless Chrome, Playwright, or an in-browser assistant), plus a reference to what each tool takes and returns. ## How to drive a tool 1. Open `https://aaronchartier.com/tools/` in a browser context — headless is fine. 2. Wait until `window.__tools[""]` exists; the page hydrates an island that registers it. 3. Optionally call `describe()` for the live schema, then call `run(args)`. ```js const t = window.__tools["base64"]; t.describe(); // { slug, params, returns, async?, guide?, example? } const r = await t.run({ input: "hello agent", mode: "encode" }); // r.output === "aGVsbG8gYWdlbnQ=" // the return value is the result. #tool-output reflects the UI / #fragment // path, not a programmatic run() — read run()'s return value instead. ``` - `run(args)` takes a plain object; the accepted keys are listed per tool below. - `run()` returns a value **or a Promise — always `await` it** (e.g. `hash`). - **Output:** `run()`'s return value is the result. The page's `#tool-output` element shows results from the UI and `#fragment` URL-invocation paths; a direct `window.__tools[...].run()` does not update it — use the return value. - Most of these are pure, deterministic functions (base64, hash, uuid, radix, case, json, url, timestamp, count, diff, regex). If you already have those capabilities you can compute them yourself; the browser path matters most where the capability is browser-only. - **URL prefill:** append `#key=value` to a tool URL to pre-fill inputs and auto-run. The fragment is never sent to the server. Prefill key names are documented in each tool's `describe()`. - **WebMCP:** where the browser exposes `document.modelContext` (experimental — not in stable browsers yet; `navigator.modelContext` on older preview runtimes) every page registers every live tool with a headless run, read-only. The interactive PDF editor (`sign`) and the image converter (`image`) have no headless run and are not registered — drive them via `window.__tools` on their own pages. The `window.__tools` API above is the path that works today. ## Tools ### base64 — base64 encoder / decoder Unicode-safe Base64 encode and decode, with URL-safe variant. Runs client-side; no input is transmitted. - params: - `input` — string — text (encode) or base64 (decode) - `mode` — 'encode' | 'decode' (default encode) - `variant` — 'standard' | 'url' (default standard) - returns: `{"mode":"string","variant":"string","output":"string","byteLength":"number"}` - example: `await window.__tools["base64"].run({"input":"hello agent","mode":"encode"})` ### url — url encoder / decoder Percent-encode and decode URLs and query components. Runs client-side; no input is transmitted. - params: - `input` — string — text/URL to encode, or percent-encoded text to decode - `mode` — 'encode' | 'decode' (default encode) - returns: `{"mode":"string","output":"string","parts":"UrlParts | undefined (when output parses as an absolute URL)"}` - example: `await window.__tools["url"].run({"input":"a b&c=d/e?f=1","mode":"encode"})` ### jwt — jwt encoder / decoder Decode a JWT's header, payload and time claims — or encode and HMAC-sign a new token. All in the browser. Runs client-side; no input is transmitted. - params: - `input` — string — compact JWT (decode; never verified) - `mode` — 'decode' | 'encode' (encode is interactive — payload+secret in the UI) - returns: `{"header":"object","payload":"object","signature":"string (base64url, unverified)","timeClaims":"Array<{name,seconds,iso}>","validity":"'valid' | 'expired' | 'not-yet-valid' | 'no-expiry'","signatureVerified":"false (always — no key is used)"}` - example: `await window.__tools["jwt"].run({"input":"
..","mode":"decode"})` — decode only; the signature is never verified ### hash — hash digest SHA-1 / SHA-256 / SHA-384 / SHA-512 of text or files, via WebCrypto. Runs client-side; no input is transmitted. Async — `await` the result. - params: - `input` — string — text to hash (file input is UI-only, not transmitted) - `algo` — optional 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512' to filter one (the site-wide WebMCP run defaults to SHA-256 instead) - returns: HashResult (one algo) | HashResult[] (every algo when `algo` is omitted) — { algo, hex, base64, byteLength, legacy } - example: `await window.__tools["hash"].run({"input":"hello agent","algo":"SHA-256"})` — async — await the result ### radix — number base converter Binary, octal, decimal, hex — arbitrary precision via BigInt. Runs client-side; no input is transmitted. - params: - `input` — string — the number to convert - `fromRadix` — integer 2..36 (default 10) - `toRadix` — optional integer 2..36 for an extra arbitrary-base rendering - returns: `{"value":"decimal string","bin":"string","oct":"string","dec":"string","hex":"string","custom":"{radix,text}?"}` - example: `await window.__tools["radix"].run({"input":"255","fromRadix":10,"toRadix":16})` ### timestamp — unix timestamp converter Epoch seconds/millis ⇄ human dates, timezone-aware, with relative time. Runs client-side; no input is transmitted. - params: - `input` — string | number — unix epoch (s or ms) or a date string - `unit` — 'auto' | 's' | 'ms' (default auto) - returns: `{"unit":"string","ms":"number","seconds":"number","iso":"string","utc":"string","relative":"string"}` - example: `await window.__tools["timestamp"].run({"input":"1700000000","unit":"auto"})` ### cron — cron explainer Cron expression → plain-English schedule + the next run times. Runs client-side; no input is transmitted. - params: - `expr` — 5-field cron expression or an @alias (`#cron=` pre-fills and auto-runs) - `count` — number of upcoming runs to return (default 5) - returns: { ok, description?, next?: ISO strings (UTC), error? } - example: `await window.__tools["cron"].run({"expr":"*/15 * * * *","count":3})` ### uuid — uuid generator UUID v4 and v7, single or bulk, copy-ready. Runs client-side; no input is transmitted. - params: - `version` — 'v4' | 'v7' (default v4) - `count` — integer 1..100 (default 1) - returns: `{"version":"string","count":"number","uuids":"string[]"}` - example: `await window.__tools["uuid"].run({"version":"v7","count":2})` ### json — json formatter & validator Pretty-print, minify, validate — with precise error positions. Runs client-side; no input is transmitted. - params: - `input` — JSON text to process (`#input=` pre-fills and auto-runs) - `op` — 'format' | 'minify' | 'validate' (`#op=`, default format) - `indent` — '2' | '4' | 'tab' - `sortKeys` — boolean — recursively sort object keys - returns: { ok, output?, error?: { message, line, column, offset } } - example: `await window.__tools["json"].run({"input":"{\"b\":1,\"a\":2}","op":"format","sortKeys":true})` ### image — image resize & convert Resize, compress, and convert images — png, jpeg, webp — entirely in the browser. Handles batches. Runs client-side; no input is transmitted. Async — `await` the result. - params: - `image` — ArrayBuffer | Uint8Array | Blob | base64/data-URL string — the source image (anything the browser decodes; output is png/jpeg/webp) - `op` — 'resize' | 'compress' | 'convert' (default convert; `#mode=` pre-fills the UI) - `format` — 'png' | 'jpeg' | 'webp' — output format (required for convert; resize defaults to the source format, compress to webp; `#format=` pre-fills) - `quality` — lossy quality for jpeg/webp, 0..1 or 1..100 (default 0.82; ignored for png; `#quality=` pre-fills) - `width` — resize target width, px — aspect kept when height is omitted (`#width=` pre-fills) - `height` — resize target height, px — aspect kept when width is omitted (`#height=` pre-fills) - `scale` — resize scale factor, e.g. 0.5 — wins over width/height - returns: { bytes: Uint8Array, width, height, type: mime string, byteLength } — the re-encoded image; the longest side is capped at 8192px - example: `await window.__tools["image"].run({"image":"","op":"convert","format":"webp","quality":0.8})` — page-only — run() lives on /tools/image via window.__tools.image (image bytes can't cross WebMCP); files can't be pre-filled from a URL ### case — case converter camelCase, snake_case, kebab-case, Title Case, CONSTANT_CASE and more. Runs client-side; no input is transmitted. - params: - `input` — text or identifier to convert (`#input=` pre-fills and auto-runs) - `case` — optional one of camel|pascal|snake|screamingSnake|kebab|title|sentence|lower|upper; omit for all - returns: { tokens: string[], result?: string, all?: Record } - example: `await window.__tools["case"].run({"input":"hello agent world","case":"camel"})` ### diff — text diff Line and word diff of two texts, computed locally. Runs client-side; no input is transmitted. - params: - `a` — original text (`#a=` pre-fills) - `b` — changed text (`#b=` pre-fills) - returns: { rows: LineRow[], added: number, removed: number } - example: `await window.__tools["diff"].run({"a":"line one\nline two","b":"line one\nline 2"})` ### regex — regex tester Live match highlighting, capture groups, and a common-pattern library. Runs client-side; no input is transmitted. - params: - `pattern` — regex source (`#pattern=` pre-fills) - `flags` — any of gimsuy (`#flags=`) - `text` — test text (`#text=` pre-fills and auto-runs) - `replace` — optional replacement string for preview - returns: { ok, matches?: MatchHit[], truncated?, inputTooLarge?, error?, replaced? } - example: `await window.__tools["regex"].run({"pattern":"\\d+","flags":"g","text":"a1b22c333"})` ### markdown — markdown editor Split-pane markdown editor — live preview, right-to-left aware, copy as HTML, Markdown, or rich text. Runs client-side; no input is transmitted. - params: - `text` — markdown source (`#text=` pre-fills and auto-renders) - returns: { html: string } — sanitized HTML with per-block dir=auto (marked + DOMPurify) - example: `await window.__tools["markdown"].run({"text":"# hi\n\n**bold** and _em_"})` ### count — word & token counter Words, characters, lines, sentences, reading time — plus a live token count (GPT o200k by default, switchable to cl100k or Llama 3). Runs client-side; no input is transmitted. The page also shows a live token count — GPT o200k by default, eagerly loaded on page mount, with a compact inline select to switch to cl100k or Llama 3 (each lazy-loads its tokenizer package on first use, then stays cached). Token counts recompute on every keystroke alongside the other stats, but aren't part of run()/describe() below — call `countTokens(text, id)` from `src/lib/tools/tokenize.ts` directly for agent workflows. - params: - `text` — text to analyze (`#text=` pre-fills and auto-runs) - returns: { words, chars, charsNoSpaces, codePoints, lines, sentences, paragraphs, readingTimeMin, readingTimeSec, topWords: {word,count}[] } - example: `await window.__tools["count"].run({"text":"the quick brown fox the fox"})` ### plot — graphing calculator Plot functions on an interactive canvas — pan, zoom, multiple expressions, shareable links. Runs client-side; no input is transmitted. - params: - `expr` — string — function of x, e.g. sin(x), 2x^2-1, exp(-x)cos(5x) - `min` — number — domain start (default -10) - `max` — number — domain end (default 10) - `steps` — number — sample intervals (default 256); returns steps + 1 samples, both endpoints included - returns: { expr, min, max, steps, samples: [{x,y}] (length steps + 1), error? } — y is NaN/Infinity where the function is undefined - example: `await window.__tools["plot"].run({"expr":"sin(x)","min":-3.14159,"max":3.14159,"steps":64})` ### sign — pdf editor Open a PDF and mark it up — signature, text, pen, highlight, boxes, lines, images — plus rotate, reorder, delete, append and extract pages. All in the browser. Runs client-side; no input is transmitted. Async — `await` the result. A PDF markup + page editor. Load a PDF, apply an ordered list of `ops`, get the edited bytes back. Annotation ops stamp content onto a page; structural ops rotate/remove pages. Ops apply in array order. **Coordinates are resolution-independent.** Every position/size is a fraction of the page in `[0,1]` with the origin at the **top-left** (x → right, y → down). `page` is **1-based**. `color` is a hex string; text `size` and shape `width` are fractions of the page height. Text is drawn in Helvetica. **Op types** | type | shape | does | | --- | --- | --- | | `text` | `{page, x, y, text, size?=0.03, color?="#000000"}` | stamp text at (x,y) | | `signature` / `image` | `{page, x, y, w, h, png}` | place a PNG (`png` = data URL) in the box | | `draw` | `{page, points:[{nx,ny},…], color?, width?=0.004}` | freehand polyline | | `highlight` | `{page, x, y, w, h, color?="#ffe14d"}` | translucent marker box | | `rect` | `{page, x, y, w, h, color?, width?}` | outlined rectangle | | `line` | `{page, x1, y1, x2, y2, color?, width?}` | straight line | | `rotate` | `{page, degrees}` | add rotation (rotates the page + its stamps) | | `deletePage` | `{page}` | drop a page from the output | Note: annotation ops target the **original** pages only (pages appended via the UI's "append PDF" aren't annotatable). The UI uses the same engine — anything the toolbar does maps to one of these ops. - params: - `pdf` — ArrayBuffer | Uint8Array | base64 string — the PDF to edit - `ops` — array of edit ops applied in order (see the op table in the guide) - returns: { bytes: Uint8Array } — the edited PDF; interactive use downloads it directly - example: `await window.__tools["sign"].run({"pdf":"","ops":[{"type":"text","page":1,"x":0.1,"y":0.1,"text":"APPROVED","color":"#0b3d91","size":0.04}]})` — stamp a label near the top-left - example: `await window.__tools["sign"].run({"pdf":"","ops":[{"type":"highlight","page":1,"x":0.12,"y":0.46,"w":0.5,"h":0.03},{"type":"rotate","page":1,"degrees":90}]})` — highlight a line, then rotate the page - example: `await window.__tools["sign"].run({"pdf":"","ops":[{"type":"deletePage","page":2}]})` — drop page 2 ### lorem — lorem ipsum generator Filler text by the word, sentence, or paragraph — copy-ready. Runs client-side; no input is transmitted. - params: - `mode` — 'paragraphs' | 'sentences' | 'words' (default paragraphs; `#mode=` pre-fills) - `count` — how many units (default 3; clamped to 50 paragraphs / 200 sentences / 1000 words; `#count=`) - `startWithLorem` — boolean — begin with the classic "Lorem ipsum dolor sit amet…" (`#classic=` pre-fills) - `seed` — optional number — same seed reproduces the same text - returns: `{"mode":"string","count":"number","text":"string","words":"number"}` - example: `await window.__tools["lorem"].run({"mode":"sentences","count":3,"startWithLorem":true})` ### csv — csv ⇄ json converter CSV to JSON and back — quoted fields, custom delimiters, header-row toggle. Values stay strings. Runs client-side; no input is transmitted. - params: - `input` — CSV text (csv2json) or JSON text (json2csv) (`#input=` pre-fills and auto-runs) - `mode` — 'csv2json' | 'json2csv' (default csv2json; `#mode=`) - `delimiter` — field delimiter — ',' ';' '|' or 'tab' (default ','; `#delimiter=`) - `header` — boolean — first row is a header / emit a header row (default true; `#header=`) - returns: { ok, mode, output?, rows?, columns?, error?: { message, line } } — csv2json values stay strings; json2csv accepts arrays of objects/arrays/primitives or one object - example: `await window.__tools["csv"].run({"input":"name,age\nada,36","mode":"csv2json"})` ### color — color converter Hex, rgb, hsl, and oklch — plus a tints-and-shades palette and a WCAG contrast checker. Runs client-side; no input is transmitted. - params: - `input` — color string — hex (#22d3ee), rgb(34 211 238), hsl(187 86% 53%), or oklch(0.78 0.13 211) (`#input=` pre-fills and auto-runs) - `against` — optional second color — adds a WCAG contrast check (`#against=`) - returns: { ok, format, hex, rgb, hsl, oklch, values: { rgb, hsl, oklch }, luminance, palette: { hex, l }[] (oklch lightness ramp), clipped? (oklch outside sRGB), contrast?: { ratio, aaNormal, aaLarge, aaaNormal, aaaLarge, against }, error? } - example: `await window.__tools["color"].run({"input":"#22d3ee","against":"#111827"})` ### data — json ⇄ yaml ⇄ toml ⇄ toon converter Convert between JSON, YAML, TOML, and TOON — auto-detects the input, points at the exact parse error, and shows the token-count savings of TOON's compact tabular arrays. Runs client-side; no input is transmitted. - params: - `input` — the document to convert (`#input=` pre-fills and auto-runs) - `from` — 'auto' | 'json' | 'yaml' | 'toml' | 'toon' (default auto-detect; `#from=`) — toon is never auto-detected, pass it explicitly - `to` — 'json' | 'yaml' | 'toml' | 'toon' (default json; `#to=`) - returns: { ok, from (detected when auto), to, detected?, output?, error?: { message, line?, column? } } — TOML needs an object root and drops nulls; multi-doc YAML is rejected; TOON round-trips any root shape (no column in its errors) but only gets token-compact on uniform arrays of objects - example: `await window.__tools["data"].run({"input":"title = \"demo\"\n[server]\nport = 8080","from":"toml","to":"yaml"})` ### qr — qr code generator Text or URL → QR code as crisp SVG, with download. Decodes images too, where the browser can. Runs client-side; no input is transmitted. - params: - `text` — text or URL to encode, up to 2953 bytes (`#text=` pre-fills and auto-runs) - `ecc` — error-correction level 'L' | 'M' | 'Q' | 'H' (default M; `#ecc=`) - `border` — quiet-zone width in modules, 0–10 (default 2; `#border=`) - returns: { ok, svg? (self-contained SVG string), size? (modules per side incl. border), version? (1–40), ecc, error? } — decoding is page-UI only (native BarcodeDetector, feature-detected) - example: `await window.__tools["qr"].run({"text":"https://example.com","ecc":"M"})` ### contact — contact composer Compose a prefilled email draft, plus quick contact links — email, GitHub, LinkedIn. Opens in your own mail client. Runs client-side; no input is transmitted. - params: - `name` — optional string — your name; added as a sign-off line in the drafted body - `subject` — optional string — email subject (default "Hello") - `message` — optional string — the email body - returns: `{"email":"string — the public address to reach Aaron","github":"string — GitHub profile URL","linkedin":"string — LinkedIn profile URL","subject":"string — the resolved subject","body":"string — the resolved body (empty when no name/message is given)","mailto":"string — a ready-to-open, fully-encoded mailto: URL; building it sends nothing — a human opens it in their mail client"}` - example: `await window.__tools["contact"].run({"name":"Ada Lovelace","subject":"saw your tools","message":"these are great — want to chat?"})` — composes a mailto: link and returns the contact points; it never sends — the site has no backend, a human opens the link in their mail client ## Lab tools The playable experiments expose tools the same way, page-scoped at their own URLs — open the page, wait for `window.__tools[""]`, then drive it exactly as above. ### chess-state — https://aaronchartier.com/lab/chess (read-only) Read the chess game on /lab/chess: FEN, SAN history, whose turn it is, your seat, and the result. Call this before moving. Runs client-side; no input is transmitted. - params: - returns: { fen, turn: 'white'|'black', moveNumber, history: SAN[], inCheck, gameOver, result, reason, seats: { human, you, opponent: 'bot'|'agent' }, yourTurn, pgn } - example: `await window.__tools["chess-state"].run({})` — read the /lab/chess game first — `yourTurn` says whether to play; `seats.you` is your side ### chess-legal-moves — https://aaronchartier.com/lab/chess (read-only) List every legal move (SAN) for the side to move in the /lab/chess game — or the target squares of one piece via `square`. Runs client-side; no input is transmitted. - params: - `square` — optional square like `e2` — returns that piece's target squares instead of the full list - returns: { turn, gameOver, legalMoves: SAN[] } — or { turn, square, targets: string[] } when `square` is given - example: `await window.__tools["chess-legal-moves"].run({})` — every legal move for the side to move, in SAN ### chess-move — https://aaronchartier.com/lab/chess Play your move in the /lab/chess game — SAN like `Nf3` or coordinates like `e2e4`. You sit opposite the human; the visible board updates live. Runs client-side; no input is transmitted. - params: - `move` — your move as SAN (`Nf3`, `exd5`) or coordinates (`e2e4`, `e7e8q`) - `from` — origin square (alternative to `move`, with `to`) - `to` — destination square - `promotion` — 'q' | 'r' | 'b' | 'n' — with from/to, when a pawn promotes - returns: { ok, played?: SAN, error?, …chess-state fields } — you play the seat opposite the human; your first move on a bot game claims it - example: `await window.__tools["chess-move"].run({"move":"e5"})` — rejected moves return { ok: false, error } and leave the board unchanged ### chess-new-game — https://aaronchartier.com/lab/chess Start a fresh game on the shared /lab/chess board and take a seat ('white' or 'black', default black). Resets the human's board too. Runs client-side; no input is transmitted. - params: - `side` — 'white' | 'black' — the side you take (default black; the human keeps the other) - returns: { ok, note, …chess-state fields } — a fresh game with you seated - example: `await window.__tools["chess-new-game"].run({"side":"black"})` — resets the shared board at /lab/chess for the human too — say so before you do it ### chess-say — https://aaronchartier.com/lab/chess Post a short coaching message to the Tutor panel on /lab/chess — how you (an agent tutoring the human) speak to them through the board itself, separate from your own chat. 1–2 sentences. Runs client-side; no input is transmitted. - params: - `message` — string — a short (1–2 sentence) coaching message shown to the human in the Tutor panel on the board - returns: { ok, message?, visible, note?, error? } — appends to the on-page Tutor panel (most recent 50 kept). visible is false when the board isn't a bring-your-own-agent game (panel hidden, the human won't see it); note then says how to start one - example: `await window.__tools["chess-say"].run({"message":"Nice development — that knight eyes the center. I'll castle to tuck my king away."})` ### chess-tutor-brief — https://aaronchartier.com/lab/chess (read-only) Read your operating instructions for tutoring a human on this board — call this FIRST when asked to be a chess tutor, then follow it (how to play at a teaching level and coach through chess_say). Runs client-side; no input is transmitted. - params: - returns: string — your operating instructions for tutoring a human on this board (how to play at a teaching level and coach via chess_say) - example: `await window.__tools["chess-tutor-brief"].run({})` — call this first when asked to tutor — it configures how you run the session ### skill-search — https://aaronchartier.com/lab/skill-oracle (read-only) Semantic search across agent skills — describe a capability in natural language, get ranked matches (trust + popularity blended) with links to each skill's source page. Runs client-side; no input is transmitted. Async — `await` the result. - params: - `query` — string — describe the capability you need in natural language, e.g. 'parse PDF form fields' - `top_k` — optional integer 1..10 (default 5) — how many ranked matches to return - returns: markdown — a ranked list of matching skills (trust + popularity blended), each with a short snippet and a link to its source page - example: `await window.__tools["skill-search"].run({"query":"convert between CSV and JSON","top_k":5})` — hits the live oracle API; a rate-limited or unreachable oracle returns one plain-text status line instead of markdown ### skill-details — https://aaronchartier.com/lab/skill-oracle (read-only) Fetch one skill's card: scores, a short snippet, and the source link. Runs client-side; no input is transmitted. Async — `await` the result. - params: - `owner` — string — the skill's owner/namespace; a combined 'owner/name' value also works if `name` is omitted - `name` — string — the skill's name (omit if already passed combined in `owner`) - returns: markdown — one skill's card: trust and popularity scores, a short snippet, and its source link - example: `await window.__tools["skill-details"].run({"owner":"acme","name":"pdf-fill"})` — also callable as { owner: "acme/pdf-fill" } One more, for the pair of you: the terminal at https://aaronchartier.com/terminal keeps a command called `handshake` off every human-facing menu — ask your human to type it there. ## Pages [Home](https://aaronchartier.com/index.md) · [About](https://aaronchartier.com/about.md) · [Projects](https://aaronchartier.com/projects.md) · [Now](https://aaronchartier.com/now.md) · [Tools](https://aaronchartier.com/tools) · [Lab](https://aaronchartier.com/lab) · [Skill oracle](https://aaronchartier.com/lab/skill-oracle.md) · [Terminal](https://aaronchartier.com/terminal) · [Blog](https://aaronchartier.com/blog) · [For agents](https://aaronchartier.com/for-agents.md) --- Canonical: https://aaronchartier.com/SKILL.md · generated from the tool registry · © 2026 Aaron Chartier