TSDR TRADING31 downloadsOne-way sync of this vault's markdown notes into your UCT Intelligence Notebook (Journal 2.0).
One-way sync of an Obsidian vault's markdown notes into your UCT Intelligence Notebook (Journal 2.0). The plugin only ever pushes — it never writes anything back into your vault.
crypto, which Obsidian's mobile runtime does not expose to
plugins (isDesktopOnly: true).Disconnecting from the dashboard revokes the device token immediately; the plugin's next sync will report that it is no longer connected, and you can reconnect with a fresh code at any time.
https://uctintelligence.com).POST /obsidian/redeem).final manifest so the
server can detect deletions.![[embedded image]] or a link to a local file stays as a broken
reference in the synced copy — the server-side provider
(providers/obsidian.py::fetch_media) refuses local vault attachments
outright. A note that already links to a public https:// image is the
one case that resolves.isDesktopOnly: true in manifest.json). The plugin
uses Node's crypto module directly for hashing and vault-id generation,
which Obsidian's mobile (Capacitor) runtime does not provide to plugins.vault_path (there is no stable per-note id this
plugin can read from the vault and forward). Renaming or moving a file
therefore looks, to the server, exactly like the old path disappearing
(garbage-collected out of data.files here, then delete-detected
server-side after a couple of missed passes) and a genuinely new path
appearing (pushed as a brand-new note). Content is never lost — the old
server-side copy survives until delete-detection catches up, and the new
path syncs immediately — but a member who has already started editing that
note inside the Notebook should expect the rename to surface as two
notes for a short window, not an in-place rename. Fixing this needs a
stable note id on the wire, which is a two-repo (plugin + server) change
and out of scope for this release.sync-manager.ts's auth-failure section for the
one exception — a 401 clears the token deliberately, rather than retrying
a dead credential). Nothing is lost: notes already durably staged by an
earlier batch in the same run are simply re-pushed next time (the server
no-ops an unchanged content_hash), and no manifest is ever sent for a
run that didn't finish pushing every batch — so a mid-run failure can
never mark a real note deleted. Recovery today is a member clicking "Sync
now" again. An automatic retry (with backoff) is the natural v1.1 on top
of this, deliberately deferred for the same reason as background sync:
keeping this first release small enough to review honestly.The server has a hard ceiling on how large a note's converted body may be
(obsidian_staging.MAX_BODY_MD_LEN, currently derived as roughly 212,765
characters of markdown — measured markdown→TipTap blowup is 3.4–4.7×, up to
~9× for a many-short-headings shape). A note over that ceiling will never
store, no matter how many times it's re-pushed.
This plugin therefore:
tooLarge.final manifest — the server's
own manifest-integrity check (obsidian_staging.py's I3 finding) treats
a manifest entry as a claim "this vault has staged body content for this
path," which is false for a note that was always rejected as too large.See src/sync-plan.ts's module docstring for the full reasoning and
src/test/sync-plan.test.ts / src/test/sync-manager.test.ts for the
tests that pin this behavior.
src/
types.ts Wire shapes + persisted-settings shape. No obsidian import.
hashing.ts sha256Hex() — content hashing.
batching.ts Splits changed notes into batches under the server's
note-count and byte-size ceilings.
sync-plan.ts Pure decision logic: what to push, what to manifest,
too-large tracking, delete detection. No obsidian import.
api-client.ts Transport interface + ObsidianHttpTransport (the one
file that imports obsidian's requestUrl).
sync-manager.ts Orchestrates scan -> plan -> batch -> push -> manifest
-> persist. Depends only on the Transport/VaultSource
interfaces — no obsidian import.
vault-source.ts ObsidianVaultSource — thin adapter over app.vault.
settings.ts The settings tab UI.
main.ts Plugin entry point; wires everything above together.
styles.css The one class settings.ts needs (a themed error color)
— per Obsidian's guidelines, a CSS class instead of an
inline style. Everything else uses Obsidian's own
built-in Setting/`setting-item-description` styling.
test/
hashing.test.ts, batching.test.ts, sync-plan.test.ts Pure-function unit tests.
sync-manager.test.ts Stubbed-transport, stubbed-vault integration tests
covering batching, the two-phase push, tooLarge
exclusion/retry, delete detection, and 401 handling.
api-client.test.ts Exercises the real HTTP-shaping logic against a
stubbed requestUrl (obsidian-stub.ts).
sync-plan.ts, batching.ts, hashing.ts, and sync-manager.ts
deliberately have zero dependency on the obsidian module — every
decision the plugin makes is unit-testable in plain Node, with no live
vault and no live server. Only api-client.ts, vault-source.ts,
settings.ts, and main.ts touch the real Obsidian API.
This plugin is one half of a contract whose other half lives in the closed-source UCT Intelligence server. The server-side names below are recorded so a future change on either side is made deliberately aware of the other:
api/routers/note_sync.py — POST /obsidian/redeem,
POST /obsidian/ingest, ObsidianRedeemBody, ObsidianIngestBody.
OBSIDIAN_INGEST_NOTES_FIELD ("notes") is pinned — do not rename the
top-level notes-array key.api/services/journal_two/note_connectors/obsidian_staging.py —
ingest_batch's caps, the too-large isolation logic, and the manifest
integrity check (a final manifest may only name paths already staged
for this vault — either in the same call or an earlier one).api/services/journal_two/note_connectors/obsidian_link.py — connect-code
mint/redeem and device-token semantics (reconnecting the same
vaultId rotates the device token rather than refusing).If the server-side contract changes, this plugin needs a matching update — there is no shared type-checking across the two repositories, so changes on either side must be made deliberately aware of the other.
npm install
npm run dev # esbuild watch mode -> main.js
npm run build # tsc typecheck + production esbuild bundle
npm test # vitest — all unit/integration tests, no live server
To try the plugin in a real vault during development, symlink (or copy)
this repository into <vault>/.obsidian/plugins/uct-notebook-sync/ after
running npm run build, then enable it in Obsidian's Community Plugins
settings (with Restricted Mode turned off for that vault).
Matches the disclosure shown in the dashboard's own "Connect Obsidian" modal before a connect code can be minted: once connected, this plugin sends the markdown text of vault notes (file path, content, and last-modified time) to the configured server. It does not upload attachments or images stored in the vault, and nothing is ever written back into the vault. Consent is captured on the dashboard side, before a connect code can even be minted — this plugin has no separate consent surface of its own because a device token cannot exist without that consent having already been given.