No description
Find a file
2026-07-17 21:31:32 +08:00
.github/workflows release: 0.1.4 2026-07-17 21:31:32 +08:00
scripts fix: distinguish branch and release refs 2026-07-17 20:09:08 +08:00
src release: 0.1.4 2026-07-17 21:31:32 +08:00
tests release: 0.1.2 2026-07-17 20:46:05 +08:00
.gitignore feat: add Ling Sync Obsidian plugin 2026-07-17 19:47:15 +08:00
CHANGELOG.md release: 0.1.4 2026-07-17 21:31:32 +08:00
esbuild.config.mjs feat: add Ling Sync Obsidian plugin 2026-07-17 19:47:15 +08:00
LICENSE release: 0.1.3 2026-07-17 20:59:52 +08:00
manifest.json release: 0.1.4 2026-07-17 21:31:32 +08:00
package-lock.json release: 0.1.4 2026-07-17 21:31:32 +08:00
package.json release: 0.1.4 2026-07-17 21:31:32 +08:00
README.md release: 0.1.4 2026-07-17 21:31:32 +08:00
tsconfig.json feat: add Ling Sync Obsidian plugin 2026-07-17 19:47:15 +08:00
versions.json release: 0.1.4 2026-07-17 21:31:32 +08:00
vitest.config.ts feat: add Ling Sync Obsidian plugin 2026-07-17 19:47:15 +08:00

Ling Sync for Obsidian

Ling Sync is a mobile-safe Obsidian Community Plugin that sends selected Markdown notes to Ling through the provider-neutral Notes Integration API. It uses only public Obsidian APIs, works with isDesktopOnly: false, and does not import Node.js, Electron, or filesystem modules at runtime.

What P0 includes

  • Pairing through obsidian://ling-sync.
  • Tokens stored only in Obsidian SecretStorage.
  • Device identity, cursor, and connection status kept in Obsidian's vault-local app storage rather than syncable plugin data.
  • Whole-Vault or selected-folder manifests.
  • Debounced create, modify, rename, and delete batches.
  • Strict server cursors and idempotent retries.
  • Startup and foreground-resume reconciliation after plugin or device interruption.
  • Desktop and mobile networking through Obsidian requestUrl.
  • One active connector device per Ling account and Vault; pairing another device revokes the previous device to keep the P0 cursor single-writer.

P0 sync is strictly Obsidian to Ling. Every client pairing and connector claim is fixed to the minimum notes.read and notes.sync scopes.

Compatibility

  • Obsidian 1.11.4 or newer (SecretStorage was added in 1.11.4).
  • Desktop and mobile Obsidian.
  • Node.js 20 or newer is needed only to build or test the plugin.

Pairing flow

  1. The user configures the Ling API and selected folders in the Obsidian plugin settings.
  2. The user chooses Obsidian in Ling; Ling creates a pairing and opens the returned launch URI.
  3. Obsidian dispatches the URI to this plugin. The plugin rejects a launch URI whose API does not exactly match the API explicitly configured in settings.
  4. Obsidian displays the Ling API host, Vault, folders, and scope. Cancel performs no network request.
  5. After the user clicks Connect, the plugin claims the pairing, stores the rotating credentials in SecretStorage, obtains the authoritative server cursor, and uploads a complete manifest.

The protocol format is:

obsidian://ling-sync?pairing_id=<id>&pairing_code=<one-time-code>&api_base_url=https%3A%2F%2Fapi.withling.top

api_base_url is optional. The default is https://api.withling.top; the plugin appends /ling-api unless it is already present.

The plugin requires HTTPS so connector tokens are never sent over plaintext. HTTP is accepted only for loopback development hosts: localhost, 127.0.0.1, and [::1].

The claim body is:

{
  "pairing_id": "pairing-id",
  "pairing_code": "one-time-code",
  "vault_id": "stable-vault-uuid",
  "vault_name": "My Vault",
  "device_id": "stable-device-uuid",
  "device_name": "Obsidian Mobile",
  "folder_paths": ["Projects/Ling"],
  "scopes": ["notes.read", "notes.sync"],
  "plugin_version": "0.1.4"
}

An empty folder selection is encoded as [""], which means the whole Vault. The plugin never sends an empty folder_paths array.

Backend contract

The API root is <Ling API base URL>/ling-api. Connector routes are:

POST /integrations/notes/obsidian/connector/claim
POST /integrations/notes/obsidian/connector/refresh
POST /integrations/notes/obsidian/connector/heartbeat
PUT  /integrations/notes/obsidian/connector/manifest
POST /integrations/notes/obsidian/connector/changes/batch

P0 always requests the exact provider-neutral scopes notes.read and notes.sync. The plugin has no broader permission option.

The plugin sends a complete manifest as:

{
  "idempotency_key": "uuid",
  "snapshot_id": "snapshot-uuid",
  "chunk_index": 0,
  "is_last": true,
  "base_cursor": 4,
  "next_cursor": 5,
  "entries": [
    {
      "note_id": "stable-note-uuid",
      "path": "Projects/Ling/plan.md",
      "content": "# Plan\n",
      "content_hash": "sha256-hex-of-utf8-content",
      "modified_at": "2026-07-17T10:00:00.000Z",
      "metadata": {}
    }
  ]
}

Large manifests are split into an ordered snapshot: at most 100 entries and safely below the 8 MiB UTF-8 JSON limit per chunk. Every acknowledged chunk advances the cursor by one; only is_last: true tombstones notes absent from the completed snapshot. A new startup reconciliation safely replaces an interrupted partial snapshot with a new snapshot_id from chunk 0.

Change batches use the same cursor window and a stable idempotency_key for an in-flight retry. Every operation has its own operation_id. Content is included for create and modify; rename sends previous_path; delete sends the deleted path. A batch contains at most 100 operations and stays safely below 8 MiB. Markdown content is measured as UTF-8. If an already mirrored note grows beyond 2 MiB, the plugin sends a delete operation so Ling cannot retain stale content. The plugin keeps an explicit connection error for the oversized path; after the file returns under the limit, a complete manifest restores the current content.

At startup and whenever Obsidian returns to the foreground, heartbeat or token refresh returns the authoritative server cursor and the plugin sends a fresh complete manifest. Focus and visibility events are coalesced so a mobile resume does not upload two manifests. If the plugin stopped after the server committed a request but before local settings were saved, it does not replay the stale cursor. A runtime 409 follows the same reconciliation path.

Vault rules

Only .md files under configured folders are uploaded. These paths are always excluded:

  • The Vault's configured settings directory (Vault.configDir)
  • .trash
  • .git
  • Any directory whose name starts with .

Vault access uses Vault.getMarkdownFiles() and Vault.cachedRead(). The plugin does not access the filesystem adapter directly.

Build and test

npm install
npm test
npm run build
npm run check:release

The build produces main.js. For a manual install, place these files in <vault>/.obsidian/plugins/ling-sync/:

main.js
manifest.json

Enable Ling Sync in Obsidian's Community Plugins settings, configure the API and folders, and start pairing from Ling.

Release preparation

manifest.json, package.json, and versions.json must describe the same plugin version and minimum Obsidian version. npm run check:release verifies that contract. A release tag must be the exact semantic version from manifest.json (for example, 0.1.4, without a v prefix).

Pushing an exact version tag runs tests, builds the production bundle, verifies the metadata, and creates a draft GitHub release with the assets Obsidian installs:

main.js
manifest.json

Review and publish the draft before submitting the repository to the Obsidian Community Plugin directory. versions.json stays in the repository root as the compatibility map; it is not a release attachment.

Ling is authoritative for unlinking. API base URL and folders are locked while connected because they are part of the pairing authorization. Unlink the connection in Ling, then pair again to change them; the plugin does not expose a local-only disconnect that could leave an active server mirror behind.

Data and privacy disclosures

  • A Ling account is required to create and manage a connection.
  • The plugin reads only Markdown files in the folders the user approves. It excludes Obsidian configuration, trash, Git data, hidden directories, and non-Markdown files.
  • It sends each included note's body, Vault-relative path, modification time, SHA-256 content hash, stable note ID, and empty provider metadata object to the explicitly configured Ling API over HTTPS (loopback HTTP is development-only).
  • Connector access and refresh tokens are stored with Obsidian SecretStorage. Tokens are never written to plugin data.
  • P0 does not write, rename, or delete files in the Vault. Sync direction is Obsidian to Ling.
  • The plugin contains no telemetry, analytics, advertising, or payment flow.
  • This is a beta integration, not yet a published Obsidian Community Plugin listing.

License

Copyright (c) 2026 ZavixAI.

The files in this repository are licensed under the Mozilla Public License 2.0. This license applies only to this plugin repository; Ling services and other separate products are not licensed by this repository. See LICENSE.