# AGENTS.md - Logseq Plugin Development Guide ## Overview This document provides guidance for AI agents (Copilot, coding assistants, etc.) to help develop Logseq plugins using the `@logseq/libs` SDK. Logseq is a privacy-first, open-source knowledge management and note-taking application built on top of local plain-text Markdown and Org-mode files. The plugin system extends Logseq's functionality through a JavaScript/TypeScript SDK. ## Repository Structure ### Core SDK (`logseq/logseq` - `libs/` folder) The official SDK source code is located in the `libs/` directory of the main Logseq repository: ``` libs/ ├── src/ │ ├── LSPlugin.ts # Main type definitions & interfaces │ ├── LSPlugin.user.ts # User-facing plugin API implementation │ ├── LSPlugin.core.ts # Core plugin system │ ├── LSPlugin. caller.ts # IPC message caller │ ├── LSPlugin.shadow.ts # Shadow DOM support │ ├── helpers.ts # Utility functions │ ├── modules/ # Additional modules (Experiments, Storage, Request) │ └── postmate/ # Cross-frame communication ├── package.json # NPM package config (@logseq/libs) ├── README.md # SDK documentation └── CHANGELOG.md # Version history ``` ### Plugin Samples (`logseq/logseq-plugin-samples`) Official sample plugins demonstrating various SDK features: | Sample | Description | Key APIs | |-------------------------------|----------------------------------|-------------------------------------------------| | `logseq-slash-commands` | Basic slash command registration | `Editor.registerSlashCommand` | | `logseq-pomodoro-timer` | Macro renderer & slot UI | `App.onMacroRendererSlotted`, `provideUI` | | `logseq-a-translator` | Selection hooks & float UI | `Editor.onInputSelectionEnd`, `provideUI` | | `logseq-awesome-fonts` | Custom styles & settings | `provideStyle`, `useSettingsSchema` | | `logseq-bujo-themes` | Custom theme development | `provideTheme` | | `logseq-journals-calendar` | Calendar integration | `App.getCurrentGraph`, `Editor.getPage` | | `logseq-emoji-picker` | UI overlay with 3rd party libs | `showMainUI`, `hideMainUI` | | `logseq-reddit-hot-news` | Batch block insertion | `Editor.insertBatchBlock`, `App.registerUIItem` | | `logseq-imdb-top250-importer` | DB graph data import | DB-specific APIs | ## Plugin Architecture ### Entry Point Pattern Every Logseq plugin follows this basic pattern: ```typescript import '@logseq/libs' async function main() { // Plugin initialization code console.log('Plugin loaded!') } // Bootstrap logseq.ready(main).catch(console.error) ``` ### Plugin Modes Plugins can run in two modes defined in `package.json`: - **`iframe`** (default): Plugin runs in an isolated iframe sandbox - **`shadow`** (still draft): Plugin runs in a Shadow DOM container (faster, less isolation) > **⚠️ Sandbox Environment:** All Logseq plugins run in an isolated iframe sandbox. > This provides security isolation between plugins and the > main application. Communication between the plugin iframe and > Logseq happens through a postMessage-based RPC mechanism. ### package.json Configuration ## Package. json Configuration ### Basic Structure ```json { "name": "logseq-my-plugin", "version": "0.1.0", "description": "A brief description of your plugin", "author": "Your Name", "license": "MIT", "main": "dist/index.html", "scripts": { "dev": "parcel ./index.html --public-url ./", "build": "parcel build --public-url . --no-source-maps index.html" }, "devDependencies": { "@logseq/libs": "^0.0.17", "parcel": "^2.0.0" }, "logseq": { "id": "my-unique-plugin-id", "main": "dist/index.html", "icon": "./icon.png" } } ``` ### Logseq Configuration Block The `logseq` field in `package.json` defines plugin metadata and behavior: ```json { "logseq": { "id": "my-unique-plugin-id", "main": "dist/index.html", "icon": "./icon. png", "title": "My Plugin Display Name", "effect": true, "themes": [ { "name": "My Dark Theme", "url": "./css/dark.css", "mode": "dark", "description": "A beautiful dark theme" }, { "name": "My Light Theme", "url": "./css/light.css", "mode": "light", "description": "A clean light theme" } ] } } ``` ### Configuration Fields | Field | Type | Required | Description | |------------|-----------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `id` | `string` | Yes | Unique plugin identifier. Must be unique across all plugins. Use lowercase with hyphens. | | `main` | `string` | Yes | Entry point HTML file path (relative to package.json). | | `entry` | `string` | No | Alias for `main`. | | `icon` | `string` | No | Plugin icon path. Displayed in plugin marketplace and settings. | | `title` | `string` | No | Display name shown in UI. Falls back to package `name` if not set. | | `effect` | `boolean` | No | When `true`, the plugin runs as a background effect without user interaction. Useful for plugins that only inject styles/themes or run automated tasks. Default: `false`. | | `themes` | `Theme[]` | No | Array of theme definitions this plugin provides. | | `devEntry` | `string` | No | Alternative entry point for development (e.g., with HMR). | ### Theme Configuration For theme-only plugins, define themes in the `themes` array: ```json { "logseq": { "id": "my-theme-pack", "main": "dist/index.html", "effect": true, "themes": [ { "name": "Nord Dark", "url": "./themes/nord-dark.css", "mode": "dark", "description": "Nord-inspired dark color scheme" }, { "name": "Nord Light", "url": "./themes/nord-light.css", "mode": "light", "description": "Nord-inspired light color scheme" } ] } } ``` **Theme Object Fields:** | Field | Type | Required | Description | |---------------|----------|----------|--------------------------------------------------| | `name` | `string` | Yes | Theme display name shown in theme selector. | | `url` | `string` | Yes | Path to the CSS file (relative to package.json). | | `mode` | `light` | `dark` | Yes | Theme mode. Determines when theme appears in selector. | | `description` | `string` | No | Brief description of the theme. | ## Core API Namespaces The SDK exposes the following primary namespaces through the global `logseq` object: ### `logseq. App` - Application APIs Application-level operations and hooks: ```typescript // Get app/user information await logseq.App.getInfo() await logseq.App.getUserConfigs() await logseq.App.getCurrentGraph() // Graph operations await logseq.App.getCurrentGraphFavorites() await logseq.App.getCurrentGraphRecent() // Navigation logseq.App.pushState('page', { name: 'my-page' }) // UI Registration logseq.App.registerUIItem('toolbar', { key: 'my-button', template: `Click` }) // Command Registration logseq.App.registerCommandPalette({ key: 'my-command', label: 'My Command', keybinding: { binding: 'mod+shift+m' } }, () => { /* action */ }) // Event Hooks logseq.App.onCurrentGraphChanged(() => {}) logseq.App.onThemeModeChanged(({ mode }) => {}) logseq.App.onRouteChanged(({ path }) => {}) logseq.App.onMacroRendererSlotted(({ slot, payload }) => {}) ``` ### `logseq.Editor` - Editor APIs Block and page manipulation: ```typescript // Slash Commands logseq.Editor.registerSlashCommand('My Command', async ({ uuid }) => { await logseq.Editor.insertAtEditingCursor('Hello!') }) // Block Context Menu logseq.Editor.registerBlockContextMenuItem('My Action', async ({ uuid }) => { const block = await logseq.Editor.getBlock(uuid) }) // Block Operations await logseq.Editor.getBlock(uuid) await logseq.Editor.insertBlock(uuid, 'content', { sibling: true }) await logseq.Editor.updateBlock(uuid, 'new content') await logseq.Editor.removeBlock(uuid) await logseq.Editor.insertBatchBlock(uuid, [ { content: 'Block 1', children: [{ content: 'Child' }] } ]) // Page Operations await logseq.Editor.getPage('page-name') await logseq.Editor.createPage('new-page', { prop: 'value' }) await logseq.Editor.deletePage('page-name') await logseq.Editor.getCurrentPageBlocksTree() // Cursor & Selection await logseq.Editor.checkEditing() await logseq.Editor.getEditingCursorPosition() await logseq.Editor.insertAtEditingCursor('text') await logseq.Editor.exitEditingMode() // Properties (DB graphs) await logseq.Editor.upsertBlockProperty(uuid, 'key', 'value') await logseq.Editor.getBlockProperties(uuid) ``` ### `logseq.DB` - Database APIs Query and data operations: ```typescript // DSL Query (Logseq query language) const results = await logseq.DB.q('[[my-page]]') // Datascript Query const results1 = await logseq.DB.datascriptQuery(` [: find (pull ?b [*]) :where [?b :block/marker "TODO"]] `) // Watch for changes logseq.DB.onChanged(({ blocks, txData }) => { console.log('Database changed:', blocks) }) // Watch specific block logseq.DB.onBlockChanged(uuid, (block, txData) => { console.log('Block changed:', block) }) // File operations await logseq.DB.getFileContent('logseq/custom.css') await logseq.DB.setFileContent('logseq/custom.js', 'console.log("hi")') ``` ### `logseq.UI` - UI Utilities ```typescript // Toast messages await logseq.UI.showMsg('Success! ', 'success') await logseq.UI.showMsg('Warning!', 'warning', { timeout: 5000 }) logseq.UI.closeMsg(key) // DOM queries const rect = await logseq.UI.queryElementRect('.my-selector') const exists = await logseq.UI.queryElementById('my-id') ``` ### `logseq.Git` - Git Operations Only supported in file-based graphs for desktop Logseq. ```typescript const result = await logseq.Git.execCommand(['status']) const ignoreContent = await logseq.Git.loadIgnoreFile() await logseq.Git.saveIgnoreFile('. DS_Store\nnode_modules') ``` ### `logseq.Assets` - Asset Management ```typescript const files = await logseq.Assets.listFilesOfCurrentGraph(['png', 'jpg']) const storage = logseq.Assets.makeSandboxStorage() const url = await logseq.Assets.makeUrl('path/to/file') await logseq.Assets.builtInOpen('path/to/asset. pdf') ``` ## UI Injection Patterns ### provideUI - Inject Custom UI ```typescript logseq.provideUI({ key: 'my-ui', path: '#my-target', // or use `slot` for macro renderers template: `