Search docs...
DocsDevelopmentDeveloper Guides

Developer Guides

Extending scraper plugins, registering LLM agent tools, and adding migrations.

4 min readEdit on GitHub

LeadForge OS supports modular extension of its scraper plugins, agent tools, database schemas, and IPC channels. This guide details the steps to implement and register these modules.

Adding a Worker Plugin

All long-running, CPU-heavy tasks must run inside forked Node.js child processes. The scheduler routes tasks using the worker plugin registry.

  1. Create a new file under apps/desktop/src/main/workers/plugins/ (e.g. slack-notifier.ts).
  2. Implement the BasePlugin interface:
    typescript
    import { Job } from '@leadforge/schema';
    import { BasePlugin } from '../base-plugin';
     
    export class SlackNotifierPlugin extends BasePlugin {
      async run(job: Job): Promise<void> {
        const { channel, message } = JSON.parse(job.payload);
     
        // Update progress periodically
        this.updateProgress(30);
     
        // Perform the operation
        await sendSlackMessage(channel, message);
     
        this.updateProgress(100);
      }
     
      async cleanup(): Promise<void> {
        // Perform cleanup on cancellation (e.g. close connections)
      }
    }
  3. Register the plugin in apps/desktop/src/main/workers/plugin-registry.ts:
    typescript
    import { SlackNotifierPlugin } from './plugins/slack-notifier';
     
    pluginRegistry.register('notify:slack', new SlackNotifierPlugin());

Adding an LLM Tool

Tools are executable actions exposed to agents.

  1. Define the parameters schema in packages/agent-core/src/tools/schemas.ts:
    typescript
    import { z } from 'zod';
     
    export const SendSlackSchema = z.object({
      channel: z.string().describe('Target slack channel name'),
      message: z.string().describe('Message text to post')
    });
  2. Implement the executor class under packages/agent-runtime/src/tools/ implementing the ToolExecutor interface:
    typescript
    import { SendSlackSchema } from '@leadforge/agent-core';
     
    export class SendSlackTool {
      name = 'send_slack_notification';
      description = 'Send a slack message to a channel';
      schema = SendSlackSchema;
     
      async execute(params: z.infer<typeof SendSlackSchema>): Promise<string> {
        // Trigger the background scheduler job or perform direct execution
        return `Message sent to ${params.channel}`;
      }
    }

Creating an Agent

Agents orchestrate tools to complete a user task.

  1. Register prompt templates under packages/ai/src/prompts/templates/.
  2. Define the agent class under packages/agent-runtime/src/research-agent.ts:
    typescript
    import { BaseAgent } from './base-agent';
     
    export class SlackAgent extends BaseAgent {
      async run(taskDescription: string): Promise<AgentResponse> {
        const toolResult = await this.invokeTool('send_slack_notification', { ... });
        return this.assembleResponse(toolResult);
      }
    }

Creating a Repository

We use the Repository pattern to wrap local SQLite database queries.

  1. Create the repository class under apps/desktop/src/main/database/repositories/ (e.g. campaign-repo.ts):
    typescript
    import { Database } from 'better-sqlite3';
    import { Campaign } from '@leadforge/schema';
     
    export class CampaignRepository {
      constructor(private db: Database) {}
     
      findById(id: string): Campaign | null {
        const row = this.db.prepare('SELECT * FROM campaigns WHERE id = ?').get(id);
        return row ? (row as Campaign) : null;
      }
     
      save(campaign: Campaign): void {
        this.db
          .prepare(
            `
          INSERT INTO campaigns (id, name, status)
          VALUES ($id, $name, $status)
          ON CONFLICT(id) DO UPDATE SET name = $name, status = $status
        `
          )
          .run(campaign);
      }
    }

Adding Database Migrations

Database schema migrations are applied sequentially and idempotently on application boot by runner.ts.

  1. Open apps/desktop/src/main/database/runner.ts.
  2. Append a new SQL string to the MIGRATIONS array:
    typescript
    const MIGRATIONS = [
      // ... existing migrations (do not edit these as they are already run)
      `
      -- Migration 024: Create Slack Campaigns table
      CREATE TABLE IF NOT EXISTS slack_campaigns (
        id TEXT PRIMARY KEY,
        channel TEXT NOT NULL,
        createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
      );
      `
    ];
  3. Verify that tests pass using pnpm test. The smoke test automatically runs these migrations on an in-memory database to check for syntax errors.

Adding IPC Handlers

IPC handlers expose main-process Node capabilities (database queries, network triggers, settings updates) to the React frontend.

  1. Register the channel contract in packages/schema/src/ipc/channels.ts:
    typescript
    export const SLACK_CHANNELS = {
      SEND_MESSAGE: 'slack:send-message'
    } as const;
  2. Implement the listener in apps/desktop/src/main/ipc/:
    typescript
    import { ipcMain } from 'electron';
    import { SLACK_CHANNELS } from '@leadforge/schema';
     
    ipcMain.handle(SLACK_CHANNELS.SEND_MESSAGE, async (event, payload) => {
      // Perform operation
      return { success: true };
    });
  3. Expose the channel in apps/desktop/src/preload/index.ts.
  4. Call the channel in your React components:
    typescript
    const response = await window.ipc.invoke('slack:send-message', { ... });