diff options
| author | Adam Malczewski <[email protected]> | 2026-03-24 00:25:03 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-03-24 00:25:03 +0900 |
| commit | e2c88087f3926ec477ea099fd771d1bc9d11d7c5 (patch) | |
| tree | be33c4cdbd1fa67464779e7cd834f002c6cae438 | |
| parent | dc2fa22c4d279199fb07a205a0c11eb155641f3d (diff) | |
| download | ai-pulse-obsidian-plugin-e2c88087f3926ec477ea099fd771d1bc9d11d7c5.tar.gz ai-pulse-obsidian-plugin-e2c88087f3926ec477ea099fd771d1bc9d11d7c5.zip | |
generate api docs
| -rw-r--r-- | .gitignore | 46 | ||||
| -rw-r--r-- | .rules/changelog/2026-03/23/01.md | 14 | ||||
| -rw-r--r-- | .rules/changelog/2026-03/24/01.md | 16 | ||||
| -rw-r--r-- | .rules/changelog/2026-03/24/02.md | 17 | ||||
| -rw-r--r-- | .rules/default/obsidian.md | 31 | ||||
| -rw-r--r-- | .rules/default/ollama.md | 22 | ||||
| -rw-r--r-- | .rules/docs/obsidian/editor-api.md | 166 | ||||
| -rw-r--r-- | .rules/docs/obsidian/events-utilities.md | 330 | ||||
| -rw-r--r-- | .rules/docs/obsidian/metadata-cache.md | 214 | ||||
| -rw-r--r-- | .rules/docs/obsidian/plugin-lifecycle.md | 168 | ||||
| -rw-r--r-- | .rules/docs/obsidian/ui-components.md | 349 | ||||
| -rw-r--r-- | .rules/docs/obsidian/vault-api.md | 301 | ||||
| -rw-r--r-- | .rules/docs/obsidian/workspace-api.md | 221 | ||||
| -rw-r--r-- | .rules/docs/ollama/chat.md | 171 | ||||
| -rw-r--r-- | .rules/docs/ollama/embed.md | 56 | ||||
| -rw-r--r-- | .rules/docs/ollama/generate.md | 121 | ||||
| -rw-r--r-- | .rules/docs/ollama/list-models.md | 56 | ||||
| -rw-r--r-- | .rules/docs/ollama/show-model.md | 43 | ||||
| -rw-r--r-- | .rules/docs/ollama/version.md | 19 |
19 files changed, 2339 insertions, 22 deletions
@@ -1,22 +1,24 @@ -# vscode
-.vscode
-
-# Intellij
-*.iml
-.idea
-
-# npm
-node_modules
-
-# Don't include the compiled main.js file in the repo.
-# They should be uploaded to GitHub releases instead.
-main.js
-
-# Exclude sourcemaps
-*.map
-
-# obsidian
-data.json
-
-# Exclude macOS Finder (System Explorer) View States
-.DS_Store
+# vscode +.vscode + +# Intellij +*.iml +.idea + +# npm +node_modules + +# Don't include the compiled main.js file in the repo. +# They should be uploaded to GitHub releases instead. +main.js + +# Exclude sourcemaps +*.map + +# obsidian +data.json + +# Exclude macOS Finder (System Explorer) View States +.DS_Store + +obsidian-developer-docs diff --git a/.rules/changelog/2026-03/23/01.md b/.rules/changelog/2026-03/23/01.md new file mode 100644 index 0000000..45d46b5 --- /dev/null +++ b/.rules/changelog/2026-03/23/01.md @@ -0,0 +1,14 @@ +# Changelog — 2026-03-23 — 01 + +## Added Ollama API documentation + +Created `.rules/docs/ollama/` with trimmed, agent-friendly API reference files: + +- `generate.md` — `POST /api/generate` (one-shot text generation) +- `chat.md` — `POST /api/chat` (conversational chat with tool calling) +- `embed.md` — `POST /api/embed` (vector embeddings) +- `list-models.md` — `GET /api/tags` (list locally available models) +- `show-model.md` — `POST /api/show` (model details and capabilities) +- `version.md` — `GET /api/version` (server version / health check) + +All files were distilled from the official Ollama OpenAPI docs, stripped of boilerplate, and formatted as concise reference tables with examples. diff --git a/.rules/changelog/2026-03/24/01.md b/.rules/changelog/2026-03/24/01.md new file mode 100644 index 0000000..b461bbd --- /dev/null +++ b/.rules/changelog/2026-03/24/01.md @@ -0,0 +1,16 @@ +# Changelog — 2026-03-24 #01 + +## Created Obsidian API documentation for AI note organizer + +### Files Added +- `.rules/docs/obsidian/README.md` — Compact AI-agent lookup table mapping tasks to doc files +- `.rules/docs/obsidian/plugin-lifecycle.md` — Plugin class, manifest, settings pattern (AI organizer focused) +- `.rules/docs/obsidian/vault-api.md` — Vault CRUD, FileManager, DataAdapter, AI batch/organize patterns +- `.rules/docs/obsidian/metadata-cache.md` — MetadataCache, CachedMetadata, AI-specific query patterns +- `.rules/docs/obsidian/workspace-api.md` — Workspace, leaves, custom views, MarkdownView +- `.rules/docs/obsidian/ui-components.md` — Commands, settings tab, modals, menus, notices, ribbon +- `.rules/docs/obsidian/editor-api.md` — Editor manipulation, markdown post processing +- `.rules/docs/obsidian/events-utilities.md` — Events, Platform, Ollama API patterns, HTTP, utilities + +### Purpose +Provide concise Obsidian plugin API reference for future AI agents building an AI-powered note management plugin with Ollama integration. Docs are self-contained — no need to reference the obsidian-developer-docs repo. diff --git a/.rules/changelog/2026-03/24/02.md b/.rules/changelog/2026-03/24/02.md new file mode 100644 index 0000000..9b3ff6f --- /dev/null +++ b/.rules/changelog/2026-03/24/02.md @@ -0,0 +1,17 @@ +# Changelog — 2026-03-24 #02 + +## Summary +Created Ollama API README and moved both doc index files to `.rules/default/`. + +## Changes + +### Added +- `.rules/default/ollama.md` — compact Ollama API doc index (new file) +- `.rules/default/obsidian.md` — Obsidian API doc index (moved from `.rules/docs/obsidian/README.md`) + +### Removed +- `.rules/docs/obsidian/README.md` — moved to `.rules/default/obsidian.md` +- `.rules/docs/ollama/README.md` — moved to `.rules/default/ollama.md` + +### Modified +- File path references in both index files updated from relative (e.g. `vault-api.md`) to full project-root-relative paths (e.g. `.rules/docs/obsidian/vault-api.md`) diff --git a/.rules/default/obsidian.md b/.rules/default/obsidian.md new file mode 100644 index 0000000..0330edd --- /dev/null +++ b/.rules/default/obsidian.md @@ -0,0 +1,31 @@ +# Obsidian API Docs — AI Note Organizer + +This plugin organizes notes via AI powered by Ollama. Docs are in `.rules/docs/obsidian/`. + +## Where to Look + +| Need to... | File | +|------------|------| +| Set up plugin class, `onload`/`onunload`, manifest.json | `.rules/docs/obsidian/plugin-lifecycle.md` | +| Load/save settings, `loadData()`/`saveData()` | `.rules/docs/obsidian/plugin-lifecycle.md` | +| Read/write/create/delete/rename files | `.rules/docs/obsidian/vault-api.md` | +| Modify frontmatter (`processFrontMatter`) | `.rules/docs/obsidian/vault-api.md` | +| Move files into folders, auto-update links | `.rules/docs/obsidian/vault-api.md` | +| Batch process notes, build note graph | `.rules/docs/obsidian/vault-api.md` | +| Query tags, links, headings, frontmatter from cache | `.rules/docs/obsidian/metadata-cache.md` | +| Find untagged/orphan notes, backlinks | `.rules/docs/obsidian/metadata-cache.md` | +| Collect vault-wide metadata summary | `.rules/docs/obsidian/metadata-cache.md` | +| React to file create/modify/delete/rename events | `.rules/docs/obsidian/vault-api.md` + `.rules/docs/obsidian/events-utilities.md` | +| Wait for metadata cache readiness | `.rules/docs/obsidian/metadata-cache.md` | +| Call Ollama API (generate, chat, embeddings, list models) | `.rules/docs/obsidian/events-utilities.md` | +| Handle HTTP requests (`requestUrl`) | `.rules/docs/obsidian/events-utilities.md` | +| Create custom sidebar/panel views | `.rules/docs/obsidian/workspace-api.md` | +| Open/navigate files in workspace leaves | `.rules/docs/obsidian/workspace-api.md` | +| Register commands (global, editor, conditional) | `.rules/docs/obsidian/ui-components.md` | +| Build settings UI (text, toggle, dropdown, etc.) | `.rules/docs/obsidian/ui-components.md` | +| Show modals, suggest modals, fuzzy search | `.rules/docs/obsidian/ui-components.md` | +| Context menus, notices, ribbon, status bar | `.rules/docs/obsidian/ui-components.md` | +| Manipulate editor content (cursor, selection, insert) | `.rules/docs/obsidian/editor-api.md` | +| Custom markdown rendering / code blocks | `.rules/docs/obsidian/editor-api.md` | +| Detect platform (desktop/mobile/OS) | `.rules/docs/obsidian/events-utilities.md` | +| Use `moment.js`, debounce, protocol handlers | `.rules/docs/obsidian/events-utilities.md` | diff --git a/.rules/default/ollama.md b/.rules/default/ollama.md new file mode 100644 index 0000000..cf6f962 --- /dev/null +++ b/.rules/default/ollama.md @@ -0,0 +1,22 @@ +# Ollama API Docs — AI Note Organizer + +Local LLM inference via Ollama (`http://localhost:11434`). Docs are in `.rules/docs/ollama/`. + +## Where to Look + +| Need to... | File | +|------------|------| +| One-shot text completion, prompt/suffix, system prompt | `.rules/docs/ollama/generate.md` | +| ModelOptions (temperature, top_k, top_p, seed, num_ctx, stop, etc.) | `.rules/docs/ollama/generate.md` | +| Structured output via `format` (JSON / JSON schema) | `.rules/docs/ollama/generate.md` | +| Load / unload a model (`keep_alive`) | `.rules/docs/ollama/generate.md` | +| Streaming vs non-streaming response handling | `.rules/docs/ollama/generate.md` | +| Thinking / reasoning traces (`think` param) | `.rules/docs/ollama/generate.md` | +| Multi-turn conversation (chat history, roles) | `.rules/docs/ollama/chat.md` | +| Tool / function calling (define tools, handle tool_calls) | `.rules/docs/ollama/chat.md` | +| ChatMessage, ToolDefinition, ToolCall schemas | `.rules/docs/ollama/chat.md` | +| Generate vector embeddings from text | `.rules/docs/ollama/embed.md` | +| Batch embed multiple strings at once | `.rules/docs/ollama/embed.md` | +| List locally available models | `.rules/docs/ollama/list-models.md` | +| Get model details (parameters, template, license) | `.rules/docs/ollama/show-model.md` | +| Check Ollama server version / health | `.rules/docs/ollama/version.md` | diff --git a/.rules/docs/obsidian/editor-api.md b/.rules/docs/obsidian/editor-api.md new file mode 100644 index 0000000..8db6463 --- /dev/null +++ b/.rules/docs/obsidian/editor-api.md @@ -0,0 +1,166 @@ +# Obsidian Editor API + +## Editor Class + +Access via `MarkdownView.editor` or through editor commands. Abstracts CodeMirror 5/6. + +```typescript +// Get editor from active view +const view = this.app.workspace.getActiveViewOfType(MarkdownView); +if (view) { + const editor = view.editor; + // ... +} + +// Or use editorCallback in commands +this.addCommand({ + id: 'my-cmd', + name: 'My Command', + editorCallback: (editor: Editor, ctx: MarkdownView | MarkdownFileInfo) => { + // editor is available here + }, +}); +``` + +### Cursor & Selection + +| Method | Returns | Description | +|--------|---------|-------------| +| `getCursor(side?)` | `EditorPosition` | Get cursor position. `side`: `'from'`, `'to'`, `'head'`, `'anchor'` | +| `setCursor(pos)` | `void` | Set cursor position | +| `getSelection()` | `string` | Get selected text | +| `setSelection(anchor, head?)` | `void` | Set selection range | +| `setSelections(ranges, main?)` | `void` | Set multiple selections | +| `listSelections()` | `EditorSelection[]` | Get all selections | +| `somethingSelected()` | `boolean` | Check if anything is selected | +| `wordAt(pos)` | `EditorRange \| null` | Get word boundaries at position | + +### EditorPosition + +```typescript +interface EditorPosition { + line: number; // 0-based line number + ch: number; // 0-based character offset in line +} +``` + +### Reading Content + +| Method | Returns | Description | +|--------|---------|-------------| +| `getValue()` | `string` | Entire document content | +| `getLine(line)` | `string` | Text at line (0-indexed) | +| `getRange(from, to)` | `string` | Text in range | +| `lineCount()` | `number` | Total lines | +| `lastLine()` | `number` | Last line number | + +### Modifying Content + +| Method | Description | +|--------|-------------| +| `setValue(content)` | Replace entire document | +| `setLine(n, text)` | Replace line content | +| `replaceRange(text, from, to?, origin?)` | Replace text in range. If only `from`, inserts at position. | +| `replaceSelection(text, origin?)` | Replace current selection | +| `transaction(tx, origin?)` | Batch multiple changes atomically | +| `processLines(read, write, ignoreEmpty?)` | Process each line | + +### Navigation + +| Method | Description | +|--------|-------------| +| `scrollTo(x, y)` | Scroll to position | +| `scrollIntoView(range, center?)` | Scroll range into view | +| `getScrollInfo()` | Get current scroll info | +| `posToOffset(pos)` | Convert position to character offset | +| `offsetToPos(offset)` | Convert character offset to position | + +### Other + +| Method | Description | +|--------|-------------| +| `focus()` / `blur()` | Focus/blur editor | +| `hasFocus()` | Check if editor has focus | +| `undo()` / `redo()` | Undo/redo | +| `exec(command)` | Execute an editor command | +| `refresh()` | Refresh editor display | + +### Common Patterns + +**Insert at cursor:** +```typescript +editor.replaceRange(text, editor.getCursor()); +``` + +**Replace selection:** +```typescript +const sel = editor.getSelection(); +editor.replaceSelection(sel.toUpperCase()); +``` + +**Insert at end of document:** +```typescript +const lastLine = editor.lastLine(); +const lastLineText = editor.getLine(lastLine); +editor.replaceRange('\nNew content', { line: lastLine, ch: lastLineText.length }); +``` + +## Markdown Post Processing + +Change how markdown renders in Reading view. + +### Post Processor + +```typescript +this.registerMarkdownPostProcessor((element: HTMLElement, context: MarkdownPostProcessorContext) => { + // element = rendered HTML fragment + // Modify DOM as needed + const codeblocks = element.findAll('code'); + for (const block of codeblocks) { + // Transform code blocks + } +}); +``` + +### Code Block Processor + +Register custom fenced code block handlers (like Mermaid): + +```typescript +this.registerMarkdownCodeBlockProcessor('csv', (source: string, el: HTMLElement, ctx) => { + // source = raw text inside the code block + // el = container div (replaces <pre><code>) + const rows = source.split('\n').filter(r => r.length > 0); + const table = el.createEl('table'); + // Build table from CSV data... +}); +``` + +Usage in markdown: +```` +```csv +Name,Age +Alice,30 +Bob,25 +``` +```` + +## MarkdownRenderer + +Static utility for rendering markdown to HTML: + +```typescript +import { MarkdownRenderer } from 'obsidian'; + +// Render markdown string to element +await MarkdownRenderer.render( + this.app, // App instance + '**bold** text', // Markdown string + containerEl, // Target HTMLElement + sourcePath, // Source file path (for link resolution) + this // Component (for lifecycle management) +); + +// Alternative (older API) +await MarkdownRenderer.renderMarkdown(markdown, el, sourcePath, component); +``` diff --git a/.rules/docs/obsidian/events-utilities.md b/.rules/docs/obsidian/events-utilities.md new file mode 100644 index 0000000..2fa3a9e --- /dev/null +++ b/.rules/docs/obsidian/events-utilities.md @@ -0,0 +1,330 @@ +# Obsidian Events, Utilities & Platform + +## Events System + +Many Obsidian classes extend `Events` (Vault, MetadataCache, Workspace, WorkspaceLeaf). + +### Events Base Class + +| Method | Description | +|--------|-------------| +| `on(name, callback, ctx?)` | Subscribe to event. Returns `EventRef`. | +| `off(name, callback)` | Unsubscribe by callback | +| `offref(ref)` | Unsubscribe by `EventRef` | +| `trigger(name, ...data)` | Emit event | + +### Registering Events in Plugins + +**Always** use `this.registerEvent()` to auto-detach on unload: + +```typescript +this.registerEvent( + this.app.vault.on('create', (file) => { + console.log('File created:', file.path); + }) +); +``` + +### Timing / Intervals + +Use `this.registerInterval()` for auto-cleanup: + +```typescript +this.registerInterval( + window.setInterval(() => this.doPeriodicTask(), 60000) +); +``` + +Use `window.setInterval` (not plain `setInterval`) to avoid TypeScript NodeJS/Browser confusion. + +### DOM Events + +```typescript +this.registerDomEvent(document, 'click', (evt: MouseEvent) => { + // Auto-detached on unload +}); +``` + +## Platform Detection + +```typescript +import { Platform } from 'obsidian'; + +Platform.isDesktop // true on desktop +Platform.isMobile // true on mobile +Platform.isDesktopApp // true on desktop app +Platform.isMobileApp // true on mobile app +Platform.isIosApp // iOS +Platform.isAndroidApp // Android +Platform.isPhone // phone form factor +Platform.isTablet // tablet form factor +Platform.isMacOS // macOS +Platform.isWin // Windows +Platform.isLinux // Linux +Platform.isSafari // Safari engine +``` + +## HTTP Requests + +Use `requestUrl` for cross-platform HTTP (bypasses CORS): + +```typescript +import { requestUrl, RequestUrlParam, RequestUrlResponse } from 'obsidian'; + +const response: RequestUrlResponse = await requestUrl({ + url: 'https://api.example.com/data', + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ key: 'value' }), + contentType: 'application/json', + throw: true, // throw on 400+ status (default: true) +}); + +// Response properties: +response.status; // number +response.headers; // Record<string, string> +response.text; // string +response.json; // any (parsed JSON) +response.arrayBuffer; // ArrayBuffer +``` + +### RequestUrlParam + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `url` | `string` | Yes | Request URL | +| `method` | `string` | No | HTTP method (GET, POST, etc.) | +| `headers` | `Record<string, string>` | No | Request headers | +| `body` | `string \| ArrayBuffer` | No | Request body | +| `contentType` | `string` | No | Content-Type header shorthand | +| `throw` | `boolean` | No | Throw on 400+ (default `true`) | + +## Moment.js + +Obsidian bundles Moment.js. Import directly: + +```typescript +import { moment } from 'obsidian'; + +const now = moment(); +const formatted = now.format('YYYY-MM-DD HH:mm:ss'); +const yesterday = moment().subtract(1, 'day'); +``` + +## Debouncing + +```typescript +import { debounce } from 'obsidian'; + +const debouncedSave = debounce((data: string) => { + // Save logic +}, 1000, true); // fn, wait ms, immediate? +``` + +Debouncer interface methods: `cancel()`, `run()` (flush immediately). + +## Obsidian Protocol Handler + +Handle `obsidian://` URLs: + +```typescript +this.registerObsidianProtocolHandler('my-action', (params) => { + // params: ObsidianProtocolData (Record<string, string>) + // URL: obsidian://my-action?param1=value1¶m2=value2 + console.log(params.param1); +}); +``` + +## Secret Storage + +Secure credential storage (v1.11.4): + +```typescript +const storage = this.app.secretStorage; + +// Store a secret +await storage.set('api-key', 'sk-...'); + +// Retrieve a secret +const key = await storage.get('api-key'); + +// Delete a secret +await storage.delete('api-key'); +``` + +## Ollama Communication Patterns + +The AI note organizer communicates with a local Ollama instance via its REST API. + +### Ollama API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `http://localhost:11434/api/generate` | POST | Single prompt completion | +| `http://localhost:11434/api/chat` | POST | Multi-turn chat completion | +| `http://localhost:11434/api/tags` | GET | List available models | +| `http://localhost:11434/api/embeddings` | POST | Generate text embeddings | +| `http://localhost:11434/api/show` | POST | Get model info | + +### Generate Request + +```typescript +import { requestUrl } from 'obsidian'; + +async function ollamaGenerate( + ollamaUrl: string, + model: string, + prompt: string, + system?: string +): Promise<string> { + const response = await requestUrl({ + url: `${ollamaUrl}/api/generate`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + prompt, + system, + stream: false, // IMPORTANT: set false for non-streaming + }), + }); + return response.json.response; +} +``` + +### Chat Request + +```typescript +interface OllamaMessage { + role: 'system' | 'user' | 'assistant'; + content: string; +} + +async function ollamaChat( + ollamaUrl: string, + model: string, + messages: OllamaMessage[] +): Promise<string> { + const response = await requestUrl({ + url: `${ollamaUrl}/api/chat`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + messages, + stream: false, + }), + }); + return response.json.message.content; +} +``` + +### List Models + +```typescript +async function ollamaListModels(ollamaUrl: string): Promise<string[]> { + const response = await requestUrl({ + url: `${ollamaUrl}/api/tags`, + method: 'GET', + }); + return response.json.models.map((m: any) => m.name); +} +``` + +### Generate Embeddings + +```typescript +async function ollamaEmbed( + ollamaUrl: string, + model: string, + input: string +): Promise<number[]> { + const response = await requestUrl({ + url: `${ollamaUrl}/api/embeddings`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model, prompt: input }), + }); + return response.json.embedding; +} +``` + +### Error Handling + +```typescript +try { + const result = await ollamaGenerate(url, model, prompt); +} catch (err) { + if (err.status === 0 || err.message?.includes('net')) { + new Notice('Cannot connect to Ollama. Is it running?'); + } else { + new Notice(`Ollama error: ${err.message}`); + } +} +``` + +### Settings Pattern for Ollama + +```typescript +interface AISettings { + ollamaUrl: string; // default: 'http://localhost:11434' + model: string; // default: 'llama3.2' + embeddingModel: string; // default: 'nomic-embed-text' + autoProcess: boolean; + systemPrompt: string; +} +``` + +### Note Analysis Pattern + +```typescript +async function analyzeNote(app: App, settings: AISettings, file: TFile) { + const content = await app.vault.cachedRead(file); + const cache = app.metadataCache.getFileCache(file); + + const prompt = `Analyze this note and suggest tags, related topics, and a brief summary. + +Title: ${file.basename} +Existing tags: ${cache?.frontmatter?.tags?.join(', ') || 'none'} +Content: +${content}`; + + const result = await ollamaGenerate(settings.ollamaUrl, settings.model, prompt); + + // Parse AI response and update frontmatter + const parsed = JSON.parse(result); // assuming structured output + await app.fileManager.processFrontMatter(file, (fm) => { + fm.tags = parsed.tags; + fm.summary = parsed.summary; + fm.aiProcessed = new Date().toISOString(); + }); +} +``` + +## Useful Import Summary + +```typescript +import { + // Core + App, Plugin, Component, PluginManifest, + // Files + TFile, TFolder, TAbstractFile, Vault, FileManager, + // Metadata + MetadataCache, CachedMetadata, FrontMatterCache, + // Workspace + Workspace, WorkspaceLeaf, View, ItemView, MarkdownView, + // UI + Modal, SuggestModal, FuzzySuggestModal, + Setting, PluginSettingTab, + Menu, MenuItem, Notice, + // Editor + Editor, EditorPosition, EditorRange, MarkdownRenderer, + // Events + Events, EventRef, + // Utilities + Platform, moment, debounce, requestUrl, + // Types + Command, Hotkey, IconName, ViewCreator, + RequestUrlParam, RequestUrlResponse, +} from 'obsidian'; +``` diff --git a/.rules/docs/obsidian/metadata-cache.md b/.rules/docs/obsidian/metadata-cache.md new file mode 100644 index 0000000..8ff5cf9 --- /dev/null +++ b/.rules/docs/obsidian/metadata-cache.md @@ -0,0 +1,214 @@ +# Obsidian MetadataCache API + +Access via `this.app.metadataCache`. Extends `Events`. + +The MetadataCache provides parsed/indexed metadata for all vault files without reading file content directly. + +## Terminology + +- **Linktext**: Internal link with path + subpath, e.g., `My note#Heading` +- **Linkpath / path**: The path portion of a linktext +- **Subpath**: The heading/block ID portion of a linktext + +## Properties + +| Property | Type | Description | +|----------|------|-------------| +| `resolvedLinks` | `Record<string, Record<string, number>>` | Map: source path → { dest path → link count }. All paths are vault-absolute. | +| `unresolvedLinks` | `Record<string, Record<string, number>>` | Map: source path → { unknown dest → count } | + +## Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `getFileCache(file: TFile)` | `CachedMetadata \| null` | Get cached metadata for a file | +| `getCache(path: string)` | `CachedMetadata \| null` | Get cached metadata by path (v0.14.5) | +| `getFirstLinkpathDest(linkpath, sourcePath)` | `TFile \| null` | Resolve a link path to a file (best match). (v0.12.5) | +| `fileToLinktext(file, sourcePath, omitMdExtension?)` | `string` | Generate linktext for a file. Uses filename if unique, full path otherwise. | + +## MetadataCache Events + +| Event | Callback Args | Description | +|-------|--------------|-------------| +| `'changed'` | `(file: TFile, data: string, cache: CachedMetadata)` | File indexed, cache updated. **Not fired on rename** — use vault `rename` event. | +| `'deleted'` | `(file: TFile, prevCache: CachedMetadata \| null)` | File deleted. Previous cache provided best-effort. | +| `'resolve'` | `(file: TFile)` | File's links resolved in `resolvedLinks`/`unresolvedLinks`. | +| `'resolved'` | `()` | All files resolved. Fires after initial load and each subsequent modification. | + +```typescript +// Example: React to metadata changes +this.registerEvent( + this.app.metadataCache.on('changed', (file, data, cache) => { + if (cache.frontmatter?.tags?.includes('important')) { + console.log('Important file changed:', file.path); + } + }) +); +``` + +## CachedMetadata Interface + +Returned by `getFileCache()` / `getCache()`. All properties are optional. + +| Property | Type | Description | +|----------|------|-------------| +| `frontmatter` | `FrontMatterCache` | Parsed YAML frontmatter as key-value object | +| `frontmatterPosition` | `Pos` | Position of frontmatter block in file (v1.4.0) | +| `frontmatterLinks` | `FrontmatterLinkCache[]` | Links found in frontmatter (v1.4.0) | +| `headings` | `HeadingCache[]` | All headings | +| `links` | `LinkCache[]` | All internal `[[links]]` | +| `embeds` | `EmbedCache[]` | All `![[embeds]]` | +| `tags` | `TagCache[]` | All inline `#tags` | +| `listItems` | `ListItemCache[]` | All list items | +| `sections` | `SectionCache[]` | Root-level markdown blocks | +| `blocks` | `Record<string, BlockCache>` | Named blocks (by block ID) | +| `footnotes` | `FootnoteCache[]` | Footnote definitions (v1.6.6) | +| `footnoteRefs` | `FootnoteRefCache[]` | Footnote references (v1.8.7) | +| `referenceLinks` | `ReferenceLinkCache[]` | Reference-style links (v1.8.7) | + +### FrontMatterCache + +A plain object with YAML frontmatter keys as properties. Access properties directly: + +```typescript +const cache = this.app.metadataCache.getFileCache(file); +if (cache?.frontmatter) { + const title = cache.frontmatter.title; + const tags = cache.frontmatter.tags; // string[] + const aliases = cache.frontmatter.aliases; // string[] +} +``` + +### HeadingCache + +| Property | Type | Description | +|----------|------|-------------| +| `heading` | `string` | Heading text | +| `level` | `number` | 1-6 | +| `position` | `Pos` | Position in file | + +### LinkCache + +| Property | Type | Description | +|----------|------|-------------| +| `link` | `string` | Link destination | +| `displayText` | `string?` | Display text if different (from `[[page|display]]`) | +| `original` | `string` | Raw text as written in document | +| `position` | `Pos` | Position in file | + +### TagCache + +| Property | Type | Description | +|----------|------|-------------| +| `tag` | `string` | Tag including `#` prefix (e.g., `"#mytag"`) | +| `position` | `Pos` | Position in file | + +### Pos (Position) + +```typescript +interface Pos { + start: Loc; // { line: number, col: number, offset: number } + end: Loc; +} +``` + +### FrontMatterInfo + +Returned by `getFrontMatterInfo(content: string)` standalone function: + +| Property | Type | Description | +|----------|------|-------------| +| `exists` | `boolean` | Whether frontmatter block exists | +| `frontmatter` | `string` | String content of frontmatter | +| `from` | `number` | Start offset (excluding `---`) | +| `to` | `number` | End offset (excluding `---`) | +| `contentStart` | `number` | Offset where frontmatter block ends (including `---`) | + +## Common Patterns + +### Get all tags for a file +```typescript +const cache = this.app.metadataCache.getFileCache(file); +const inlineTags = cache?.tags?.map(t => t.tag) ?? []; +const fmTags = cache?.frontmatter?.tags ?? []; +const allTags = [...inlineTags, ...fmTags]; +``` + +### Find all files linking to a file +```typescript +function getBacklinks(app: App, filePath: string): string[] { + const backlinks: string[] = []; + for (const [source, links] of Object.entries(app.metadataCache.resolvedLinks)) { + if (links[filePath]) { + backlinks.push(source); + } + } + return backlinks; +} +``` + +### Resolve a link +```typescript +const targetFile = this.app.metadataCache.getFirstLinkpathDest('My Note', currentFile.path); +if (targetFile) { + const content = await this.app.vault.cachedRead(targetFile); +} +``` + +### Collect all vault metadata for AI context +```typescript +function collectVaultSummary(app: App): { path: string; tags: string[]; links: string[]; headings: string[] }[] { + return app.vault.getMarkdownFiles().map(file => { + const cache = app.metadataCache.getFileCache(file); + return { + path: file.path, + tags: [ + ...(cache?.tags?.map(t => t.tag) ?? []), + ...(cache?.frontmatter?.tags ?? []), + ], + links: cache?.links?.map(l => l.link) ?? [], + headings: cache?.headings?.map(h => h.heading) ?? [], + }; + }); +} +``` + +### Find untagged notes (candidates for AI tagging) +```typescript +function getUntaggedNotes(app: App): TFile[] { + return app.vault.getMarkdownFiles().filter(file => { + const cache = app.metadataCache.getFileCache(file); + const inlineTags = cache?.tags ?? []; + const fmTags = cache?.frontmatter?.tags ?? []; + return inlineTags.length === 0 && fmTags.length === 0; + }); +} +``` + +### Find orphan notes (no inbound or outbound links) +```typescript +function getOrphanNotes(app: App): TFile[] { + const { resolvedLinks } = app.metadataCache; + const linked = new Set<string>(); + for (const [source, targets] of Object.entries(resolvedLinks)) { + if (Object.keys(targets).length > 0) linked.add(source); + for (const dest of Object.keys(targets)) linked.add(dest); + } + return app.vault.getMarkdownFiles().filter(f => !linked.has(f.path)); +} +``` + +### Wait for metadata cache to be ready +```typescript +// In onload(), ensure all metadata is indexed before processing: +if (this.app.metadataCache.resolvedLinks) { + // Already resolved + this.startProcessing(); +} else { + this.registerEvent( + this.app.metadataCache.on('resolved', () => { + this.startProcessing(); + }) + ); +} +``` diff --git a/.rules/docs/obsidian/plugin-lifecycle.md b/.rules/docs/obsidian/plugin-lifecycle.md new file mode 100644 index 0000000..d6adaa6 --- /dev/null +++ b/.rules/docs/obsidian/plugin-lifecycle.md @@ -0,0 +1,168 @@ +# Obsidian Plugin Lifecycle & Structure + +## manifest.json + +Every plugin requires a `manifest.json` in the plugin root: + +```json +{ + "id": "my-plugin-id", + "name": "Display Name", + "version": "1.0.0", + "minAppVersion": "0.15.0", + "description": "What the plugin does", + "author": "Author Name", + "authorUrl": "https://example.com", + "isDesktopOnly": false, + "fundingUrl": "https://buymeacoffee.com" +} +``` + +- `id` (string, required): Globally unique. Cannot contain `obsidian`. Must match plugin folder name for local dev. +- `name` (string, required): Display name. +- `version` (string, required): Semantic versioning `x.y.z`. +- `minAppVersion` (string, required): Minimum Obsidian version. +- `description` (string, required): Plugin description. +- `isDesktopOnly` (boolean, required): Set `true` if using NodeJS/Electron APIs. +- `fundingUrl` (string|object, optional): Single URL string or `{ "Label": "url" }` object. + +## Plugin Class + +```typescript +import { Plugin } from 'obsidian'; + +export default class MyPlugin extends Plugin { + async onload() { + // Called when plugin is enabled. Register all resources here. + } + + async onunload() { + // Called when plugin is disabled. Release all resources. + } +} +``` + +`Plugin extends Component`. Access `this.app` (App) and `this.manifest` (PluginManifest). + +### Key Plugin Methods + +| Method | Description | +|--------|-------------| +| `onload()` | Setup: register commands, views, events, settings | +| `onunload()` | Cleanup: runs automatically for registered resources | +| `onUserEnable()` | Called when user explicitly enables plugin. Safe to open views here. (v1.7.2) | +| `loadData(): Promise<any>` | Load `data.json` from plugin folder | +| `saveData(data: any): Promise<void>` | Save to `data.json` in plugin folder | +| `onExternalSettingsChange()` | Called when `data.json` modified externally (e.g., Sync). (v1.5.7) | +| `addCommand(command)` | Register global command (auto-prefixed with plugin id/name) | +| `addRibbonIcon(icon, title, callback)` | Add icon to left sidebar ribbon | +| `addStatusBarItem(): HTMLElement` | Add status bar item (desktop only) | +| `addSettingTab(tab)` | Register settings tab | +| `registerView(type, factory)` | Register custom view type | +| `registerEvent(eventRef)` | Register event (auto-detached on unload) | +| `registerDomEvent(el, type, cb)` | Register DOM event (auto-detached on unload) | +| `registerInterval(id)` | Register interval (auto-cancelled on unload). Use `window.setInterval()`. | +| `registerMarkdownPostProcessor(fn, sortOrder)` | Register reading-mode post processor | +| `registerMarkdownCodeBlockProcessor(lang, handler)` | Register custom code block handler | +| `registerEditorExtension(extension)` | Register CodeMirror 6 extension | +| `registerEditorSuggest(suggest)` | Register live typing suggestions (v0.12.7) | +| `registerObsidianProtocolHandler(action, handler)` | Handle `obsidian://` URLs | +| `registerExtensions(exts, viewType)` | Register file extensions for a view type | +| `removeCommand(commandId)` | Dynamically remove a command (v1.7.2) | + +## Component Class (Base) + +`Plugin` extends `Component`. All Component methods are available: + +| Method | Description | +|--------|-------------| +| `addChild(component)` | Add child component (loaded if parent loaded) | +| `removeChild(component)` | Remove and unload child component | +| `register(cb)` | Register cleanup callback for unload | +| `load()` / `unload()` | Manually control lifecycle | + +## App Object + +Access via `this.app` in Plugin or any View. Core properties: + +| Property | Type | Description | +|----------|------|-------------| +| `vault` | `Vault` | File/folder operations | +| `workspace` | `Workspace` | UI layout, leaves, views | +| `metadataCache` | `MetadataCache` | Cached file metadata (links, tags, frontmatter) | +| `fileManager` | `FileManager` | High-level file operations (rename with link updates, frontmatter) | +| `keymap` | `Keymap` | Keyboard shortcut management | +| `scope` | `Scope` | Current keyboard scope | +| `secretStorage` | `SecretStorage` | Secure storage for secrets (v1.11.4) | + +### App Methods + +- `isDarkMode(): boolean` - Check if dark mode is active (v1.10.0) +- `loadLocalStorage(key): string | null` - Load vault-specific localStorage value +- `saveLocalStorage(key, data)` - Save vault-specific localStorage value. Pass `null` to clear. + +## Settings Pattern (AI Note Organizer) + +```typescript +interface AIOrganizerSettings { + ollamaUrl: string; + model: string; + embeddingModel: string; + autoProcessOnCreate: boolean; + autoProcessOnModify: boolean; + excludeFolders: string[]; + systemPrompt: string; + batchSize: number; +} + +const DEFAULT_SETTINGS: Partial<AIOrganizerSettings> = { + ollamaUrl: 'http://localhost:11434', + model: 'llama3.2', + embeddingModel: 'nomic-embed-text', + autoProcessOnCreate: false, + autoProcessOnModify: false, + excludeFolders: ['.obsidian', 'templates'], + systemPrompt: 'You are a note organization assistant.', + batchSize: 10, +}; + +export default class AIOrganizer extends Plugin { + settings: AIOrganizerSettings; + + async onload() { + await this.loadSettings(); + this.addSettingTab(new AIOrganizerSettingTab(this.app, this)); + + // Register commands for AI operations + this.addCommand({ + id: 'analyze-current-note', + name: 'Analyze current note with AI', + editorCallback: async (editor, ctx) => { /* ... */ }, + }); + + this.addCommand({ + id: 'batch-organize', + name: 'Batch organize all notes', + callback: async () => { /* ... */ }, + }); + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + } +} +``` + +**Warning**: `Object.assign` does shallow copy. Deep copy nested objects (like `excludeFolders` array) manually. + +## File Structure + +A typical plugin folder in `.obsidian/plugins/<plugin-id>/`: +- `manifest.json` - Plugin metadata +- `main.js` - Compiled plugin code +- `styles.css` - Optional CSS styles +- `data.json` - Auto-created by `saveData()` diff --git a/.rules/docs/obsidian/ui-components.md b/.rules/docs/obsidian/ui-components.md new file mode 100644 index 0000000..1187e08 --- /dev/null +++ b/.rules/docs/obsidian/ui-components.md @@ -0,0 +1,349 @@ +# Obsidian UI Components + +## Commands + +Register via `this.addCommand()` in `onload()`. Command `id` and `name` are auto-prefixed with plugin id/name. + +### Command Interface + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `id` | `string` | Yes | Globally unique ID | +| `name` | `string` | Yes | Human-friendly name for command palette | +| `icon` | `IconName` | No | Icon for toolbar | +| `hotkeys` | `Hotkey[]` | No | Default hotkeys. Avoid in shared plugins. | +| `repeatable` | `boolean` | No | Repeat on held hotkey | +| `mobileOnly` | `boolean` | No | Mobile-only command | + +### Callback Types (mutually exclusive, pick one) + +**`callback`**: Simple global command. +```typescript +this.addCommand({ + id: 'my-command', + name: 'My Command', + callback: () => { /* do something */ }, +}); +``` + +**`checkCallback`**: Conditional command. Called twice: first with `checking=true` (return `true` if available, `false` to hide), then `checking=false` to execute. +```typescript +this.addCommand({ + id: 'my-command', + name: 'My Command', + checkCallback: (checking: boolean) => { + const canRun = someCondition(); + if (canRun) { + if (!checking) { doAction(); } + return true; + } + return false; + }, +}); +``` + +**`editorCallback`**: Only available when editor is active. Overrides `callback`/`checkCallback`. +```typescript +this.addCommand({ + id: 'my-command', + name: 'My Command', + editorCallback: (editor: Editor, ctx: MarkdownView | MarkdownFileInfo) => { + const selection = editor.getSelection(); + editor.replaceSelection(selection.toUpperCase()); + }, +}); +``` + +**`editorCheckCallback`**: Conditional editor command. Overrides all others. + +### Hotkey Format +```typescript +hotkeys: [{ modifiers: ['Mod', 'Shift'], key: 'a' }] +// 'Mod' = Ctrl on Windows/Linux, Cmd on macOS +``` + +## Settings Tab + +```typescript +import { App, PluginSettingTab, Setting } from 'obsidian'; + +export class MySettingTab extends PluginSettingTab { + plugin: MyPlugin; + + constructor(app: App, plugin: MyPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + + // Section heading + new Setting(containerEl).setName('General').setHeading(); + + // Text input + new Setting(containerEl) + .setName('API Key') + .setDesc('Your API key') + .addText(text => text + .setPlaceholder('Enter key...') + .setValue(this.plugin.settings.apiKey) + .onChange(async (value) => { + this.plugin.settings.apiKey = value; + await this.plugin.saveSettings(); + })); + } +} +``` + +Register: `this.addSettingTab(new MySettingTab(this.app, this));` + +### Setting Class + +Constructor: `new Setting(containerEl)`. Chainable methods: + +| Method | Description | +|--------|-------------| +| `setName(name)` | Set setting name | +| `setDesc(desc)` | Set description (string or DocumentFragment) | +| `setHeading()` | Make this a section heading | +| `setClass(cls)` | Add CSS class | +| `setDisabled(disabled)` | Disable setting (v1.2.3) | +| `setTooltip(tooltip, options?)` | Add tooltip (v1.1.0) | +| `clear()` | Clear all components | +| `then(cb)` | Chain callback | + +### Input Components + +| Method | Component | Description | +|--------|-----------|-------------| +| `addText(cb)` | `TextComponent` | Single-line text input | +| `addTextArea(cb)` | `TextAreaComponent` | Multi-line text | +| `addSearch(cb)` | `SearchComponent` | Searchable input | +| `addToggle(cb)` | `ToggleComponent` | Boolean toggle | +| `addDropdown(cb)` | `DropdownComponent` | Select dropdown. `.addOption(value, display)` | +| `addSlider(cb)` | `SliderComponent` | Numeric slider. `.setDynamicTooltip()` | +| `addButton(cb)` | `ButtonComponent` | Button. `.setButtonText()`, `.setCta()`, `.onClick()` | +| `addExtraButton(cb)` | `ExtraButtonComponent` | Small icon button. `.setIcon()` | +| `addColorPicker(cb)` | `ColorComponent` | Color picker. `.setValue('#hex')` | +| `addProgressBar(cb)` | `ProgressBarComponent` | Progress bar. `.setValue(0-100)` | +| `addMomentFormat(cb)` | `MomentFormatComponent` | Date format with live preview | +| `addComponent(cb)` | Any `BaseComponent` | Custom component (v1.11.0) | + +Common component methods: `.setValue()`, `.getValue()`, `.onChange(cb)`, `.setPlaceholder()`, `.setDisabled()`. + +## Modals + +### Basic Modal + +```typescript +import { App, Modal, Setting } from 'obsidian'; + +export class MyModal extends Modal { + result: string; + onSubmit: (result: string) => void; + + constructor(app: App, onSubmit: (result: string) => void) { + super(app); + this.onSubmit = onSubmit; + this.setTitle('Enter Value'); + + new Setting(this.contentEl) + .setName('Value') + .addText(text => text.onChange(v => { this.result = v; })); + + new Setting(this.contentEl) + .addButton(btn => btn.setButtonText('Submit').setCta() + .onClick(() => { this.close(); this.onSubmit(this.result); })); + } +} + +// Usage: +new MyModal(this.app, (result) => { + new Notice(`Got: ${result}`); +}).open(); +``` + +### Modal Properties & Methods + +| Property/Method | Description | +|-----------------|-------------| +| `app` | App reference | +| `containerEl` / `modalEl` / `contentEl` / `titleEl` | DOM elements | +| `scope` | Keyboard scope | +| `open()` | Show the modal | +| `close()` | Hide the modal | +| `onOpen()` | Override for setup | +| `onClose()` | Override for cleanup | +| `setTitle(title)` | Set title text | +| `setContent(content)` | Set content (string or DocumentFragment) | +| `setCloseCallback(cb)` | Custom close handler (v1.10.0) | + +### SuggestModal<T> + +List selection modal. User types to filter. + +```typescript +export class FileSuggestModal extends SuggestModal<TFile> { + getSuggestions(query: string): TFile[] { + return this.app.vault.getMarkdownFiles() + .filter(f => f.path.toLowerCase().includes(query.toLowerCase())); + } + + renderSuggestion(file: TFile, el: HTMLElement) { + el.createEl('div', { text: file.basename }); + el.createEl('small', { text: file.path }); + } + + onChooseSuggestion(file: TFile, evt: MouseEvent | KeyboardEvent) { + new Notice(`Selected: ${file.path}`); + } +} +``` + +Additional properties: `inputEl`, `resultContainerEl`, `emptyStateText`, `limit`. +Methods: `setPlaceholder()`, `setInstructions()`. + +### FuzzySuggestModal<T> + +Fuzzy search modal. Only need `getItems()`, `getItemText()`, `onChooseItem()`. + +```typescript +export class MyFuzzyModal extends FuzzySuggestModal<TFile> { + getItems(): TFile[] { + return this.app.vault.getMarkdownFiles(); + } + getItemText(file: TFile): string { + return file.path; + } + onChooseItem(file: TFile, evt: MouseEvent | KeyboardEvent) { + new Notice(`Selected: ${file.path}`); + } +} +``` + +## Context Menus + +```typescript +import { Menu, Notice } from 'obsidian'; + +// Custom menu +const menu = new Menu(); +menu.addItem(item => item.setTitle('Action').setIcon('icon-name').onClick(() => { ... })); +menu.addSeparator(); +menu.addItem(item => item.setTitle('Another').onClick(() => { ... })); +menu.showAtMouseEvent(event); +// Or: menu.showAtPosition({ x: 20, y: 20 }); +``` + +### Menu Methods + +| Method | Description | +|--------|-------------| +| `addItem(cb: (item: MenuItem) => any)` | Add menu item | +| `addSeparator()` | Add separator | +| `showAtMouseEvent(evt)` | Show at mouse position | +| `showAtPosition(pos, doc?)` | Show at `{x, y}` position | +| `close()` / `hide()` | Close menu | +| `onHide(callback)` | Called when menu hides | +| `setNoIcon()` | Remove icon column | +| `setUseNativeMenu(bool)` | Force native/DOM menu (desktop) | + +### MenuItem Methods + +| Method | Description | +|--------|-------------| +| `setTitle(title)` | Item label | +| `setIcon(icon)` | Item icon | +| `onClick(callback)` | Click handler | +| `setChecked(checked)` | Show checkmark | +| `setDisabled(disabled)` | Disable item | +| `setSection(section)` | Set section (for ordering) | +| `setWarning(isWarning)` | Warning style | + +### Adding to Built-in Menus + +```typescript +// File context menu +this.registerEvent( + this.app.workspace.on('file-menu', (menu, file) => { + menu.addItem(item => item.setTitle('My Action').onClick(() => { ... })); + }) +); + +// Editor context menu +this.registerEvent( + this.app.workspace.on('editor-menu', (menu, editor, view) => { + menu.addItem(item => item.setTitle('My Action').onClick(() => { ... })); + }) +); +``` + +## Notice (Toast Notifications) + +```typescript +import { Notice } from 'obsidian'; + +// Basic notice (auto-hides) +new Notice('Operation complete!'); + +// Custom duration (ms). 0 = stays until clicked. +new Notice('Important message', 10000); + +// Access elements +const notice = new Notice('Processing...'); +notice.setMessage('Done!'); // Update message +notice.hide(); // Hide programmatically +// notice.noticeEl, notice.messageEl, notice.containerEl — DOM access +``` + +## Ribbon Actions + +```typescript +this.addRibbonIcon('dice', 'Tooltip text', (evt: MouseEvent) => { + console.log('Ribbon clicked'); +}); +``` + +First arg is icon name. Users can remove ribbon icons, so provide alternative access (commands). + +## Status Bar (Desktop Only) + +```typescript +const statusBarEl = this.addStatusBarItem(); +statusBarEl.createEl('span', { text: 'Status text' }); +``` + +Multiple items get auto-spaced. Group elements in one item for custom spacing. + +## HTML Elements + +Obsidian extends `HTMLElement` with helper methods: + +```typescript +// Create elements +containerEl.createEl('h1', { text: 'Title' }); +containerEl.createEl('div', { cls: 'my-class', text: 'Content' }); +containerEl.createEl('a', { text: 'Link', attr: { href: '...', target: '_blank' } }); + +// Nested +const parent = containerEl.createEl('div'); +parent.createEl('span', { text: 'Child' }); + +// createSpan / createDiv shortcuts +containerEl.createDiv({ cls: 'wrapper' }); + +// Conditional CSS class +el.toggleClass('active', isActive); + +// Clear contents +containerEl.empty(); +``` + +### Styling + +Add `styles.css` in plugin root. Use Obsidian CSS variables for theme compatibility: +- `--background-modifier-border` — border color +- `--text-muted` — muted text color +- Many more available (see Obsidian CSS variable reference) diff --git a/.rules/docs/obsidian/vault-api.md b/.rules/docs/obsidian/vault-api.md new file mode 100644 index 0000000..feaf08e --- /dev/null +++ b/.rules/docs/obsidian/vault-api.md @@ -0,0 +1,301 @@ +# Obsidian Vault API — Files & Folders + +## TAbstractFile (abstract base) + +Base class for both files and folders. Check type with `instanceof TFile` or `instanceof TFolder`. + +| Property | Type | Description | +|----------|------|-------------| +| `name` | `string` | Filename with extension (e.g., `"note.md"`) | +| `path` | `string` | Vault-relative path (e.g., `"folder/note.md"`) | +| `parent` | `TFolder \| null` | Parent folder (`null` for vault root) | +| `vault` | `Vault` | Reference to the vault | + +## TFile + +Represents a file. Extends `TAbstractFile`. + +| Property | Type | Description | +|----------|------|-------------| +| `basename` | `string` | Filename without extension (e.g., `"note"`) | +| `extension` | `string` | File extension (e.g., `"md"`) | +| `stat` | `FileStats` | File statistics | + +### FileStats + +| Property | Type | Description | +|----------|------|-------------| +| `ctime` | `number` | Creation time (unix ms) | +| `mtime` | `number` | Last modified time (unix ms) | +| `size` | `number` | Size in bytes | + +## TFolder + +Represents a folder. Extends `TAbstractFile`. + +| Property | Type | Description | +|----------|------|-------------| +| `children` | `TAbstractFile[]` | Direct children (files and subfolders) | + +| Method | Description | +|--------|-------------| +| `isRoot(): boolean` | Whether this is the vault root folder | + +## Vault Class + +Access via `this.app.vault`. Extends `Events`. + +| Property | Type | Description | +|----------|------|-------------| +| `adapter` | `DataAdapter` | Low-level file system access | +| `configDir` | `string` | Config folder path (usually `.obsidian`) | + +### Querying Files + +| Method | Returns | Description | +|--------|---------|-------------| +| `getFiles()` | `TFile[]` | All files in vault | +| `getMarkdownFiles()` | `TFile[]` | All `.md` files | +| `getAllLoadedFiles()` | `TAbstractFile[]` | All files and folders | +| `getAllFolders(includeRoot?)` | `TFolder[]` | All folders | +| `getAbstractFileByPath(path)` | `TAbstractFile \| null` | Get file or folder by path | +| `getFileByPath(path)` | `TFile \| null` | Get file by path | +| `getFolderByPath(path)` | `TFolder \| null` | Get folder by path | +| `getRoot()` | `TFolder` | Vault root folder | +| `getName()` | `string` | Vault name | +| `getResourcePath(file)` | `string` | URI for browser engine (e.g., images) | + +### Reading Files + +| Method | Returns | Description | +|--------|---------|-------------| +| `read(file)` | `Promise<string>` | Read from disk. Use when you intend to modify the content. | +| `cachedRead(file)` | `Promise<string>` | Read from cache. Better performance for display-only. | +| `readBinary(file)` | `Promise<ArrayBuffer>` | Read binary file | + +**When to use which**: Use `cachedRead()` for display. Use `read()` if you'll modify and write back (avoids stale data). Cache is flushed on save or external change. + +### Writing Files + +| Method | Description | +|--------|-------------| +| `create(path, data, options?)` | Create new plaintext file. Returns `Promise<TFile>`. | +| `createBinary(path, data, options?)` | Create new binary file | +| `createFolder(path)` | Create new folder | +| `modify(file, data, options?)` | Overwrite file content | +| `modifyBinary(file, data, options?)` | Overwrite binary content | +| `append(file, data, options?)` | Append text to file | +| `appendBinary(file, data, options?)` | Append binary data | +| `process(file, fn, options?)` | **Preferred**: Atomically read-modify-save. `fn(data) => newData` is synchronous. | + +**Important**: Always prefer `process()` over separate `read()`/`modify()` to prevent data loss from concurrent changes. + +#### Async Modification Pattern + +```typescript +// For async operations between read and write: +const content = await vault.cachedRead(file); +const newContent = await transformAsync(content); +await vault.process(file, (data) => { + if (data !== content) { + // File changed since we read it — handle conflict + return data; // or merge, or prompt user + } + return newContent; +}); +``` + +### Moving/Renaming/Deleting Files + +| Method | Description | +|--------|-------------| +| `rename(file, newPath)` | Rename/move. **Does NOT update links.** Use `FileManager.renameFile()` instead. | +| `copy(file, newPath)` | Copy file or folder | +| `delete(file, force?)` | Permanently delete | +| `trash(file, system?)` | Move to trash. `system=true` for OS trash, `false` for `.trash/` folder. | + +### Static Methods + +| Method | Description | +|--------|-------------| +| `Vault.recurseChildren(root, cb)` | Recursively iterate all children | + +### Vault Events + +Subscribe via `this.app.vault.on(...)`. Always wrap with `this.registerEvent(...)`. + +| Event | Callback Args | Description | +|-------|--------------|-------------| +| `'create'` | `(file: TAbstractFile)` | File created. Also fires on vault load for existing files. Register inside `workspace.onLayoutReady()` to skip initial load events. | +| `'modify'` | `(file: TAbstractFile)` | File modified | +| `'delete'` | `(file: TAbstractFile)` | File deleted | +| `'rename'` | `(file: TAbstractFile, oldPath: string)` | File renamed/moved | + +```typescript +// Example: Listen for file creation +this.registerEvent( + this.app.vault.on('create', (file) => { + if (file instanceof TFile) { + console.log('New file:', file.path); + } + }) +); +``` + +## FileManager Class + +Access via `this.app.fileManager`. Higher-level file operations. + +| Method | Description | +|--------|-------------| +| `renameFile(file, newPath)` | Rename/move and **auto-update all links** per user preferences | +| `trashFile(file)` | Delete respecting user's trash preferences | +| `promptForDeletion(file)` | Show confirmation dialog before deleting | +| `generateMarkdownLink(file, sourcePath, subpath?, alias?)` | Generate `[[link]]` or `[alias](path)` based on user preferences | +| `getNewFileParent(sourcePath, newFilePath)` | Get folder for new files based on user preferences | +| `getAvailablePathForAttachment(filename, sourcePath)` | Resolve unique attachment path, deduplicating if needed | +| `processFrontMatter(file, fn, options?)` | Atomically read/modify/save frontmatter as JS object | + +### Frontmatter Processing + +```typescript +// Add or update frontmatter properties +await this.app.fileManager.processFrontMatter(file, (fm) => { + fm.tags = ['ai', 'organized']; + fm.lastProcessed = new Date().toISOString(); +}); +``` + +The `fm` object is mutated directly. Handle errors from this method. + +## DataAdapter Interface + +Low-level filesystem access via `this.app.vault.adapter`. Prefer Vault API when possible. + +| Method | Description | +|--------|-------------| +| `exists(path, sensitive?)` | Check if path exists | +| `read(path)` / `readBinary(path)` | Read file by path string | +| `write(path, data)` / `writeBinary(path, data)` | Write file (creates if needed) | +| `append(path, data)` | Append to file | +| `process(path, fn)` | Atomic read-modify-save | +| `mkdir(path)` | Create directory | +| `list(path)` | List files/folders (non-recursive) | +| `stat(path)` | Get file metadata | +| `remove(path)` | Delete file | +| `rmdir(path, recursive?)` | Remove directory | +| `rename(path, newPath)` | Rename/move | +| `copy(path, newPath)` | Copy file | +| `trashLocal(path)` | Move to `.trash/` | +| `trashSystem(path)` | Move to OS trash | +| `getResourcePath(path)` | Get browser-usable URI | + +**Note**: Vault API only accesses files visible in the app. Hidden folders (like `.obsidian`) require the DataAdapter. + +## AI Note Organizer Patterns + +### Batch Processing All Notes + +```typescript +async function processAllNotes(app: App) { + const files = app.vault.getMarkdownFiles(); + for (const file of files) { + const cache = app.metadataCache.getFileCache(file); + // Skip already-processed files + if (cache?.frontmatter?.aiProcessed) continue; + + const content = await app.vault.cachedRead(file); + // Send to AI, get results, then update: + await app.fileManager.processFrontMatter(file, (fm) => { + fm.tags = ['ai-suggested-tag']; + fm.aiProcessed = new Date().toISOString(); + }); + } +} +``` + +### Moving Notes into Organized Folders + +```typescript +async function organizeNote(app: App, file: TFile, targetFolder: string) { + // Ensure folder exists + if (!app.vault.getFolderByPath(targetFolder)) { + await app.vault.createFolder(targetFolder); + } + // renameFile updates all links automatically + const newPath = `${targetFolder}/${file.name}`; + await app.fileManager.renameFile(file, newPath); +} +``` + +### Creating AI-Generated Summary Notes + +```typescript +async function createSummaryNote(vault: Vault, folder: string, title: string, content: string) { + const path = `${folder}/${title}.md`; + const existing = vault.getFileByPath(path); + if (existing) { + await vault.modify(existing, content); + } else { + await vault.create(path, content); + } +} +``` + +### Auto-Processing on File Change + +```typescript +// In onload(): +this.registerEvent( + this.app.vault.on('modify', async (file) => { + if (file instanceof TFile && file.extension === 'md') { + // Debounce or queue for AI processing + this.queueForProcessing(file); + } + }) +); + +// Skip initial vault load events: +this.app.workspace.onLayoutReady(() => { + this.registerEvent( + this.app.vault.on('create', async (file) => { + if (file instanceof TFile && file.extension === 'md') { + this.queueForProcessing(file); + } + }) + ); +}); +``` + +### Building a Note Graph for AI Context + +```typescript +function buildNoteGraph(app: App): Map<string, string[]> { + const graph = new Map<string, string[]>(); + for (const [source, targets] of Object.entries(app.metadataCache.resolvedLinks)) { + graph.set(source, Object.keys(targets)); + } + return graph; +} + +function getRelatedNotes(app: App, filePath: string): string[] { + const related = new Set<string>(); + // Outgoing links + const outgoing = app.metadataCache.resolvedLinks[filePath]; + if (outgoing) Object.keys(outgoing).forEach(p => related.add(p)); + // Incoming links (backlinks) + for (const [source, targets] of Object.entries(app.metadataCache.resolvedLinks)) { + if (targets[filePath]) related.add(source); + } + return [...related]; +} +``` + +```typescript +const item = this.app.vault.getAbstractFileByPath('path/to/something'); +if (item instanceof TFile) { + // It's a file +} else if (item instanceof TFolder) { + // It's a folder +} +``` diff --git a/.rules/docs/obsidian/workspace-api.md b/.rules/docs/obsidian/workspace-api.md new file mode 100644 index 0000000..deedb03 --- /dev/null +++ b/.rules/docs/obsidian/workspace-api.md @@ -0,0 +1,221 @@ +# Obsidian Workspace & Views API + +## Workspace + +Access via `this.app.workspace`. Extends `Events`. Manages the UI layout as a tree of workspace items. + +### Layout Structure + +The workspace is a tree: +- **WorkspaceSplit**: Lays out children side by side (vertical or horizontal) +- **WorkspaceTabs**: Shows one child at a time with tab headers +- **WorkspaceLeaf**: Terminal node that displays a View + +Three root splits: `leftSplit` (sidebar), `rootSplit` (main area), `rightSplit` (sidebar). + +### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `activeEditor` | `MarkdownFileInfo \| null` | Current editor component | +| `activeLeaf` | `WorkspaceLeaf \| null` | Currently focused leaf. Avoid using directly; prefer helper methods. | +| `layoutReady` | `boolean` | Whether layout is initialized | +| `containerEl` | `HTMLElement` | Workspace container | +| `leftSplit` | `WorkspaceSidedock \| WorkspaceMobileDrawer` | Left sidebar | +| `rightSplit` | `WorkspaceSidedock \| WorkspaceMobileDrawer` | Right sidebar | +| `rootSplit` | `WorkspaceRoot` | Main content area | +| `leftRibbon` / `rightRibbon` | `WorkspaceRibbon` | Ribbon bars | +| `requestSaveLayout` | `Debouncer` | Debounced layout save | + +### Getting Leaves & Views + +| Method | Returns | Description | +|--------|---------|-------------| +| `getActiveFile()` | `TFile \| null` | Active file from current FileView, or most recent | +| `getActiveViewOfType(type)` | `T \| null` | Get active view if it matches type | +| `getLeavesOfType(viewType)` | `WorkspaceLeaf[]` | All leaves of a view type | +| `getLeafById(id)` | `WorkspaceLeaf \| null` | Get leaf by ID | +| `getLastOpenFiles()` | `string[]` | 10 most recently opened filenames | +| `getMostRecentLeaf(root?)` | `WorkspaceLeaf \| null` | Most recent leaf in root split | +| `iterateAllLeaves(cb)` | `void` | Iterate all leaves (main + sidebars + floating) | +| `iterateRootLeaves(cb)` | `void` | Iterate main area leaves only | +| `getGroupLeaves(group)` | `WorkspaceLeaf[]` | Get leaves in a link group | + +### Creating & Managing Leaves + +| Method | Returns | Description | +|--------|---------|-------------| +| `getLeaf(newLeaf?)` | `WorkspaceLeaf` | Get/create leaf. `false`=reuse existing, `true`/`'tab'`=new tab, `'split'`=split adjacent, `'window'`=popout. | +| `getLeaf('split', direction)` | `WorkspaceLeaf` | Split: `'vertical'` (right) or `'horizontal'` (below) | +| `getLeftLeaf(split)` | `WorkspaceLeaf` | Create leaf in left sidebar | +| `getRightLeaf(split)` | `WorkspaceLeaf` | Create leaf in right sidebar | +| `ensureSideLeaf(type, side, options?)` | `WorkspaceLeaf` | Get or create sidebar leaf (v1.7.2) | +| `createLeafBySplit(leaf, direction?, before?)` | `WorkspaceLeaf` | Split an existing leaf | +| `createLeafInParent(parent, index)` | `WorkspaceLeaf` | Create leaf in specific parent | +| `duplicateLeaf(leaf, leafType?, direction?)` | `Promise<WorkspaceLeaf>` | Duplicate a leaf | +| `detachLeavesOfType(viewType)` | `void` | Remove all leaves of a type | + +### Navigation + +| Method | Description | +|--------|-------------| +| `setActiveLeaf(leaf, params?)` | Set the active leaf | +| `revealLeaf(leaf)` | Bring leaf to foreground, uncollapse sidebar if needed. `await` for full load. (v1.7.2) | +| `openLinkText(linktext, sourcePath, newLeaf?, openViewState?)` | Open internal link | +| `moveLeafToPopout(leaf, data?)` | Move leaf to popout window (desktop only) | +| `openPopoutLeaf(data?)` | Open new popout window with leaf (desktop only) | +| `onLayoutReady(callback)` | Run callback when layout is ready (or immediately if already ready). (v0.11.0) | + +### Workspace Events + +| Event | Callback Args | Description | +|-------|--------------|-------------| +| `'file-open'` | `(file: TFile \| null)` | Active file changed (new leaf, existing leaf, or embed) | +| `'active-leaf-change'` | `(leaf: WorkspaceLeaf \| null)` | Active leaf changed | +| `'layout-change'` | `()` | Layout changed | +| `'editor-change'` | `(editor: Editor, info: MarkdownView \| MarkdownFileInfo)` | Editor content changed | +| `'editor-paste'` | `(evt: ClipboardEvent, editor: Editor, info)` | Editor paste. Check `evt.defaultPrevented`. | +| `'editor-drop'` | `(evt: DragEvent, editor: Editor, info)` | Editor drop. Check `evt.defaultPrevented`. | +| `'editor-menu'` | `(menu: Menu, editor: Editor, info)` | Editor context menu (v1.1.0) | +| `'file-menu'` | `(menu: Menu, file: TAbstractFile, source: string, leaf?: WorkspaceLeaf)` | File context menu | +| `'files-menu'` | `(menu: Menu, files: TAbstractFile[], source: string, leaf?: WorkspaceLeaf)` | Multi-file context menu (v1.4.10) | +| `'url-menu'` | `(menu: Menu, url: string)` | External URL context menu (v1.5.1) | +| `'quick-preview'` | `(file: TFile, data: string)` | Active markdown file modified (pre-save) | +| `'resize'` | `()` | Window/item resized | +| `'quit'` | `()` | App about to quit (best-effort, not guaranteed) | +| `'window-open'` | `(win: WorkspaceWindow)` | Popout window created | +| `'window-close'` | `(win: WorkspaceWindow)` | Popout window closed | +| `'css-change'` | `()` | CSS changed | + +## WorkspaceLeaf + +A leaf hosts a single View. + +### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `view` | `View` | The view in this leaf. Check `instanceof` before casting. | +| `parent` | `WorkspaceTabs \| WorkspaceMobileDrawer` | Parent container | +| `isDeferred` | `boolean` (readonly) | Whether leaf is deferred/background (v1.7.2) | + +### Methods + +| Method | Description | +|--------|-------------| +| `openFile(file, openState?)` | Open a file in this leaf | +| `setViewState(viewState, eState?)` | Set the view state (type, state, active) | +| `getViewState()` | Get current view state | +| `getDisplayText()` | Display text for tab | +| `detach()` | Remove this leaf | +| `setPinned(pinned)` / `togglePinned()` | Pin/unpin leaf | +| `setGroup(group)` | Set link group | +| `loadIfDeferred()` | Load deferred leaf. Await for full load. (v1.7.2) | + +## Custom Views + +### ItemView (for custom panels) + +```typescript +import { ItemView, WorkspaceLeaf } from 'obsidian'; + +export const VIEW_TYPE = 'my-view'; + +export class MyView extends ItemView { + constructor(leaf: WorkspaceLeaf) { + super(leaf); + } + + getViewType(): string { return VIEW_TYPE; } + getDisplayText(): string { return 'My View'; } + + async onOpen() { + const container = this.contentEl; + container.empty(); + container.createEl('h4', { text: 'Hello' }); + } + + async onClose() { + // Cleanup + } +} +``` + +### View Properties + +| Property | Type | Description | +|----------|------|-------------| +| `app` | `App` | App reference | +| `leaf` | `WorkspaceLeaf` | Parent leaf | +| `containerEl` | `HTMLElement` | Container element | +| `contentEl` | `HTMLElement` | Content area (ItemView) | +| `icon` | `IconName` | View icon | +| `navigation` | `boolean` | `true` if navigable (file views). `false` for static panels (explorer, calendar). | +| `scope` | `Scope \| null` | Optional hotkey scope | + +### View Methods + +| Method | Description | +|--------|-------------| +| `getViewType()` | Return unique view type string (abstract) | +| `getDisplayText()` | Return human-readable name (abstract) | +| `onOpen()` | Build view content | +| `onClose()` | Cleanup resources | +| `onResize()` | Handle size changes | +| `onPaneMenu(menu, source)` | Populate pane context menu | +| `addAction(icon, title, callback)` | Add action button to view header | +| `getState()` / `setState(state, result)` | Serialize/restore state | +| `getEphemeralState()` / `setEphemeralState(state)` | Non-persistent state (scroll position, etc.) | + +### MarkdownView + +Extends `TextFileView`. The built-in markdown editor view. + +| Property | Type | Description | +|----------|------|-------------| +| `editor` | `Editor` | The editor instance | +| `file` | `TFile \| null` | Currently open file | +| `data` | `string` | In-memory file content | +| `currentMode` | `MarkdownSubView` | Current editing subview | +| `previewMode` | `MarkdownPreviewView` | Preview renderer | + +| Method | Description | +|--------|-------------| +| `getMode()` | Get current mode | +| `getViewData()` | Get view data | +| `setViewData(data, clear)` | Set view data | +| `showSearch(replace?)` | Show search (and optionally replace) | + +### Registering & Activating Views + +```typescript +// Register in onload() +this.registerView(VIEW_TYPE, (leaf) => new MyView(leaf)); + +// Activate view +async activateView() { + const { workspace } = this.app; + let leaf: WorkspaceLeaf | null = null; + const leaves = workspace.getLeavesOfType(VIEW_TYPE); + + if (leaves.length > 0) { + leaf = leaves[0]; + } else { + leaf = workspace.getRightLeaf(false); + await leaf.setViewState({ type: VIEW_TYPE, active: true }); + } + workspace.revealLeaf(leaf); +} +``` + +**Warning**: Never store view references in your plugin. Obsidian may call the factory multiple times. Use `getLeavesOfType()` to access views. + +```typescript +this.app.workspace.getLeavesOfType(VIEW_TYPE).forEach((leaf) => { + if (leaf.view instanceof MyView) { + // Access view instance + } +}); +``` + +Plugins must remove their leaves when disabled. `detachLeavesOfType(viewType)` removes all. diff --git a/.rules/docs/ollama/chat.md b/.rules/docs/ollama/chat.md new file mode 100644 index 0000000..874243d --- /dev/null +++ b/.rules/docs/ollama/chat.md @@ -0,0 +1,171 @@ +# Generate a chat message + +`POST /api/chat` — Generate the next message in a conversation between a user and an assistant. + +**Server:** `http://localhost:11434` + +## Request + +| Field | Type | Required | Description | +|---|---|---|---| +| `model` | string | yes | Model name | +| `messages` | ChatMessage[] | yes | Chat history (array of message objects) | +| `tools` | ToolDefinition[] | no | Function tools the model may call | +| `format` | `"json"` \| object | no | Response format — `"json"` or a JSON schema | +| `options` | ModelOptions | no | Runtime generation options (see generate.md) | +| `stream` | boolean | no | Stream partial responses (default: `true`) | +| `think` | boolean \| string | no | Enable thinking output (`true`/`false` or `"high"`, `"medium"`, `"low"`) | +| `keep_alive` | string \| number | no | Keep-alive duration (e.g. `"5m"` or `0` to unload) | + +### ChatMessage + +| Field | Type | Required | Description | +|---|---|---|---| +| `role` | string | yes | `"system"`, `"user"`, `"assistant"`, or `"tool"` | +| `content` | string | yes | Message text | +| `images` | string[] | no | Base64-encoded images (multimodal) | +| `tool_calls` | ToolCall[] | no | Tool calls from the model | + +### ToolDefinition + +```json +{ + "type": "function", + "function": { + "name": "function_name", + "description": "What the function does", + "parameters": { /* JSON Schema */ } + } +} +``` + +### ToolCall + +```json +{ + "function": { + "name": "function_name", + "arguments": { /* key-value args */ } + } +} +``` + +## Response (non-streaming, `stream: false`) + +| Field | Type | Description | +|---|---|---| +| `model` | string | Model name | +| `created_at` | string | ISO 8601 timestamp | +| `message.role` | string | Always `"assistant"` | +| `message.content` | string | Assistant reply text | +| `message.thinking` | string | Thinking trace (when `think` enabled) | +| `message.tool_calls` | ToolCall[] | Tool calls requested by assistant | +| `done` | boolean | Whether the response finished | +| `done_reason` | string | Why it finished | +| `total_duration` | integer | Total time (nanoseconds) | +| `load_duration` | integer | Model load time (nanoseconds) | +| `prompt_eval_count` | integer | Input token count | +| `prompt_eval_duration` | integer | Prompt eval time (nanoseconds) | +| `eval_count` | integer | Output token count | +| `eval_duration` | integer | Token generation time (nanoseconds) | + +## Streaming Response (`stream: true`, default) + +Returns `application/x-ndjson`. Each chunk has `message.content` (partial text). Final chunk has `done: true` with duration/count stats. + +## Examples + +### Basic (streaming) +```bash +curl http://localhost:11434/api/chat -d '{ + "model": "gemma3", + "messages": [ + {"role": "user", "content": "why is the sky blue?"} + ] +}' +``` + +### Non-streaming +```bash +curl http://localhost:11434/api/chat -d '{ + "model": "gemma3", + "messages": [ + {"role": "user", "content": "why is the sky blue?"} + ], + "stream": false +}' +``` + +### Structured output +```bash +curl http://localhost:11434/api/chat -d '{ + "model": "gemma3", + "messages": [ + {"role": "user", "content": "What are the populations of the United States and Canada?"} + ], + "stream": false, + "format": { + "type": "object", + "properties": { + "countries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "country": {"type": "string"}, + "population": {"type": "integer"} + }, + "required": ["country", "population"] + } + } + }, + "required": ["countries"] + } +}' +``` + +### Tool calling +```bash +curl http://localhost:11434/api/chat -d '{ + "model": "qwen3", + "messages": [ + {"role": "user", "content": "What is the weather today in Paris?"} + ], + "stream": false, + "tools": [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather for a location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The location, e.g. San Francisco, CA" + }, + "format": { + "type": "string", + "description": "celsius or fahrenheit", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location", "format"] + } + } + } + ] +}' +``` + +### Thinking +```bash +curl http://localhost:11434/api/chat -d '{ + "model": "gpt-oss", + "messages": [ + {"role": "user", "content": "What is 1+1?"} + ], + "think": "low" +}' +``` diff --git a/.rules/docs/ollama/embed.md b/.rules/docs/ollama/embed.md new file mode 100644 index 0000000..9c81ebf --- /dev/null +++ b/.rules/docs/ollama/embed.md @@ -0,0 +1,56 @@ +# Generate embeddings + +`POST /api/embed` — Creates vector embeddings representing the input text. + +**Server:** `http://localhost:11434` + +## Request + +| Field | Type | Required | Description | +|---|---|---|---| +| `model` | string | yes | Model name (e.g. `"embeddinggemma"`) | +| `input` | string \| string[] | yes | Text or array of texts to embed | +| `truncate` | boolean | no | Truncate inputs exceeding context window (default: `true`). If `false`, returns an error. | +| `dimensions` | integer | no | Number of dimensions for the embedding vectors | +| `keep_alive` | string | no | Model keep-alive duration | +| `options` | ModelOptions | no | Runtime options (see generate.md) | + +## Response + +| Field | Type | Description | +|---|---|---| +| `model` | string | Model that produced the embeddings | +| `embeddings` | number[][] | Array of embedding vectors (one per input) | +| `total_duration` | integer | Total time (nanoseconds) | +| `load_duration` | integer | Model load time (nanoseconds) | +| `prompt_eval_count` | integer | Number of input tokens processed | + +## Examples + +### Single input +```bash +curl http://localhost:11434/api/embed -d '{ + "model": "embeddinggemma", + "input": "Why is the sky blue?" +}' +``` + +### Multiple inputs (batch) +```bash +curl http://localhost:11434/api/embed -d '{ + "model": "embeddinggemma", + "input": [ + "Why is the sky blue?", + "Why is the grass green?" + ] +}' +``` + +### Custom dimensions +```bash +curl http://localhost:11434/api/embed -d '{ + "model": "embeddinggemma", + "input": "Generate embeddings for this text", + "dimensions": 128 +}' +``` diff --git a/.rules/docs/ollama/generate.md b/.rules/docs/ollama/generate.md new file mode 100644 index 0000000..30534c2 --- /dev/null +++ b/.rules/docs/ollama/generate.md @@ -0,0 +1,121 @@ +# Generate a response + +`POST /api/generate` — Generates a response for a provided prompt. + +**Server:** `http://localhost:11434` + +## Request + +| Field | Type | Required | Description | +|---|---|---|---| +| `model` | string | yes | Model name | +| `prompt` | string | no | Text for the model to generate a response from | +| `suffix` | string | no | Fill-in-the-middle text after the prompt, before the response | +| `images` | string[] | no | Base64-encoded images (for multimodal models) | +| `format` | string \| object | no | `"json"` or a JSON schema object for structured output | +| `system` | string | no | System prompt | +| `stream` | boolean | no | Stream partial responses (default: `true`) | +| `think` | boolean \| string | no | Enable thinking output (`true`/`false` or `"high"`, `"medium"`, `"low"`) | +| `raw` | boolean | no | Return raw response without prompt templating | +| `keep_alive` | string \| number | no | Keep-alive duration (e.g. `"5m"` or `0` to unload immediately) | +| `options` | ModelOptions | no | Runtime generation options (see below) | + +### ModelOptions + +| Field | Type | Description | +|---|---|---| +| `seed` | integer | Random seed for reproducible outputs | +| `temperature` | float | Randomness (higher = more random) | +| `top_k` | integer | Limit next token to K most likely | +| `top_p` | float | Nucleus sampling cumulative probability threshold | +| `min_p` | float | Minimum probability threshold | +| `stop` | string \| string[] | Stop sequences | +| `num_ctx` | integer | Context length (number of tokens) | +| `num_predict` | integer | Max tokens to generate | + +## Response (non-streaming, `stream: false`) + +| Field | Type | Description | +|---|---|---| +| `model` | string | Model name | +| `created_at` | string | ISO 8601 timestamp | +| `response` | string | Generated text | +| `thinking` | string | Thinking output (when `think` enabled) | +| `done` | boolean | Whether generation finished | +| `done_reason` | string | Why generation stopped | +| `total_duration` | integer | Total time (nanoseconds) | +| `load_duration` | integer | Model load time (nanoseconds) | +| `prompt_eval_count` | integer | Number of input tokens | +| `prompt_eval_duration` | integer | Prompt eval time (nanoseconds) | +| `eval_count` | integer | Number of output tokens | +| `eval_duration` | integer | Token generation time (nanoseconds) | + +## Streaming Response (`stream: true`, default) + +Returns `application/x-ndjson` — one JSON object per line. Each chunk has the same fields as the non-streaming response. The final chunk has `done: true` with duration/count stats. + +## Examples + +### Basic (streaming) +```bash +curl http://localhost:11434/api/generate -d '{ + "model": "gemma3", + "prompt": "Why is the sky blue?" +}' +``` + +### Non-streaming +```bash +curl http://localhost:11434/api/generate -d '{ + "model": "gemma3", + "prompt": "Why is the sky blue?", + "stream": false +}' +``` + +### With options +```bash +curl http://localhost:11434/api/generate -d '{ + "model": "gemma3", + "prompt": "Why is the sky blue?", + "options": { + "temperature": 0.8, + "top_p": 0.9, + "seed": 42 + } +}' +``` + +### Structured output (JSON schema) +```bash +curl http://localhost:11434/api/generate -d '{ + "model": "gemma3", + "prompt": "What are the populations of the United States and Canada?", + "stream": false, + "format": { + "type": "object", + "properties": { + "countries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "country": {"type": "string"}, + "population": {"type": "integer"} + }, + "required": ["country", "population"] + } + } + }, + "required": ["countries"] + } +}' +``` + +### Load / Unload model +```bash +# Load +curl http://localhost:11434/api/generate -d '{"model": "gemma3"}' +# Unload +curl http://localhost:11434/api/generate -d '{"model": "gemma3", "keep_alive": 0}' +``` diff --git a/.rules/docs/ollama/list-models.md b/.rules/docs/ollama/list-models.md new file mode 100644 index 0000000..f5da57f --- /dev/null +++ b/.rules/docs/ollama/list-models.md @@ -0,0 +1,56 @@ +# List models + +`GET /api/tags` — Fetch a list of locally available models and their details. + +**Server:** `http://localhost:11434` + +## Request + +No parameters required. + +```bash +curl http://localhost:11434/api/tags +``` + +## Response + +| Field | Type | Description | +|---|---|---| +| `models` | ModelSummary[] | Array of available models | + +### ModelSummary + +| Field | Type | Description | +|---|---|---| +| `name` | string | Model name | +| `model` | string | Model name | +| `modified_at` | string | Last modified (ISO 8601) | +| `size` | integer | Size on disk (bytes) | +| `digest` | string | SHA256 digest | +| `details.format` | string | File format (e.g. `"gguf"`) | +| `details.family` | string | Primary model family (e.g. `"llama"`) | +| `details.families` | string[] | All families the model belongs to | +| `details.parameter_size` | string | Parameter count label (e.g. `"7B"`) | +| `details.quantization_level` | string | Quantization level (e.g. `"Q4_0"`) | + +### Example response +```json +{ + "models": [ + { + "name": "gemma3", + "model": "gemma3", + "modified_at": "2025-10-03T23:34:03.409490317-07:00", + "size": 3338801804, + "digest": "a2af6cc3eb7fa8be8504abaf9b04e88f17a119ec3f04a3addf55f92841195f5a", + "details": { + "format": "gguf", + "family": "gemma", + "families": ["gemma"], + "parameter_size": "4.3B", + "quantization_level": "Q4_K_M" + } + } + ] +} +``` diff --git a/.rules/docs/ollama/show-model.md b/.rules/docs/ollama/show-model.md new file mode 100644 index 0000000..befbd22 --- /dev/null +++ b/.rules/docs/ollama/show-model.md @@ -0,0 +1,43 @@ +# Show model details + +`POST /api/show` — Get detailed information about a specific model. + +**Server:** `http://localhost:11434` + +## Request + +| Field | Type | Required | Description | +|---|---|---|---| +| `model` | string | yes | Model name to show | +| `verbose` | boolean | no | Include large verbose fields in the response | + +## Response + +| Field | Type | Description | +|---|---|---| +| `parameters` | string | Model parameter settings (text) | +| `modified_at` | string | Last modified (ISO 8601) | +| `template` | string | Prompt template used by the model | +| `capabilities` | string[] | Supported features (e.g. `"completion"`, `"vision"`) | +| `details.format` | string | File format (e.g. `"gguf"`) | +| `details.family` | string | Model family | +| `details.families` | string[] | All families | +| `details.parameter_size` | string | Parameter count label (e.g. `"4.3B"`) | +| `details.quantization_level` | string | Quantization level (e.g. `"Q4_K_M"`) | +| `model_info` | object | Architecture metadata (context length, embedding size, etc.) | + +## Examples + +```bash +curl http://localhost:11434/api/show -d '{ + "model": "gemma3" +}' +``` + +### Verbose +```bash +curl http://localhost:11434/api/show -d '{ + "model": "gemma3", + "verbose": true +}' +``` diff --git a/.rules/docs/ollama/version.md b/.rules/docs/ollama/version.md new file mode 100644 index 0000000..29fb757 --- /dev/null +++ b/.rules/docs/ollama/version.md @@ -0,0 +1,19 @@ +# Get version + +`GET /api/version` — Retrieve the Ollama server version. + +**Server:** `http://localhost:11434` + +## Request + +No parameters required. + +```bash +curl http://localhost:11434/api/version +``` + +## Response + +| Field | Type | Description | +|---|---|---| +| `version` | string | Ollama version (e.g. `"0.12.6"`) | |
