docs: add clawdex.sh feature pages and Rolodex-themed site

Adds 14 per-feature docs pages under docs/ covering install, quickstart,
people, notes, timeline, search, avatars, imports, vCard export, git
sync, markdown storage, doctor, and config. Replaces the placeholder
landing page with a single-page Rolodex/index-card site that fetches and
renders the docs client-side via marked + highlight.js, with code syntax
highlighting and dark-mode support. .nojekyll keeps GitHub Pages serving
the markdown files raw to the SPA.
This commit is contained in:
Peter Steinberger 2026-05-08 16:50:41 +01:00
parent c5fef34885
commit be315dd311
No known key found for this signature in database
16 changed files with 1999 additions and 11 deletions

0
.nojekyll Normal file
View File

96
docs/avatars.md Normal file
View File

@ -0,0 +1,96 @@
# Avatars
Every person can carry a single avatar image, stored as a real file next to
their `person.md`. Avatars are deliberately first-class — they are how
exported vCards stay recognizable in Apple Contacts, Google Contacts, and
phone dial screens.
On disk:
```text
people/sally-o-malley/
person.md
avatars/
avatar.jpg # or .png
```
The `person.md` frontmatter records the avatar's MIME type, sha256, source
(`manual`, `apple`, `google`), and updated time. The bytes are kept on disk;
the markdown only points at them.
## Set a manual avatar
```bash
clawdex person avatar set sally ~/Pictures/sally.jpg
clawdex person avatar set sally ~/Pictures/sally.png --dry-run
```
`--dry-run` inspects the source image (MIME, SHA256) and previews the
avatar metadata that would be written, without touching the data repo.
Manual avatars are sticky: subsequent `clawdex import apple` and
`clawdex import google` runs will **never overwrite a manual avatar**. To
let imports manage the avatar again, clear it first.
## Show
```bash
clawdex person avatar show sally
clawdex person avatar show sally --path
clawdex person avatar show sally --json
```
`--path` prints the absolute path to the avatar file and nothing else, so
you can pipe it into a viewer:
```bash
open "$(clawdex person avatar show sally --path)"
```
## Clear
```bash
clawdex person avatar clear sally
```
Clears the avatar metadata from `person.md`. The file under `avatars/` is
left alone in case you want to recover it; remove it manually if you're
sure.
## Avatars from imports
Avatars are opt-in on imports — the `--avatars` flag is required:
```bash
clawdex import apple --avatars
clawdex import google --account you@gmail.com --avatars
```
- **Apple.** Reads the thumbnail bytes that
[`Contacts.framework`](https://developer.apple.com/documentation/contacts)
hands out. macOS-only.
- **Google.** Calls `gog contacts raw --person-fields photos`, picks the
selected photo URL, fetches the bytes through
[`gog`](https://github.com/steipete/gogcli), and stores them locally.
Only metadata (URL, MIME, SHA256) is written into `person.md`.
In both cases the bytes live on your machine. Clawdex never silently
re-fetches them on later runs.
## vCard export
Avatars are embedded into vCards as base64 `PHOTO;ENCODING=b` payloads when
you opt in:
```bash
clawdex export vcard --all --include-avatars -o contacts.vcf
```
Without `--include-avatars`, the export is text-only — handy when you want a
small file or are emailing the vCard to someone.
## Related pages
- [People](people.md), [Imports](imports.md), [vCard Export](vcard-export.md)
- [Doctor](doctor.md) — `clawdex doctor` reports stale avatar metadata
(file gone, hash mismatch) and `--repair` rewrites it from disk.

104
docs/config.md Normal file
View File

@ -0,0 +1,104 @@
# Config
Clawdex has two config files, on purpose:
| File | Scope | What lives here |
|-------------------------------|---------------|----------------------------------------------|
| `~/.clawdex/config.toml` | User-level | Default repo path, default Google account. |
| `<repo>/clawdex.toml` | Repo-local | Git remote, branch, repair behavior. |
The user-level file follows you across data repos. The repo-local file
follows the data — it ships with the repo when you clone it on a second
machine.
## User-level config
Default location: `~/.clawdex/config.toml`. Override with `--config PATH`
or `CLAWDEX_CONFIG=PATH`.
```toml
repo_path = "/Users/you/.clawdex/contacts"
[google]
default_account = "you@gmail.com"
```
## Repo-local config
Default location: `<repo>/clawdex.toml`. Created by `clawdex init`.
```toml
[git]
remote = "https://github.com/you/backup-clawdex.git"
branch = "main"
[repair]
backup_before_repair = true
auto_repair = false
```
## `clawdex config`
```bash
clawdex config # default subcommand: show
clawdex config show
clawdex config show --json
clawdex config set repo_path ~/.clawdex/contacts
clawdex config set git.remote https://github.com/you/backup-clawdex.git
clawdex config set git.branch main
clawdex config set google.default_account you@gmail.com
```
`clawdex config set` writes the user-level config file. `--dry-run`
echoes the resolved config without writing.
Supported keys:
- `repo_path`
- `git.remote`
- `git.branch`
- `google.default_account`
Other keys are edited by hand in the appropriate TOML file.
## Per-run overrides
Every config value can be overridden for a single command:
```bash
clawdex --config /tmp/alt.toml person list
clawdex --repo /tmp/scratch person add "Test Person"
CLAWDEX_REPO=/tmp/scratch clawdex import apple --dry-run
```
These don't persist anything — they're for one-off runs, CI jobs, and
testing.
## Global flags
These apply to every subcommand:
| Flag | Env | Effect |
|------------------|--------------------|---------------------------------------------------------|
| `--config PATH` | `CLAWDEX_CONFIG` | Override the user-level config path. |
| `--repo DIR` | `CLAWDEX_REPO` | Override the contacts data repo for this run. |
| `--json` | | Stable JSON envelope on stdout. |
| `--plain` | | TSV on stdout (script-friendly). |
| `--dry-run`, `-n`| | Preview without writing. |
| `--no-input` | | Never prompt; useful in CI. |
| `--verbose`, `-v`| | Verbose diagnostics on stderr. |
| `--version` | | Print version and exit. |
## Environment
- `CLAWDEX_CONFIG` — user-level config path.
- `CLAWDEX_REPO` — contacts data repo path.
- `EDITOR` — used by `clawdex person edit`. Falls back to `code` (Visual
Studio Code) when unset.
## Related pages
- [Quickstart](quickstart.md)
- [Markdown Storage](markdown-storage.md)
- [Git Sync](git-sync.md)
- [Doctor](doctor.md)

110
docs/doctor.md Normal file
View File

@ -0,0 +1,110 @@
# Doctor
`clawdex doctor` is a one-shot health check for your data repo. It reports
counts and surfaces problems; with `--repair`, it fixes the ones it knows
how to fix.
```bash
clawdex doctor
clawdex doctor --json
clawdex doctor --repair --dry-run
clawdex doctor --repair
```
## What it reports
```text
config_path: /Users/you/.clawdex/config.toml
repo_path: /Users/you/.clawdex/contacts
remote: https://github.com/you/backup-clawdex.git
people: 412
git_dirty: true
avatar_problems: 3
```
Fields:
- `config_path` — the user-level config clawdex loaded.
- `repo_path` — which contacts data repo is in use (after `--repo`,
`CLAWDEX_REPO`, and config resolution).
- `remote`, `people`, `git_dirty` — sanity numbers.
- `avatar_problems` — count of person records whose avatar metadata
doesn't match what's on disk (file gone, sha256 mismatch). Only printed
when non-zero.
## What `--repair` repairs
Two classes of problem:
### 1. Damaged frontmatter
If `person.md` or a note's YAML frontmatter is malformed — a stray quote,
a truncated block, a dangling key — clawdex's strict parse fails. The
repair pass:
1. Salvages known scalar keys: `id`, `name`, `created_at`, and the note
fields (`kind`, `source`, `occurred_at`, `topics`).
2. Infers a missing `id` from the file path and a missing
`created_at`/`updated_at` from the file mtime.
3. Preserves the Markdown body verbatim.
4. Copies the original damaged file under `.clawdex/repairs/` so nothing
is lost.
5. Appends the unsalvageable scrap to the body under a `## Recovered
metadata` heading, so you can finish the cleanup by hand.
### 2. Stale avatar metadata
If `person.md` says there's an avatar but the file is missing, the SHA256
no longer matches, or the MIME type is wrong, repair will:
- Drop the metadata if the file is gone.
- Recompute MIME and SHA256 if the file is present.
- Leave the bytes themselves untouched.
Avatar bytes are never *recovered* by repair — only the metadata is
synced to whatever's actually on disk. Use
[`clawdex person avatar set`](avatars.md) to put bytes back.
## Dry-run
```bash
clawdex doctor --repair --dry-run
```
Walks the repo and reports counts without writing anything. The output
includes:
```text
repaired: 4
avatar_repaired: 2
dry_run: true
```
Treat the counts as the worst case — once you re-run without `--dry-run`,
the numbers should drop to zero on the next clean `clawdex doctor`.
## When to run it
- After a big import (`apple`, `google`, `birdclaw`, `discrawl`).
- After resolving Git merge conflicts in the data repo.
- After a hand-edit binge.
- As the last step before `clawdex git commit && clawdex git push`.
`clawdex doctor` is read-only. `clawdex doctor --repair` is the only
variant that writes; it always backs up the original file first when
`repair.backup_before_repair = true` (the default).
## Exit codes
- `0` — everything looked fine.
- `1` — runtime error (bad path, IO error, malformed config).
- `2` — usage error (bad flag combination).
`avatar_problems` and `git_dirty` are diagnostics, not errors. They do not
change the exit code on their own.
## Related pages
- [Markdown Storage](markdown-storage.md)
- [People](people.md), [Notes](notes.md), [Avatars](avatars.md)
- [Config](config.md)

120
docs/git-sync.md Normal file
View File

@ -0,0 +1,120 @@
# Git Sync
Your contacts data repo is just a Git repo. Clawdex doesn't run a sync
daemon, doesn't talk to a clawdex.sh server, and doesn't store anything
about you remotely. Backup and multi-device sync are 100% Git.
The default suggested remote is a *private* GitHub repo:
```text
https://github.com/<you>/backup-clawdex.git
```
You set this once in [Quickstart](quickstart.md) with:
```bash
clawdex config set git.remote https://github.com/<you>/backup-clawdex.git
```
## `clawdex git` commands
All four commands run *inside the data repo*, not the clawdex source repo.
They're thin wrappers around `git` so you don't have to `cd` constantly.
### Status
```bash
clawdex git status
```
A wrapper for `git -C <repo> status --short --branch`. Same output, same
exit codes. Use this before committing to see what an import or edit
changed.
### Commit
```bash
clawdex git commit
clawdex git commit -m "sync: import google contacts"
```
Stages everything modified under the data repo and commits with the
provided message. The default message is
`sync: update clawdex contacts`. Returns whether a commit was actually
created — if there were no changes, `committed: false`.
### Pull
```bash
clawdex git pull
```
Pulls from the configured remote on the configured branch. Resolve
conflicts the way you'd resolve them in any other repo.
### Push
```bash
clawdex git push
```
Pushes to the configured remote on the configured branch. The first push
on a fresh repo also sets the upstream.
## Choosing a remote
Anything Git-hostable works:
- **Private GitHub repo** — recommended; integrates with GitHub Mobile if
you want to read your notes on a phone.
- **Self-hosted** — Forgejo, Gitea, sourcehut, a bare repo over SSH.
- **Local-only** — leave `git.remote` unset and clawdex won't push. The
repo is still version-controlled locally.
Whatever you pick, **the remote should be private**. The data is plain
markdown — names, phone numbers, emails, conversation snippets. Treat it
like a journal.
## Multi-device
The flow on a second machine is the same as on the first:
```bash
brew install steipete/tap/clawdex
git clone https://github.com/<you>/backup-clawdex.git ~/.clawdex/contacts
clawdex config set repo_path ~/.clawdex/contacts
clawdex config set git.remote https://github.com/<you>/backup-clawdex.git
clawdex git pull
clawdex doctor
```
Day-to-day:
```bash
clawdex git pull
# ... edit, import, add notes ...
clawdex git commit -m "sync: ..."
clawdex git push
```
Conflicts are normal Git conflicts. Markdown frontmatter merges
predictably; if a frontmatter merge ends up malformed, run
[`clawdex doctor --repair`](doctor.md) to salvage it.
## Encryption
vanilla `clawdex git` does **not** encrypt the repo. The data lives as
plaintext markdown both locally and on the remote. If you need encryption
at rest:
- Use a private remote you trust.
- Or layer `git-crypt` / `age` over the data repo manually.
- Or back up encrypted snapshots out-of-band, alongside Git.
A built-in encrypted backup mode (à la `gog backup`) is on the roadmap
but not shipped.
## Related pages
- [Quickstart](quickstart.md), [Config](config.md)
- [Markdown Storage](markdown-storage.md)

128
docs/imports.md Normal file
View File

@ -0,0 +1,128 @@
# Imports
Imports project an external contact graph into the same markdown shape
clawdex uses everywhere else. They are **local-only**: every import only
writes to your data repo. No address book on Apple, Google, X, or Discord
is mutated by any `clawdex import` subcommand.
If you want to push changes back into Apple Contacts or Google Contacts, see
[Sync](#sync-preview-only) below — it's preview-only today.
## What gets written
Each import returns a list of `ImportChange` rows, printed as TSV:
```text
add Sally O'Malley sally-o-malley
update Bo Burnham bo-burnham
unchanged Frank Booth frank-booth
```
Combine with `--dry-run` to preview without writing:
```bash
clawdex import apple --dry-run
clawdex import google --account you@gmail.com --dry-run
```
## Apple Contacts
```bash
clawdex import apple --dry-run
clawdex import apple
clawdex import apple --avatars
clawdex import apple --input ~/Desktop/contacts.json
```
- Default source is the macOS Contacts database via `Contacts.framework`.
The first run prompts for *Contacts* access in System Settings.
- `--input PATH` reads JSON or NDJSON instead — useful on Linux, in CI,
or when round-tripping a snapshot.
- `--avatars` imports thumbnail bytes. Without it, only structured fields
are imported.
Manual avatars set with [`clawdex person avatar set`](avatars.md) are never
overwritten. Tags, notes, and any custom frontmatter you've added by hand
are preserved.
## Google Contacts
```bash
clawdex import google --account you@gmail.com --dry-run
clawdex import google --account you@gmail.com
clawdex import google --account you@gmail.com --avatars
```
The Google adapter shells out to [`gog`](https://github.com/steipete/gogcli),
the local-first Google Workspace CLI. You need to be authenticated there
first:
```bash
gog auth credentials ~/Downloads/client_secret_*.json
gog auth add you@gmail.com --services contacts
```
If `--account` is omitted, clawdex falls back to `google.default_account`
from your config — set it once with:
```bash
clawdex config set google.default_account you@gmail.com
```
`--avatars` fetches photo bytes through `gog contacts raw --person-fields photos`
and stores them locally.
## Birdclaw — X / Twitter DMs
```bash
clawdex import birdclaw --dry-run
clawdex import birdclaw --min-messages 4
clawdex import birdclaw --db ~/.birdclaw/birdclaw.sqlite
```
Reads from your local [birdclaw](https://github.com/steipete/birdclaw)
SQLite archive. For each DM thread above the `--min-messages` threshold,
clawdex creates or updates a person, stores the X handle as a stable
pointer under `accounts.x`, and adds a source-specific tag.
The default DB path is `~/.birdclaw/birdclaw.sqlite`. Threads with fewer
than `--min-messages` messages are skipped — most of those are one-shot
spam or intros that died.
## Discrawl — Discord DMs
```bash
clawdex import discrawl --dry-run
clawdex import discrawl --min-messages 4
clawdex import discrawl --db ~/.discrawl/discrawl.db
```
Same shape as birdclaw, but reads from
[discrawl](https://github.com/steipete/discrawl)'s SQLite cache. Discord
handles land under `accounts.discord`.
## Sync (preview-only)
```bash
clawdex sync apple
clawdex sync google --account you@gmail.com
```
These commands exist as placeholders. They report:
```text
status: remote writes not implemented yet; use import apple for local
markdown projection
```
Two-way sync requires a conflict report you can read before anything is
written remotely; that report doesn't exist yet, so the writes don't
either. Until it lands, treat clawdex as a one-way mirror: imports come
in, [vCard export](vcard-export.md) goes out.
## Related pages
- [People](people.md), [Avatars](avatars.md)
- [Markdown Storage](markdown-storage.md) — the shape imports project into
- [Git Sync](git-sync.md) — committing the import diff
- [Config](config.md) — `google.default_account`, repo path

59
docs/index.md Normal file
View File

@ -0,0 +1,59 @@
# Clawdex
`clawdex` is a local-first, markdown-first personal contact index. The CLI
lives in this repo. Your contacts live in a separate private Git-backed
markdown repository that you own.
The whole thing is a Rolodex you can `grep` — every person is a folder, every
note is a timestamped markdown file, every change is a commit.
## Try it
After [installing](install.md) and [pointing clawdex at a data repo](quickstart.md):
```bash
clawdex person add "Sally O'Malley" --email sally@example.com --tag friend
clawdex note add sally --kind dm --source whatsapp --text "Follow up about dinner"
clawdex timeline sally
clawdex search dinner
clawdex export vcard --all --include-avatars -o contacts.vcf
```
`--json` produces a stable JSON envelope on stdout. `--plain` produces TSV.
Human messages go to stderr, so pipes stay parseable.
## What clawdex does
- **Markdown is canonical.** People are folders under `people/`. Notes are
timestamped files. Indexes under `index/` are derived and rebuildable.
- **Git is the sync layer.** No proprietary daemon, no opaque database.
`clawdex git push` pushes to your private remote.
- **Imports are local-only.** Apple, Google, Birdclaw (X DMs), and Discrawl
(Discord DMs) all project into the same markdown shape. Address-book writes
are still preview-only — see [Imports](imports.md).
- **Avatars are real files.** Stored next to each person; `vcard` export can
embed them; manual avatars are never overwritten by imports.
- **Notes are local-only.** They are never written to Apple or Google.
## Pick your path
- **Trying it.** [Install](install.md) → [Quickstart](quickstart.md). Five
minutes from `brew install` to your first commit.
- **Importing the network you already have.** [Imports](imports.md) covers
Apple Contacts, Google Contacts, X DMs (birdclaw), and Discord DMs
(discrawl).
- **Daily use.** [People](people.md), [Notes](notes.md),
[Timeline](timeline.md), [Search](search.md).
- **Sharing back.** [vCard Export](vcard-export.md) for round-tripping into
Apple/Google/anything-else, [Git Sync](git-sync.md) for the private backup
remote.
- **Repair and storage.** [Markdown Storage](markdown-storage.md),
[Doctor](doctor.md), [Config](config.md).
## Project
Source: [openclaw/clawdex](https://github.com/openclaw/clawdex).
[Changelog](https://github.com/openclaw/clawdex/blob/main/CHANGELOG.md) tracks
what shipped recently. Released under the
[MIT license](https://github.com/openclaw/clawdex/blob/main/LICENSE). Not
affiliated with Apple, Google, X, or Discord.

80
docs/install.md Normal file
View File

@ -0,0 +1,80 @@
# Install
`clawdex` ships as a single Go binary. The visible version is injected at
build time: release builds use the tag, local builds use `git describe`.
## Homebrew (macOS, Linux)
```bash
brew install steipete/tap/clawdex
clawdex --version
```
The Homebrew formula lives in `steipete/homebrew-tap` and is updated by the
clawdex release workflow after each tagged release.
## Go install
```bash
go install github.com/openclaw/clawdex/cmd/clawdex@latest
clawdex --version
```
Source builds require the Go version declared in
[`go.mod`](https://github.com/openclaw/clawdex/blob/main/go.mod).
## Build from source
```bash
git clone https://github.com/openclaw/clawdex.git
cd clawdex
go build -o ./bin/clawdex ./cmd/clawdex
./bin/clawdex --version
```
## GitHub release archives
Release assets are published by GoReleaser:
- `clawdex_<version>_darwin_amd64.tar.gz`
- `clawdex_<version>_darwin_arm64.tar.gz`
- `clawdex_<version>_linux_amd64.tar.gz`
- `clawdex_<version>_linux_arm64.tar.gz`
- `clawdex_<version>_windows_amd64.zip`
- `checksums.txt`
Browse the [releases page](https://github.com/openclaw/clawdex/releases) for
the latest tag.
## Platform notes
- **macOS** is the most exercised target. `clawdex import apple` reads
Contacts via `Contacts.framework`, so the binary must be allowed in
*Settings → Privacy & Security → Contacts* the first time you run it.
- **Linux** builds support markdown editing, notes, search, Git, Google
imports through `gog`, and vCard export. Apple direct import is macOS-only.
- **Windows** binaries are produced but lightly tested; the Git layer assumes
a working `git` on `PATH`.
## Verify the install
```bash
clawdex --version
clawdex --help
clawdex doctor
```
After `clawdex init` (see [Quickstart](quickstart.md)), `clawdex doctor`
prints a one-shot health summary: config path, repo path, remote, person
count, and any avatar problems.
## Updating
- **Homebrew:** `brew upgrade clawdex`.
- **Go install:** rerun `go install github.com/openclaw/clawdex/cmd/clawdex@latest`.
- **Release archives:** download the new tarball and replace the binary.
- **Source:** `git pull && go build -o ./bin/clawdex ./cmd/clawdex`.
The on-disk markdown layout is forward-compatible across point releases. A
breaking layout change would ship a `clawdex doctor --repair` migration —
see [Doctor](doctor.md).

140
docs/markdown-storage.md Normal file
View File

@ -0,0 +1,140 @@
# Markdown Storage
Clawdex stores everything as files on disk. There is no database, no opaque
binary format, no migration step that locks you out. If clawdex disappeared
tomorrow, you would still have your data, in plaintext, in a Git repo.
## Layout
```text
<repo>/
clawdex.toml # repo-local settings
people/
sally-o-malley/
person.md
avatars/
avatar.jpg
notes/
2026-05-08T09-15-00Z-whatsapp.md
2026-05-09T18-02-00Z-imessage.md
attachments/
... # opt-in; not yet wired into the CLI
index/
emails.json
phones.json
handles.json
.clawdex/
repairs/ # backups written by `doctor --repair`
```
Slugs are derived from the person's name and are stable: renaming a person
in `person.md` updates the display name but keeps the folder path. To
rename the slug itself, move the folder by hand and re-run
[`clawdex doctor`](doctor.md).
## `person.md`
```markdown
---
id: sally-o-malley
name: Sally O'Malley
emails:
- value: sally@example.com
kind: work
phones:
- value: "+15550100"
kind: mobile
tags: [friend, dinner-club]
accounts:
x: { handle: sally }
discord: { id: "234234234234234234", username: "sally" }
created_at: 2026-05-08T09:15:00Z
updated_at: 2026-05-08T09:15:00Z
avatar:
path: avatars/avatar.jpg
mime: image/jpeg
sha256: "..."
source: manual
---
# Sally O'Malley
Met at the dinner club in 2024. Loves Negronis.
```
YAML frontmatter is parsed strictly first; if that fails, clawdex falls
back to a best-effort scalar salvage and copies the original file under
`.clawdex/repairs/` before writing anything new. See
[Doctor](doctor.md).
The Markdown body is preserved verbatim across reads, edits, and repair.
Your hand-written prose is safe.
## Note files
Notes are timestamped markdown files under `notes/`:
```markdown
---
id: 2026-05-08T09-15-00Z-whatsapp
kind: dm
source: whatsapp
occurred_at: 2026-05-08T09:15:00Z
created_at: 2026-05-08T09:15:00Z
topics: [dinner, logistics]
---
Follow up about dinner next Thursday.
```
The filename encodes `occurred_at` as `2006-01-02T15-04-05Z` plus the
source. Sorting by filename and sorting by `occurred_at` produce the same
order, which is intentional — `ls notes/` is a serviceable timeline.
See [Notes](notes.md) and [Timeline](timeline.md).
## Index files
`index/*.json` are derived caches:
- `emails.json` — email → person ID
- `phones.json` — normalized phone → person ID
- `handles.json` — service handle (X, Discord, …) → person ID
Clawdex rebuilds these on read whenever they're stale. They are safe to
delete — the next command will regenerate them. They are *not* the source
of truth: markdown is.
## clawdex.toml
A small repo-local config file written by `clawdex init`:
```toml
[git]
remote = "https://github.com/you/backup-clawdex.git"
branch = "main"
[repair]
backup_before_repair = true
auto_repair = false
```
This is *separate* from the user-level config at `~/.clawdex/config.toml`,
which holds your default repo path, default Google account, and editor
preferences. See [Config](config.md).
## Why markdown
- **Diffable.** A person rename, a tag change, a note edit — they show up
as readable diffs in `git log`.
- **Editable anywhere.** Your editor, GitHub's web UI, mobile markdown
editors, plain `vim` over SSH.
- **Greppable.** `rg`, `awk`, and `sed` work on the data repo without
needing clawdex on the host.
- **Future-proof.** Plain text outlives every CLI built on top of it.
## Related pages
- [People](people.md), [Notes](notes.md), [Avatars](avatars.md)
- [Doctor](doctor.md), [Config](config.md)
- [Git Sync](git-sync.md)

84
docs/notes.md Normal file
View File

@ -0,0 +1,84 @@
# Notes
A *note* is a timestamped event attached to a person. Notes capture what
contacts can't: what you talked about, when, and where it happened.
Notes live under `people/<slug>/notes/`. Each note is a single markdown file
named `<occurred-at>-<source>.md`, e.g.
`2026-05-08T09-15-00Z-whatsapp.md`.
**Notes are local-only.** They are never written to Apple Contacts, Google
Contacts, or anywhere else. They are pushed only to your private backup
remote when you run [`clawdex git push`](git-sync.md).
## Add
```bash
clawdex note add sally \
--kind dm \
--source whatsapp \
--text "Follow up about dinner next Thursday" \
--topic dinner --topic logistics
```
Flags:
- `--kind` *(required)* — type of interaction: `dm`, `call`, `meeting`,
`email`, `event`, etc. Free-form; clawdex doesn't enforce a vocabulary.
- `--source` *(required)* — where it happened: `whatsapp`, `imessage`,
`discord`, `x`, `email`, `inperson`, etc.
- `--text` *(required)* — note body. Multiline is fine; quote it.
- `--occurred-at` — ISO 8601 (`2026-05-08T09:15:00Z`), `2026-05-08 09:15`,
or `2026-05-08`. Defaults to *now*.
- `--topic` — repeatable, becomes the `topics:` frontmatter array.
`--dry-run` shows the resolved note as JSON without writing.
## List
```bash
clawdex note list sally
clawdex note list sally --json
```
The default output is a TSV of `occurred-at<TAB>kind<TAB>source<TAB>body`.
Note bodies are flattened to a single line for TSV; use `--json` to get the
full body.
## Note file format
```markdown
---
id: 2026-05-08T09-15-00Z-whatsapp
kind: dm
source: whatsapp
occurred_at: 2026-05-08T09:15:00Z
created_at: 2026-05-08T09:15:00Z
topics: [dinner, logistics]
---
Follow up about dinner next Thursday.
```
You can edit notes directly with `$EDITOR` — clawdex reads them back. If
the frontmatter gets damaged, [`clawdex doctor --repair`](doctor.md)
salvages known fields and preserves the body verbatim under a *Recovered
metadata* heading.
## Conventions that pay off
- **Use stable `--source` values.** `whatsapp`, not `WhatsApp`. Search and
imports both rely on lowercase source slugs.
- **Tag topics, not relationships.** Topics are about the *content*; tags
on the person are about the *role*. A `dinner` topic is fine; a
`friend` topic is probably a person tag instead.
- **Don't store secrets in notes.** They sync to your private backup repo,
but they're still in plain markdown on disk. Treat them like a Git repo,
not a vault.
## Related pages
- [People](people.md), [Timeline](timeline.md), [Search](search.md)
- [Imports](imports.md) — birdclaw and discrawl create one note per imported
DM thread head.
- [Markdown Storage](markdown-storage.md)

101
docs/people.md Normal file
View File

@ -0,0 +1,101 @@
# People
A *person* is the unit of clawdex. On disk, each person is a folder under
`people/` containing a `person.md` file, an optional `avatars/` folder, and
an optional `notes/` folder. The folder slug is derived from the person's
name — for example `Sally O'Malley` becomes `sally-o-malley`.
## Add
```bash
clawdex person add "Sally O'Malley" \
--email sally@example.com -e sally.alt@example.com \
--phone "+1 555 0100" \
--tag friend --tag dinner-club
```
Flags:
- `--email`, `-e` — repeatable
- `--phone`, `-p` — repeatable
- `--tag`, `-t` — repeatable
`--dry-run` previews the planned slug without writing anything.
## List
```bash
clawdex person list
clawdex person list --query sally
clawdex person list --json | jq '.[].name'
```
`--query` filters by substring match against name, ID, and tags. The default
output is a TSV of `id<TAB>name<TAB>first-email`.
## Show
```bash
clawdex person show sally
clawdex person show sally@example.com
clawdex person show "+15550100"
clawdex person show sally-o-malley # exact ID
```
`show` accepts an ID, a substring of the name, an email, or a phone number.
The first unambiguous match wins. If multiple people share a key, the
command errors and asks you to be more specific.
## Edit
```bash
clawdex person edit sally
EDITOR=nvim clawdex person edit sally
```
Opens the person's `person.md` in `$EDITOR`, falling back to `code` (Visual
Studio Code) if `EDITOR` is unset. Clawdex re-reads the file on the next
command — your edits are the source of truth.
You can also edit `person.md` directly in your shell, in another editor, or
in a pull request review. The file is plain markdown:
```markdown
---
id: sally-o-malley
name: Sally O'Malley
emails:
- value: sally@example.com
phones:
- value: "+15550100"
tags: [friend, dinner-club]
created_at: 2026-05-08T09:15:00Z
updated_at: 2026-05-08T09:15:00Z
---
# Sally O'Malley
Met at the dinner club in 2024. Loves Negronis.
```
If frontmatter gets damaged — a stray quote, a truncated YAML block —
[`clawdex doctor --repair`](doctor.md) salvages what it can and preserves
the body.
## Avatars
Avatars are managed under `clawdex person avatar`. They're a feature on
their own: see [Avatars](avatars.md).
## Bulk import
To populate clawdex from Apple Contacts, Google Contacts, X DMs, or Discord
DMs in one shot, use [Imports](imports.md). Imports project into the same
markdown shape — they don't bypass any of the rules above.
## Related pages
- [Notes](notes.md), [Timeline](timeline.md)
- [Search](search.md)
- [Markdown Storage](markdown-storage.md)
- [Doctor](doctor.md)

139
docs/quickstart.md Normal file
View File

@ -0,0 +1,139 @@
# Quickstart
Five minutes from a fresh install to a populated, committed contact index.
## 1. Install
```bash
brew install steipete/tap/clawdex
clawdex --version
```
Other paths (Go install, source build, release archives) are documented on
[Install](install.md).
## 2. Pick a place for your contacts
The CLI lives in this repo. Your contacts live in **a separate, private
markdown repo** that you own. The default suggested remote is:
```text
https://github.com/<you>/backup-clawdex.git
```
Create that empty private repo on GitHub first — it's where your data will
back up to.
## 3. Initialize a contacts data repo
```bash
clawdex init ~/.clawdex/contacts
clawdex config set repo_path ~/.clawdex/contacts
clawdex config set git.remote https://github.com/<you>/backup-clawdex.git
```
`init` writes:
```text
clawdex.toml
people/
index/
.clawdex/repairs/
```
The app config lives at `~/.clawdex/config.toml` by default. `--repo DIR` or
`CLAWDEX_REPO=DIR` overrides the configured repo for one run. See
[Config](config.md) for the full key list.
## 4. Add your first person
```bash
clawdex person add "Sally O'Malley" \
--email sally@example.com \
--phone "+1 555 0100" \
--tag friend
```
Look at what just appeared in the data repo:
```text
people/sally-o-malley/
person.md
```
Open `person.md` — it's plain markdown with YAML frontmatter. Edit it by
hand, in your editor, on another machine, in a Pull Request review. Clawdex
will read your edits back. See [People](people.md).
## 5. Add a note
```bash
clawdex note add sally \
--kind dm \
--source whatsapp \
--text "Follow up about dinner next Thursday"
clawdex timeline sally
```
Notes land in `people/sally-o-malley/notes/` as timestamped files. They are
local-only — never written to Apple, Google, or anywhere else. See
[Notes](notes.md) and [Timeline](timeline.md).
## 6. Search across everything
```bash
clawdex search dinner
clawdex search +1555
clawdex search sally@example.com
```
Search hits emails, phones, names, tags, and note bodies. See
[Search](search.md).
## 7. Import the network you already have
Optional, but most people start here on day one. All imports are local-only:
they only write to your markdown repo. Address-book writes (Apple Contacts,
Google Contacts) are not implemented yet — see [Imports](imports.md).
```bash
clawdex import apple --dry-run
clawdex import apple --avatars
clawdex import google --account you@gmail.com --dry-run
clawdex import google --account you@gmail.com --avatars
clawdex import birdclaw --min-messages 4 --dry-run
clawdex import discrawl --min-messages 4 --dry-run
```
## 8. Commit and push
```bash
clawdex git status
clawdex git commit -m "sync: import apple + google"
clawdex git push
```
`git status` is a thin wrapper over `git -C <repo> status --short --branch`.
Commit and push run inside the data repo, not this repo. See
[Git Sync](git-sync.md).
## 9. Export back into the world
```bash
clawdex export vcard --all --include-avatars -o ~/Desktop/contacts.vcf
clawdex export vcard --person sally -o - # stdout
```
The `.vcf` file imports cleanly into macOS Contacts, Google Contacts, iOS
Contacts, and most other address books. See [vCard Export](vcard-export.md).
## Where next
- **The four pillars.** [People](people.md), [Notes](notes.md),
[Avatars](avatars.md), [Imports](imports.md).
- **Flow.** [Search](search.md), [Timeline](timeline.md),
[Git Sync](git-sync.md), [vCard Export](vcard-export.md).
- **Maintenance.** [Doctor](doctor.md), [Markdown Storage](markdown-storage.md),
[Config](config.md).

78
docs/search.md Normal file
View File

@ -0,0 +1,78 @@
# Search
`clawdex search <query>` finds people and notes that match a substring. It's
a local, offline, full-corpus search — no external service.
```bash
clawdex search dinner
clawdex search sally@example.com
clawdex search +1555
clawdex search "negroni recipe"
clawdex search whatsapp --json
```
## What gets searched
- Person names, IDs, and tags
- Person emails and phone numbers (normalized)
- Note bodies, kinds, sources, and topics
Hits are printed one per line, with the kind, name, a snippet, and the
file path:
```text
person Sally O'Malley sally@example.com people/sally-o-malley/person.md
note Sally O'Malley ...follow up about dinner... people/sally-o-malley/notes/2026-05-08T09-15-00Z-whatsapp.md
```
`--json` returns a list of `SearchHit` objects; `--plain` swaps the
snippet for the stable ID, which is friendlier to scripts.
## How matching works
The query is a case-insensitive substring match against indexed fields.
For phone numbers the search normalizes both the query and the stored
value (strips spaces, dashes, parentheses, and a leading `+`), so any of
these find Sally:
```bash
clawdex search "+1 555 0100"
clawdex search "(555) 0100"
clawdex search 15550100
```
For emails the match is plain substring against the lowercase value, so
`gmail.com` works as a "find everyone on Gmail" query.
## Combine with timeline and grep
`search` is for finding the right thread; once you've got it, use
[`timeline`](timeline.md) for the full history of that person, or `rg` for
free-form regex on the data repo:
```bash
clawdex search "ankara"
clawdex timeline mehmet
rg -n "ankara" ~/.clawdex/contacts/people
```
## Indexes
Derived indexes live under `index/`:
```text
index/
emails.json
phones.json
handles.json
```
These are rebuilt automatically as the markdown changes. They are
*derivable*, not authoritative — delete the folder and clawdex regenerates
it on the next read. Markdown is canonical; see
[Markdown Storage](markdown-storage.md).
## Related pages
- [People](people.md), [Notes](notes.md), [Timeline](timeline.md)
- [Markdown Storage](markdown-storage.md)

59
docs/timeline.md Normal file
View File

@ -0,0 +1,59 @@
# Timeline
`clawdex timeline <person>` prints every note for one person, sorted by
`occurred_at`. It's the fastest way to remember what's been going on with
someone.
```bash
clawdex timeline sally
clawdex timeline sally --json
clawdex timeline sally@example.com --plain
```
Default output is a TSV:
```text
2026-04-12T19:30:00Z meeting inperson Drinks at Bar Centrale
2026-04-22T08:01:00Z dm whatsapp Sent recipe link
2026-05-08T09:15:00Z dm whatsapp Follow up about dinner
```
`--json` returns the full note objects, including bodies, topics, and IDs.
`--plain` is a simpler TSV intended for `awk`/`cut` pipelines.
## Resolving the person
The argument is the same query string that
[`clawdex person show`](people.md) accepts: an ID slug, a name substring,
an email, or a phone number. The first unambiguous match wins.
## Reading flow
Pair `timeline` with `search` for a quick "what was that about" loop:
```bash
clawdex search "negroni" # find the conversation
clawdex timeline sally | head -20 # surrounding context
```
Or pipe into `less` for long histories:
```bash
clawdex timeline sally | column -t -s $'\t' | less -S
```
## Limits
- Sort key is `occurred_at`, not file mtime. Edit `occurred_at` in the note
frontmatter to fix ordering after the fact.
- Only one person at a time. To get a multi-person timeline, use
[Search](search.md) with a date-ish term, or grep notes directly:
```bash
rg -n "occurred_at: 2026-05" ~/.clawdex/contacts/people
```
## Related pages
- [Notes](notes.md), [People](people.md), [Search](search.md)
- [Markdown Storage](markdown-storage.md)

75
docs/vcard-export.md Normal file
View File

@ -0,0 +1,75 @@
# vCard Export
`clawdex export vcard` writes one or more people as RFC 6350 vCards. The
result imports cleanly into Apple Contacts, Google Contacts, iOS Contacts,
Outlook, and most other address books.
This is the *outbound* half of clawdex. [Imports](imports.md) bring the
world in; vCard export sends a curated slice back out.
## Export everything
```bash
clawdex export vcard --all -o contacts.vcf
clawdex export vcard --all --include-avatars -o contacts.vcf
```
Without `--include-avatars`, the file is text-only and small. With it, each
person's avatar is embedded as a base64 `PHOTO;ENCODING=b` payload. A few
hundred avatars adds up — expect a few megabytes.
## Export one person
```bash
clawdex export vcard --person sally -o sally.vcf
clawdex export vcard --person sally@example.com -o sally.vcf
```
The `--person` argument accepts the same query string that
[`clawdex person show`](people.md) accepts: an ID, a name substring, an
email, or a phone number.
## Stream to stdout
`-o -` writes to stdout, so you can pipe directly:
```bash
clawdex export vcard --person sally -o - | pbcopy # macOS
clawdex export vcard --all -o - | wl-copy # wayland
clawdex export vcard --person sally -o - | mail -a contacts.vcf you@example.com
```
## What's in the vCard
Each vCard includes:
- `FN` — display name from `person.md`
- `N` — best-effort surname/given split
- `EMAIL` per email entry, with the original `kind` as a `TYPE` parameter
when present
- `TEL` per phone entry, with the original `kind` as a `TYPE` parameter
- `NOTE` — short summary from the person body, when present
- `PHOTO;ENCODING=b` — only when `--include-avatars` is set
- `UID` — the person's stable ID slug, so re-imports update existing
cards instead of duplicating
## Round-tripping
Apple Contacts and Google Contacts both treat repeated `UID` as an update
trigger. That means you can:
1. Import from Apple → markdown.
2. Edit names / emails / tags in markdown.
3. `clawdex export vcard --all -o contacts.vcf`.
4. Drag the `.vcf` into Apple Contacts → it updates existing cards in
place.
This is the closest thing clawdex has to two-way sync today, and it works
because vCard files are dumb, well-understood text. Programmatic
[Sync](imports.md#sync-preview-only) is still preview-only.
## Related pages
- [People](people.md), [Avatars](avatars.md)
- [Imports](imports.md) — the inbound counterpart
- [Markdown Storage](markdown-storage.md) — the source of truth

View File

@ -3,20 +3,635 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Clawdex</title>
<meta name="color-scheme" content="light dark">
<title>Clawdex — local-first contact index</title>
<meta name="description" content="Personal contact index backed by markdown and private Git. Imports Apple, Google, X DMs, and Discord DMs.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect x='6' y='14' width='52' height='40' rx='4' fill='%23f5e9c8' stroke='%23231f1c' stroke-width='3'/%3E%3Crect x='6' y='14' width='52' height='8' fill='%23d94e3a'/%3E%3Cline x1='14' y1='32' x2='50' y2='32' stroke='%23231f1c' stroke-width='2'/%3E%3Cline x1='14' y1='40' x2='50' y2='40' stroke='%23231f1c' stroke-width='2'/%3E%3Cline x1='14' y1='48' x2='38' y2='48' stroke='%23231f1c' stroke-width='2'/%3E%3C/svg%3E">
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.10.0/styles/atom-one-light.min.css" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.10.0/styles/atom-one-dark.min.css" media="(prefers-color-scheme: dark)">
<style>
:root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #0f172a; color: #f8fafc; }
main { width: min(720px, calc(100% - 48px)); }
h1 { margin: 0 0 12px; font-size: clamp(42px, 8vw, 84px); line-height: .95; letter-spacing: 0; }
p { margin: 0; color: #cbd5e1; font-size: 18px; line-height: 1.6; }
a { color: #93c5fd; }
:root {
--paper: #f6ecd2;
--paper-edge: #e7d8a8;
--paper-grid: #e3d3a0;
--ink: #211c17;
--ink-soft: #4a3f33;
--ink-faint: #7a6a55;
--tab: #d94e3a;
--tab-deep: #b03a28;
--tab-alt: #2a6c8a;
--tab-alt-deep: #1c4d63;
--rule: #3a2f24;
--code-bg: #f1e3bd;
--code-border: #d8c389;
--shadow: 0 24px 60px -28px rgba(40, 28, 12, .55), 0 4px 14px -8px rgba(40, 28, 12, .3);
--hand: "Caveat", "Bradley Hand", "Segoe Script", cursive;
--serif: "Iowan Old Style", "Charter", "Georgia", serif;
--mono: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--paper: #1e1a17;
--paper-edge: #2a2521;
--paper-grid: #2c2620;
--ink: #f1e7d0;
--ink-soft: #d6c8aa;
--ink-faint: #9a8b73;
--tab: #e07560;
--tab-deep: #a8462f;
--tab-alt: #6fb3d1;
--tab-alt-deep: #3d7d99;
--rule: #c9b994;
--code-bg: #16110d;
--code-border: #3a2f24;
--shadow: 0 30px 80px -28px rgba(0, 0, 0, .8), 0 6px 18px -8px rgba(0, 0, 0, .55);
}
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--serif);
color: var(--ink);
background: var(--paper);
background-image:
radial-gradient(ellipse at top, transparent 0, rgba(0,0,0,.05) 100%),
repeating-linear-gradient(0deg, transparent 0 31px, color-mix(in srgb, var(--paper-grid) 65%, transparent) 31px 32px),
repeating-linear-gradient(90deg, transparent 0 1200px, color-mix(in srgb, var(--tab) 35%, transparent) 1200px 1201px);
min-height: 100vh;
line-height: 1.55;
font-size: 17px;
-webkit-font-smoothing: antialiased;
}
.desk {
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
gap: 28px;
max-width: 1180px;
margin: 0 auto;
padding: 28px 28px 80px;
}
/* Header */
.masthead {
grid-column: 1 / -1;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
padding: 6px 4px 14px;
border-bottom: 2px solid var(--rule);
position: relative;
}
.masthead::after {
content: "";
position: absolute; left: 0; right: 0; bottom: -6px;
height: 2px;
background: var(--rule);
opacity: .35;
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand .logo {
width: 52px; height: 52px;
border-radius: 6px;
background: var(--paper);
border: 2px solid var(--ink);
box-shadow: 4px 4px 0 var(--ink);
position: relative;
flex: none;
}
.brand .logo::before {
content: "";
position: absolute; inset: 0 0 auto 0; height: 12px;
background: var(--tab);
border-bottom: 2px solid var(--ink);
border-radius: 4px 4px 0 0;
}
.brand .logo::after {
content: "";
position: absolute; left: 8px; right: 8px; top: 22px; bottom: 8px;
background:
linear-gradient(var(--ink), var(--ink)) 0 0/100% 2px no-repeat,
linear-gradient(var(--ink), var(--ink)) 0 9px/100% 2px no-repeat,
linear-gradient(var(--ink), var(--ink)) 0 18px/60% 2px no-repeat;
}
.brand h1 {
margin: 0;
font-family: var(--mono);
font-size: 30px;
letter-spacing: -.02em;
line-height: 1;
}
.brand .tag {
margin: 4px 0 0;
font-family: var(--hand);
font-size: 19px;
color: var(--ink-faint);
transform: rotate(-1.5deg);
display: inline-block;
}
.masthead nav {
display: flex; gap: 18px;
font-family: var(--mono);
font-size: 13px;
letter-spacing: .04em;
text-transform: uppercase;
}
.masthead nav a {
color: var(--ink-soft);
text-decoration: none;
border-bottom: 2px solid transparent;
padding: 0 0 2px;
}
.masthead nav a:hover { border-bottom-color: var(--tab); color: var(--ink); }
/* Sidebar */
aside.tabs {
position: sticky;
top: 24px;
align-self: start;
max-height: calc(100vh - 48px);
overflow: auto;
padding: 8px 0;
}
aside.tabs .stack {
display: flex;
flex-direction: column;
border-left: 2px solid var(--ink);
position: relative;
}
aside.tabs h2 {
font-family: var(--mono);
font-size: 11px;
letter-spacing: .15em;
text-transform: uppercase;
color: var(--ink-faint);
margin: 14px 0 8px;
padding-left: 10px;
}
aside.tabs h2:first-child { margin-top: 0; }
aside.tabs a.tab {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--ink);
font-family: var(--serif);
font-size: 16px;
padding: 7px 10px 7px 14px;
margin-left: -2px;
border-left: 4px solid transparent;
position: relative;
transition: background 120ms ease, border-color 120ms ease, transform 120ms ease;
}
aside.tabs a.tab .num {
font-family: var(--mono);
font-size: 11px;
color: var(--ink-faint);
width: 22px;
text-align: right;
}
aside.tabs a.tab:hover {
background: color-mix(in srgb, var(--paper-edge) 70%, transparent);
border-left-color: var(--tab-alt);
}
aside.tabs a.tab.active {
background: color-mix(in srgb, var(--tab) 14%, transparent);
border-left-color: var(--tab);
transform: translateX(2px);
}
aside.tabs a.tab.active .num { color: var(--tab-deep); }
/* Card */
main { min-width: 0; }
.card {
background: var(--paper);
border: 2px solid var(--ink);
border-radius: 6px;
box-shadow: var(--shadow);
position: relative;
padding: 56px 44px 44px;
min-height: 60vh;
}
.card::before {
content: attr(data-tab);
position: absolute;
top: -16px;
left: 36px;
background: var(--tab);
color: var(--paper);
font-family: var(--mono);
font-size: 11px;
letter-spacing: .18em;
text-transform: uppercase;
padding: 6px 14px;
border: 2px solid var(--ink);
border-radius: 4px 4px 0 0;
box-shadow: 3px 3px 0 var(--ink);
}
.card::after {
content: "";
position: absolute;
left: 22px; top: 22px;
width: 14px; height: 14px;
border-radius: 50%;
background: var(--paper-edge);
box-shadow: inset 0 1px 2px rgba(0,0,0,.4);
}
.meta {
font-family: var(--mono);
font-size: 12px;
letter-spacing: .1em;
text-transform: uppercase;
color: var(--ink-faint);
display: flex; gap: 14px;
margin: -10px 0 22px;
padding-bottom: 10px;
border-bottom: 1px dashed var(--rule);
}
.meta .source a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
article h1 {
font-family: var(--mono);
font-size: clamp(30px, 4.6vw, 44px);
line-height: 1.05;
letter-spacing: -.02em;
margin: 0 0 18px;
}
article h2 {
font-family: var(--mono);
font-size: 22px;
letter-spacing: -.01em;
margin: 36px 0 14px;
padding-bottom: 6px;
border-bottom: 1.5px solid var(--rule);
}
article h3 {
font-family: var(--mono);
font-size: 17px;
margin: 26px 0 8px;
}
article p, article li { color: var(--ink); }
article a {
color: var(--ink);
text-decoration: underline;
text-decoration-color: var(--tab);
text-decoration-thickness: 2px;
text-underline-offset: 3px;
}
article a:hover { color: var(--tab-deep); }
article ul, article ol { padding-left: 22px; }
article li { margin: 6px 0; }
article hr { border: 0; border-top: 2px dashed var(--rule); margin: 32px 0; }
article blockquote {
border-left: 4px solid var(--tab-alt);
margin: 18px 0;
padding: 6px 14px;
background: color-mix(in srgb, var(--tab-alt) 8%, transparent);
color: var(--ink-soft);
font-style: italic;
}
article table {
width: 100%;
border-collapse: collapse;
margin: 18px 0;
font-size: 15px;
}
article th, article td {
border-bottom: 1px solid var(--rule);
padding: 8px 10px;
text-align: left;
vertical-align: top;
}
article th {
font-family: var(--mono);
font-size: 12px;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--ink-faint);
border-bottom-width: 2px;
}
article code {
font-family: var(--mono);
font-size: .9em;
background: var(--code-bg);
border: 1px solid var(--code-border);
border-radius: 3px;
padding: 1px 5px;
}
article pre {
background: var(--code-bg);
border: 1.5px solid var(--code-border);
border-radius: 6px;
padding: 14px 16px;
overflow-x: auto;
box-shadow: inset 0 1px 0 rgba(255,255,255,.4);
position: relative;
}
article pre code {
background: transparent; border: 0; padding: 0;
font-size: 13.5px;
line-height: 1.55;
}
/* Let hljs paint syntax colors but keep our paper background */
article pre code.hljs { background: transparent; padding: 0; }
/* Tone hljs colors a touch toward the paper palette in light mode */
@media (prefers-color-scheme: light) {
.hljs-comment, .hljs-quote { color: #8a7a5e; font-style: italic; }
.hljs-keyword, .hljs-selector-tag, .hljs-section { color: #b03a28; }
.hljs-string, .hljs-attr { color: #6b7d3a; }
.hljs-number, .hljs-literal, .hljs-built_in { color: #1c4d63; }
.hljs-title, .hljs-name, .hljs-meta { color: #3a2f24; font-weight: 600; }
.hljs-variable, .hljs-template-variable { color: #8c5a1a; }
.hljs-symbol, .hljs-bullet, .hljs-link { color: #2a6c8a; }
}
.placeholder {
font-family: var(--mono);
color: var(--ink-faint);
padding: 40px 0;
text-align: center;
}
.placeholder.error { color: var(--tab-deep); }
footer.colophon {
grid-column: 1 / -1;
margin-top: 42px;
padding: 18px 4px 0;
border-top: 2px solid var(--rule);
display: flex;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
font-family: var(--mono);
font-size: 12px;
color: var(--ink-faint);
letter-spacing: .04em;
}
footer.colophon a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
footer.colophon .stamp {
font-family: var(--hand);
font-size: 17px;
color: var(--tab-deep);
transform: rotate(-3deg);
border: 2px dashed var(--tab-deep);
padding: 4px 12px;
border-radius: 4px;
letter-spacing: 0;
}
.menu-button {
display: none;
font-family: var(--mono);
font-size: 12px;
letter-spacing: .1em;
text-transform: uppercase;
background: var(--paper);
border: 2px solid var(--ink);
box-shadow: 3px 3px 0 var(--ink);
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
color: var(--ink);
margin-bottom: 8px;
}
@media (max-width: 880px) {
.desk { grid-template-columns: 1fr; padding: 16px 14px 60px; gap: 18px; }
.masthead { flex-wrap: wrap; }
.masthead nav { font-size: 12px; gap: 12px; }
aside.tabs {
position: static;
max-height: none;
overflow: visible;
order: 3;
}
aside.tabs.collapsed .stack { display: none; }
.menu-button { display: inline-block; align-self: flex-start; }
.card { padding: 44px 20px 30px; }
.card::before { left: 18px; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; scroll-behavior: auto !important; }
}
</style>
</head>
<body>
<main>
<h1>Clawdex</h1>
<p>OpenClaw documentation and project index. Source lives at <a href="https://github.com/openclaw/clawdex">openclaw/clawdex</a>.</p>
</main>
<div class="desk">
<header class="masthead">
<div class="brand">
<div class="logo" aria-hidden="true"></div>
<div>
<h1>clawdex</h1>
<span class="tag">your Rolodex, in markdown.</span>
</div>
</div>
<nav>
<a href="#/quickstart">Quickstart</a>
<a href="#/install">Install</a>
<a href="https://github.com/openclaw/clawdex">GitHub</a>
</nav>
</header>
<aside class="tabs" aria-label="Documentation index">
<button class="menu-button" type="button" aria-expanded="true" aria-controls="tab-stack">Index</button>
<div class="stack" id="tab-stack" role="navigation">
<h2>Start</h2>
<a class="tab" data-slug="index" href="#/"><span class="num">00</span><span>Overview</span></a>
<a class="tab" data-slug="install" href="#/install"><span class="num">01</span><span>Install</span></a>
<a class="tab" data-slug="quickstart" href="#/quickstart"><span class="num">02</span><span>Quickstart</span></a>
<h2>Daily Use</h2>
<a class="tab" data-slug="people" href="#/people"><span class="num">03</span><span>People</span></a>
<a class="tab" data-slug="notes" href="#/notes"><span class="num">04</span><span>Notes</span></a>
<a class="tab" data-slug="timeline" href="#/timeline"><span class="num">05</span><span>Timeline</span></a>
<a class="tab" data-slug="search" href="#/search"><span class="num">06</span><span>Search</span></a>
<a class="tab" data-slug="avatars" href="#/avatars"><span class="num">07</span><span>Avatars</span></a>
<h2>In and Out</h2>
<a class="tab" data-slug="imports" href="#/imports"><span class="num">08</span><span>Imports</span></a>
<a class="tab" data-slug="vcard-export" href="#/vcard-export"><span class="num">09</span><span>vCard Export</span></a>
<a class="tab" data-slug="git-sync" href="#/git-sync"><span class="num">10</span><span>Git Sync</span></a>
<h2>Storage &amp; Care</h2>
<a class="tab" data-slug="markdown-storage" href="#/markdown-storage"><span class="num">11</span><span>Markdown Storage</span></a>
<a class="tab" data-slug="doctor" href="#/doctor"><span class="num">12</span><span>Doctor</span></a>
<a class="tab" data-slug="config" href="#/config"><span class="num">13</span><span>Config</span></a>
<h2>Project</h2>
<a class="tab" data-slug="RELEASING" href="#/RELEASING"><span class="num">14</span><span>Releasing</span></a>
</div>
</aside>
<main>
<section class="card" id="card" data-tab="overview" aria-live="polite">
<div class="meta">
<span id="meta-slug">docs/index.md</span>
<span class="source">source: <a id="meta-link" href="https://github.com/openclaw/clawdex/blob/main/docs/index.md" target="_blank" rel="noopener">github</a></span>
</div>
<article id="article">
<p class="placeholder">opening the drawer…</p>
</article>
</section>
</main>
<footer class="colophon">
<span>&copy; 2026 Peter Steinberger. Released under MIT.</span>
<span class="stamp">filed under: contacts</span>
<span><a href="https://github.com/openclaw/clawdex">openclaw/clawdex</a> · <a href="https://github.com/openclaw/clawdex/blob/main/CHANGELOG.md">changelog</a></span>
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked@14.1.3/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/go.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/dockerfile.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.min.js"></script>
<script>
(function () {
const KNOWN = new Set([
"index", "install", "quickstart",
"people", "notes", "timeline", "search", "avatars",
"imports", "vcard-export", "git-sync",
"markdown-storage", "doctor", "config",
"RELEASING"
]);
const TAB_LABEL = {
"index": "overview",
"install": "install",
"quickstart": "quickstart",
"people": "people",
"notes": "notes",
"timeline": "timeline",
"search": "search",
"avatars": "avatars",
"imports": "imports",
"vcard-export": "vcard export",
"git-sync": "git sync",
"markdown-storage": "storage",
"doctor": "doctor",
"config": "config",
"RELEASING": "releasing"
};
const article = document.getElementById("article");
const card = document.getElementById("card");
const metaSlug = document.getElementById("meta-slug");
const metaLink = document.getElementById("meta-link");
const cache = new Map();
// Register short aliases highlight.js doesn't ship by default.
const ALIASES = { toml: "ini", sh: "bash", shell: "bash", zsh: "bash", txt: "plaintext", text: "plaintext" };
function resolveLang(lang) {
if (!lang) return "";
if (hljs.getLanguage(lang)) return lang;
const a = ALIASES[lang.toLowerCase()];
if (a && hljs.getLanguage(a)) return a;
return "";
}
const renderer = {
link(token) {
const href = token.href || "";
const text = token.text || "";
const title = token.title || "";
let h = String(href);
const m = h.match(/^([A-Za-z0-9_-]+)\.md(#.+)?$/);
if (m) h = "#/" + m[1] + (m[2] || "");
const t = title ? ` title="${title}"` : "";
const ext = /^https?:/.test(h) ? ` target="_blank" rel="noopener"` : "";
return `<a href="${h}"${t}${ext}>${text}</a>`;
},
code(token) {
const c = token.text || "";
const requested = (token.lang || "").trim();
const lang = resolveLang(requested);
let html;
try {
if (lang) {
html = hljs.highlight(c, { language: lang, ignoreIllegals: true }).value;
} else {
const auto = hljs.highlightAuto(c, ["bash", "go", "json", "yaml", "ini", "markdown", "javascript", "xml"]);
html = auto.value;
}
} catch (_) {
html = c.replace(/[&<>]/g, s => ({"&":"&amp;","<":"&lt;",">":"&gt;"}[s]));
}
const cls = lang ? `language-${lang}` : (requested ? `language-${requested}` : "");
return `<pre><code class="hljs ${cls}">${html}</code></pre>`;
}
};
marked.use({ gfm: true, breaks: false, renderer });
function setActive(slug) {
document.querySelectorAll("aside.tabs a.tab").forEach(a => {
a.classList.toggle("active", a.dataset.slug === slug);
});
card.dataset.tab = TAB_LABEL[slug] || slug;
const file = (slug === "RELEASING") ? "RELEASING.md" : (slug + ".md");
metaSlug.textContent = "docs/" + file;
metaLink.href = `https://github.com/openclaw/clawdex/blob/main/docs/${file}`;
}
async function load(slug) {
setActive(slug);
article.innerHTML = '<p class="placeholder">flipping to ' + slug + '…</p>';
let md = cache.get(slug);
if (!md) {
try {
const file = (slug === "RELEASING") ? "RELEASING.md" : (slug + ".md");
const res = await fetch("docs/" + file, { headers: { "Accept": "text/plain" } });
if (!res.ok) throw new Error(res.status + " " + res.statusText);
md = await res.text();
cache.set(slug, md);
} catch (err) {
article.innerHTML = '<p class="placeholder error">card not found: ' + slug + ' — ' + err.message + '</p>';
return;
}
}
const dirty = marked.parse(md);
const clean = window.DOMPurify ? DOMPurify.sanitize(dirty, { ADD_ATTR: ['target'] }) : dirty;
article.innerHTML = clean;
const sub = location.hash.split("#")[2];
if (sub) {
const el = document.getElementById(sub);
if (el) { el.scrollIntoView({ behavior: "smooth", block: "start" }); return; }
}
window.scrollTo({ top: 0, behavior: "smooth" });
}
function route() {
const h = location.hash.replace(/^#\/?/, "").split("#")[0] || "index";
const slug = KNOWN.has(h) ? h : "index";
load(slug);
}
const tabsAside = document.querySelector("aside.tabs");
const menuBtn = tabsAside.querySelector(".menu-button");
menuBtn.addEventListener("click", () => {
const open = tabsAside.classList.toggle("collapsed");
menuBtn.setAttribute("aria-expanded", String(!open));
});
if (window.matchMedia("(max-width: 880px)").matches) {
tabsAside.classList.add("collapsed");
menuBtn.setAttribute("aria-expanded", "false");
}
window.addEventListener("hashchange", route);
route();
})();
</script>
</body>
</html>