# AGENTS.md ## Development Environment Commands ### Primary Commands (run inside `nix develop`) ```bash # Development nix develop # Enter dev shell with Node.js and tooling npm run start # Start dev server with hot reload npm run build # Build production XPI to .scaffold/build/ # Code Quality npm run lint # Run ESLint on TypeScript source npm run lint:fix # Auto-fix ESLint issues npm run format # Format with Prettier npm run format:check # Check formatting without changes # Release npm run release # Bump version, commit, tag, and build ``` ### Testing This project does not currently have automated tests. To test manually: 1. Run `npm run build` to create the XPI 2. Install the XPI in Zotero 7 3. Test export functionality manually via Tools menu --- ## Code Style Guidelines ### Imports and Exports ```typescript // Use relative imports for internal modules import { OrgExporter } from "./modules/exporter"; import type { Prefs } from "./modules/prefs"; // Use type-only imports where possible import type { AddonInfo } from "./addon"; // Use default exports for single main entity export class OrgExportAnnotationsAddon {} export function createHooks() {} // Use named exports for utilities export async function convertToOrg() {} export async function testPandoc() {} ``` **Rules:** - Prefer named exports for functions - Use `import type` for type-only imports - Import from `./modules/` with relative paths - No namespace imports (`import * as X`) - No `export default` for modules (use named exports) ### Formatting (Prettier) ```json { "semi": true, // Always use semicolons "singleQuote": false, // Use double quotes "tabWidth": 2, // 2 space indentation "trailingComma": "es5", // Trailing commas in valid ES5 "printWidth": 100 // Max line length } ``` ### TypeScript Configuration - **Strict mode enabled**: All type checking rules active - **Target**: ES2022 with DOM types - **Module**: ESNext with bundler resolution - **Source maps**: Enabled for debugging - **Output**: Bundled to `.scaffold/build/addon/content/scripts/` ### Naming Conventions | Entity | Convention | Examples | | --------------- | ---------------- | ---------------------------------------------------------- | | Classes | PascalCase | `OrgExporter`, `Prefs`, `AddonInfo` | | Functions | camelCase | `convertToOrg`, `isAnnotationNote`, `ensureNotesDirectory` | | Methods | camelCase | `init()`, `exportAttachment()`, `cleanupTempFile()` | | Private Methods | camelCase | Prefix with `private` keyword | | Constants | UPPER_SNAKE_CASE | `PREF_PREFIX`, `MENU_IDS` | | Interfaces | PascalCase | `AddonInfo`, `Hooks` | | Types | PascalCase | `export type Preference = ...` | | File Names | kebab-case | `converter.ts`, `exporter.ts`, `preferences.js` | ```typescript // Constants const PREF_PREFIX = "extensions.zotero.orgexportannotations"; const MENU_IDS = { toolsExportAll: "...", itemExportSelected: "..." }; // Private methods private async getLastAnnotationsNote(): Promise {} private isAnnotationNote(noteHTML: string): boolean {} ``` ### Type Annotations ```typescript // Explicit return types on functions export async function convertToOrg(...): Promise {} export function testPandoc(pandocPath: string): Promise {} // Type assertions for Zotero API returns const title = attachment.parentItem.getField("title") as string; const citationKey = item.getField("citationKey") as string; // Optional chaining with nullish coalescing const citationKey = note.parentItem?.getField("citationKey") || "temp"; // Generic type parameters private getPref(key: string, defaultValue: T): T { return (value as T) ?? defaultValue; } ``` ### Error Handling ```typescript // Standard pattern with logging try { await operation(); Zotero.OrgExportAnnotations.log("Operation succeeded"); } catch (error) { Zotero.OrgExportAnnotations.error("Operation failed", error as Error); // Optionally re-throw throw error; } // Empty catch blocks must use underscore prefix try { await cleanup(); } catch (_error) { // Cleanup errors are non-critical } // Return error values instead of throwing for utility functions export async function testPandoc(pandocPath: string): Promise { try { await Zotero.Utilities.Internal.exec(pandocPath, ["--version"]); return true; } catch { return false; } } ``` ### Logging ```typescript // Debug logging (respects debug mode) Zotero.OrgExportAnnotations.log("Message", ...args); // Error logging (always logged) Zotero.OrgExportAnnotations.error("Message", error as Error); // Warning logging (always logged) Zotero.OrgExportAnnotations.warn("Message"); // Verbose debugging with objects Zotero.OrgExportAnnotations.log("Processing item", { id, title }); ``` ### Async/Await Patterns ```typescript // Use for...of for sequential async operations for (const attachment of attachments) { if (await this.exportAttachment(attachment)) { count++; } } // Early returns with await if (!item.isFileAttachment()) return false; if (!path) throw new Error("Notes path not configured"); ``` ### Zotero API Usage ```typescript // Non-null assertions for known APIs Zotero.BetterNotes!.api.convert.note2md(note, notesPath); // Zotero utilities for file operations await Zotero.File.putContentsAsync(path, content); const content = await Zotero.File.getContentsAsync(path); await Zotero.Utilities.Internal.exec(command, args); // Zotero preferences Zotero.Prefs.get(key, global); Zotero.Prefs.set(key, value, global); // Zotero items const item = Zotero.Items.get(id); await Zotero.Items.trashTx(id); ``` ### Class Structure ```typescript export class MyClass { // Public properties public property!: string; // Private properties private internal = { enabled: false }; // Constructor with dependency injection constructor(private deps: Dependencies) {} // Public methods public async doSomething(): Promise { try { // Implementation } catch (error) { this.handleError(error as Error); } } // Private helper methods private handleError(error: Error): void { Zotero.OrgExportAnnotations.error("Error occurred", error); } } ``` ### ESLint Rules - **Unused variables**: Error, but ignored if prefixed with `_` - **Explicit any**: Warning (prefer specific types) - **No-empty**: Error for empty catch blocks (use `catch (_error)`) ### File Organization ``` src/ ├── index.ts # Main entry point ├── addon.ts # Core addon class ├── hooks.ts # Lifecycle hooks └── modules/ ├── prefs.ts # Preferences management ├── exporter.ts # Core export logic ├── converter.ts # Format conversion ├── notifier.ts # Zotero event handlers └── menu.ts # UI menu registration ``` ### Common Patterns **Preference getters/setters**: ```typescript get notesPath(): string { const path = this.getPref("notesPath", ""); if (path && path.startsWith("~")) { return path.replace("~", PathUtils.profileDir); } return path; } set notesPath(value: string) { this.setPref("notesPath", value); } ``` **Filter operations**: ```typescript return items .filter((item) => this.checkCondition(item)) .filter((item) => item.parentItem !== undefined); ``` **Reduce operations**: ```typescript return notes.reduce((max, current) => (current.dateModified > max.dateModified ? current : max)); ```