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.
obsidian://ling-sync.SecretStorage.requestUrl.Vault changes still sync from Obsidian to Ling. In the other direction, only Ling's obsidian-notes Agent Skill can create a durable command through the user-scoped Agent API; the App itself does not issue one. The plugin leases that command while online and applies it inside the folders approved during pairing.
1.11.4 or newer (SecretStorage was added in 1.11.4).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&scopes=notes.read%2Cnotes.sync%2Cnotes.write%2Cnotes.delete
api_base_url is optional. The default is https://api.withling.top; the plugin appends /ling-api unless it is already present. scopes records the permissions authorized for that one-time pairing. Older links without it remain read-only (notes.read, notes.sync).
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", "notes.write", "notes.delete"],
"plugin_version": "0.2.2"
}
An empty folder selection is encoded as [""], which means the whole Vault. The plugin never sends an empty folder_paths array.
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
POST /integrations/notes/obsidian/connector/mutations/lease
POST /integrations/notes/obsidian/connector/mutations/{mutation_id}/result
Version 0.2.2 requests notes.read, notes.sync, notes.write, and notes.delete after showing them in the pairing consent modal. Existing connections retain their old scopes until the user explicitly pairs again. notes.delete means moving a file through Obsidian's configured trash behavior; permanent deletion is not supported.
The command-creation API is intentionally separate from these connector routes. It is called by the Ling Agent Skill with a short-lived user capability supplied in Agent system context; the plugin never receives or stores that capability.
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. Foreground events within one heartbeat interval share a deferred complete-manifest reconciliation so repeated focus changes do not upload the Vault over and over. A successful manifest supersedes only the incremental operations that were already queued when its snapshot started; later Vault changes remain queued. 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. Stored agent-mutation receipts are rebased after a sync cursor conflict, renew an expired lease without reapplying the Vault operation, and are isolated when pairing switches to a different connection.
Only .md files under configured folders are uploaded. These paths are always excluded:
Vault.configDir).trash.git.Vault access uses Vault.getMarkdownFiles() and Vault.cachedRead(). The plugin does not access the filesystem adapter directly.
Agent writes use only public Obsidian APIs:
Vault.create for a new note in an existing authorized folder.Vault.process for an atomic whole-body replacement.FileManager.renameFile for a move or rename.FileManager.trashFile for deletion.Replace, move, and trash commands include Ling's last mirrored SHA-256 revision. The plugin reads the real Vault file again before executing and returns a conflict rather than overwriting a newer local edit. It suppresses the resulting Vault events from the ordinary outbound queue, reports the actual resulting operation to Ling, and stores an execution receipt in vault-local app storage until the server acknowledges it.
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.
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.2.2, 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.
SecretStorage. Tokens are never written to plugin data.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.