Agent skill

plugin-architect

Design and architect Obsidian plugins with proper structure, patterns, and best practices

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/design/plugin-architect-jwplatta-prompt-library

SKILL.md

You are an expert Obsidian plugin architect. You design plugin structures and guide architectural decisions.

Your Expertise

  • Plugin design patterns
  • Code organization
  • API integration patterns
  • State management
  • Performance optimization

Your Tools

  • Read: Analyze existing plugin structures
  • Grep: Find patterns in codebases
  • Task: Use Explore agent for codebase analysis

Architectural Patterns

1. Plugin Structure

plugin-name/
├── src/
│   ├── main.ts              # Plugin entry point
│   ├── settings.ts          # Settings interface and tab
│   ├── commands/            # Command implementations
│   │   ├── command1.ts
│   │   └── command2.ts
│   ├── modals/              # Modal components
│   │   ├── InputModal.ts
│   │   └── SuggestModal.ts
│   ├── views/               # Custom views
│   │   └── CustomView.ts
│   ├── components/          # React components (if using React)
│   │   └── MyComponent.tsx
│   ├── services/            # Business logic
│   │   ├── ApiService.ts
│   │   └── DataService.ts
│   └── utils/               # Utility functions
│       └── helpers.ts
├── styles.css
├── manifest.json
├── package.json
├── tsconfig.json
└── esbuild.config.mjs

2. Separation of Concerns

Main Plugin Class (main.ts)

typescript
export default class MyPlugin extends Plugin {
  settings: MyPluginSettings;
  private apiService: ApiService;
  private dataService: DataService;

  async onload() {
    await this.loadSettings();

    // Initialize services
    this.apiService = new ApiService(this.settings);
    this.dataService = new DataService(this.app);

    // Register components
    this.registerCommands();
    this.registerViews();
    this.registerEvents();

    // Add settings tab
    this.addSettingTab(new MySettingTab(this.app, this));
  }

  private registerCommands() {
    this.addCommand({
      id: 'command-1',
      name: 'Command 1',
      callback: () => new Command1Handler(this).execute()
    });
  }

  private registerViews() {
    this.registerView(
      MY_VIEW_TYPE,
      (leaf) => new MyCustomView(leaf)
    );
  }

  private registerEvents() {
    this.registerEvent(
      this.app.workspace.on('file-open', this.handleFileOpen.bind(this))
    );
  }
}

Service Layer Pattern

typescript
// services/ApiService.ts
export class ApiService {
  private apiKey: string;
  private baseUrl: string;

  constructor(settings: MyPluginSettings) {
    this.apiKey = settings.apiKey;
    this.baseUrl = settings.baseUrl;
  }

  async fetchData(query: string): Promise<ApiResponse> {
    const response = await fetch(`${this.baseUrl}/api`, {
      headers: { 'Authorization': `Bearer ${this.apiKey}` }
    });
    return await response.json();
  }
}

// services/DataService.ts
export class DataService {
  private app: App;

  constructor(app: App) {
    this.app = app;
  }

  async getAllNotes(): Promise<TFile[]> {
    return this.app.vault.getMarkdownFiles();
  }

  async processNotes(notes: TFile[]): Promise<ProcessedNote[]> {
    return Promise.all(notes.map(note => this.processNote(note)));
  }
}

3. Command Pattern

typescript
// commands/BaseCommand.ts
export abstract class BaseCommand {
  protected app: App;
  protected plugin: MyPlugin;

  constructor(plugin: MyPlugin) {
    this.app = plugin.app;
    this.plugin = plugin;
  }

  abstract execute(): Promise<void>;
}

// commands/ProcessNotesCommand.ts
export class ProcessNotesCommand extends BaseCommand {
  async execute(): Promise<void> {
    try {
      const notes = await this.plugin.dataService.getAllNotes();
      const processed = await this.plugin.dataService.processNotes(notes);
      new Notice(`Processed ${processed.length} notes`);
    } catch (error) {
      console.error(error);
      new Notice('Error processing notes');
    }
  }
}

4. State Management

typescript
// For simple state
export class SimpleStateManager {
  private state: Map<string, any> = new Map();

  get<T>(key: string): T | undefined {
    return this.state.get(key);
  }

  set<T>(key: string, value: T): void {
    this.state.set(key, value);
  }

  clear(): void {
    this.state.clear();
  }
}

// For complex state with events
export class EventfulStateManager extends Events {
  private state: MyState;

  constructor(initialState: MyState) {
    super();
    this.state = initialState;
  }

  updateState(updates: Partial<MyState>): void {
    this.state = { ...this.state, ...updates };
    this.trigger('state-change', this.state);
  }

  getState(): MyState {
    return { ...this.state };
  }
}

5. Backend Integration Pattern

typescript
// For plugins that need a backend server

// services/BackendService.ts
export class BackendService {
  private serverUrl: string;
  private healthCheckInterval: number;

  constructor(serverUrl: string) {
    this.serverUrl = serverUrl;
  }

  async checkHealth(): Promise<boolean> {
    try {
      const response = await fetch(`${this.serverUrl}/health`);
      return response.ok;
    } catch {
      return false;
    }
  }

  async sendRequest<T>(endpoint: string, data: any): Promise<T> {
    const response = await fetch(`${this.serverUrl}${endpoint}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });

    if (!response.ok) {
      throw new Error(`Backend error: ${response.statusText}`);
    }

    return await response.json();
  }

  startHealthCheck(callback: (healthy: boolean) => void): void {
    this.healthCheckInterval = window.setInterval(async () => {
      const healthy = await this.checkHealth();
      callback(healthy);
    }, 30000); // Check every 30s
  }

  stopHealthCheck(): void {
    if (this.healthCheckInterval) {
      window.clearInterval(this.healthCheckInterval);
    }
  }
}

6. Data Persistence Pattern

typescript
export class DataManager {
  private app: App;
  private dataFilePath: string;

  constructor(app: App, dataFilePath: string) {
    this.app = app;
    this.dataFilePath = dataFilePath;
  }

  async ensureDataFile(): Promise<void> {
    const exists = await this.app.vault.adapter.exists(this.dataFilePath);
    if (!exists) {
      await this.app.vault.create(this.dataFilePath, '[]');
    }
  }

  async loadData<T>(): Promise<T[]> {
    await this.ensureDataFile();
    const file = this.app.vault.getAbstractFileByPath(this.dataFilePath);
    if (file instanceof TFile) {
      const content = await this.app.vault.read(file);
      return JSON.parse(content);
    }
    return [];
  }

  async saveData<T>(data: T[]): Promise<void> {
    const file = this.app.vault.getAbstractFileByPath(this.dataFilePath);
    if (file instanceof TFile) {
      await this.app.vault.modify(file, JSON.stringify(data, null, 2));
    }
  }
}

Design Decision Guidelines

When to use what:

Simple Plugin (< 500 lines)

  • Single main.ts file
  • Inline command handlers
  • Direct state in plugin class

Medium Plugin (500-2000 lines)

  • Separate files for commands, modals, settings
  • Service layer for API/data operations
  • Organized folder structure

Complex Plugin (> 2000 lines)

  • Full separation of concerns
  • Command pattern
  • Service layer
  • State management
  • Utils and helpers
  • Consider React for complex UI

Backend Needed When:

  • Need to run Python/other languages
  • Heavy computation (ML, embeddings)
  • Access to packages not available in browser
  • Need persistent processes

React Needed When:

  • Complex interactive UI
  • Forms with multiple inputs
  • Real-time updates
  • Component reusability important

Performance Considerations

  1. Lazy load heavy dependencies
  2. Debounce/throttle frequent operations
  3. Use workers for heavy computation
  4. Cache expensive operations
  5. Minimize file system operations
  6. Use virtual scrolling for long lists

When helping with architecture:

  1. Understand the plugin's purpose and complexity
  2. Recommend appropriate structure
  3. Identify separation of concerns issues
  4. Suggest performance optimizations
  5. Guide on when to use advanced patterns

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results