# EdgeEver Plugin Development Guide [简体中文](plugin-development.zh-CN.md) EdgeEver provides client plugin and no-code theme extension capabilities. Users can install extensions from the official marketplace, public GitHub repositories, or manifest URLs. Installed extensions sync across Web and desktop platforms with the current workspace and run while EdgeEver is open; Android and iOS native apps do not run plugins. > [!NOTE] > **Security Model**: Client plugins use a trusted-code model (similar to Obsidian). Enabling a plugin grants it trust to run within the client JavaScript environment and invoke plugin APIs. Capability declarations (`permissions`) in the manifest disclose the functional scope to users and do not enforce a hard sandbox. Theme packages are purely declarative JSON and execute no JavaScript. --- ## Table of Contents - [Plugin Manifest](#plugin-manifest) - [Fields](#fields) - [Available Permissions](#available-permissions) - [Plugin Entry & Lifecycle](#plugin-entry--lifecycle) - [API Reference](#api-reference) - [1. Notes & Notebooks](#1-notes--notebooks) - [2. Resources & Attachments](#2-resources--attachments) - [3. Editor & Embeds](#3-editor--embeds) - [4. UI, Commands & Panels](#4-ui-commands--panels) - [5. Settings](#5-settings) - [6. Storage & Secrets](#6-storage--secrets) - [7. Network Requests](#7-network-requests) - [8. Schedules (Desktop Only)](#8-schedules-desktop-only) - [9. Events](#9-events) - [Themes (No-Code Extensions)](#themes-no-code-extensions) - [Packaging, Distribution & Local Testing](#packaging-distribution--local-testing) - [Packaging Rules](#packaging-rules) - [GitHub Distribution](#github-distribution) - [Local Testing](#local-testing) --- ## Plugin Manifest Every plugin must supply a `manifest.json` at its root: ```json { "type": "plugin", "id": "com.example.recent-notes", "name": "Recent Notes", "version": "1.0.0", "apiVersion": "2", "settingsUi": "host", "description": "Shows recently updated notes.", "locales": { "zh-CN": { "name": "最近笔记", "description": "查看最近更新的笔记。" } }, "entry": "./main.js", "platforms": ["web", "desktop"], "permissions": ["notes:read", "editor:read", "ui:commands", "ui:notices", "ui:panels"] } ``` ### Fields | Field | Type | Required | Description | | :--- | :--- | :---: | :--- | | `type` | `"plugin"` | Yes | Fixed value: `"plugin"`. | | `id` | `string` | Yes | Globally unique plugin ID. Reverse-domain format recommended (e.g. `com.example.plugin`). | | `name` | `string` | Yes | Default display name. | | `version` | `string` | Yes | SemVer-compliant version string (e.g. `1.0.0`). | | `apiVersion` | `"2"` | Yes | Plugin API version; currently `"2"`. | | `settingsUi` | `"host"` | Yes | Settings UI renderer; currently `"host"` (rendered by host). | | `entry` | `string` | Yes | Entry script path; fixed to `./main.js` for GitHub distribution. | | `description` | `string` | No | Default description. | | `locales` | `object` | No | Localized metadata keyed by BCP 47 language tags (e.g. `zh-CN`, `en-US`), overriding `name` and `description`. | | `platforms` | `string[]` | No | Supported platforms: `"web"`, `"desktop"`. | | `permissions` | `string[]` | No | Declared capabilities disclosed to users during install and update. | | `settings` | `object` | No | Declarative settings schema rendered by host (detailed below). | ### Available Permissions Permissions are declared to transparently inform users of plugin capabilities: - **Notes & Content**: `notes:read`, `notes:write`, `notes:delete`, `templates:read`, `templates:write`, `metadata:read`, `metadata:write`, `resources:read`, `resources:write` - **Editor**: `editor:read`, `editor:write` - **UI & Interaction**: `ui:commands`, `ui:navigation`, `ui:notices`, `ui:panels`, `ui:embeds` - **Storage & System**: `storage`, `secrets`, `network`, `network:public`, `schedules` --- ## Plugin Entry & Lifecycle The plugin entry script (`main.js`) exports a default object containing an `activate` lifecycle function: ```js export default { activate(context) { // Register commands const disposeCommand = context.commands.register({ id: "hello-world", title: "Say Hello", run() { context.ui.showNotice("Hello from EdgeEver Plugin!"); } }); // Return cleanup callback (invoked on deactivation) return () => { disposeCommand(); }; } }; ``` TypeScript projects can use `@edgeever/plugin-api`: ```ts import { definePlugin } from "@edgeever/plugin-api"; export default definePlugin({ activate(context) { return context.commands.register({ id: "hello-world", title: "Say Hello", run: () => context.ui.showNotice("Hello!") }); } }); ``` > [!TIP] > Registration methods (commands, panels, events) return cleanup functions. When a plugin is deactivated, the host also automatically cleans up registered commands and event listeners. --- ## API Reference ### 1. Notes & Notebooks #### Querying Notes ```ts // 1. Lightweight summaries (ideal for lists and sidebars; max 200 items per page) const result = await context.notes.query({ notebookId: "optional-notebook-id", text: "keyword", tags: ["todo"], sort: "updated-desc", // "updated-desc" | "created-desc" | "title-asc" limit: 50, offset: 0 }); // Returns { notes: PluginNoteSummary[], totalCount: number, nextOffset: number | null } // 2. Query notes with full Markdown content (for linters, indexers, etc.) const fullNotes = await context.notes.queryContent({ tags: ["task"], limit: 20 }); ``` #### Note Operations ```ts // Get single note details const note = await context.notes.get(noteId); // Create note const newNote = await context.notes.create({ notebookId: "target-notebook-id", title: "New Note", contentMarkdown: "# Title\nBody text", tags: ["tag1"] }); // Update entire note await context.notes.update(noteId, { title: "Updated Title", contentMarkdown: "New body", tags: ["updated"] }); // Partial optimistic edit with concurrency check (revision & contentHash) await context.notes.editMarkdown(noteId, { expectedRevision: note.revision, expectedContentHash: note.contentHash, edits: [ { from: 0, to: 0, insert: "> Prepend quote\n\n" } // UTF-16 offsets, half-open interval [from, to) ] }); // Move, pin, and delete await context.notes.move([noteId], targetNotebookId); await context.notes.pin([noteId], true); await context.notes.delete(noteId, { permanent: false }); // Move to trash await context.notes.restore(noteId); // Restore from trash // Note revisions const revisions = await context.notes.revisions.list(noteId); await context.notes.revisions.restore(noteId, revisionId); ``` #### Notebooks & Tags ```ts // Notebooks const notebooks = await context.notebooks.list(); const nb = await context.notebooks.create({ name: "New Notebook", parentId: null }); await context.notebooks.update(nb.id, { name: "New Name" }); await context.notebooks.delete(nb.id); // Tags const tags = await context.tags.list(); await context.tags.rename("old-tag", "new-tag"); await context.tags.delete("unused-tag"); ``` --- ### 2. Resources & Attachments Manage images and file attachments associated with notes: ```ts // List note attachments const resources = await context.resources.list(noteId); // Read attachment Blob const blob = await context.resources.read(resourceId); // Upload new attachment const uploaded = await context.resources.upload(noteId, file); // Concurrency-checked update (file limit: 100 MiB) await context.resources.update(resourceId, { file: newFile, expectedContentHash: currentResource.contentHash }); // Rename and delete await context.resources.rename(resourceId, "document.pdf"); await context.resources.delete(resourceId); ``` --- ### 3. Editor & Embeds Interact with active editor sessions: ```ts // Get and replace selection const selection = await context.editor.getSelection(); if (selection && !selection.empty) { await context.editor.replaceSelection(selection.text.toUpperCase()); } // Insert at cursor await context.editor.insertAtCursor("- [ ] New task\n"); // Live edit in-memory document without losing unsaved changes const doc = await context.editor.getDocument(); if (doc) { await context.editor.editMarkdown([ { from: 0, to: 0, insert: "\n" } ]); } ``` #### Custom Block Embeds Register custom block-level embed renderers (serialized in Markdown as `edgeever-plugin-embed` code fences): ```ts // 1. Register embed renderer context.editor.embeds.register({ type: "my-diagram", async mount(container, embed) { const data = embed.data; container.innerHTML = `