docs(site): add generated documentation hub
This commit is contained in:
parent
34aa6b9df4
commit
81ddeb43eb
30
.github/workflows/pages.yml
vendored
30
.github/workflows/pages.yml
vendored
@ -4,7 +4,9 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "docs/site/**"
|
||||
- "docs/**"
|
||||
- "scripts/build-docs-site.mjs"
|
||||
- "scripts/docs-site-assets.mjs"
|
||||
- ".github/workflows/pages.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
@ -14,27 +16,30 @@ permissions:
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Build static site
|
||||
run: |
|
||||
rm -rf _site
|
||||
mkdir -p _site
|
||||
cp -R docs/site/* _site/
|
||||
- name: Build docs site
|
||||
run: node scripts/build-docs-site.mjs
|
||||
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@v6
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: _site
|
||||
|
||||
@ -47,5 +52,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
uses: actions/deploy-pages@v5
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -77,6 +77,7 @@ lib-cov/
|
||||
*.launch
|
||||
.settings/
|
||||
.claude/settings.local.json
|
||||
_site/
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
|
||||
|
||||
89
docs/automation.md
Normal file
89
docs/automation.md
Normal file
@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Automation
|
||||
summary: 'Overview of Peekaboo UI automation targets, input primitives, app surfaces, recipes, and resilience tips.'
|
||||
description: How to drive macOS UI with Peekaboo — click, type, scroll, drag, hotkeys, menus, dialogs, windows, Spaces.
|
||||
read_when:
|
||||
- 'deciding which UI automation command or targeting mode to use'
|
||||
- 'documenting agent, MCP, or CLI behavior that mutates macOS UI'
|
||||
---
|
||||
|
||||
# Automation
|
||||
|
||||
Peekaboo's automation surface is small but covers the whole macOS UI graph. Each command is documented separately under `commands/`; this page is the map.
|
||||
|
||||
## Targeting model
|
||||
|
||||
Every input command accepts one of three target shapes:
|
||||
|
||||
- **Element ID** — `--id E12` (from `peekaboo see`); the most reliable.
|
||||
- **Label / role / app** — `--label "Send" --app Mail`; resolved via the AX tree.
|
||||
- **Coordinates** — `--at 480,120`; the fallback when the AX tree lies.
|
||||
|
||||
Prefer IDs when you can capture them, labels when you can't, and coordinates only as a last resort. The agent and MCP tooling default to the first two.
|
||||
|
||||
## Input primitives
|
||||
|
||||
| Command | Use it for |
|
||||
| --- | --- |
|
||||
| [click](commands/click.md) | mouse clicks, double/triple, right/middle, hold |
|
||||
| [type](commands/type.md) | typing strings into focused fields |
|
||||
| [press](commands/press.md) | individual key presses (return, escape, arrows, etc.) |
|
||||
| [hotkey](commands/hotkey.md) | shortcut combos, including background apps |
|
||||
| [scroll](commands/scroll.md) | wheel scrolling at a point or on a target |
|
||||
| [drag](commands/drag.md) | press, move, release — files, sliders, selections |
|
||||
| [swipe](commands/swipe.md) | trackpad-style multi-finger gestures |
|
||||
| [move](commands/move.md) | warp the mouse without clicking |
|
||||
| [set-value](commands/set-value.md) | write to text fields without typing |
|
||||
| [perform-action](commands/perform-action.md) | trigger any AX action (`AXPress`, `AXShowMenu`, …) |
|
||||
| [sleep](commands/sleep.md) | wait between steps with deterministic timing |
|
||||
|
||||
For UX parity with humans (jitter, easing, dwell), see [human-typing.md](human-typing.md) and [human-mouse-move.md](human-mouse-move.md).
|
||||
|
||||
## Surfaces
|
||||
|
||||
| Surface | Command | Notes |
|
||||
| --- | --- | --- |
|
||||
| App lifecycle | [app](commands/app.md) | launch, quit, focus, hide |
|
||||
| Windows | [window](commands/window.md) | move, resize, focus, minimize, fullscreen |
|
||||
| Spaces & Stage Manager | [space](commands/space.md) | enumerate and switch Spaces |
|
||||
| Menus | [menu](commands/menu.md) | walk app menus by path |
|
||||
| Menu bar / status items | [menubar.md](commands/menubar.md) | extra-fiddly popovers |
|
||||
| Dialogs | [dialog](commands/dialog.md) | sheets, alerts, save panels |
|
||||
| Dock | [dock](commands/dock.md) | inspect/click dock items |
|
||||
| Clipboard | [clipboard](commands/clipboard.md) | read/write pasteboard contents |
|
||||
| Open files / URLs | [open](commands/open.md) | with focus controls |
|
||||
| Visual feedback | [visualizer](visualizer.md) | overlay so a human can follow what the agent is doing |
|
||||
|
||||
## Recipe: click a button by label
|
||||
|
||||
```bash
|
||||
# 1. Inspect first to find a stable label.
|
||||
peekaboo see --app Safari --annotate --output safari.png
|
||||
|
||||
# 2. Click it.
|
||||
peekaboo click --label "Reload" --app Safari
|
||||
```
|
||||
|
||||
## Recipe: a small flow
|
||||
|
||||
```bash
|
||||
peekaboo app focus --name "Notes"
|
||||
peekaboo hotkey cmd+n
|
||||
peekaboo type "Standup notes\n\n- Shipped Peekaboo docs\n- Reviewed PR #42\n"
|
||||
peekaboo hotkey cmd+s
|
||||
```
|
||||
|
||||
Three primitives, four lines. The agent does the same thing under the hood — it just plans the sequence for you.
|
||||
|
||||
## Resilience tips
|
||||
|
||||
- Always run [`peekaboo see`](commands/see.md) when an element is unreachable. The AX tree refreshes after focus changes; capture again if a click fails.
|
||||
- Use [focus](focus.md) and [application-resolving](application-resolving.md) for tricky cases (multiple windows, helper apps, processes that hide on activation).
|
||||
- Wrap risky sequences with `peekaboo sleep 0.2` — humans don't fire ten clicks in a single frame, and neither should you.
|
||||
- Prefer [`hotkey --focus-background`](commands/hotkey.md) when you need to drive an app without stealing focus from the user.
|
||||
|
||||
## Going further
|
||||
|
||||
- [Agent overview](commands/agent.md) — let Peekaboo plan input sequences from a goal.
|
||||
- [MCP](MCP.md) — expose all of the above to Claude Desktop, Cursor, and Codex.
|
||||
- [Architecture](ARCHITECTURE.md) — how the input pipeline routes through Bridge and Daemon.
|
||||
50
docs/index.md
Normal file
50
docs/index.md
Normal file
@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Peekaboo documentation
|
||||
summary: 'Entry point for installing, configuring, and using Peekaboo across CLI, MCP, app, and library surfaces.'
|
||||
description: macOS automation that sees the screen and does the clicks. Native CLI, MCP server, and agent runtime for OpenAI, Claude, Grok, Gemini, and Ollama.
|
||||
read_when:
|
||||
- 'starting with Peekaboo or looking for the right documentation page'
|
||||
- 'linking the public documentation hub from README, site, or release notes'
|
||||
---
|
||||
|
||||
# Peekaboo documentation
|
||||
|
||||
Peekaboo is a macOS automation toolkit for humans and agents. It captures pixels, reads the accessibility tree, drives input, and ships an agent runtime plus an MCP server so AI clients (Claude Desktop, Cursor, Codex, etc.) can drive the desktop with the same primitives you'd use from the shell.
|
||||
|
||||
> **TL;DR** — `brew install steipete/tap/peekaboo`, grant Screen Recording + Accessibility, then `peekaboo agent "open Safari and search for Peekaboo"`.
|
||||
|
||||
## Where to start
|
||||
|
||||
- **[Install](install.md)** — Homebrew, npm/MCP, source builds.
|
||||
- **[Quickstart](quickstart.md)** — first capture, first click, first agent run in five minutes.
|
||||
- **[Permissions](permissions.md)** — what to grant, why, and how to verify.
|
||||
- **[Configuration](configuration.md)** — environment variables, config files, credential storage.
|
||||
|
||||
## What Peekaboo does
|
||||
|
||||
- **[Capture & vision](commands/capture.md)** — pixel-accurate screen, window, and menu-bar capture; annotated AX maps.
|
||||
- **[Automation](automation.md)** — click, type, scroll, drag, hotkeys, menus, dialogs, windows, Spaces.
|
||||
- **[Agent](commands/agent.md)** — natural-language plan/act loop with provider switching, resumable sessions, and visualizer feedback.
|
||||
- **[MCP](MCP.md)** — expose every Peekaboo tool over stdio for Claude Desktop, Cursor, Codex, and other MCP clients.
|
||||
|
||||
## Reference
|
||||
|
||||
- **[Command reference](cli-command-reference.md)** — every CLI command, grouped.
|
||||
- **[Command index](commands/README.md)** — one page per command with flags and examples.
|
||||
- **[Architecture](ARCHITECTURE.md)** — Core, CLI, Bridge, Daemon, Visualizer.
|
||||
- **[Releasing](RELEASING.md)** — versioning, signing, distribution.
|
||||
|
||||
## Surfaces
|
||||
|
||||
| Surface | Use it for | Entry point |
|
||||
| --- | --- | --- |
|
||||
| **CLI** | scripts, ad-hoc captures, CI | `brew install steipete/tap/peekaboo` |
|
||||
| **MCP server** | Claude Desktop, Cursor, Codex CLI | `npx @steipete/peekaboo mcp` |
|
||||
| **Mac app** | menu-bar visualizer, permission prompts | [Releases](https://github.com/steipete/Peekaboo/releases/latest) |
|
||||
| **Library** | embed in Swift apps and tools | `Core/PeekabooCore` (Swift Package) |
|
||||
|
||||
## Get help
|
||||
|
||||
- File issues: [github.com/steipete/Peekaboo/issues](https://github.com/steipete/Peekaboo/issues)
|
||||
- Source: [github.com/steipete/Peekaboo](https://github.com/steipete/Peekaboo)
|
||||
- Author: [@steipete](https://x.com/steipete)
|
||||
61
docs/install.md
Normal file
61
docs/install.md
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Install Peekaboo
|
||||
summary: 'Install Peekaboo through Homebrew, npm/MCP, the Mac app, or a source checkout.'
|
||||
description: Install the Peekaboo CLI, MCP server, or Mac app. Homebrew, npm, and source paths.
|
||||
read_when:
|
||||
- 'setting up Peekaboo for the first time'
|
||||
- 'choosing between Homebrew, npm, Mac app, and source builds'
|
||||
---
|
||||
|
||||
# Install
|
||||
|
||||
Peekaboo ships in three flavors. They all use the same Swift core and the same toolset — pick whichever surface fits your workflow.
|
||||
|
||||
## Homebrew (recommended)
|
||||
|
||||
The CLI is signed, notarized, and lives in [steipete/homebrew-tap](https://github.com/steipete/homebrew-tap).
|
||||
|
||||
```bash
|
||||
brew install steipete/tap/peekaboo
|
||||
peekaboo --version
|
||||
```
|
||||
|
||||
Update with `brew upgrade steipete/tap/peekaboo`.
|
||||
|
||||
## npm (for MCP clients)
|
||||
|
||||
The npm package wraps the same CLI plus an MCP shim, so you can launch the server with `npx`:
|
||||
|
||||
```bash
|
||||
npx -y @steipete/peekaboo mcp
|
||||
```
|
||||
|
||||
This is the form you point Claude Desktop, Cursor, and Codex at. See [MCP.md](MCP.md) and [install-mcp-claude-desktop.md](install-mcp-claude-desktop.md).
|
||||
|
||||
## Mac app
|
||||
|
||||
The full menu-bar app (visualizer, permission flows, status item) is on the [Releases](https://github.com/steipete/Peekaboo/releases/latest) page. The bundled CLI lives at `/Applications/Peekaboo.app/Contents/MacOS/peekaboo`; symlink it if you want it on your `PATH` without Homebrew.
|
||||
|
||||
## Build from source
|
||||
|
||||
Requires macOS 26.1+, Xcode 26+, Swift 6.2.
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/steipete/Peekaboo.git
|
||||
cd Peekaboo
|
||||
pnpm install
|
||||
pnpm run build:cli # debug build
|
||||
pnpm run build:swift:all # universal release
|
||||
```
|
||||
|
||||
The output binary lives under `Apps/CLI/.build/...`. See [building.md](building.md) for signing, notarization, and the `pnpm run poltergeist:haunt` rapid-rebuild loop.
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
peekaboo --version
|
||||
peekaboo permissions status
|
||||
peekaboo list apps
|
||||
```
|
||||
|
||||
If any of those error out, jump to [permissions.md](permissions.md).
|
||||
72
docs/providers.md
Normal file
72
docs/providers.md
Normal file
@ -0,0 +1,72 @@
|
||||
---
|
||||
title: AI providers
|
||||
summary: 'Configure model providers and credentials for the Peekaboo agent runtime.'
|
||||
description: Configure OpenAI, Anthropic Claude, xAI Grok, Google Gemini, and Ollama for the Peekaboo agent.
|
||||
read_when:
|
||||
- 'configuring model credentials or provider selection'
|
||||
- 'debugging agent model, tool-calling, or local Ollama setup'
|
||||
---
|
||||
|
||||
# AI providers
|
||||
|
||||
Peekaboo's agent runtime is provider-agnostic — it talks to any chat-completions-style backend through Tachikoma. You configure provider credentials once and pick a model per-run.
|
||||
|
||||
## Supported providers
|
||||
|
||||
| Provider | Models we test | Credential |
|
||||
| --- | --- | --- |
|
||||
| **OpenAI** | gpt-5, gpt-5-mini, gpt-4.1 | `OPENAI_API_KEY` |
|
||||
| **Anthropic** | claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5 | `ANTHROPIC_API_KEY` |
|
||||
| **xAI** | grok-4 | `XAI_API_KEY` |
|
||||
| **Google** | gemini-3-pro, gemini-3-flash | `GEMINI_API_KEY` |
|
||||
| **Ollama** | any local model with tool-calling | runs at `http://localhost:11434` |
|
||||
|
||||
Other Tachikoma-supported providers also work — see the [Tachikoma docs](https://github.com/steipete/Tachikoma) for the full list.
|
||||
|
||||
## Credentials
|
||||
|
||||
Credentials live in `~/.peekaboo/credentials.json`, encrypted at rest with the macOS Keychain when available. Set them once via the CLI:
|
||||
|
||||
```bash
|
||||
peekaboo config set-credential openai # interactive
|
||||
peekaboo config set-credential anthropic
|
||||
```
|
||||
|
||||
Environment variables override the stored values, which is handy in CI:
|
||||
|
||||
```bash
|
||||
OPENAI_API_KEY=sk-... peekaboo agent "open a browser"
|
||||
```
|
||||
|
||||
See [configuration.md](configuration.md) for the full precedence table.
|
||||
|
||||
## Picking a model
|
||||
|
||||
```bash
|
||||
peekaboo agent --model claude-opus-4-7 "summarize this window"
|
||||
peekaboo agent --model gpt-5-mini "click Continue and wait for the dialog"
|
||||
peekaboo agent --model ollama:llama3.1:8b "describe this screenshot"
|
||||
```
|
||||
|
||||
Defaults come from `agent.defaultModel` in `~/.peekaboo/config.json`. Set a per-project default with `PEEKABOO_AGENT_MODEL`.
|
||||
|
||||
## Tool calling
|
||||
|
||||
The agent expects tool-calling capable models. If your provider doesn't support it (some tiny local models), Peekaboo falls back to a structured-output prompt — slower and less reliable. Stick with mainstream tool-calling models for production runs.
|
||||
|
||||
## Local-only mode
|
||||
|
||||
Want everything on-device? Run an Ollama model with tool calling and point the CLI at it:
|
||||
|
||||
```bash
|
||||
ollama run llama3.1:8b
|
||||
peekaboo agent --model ollama:llama3.1:8b "open System Settings"
|
||||
```
|
||||
|
||||
No network requests leave the machine. Captures, AX queries, and reasoning all stay local.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"401 Unauthorized"** — credential isn't set, or env var overrides the saved one. Run `peekaboo config get-credential <provider>`.
|
||||
- **"context length exceeded"** — long sessions accumulate screenshots. Start a fresh session with `peekaboo agent --new`.
|
||||
- **"no tool-call support"** — pick a different model. The error log lists the providers and models with confirmed tool-calling.
|
||||
95
docs/quickstart.md
Normal file
95
docs/quickstart.md
Normal file
@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Quickstart
|
||||
summary: 'First-run walkthrough for permissions, capture, see, click, type, agent mode, and MCP setup.'
|
||||
description: First capture, first click, first agent run with Peekaboo. Five minutes from install to working automation.
|
||||
read_when:
|
||||
- 'validating a fresh Peekaboo install'
|
||||
- 'showing users the shortest path from install to working automation'
|
||||
---
|
||||
|
||||
# Quickstart
|
||||
|
||||
This page assumes you've already followed [install.md](install.md). If `peekaboo --version` prints a version, you're ready.
|
||||
|
||||
## 1. Grant permissions
|
||||
|
||||
```bash
|
||||
peekaboo permissions status
|
||||
peekaboo permissions grant
|
||||
```
|
||||
|
||||
`grant` opens System Settings to the right pane. You need **Screen Recording** (required) and **Accessibility** (recommended). Re-run `permissions status` until both are green. Background hotkeys also need **Event Synthesizing** — see [permissions.md](permissions.md).
|
||||
|
||||
## 2. Take a screenshot
|
||||
|
||||
```bash
|
||||
# whole screen → ./screen.png
|
||||
peekaboo capture --output screen.png
|
||||
|
||||
# only the focused window
|
||||
peekaboo capture --window-focused --output focused.png
|
||||
|
||||
# a specific app's frontmost window
|
||||
peekaboo capture --app Safari --output safari.png
|
||||
```
|
||||
|
||||
The output is a regular PNG. Add `--format jpeg --quality 85` for smaller files. See [commands/capture.md](commands/capture.md) for every flag.
|
||||
|
||||
## 3. Inspect the UI
|
||||
|
||||
`see` returns a structured map of clickable elements with stable IDs:
|
||||
|
||||
```bash
|
||||
peekaboo see --app Safari --json | jq '.elements[0:3]'
|
||||
```
|
||||
|
||||
Add `--annotate` to write a labelled PNG you can eyeball:
|
||||
|
||||
```bash
|
||||
peekaboo see --app Safari --annotate --output safari.png
|
||||
```
|
||||
|
||||
Each element has `id`, `role`, `label`, `frame`, and `actions`. Pass an `id` to other commands to act on it.
|
||||
|
||||
## 4. Click and type
|
||||
|
||||
```bash
|
||||
peekaboo click --label "Address and search bar" --app Safari
|
||||
peekaboo type "github.com/steipete/Peekaboo" --press-return
|
||||
```
|
||||
|
||||
Coordinates also work: `peekaboo click --at 480,120`. See [automation.md](automation.md) for the full input vocabulary.
|
||||
|
||||
## 5. Run an agent
|
||||
|
||||
The agent picks tools, plans, and executes — give it a goal in natural language:
|
||||
|
||||
```bash
|
||||
peekaboo agent "Open Safari, go to github.com, and search for Peekaboo"
|
||||
```
|
||||
|
||||
Watch the visualizer overlay as it works. Pause/resume with `peekaboo agent --resume <session-id>`. See [commands/agent.md](commands/agent.md) for provider switching and session management.
|
||||
|
||||
## 6. (Optional) Wire up MCP
|
||||
|
||||
Want Claude Desktop or Cursor to drive Peekaboo? Drop this into your MCP client config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"peekaboo": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@steipete/peekaboo", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Full setup, including environment variables and provider keys, is in [install-mcp-claude-desktop.md](install-mcp-claude-desktop.md).
|
||||
|
||||
## What next?
|
||||
|
||||
- [Automation overview](automation.md) — every input primitive, when to use which.
|
||||
- [Agent](commands/agent.md) — providers, sessions, tools.
|
||||
- [MCP](MCP.md) — expose Peekaboo to any MCP client.
|
||||
- [Configuration](configuration.md) — env vars, profiles, credentials.
|
||||
@ -1,2 +1 @@
|
||||
www.peekaboo.boo
|
||||
|
||||
peekaboo.sh
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
name="description"
|
||||
content="Peekaboo brings high‑fidelity screen capture, AI analysis, and complete GUI automation to macOS. Give your agents eyes."
|
||||
/>
|
||||
<link rel="canonical" href="https://www.peekaboo.boo/" />
|
||||
<link rel="canonical" href="https://peekaboo.sh/" />
|
||||
|
||||
<meta property="og:title" content="Peekaboo" />
|
||||
<meta
|
||||
@ -17,7 +17,7 @@
|
||||
content="macOS automation that sees the screen and does the clicks. Give your agents eyes."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://www.peekaboo.boo/" />
|
||||
<meta property="og:url" content="https://peekaboo.sh/" />
|
||||
|
||||
<meta name="theme-color" content="#0b0c0f" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
@ -53,7 +53,7 @@
|
||||
<a class="navLink" href="#install">Install</a>
|
||||
<a class="navLink" href="#features">Features</a>
|
||||
<a class="navLink" href="#how">How it works</a>
|
||||
<a class="navLink" href="#docs">Docs</a>
|
||||
<a class="navLink" href="./docs/">Docs</a>
|
||||
<a class="navLink navLinkStrong" href="https://github.com/steipete/Peekaboo">GitHub</a>
|
||||
</nav>
|
||||
</div>
|
||||
@ -303,22 +303,26 @@
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a class="linkCard" href="./docs/">
|
||||
<span class="linkTitle">Documentation hub</span>
|
||||
<span class="linkHint">install · quickstart · CLI · MCP · agent</span>
|
||||
</a>
|
||||
<a class="linkCard" href="./docs/quickstart.html">
|
||||
<span class="linkTitle">Quickstart</span>
|
||||
<span class="linkHint">first capture, click, and agent run</span>
|
||||
</a>
|
||||
<a class="linkCard" href="./docs/MCP.html">
|
||||
<span class="linkTitle">MCP setup</span>
|
||||
<span class="linkHint">Claude Desktop · Cursor · Codex</span>
|
||||
</a>
|
||||
<a class="linkCard" href="./docs/ARCHITECTURE.html">
|
||||
<span class="linkTitle">Architecture</span>
|
||||
<span class="linkHint">how the pieces fit</span>
|
||||
</a>
|
||||
<a class="linkCard" href="https://github.com/steipete/Peekaboo">
|
||||
<span class="linkTitle">Repository</span>
|
||||
<span class="linkHint">source · releases · issues</span>
|
||||
</a>
|
||||
<a class="linkCard" href="https://github.com/steipete/Peekaboo/blob/main/docs/cli-command-reference.md">
|
||||
<span class="linkTitle">Command reference</span>
|
||||
<span class="linkHint">all CLI commands, grouped</span>
|
||||
</a>
|
||||
<a class="linkCard" href="https://github.com/steipete/Peekaboo/blob/main/docs/commands/mcp.md">
|
||||
<span class="linkTitle">MCP setup</span>
|
||||
<span class="linkHint">Claude Desktop · Cursor</span>
|
||||
</a>
|
||||
<a class="linkCard" href="https://github.com/steipete/Peekaboo/blob/main/docs/ARCHITECTURE.md">
|
||||
<span class="linkTitle">Architecture</span>
|
||||
<span class="linkHint">how the pieces fit</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://www.peekaboo.boo/sitemap.xml
|
||||
|
||||
Sitemap: https://peekaboo.sh/sitemap.xml
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://www.peekaboo.boo/</loc>
|
||||
</url>
|
||||
<url><loc>https://peekaboo.sh/</loc></url>
|
||||
<url><loc>https://peekaboo.sh/docs/</loc></url>
|
||||
<url><loc>https://peekaboo.sh/docs/install.html</loc></url>
|
||||
<url><loc>https://peekaboo.sh/docs/quickstart.html</loc></url>
|
||||
<url><loc>https://peekaboo.sh/docs/permissions.html</loc></url>
|
||||
<url><loc>https://peekaboo.sh/docs/configuration.html</loc></url>
|
||||
<url><loc>https://peekaboo.sh/docs/automation.html</loc></url>
|
||||
<url><loc>https://peekaboo.sh/docs/MCP.html</loc></url>
|
||||
<url><loc>https://peekaboo.sh/docs/providers.html</loc></url>
|
||||
<url><loc>https://peekaboo.sh/docs/cli-command-reference.html</loc></url>
|
||||
<url><loc>https://peekaboo.sh/docs/ARCHITECTURE.html</loc></url>
|
||||
</urlset>
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
"prepare-release": "node scripts/prepare-release.js",
|
||||
"release:mac-app": "./scripts/release-macos-app.sh",
|
||||
"docs:list": "node scripts/docs-list.mjs",
|
||||
"docs:site": "node scripts/build-docs-site.mjs",
|
||||
"lint:docs": "node scripts/docs-lint.mjs",
|
||||
"polter": "FORCE_COLOR=1 CLICOLOR_FORCE=1 NODE_PATH=../poltergeist/node_modules script -q /dev/null node ../poltergeist/dist/polter.js",
|
||||
"polter:dev": "cd /Users/steipete/Projects/Peekaboo && FORCE_COLOR=1 CLICOLOR_FORCE=1 NODE_PATH=../poltergeist/node_modules pnpm --dir ../poltergeist exec tsx ../poltergeist/src/polter.ts",
|
||||
|
||||
840
scripts/build-docs-site.mjs
Normal file
840
scripts/build-docs-site.mjs
Normal file
@ -0,0 +1,840 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { css, faviconSvg, js } from "./docs-site-assets.mjs";
|
||||
|
||||
const root = process.cwd();
|
||||
const docsDir = path.join(root, "docs");
|
||||
const siteSrcDir = path.join(docsDir, "site");
|
||||
const outDir = path.join(root, "_site");
|
||||
const repoBase = "https://github.com/steipete/Peekaboo";
|
||||
const repoEditBase = `${repoBase}/edit/main/docs`;
|
||||
const cname = readCname();
|
||||
const siteBase = cname ? `https://${cname}` : "";
|
||||
|
||||
const productName = "Peekaboo";
|
||||
const productTagline = "macOS automation that sees the screen and does the clicks";
|
||||
const productDescription =
|
||||
"Peekaboo brings high-fidelity screen capture, AI analysis, and complete GUI automation to macOS. Give your agents eyes.";
|
||||
|
||||
// Sidebar order. Files in `docs/` referenced by relative path. Anything not listed
|
||||
// here is still built (so links work) but doesn't appear in the nav.
|
||||
const sections = [
|
||||
["Start", ["index.md", "install.md", "quickstart.md", "permissions.md", "configuration.md"]],
|
||||
[
|
||||
"Capture & vision",
|
||||
[
|
||||
"commands/capture.md",
|
||||
"commands/see.md",
|
||||
"commands/image.md",
|
||||
"window-screenshot-smart-select.md",
|
||||
"visualizer.md",
|
||||
],
|
||||
],
|
||||
[
|
||||
"Automation",
|
||||
[
|
||||
"automation.md",
|
||||
"commands/click.md",
|
||||
"commands/type.md",
|
||||
"commands/hotkey.md",
|
||||
"commands/press.md",
|
||||
"commands/scroll.md",
|
||||
"commands/drag.md",
|
||||
"commands/menu.md",
|
||||
"commands/dialog.md",
|
||||
"commands/window.md",
|
||||
"commands/space.md",
|
||||
"commands/app.md",
|
||||
"human-typing.md",
|
||||
"human-mouse-move.md",
|
||||
"focus.md",
|
||||
"application-resolving.md",
|
||||
],
|
||||
],
|
||||
[
|
||||
"Agent & AI",
|
||||
[
|
||||
"commands/agent.md",
|
||||
"agent-chat.md",
|
||||
"agent-patterns.md",
|
||||
"agent-skill.md",
|
||||
"providers.md",
|
||||
],
|
||||
],
|
||||
[
|
||||
"MCP",
|
||||
[
|
||||
"MCP.md",
|
||||
"commands/mcp.md",
|
||||
"install-mcp-claude-desktop.md",
|
||||
"mcp-best-practices.md",
|
||||
],
|
||||
],
|
||||
[
|
||||
"Architecture",
|
||||
[
|
||||
"ARCHITECTURE.md",
|
||||
"engine.md",
|
||||
"daemon.md",
|
||||
"bridge-host.md",
|
||||
"concurrency.md",
|
||||
],
|
||||
],
|
||||
[
|
||||
"Reference",
|
||||
[
|
||||
"cli-command-reference.md",
|
||||
"commands/README.md",
|
||||
"logging-guide.md",
|
||||
"RELEASING.md",
|
||||
"building.md",
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Files we don't want to ship as their own pages on the site (internal/dev notes).
|
||||
const buildExcludes = [
|
||||
/^archive\//,
|
||||
/^refactor\//,
|
||||
/^refactor\.md$/,
|
||||
/^debug\//,
|
||||
/^dev\//,
|
||||
/^research\//,
|
||||
/^reports\//,
|
||||
/^references\//,
|
||||
/^testing\//,
|
||||
/^logging-profiles\//,
|
||||
/^providers\//,
|
||||
/^TODO\.md$/,
|
||||
/^test-refactor\.md$/,
|
||||
/^module-architecture-refactoring\.md$/,
|
||||
/^module-refactoring-example\.md$/,
|
||||
/^modern-api\.md$/,
|
||||
/^modern-swift\.md$/,
|
||||
/^silgen-crash-debug\.md$/,
|
||||
/^swift-.*\.md$/,
|
||||
/^swift6-.*\.md$/,
|
||||
/^SwiftUI-.*\.md$/,
|
||||
/^AppKit-.*\.md$/,
|
||||
/^skylight-.*\.md$/,
|
||||
/^playground-testing\.md$/,
|
||||
/^claude-hooks\.md$/,
|
||||
/^manual-testing\.md$/,
|
||||
/^remote-testing\.md$/,
|
||||
/^tool-formatter-architecture\.md$/,
|
||||
/^tui\.md$/,
|
||||
/^restore\.md$/,
|
||||
/^poltergeist\.md$/,
|
||||
/^homebrew-setup\.md$/,
|
||||
/^oauth\.md$/,
|
||||
/^audio\.md$/,
|
||||
/^commander\.md$/,
|
||||
/^spec\.md$/,
|
||||
/^service-api-reference\.md$/,
|
||||
/^error-handling-guide\.md$/,
|
||||
/^mcp-testing\.md$/,
|
||||
/^security\.md$/,
|
||||
];
|
||||
|
||||
fs.rmSync(outDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const allPages = allMarkdown(docsDir).map((file) => {
|
||||
const rel = path.relative(docsDir, file).replaceAll(path.sep, "/");
|
||||
const raw = fs.readFileSync(file, "utf8");
|
||||
const { frontmatter, body } = parseFrontmatter(raw);
|
||||
const cleaned = stripStrayDirectives(body);
|
||||
const title = frontmatter.title || firstHeading(cleaned) || titleize(path.basename(rel, ".md"));
|
||||
return {
|
||||
file,
|
||||
rel,
|
||||
title,
|
||||
outRel: outPath(rel, frontmatter),
|
||||
markdown: cleaned,
|
||||
frontmatter,
|
||||
};
|
||||
});
|
||||
|
||||
const pages = allPages.filter((page) => !buildExcludes.some((re) => re.test(page.rel)));
|
||||
const pageMap = new Map(pages.map((page) => [page.rel, page]));
|
||||
|
||||
const nav = sections
|
||||
.map(([name, rels]) => ({
|
||||
name,
|
||||
pages: rels.map((rel) => pageMap.get(rel)).filter(Boolean),
|
||||
}))
|
||||
.filter((section) => section.pages.length);
|
||||
|
||||
const sectionByRel = new Map();
|
||||
for (const section of nav) for (const page of section.pages) sectionByRel.set(page.rel, section.name);
|
||||
const orderedPages = nav.flatMap((s) => s.pages);
|
||||
|
||||
// Build sub-pages under /docs/<rel>.html
|
||||
for (const page of pages) {
|
||||
const html = markdownToHtml(page.markdown, page.rel);
|
||||
const toc = tocFromHtml(html);
|
||||
const idx = orderedPages.findIndex((p) => p.rel === page.rel);
|
||||
const prev = idx > 0 ? orderedPages[idx - 1] : null;
|
||||
const next = idx >= 0 && idx < orderedPages.length - 1 ? orderedPages[idx + 1] : null;
|
||||
const sectionName = sectionByRel.get(page.rel) || "Reference";
|
||||
const pageOut = path.join(outDir, "docs", page.outRel);
|
||||
fs.mkdirSync(path.dirname(pageOut), { recursive: true });
|
||||
fs.writeFileSync(pageOut, layout({ page, html, toc, prev, next, sectionName }), "utf8");
|
||||
}
|
||||
|
||||
// Copy hand-built landing site (root index.html, css, js, images, etc.)
|
||||
copyTree(siteSrcDir, outDir);
|
||||
|
||||
// Site-wide assets used by docs sub-pages
|
||||
fs.writeFileSync(path.join(outDir, "favicon.svg"), faviconSvg(), "utf8");
|
||||
fs.writeFileSync(path.join(outDir, ".nojekyll"), "", "utf8");
|
||||
if (cname) fs.writeFileSync(path.join(outDir, "CNAME"), cname, "utf8");
|
||||
validateLinks(outDir);
|
||||
console.log(`built docs site: ${path.relative(root, outDir)}`);
|
||||
|
||||
function readCname() {
|
||||
for (const candidate of [
|
||||
path.join(siteSrcDir, "CNAME"),
|
||||
path.join(docsDir, "CNAME"),
|
||||
path.join(root, "CNAME"),
|
||||
]) {
|
||||
if (fs.existsSync(candidate)) return fs.readFileSync(candidate, "utf8").trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function copyTree(src, dest) {
|
||||
if (!fs.existsSync(src)) return;
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const s = path.join(src, entry.name);
|
||||
const d = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
fs.mkdirSync(d, { recursive: true });
|
||||
copyTree(s, d);
|
||||
} else {
|
||||
fs.copyFileSync(s, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseFrontmatter(raw) {
|
||||
const match = raw.match(/^---\n([\s\S]*?)\n---\n?/);
|
||||
if (!match) return { frontmatter: {}, body: raw };
|
||||
const fm = {};
|
||||
for (const line of match[1].split("\n")) {
|
||||
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
|
||||
if (!m) continue;
|
||||
let value = m[2];
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
fm[m[1]] = value;
|
||||
}
|
||||
return { frontmatter: fm, body: raw.slice(match[0].length) };
|
||||
}
|
||||
|
||||
function stripStrayDirectives(body) {
|
||||
return body
|
||||
.replace(/\r\n/g, "\n")
|
||||
.split("\n")
|
||||
.filter((line) => !/^\s*\{:\s*[^}]*\}\s*$/.test(line))
|
||||
.map((line) => line.replace(/\s*\{:\s*[^}]*\}\s*$/, ""))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function allMarkdown(dir) {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs
|
||||
.readdirSync(dir, { withFileTypes: true })
|
||||
.flatMap((entry) => {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.name === "site") return [];
|
||||
if (entry.isDirectory()) return allMarkdown(full);
|
||||
return entry.name.endsWith(".md") ? [full] : [];
|
||||
})
|
||||
.sort();
|
||||
}
|
||||
|
||||
function outPath(rel) {
|
||||
if (rel === "index.md") return "index.html";
|
||||
if (rel === "README.md") return "index.html";
|
||||
if (rel.endsWith("/README.md")) return rel.replace(/README\.md$/, "index.html");
|
||||
return rel.replace(/\.md$/, ".html");
|
||||
}
|
||||
|
||||
function firstHeading(markdown) {
|
||||
return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
||||
}
|
||||
|
||||
function titleize(input) {
|
||||
return input.replaceAll("-", " ").replace(/\b\w/g, (m) => m.toUpperCase());
|
||||
}
|
||||
|
||||
function markdownToHtml(markdown, currentRel) {
|
||||
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
||||
const html = [];
|
||||
let paragraph = [];
|
||||
let list = null;
|
||||
let fence = null;
|
||||
let blockquote = [];
|
||||
|
||||
const flushParagraph = () => {
|
||||
if (!paragraph.length) return;
|
||||
html.push(`<p>${inline(paragraph.join(" "), currentRel)}</p>`);
|
||||
paragraph = [];
|
||||
};
|
||||
const closeList = () => {
|
||||
if (!list) return;
|
||||
html.push(`</${list}>`);
|
||||
list = null;
|
||||
};
|
||||
const flushBlockquote = () => {
|
||||
if (!blockquote.length) return;
|
||||
const inner = markdownToHtml(blockquote.join("\n"), currentRel);
|
||||
html.push(`<blockquote>${inner}</blockquote>`);
|
||||
blockquote = [];
|
||||
};
|
||||
const splitRow = (line) => {
|
||||
let trimmed = line.trim();
|
||||
if (trimmed.startsWith("|")) trimmed = trimmed.slice(1);
|
||||
if (trimmed.endsWith("|") && !trimmed.endsWith("\\|")) trimmed = trimmed.slice(0, -1);
|
||||
const cells = [];
|
||||
let current = "";
|
||||
for (let idx = 0; idx < trimmed.length; idx++) {
|
||||
const char = trimmed[idx];
|
||||
if (char === "\\" && trimmed[idx + 1] === "|") {
|
||||
current += "\\|";
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
if (char === "|") {
|
||||
cells.push(current.trim().replace(/\\\|/g, "|"));
|
||||
current = "";
|
||||
continue;
|
||||
}
|
||||
current += char;
|
||||
}
|
||||
cells.push(current.trim().replace(/\\\|/g, "|"));
|
||||
return cells;
|
||||
};
|
||||
const isDivider = (line) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(line);
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const fenceMatch = line.match(/^```([\w+-]+)?\s*$/);
|
||||
if (fenceMatch) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
flushBlockquote();
|
||||
if (fence) {
|
||||
const body = highlightCode(fence.lines.join("\n"), fence.lang);
|
||||
html.push(`<pre><code class="language-${escapeAttr(fence.lang)}">${body}</code></pre>`);
|
||||
fence = null;
|
||||
} else {
|
||||
fence = { lang: fenceMatch[1] || "text", lines: [] };
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (fence) {
|
||||
fence.lines.push(line);
|
||||
continue;
|
||||
}
|
||||
if (/^>\s?/.test(line)) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
blockquote.push(line.replace(/^>\s?/, ""));
|
||||
continue;
|
||||
}
|
||||
flushBlockquote();
|
||||
if (!line.trim()) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
continue;
|
||||
}
|
||||
if (/^\s*---+\s*$/.test(line)) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
html.push("<hr>");
|
||||
continue;
|
||||
}
|
||||
const heading = line.match(/^(#{1,4})\s+(.+)$/);
|
||||
if (heading) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
const level = heading[1].length;
|
||||
const text = heading[2].trim();
|
||||
const id = slug(text);
|
||||
const inner = inline(text, currentRel);
|
||||
if (level === 1) {
|
||||
html.push(`<h1 id="${id}">${inner}</h1>`);
|
||||
} else {
|
||||
html.push(
|
||||
`<h${level} id="${id}"><a class="anchor" href="#${id}" aria-label="Anchor link">#</a>${inner}</h${level}>`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (line.trimStart().startsWith("|") && line.includes("|", line.indexOf("|") + 1) && isDivider(lines[i + 1] || "")) {
|
||||
flushParagraph();
|
||||
closeList();
|
||||
const header = splitRow(line);
|
||||
const aligns = splitRow(lines[i + 1]).map((cell) => {
|
||||
const left = cell.startsWith(":");
|
||||
const right = cell.endsWith(":");
|
||||
return right && left ? "center" : right ? "right" : left ? "left" : "";
|
||||
});
|
||||
i += 1;
|
||||
const rows = [];
|
||||
while (i + 1 < lines.length && lines[i + 1].trimStart().startsWith("|")) {
|
||||
i += 1;
|
||||
rows.push(splitRow(lines[i]));
|
||||
}
|
||||
const th = header
|
||||
.map((c, idx) => `<th${aligns[idx] ? ` style="text-align:${aligns[idx]}"` : ""}>${inline(c, currentRel)}</th>`)
|
||||
.join("");
|
||||
const tb = rows
|
||||
.map(
|
||||
(r) =>
|
||||
`<tr>${r
|
||||
.map(
|
||||
(c, idx) =>
|
||||
`<td${aligns[idx] ? ` style="text-align:${aligns[idx]}"` : ""}>${inline(c, currentRel)}</td>`,
|
||||
)
|
||||
.join("")}</tr>`,
|
||||
)
|
||||
.join("");
|
||||
html.push(`<table><thead><tr>${th}</tr></thead><tbody>${tb}</tbody></table>`);
|
||||
continue;
|
||||
}
|
||||
const bullet = line.match(/^\s*-\s+(.+)$/);
|
||||
const numbered = line.match(/^\s*\d+\.\s+(.+)$/);
|
||||
if (bullet || numbered) {
|
||||
flushParagraph();
|
||||
const tag = bullet ? "ul" : "ol";
|
||||
if (list && list !== tag) closeList();
|
||||
if (!list) {
|
||||
list = tag;
|
||||
html.push(`<${tag}>`);
|
||||
}
|
||||
html.push(`<li>${inline((bullet || numbered)[1], currentRel)}</li>`);
|
||||
continue;
|
||||
}
|
||||
paragraph.push(line.trim());
|
||||
}
|
||||
flushParagraph();
|
||||
closeList();
|
||||
flushBlockquote();
|
||||
return html.join("\n");
|
||||
}
|
||||
|
||||
function inline(text, currentRel) {
|
||||
const stash = [];
|
||||
let out = text.replace(/`([^`]+)`/g, (_, code) => {
|
||||
stash.push(`<code>${escapeHtml(code)}</code>`);
|
||||
return ` | ||||