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:
parent
c5fef34885
commit
be315dd311
96
docs/avatars.md
Normal file
96
docs/avatars.md
Normal 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
104
docs/config.md
Normal 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
110
docs/doctor.md
Normal 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
120
docs/git-sync.md
Normal 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
128
docs/imports.md
Normal 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
59
docs/index.md
Normal 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
80
docs/install.md
Normal 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
140
docs/markdown-storage.md
Normal 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
84
docs/notes.md
Normal 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
101
docs/people.md
Normal 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
139
docs/quickstart.md
Normal 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
78
docs/search.md
Normal 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
59
docs/timeline.md
Normal 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
75
docs/vcard-export.md
Normal 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
|
||||
637
index.html
637
index.html
@ -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 & 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>© 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 => ({"&":"&","<":"<",">":">"}[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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user