obsidian-gemini
AGENTS.md534 starsChanged 36 days agoMITView on GitHub
Obsidian Gemini Scribe is an Obsidian plugin that integrates Google's Gemini AI models for AI-driven assistance within Obsidian. It provides context-aware chat, document summarization, text rewriting, and IDE-style completions. IMPORTANT: Always run npm install first if you encounter TypeScript errors or missing module errors during build. The build requires all dependencies in node_modules to be present. If you get errors about missing modules like obsidian, @google/genai, handlebars, or tslib, run npm install before attempting npm run build again.
- Reads credentials
- Installs packages
We split this AGENTS.md into the rules it gives an agent
43 rules. About 1 is common, 21 are distinctive, and 21 are very specific to this project.
≈1
Common · estimate
Many files like this have a rule of this kind.
≈21
Distinctive · estimate
This author's own choices. Worth a look.
21
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 35
IMPORTANT: Always run npm install first if you encounter TypeScript errors or missing module errors during build.
- Very specific: line 39
Run single test: npm test -- path/to/test.ts
- Distinctive: line 98
Obsidian API First: Always use built-in Obsidian API functions when available instead of low-level operations (for detailed Obsidian API …
- Very specific: line 99
Use vault.getMarkdownFiles() instead of vault.adapter.list()
- Very specific: line 100
Use app.fileManager.processFrontMatter() for frontmatter manipulation
- Very specific: line 101
Use vault.getAbstractFileByPath() for file operations
- Very specific: line 102
Use app.metadataCache for file metadata access
- Very specific: line 103
Use app.fileManager.renameFile() for renaming files (preserves metadata)
- Very specific: line 104
Use app.workspace.openLinkText() for clickable file links in views
- Distinctive: line 105
File Operations: Always use Obsidian's normalized paths and metadata cache
- Distinctive: line 132
System Folder Protection: Always exclude system folders from file operations:
- Distinctive: line 135
Use exclusion checks in all vault operation tools
- Distinctive: line 136
Tool Execution Order: When AI needs to perform multiple operations:
- Distinctive: line 137
Always prioritize read operations before destructive operations
- Distinctive: line 181
Use camelCase for variables/functions, PascalCase for classes/types, and kebab-case filenames aligned with their feature area.
- Distinctive: line 183
Use proper async/await patterns for all asynchronous operations
- Very specific: line 197
logger.log()andlogger.debug(): Only output when debug mode is enabled - Very specific: line 201
logger.error()andlogger.warn(): Always visible regardless of debug mode - Distinctive: line 202
Use for important errors and warnings that users should always see
- Very specific: line 207
Use logger.log() for debug information that helps development and troubleshooting
- Very specific: line 208
Use logger.error() for errors that indicate something went wrong
- Very specific: line 209
Use logger.warn() for warnings about deprecated features or potential issues
- Very specific: line 210
Never use native console.log() or console.debug() directly
- Very specific: line 249
Keep unit tests next to implementations and name them after the unit (models.test.ts, main.test.ts)
- Very specific: line 252
Run npm test before each PR and run the relevant evals/ eval harness tasks (see npm run eval and the eval-harness skill) after touching …
- Distinctive: line 258
When adding features, ensure tests cover:
- Distinctive: line 276
Update README.md with new feature description
- Very specific: line 277
Create or update relevant user guides in docs/
- Distinctive: line 278
Add examples and use cases
- Distinctive: line 279
Update table of contents and navigation
- Distinctive: line 283
Update code examples to reflect changes
- Distinctive: line 287
Remove or rewrite documentation for removed features
- Distinctive: line 288
Delete archived docs (users auto-update, no need for old docs)
- Distinctive: line 289
Update migration guides if needed
- Distinctive: line 293
Update code examples in guides
- Distinctive: line 294
Document breaking changes clearly
- Distinctive: line 308
When planning new features:
- Very specific: line 347
Write concise, imperative commit subjects (Fix agent session cleanup, Improve prompt builder).
- Very specific: line 348
Commit generated artifacts (manifest.json, styles.css, versions.json) alongside source changes.
- Very specific: line 363
When writing or modifying UI styles, consume these tokens rather than hardcoding color or reaching for raw var(--obsidian-var) / magic …
- Common: line 377
Never commit API keys or vault data.
- Very specific: line 378
Document new network calls or permissions in docs/ when adding features or capabilities
- Distinctive: line 379
Always use native Obsidian API calls when possible.
≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Repository Guidelines
Project Overview
Obsidian Gemini Scribe is an Obsidian plugin that integrates Google's Gemini AI models for AI-driven assistance within Obsidian. It provides context-aware chat, document summarization, text rewriting, and IDE-style completions.
Project Structure & Module Organization
src/contains TypeScript plugin code;src/main.tsis the entry point with domain folders such asagent/,api/,tools/,ui/, andservices/, plus shared utilities inutils/.docs/hosts user and operator guides;prompts/ships default agent prompts;evals/holds the agent eval harness (task fixtures, runner, baselines).- Unit tests live in the
test/directory mirroringsrc/structure as*.test.ts; generated artifacts (main.js,manifest.json,styles.css) stay in the repo root for Obsidian.
Commands
Development
npm install # Install dependencies
npm run dev # Development build with watch mode
npm run build # Production build (generates refs, runs TypeScript check, then bundles)
npm run generate-refs # Regenerate help references from docs/ (runs automatically in build/dev)
npm test # Run Vitest tests
npm run format # Format code with Prettier
npm run format-check # Check formatting without changes
npm run lint # Lint with ESLint (eslint-plugin-obsidianmd recommended preset)
npm run lint:fix # Auto-fix ESLint violations where possible
npm run lint:cycles # Fail on circular imports (madge --circular over src/; baseline is 0 and CI's lint workflow enforces it)
npm run check:fields # Advisory unread-field scan (per-field reachability over src/ interface and type-alias fields; #1469). Always exits 0 — the report lands in the Unread-field workflow artifact. Suppress a genuinely write-only field with 'wiring:keep' on its declaration line. Flip to blocking after the noise rate has been measured for a release.
npm run knip # Detect unused files, exports, types, and dependencies (configured in knip.json; CI's knip workflow enforces it, so a newly-dead export fails the PR — put intentional public surface in knip.json rather than re-arguing it per PR; every knip.json ignore carries its justification written next to it; nothing may mark an export reachable except a real caller — not an entry-point declaration, not a test-only import, not a JSDoc @public/@beta/@alias tag, which the no-tags-as-reachability ESLint rule fails (#1525))
npm test # Vitest suite — includes the provider-docs-sync guard (test/api/provider-docs-sync.test.ts): the provider pipeline diagram in AGENTS.md and .claude/guidelines/invariants.md must name exactly the ModelProvider union from src/api/providers/registry.ts, so adding a provider fails the PR until the diagrams are updated (#1567)
npm run install:test-vault # Copy built artifacts into test vault. Target precedence: TEST_VAULT_PLUGIN_DIR (exact plugin folder, validated by id) > TEST_VAULT_DIR (vault root, scanned by id) > default ~/Obsidian/Test Vault
npm run translate # Regenerate AI translations in src/i18n/ (needs GOOGLE_API_KEY; not part of build/test — runs in its own `Update UI translations` workflow on en.ts changes, or manually)IMPORTANT: Always run npm install first if you encounter TypeScript errors or missing module errors during build. The build requires all dependencies in node_modules to be present. If you get errors about missing modules like obsidian, @google/genai, handlebars, or tslib, run npm install before attempting npm run build again.
Testing
- Run single test:
npm test -- path/to/test.ts - Coverage floor:
npm test -- --coverageenforces thresholds invitest.config.ts(baseline 73.56/73.17/71.00/71.96 for lines/statements/branches/functions; see thethresholdscomment there). CI fails when coverage falls below the floor.autoUpdateratchets the numbers up automatically on full runs; a legitimate drop (e.g. a dead-code sweep) is fixed by editing the number in the same PR, with the reason in the PR body — same convention as.console-baseline. - Manual integration:
npm run evaldrives the eval harness against a real Obsidian instance to validate agent toolchains before shipping (seeevals/README.mdand the eval-harness skill) - Typecheck test files before pushing test changes: the CI lint job runs
npm run typecheck:test(tsc --project tsconfig.test.json), which type-checks thetest/tree and catches errors the production build misses —npm run buildusestsc -skipLibCheckand excludes tests. A green localnpm run builddoes not guarantee CI passes; runnpm run typecheck:testtoo. (Classic trap: an expression-bodied arrow like(t) => arr.push(t)returnsnumber, notvoid, and only the test typecheck flags it.)
Versioning & Releases
For the full release process, use the release skill (from the maintainerd plugin; the
repo-specific release gates live in .claude/guidelines/release.md).
Architecture
Core Pattern: Factory + Decorator
src/main.ts → ModelClientFactory.createFromPlugin() → GeminiClient | OllamaClient | OpenAIClient | AnthropicClient → RetryDecorator → ModelApiThe plugin uses a factory pattern (ModelClientFactory in src/api/factory.ts) to create model API clients, wrapped with a retry decorator (RetryDecorator) for resilience. Since the settings redesign, routing is a dense table keyed by feature: settings.features: Record<FeatureId, { provider, model }> (FeatureId = chat, summary, completions, rewrite, webSearch, deepResearch, rag, imageGen), with settings.defaultProvider never serving a request: it only seeds entries missing at settings load (sanitizeFeatureRoutes in src/main.ts), and otherwise feeds display/re-init helpers. The factory resolves each call's provider via featureProvider(settings, featureId) in the leaf module src/api/feature-routing.ts, which returns null when the feature is off (provider === 'none') or unsupported/unconfigured — there is no silent fallback; an unservable feature stays off rather than being re-routed to another provider — then instantiates the ModelApi implementation for the resolved ModelProvider (one client class per provider id; the union lives in src/api/providers/registry.ts). The model comes from resolveFeatureModel(settings, featureId) in src/models.ts, which falls back to the provider's role default only when the stored value is ''. All API implementations follow the ModelApi interface. Retry logic (exponential backoff) is a fixed policy in RetryDecorator, not settings-driven. Each provider lives in its own package under src/api/providers/{gemini,ollama,openai,anthropic}/; the capability matrix lives in src/api/providers/registry.ts and routing helpers live in src/api/feature-routing.ts. Decoding of incoming conversation history — the Gemini Content shape plus the legacy { role, text | message } entry — is shared by the non-Gemini clients in the leaf module src/api/providers/history-walk.ts; OllamaClient/OpenAIClient/AnthropicClient call walkHistoryEntry() (Anthropic passing { acceptsPdf: true }) and then only emit their own message type, so the provider-agnostic half cannot drift between them (the Gemini client speaks Content natively and has no such step). Two shared cross-provider leaves deliberately live at the src/api/providers/ package root, outside the per-provider packages: history-walk.ts (above) and tool-id-ledger.ts (called by AnthropicClient and OpenAIClient).
Key Components
- API Layer (
src/api/): Factory pattern (ModelClientFactory) for creating provider-appropriate model API clients, decorator pattern (RetryDecorator) for resilience, and interface abstraction (ModelApi) for consistent API interactions. Provider-specific code is encapsulated undersrc/api/providers/{gemini,ollama,openai,anthropic}/. - Feature Modules: Separate modules for chat, completions (
completions.ts), summary (summary.ts), and rewrite (rewrite.ts) - Context System (
src/files/index.ts,ScribeFile): Builds context from the active file or explicit file list (no link traversal — the agent follows links dynamically via theread_filetool) - History (
src/history/): Markdown-based conversation history with Handlebars templates for agent sessions, stored in[state-folder]/Agent-Sessions/(legacy note-centric chat history from v3.x remains in[state-folder]/History/)- Reasoning persistence: model "thinking" is stored on
GeminiConversationEntry.thoughtsand serialized as a collapsed> [!reasoning]-callout. Turns with a message (user prompt, final answer) carry a##header + Message Info table; streamlined reasoning-only turns are just the bare[!reasoning]callout with no header. Parser invariant (SessionHistory.parseHistoryContent): entries are identified by their callout ([!user]/[!assistant]/[!reasoning]), not a##header, and the parser walks every callout in a----delimited section (reasoning + tool callouts flow together with no dividers). This keeps the divider-free activity stream, headerless reasoning, and legacy per-entry files all round-tripping — preserve it, and cover format changes with old-format fixtures intest/agent/session-history.test.ts.
- Reasoning persistence: model "thinking" is stored on
- Custom Prompts (
src/prompts/): User-defined prompt templates stored in[state-folder]/Prompts/ - Agent mode (
src/agent/,src/tools/): AI agent with tool calling capabilities- Session management with persistent history; new session IDs use
session_<UUID>from Web Crypto, while existingsession_idvalues in saved history are preserved. - Tool registry and execution engine
- Vault operations tools with permission system
- Google Search integration (separate from function calling)
- Web fetch tool using Google's URL Context API
- Session-level permission system for bypassing confirmations
- Tool loop detection to prevent infinite execution cycles
- MCP server integration with stdio and Streamable HTTP transports (
src/mcp/) - Agent skills system for extensible AI capabilities (see below)
AgentLoop(src/agent/agent-loop.ts) — UI-agnostic class that drives the tool-execution loop after the initial model response. Handles iteration, history construction (via pure helpers inagent-loop-helpers.ts), follow-up requests, empty-response retry, cancellation polling, andagentEventBusemissions. UI side effects flow through optionalAgentLoopHooks(onToolBatchStart,onToolCallStart,onToolCallComplete,onToolCounted,onFollowUpRequestStart,onModelReasoning,onBudgetUpdate,onFollowUpChunk,onFollowUpStreamReady,onMidLoopCompaction).onModelReasoningfires when an intermediate follow-up response carries model reasoning but continues to another tool batch (the "why I'm calling these tools" thinking); the terminal response's reasoning is returned onAgentLoopResult.thoughtsinstead.onFollowUpStreamReadyfires on the streaming follow-up path with the liveStreamingResponsewhen it starts and withnullonce it settles, so the UI can register it in the samecurrentStreamingResponseslot the Stop button cancels — letting Stop halt a mid-stream follow-up immediately instead of waiting forstream.complete(see #1054). The agent view renders both into the tool activity block, interleaved with tool rows. After every tool batch,AgentLoopcallsContextManager.prepareHistorywithprotectFromIndexpinned to the start of the current turn's tool-loop turns, so long tool chains can be compacted mid-flight without ever folding the in-flightfunctionCall/thoughtSignaturecontinuity into a summary (only turns from before the loop started are eligible);onMidLoopCompactionfires when that happens so the UI can surface the same "Context Compacted" notice as the pre-turn path (see #662). The tool sort reorders execution only: the replayed turn pair is order-locked to the model's emitted order, andbuildToolHistoryTurnsenforces that by realigning results ontotoolCallsvia thesourceIndexstamped pre-sort, so the two arrays are never assumed parallel (callers pass the emitted array the indices came from, never the sorted one), and a cancelled batch gets a syntheticfunctionResponseper unrun call rather than an unpairedfunctionCall(see #1499). Callers own: the initial API call, persisting the final response (UI saves to session history; headless callers may write a file), and any UI rendering.AgentLoopOptions.confirmationProvideris required — UI callers pass theAgentView(which implementsIConfirmationProvider), headless callers pass an auto-approve/deny provider; the engine never looks it up on the plugin. The agent view (AgentViewTools) is a thin adapter over this loop. Headless callers (e.g. scheduled-task runners) should consumeAgentLooprather than reimplementing the loop. The lazyrequire('./agent-factory')insideAgentLoop.runis a deliberate pattern to break the import cycle betweenAgentFactoryand the loop — keep top-level imports unless a cycle forces it.
- Session management with persistent history; new session IDs use
- Attachment Pipeline (
src/ui/agent-view/agent-view.ts,src/ui/agent-view/agent-view-ui.ts,src/ui/agent-view/agent-view-attachments.ts,src/ui/agent-view/inline-attachment.ts,src/utils/file-classification.ts): Unified drag-and-drop and paste pipeline for file attachments- Files dropped or pasted into the agent view are classified by extension using
classifyFile()fromfile-classification.ts - Text files (
.md,.ts,.json,.base,.canvas, etc.) → context chips (AI reads content) - Gemini-supported binary files (images, audio, video, PDF) → base64 inline attachments sent to the model via
inlineAttachmentsonExtendedModelRequest - Unsupported files (
.zip,.exe, etc.) → user notification - Cumulative 20 MB size limit enforced across vault drops, external drops, and paste
- Folder drops recursively expand and classify all contained files
InlineAttachment(renamed fromImageAttachment) holds base64 data, MIME type, and optional vault path- Image attachments show thumbnails; non-image attachments show Lucide icon + filename label
- Binary file awareness in tools:
ReadFileToolusesclassifyFile()to detect binary files and reads them viavault.readBinary(), returninginlineDataonToolResult. The tool execution pipeline (agent-view-tools.ts) injects these asinlineDataparts alongsidefunctionResponsein conversation history. This allows the agent to autonomously read images, audio, video, and PDFs encountered via tools without manual drag-and-drop. OBSIDIAN_TEXT_EXTENSIONSmap infile-classification.tsclassifies.baseand.canvasas text
- Files dropped or pasted into the agent view are classified by extension using
Model Configuration
- Models defined in
src/models.tswith automatic version migration - Different models for different tasks (chat, summary, completions, rewrite)
- Settings changes trigger full plugin reload
Important Patterns
- Obsidian API First: Always use built-in Obsidian API functions when available instead of low-level operations (for detailed Obsidian API guidance, use the obsidian-plugin-development skill):
- Use
vault.getMarkdownFiles()instead ofvault.adapter.list() - Use
app.fileManager.processFrontMatter()for frontmatter manipulation - Use
vault.getAbstractFileByPath()for file operations - Use
app.metadataCachefor file metadata access - Use
app.fileManager.renameFile()for renaming files (preserves metadata) - Use
app.workspace.openLinkText()for clickable file links in views
- Use
- File Operations: Always use Obsidian's normalized paths and metadata cache
- Error Handling: API calls wrapped with retry logic and exponential backoff
- Prompts: Handlebars templates in
prompts/directory, loaded as text files - Debouncing: Completions use a 500ms debounce (the
codemirror-companion-extensiondefault; the plugin doesn't overridedelay) to prevent excessive API calls - State Management: Plugin instance holds all component references with proper cleanup
- Folder Structure: Plugin uses structured state folder:
[state-folder]/- Main plugin state folder (default:gemini-scribe)[state-folder]/History/- Legacy note-centric chat history files (v3.x)[state-folder]/Prompts/- Custom prompt templates[state-folder]/Agent-Sessions/- Agent mode session files[state-folder]/Skills/- Agent skill packages (agentskills.io format)[state-folder]/Scheduled-Tasks/- Scheduled task definitions and run output[state-folder]/Background-Tasks/- Output from background deep-research and image-gen tasks[state-folder]/Hooks/- Lifecycle hook definitions and run output (created whenhooksEnabledistrue)- Automatic migration for existing users from flat structure
src/services/state-folder.tsowns this layout (#1382) —STATE_SUBFOLDERS(folder names),STATE_FILES(root-level state files:AGENTS.md,example-prompts.json,rag-index-cache.json, the two debug logs), andstateFolderPath(settings, ...segments), which appliesnormalizePathonce. Compose every state path through the helper; never writenormalizePath(`${settings.historyFolder}/Prompts`)by hand.FolderInitializercreatesEAGER_SUBFOLDERS, derived from the same constants —Hooks/is created on demand only whenhooksEnabledis on, and read-only legacyHistory/is never created by current code. Both omissions are documented in the module.FolderInitializeris the single creator ofScheduled-Tasks/andScheduled-Tasks/Runs/(#1540):LifecycleService.setup()runs the folder pass before refreshing the managers on a re-init, soScheduledTaskManager.initializeconsumes the folders rather than re-creating them. It is a leaf module, so any consumer can import it without a cycle.
- System Folder Protection: Always exclude system folders from file operations:
- The plugin state folder (
settings.historyFolder) - The
.obsidianconfiguration folder - Use exclusion checks in all vault operation tools
- The plugin state folder (
- Tool Execution Order: When AI needs to perform multiple operations:
- Always prioritize read operations before destructive operations
- Sort tool calls to execute reads before writes/deletes
- Prevents race conditions where files are deleted before being read
- Loop Detection: Tool execution includes loop detection to prevent infinite cycles:
- Tracks identical tool calls within time windows
- Fixed thresholds (3 identical calls within 30 seconds, set in
ToolLoopDetector), always on — not user-configurable - Session-specific tracking with automatic cleanup
- Blocked calls are flagged with
loopDetected: trueonToolResultand emit atoolLoopDetectedevent on the agent bus AgentLoopcounts those fires per turn and aborts the turn afterAGENT_LOOP_ABORT_THRESHOLD(currently 3) — the result comes back withloopAborted: trueand a user-visible notice, which the UI displays but does not persist to session history
- Tool Implementation:
ToolExecutionContext.pluginis typedObsidianGemini; no cast is needed in tool implementations — usecontext.plugindirectly. - YAML Frontmatter: Agent instructions include guidance for respecting YAML frontmatter when modifying files
- The AI is trained to place "top of note" content after frontmatter blocks (defined in
prompts/agentRulesPrompt.hbs) - YAML frontmatter must start with
---on line 1 and end with--- - Content is only placed before frontmatter when explicitly instructed to modify frontmatter
- Agent Skills (
src/services/skill-manager.ts,src/tools/skill-tools.ts): Extensible skill system following the agentskills.io specification
- Skills are self-contained packages stored in
[state-folder]/Skills/<skill-name>/SKILL.md SkillManagerhandles discovery, metadata parsing, content loading, resource reading, creation, and name validation- Uses progressive disclosure: skill summaries (name + description) are injected into the agent system prompt; full instructions are loaded on-demand via
activate_skill - Three tools:
activate_skill(read-only, loads instructions or resources),create_skill(creates new skill directories with valid SKILL.md), andedit_skill(updates an existing skill's description or instruction body, with diff-review confirmation) - Skill names must be lowercase alphanumeric with hyphens, 1-64 chars, no consecutive/leading/trailing hyphens
- Frontmatter parsing uses Obsidian's native
metadataCacheAPI scripts/directories are treated as read-only reference material (no execution in Obsidian)- Bundled help references are auto-generated:
scripts/generate-help-references.mjsscansdocs/guide/anddocs/reference/to producesrc/services/generated-help-references.tsat build time. Adding or removing a markdown file in those directories automatically updates the help skill — no manual edits tobundled-skills.tsorSKILL.mdare needed.
- UI String Localization (i18n) (
src/i18n/): All static user-facing UI strings (settings, modals, agent view, command names, notices, tooltips) go throught(key, vars?)fromsrc/i18n/index.ts— never hardcode user-visible English. Model-facing strings (prompts, tool descriptions), strings persisted to vault files (history callout markers, frontmatter), logger output, and strings compared with===stay English.src/i18n/en.tsis the source of truth (key →{message, context}); per-language files (ru.ts,pt-br.ts, …) are AI-generated bynpm run translate(scripts/translate.mjs, needsGOOGLE_API_KEY) and committed. The script only retranslates keys whose English source hash changed (src/i18n/translation-state.json), so hand-refined translations survive regeneration. Locale comes from Obsidian'sgetLanguage()with exact → base-language → English fallback. When adding a UI string to a migrated area: add the key + context toen.tsand uset(). You can runnpm run translatelocally and commit the regenerated files, but you don't have to — theUpdate UI translationsGitHub Action (.github/workflows/i18n-translate.yml) regenerates them automatically whenen.tslands on master (using theGOOGLE_API_KEYrepo secret) and opens achore(i18n)PR for review. It also runs weekly and can be dispatched manually. When adding a language: add a row toLANGUAGESinscripts/translate.mjs, register the export insrc/i18n/index.ts, and runnpm run translate -- --langs <code>(or trigger the workflow with thatlangsinput). - Plugin type surface — no imports of
main.ts(src/types/plugin.ts,src/types/plugin-services.ts): Components never reference the concrete plugin class —import type { ObsidianGemini } from '../types/plugin'(a leaf interface), neverimport type ObsidianGemini from '../main'.main.tsimports nearly the whole codebase, so a type reference back to it folds the module graph into hundreds of cycles (#1155). The interface declares only core members (settings,apiKey,checkInitialized, …); every service/manager handle (logger,sessionManager, …) is contributed by module augmentation insrc/types/plugin-services.ts— an orphan, type-only module nothing imports (it participates via tsconfiginclude), which keepstypes/plugin.tsa true leaf. When adding a service handle to the plugin class inmain.ts, mirror it inplugin-services.ts; the class'simplements ObsidianGeminiApiclause fails to compile until the two are in sync. More broadly, when a manager and a sibling module (runner, registry, UI section) need the same type or helper, put it in a leaf module both import (services/hook-types.ts,services/skill-types.ts,ui/settings/writer-types.ts,types/settings.ts) instead of importing the manager back. The whole graph is kept acyclic bynpm run lint:cycles(madge) in the lint CI workflow. - Derive entity parameter types — don't re-list fields (
src/services/hook-types.ts,src/services/scheduled-tasks/types.ts): For a vault-backed entity managed throughFileBackedFeatureManager(Hook,ScheduledTask, …), derive the manager's create/update param types from the entity interface —Omit<Entity, 'filePath' | EntityDefaultedField> & Partial<Pick<Entity, EntityDefaultedField>>for create,Partial<Omit<EntityCreateParams, 'slug'>>for update — and have the management modal build its save payload once for both the create and update branch, rather than writing the field list out longhand. A hand-written param type still compiles when the entity gains a field, so the compiler cannot tell you the new field is unsettable or that one save branch dropped it (#1273, #1314, #1322). Full rule in .claude/guidelines/coding.md.
Coding Style & Naming Conventions
For in-depth code quality standards (DRY, SOLID, error handling, performance, security), use the code-review skill.
- TypeScript-first codebase; group modules by domain and add barrel exports only when they simplify imports.
- Format with Prettier (
npm run format): 2-space indent, 120-column width, semicolons, single quotes, trailing commas. - Lint with ESLint (
npm run lint, autofix withnpm run lint:fix): theeslint-plugin-obsidianmdrecommended preset enforces Obsidian-specific best practices (memory-leak prevention, cross-window safety, command-ID conventions, manifest validation, etc.). Pervasive rules that need broader refactoring are temporarily disabled ineslint.config.mjs— see thePERVASIVE_OBSIDIANMD_RULES_TODOblock for follow-up items. Never inline-disable anobsidianmd/*rule,no-explicit-any,no-deprecated,@microsoft/sdl/no-inner-html,no-console,no-alert, or the other rules in theeslint-comments/no-restricted-disablelist ineslint.config.mjs: the Obsidian community-plugin audit rejects a release that carries one (4.12.0 failed it over ~20 such disables), and lint fails on them locally with the same list. Fix the code instead. A genuine exception becomes a file-scoped override ineslint.config.mjswith its reason. Rules outside that list may still take a line-scoped disable with a-- reason. One repo-specificno-restricted-syntaxentry (PATH_CONTAINMENT_RULE) flags hand-rolled path containment through two selectors — the concatenation passed straight to.startsWith()(p.startsWith(folder + '/'), #1402) and the same concatenation hoisted into a local first (const prefix = folder + '/', #1482), which the first selector cannot see — and points atisPathInFolder()insrc/utils/file-utils.ts, which owns that predicate and is the rule's only exempt file; a deliberate strict-descendant site takes a line-scoped disable carrying the reason, and both selectors share the one rule so that per-rule disable still suppresses only its own site. A third entry (PROVIDER_LITERAL_RULE, #1317) fails any string literal'gemini'/'ollama'/'openai'/'anthropic'outside the modules that own provider identity (src/api/providers/**, thesrc/apirouting/credential/status leaves,factory.ts,models.ts,types/) — non-provider modules must ask the registry (getCapabilities/featureProvider/PROVIDER_IDS) instead of branching on provider names; pre-existing sites carry line-scoped disables with reasons that come out as #1308/#703 clear them, and the exemption file list plus per-rule disable caveat live in the comment on the rule ineslint.config.mjs. A second repo-specific entry (no-tags-as-reachability, a local rule in the same file) fails any@public/@beta/@aliasJSDoc tag undersrc/— the plugin has no public API and the tags would exempt exports from the knip gate (#1525). .editorconfigenforces LF endings and tabbed Markdown/config; avoid hand-editing generated bundles.- Use camelCase for variables/functions, PascalCase for classes/types, and kebab-case filenames aligned with their feature area.
- Handle TypeScript errors properly - ensure all properties are correctly typed
- Use proper async/await patterns for all asynchronous operations
Console Logging
The plugin uses a dedicated Logger service (src/utils/logger.ts) that respects the debug mode setting. This approach avoids global console patching, preventing conflicts with other plugins and Obsidian's debugging tools.
Accessing the Logger:
- Plugin components:
this.plugin.logger - Tool implementations:
context.plugin.logger(via ToolExecutionContext) - Utility functions: Accept logger as parameter
Logger Methods:
logger.log()andlogger.debug(): Only output when debug mode is enabled- Automatically filtered based on settings.debugMode
- Prefixed with
[Gemini Scribe]for easy identification
logger.error()andlogger.warn(): Always visible regardless of debug mode- Use for important errors and warnings that users should always see
- Critical failures, API errors, and data integrity issues
Best Practices:
- Use
logger.log()for debug information that helps development and troubleshooting - Use
logger.error()for errors that indicate something went wrong - Use
logger.warn()for warnings about deprecated features or potential issues - Never use native
console.log()orconsole.debug()directly - Pass logger instance to utility functions that need logging
Examples:
// ✅ Good - in plugin components
this.plugin.logger.log('Processing file:', file.path);
this.plugin.logger.debug('Tool execution context:', context);
// ✅ Good - in tool implementations
async execute(params: any, context: ToolExecutionContext) {
const plugin = context.plugin;
plugin.logger.log('Executing tool with params:', params);
}
// ✅ Good - in utility functions
export function processData(logger: Logger, data: any) {
logger.log('Processing data:', data);
}
// ✅ Good - always visible for critical issues
this.plugin.logger.error('Failed to load API key:', error);
this.plugin.logger.warn('Model deprecated, using fallback');
// ❌ Bad - using console directly
console.log('Debug message');
// ❌ Bad - manual debug mode checks (logger handles this)
if (this.plugin.settings.debugMode) {
this.plugin.logger.log('Debug message');
}Testing Guidelines
- Vitest (esbuild-powered) for TypeScript support
- jsdom environment for DOM testing
- Test pattern:
test/**/?(*.)+(spec|test).[tj]s - Keep unit tests next to implementations and name them after the unit (
models.test.ts,main.test.ts) - Assert observable behavior of prompts, services, and tool orchestration; add regression coverage for bugs
- Extend shared fixtures under
__mocks__/when mocking new APIs - Run
npm testbefore each PR and run the relevantevals/eval harness tasks (seenpm run evaland the eval-harness skill) after touching agent or tool code
For manual testing procedures (desktop symlink setup, mobile testing, smoke test checklists), see docs/contributing/testing.md. For runtime debugging and plugin inspection, use the obsidian-cli skill.
Testing Focus
When adding features, ensure tests cover:
- Core utility functions
- API error scenarios with retry behavior
- File context tree building and circular reference prevention
- Prompt generation with proper template rendering
Development Practices
Documentation Maintenance
🚨 CRITICAL - DOCUMENTATION IS MANDATORY 🚨
Documentation updates are REQUIRED, not optional. Every code change MUST include corresponding documentation updates in the same PR/commit.
When making ANY change:
- Feature Addition:
- Update README.md with new feature description
- Create or update relevant user guides in
docs/ - Add examples and use cases
- Update table of contents and navigation
- Feature Updates:
- Modify ALL affected documentation files
- Update code examples to reflect changes
- Revise screenshots or diagrams if needed
- Feature Removal:
- Remove or rewrite documentation for removed features
- Delete archived docs (users auto-update, no need for old docs)
- Update migration guides if needed
- API/Settings Changes:
- Update settings-reference.md
- Update code examples in guides
- Document breaking changes clearly
Documentation Review Checklist:
- README.md updated if user-facing change
- Relevant guides in
docs/updated - Code examples tested and current
- Settings documentation matches actual defaults
- No references to removed features
- Internal doc links not broken Remember: Outdated documentation is worse than no documentation. If you change code, you MUST update docs.
Implementation Planning
When planning new features:
- Create detailed implementation plans for significant features
- Include plans directly in GitHub issues rather than separate files
- Structure plans with:
- Architecture overview
- Core components with code examples
- Integration points
- Testing strategy (unit and integration tests)
- Migration considerations
- Timeline estimates
Example: See issue #90 for the custom prompt system implementation plan.
This keeps technical planning centralized and accessible for all contributors.
Autonomous issue pipeline (auto-dev)
The auto-dev skill (from the maintainerd plugin, configured via .claude/maintainerd.json) runs one tick of an unattended issue-to-PR pipeline: it triages open issues for readiness (asking clarifying questions where needed), posts implementation plans for maintainer approval, builds the oldest approved issue into a PR, and addresses CodeRabbit/human review feedback on the open automated PR. When CodeRabbit is rate-limited and a ready, CI-green automated PR has sat past the review window (autoDev.fallbackReviewMinutes, default 60) with no review activity at all, a tick posts its own fresh-eyes fallback self-review — fixing valid findings first, then summarizing in a marker comment with a ## Fallback review heading — so the maintainer still gets a review signal; it is clearly labeled a self-review, capped at one per PR, and never a formal GitHub approval. It never merges — merging is always the maintainer's act — and it keeps at most autoDev.maxPrsInFlight automated PRs (default 6) in flight at once, all awaiting the maintainer's merge.
- State lives in the
auto:*GitHub labels (auto:needs-info,auto:planned,auto:ready,auto:in-progress,auto:parked,auto:skip). Addauto:skipto any issue the pipeline should never touch; addauto:readyto approve a posted plan directly (an approving reply on the plan comment also works); addauto:parked(or reply "park it" to a park proposal) to hold an issue you're still thinking about — the pipeline leaves a parked issue alone until you remove the label or add a new comment. The pipeline builds approved (auto:ready) issues ahead of grooming the backlog, so a greenlit plan never waits behind triage. - Comments posted by the pipeline run under the maintainer's own GitHub account and are identified by a hidden
<!-- auto-dev -->marker — that marker, not authorship, distinguishes automated comments from human ones. - Scheduled runs are driven by a recurring scheduled task (in the cloud) that invokes the skill directly (
/auto-dev) each tick — there is no local runner script or permissions allowlist. Each run gets a fresh, disposable full-toolchain sandbox (git, Node,ghauthed as the maintainer); the skill owns its own setup (Step 0 puts the sandbox on a cleanmasterand installs deps) and the tick's exit report is the run's summary output. The safety that the old allowlist enforced now lives in the skill's invariants (the self-enforced hard prohibitions in invariant 5: never merge/close, force-push, push to master, release, touch non-auto:*labels, or run destructive shell). Set the schedule's cadence comfortably longer than a typical build tick — there is no lockfile, so the task must not overlap its own runs (theauto:*label state machine, the in-flight cap (autoDev.maxPrsInFlight), and Step 1's orphan age-gate are the backstop).
Driving the maintainer side from a Claude Code session
The maintainer's half of the pipeline (reviewing plans, approving, answering questions) is usually done through an interactive Claude Code session. The review-queue skill (from the maintainerd plugin, invoked as /review-queue or "review the pipeline") packages this whole flow into a guided loop: it surfaces everything blocked on you (open PR, plans, questions, parked, new issues) as a one-line worklist, you reply with an issue/PR number and your decision, and it executes that against the auto:* labels as you (never with the marker), looping until the queue is drained. It never merges on its own and never builds. The steps below are what that skill automates, and what to do when driving the maintainer side by hand. When asked to "review the pipeline", "check what auto-dev is waiting on", "approve the plan on #N", or similar:
- See the pipeline state:
gh issue list --label "auto:planned"(plans awaiting approval),--label "auto:needs-info"(questions/park proposals awaiting answers),--label "auto:ready"(build queue),--label "auto:in-progress"(being built),--label "auto:parked"(issues you've chosen to hold), plusgh pr list --state openfiltered toauto/branches for the PR(s) in flight. Pipeline comments are the ones starting with the hidden<!-- auto-dev -->marker; third-party bot comments (coderabbitai,dependabot, …) are ignored for state decisions; everything else is human input. - Approve a plan: reply on the issue with an explicit approval ("Approved.", optionally with amendments — "Approved, but use X instead of Y" counts and the pipeline incorporates the change), or add the
auto:readylabel directly. Request changes: reply with the changes; the next tick revises the plan. Answer questions onauto:needs-infoissues with an ordinary comment; the next tick incorporates it. - Never include the
<!-- auto-dev -->marker in comments posted on the maintainer's behalf. The pipeline classifies marker comments as its own output — a marked reply would be invisible to it as human input. Don't imitate the pipeline's comment templates either. - Taking an issue over manually: before implementing an issue yourself that carries
auto:planned/auto:ready(or answering its plan with "I'll do this one"), addauto:skip(or at minimum removeauto:ready) so a scheduled tick doesn't build it concurrently. Removeauto:skiplater to hand it back. - Reviewing the automated PR works like any PR review: comment on the PR; the next tick addresses the feedback and replies. A PR whose only review is the pipeline's fallback self-review (marker comment with a
## Fallback reviewheading) has had no independent eyes on it — give it a closer look before merging. Merging is always the human's call — the pipeline never merges, and a merge is what unblocks the next build. - Testing / manual ticks:
/auto-dev dry-runprints what a tick would do with zero side effects;/auto-devruns one live tick interactively under normal permission prompts.
Commit & Pull Request Guidelines
For creating pull requests, use the create-pr skill which enforces the PR template and runs all pre-flight checks.
- Write concise, imperative commit subjects (
Fix agent session cleanup,Improve prompt builder); reference issues/PRs with#123 - Commit generated artifacts (
manifest.json,styles.css,versions.json) alongside source changes;main.jsis a gitignored build output and is never committed. Usenpm run versionfor releases - MANDATORY: Include documentation updates in the same PR/commit as code changes (see Documentation Maintenance section)
- PRs should explain motivation, highlight user-visible impact, list automated/manual tests, and attach screenshots or vault clips for UI tweaks
- Flag reviewers who own the affected area and mention required follow-up or rollout notes
- PR descriptions should explicitly list which documentation files were updated
UI/UX Best Practices
For UI/UX guidelines, use the ui-ux-guidelines skill.
Design system
The plugin has a theme-adaptive design token layer at the top of styles.css
(body { --gs-* }) — semantic tokens aliasing Obsidian's theme variables, plus
plugin-owned spacing / radius / icon / motion scales and one Gemini brand accent.
When writing or modifying UI styles, consume these tokens rather than hardcoding
color or reaching for raw var(--obsidian-var) / magic numbers; keep the plugin
theme-adaptive. Icons are Lucide via setIcon(), sized from --gs-icon-* (lg =
18px is the default for toolbar/action icons — one size per context). The full
reference and token table live in
docs/contributing/design-system.md; every
surface is now on the tokens (with a few intentional raw-var holdouts). When making
a value-preserving CSS/token change,
verify no computed-value change in a live vault (obsidian eval +
getComputedStyle, or token-resolution equality) rather than eyeballing — that
catches token mis-maps and theme-scope bugs a screenshot won't.
Security & Configuration
- Never commit API keys or vault data; keep secrets in local Obsidian configuration
- Document new network calls or permissions in
docs/when adding features or capabilities - Always use native Obsidian API calls when possible. Documentation here: https://docs.obsidian.md/Home
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository8 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- eval-harnessSkill
- gemini-api-devSkill
- obsidian-cliSkill
- obsidian-plugin-developmentSkill
- plugin-testSkill
- triage-issuesSkill
- ui-ux-guidelinesSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

