diff --git a/apps/server/src/services/llm/skills/backend_scripting.md b/apps/server/src/services/llm/skills/backend_scripting.md new file mode 100644 index 0000000000..59ec934ece --- /dev/null +++ b/apps/server/src/services/llm/skills/backend_scripting.md @@ -0,0 +1,156 @@ +# Trilium Backend Scripting + +Backend scripts run in Node.js on the server. They have direct access to notes in memory and can interact with the system (files, processes). + +## Creating a backend script + +1. Create a Code note with language "JS backend". +2. The script can be run manually (Execute button) or triggered automatically. + +## Script API (`api` global) + +### Note retrieval +- `api.getNote(noteId)` - get note by ID +- `api.searchForNotes(query, searchParams)` - search notes (returns array) +- `api.searchForNote(query)` - search notes (returns first match) +- `api.getNotesWithLabel(name, value?)` - find notes by label +- `api.getNoteWithLabel(name, value?)` - find first note by label +- `api.getBranch(branchId)` - get branch by ID +- `api.getAttribute(attributeId)` - get attribute by ID + +### Note creation +- `api.createTextNote(parentNoteId, title, content)` - create text note +- `api.createDataNote(parentNoteId, title, content)` - create JSON note +- `api.createNewNote({ parentNoteId, title, content, type })` - create note with full options + +### Branch management +- `api.ensureNoteIsPresentInParent(noteId, parentNoteId, prefix?)` - create or reuse branch +- `api.ensureNoteIsAbsentFromParent(noteId, parentNoteId)` - remove branch if exists +- `api.toggleNoteInParent(present, noteId, parentNoteId, prefix?)` - toggle branch + +### Calendar/date notes +- `api.getTodayNote()` - get/create today's day note +- `api.getDayNote(date)` - get/create day note (YYYY-MM-DD) +- `api.getWeekNote(date)` - get/create week note +- `api.getMonthNote(date)` - get/create month note (YYYY-MM) +- `api.getYearNote(year)` - get/create year note (YYYY) + +### Utilities +- `api.log(message)` - log to Trilium logs and UI +- `api.randomString(length)` - generate random string +- `api.escapeHtml(string)` / `api.unescapeHtml(string)` +- `api.getInstanceName()` - get instance name +- `api.getAppInfo()` - get application info + +### Libraries +- `api.axios` - HTTP client +- `api.dayjs` - date manipulation +- `api.xml2js` - XML parser +- `api.cheerio` - HTML/XML parser + +### Advanced +- `api.transactional(func)` - wrap code in a database transaction +- `api.sql` - direct SQL access +- `api.sortNotes(parentNoteId, sortConfig)` - sort child notes +- `api.runOnFrontend(script, params)` - execute code on all connected frontends +- `api.backupNow(backupName)` - create a backup +- `api.exportSubtreeToZipFile(noteId, format, zipFilePath)` - export subtree (format: "markdown" or "html") +- `api.duplicateSubtree(origNoteId, newParentNoteId)` - clone note and children + +## BNote object + +Available on notes returned from API methods (`api.getNote()`, `api.originEntity`, etc.). + +### Content +- `note.getContent()` / `note.setContent(content)` +- `note.getJsonContent()` / `note.setJsonContent(obj)` +- `note.getJsonContentSafely()` - returns null on parse error + +### Properties +- `note.noteId`, `note.title`, `note.type`, `note.mime` +- `note.dateCreated`, `note.dateModified` +- `note.isProtected`, `note.isArchived` + +### Hierarchy +- `note.getParentNotes()` / `note.getChildNotes()` +- `note.getParentBranches()` / `note.getChildBranches()` +- `note.hasChildren()`, `note.getAncestors()` +- `note.getSubtreeNoteIds()` - all descendant IDs +- `note.hasAncestor(ancestorNoteId)` + +### Attributes (including inherited) +- `note.getLabels(name?)` / `note.getLabelValue(name)` +- `note.getRelations(name?)` / `note.getRelation(name)` +- `note.hasLabel(name, value?)` / `note.hasRelation(name, value?)` + +### Attribute modification +- `note.setLabel(name, value?)` / `note.removeLabel(name, value?)` +- `note.setRelation(name, targetNoteId)` / `note.removeRelation(name, value?)` +- `note.addLabel(name, value?, isInheritable?)` / `note.addRelation(name, targetNoteId, isInheritable?)` +- `note.toggleLabel(enabled, name, value?)` + +### Operations +- `note.save()` - persist changes +- `note.deleteNote()` - soft delete +- `note.cloneTo(parentNoteId)` - clone to another parent + +### Type checks +- `note.isJson()`, `note.isJavaScript()`, `note.isHtml()`, `note.isImage()` +- `note.hasStringContent()` - true if not binary + +## Events and triggers + +### Global events (via `#run` label on the script note) +- `#run=backendStartup` - run when server starts +- `#run=hourly` - run once per hour (use `#runAtHour=N` to specify which hours) +- `#run=daily` - run once per day + +### Entity events (via relation from the entity to the script note) +These are defined as relations. `api.originEntity` contains the entity that triggered the event. + +| Relation | Trigger | originEntity | +|---|---|---| +| `~runOnNoteCreation` | note created | BNote | +| `~runOnChildNoteCreation` | child note created under this note | BNote (child) | +| `~runOnNoteTitleChange` | note title changed | BNote | +| `~runOnNoteContentChange` | note content changed | BNote | +| `~runOnNoteChange` | note metadata changed (not content) | BNote | +| `~runOnNoteDeletion` | note deleted | BNote | +| `~runOnBranchCreation` | branch created (clone/move) | BBranch | +| `~runOnBranchChange` | branch updated | BBranch | +| `~runOnBranchDeletion` | branch deleted | BBranch | +| `~runOnAttributeCreation` | attribute created on this note | BAttribute | +| `~runOnAttributeChange` | attribute changed/deleted on this note | BAttribute | + +Relations can be inheritable — when set, they apply to all descendant notes. + +## Example: auto-color notes by category + +```javascript +// Attach via ~runOnAttributeChange relation +const attr = api.originEntity; +if (attr.name !== "mycategory") return; +const note = api.getNote(attr.noteId); +if (attr.value === "Health") { + note.setLabel("color", "green"); +} else { + note.removeLabel("color"); +} +``` + +## Example: create a daily summary + +```javascript +// Attach #run=daily label +const today = api.getTodayNote(); +const tasks = api.searchForNotes('#task #!completed'); +let summary = "## Open Tasks\n"; +for (const task of tasks) { + summary += `- ${task.title}\n`; +} +api.createTextNote(today.noteId, "Daily Summary", summary); +``` + +## Module system + +Child notes of a script act as modules. Export with `module.exports = ...` and import via function parameters matching the child note title, or use `require('noteName')`. diff --git a/apps/server/src/services/llm/skills/frontend_scripting.md b/apps/server/src/services/llm/skills/frontend_scripting.md new file mode 100644 index 0000000000..2d17e04e56 --- /dev/null +++ b/apps/server/src/services/llm/skills/frontend_scripting.md @@ -0,0 +1,197 @@ +# Trilium Frontend Scripting + +Frontend scripts run in the browser. They can manipulate the UI, navigate notes, show dialogs, and create custom widgets. + +## Creating a frontend script + +1. Create a Code note with language "JS frontend" (or "JSX" for Preact widgets). +2. Run manually (Execute button) or set `#run=frontendStartup` to auto-run on startup. +3. For mobile, use `#run=mobileStartup` instead. + +## Script types + +| Type | Description | Required attribute | +|---|---|---| +| Regular script | Runs with current app/note context | `#run=frontendStartup` (optional) | +| Custom widget | UI element in various positions | `#widget` | +| Launch bar widget | Button in the launch bar | `#widget` | +| Render note | Custom content inside a note | None (used via render relation) | + +## Script API (`api` global) + +### Navigation & tabs +- `api.activateNote(notePath)` - navigate to a note +- `api.activateNewNote(notePath)` - navigate and wait for sync +- `api.openTabWithNote(notePath, activate?)` - open in new tab +- `api.openSplitWithNote(notePath, activate?)` - open in new split +- `api.getActiveContextNote()` - get currently active note +- `api.getActiveContextNotePath()` - get path of active note +- `api.setHoistedNoteId(noteId)` - hoist/unhoist note in current tab + +### Note access & search +- `api.getNote(noteId)` - get note by ID +- `api.getNotes(noteIds)` - bulk fetch notes +- `api.searchForNotes(searchString)` - search with full query syntax +- `api.searchForNote(searchString)` - search returning first result +- `api.reloadNotes(noteIds)` - refresh cache from backend + +### Calendar/date notes +- `api.getTodayNote()` - get/create today's note +- `api.getDayNote(date)` - get/create day note for date +- `api.getWeekNote(date)` / `api.getMonthNote(month)` / `api.getYearNote(year)` + +### Editor access +- `api.getActiveContextTextEditor()` - get CKEditor instance +- `api.getActiveContextCodeEditor()` - get CodeMirror instance +- `api.addTextToActiveContextEditor(text)` - insert text into active editor + +### Dialogs & notifications +- `api.showMessage(msg)` - show info toast +- `api.showError(msg)` - show error toast +- `api.showInfoDialog(msg)` - show info dialog +- `api.showConfirmDialog(msg)` - show confirm dialog (returns boolean) +- `api.showPromptDialog(msg)` - show prompt dialog (returns user input) + +### Links +- `api.createLink(notePath, { title?, showTooltip?, showNoteIcon? })` - create jQuery link element + +### Backend integration +- `api.runOnBackend(func, params)` - execute a function on the backend (sync) + +### Protection +- `api.protectNote(noteId, protect)` - protect/unprotect note +- `api.protectSubTree(noteId, protect)` - protect/unprotect subtree + +### UI interaction +- `api.triggerCommand(name, data)` - trigger a command +- `api.triggerEvent(name, data)` - trigger an event +- `api.bindGlobalShortcut(shortcut, handler, namespace?)` - add keyboard shortcut + +### Utilities +- `api.formatDateISO(date)` - format as YYYY-MM-DD +- `api.randomString(length)` - generate random string +- `api.dayjs` - day.js library +- `api.log(message)` - log to script log pane + +### Widget base classes +- `api.BasicWidget` - base widget class +- `api.NoteContextAwareWidget` - widget aware of note context changes +- `api.RightPanelWidget` - right sidebar widget + +## FNote object + +Available via `api.getNote()`, `api.getActiveContextNote()`, etc. + +### Properties +- `note.noteId`, `note.title`, `note.type`, `note.mime` +- `note.isProtected`, `note.isArchived` + +### Content +- `note.getContent()` - get note content +- `note.getJsonContent()` - parse content as JSON + +### Hierarchy +- `note.getParentNotes()` / `note.getChildNotes()` +- `note.hasChildren()`, `note.getSubtreeNoteIds()` + +### Attributes +- `note.getAttributes(type?, name?)` - get all attributes (including inherited) +- `note.getOwnedAttributes(type?, name?)` - get only owned attributes +- `note.hasAttribute(type, name)` - check for attribute + +## Custom widgets (legacy jQuery) + +```javascript +class MyWidget extends api.BasicWidget { + get position() { return 1; } + get parentWidget() { return "center-pane"; } + + doRender() { + this.$widget = $("
"); + this.$widget.append($("") + .on("click", () => api.showMessage("Hello!"))); + return this.$widget; + } +} + +module.exports = new MyWidget(); +``` + +### Widget locations (`parentWidget` values) +- `left-pane` - alongside the note tree +- `center-pane` - in the content area, spanning all splits +- `note-detail-pane` - inside a note (split-aware, export class not instance, use static parentWidget) +- `right-pane` - in the right sidebar (use `RightPanelWidget`) + +### Note context aware widget + +```javascript +class MyWidget extends api.NoteContextAwareWidget { + static get parentWidget() { return "note-detail-pane"; } + get position() { return 100; } + + doRender() { + this.$widget = $("
"); + return this.$widget; + } + + async refreshWithNote(note) { + // Called when the active note changes + this.$widget.text(`Current note: ${note.title}`); + } +} + +module.exports = MyWidget; // Export class, not instance! +``` + +## Custom widgets (Preact JSX) + +Requires JSX language enabled in Options -> Code Notes. + +```jsx +import { defineWidget } from "trilium:preact"; +import { useState } from "trilium:preact"; + +export default defineWidget({ + parent: "center-pane", + position: 10, + render: () => { + const [count, setCount] = useState(0); + return ( +
+ +
+ ); + } +}); +``` + +### Preact imports +- `import { showMessage, getNote, ... } from "trilium:api"` - API methods +- `import { useState, useEffect, ... } from "trilium:preact"` - hooks +- `import { defineWidget, defineLauncherWidget } from "trilium:preact"` - widget helpers +- Built-in components: Button, ActionButton, Modal, NoteAutocomplete, FormTextBox, FormToggle, etc. + +## Example: launcher button + +```javascript +// Set #run=frontendStartup +api.createOrUpdateLauncher({ + id: "my-task-button", + type: "customWidget", + title: "New Task", + icon: "bx bx-task", + action: async () => { + const todayNote = await api.getTodayNote(); + await api.runOnBackend(async (parentNoteId) => { + api.createTextNote(parentNoteId, "New Task", ""); + }, [todayNote.noteId]); + } +}); +``` + +## Module system + +Child notes of a script act as modules. For JS frontend, use `module.exports` and function parameters. For JSX, use `import`/`export` syntax. diff --git a/apps/server/src/services/llm/skills/index.ts b/apps/server/src/services/llm/skills/index.ts index 11f04fef7b..ff22097111 100644 --- a/apps/server/src/services/llm/skills/index.ts +++ b/apps/server/src/services/llm/skills/index.ts @@ -23,6 +23,16 @@ const SKILLS: SkillDefinition[] = [ name: "search_syntax", description: "Trilium search query syntax reference — labels, relations, note properties, boolean logic, ordering, and more.", file: "search_syntax.md" + }, + { + name: "backend_scripting", + description: "Backend (Node.js) scripting API — creating notes, handling events, accessing entities, database operations, and automation.", + file: "backend_scripting.md" + }, + { + name: "frontend_scripting", + description: "Frontend (browser) scripting API — UI widgets, navigation, dialogs, editor access, Preact/JSX components, and keyboard shortcuts.", + file: "frontend_scripting.md" } ];