Files
obsidian-foundry-sync/README.md

144 lines
7.1 KiB
Markdown

# foundry-obsidian-sync
A strict, **offline**, bidirectional converter between Foundry VTT Campaign Codex
journal data (a LevelDB snapshot) and an Obsidian vault. No Foundry instance, no
macros, no network — just the exported DB.
Two markdown formats are bridged:
- **Obsidian curated** (`Roland Raventhorne.md`) — frontmatter `type/tags/aliases/
faction/region/race/portrait`, italic tagline, `## Appearance / Personality /
Background / Goals / Secrets`, `[[Name|Display]]` links.
- **Campaign Codex native export** (`Roland Raventhornecc.md`) — frontmatter
`cc_id/cc_uuid/cc_type/cc_folder_path/cc_exported_at`, `# Title`, `## Information`,
body sections, `#### Bio`/`#### Social` boxes, `## Linked Sheets`/`### Associates`,
`## Notes`, `[[Name]]` links.
Foundry is the source of truth. Each Obsidian note carries a `foundry:` identity
block (`cc_uuid`, `cc_type`, `folder_path`, `contentHash`, `syncedAt`) so the two
sides stay linked across syncs and renames.
## Quick start (Docker Compose)
Clone, then bring up the relay wired to your own **external** Foundry. You need: a
running Foundry VTT (license + the
[rest-api](https://github.com/ThreeHats/foundryvtt-rest-api-relay) module) and an
Obsidian vault on disk that you edit in your own Obsidian desktop app.
1. `cp .env.example .env` and fill in `FOUNDRY_URL`, `FOUNDRY_WORLD`, `RELAY_USER`/
`RELAY_PASSWORD`, and `VAULT` (or let the **`/setup`** skill do all of this for
you — it runs the commands itself; you only paste values it can't know).
2. `docker compose up -d relay` — relay on `:3010`.
3. Open `http://localhost:3010`, sign up, and copy the API key into `.env` as
`RELAY_API_KEY`. In Foundry, point the rest-api module at
`ws(s)://<host-Foundry-can-reach>:3010`.
4. `node scripts/start-relay-session.js` — launches a headless Foundry session the
relay drives.
5. `./sync.sh refresh --out ./out` then `./sync.sh ui --vault "$VAULT/Refined" \
--out ./out --host 0.0.0.0` — opens the dashboard on `:7788`.
The sync tool stays host-run (it reads the journal LevelDB and shells out to
`docker`); the compose provides only the relay. You edit notes in your own Obsidian
desktop app pointed at `$VAULT`; the dashboard reads/writes the same files on disk.
Push edits back to Foundry from the **dashboard** (`/api/push`, `/api/push-all`), or
flip on **Auto-sync mode** to push Obsidian→Foundry automatically the moment you save
a note (Foundry→Obsidian stays manual — use Sync / Re-pull for that direction).
## How it works
1. `db.ts` opens the journal LevelDB **read-only** with `classic-level`, indexes every
Campaign Codex JournalEntry, and resolves `@UUID` ⇄ note name.
2. `htmlMd.ts` ports the `Macros/cc-4-export.js` HTML→Markdown logic (tagline,
sections, Bio/Social boxes) but resolves `@UUID` to `[[Name|Display]]` via the index.
3. `toObsidian.ts` / `toFoundry.ts` convert each direction. `toFoundry` pulls
`cc_id`/associates/image from the matched Foundry entry (by `foundry:` block, then
name). `normalize.ts` canonicalizes wikilinks/whitespace and hashes content so
cosmetic edits don't churn.
## Modes (safety)
The tool **defaults to dev mode** and never risks the real world data.
- **`--dev` (default):** reads your *copy* of the world data, writes **only** into
`--out`. Sources are never mutated. Refuses `--out` equal to `--journal` or the
source vault dir.
- **`--dry-run`:** computes outputs and prints diffs only — writes nothing.
- **`--apply`:** writes in place to the real vault/cc dir, writing a timestamped
`.bak-<iso>` of every file it overwrites first. Gated explicitly.
The journal LevelDB is opened read-only in all modes; the code only ever calls
read methods (`get`/`iterator`), never `put`/`del`/`batch`.
## Usage
```bash
npm install
# Prep a dev copy of your world data (you do this, the tool won't):
cp -r ~/hosting/"Lore examples"/journal /tmp/journal-copy
cp -r ~/hosting/"Lore examples" /tmp/lore-copy
# Foundry -> Obsidian (one entry by --id, or all if omitted)
npx tsx src/cli.ts to-obsidian --dev --journal /tmp/journal-copy --out /tmp/devout --id flGsAYaK24eUZhQE
# Obsidian -> cc.md (+ optional Foundry-importable JSON with --emit-json)
npx tsx src/cli.ts to-foundry --dev --journal /tmp/journal-copy \
--vault "/tmp/lore-copy/Roland Raventhorne.md" --out /tmp/devoutcc --emit-json
# See what would change, write nothing
npx tsx src/cli.ts to-foundry --dry-run --journal ... --vault ... --out ...
```
## Batch dashboard — connect the whole vault at once
For more than one entry, the `ui` command starts a review dashboard over the batch
engine (binds 0.0.0.0 by default so it's reachable on your tailnet at this VM's IP,
port 7788; pass `--host 127.0.0.1` to restrict to localhost). It indexes the journal
LevelDB + your cc export + your refined vault, shows every file's match status and
diff, and runs the three "connect" operations:
- **Seed** — inject the `foundry:` identity block into each refined note (matched by
filename → `cc_id` → journal entry). Minimal: only the block is added, leaving all
curation (curated `type`, `aliases`, status tags, hand-edited body, links) untouched.
- **Sync → cc** — regenerate each `cc.md` from its curated refined note, so curation
(new `[[links]]`, edited prose) flows back into the Campaign Codex export.
- **Import** — pull the cc-only entries (no refined counterpart) into new refined
notes under `refined/imported/<cc_folder>/` for curation.
```bash
# Prep dev copies (you do this; the tool won't):
cp -r ~/hosting/"Lore examples"/journal /tmp/journal-copy
cp -r ~/hosting/"Lore examples" /tmp/lore-copy
npx tsx src/cli.ts ui --dev --journal /tmp/journal-copy \
--vault "/tmp/lore-copy/Obsidian vault/Land of Mardonar/Refined" \
--cc "/tmp/lore-copy/campaign codex" --out /tmp/devout
# → open http://127.0.0.1:7788
```
The dashboard opens in dev mode with **dry-run on by default** — actions preview
without writing. Uncheck dry-run to write into the `--out` sandbox. To write back to
the real refined/cc dirs (with timestamped `.bak-<iso>` backups), start with `--apply`
instead of `--dev`; the UI cannot escalate to apply unless the server was started
with it. The journal LevelDB is read-only in all modes.
API (for scripting): `GET /api/status`, `GET /api/index`, `GET /api/file?name=`,
`POST /api/action` with `{op:"seed"|"sync"|"import"|"seedAll"|"syncAll"|"importAll",
names?:[], dryRun?:bool}`.
## Test
```bash
npm test # 30 tests: links, Roland round-trip, dev/dry-run/apply safety, batch + server
```
## Notes
- `status/*` tags and `aliases` are curation-only on the Obsidian side; they are
preserved across sync, never sourced from or written to Foundry.
- `--emit-json` reconstructs a Foundry-importable JournalEntry JSON with clean
(not byte-identical) description/notes HTML — for a future push back into a live
Foundry world via the existing macros or a DB write, both out of scope here.
- The cc.md format drops wiki-link display aliases (`[[Name|Display]]` → `[[Name]]`),
matching Campaign Codex's native export. Display text round-trips losslessly only
through the Foundry JSON / `foundry:`-backed sync, not through cc.md.