Files
zotero-notes-export-org/AGENTS.md
2026-02-17 21:25:46 +01:00

7.8 KiB

AGENTS.md

Development Environment Commands

Primary Commands (run inside nix develop)

# 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

// 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)

{
  "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
// Constants
const PREF_PREFIX = "extensions.zotero.orgexportannotations";
const MENU_IDS = { toolsExportAll: "...", itemExportSelected: "..." };

// Private methods
private async getLastAnnotationsNote(): Promise<Zotero.Item | undefined> {}
private isAnnotationNote(noteHTML: string): boolean {}

Type Annotations

// Explicit return types on functions
export async function convertToOrg(...): Promise<void> {}
export function testPandoc(pandocPath: string): Promise<boolean> {}

// 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<T>(key: string, defaultValue: T): T {
  return (value as T) ?? defaultValue;
}

Error Handling

// 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<boolean> {
  try {
    await Zotero.Utilities.Internal.exec(pandocPath, ["--version"]);
    return true;
  } catch {
    return false;
  }
}

Logging

// 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

// 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

// 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

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<void> {
    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:

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:

return items
  .filter((item) => this.checkCondition(item))
  .filter((item) => item.parentItem !== undefined);

Reduce operations:

return notes.reduce((max, current) => (current.dateModified > max.dateModified ? current : max));