From e2c88087f3926ec477ea099fd771d1bc9d11d7c5 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Tue, 24 Mar 2026 00:25:03 +0900 Subject: generate api docs --- .rules/changelog/2026-03/23/01.md | 14 ++ .rules/changelog/2026-03/24/01.md | 16 ++ .rules/changelog/2026-03/24/02.md | 17 ++ .rules/default/obsidian.md | 31 +++ .rules/default/ollama.md | 22 ++ .rules/docs/obsidian/editor-api.md | 166 +++++++++++++++ .rules/docs/obsidian/events-utilities.md | 330 +++++++++++++++++++++++++++++ .rules/docs/obsidian/metadata-cache.md | 214 +++++++++++++++++++ .rules/docs/obsidian/plugin-lifecycle.md | 168 +++++++++++++++ .rules/docs/obsidian/ui-components.md | 349 +++++++++++++++++++++++++++++++ .rules/docs/obsidian/vault-api.md | 301 ++++++++++++++++++++++++++ .rules/docs/obsidian/workspace-api.md | 221 +++++++++++++++++++ .rules/docs/ollama/chat.md | 171 +++++++++++++++ .rules/docs/ollama/embed.md | 56 +++++ .rules/docs/ollama/generate.md | 121 +++++++++++ .rules/docs/ollama/list-models.md | 56 +++++ .rules/docs/ollama/show-model.md | 43 ++++ .rules/docs/ollama/version.md | 19 ++ 18 files changed, 2315 insertions(+) create mode 100644 .rules/changelog/2026-03/23/01.md create mode 100644 .rules/changelog/2026-03/24/01.md create mode 100644 .rules/changelog/2026-03/24/02.md create mode 100644 .rules/default/obsidian.md create mode 100644 .rules/default/ollama.md create mode 100644 .rules/docs/obsidian/editor-api.md create mode 100644 .rules/docs/obsidian/events-utilities.md create mode 100644 .rules/docs/obsidian/metadata-cache.md create mode 100644 .rules/docs/obsidian/plugin-lifecycle.md create mode 100644 .rules/docs/obsidian/ui-components.md create mode 100644 .rules/docs/obsidian/vault-api.md create mode 100644 .rules/docs/obsidian/workspace-api.md create mode 100644 .rules/docs/ollama/chat.md create mode 100644 .rules/docs/ollama/embed.md create mode 100644 .rules/docs/ollama/generate.md create mode 100644 .rules/docs/ollama/list-models.md create mode 100644 .rules/docs/ollama/show-model.md create mode 100644 .rules/docs/ollama/version.md (limited to '.rules') 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
)
+  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
+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` | 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)
+  // 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 {
+  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 {
+  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 {
+  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 {
+  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>` | Map: source path → { dest path → link count }. All paths are vault-absolute. |
+| `unresolvedLinks` | `Record>` | 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` | 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();
+  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` | Load `data.json` from plugin folder |
+| `saveData(data: any): Promise` | 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 = {
+  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//`:
+- `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
+
+List selection modal. User types to filter.
+
+```typescript
+export class FileSuggestModal extends SuggestModal {
+  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
+
+Fuzzy search modal. Only need `getItems()`, `getItemText()`, `onChooseItem()`.
+
+```typescript
+export class MyFuzzyModal extends FuzzySuggestModal {
+  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` | Read from disk. Use when you intend to modify the content. |
+| `cachedRead(file)` | `Promise` | Read from cache. Better performance for display-only. |
+| `readBinary(file)` | `Promise` | 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`. |
+| `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 {
+  const graph = new Map();
+  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();
+  // 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` | 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"`) |
-- 
cgit v1.2.3