2026-06-20 19:15:38 +00:00
2026-06-20 19:15:38 +00:00
2026-06-20 19:15:38 +00:00
2026-06-20 19:15:38 +00:00
2026-06-20 19:15:38 +00:00
2026-06-20 19:15:38 +00:00
2026-06-20 19:15:38 +00:00
2026-06-20 19:15:38 +00:00
2026-06-20 19:15:38 +00:00
2026-06-20 19:15:38 +00:00

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.

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

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 local-only review dashboard (127.0.0.1, no external network) over the batch engine. 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.
# 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

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.
Description
No description provided
Readme 591 KiB
Languages
TypeScript 89.8%
HTML 9.2%
JavaScript 0.8%
Shell 0.2%