144 lines
7.1 KiB
Markdown
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. |