@geml/logseq-sync 2.0.0 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,212 +1,310 @@
1
- # Sync Vault with GEML
2
-
3
- Your Logseq DB graph as a **continuously synced, Git-friendly plain-text
4
- vault** — pages and journals back in readable files and folders, the way OG
5
- vaults felt, kept in step with the database.
6
-
7
- ![How it works](docs/how-it-works.svg)
8
-
9
- ## What you get
10
-
11
- - 🌿 **Real git workflows** — clean commits, readable line-by-line diffs, full
12
- version history for a DB graph
13
- - 📦 **A plain-text escape hatch that stays yours** — every page a readable
14
- file, not a database dump
15
- - 🔁 **Continuous, not one-shot** — edit in Logseq, and seconds later the file
16
- and its git commit exist
17
-
18
- Logseq 2.0 ships both ends of a trade-off: `logseq export` gives Markdown
19
- (readable, lossy) and `logseq export-edn` gives EDN (lossless, not something a
20
- person edits). The vault's format, [GEML](https://github.com/geml-spec/geml),
21
- is the point between: **as readable as the Markdown export, as lossless as the
22
- EDN one** — and addressable, so external tools and agents can edit one block
23
- of a graph instead of round-tripping all of it.
24
-
25
- The tree is laid out the way an OG vault is the thing a file-version user
26
- recognizes as "my graph, as files again":
27
-
28
- ```
29
- {:pages-and-blocks [...]} ontology.geml :properties/:classes, verbatim EDN
30
- ⇄ graph.geml page ORDER (an addressable data block,
31
- so filenames need no numeric prefixes)
32
- journals/2025_02_20.geml journal pages, OG date names
33
- pages/<name>.geml one per page:
34
- block title → `=== text` body
35
- block uuid → `{#uuid}` geml get/set address
36
- outline tree flat blocks with `level=N`
37
- everything else rides along in `code {lang=edn}`
38
- ```
39
-
40
- (`@logseq/cli` 0.4.3's `export-edn` does not include journal pages, so live
41
- exports show `pages/` only today; the journal mapping is fixture-tested.)
42
-
43
- ## How it works — two halves, one honest boundary
44
-
45
- A Logseq 2.0 plugin runs in a sandboxed iframe: no arbitrary-path filesystem,
46
- no git, no shell (verified against the 2.0.1 app bundle). So the in-app plugin
47
- (`plugin/`) does the only two things only it can do:
48
-
49
- - **hear** the graph change (`logseq.DB.onChanged`, debounced) and write a
50
- dirty-marker file through the plugin storage API;
51
- - **show** the last sync result in the toolbar (`⇄`) and command palette.
52
-
53
- Everything with side effects lives in the **watcher** (`watcher/bin/geml-sync.mjs`),
54
- built on the official `@logseq/cli` export. It reacts to the marker file
55
- immediately (interval polling stays on as a fallback), writes only the files
56
- that actually changed — so `git diff` is never noise — commits with a pathspec
57
- scoped strictly to the vault, and reports back for the toolbar to display.
58
- The two halves meet in the plugin's own storage directory
59
- (`<dotdir>/storages/logseq-plugin-sync-vault-with-geml/`), the one disk location both can
60
- reach. A file as the bridge beats a local HTTP API: no port, no server, no
61
- CORS.
62
-
63
- Real output, real DB graph (exported with the official CLI, validated by
64
- `logseq validate`):
65
-
66
- ```text
67
- $ node watcher/bin/geml-sync.mjs geml-spike ~/vault-demo --git-commit --signal <storage>/geml-sync-dirty.json
68
- Starting GEML Sync: Graph "geml-spike" ~/vault-demo
69
- Git auto-commit: enabled (scoped to target paths)
70
- [19:29:14] Synced: 8 written, 0 unchanged.
71
- Git: [master (root-commit) 9cc348c] logseq-geml: sync graph "geml-spike"
72
- 9 files changed, 142 insertions(+)
73
- create mode 100644 graph.geml
74
- create mode 100644 pages/contents.geml
75
- ...
76
-
77
- $ node watcher/bin/geml-sync.mjs geml-spike ~/vault-demo --git-commit --signal ... # run again
78
- [19:29:55] Graph is up-to-date (0 written, 8 unchanged).
79
- ```
80
-
81
- ## Setup
82
-
83
- **1. Install the plugin** from the marketplace — or build and load it
84
- unpacked (`dist/` is not checked in):
85
-
86
- ```sh
87
- cd plugin && npm install && npm run build
88
- ```
89
-
90
- then Settings Advanced Developer mode "Load unpacked plugin" → `plugin/`.
91
-
92
- **2. Get the watcher** — one npm install, gives you the `geml-sync` command:
93
-
94
- ```sh
95
- npm install -g @geml/logseq-sync
96
- ```
97
-
98
- (or run it ad hoc with `npx @geml/logseq-sync …`; the source lives in this
99
- repository under `watcher/` and `core/`)
100
-
101
- **3. Install `@logseq/cli`** (one time, anywhere). On Node 24 its
102
- `better-sqlite3` has no prebuilt binding until 12.11.1, so pin an override
103
- (without it, install tries to compile and node-gyp does not recognize
104
- VS 2026 yet):
105
-
106
- ```sh
107
- mkdir logseq-cli && cd logseq-cli && npm init -y
108
- npm pkg set overrides.better-sqlite3=12.11.1
109
- npm i @logseq/cli
110
- ```
111
-
112
- **4. Run the watcher**, with `LOGSEQ_CLI_DIR` pointing at that directory and
113
- `--signal` pointing at this plugin's storage directory:
114
-
115
- ```sh
116
- geml-sync <your-graph> <your-vault-dir> --watch --git-commit \
117
- --signal <logseq-dotdir>/storages/logseq-plugin-sync-vault-with-geml/geml-sync-dirty.json
118
- ```
119
-
120
- Edit a block in Logseq → the plugin signals → the watcher syncs → the toolbar
121
- `⇄` button shows `Sync Vault with GEML: last sync at … — 1 written, 7 unchanged.`
122
-
123
- **Settings**: *Debounce (seconds)* quiet period after the last change before
124
- the watcher is signalled (default 5; syncs feed git commits, so this is
125
- deliberately calmer than UI-style debounce).
126
-
127
- ## Honesty corner
128
-
129
- - Sync is **export-direction** today (graph → files, continuously). The
130
- write-back path (edit a `.geml` file import back by UUID) is proven in the
131
- engine (`syncDiskToEdn`) and lands next; deletions are reported, never
132
- auto-propagated (`--signal` never deletes your hand-written files either — a
133
- manifest tracks what the sync owns).
134
- - Journal pages appear as soon as `@logseq/cli` exports them (0.4.3 does not).
135
- - The watcher half is tested end-to-end in CI (a planted fake CLI exports
136
- fixture EDN, so the signal → re-sync → status round trip runs with no Logseq
137
- installed). The in-app half is verified against the 2.0.1 runtime — the
138
- plugin API surface, `hook:db:changed`, the storage-file bridge and its
139
- SDK is `@logseq/libs` 0.3.x (the `next` tag). If anything misbehaves in
140
- your setup, an issue with your Logseq version is gold.
141
-
142
- ## Proven on a live DB graph, judged by Logseq's own validator
143
-
144
- `npm test` proves, on fixtures lifted from Logseq's own `deps/db` export tests:
145
-
146
- 1. **EDN → GEML → EDN is a structural identity** (EDN map/set semantics).
147
- 2. Every generated document parses as GEML with **zero error diagnostics**.
148
- 3. A block Logseq considers addressable (exported uuid) is **addressable in
149
- GEML by the same id**.
150
- 4. **Editing one block's text changes exactly that block** in the EDN — no
151
- collateral change anywhere in the graph.
152
-
153
- And `bin/live-roundtrip.mjs` has confirmed all four against a real DB graph
154
- (2026-08-20, schema 65.22): export 6 clean documents identity; then with
155
- `--edit`, a `geml set` on one block imported back with `logseq import-edn`,
156
- **`logseq validate`: Valid!**, and the re-export showed the edit landed **in
157
- place by uuid, exactly once whole-graph re-import merges, it does not
158
- duplicate**.
159
-
160
- The design and the reasoning live in the
161
- [GEML monorepo](https://github.com/geml-spec/geml)
162
- (`docs/design/specs/2026-08-20-logseq-integration-scoping.md`); the community
163
- threads are
164
- [logseq/logseq#13086](https://github.com/logseq/logseq/discussions/13086) and
165
- [the forum post](https://discuss.logseq.com/t/35193).
166
-
167
- ## Development
168
-
169
- ```
170
- core/ converter (mapping.mjs), sync engine, bridge.mjs (the signal/status file contract)
171
- watcher/ the geml-sync CLI and its end-to-end tests published to npm as @geml/logseq-sync
172
- plugin/ the in-app half (this package.json is the Logseq plugin manifest)
173
- ```
174
-
175
- Source of truth is
176
- [`integrations/logseq/`](https://github.com/geml-spec/geml/tree/main/integrations/logseq)
177
- in the GEML monorepo; this repository mirrors it for the marketplace and
178
- carries the releases. Please open issues here, and PRs against the monorepo.
179
-
180
- The converter is two pure functions in `core/src/mapping.mjs` —
181
- `ednToGemlFiles(ednText)` and `gemlFilesToEdn(files, lib)` with the reference
182
- parser injected. The tests import the parser's build:
183
-
184
- ```sh
185
- cd geml-parser && npm install && npm run build && cd ../integrations/logseq
186
- npm install
187
- npm test
188
- ```
189
-
190
- Live-stage demos (need `@logseq/cli` via `LOGSEQ_CLI_DIR`, see Setup step 3):
191
-
192
- ```sh
193
- node watcher/bin/create-graph.mjs my-graph # create a DB graph WITHOUT the desktop app
194
- node watcher/bin/live-roundtrip.mjs my-graph # read-only: export GEML back → compare
195
- node watcher/bin/live-roundtrip.mjs my-graph --edit # + geml set import-edn logseq validate
196
- ```
197
-
198
- Versioning: the MAJOR version tracks the Logseq major it targets this is
199
- 2.x because it speaks Logseq 2.x (DB graphs) and nothing older. Minor/patch
200
- are this package's own.
201
-
202
- ## Next
203
-
204
- - **Reference translation**: block refs in titles are literally `[[<uuid>]]`,
205
- one character away from GEML's checked `[[#uuid]]` translating them lets
206
- `geml check` catch broken block refs, the actual headline of the proposal.
207
- - Property readability: scalar `:build/properties` as GEML attributes instead
208
- of the `.block-meta` EDN ride-along (NAME rules permitting).
209
- - **Write-back**: wiring `syncDiskToEdn` to the CLI so the vault is
210
- two-way edit the file, the graph follows.
211
-
212
- MIT © GEML contributors
1
+ # Sync Vault with GEML
2
+
3
+ Your Logseq DB graph as **continuously synced plain-text files** — pages and
4
+ journals back in readable files and folders, the way OG vaults felt, kept in
5
+ step with the database. And, when you want it, back again.
6
+
7
+ ![How it works](docs/how-it-works.svg)
8
+
9
+ Edit a block; seconds later the file on disk has caught up, and the toolbar says
10
+ so:
11
+
12
+ ![The toolbar reports the last sync](docs/screenshot-toolbar.png)
13
+
14
+ Two settings, and only the first one usually needs touching:
15
+
16
+ ![The plugin's settings](docs/screenshot-settings.png)
17
+
18
+ ## What you get
19
+
20
+ - 📦 **A plain-text copy that stays yours** — every page a readable file, not a
21
+ database dump, in a folder you chose
22
+ - 🔁 **Continuous, not one-shot** — edit in Logseq, and seconds later the file
23
+ on disk has caught up
24
+ - ↩️ **A way back** — `logseq-sync restore` imports the vault into a graph,
25
+ merging by block uuid. Files you can read are worth more when they are also
26
+ files you can return
27
+ - 🌿 **Git if you want it** — point the vault at a repository and every sync is
28
+ a clean commit with a line-by-line diff. Point it at a plain folder, or one
29
+ your backup tool already watches, and nothing git-shaped appears
30
+
31
+ A vault is not a mirror by default: pages you delete in Logseq are **kept** on
32
+ disk and reported, because a plain folder has no history to recover them from.
33
+ `--mirror` is how you ask for an exact copy instead.
34
+
35
+ Logseq 2.0 ships both ends of a trade-off: `logseq export` gives Markdown
36
+ (readable, lossy) and `logseq export-edn` gives EDN (lossless, not something a
37
+ person edits). The vault's format, [GEML](https://github.com/geml-spec/geml),
38
+ is the point between: **as readable as the Markdown export, as lossless as the
39
+ EDN one** — and addressable, so external tools and agents can edit one block
40
+ of a graph instead of round-tripping all of it.
41
+
42
+ The tree is laid out the way an OG vault is — the thing a file-version user
43
+ recognizes as "my graph, as files again":
44
+
45
+ ```
46
+ {:pages-and-blocks [...]} ontology.geml :properties/:classes, verbatim EDN
47
+ ⇄ graph.geml page ORDER (an addressable data block,
48
+ so filenames need no numeric prefixes)
49
+ journals/2025_02_20.geml journal pages, OG date names
50
+ pages/<name>.geml one per page:
51
+ block title → `=== text` body
52
+ block uuid → `{#uuid}` ← geml get/set address
53
+ outline tree flat blocks with `level=N`
54
+ everything else rides along in `code {lang=edn}`
55
+ ```
56
+
57
+ (Journal pages export as pages carrying `{:build/journal <yyyymmdd>}`, and the
58
+ mapping routes them into `journals/` under their OG date name — verified on a
59
+ live 2.0.1 graph, schema 65.33.)
60
+
61
+ ## How it works — two halves, one honest boundary
62
+
63
+ A Logseq 2.0 plugin runs in a sandboxed iframe: no arbitrary-path filesystem,
64
+ no git, no shell (verified against the 2.0.1 app bundle). So the in-app plugin
65
+ (`plugin/`) does the only two things only it can do:
66
+
67
+ - **hear** the graph change (`logseq.DB.onChanged`, debounced) and write a
68
+ dirty-marker file through the plugin storage API;
69
+ - **show** the last sync result in the toolbar (`⇄`) and command palette.
70
+
71
+ Everything with side effects lives in the **watcher** (`watcher/bin/logseq-sync.mjs`),
72
+ built on Logseq's own EDN export. It reacts to the marker file
73
+ immediately (interval polling stays on as a fallback), writes only the files
74
+ that actually changed so `git diff` is never noise — commits with a pathspec
75
+ scoped strictly to the vault, and reports back for the toolbar to display.
76
+ The two halves meet in the plugin's own storage directory
77
+ (`<dotdir>/storages/logseq-plugin-sync-vault-with-geml/`), the one disk location both can
78
+ reach. A file as the bridge beats a local HTTP API: no port, no server, no
79
+ CORS.
80
+
81
+ Real output, real DB graph (exported with the official CLI, validated by
82
+ `logseq validate`):
83
+
84
+ ```text
85
+ $ logseq-sync geml-spike ~/vault-demo --once --git-commit --no-signal
86
+ Sync Vault with GEML: graph "geml-spike" ➔ ~/vault-demo
87
+ export via @logseq/cli, opening the graph file directly close the graph in Logseq first
88
+ git auto-commit on, scoped to the vault
89
+ [10:15:28] Synced: 8 written, 0 unchanged.
90
+ Git: [master (root-commit) 2854063] logseq-geml: sync graph "geml-spike"
91
+ 9 files changed, 142 insertions(+)
92
+ create mode 100644 graph.geml
93
+ create mode 100644 pages/contents.geml
94
+ ...
95
+
96
+ $ logseq-sync geml-spike ~/vault-demo --once --git-commit --no-signal # run again
97
+ [10:15:31] Graph is up-to-date (0 written, 8 unchanged).
98
+ ```
99
+
100
+ ## Setup
101
+
102
+ **1. Install the plugin.** From the marketplace, or download the zip from the
103
+ [latest release](https://github.com/geml-spec/logseq-plugin-sync-vault-with-geml/releases/latest)
104
+ and load it — the release carries the built plugin, so there is nothing to
105
+ compile.
106
+
107
+ **2. Set the vault folder** in Logseq: Settings → Plugins → *Sync Vault with
108
+ GEML* **Vault folder**. Any folder you like — `~/logseq-vault`, a directory
109
+ inside a repository you already keep, one your backup tool already watches. It
110
+ is created if it does not exist, `~` means your home directory, and `restore`
111
+ reads the vault back from the same place. There is deliberately **no default**:
112
+ left empty, `logseq-sync` asks you for a folder rather than picking one for you.
113
+
114
+ That is the folder the files are written **into**; the graph they come **from**
115
+ is detected, and you do not name it.
116
+
117
+ **3. Run the watcher:**
118
+
119
+ ```sh
120
+ npx @geml/logseq-sync
121
+ ```
122
+
123
+ That is the setup. With no arguments the watcher works out the rest: the CLI
124
+ that ships inside the Logseq app, the graph the app currently has open, the
125
+ plugin's signal file, and the vault path you just set. It makes the vault a
126
+ git repository if it is not one already, syncs, and keeps watching. Edit a
127
+ block in Logseq → the plugin signals → the watcher syncs → the toolbar `⇄`
128
+ shows `Sync Vault with GEML: last sync at … — 1 written, 7 unchanged.`
129
+
130
+ Not sure it is wired up? **`npx @geml/logseq-sync doctor`** prints what it
131
+ found and what is missing, and exits non-zero when the setup cannot sync:
132
+
133
+ ```text
134
+ ok Logseq dotdir /Users/you/.logseq
135
+ ok plugin /Users/you/.logseq/storages/logseq-plugin-sync-vault-with-geml
136
+ ok app CLI /Users/you/.local/bin/logseq (found on PATH)
137
+ ok graph Demo (open in the app)
138
+ MISS vault unset Settings Plugins Sync Vault with GEML → "Vault folder"
139
+ ok git identity configured
140
+ ok bridge /Users/you/.logseq/storages/.../geml-sync-dirty.json
141
+ ```
142
+
143
+ ### When you want to say it yourself
144
+
145
+ | | |
146
+ |---|---|
147
+ | `logseq-sync <vault-dir>` | vault here instead of in the plugin settings |
148
+ | `logseq-sync <graph> <vault-dir>` | both explicitly |
149
+ | `--graph <name>` | pick the graph — needed when several are open |
150
+ | `--once` | sync once and exit, instead of watching |
151
+ | `--git-commit` | commit, creating the vault repository if there is none |
152
+ | `--no-git-commit` | never touch git |
153
+ | `--mirror` | delete vault files for pages removed from the graph |
154
+ | `--markdown <dir>` | also write a lossy Markdown copy there, for other tools |
155
+ | `--interval <seconds>` | heartbeat between signals (default 10) |
156
+ | `--app-cli <path>` | a Logseq CLI the search did not find |
157
+ | `--signal <file>` / `--no-signal` | the plugin bridge, or none |
158
+
159
+ ### Going back: `logseq-sync restore`
160
+
161
+ ```sh
162
+ logseq-sync restore # rehearse: says what it would import, writes nothing
163
+ logseq-sync restore --yes # take a Logseq backup, then import the vault
164
+ ```
165
+
166
+ The vault imports into the graph by block uuid, so an edit lands in place
167
+ rather than duplicating. This is the one direction that writes into your notes,
168
+ so it rehearses unless you pass `--yes`, and `--yes` takes the app's own graph
169
+ backup first (`--no-backup` opts out, and then you are on your own).
170
+
171
+ ### The exporter, and why the app's own CLI
172
+
173
+ While Logseq has a graph open its db-worker holds an **exclusive lock** on that
174
+ graph's `db.sqlite`, so an exporter that opens the file directly dies with
175
+ `database is locked` — which is every export while you are actually working.
176
+ The CLI inside the desktop app does not open the file, it asks the running app,
177
+ so it exports mid-edit. That is why the watcher looks for it first: on PATH, at
178
+ `~/.local/bin/logseq`, then the app bundle itself.
179
+
180
+ `--no-app-cli` falls back to the separate [`@logseq/cli`](https://www.npmjs.com/package/@logseq/cli)
181
+ npm package, which opens the graph file directly. It is only useful against a
182
+ graph the app does **not** have open, and on Node 24 it needs a
183
+ `better-sqlite3` override to install at all:
184
+
185
+ ```sh
186
+ mkdir logseq-cli && cd logseq-cli && npm init -y
187
+ npm pkg set overrides.better-sqlite3=12.11.1
188
+ npm i @logseq/cli
189
+ # then: LOGSEQ_CLI_DIR=$PWD logseq-sync --no-app-cli …
190
+ ```
191
+
192
+ `--api-server-token` (or `LOGSEQ_API_SERVER_TOKEN`) routes that fallback
193
+ through the app's HTTP API server rather than the file — but `@logseq/cli`
194
+ 0.4.3 hardcodes `http://127.0.0.1:12315` and Logseq 2.0.1 does not listen
195
+ there, so on 2.0.1 this path goes nowhere. Prefer the app CLI.
196
+
197
+ **Settings**: *Vault folder* — where the files are written, and where `restore`
198
+ reads them back from. *Debounce (seconds)*quiet
199
+ period after the last change before the watcher is signalled (default 5; syncs
200
+ feed git commits, so this is deliberately calmer than UI-style debounce).
201
+
202
+ ## Honesty corner
203
+
204
+ - The **continuous** direction is graph files. Going back is a deliberate
205
+ command (`restore`), not a background looptwo live writers over one graph
206
+ is a merge problem this does not pretend to have solved.
207
+ - Files the sync did not write are never touched: a manifest tracks what it
208
+ owns, and `--mirror` only ever removes files from that list.
209
+ - **The app's lock is the thing to know about.** A running Logseq holds
210
+ `db.sqlite` exclusively, so the `@logseq/cli` export only works with the app
211
+ closed (or on a graph it does not have open). Continuous sync therefore runs
212
+ through the desktop app's own CLI (`--app-cli`), which asks the running app
213
+ instead of touching the file. Verified on 2.0.1: same 9 documents as the
214
+ offline export, byte-identical except three keys of export metadata.
215
+ - **The Markdown tree is a copy, not the vault.** `--markdown` runs the GEML
216
+ through the reference parser's Markdown output, which is lossy by design and
217
+ is **not** a Logseq graph — it will not open in the file version. The GEML
218
+ tree stays the one that round-trips; nothing reads the Markdown back.
219
+ - **Restore merges, it does not replace.** An import lands by uuid over
220
+ whatever the graph currently holds; it will not remove pages the vault no
221
+ longer has. Take the backup.
222
+ - **A graph name you mistype is created, not rejected.** `logseq graph export
223
+ --graph <name>` silently makes a new empty graph rather than failing, and
224
+ syncing that emptiness would wipe the vault's synced files. The watcher
225
+ refuses any graph name it cannot see under `<root>/graphs` first.
226
+ - **A commit that fails is printed, not swallowed.** `git` with no configured
227
+ author (or `user.useConfigOnly`) writes the files and commits nothing;
228
+ `doctor` calls that out up front, and a sync that could not commit says
229
+ `Git: NOT COMMITTED — …` rather than just `Synced`.
230
+ - **2.0 renamed the export we read.** `:export-type :graph` now means a datoms
231
+ dump; the `{:pages-and-blocks ...}` shape this converter reads is
232
+ `:graph-human`. The watcher asks for `:graph-human` explicitly.
233
+ - The watcher half is tested end-to-end in CI (a planted fake CLI exports
234
+ fixture EDN, so the signal → re-sync → status round trip runs with no Logseq
235
+ installed). The in-app half is verified against the 2.0.1 runtime — the
236
+ plugin API surface, `hook:db:changed`, the storage-file bridge — and its
237
+ SDK is `@logseq/libs` 0.3.x (the `next` tag). If anything misbehaves in
238
+ your setup, an issue with your Logseq version is gold.
239
+
240
+ ## Proven on a live DB graph, judged by Logseq's own validator
241
+
242
+ `npm test` proves, on fixtures lifted from Logseq's own `deps/db` export tests:
243
+
244
+ 1. **EDN → GEML → EDN is a structural identity** (EDN map/set semantics).
245
+ 2. Every generated document parses as GEML with **zero error diagnostics**.
246
+ 3. A block Logseq considers addressable (exported uuid) is **addressable in
247
+ GEML by the same id**.
248
+ 4. **Editing one block's text changes exactly that block** in the EDN — no
249
+ collateral change anywhere in the graph.
250
+
251
+ And `bin/live-roundtrip.mjs` has confirmed all four against a real DB graph
252
+ (2026-08-20, schema 65.22): export → 6 clean documents → identity; then with
253
+ `--edit`, a `geml set` on one block imported back with `logseq import-edn`,
254
+ **`logseq validate`: Valid!**, and the re-export showed the edit landed **in
255
+ place by uuid, exactly once — whole-graph re-import merges, it does not
256
+ duplicate**.
257
+
258
+ The design and the reasoning live in the
259
+ [GEML monorepo](https://github.com/geml-spec/geml)
260
+ (`docs/design/specs/2026-08-20-logseq-integration-scoping.md`); the community
261
+ threads are
262
+ [logseq/logseq#13086](https://github.com/logseq/logseq/discussions/13086) and
263
+ [the forum post](https://discuss.logseq.com/t/35193).
264
+
265
+ ## Development
266
+
267
+ ```
268
+ core/ converter (mapping.mjs), sync engine, bridge.mjs (the signal/status file contract)
269
+ watcher/ the logseq-sync CLI and its end-to-end tests — published to npm as @geml/logseq-sync
270
+ plugin/ the in-app half (this package.json is the Logseq plugin manifest)
271
+ ```
272
+
273
+ Source of truth is
274
+ [`integrations/logseq/`](https://github.com/geml-spec/geml/tree/main/integrations/logseq)
275
+ in the GEML monorepo; this repository mirrors it for the marketplace and
276
+ carries the releases. Please open issues here, and PRs against the monorepo.
277
+
278
+ The converter is two pure functions in `core/src/mapping.mjs` —
279
+ `ednToGemlFiles(ednText)` and `gemlFilesToEdn(files, lib)` — with the reference
280
+ parser injected. The tests import the parser's build:
281
+
282
+ ```sh
283
+ cd geml-parser && npm install && npm run build && cd ../integrations/logseq
284
+ npm install
285
+ npm test
286
+ ```
287
+
288
+ Live-stage demos (need `@logseq/cli` via `LOGSEQ_CLI_DIR` — see "The exporter" above):
289
+
290
+ ```sh
291
+ node watcher/bin/create-graph.mjs my-graph # create a DB graph WITHOUT the desktop app
292
+ node watcher/bin/live-roundtrip.mjs my-graph # read-only: export → GEML → back → compare
293
+ node watcher/bin/live-roundtrip.mjs my-graph --edit # + geml set → import-edn → logseq validate
294
+ ```
295
+
296
+ Versioning: the MAJOR version tracks the Logseq major it targets — this is
297
+ 2.x because it speaks Logseq 2.x (DB graphs) and nothing older. Minor/patch
298
+ are this package's own.
299
+
300
+ ## Next
301
+
302
+ - **Reference translation**: block refs in titles are literally `[[<uuid>]]`,
303
+ one character away from GEML's checked `[[#uuid]]` — translating them lets
304
+ `geml check` catch broken block refs, the actual headline of the proposal.
305
+ - Property readability: scalar `:build/properties` as GEML attributes instead
306
+ of the `.block-meta` EDN ride-along (NAME rules permitting).
307
+ - **Write-back**: wiring `syncDiskToEdn` to the CLI so the vault is
308
+ two-way — edit the file, the graph follows.
309
+
310
+ MIT © GEML contributors