Skip to content

Commit b028d94

Browse files
InfantLabclaude
andcommitted
docs(obsidian-sync): home-server agent setup guide
Operational guide for running scripts/sync-obsidian.mjs from a home server: permissions (entries:read + entries:write), vault layout, .tada-sync.json schema, CLI reference, markdown frontmatter format, conflict resolution, and error cases. Modelled on docs/modules/ourmoji-agent-setup.md for consistency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3ebeee0 commit b028d94

1 file changed

Lines changed: 284 additions & 0 deletions

File tree

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
# Obsidian ↔ Dreams Sync — Home-Server Agent Setup
2+
3+
How to configure an external agent (running on your home server) to keep a
4+
dreams folder in an Obsidian vault in sync with the Ta-Da! instance.
5+
6+
The sync is driven by a standalone Node script
7+
([app/scripts/sync-obsidian.mjs](../../app/scripts/sync-obsidian.mjs)) that
8+
talks to the Ta-Da! API over HTTPS with a Bearer token. No Obsidian plugin
9+
is required — the script reads and writes `.md` files directly.
10+
11+
Production base URL: `https://tada.living`
12+
13+
Replace `https://tada.living` below if running against a different
14+
deployment (e.g. `http://localhost:3000` for local dev).
15+
16+
---
17+
18+
## Prerequisites
19+
20+
1. **Node.js 20+** on the home server (the script uses `node:util`
21+
`parseArgs` and top-level `await`).
22+
2. **A checkout of the repo**, or just the file
23+
`app/scripts/sync-obsidian.mjs` dropped onto the home server — it has
24+
no third-party dependencies.
25+
3. **A writable Obsidian vault path** the agent can read and write.
26+
27+
---
28+
29+
## Step 1 — Mint an API key
30+
31+
The agent authenticates with a Bearer API key that has two permissions:
32+
33+
- `entries:read` — list and read entries
34+
- `entries:write` — create and update entries
35+
36+
Open devtools on any `https://tada.living/*` page while signed in, and
37+
run:
38+
39+
```js
40+
await fetch("https://tada.living/api/v1/auth/keys", {
41+
method: "POST",
42+
credentials: "include",
43+
headers: { "Content-Type": "application/json" },
44+
body: JSON.stringify({
45+
name: "home-server obsidian sync",
46+
permissions: ["entries:read", "entries:write"],
47+
}),
48+
}).then((r) => r.json());
49+
```
50+
51+
The response includes a `key` of the form `tada_key_…`. **This is the
52+
only time the plaintext key is shown.** Copy it to the home-server
53+
agent's secret store immediately.
54+
55+
> Do not grant `admin:*` scopes for this agent. Scope the key to the
56+
> minimum it needs.
57+
58+
---
59+
60+
## Step 2 — Lay out the vault
61+
62+
The script expects a subfolder inside the vault for its managed files
63+
(default: `tada/`). For dreams specifically, a nested folder like
64+
`tada/dreams/` keeps things tidy:
65+
66+
```
67+
MyVault/
68+
├── .tada-sync.json ← optional config file
69+
├── .tada-sync-state.json ← auto-created by the script (do not edit)
70+
└── tada/
71+
└── dreams/
72+
├── 2026-04-21-waning-gibbous.md
73+
└── …
74+
```
75+
76+
The script will create `tada/dreams/` on first run if it doesn't exist.
77+
It also creates `tada/dreams/.trash/` for files whose underlying entries
78+
have been deleted in Ta-Da!.
79+
80+
---
81+
82+
## Step 3 — Configure the agent
83+
84+
Two ways to supply config, which can be mixed. CLI flags always win over
85+
config-file values, and env vars are the fallback.
86+
87+
### Option A — `.tada-sync.json` in the vault root (auto-loaded)
88+
89+
```json
90+
{
91+
"apiUrl": "https://tada.living",
92+
"apiKey": "tada_key_…",
93+
"syncFolder": "tada/dreams",
94+
"categories": ["journal"],
95+
"subcategories": ["dream"],
96+
"fileNamePattern": "{{date}}-{{name}}.md",
97+
"pushDeletes": false
98+
}
99+
```
100+
101+
### Option B — environment variables + CLI flags
102+
103+
```bash
104+
export TADA_API_KEY=tada_key_…
105+
export TADA_API_URL=https://tada.living
106+
107+
node scripts/sync-obsidian.mjs \
108+
--vault /home/me/MyVault \
109+
--sync-folder tada/dreams \
110+
--subcategories dream
111+
```
112+
113+
---
114+
115+
## Step 4 — Run the sync
116+
117+
### Normal bidirectional sync
118+
119+
```bash
120+
node scripts/sync-obsidian.mjs --vault /home/me/MyVault
121+
```
122+
123+
### Dry run (recommended for first invocation)
124+
125+
Prints what _would_ happen without writing anything to the vault or the
126+
API:
127+
128+
```bash
129+
node scripts/sync-obsidian.mjs --vault /home/me/MyVault --dry-run --verbose
130+
```
131+
132+
### One-way only
133+
134+
- `--direction pull` — Ta-Da! → vault only (safe on a new vault).
135+
- `--direction push` — vault → Ta-Da! only.
136+
- `--direction both` (default) — pull first, then push.
137+
138+
### Scheduling
139+
140+
Drop it in cron on the home server, e.g. every 15 minutes:
141+
142+
```cron
143+
*/15 * * * * /usr/bin/node /opt/tada/scripts/sync-obsidian.mjs \
144+
--config /home/me/MyVault/.tada-sync.json >> /var/log/tada-sync.log 2>&1
145+
```
146+
147+
---
148+
149+
## CLI reference
150+
151+
| Flag | Short | Default | Notes |
152+
|------|-------|---------|-------|
153+
| `--vault <path>` | `-v` || **Required.** Absolute path to the Obsidian vault |
154+
| `--api-key <key>` | `-k` | `$TADA_API_KEY` | **Required** via flag, config, or env |
155+
| `--api-url <url>` | `-u` | `http://localhost:3000` | Override with `$TADA_API_URL` or config |
156+
| `--config <path>` | `-c` || Path to a config JSON file |
157+
| `--direction <dir>` | `-d` | `both` | `pull`, `push`, or `both` |
158+
| `--dry-run` || `false` | Preview without writing |
159+
| `--categories <list>` ||| Comma-separated category filter (pull + push) |
160+
| `--subcategories <list>` ||| Comma-separated subcategory filter (pull + push) |
161+
| `--sync-folder <path>` || `tada` | Subfolder inside the vault the script owns |
162+
| `--file-pattern <pat>` || `{{date}}-{{name}}.md` | Filename template — see below |
163+
| `--verbose` || `false` | Per-file log lines |
164+
| `--help` | `-h` || Show inline help |
165+
166+
**Filename template placeholders**: `{{date}}` (YYYY-MM-DD from
167+
timestamp), `{{name}}` (slugified entry name), `{{category}}`,
168+
`{{subcategory}}`, `{{id}}` (first 8 chars of entry id).
169+
170+
---
171+
172+
## Markdown format
173+
174+
Each synced entry is a `.md` file with YAML frontmatter followed by an
175+
H1 title and body. For a dream:
176+
177+
```markdown
178+
---
179+
tada_id: abc123…
180+
type: moment
181+
category: journal
182+
subcategory: dream
183+
timestamp: 2026-04-21T08:00:00+01:00
184+
tags: [lucid, flying]
185+
source: obsidian
186+
---
187+
188+
# Flying over the house
189+
190+
I was skimming the rooftops, palm-up against the sky…
191+
```
192+
193+
- `tada_id` is the authoritative link back to the Ta-Da! entry. Leave it
194+
alone. If you author a new `.md` file in the sync folder and omit
195+
`tada_id`, the script will create a matching entry on the next push
196+
and stamp the id back into the frontmatter.
197+
- The H1 title becomes the entry `name`. Body below the H1 becomes
198+
`notes`.
199+
- Only frontmatter fields listed above round-trip. Extra keys are
200+
ignored on push and will be dropped on the next pull.
201+
202+
---
203+
204+
## What counts as a "dream"?
205+
206+
Dreams are ordinary Ta-Da! entries tagged with the category /
207+
subcategory the agent is configured to sync. The current convention:
208+
209+
- `category: journal`
210+
- `subcategory: dream`
211+
212+
Any entry matching that pair is pulled into the vault; any `.md` file
213+
whose frontmatter matches is pushed back up. If you change the
214+
convention on one side, update `categories` / `subcategories` in the
215+
agent's config on the same day or the sync will start skipping files.
216+
217+
---
218+
219+
## Conflict resolution
220+
221+
- **Remote newer than local (pull phase):** remote overwrites local.
222+
- **Local file modified since last sync, local `mtime` > remote
223+
`updatedAt`:** local wins — file is skipped this pull, will be pushed
224+
next run.
225+
- **Same content hash on both sides:** skipped (no-op).
226+
227+
State is kept in `.tada-sync-state.json` inside the vault root. It maps
228+
each `tada_id` to its filename plus content hashes. Do **not** edit or
229+
delete this file unless you want to force a full resync.
230+
231+
---
232+
233+
## Error cases
234+
235+
| Message | Meaning | Fix |
236+
|---------|---------|-----|
237+
| `Cannot connect to Ta-Da! API at …` | API URL wrong or the instance is down | Check `--api-url` / `apiUrl` |
238+
| `API 401: …` | Key missing, revoked, or typo'd | Rotate key (Step 1) |
239+
| `API 403: …` | Key lacks `entries:read` or `entries:write` | Re-mint with correct permissions |
240+
| `Vault path does not exist: …` | `--vault` points at a missing directory | Fix the path |
241+
| `Failed to read config file: …` | Bad JSON or wrong path | Validate with `jq . < .tada-sync.json` |
242+
243+
Non-zero exit code means at least one entry or file errored — check the
244+
log for `Error syncing entry …` or `Error pushing …` lines before the
245+
summary.
246+
247+
---
248+
249+
## Rotating / revoking the key
250+
251+
List your keys (browser console, logged in):
252+
253+
```js
254+
await fetch("https://tada.living/api/v1/auth/keys", {
255+
credentials: "include",
256+
}).then((r) => r.json());
257+
```
258+
259+
Revoke by id:
260+
261+
```js
262+
await fetch("https://tada.living/api/v1/auth/keys/<KEY_ID>", {
263+
method: "DELETE",
264+
credentials: "include",
265+
});
266+
```
267+
268+
Then redo Step 1 to mint a replacement and swap it into the agent's
269+
secret store (or `$TADA_API_KEY`).
270+
271+
---
272+
273+
## Reference: live API docs
274+
275+
- **Scalar UI:** https://tada.living/api-docs
276+
- **OpenAPI JSON:** https://tada.living/api/openapi.json
277+
278+
Relevant endpoints this script uses: `GET /api/v1/sync/status`,
279+
`GET /api/v1/entries`, `POST /api/v1/entries`,
280+
`PATCH /api/v1/entries/{id}`.
281+
282+
---
283+
284+
[Back to Modules](./README.md)

0 commit comments

Comments
 (0)