Plugin Examples
Complete, production-ready plugin implementations with detailed explanations and customization options.
Basic Plugin Structure
Every Lokud plugin follows this basic structure:
// plugin-template.js
export default {
// Plugin metadata
name: 'my-plugin',
version: '1.0.0',
description: 'Description of what this plugin does',
author: 'Your Name',
// Plugin configuration options
config: {
// Default configuration values
enabled: true,
option1: 'default-value',
option2: true
},
// Initialize plugin when Lokud starts
async initialize(lokud, config) {
console.log('Plugin initialized with config:', config);
// Setup code here
},
// Clean up when plugin is disabled/unloaded
async cleanup() {
console.log('Plugin cleaning up');
// Cleanup code here
},
// Register event handlers
events: {
'record.created': async (event) => {
// Handle record creation
},
'record.updated': async (event) => {
// Handle record updates
}
}
};Auto-Daily Notes Plugin
Automatically creates daily note entries with customizable templates.
// plugins/auto-daily-notes.js
/**
* Auto Daily Notes Plugin
* Automatically creates a daily note entry every morning
* Supports custom templates and time scheduling
*/
export default {
name: 'auto-daily-notes',
version: '1.0.0',
description: 'Automatically create daily note entries',
author: 'Lokud Team',
config: {
enabled: true,
// Time to create daily note (24-hour format)
creationTime: '06:00',
// Base to create notes in
targetBase: 'Daily Notes',
// Template to use
template: `# {{date}}
## Today's Focus
-
## Notes
## Tasks
- [ ]
## Goals
-
## Reflections
---
Created automatically by Auto Daily Notes plugin`,
// Days to create notes (0=Sunday, 6=Saturday)
activeDays: [1, 2, 3, 4, 5], // Monday-Friday
// Timezone for scheduling
timezone: 'UTC'
},
// Store scheduled job reference
scheduledJob: null,
async initialize(lokud, config) {
console.log('Initializing Auto Daily Notes plugin');
// Validate config
if (!config.targetBase) {
throw new Error('targetBase configuration is required');
}
// Check if target base exists
const base = await lokud.getBase(config.targetBase);
if (!base) {
throw new Error(`Base "${config.targetBase}" not found`);
}
// Schedule daily note creation
this.scheduledJob = lokud.scheduler.schedule(
config.creationTime,
async () => await this.createDailyNote(lokud, config),
{ timezone: config.timezone }
);
console.log(`Daily notes scheduled for ${config.creationTime}`);
},
async cleanup() {
// Cancel scheduled job
if (this.scheduledJob) {
this.scheduledJob.cancel();
console.log('Daily note creation job cancelled');
}
},
async createDailyNote(lokud, config) {
const now = new Date();
const dayOfWeek = now.getDay();
// Check if today is an active day
if (!config.activeDays.includes(dayOfWeek)) {
console.log('Skipping daily note creation - not an active day');
return;
}
// Format date
const dateStr = lokud.utils.formatDate(now, 'YYYY-MM-DD');
const displayDate = lokud.utils.formatDate(now, 'MMMM D, YYYY');
// Check if note already exists
const base = await lokud.getBase(config.targetBase);
const existing = await base.findRecord({
field: 'Date',
value: dateStr
});
if (existing) {
console.log(`Daily note for ${dateStr} already exists`);
return;
}
// Process template
const content = this.processTemplate(config.template, {
date: displayDate,
dateShort: dateStr,
dayOfWeek: lokud.utils.formatDate(now, 'dddd'),
weekNumber: lokud.utils.getWeekNumber(now)
});
// Create new record
try {
await base.createRecord({
'Date': dateStr,
'Title': `Daily Note - ${displayDate}`,
'Content': content
});
console.log(`Created daily note for ${dateStr}`);
// Send notification
await lokud.notifications.send({
title: 'Daily Note Created',
message: `Your daily note for ${displayDate} is ready!`,
priority: 'low'
});
} catch (error) {
console.error('Error creating daily note:', error);
// Send error notification
await lokud.notifications.send({
title: 'Daily Note Creation Failed',
message: error.message,
priority: 'high'
});
}
},
processTemplate(template, variables) {
let processed = template;
// Replace all {{variable}} placeholders
for (const [key, value] of Object.entries(variables)) {
const regex = new RegExp(`{{${key}}}`, 'g');
processed = processed.replace(regex, value);
}
return processed;
},
// Add command to manually create daily note
commands: {
'create-daily-note': {
description: 'Manually create today\'s daily note',
async execute(lokud, config) {
await this.createDailyNote(lokud, config);
}
}
}
};Habit Tracker Plugin
Track daily habits with streak counting and reminders.
// plugins/habit-tracker.js
/**
* Habit Tracker Plugin
* Tracks daily habits, calculates streaks, and sends reminders
*/
export default {
name: 'habit-tracker',
version: '1.0.0',
description: 'Track daily habits with streak counting',
author: 'Lokud Team',
config: {
enabled: true,
// Base containing habits
habitsBase: 'Habits',
// Time to send daily reminder
reminderTime: '20:00',
// Enable notifications
sendReminders: true,
// Streak threshold for celebration
celebrationStreak: 7
},
reminderJob: null,
async initialize(lokud, config) {
console.log('Initializing Habit Tracker plugin');
// Validate configuration
const base = await lokud.getBase(config.habitsBase);
if (!base) {
throw new Error(`Base "${config.habitsBase}" not found`);
}
// Schedule daily reminder
if (config.sendReminders) {
this.reminderJob = lokud.scheduler.schedule(
config.reminderTime,
async () => await this.sendHabitReminders(lokud, config)
);
}
// Register event handlers
this.registerEventHandlers(lokud, config);
console.log('Habit Tracker plugin initialized');
},
async cleanup() {
if (this.reminderJob) {
this.reminderJob.cancel();
}
},
registerEventHandlers(lokud, config) {
// Update streaks when habit is marked complete
lokud.on('record.updated', async (event) => {
if (event.base !== config.habitsBase) return;
const record = event.record;
const changes = event.changes;
// Check if Last Completed field was updated
if (changes['Last Completed']) {
await this.updateStreaks(lokud, config, record);
}
});
},
async updateStreaks(lokud, config, habitRecord) {
const lastCompleted = new Date(habitRecord['Last Completed']);
const today = new Date();
today.setHours(0, 0, 0, 0);
const lastCompletedDate = new Date(lastCompleted);
lastCompletedDate.setHours(0, 0, 0, 0);
const daysDiff = Math.floor(
(today - lastCompletedDate) / (1000 * 60 * 60 * 24)
);
const base = await lokud.getBase(config.habitsBase);
const currentStreak = habitRecord['Current Streak'] || 0;
const longestStreak = habitRecord['Longest Streak'] || 0;
let newStreak = currentStreak;
if (daysDiff === 0) {
// Completed today - increment streak
newStreak = currentStreak + 1;
} else if (daysDiff === 1) {
// Completed yesterday - continue streak
newStreak = currentStreak + 1;
} else {
// Streak broken - reset
newStreak = 1;
}
// Update record
await base.updateRecord(habitRecord.id, {
'Current Streak': newStreak,
'Longest Streak': Math.max(newStreak, longestStreak)
});
// Check for celebration milestone
if (newStreak % config.celebrationStreak === 0) {
await this.celebrateStreak(lokud, habitRecord, newStreak);
}
},
async sendHabitReminders(lokud, config) {
const base = await lokud.getBase(config.habitsBase);
// Get all active habits
const habits = await base.findRecords({
filter: {
field: 'Active',
operator: 'is',
value: true
}
});
if (habits.length === 0) return;
// Check which habits haven't been completed today
const today = new Date();
today.setHours(0, 0, 0, 0);
const pendingHabits = habits.filter(habit => {
const lastCompleted = habit['Last Completed'];
if (!lastCompleted) return true;
const lastDate = new Date(lastCompleted);
lastDate.setHours(0, 0, 0, 0);
return lastDate < today;
});
if (pendingHabits.length > 0) {
const habitNames = pendingHabits
.map(h => h['Habit Name'])
.join(', ');
await lokud.notifications.send({
title: 'Daily Habit Reminder',
message: `Don't forget: ${habitNames}`,
priority: 'normal',
actions: [
{
label: 'Mark Complete',
action: 'open',
target: config.habitsBase
}
]
});
}
},
async celebrateStreak(lokud, habitRecord, streak) {
await lokud.notifications.send({
title: `${streak}-Day Streak!`,
message: `Congratulations on your ${streak}-day streak for "${habitRecord['Habit Name']}"!`,
priority: 'high',
celebration: true
});
},
// Add commands
commands: {
'check-habits': {
description: 'Check today\'s habit completion status',
async execute(lokud, config) {
const base = await lokud.getBase(config.habitsBase);
const habits = await base.findRecords({
filter: { field: 'Active', operator: 'is', value: true }
});
const today = new Date();
today.setHours(0, 0, 0, 0);
const completed = [];
const pending = [];
habits.forEach(habit => {
const lastCompleted = habit['Last Completed'];
if (lastCompleted) {
const lastDate = new Date(lastCompleted);
lastDate.setHours(0, 0, 0, 0);
if (lastDate >= today) {
completed.push(habit['Habit Name']);
} else {
pending.push(habit['Habit Name']);
}
} else {
pending.push(habit['Habit Name']);
}
});
console.log('Completed:', completed);
console.log('Pending:', pending);
return {
completed,
pending,
completionRate: habits.length > 0
? (completed.length / habits.length * 100).toFixed(1) + '%'
: '0%'
};
}
}
}
};Gmail Integration Plugin
Sync emails to Lokud for better organization.
// plugins/gmail-integration.js
/**
* Gmail Integration Plugin
* Sync important emails to Lokud bases
* Requires Gmail API credentials
*/
import { google } from 'googleapis';
export default {
name: 'gmail-integration',
version: '1.0.0',
description: 'Integrate Gmail with Lokud',
author: 'Lokud Team',
config: {
enabled: false, // Requires API setup
// Gmail API credentials (from Google Cloud Console)
credentials: {
clientId: '',
clientSecret: '',
redirectUri: 'http://localhost:3000/oauth/callback'
},
// Refresh token (obtained during OAuth flow)
refreshToken: '',
// Base to sync emails to
targetBase: 'Emails',
// Label to watch (null = all)
watchLabel: 'Important',
// Sync interval in minutes
syncInterval: 15,
// Maximum emails to fetch per sync
maxResults: 50,
// Create tasks from starred emails
createTasksFromStarred: true,
tasksBase: 'Tasks'
},
gmailClient: null,
syncJob: null,
async initialize(lokud, config) {
console.log('Initializing Gmail Integration plugin');
// Validate configuration
if (!config.credentials.clientId || !config.credentials.clientSecret) {
throw new Error('Gmail API credentials not configured');
}
if (!config.refreshToken) {
console.warn('No refresh token - OAuth flow required');
return;
}
// Initialize Gmail client
const oauth2Client = new google.auth.OAuth2(
config.credentials.clientId,
config.credentials.clientSecret,
config.credentials.redirectUri
);
oauth2Client.setCredentials({
refresh_token: config.refreshToken
});
this.gmailClient = google.gmail({
version: 'v1',
auth: oauth2Client
});
// Test connection
try {
await this.gmailClient.users.getProfile({ userId: 'me' });
console.log('Gmail connection successful');
} catch (error) {
throw new Error(`Gmail connection failed: ${error.message}`);
}
// Schedule periodic sync
this.syncJob = lokud.scheduler.interval(
config.syncInterval * 60 * 1000, // Convert to milliseconds
async () => await this.syncEmails(lokud, config)
);
// Run initial sync
await this.syncEmails(lokud, config);
console.log('Gmail Integration plugin initialized');
},
async cleanup() {
if (this.syncJob) {
this.syncJob.cancel();
}
},
async syncEmails(lokud, config) {
console.log('Starting email sync...');
try {
// Build query
let query = '';
if (config.watchLabel) {
query = `label:${config.watchLabel}`;
}
// Add date filter to only get recent emails
const lastSync = await lokud.storage.get('gmail_last_sync');
if (lastSync) {
const date = new Date(lastSync);
const dateStr = date.toISOString().split('T')[0].replace(/-/g, '/');
query += ` after:${dateStr}`;
}
// Fetch emails
const response = await this.gmailClient.users.messages.list({
userId: 'me',
q: query,
maxResults: config.maxResults
});
if (!response.data.messages) {
console.log('No new emails to sync');
return;
}
const base = await lokud.getBase(config.targetBase);
let syncedCount = 0;
// Process each email
for (const message of response.data.messages) {
// Get full message details
const email = await this.gmailClient.users.messages.get({
userId: 'me',
id: message.id,
format: 'full'
});
// Extract email data
const emailData = this.parseEmail(email.data);
// Check if email already exists
const existing = await base.findRecord({
field: 'Gmail ID',
value: message.id
});
if (existing) {
// Update existing record
await base.updateRecord(existing.id, {
'Labels': emailData.labels,
'Is Read': !emailData.isUnread,
'Is Starred': emailData.isStarred
});
} else {
// Create new record
await base.createRecord({
'Gmail ID': message.id,
'Subject': emailData.subject,
'From': emailData.from,
'To': emailData.to,
'Date': emailData.date,
'Body': emailData.body,
'Labels': emailData.labels,
'Is Read': !emailData.isUnread,
'Is Starred': emailData.isStarred,
'Thread ID': emailData.threadId,
'Gmail Link': `https://mail.google.com/mail/u/0/#inbox/${message.id}`
});
syncedCount++;
// Create task if starred and option enabled
if (emailData.isStarred && config.createTasksFromStarred) {
await this.createTaskFromEmail(lokud, config, emailData, message.id);
}
}
}
// Update last sync timestamp
await lokud.storage.set('gmail_last_sync', new Date().toISOString());
console.log(`Email sync complete. Synced ${syncedCount} new emails.`);
// Send notification if new emails
if (syncedCount > 0) {
await lokud.notifications.send({
title: 'Gmail Sync Complete',
message: `Synced ${syncedCount} new email${syncedCount > 1 ? 's' : ''}`,
priority: 'low'
});
}
} catch (error) {
console.error('Email sync failed:', error);
await lokud.notifications.send({
title: 'Gmail Sync Failed',
message: error.message,
priority: 'high'
});
}
},
parseEmail(emailData) {
const headers = emailData.payload.headers;
// Extract headers
const getHeader = (name) => {
const header = headers.find(h => h.name.toLowerCase() === name.toLowerCase());
return header ? header.value : '';
};
// Get email body
let body = '';
if (emailData.payload.body.data) {
body = Buffer.from(emailData.payload.body.data, 'base64').toString();
} else if (emailData.payload.parts) {
// Multi-part email
const textPart = emailData.payload.parts.find(
part => part.mimeType === 'text/plain'
);
if (textPart && textPart.body.data) {
body = Buffer.from(textPart.body.data, 'base64').toString();
}
}
return {
subject: getHeader('Subject'),
from: getHeader('From'),
to: getHeader('To'),
date: new Date(getHeader('Date')),
body: body.substring(0, 5000), // Limit body length
labels: emailData.labelIds || [],
isUnread: emailData.labelIds?.includes('UNREAD') || false,
isStarred: emailData.labelIds?.includes('STARRED') || false,
threadId: emailData.threadId
};
},
async createTaskFromEmail(lokud, config, emailData, messageId) {
try {
const tasksBase = await lokud.getBase(config.tasksBase);
await tasksBase.createRecord({
'Task Name': `Email: ${emailData.subject}`,
'Description': `From: ${emailData.from}\n\n${emailData.body.substring(0, 500)}...`,
'Status': 'To Do',
'Priority': 'Medium',
'Source': 'Gmail',
'Reference Link': `https://mail.google.com/mail/u/0/#inbox/${messageId}`
});
console.log(`Created task from email: ${emailData.subject}`);
} catch (error) {
console.error('Error creating task from email:', error);
}
},
// Add commands
commands: {
'gmail-auth': {
description: 'Start Gmail OAuth authentication flow',
async execute(lokud, config) {
const oauth2Client = new google.auth.OAuth2(
config.credentials.clientId,
config.credentials.clientSecret,
config.credentials.redirectUri
);
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: ['https://www.googleapis.com/auth/gmail.readonly']
});
console.log('Open this URL to authorize:');
console.log(authUrl);
return { authUrl };
}
},
'gmail-sync-now': {
description: 'Manually trigger email sync',
async execute(lokud, config) {
await this.syncEmails(lokud, config);
}
}
}
};Backup Plugin
Automatically backup workspace data to local storage or cloud.
// plugins/backup.js
/**
* Backup Plugin
* Automatically backup workspace data
* Supports local filesystem and cloud storage
*/
import fs from 'fs/promises';
import path from 'path';
import { createWriteStream } from 'fs';
import archiver from 'archiver';
export default {
name: 'backup',
version: '1.0.0',
description: 'Automatic workspace backups',
author: 'Lokud Team',
config: {
enabled: true,
// Backup schedule (cron format)
schedule: '0 2 * * *', // 2 AM daily
// Backup location
backupPath: './backups',
// Backup format: 'json' or 'zip'
format: 'zip',
// Maximum number of backups to keep
maxBackups: 7,
// Include attachments
includeAttachments: true,
// Cloud backup settings (optional)
cloud: {
enabled: false,
provider: 's3', // 's3', 'dropbox', 'gdrive'
credentials: {}
},
// Bases to backup (empty = all)
includeBases: [],
// Bases to exclude
excludeBases: []
},
backupJob: null,
async initialize(lokud, config) {
console.log('Initializing Backup plugin');
// Create backup directory if it doesn't exist
try {
await fs.mkdir(config.backupPath, { recursive: true });
} catch (error) {
console.error('Failed to create backup directory:', error);
throw error;
}
// Schedule backups
this.backupJob = lokud.scheduler.cron(
config.schedule,
async () => await this.performBackup(lokud, config)
);
console.log(`Backups scheduled: ${config.schedule}`);
},
async cleanup() {
if (this.backupJob) {
this.backupJob.cancel();
}
},
async performBackup(lokud, config) {
console.log('Starting backup...');
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupName = `lokud-backup-${timestamp}`;
// Get workspace data
const workspaceData = await this.getWorkspaceData(lokud, config);
let backupPath;
if (config.format === 'zip') {
backupPath = await this.createZipBackup(
config.backupPath,
backupName,
workspaceData,
config.includeAttachments
);
} else {
backupPath = await this.createJsonBackup(
config.backupPath,
backupName,
workspaceData
);
}
console.log(`Backup created: ${backupPath}`);
// Upload to cloud if enabled
if (config.cloud.enabled) {
await this.uploadToCloud(backupPath, config.cloud);
}
// Clean old backups
await this.cleanOldBackups(config.backupPath, config.maxBackups);
// Send success notification
await lokud.notifications.send({
title: 'Backup Complete',
message: `Workspace backed up successfully to ${backupName}`,
priority: 'low'
});
} catch (error) {
console.error('Backup failed:', error);
await lokud.notifications.send({
title: 'Backup Failed',
message: error.message,
priority: 'high'
});
}
},
async getWorkspaceData(lokud, config) {
const data = {
metadata: {
version: lokud.version,
timestamp: new Date().toISOString(),
workspace: lokud.workspace.name
},
bases: {}
};
// Get all bases
const allBases = await lokud.getBases();
for (const base of allBases) {
// Check if should include this base
if (config.includeBases.length > 0 &&
!config.includeBases.includes(base.name)) {
continue;
}
if (config.excludeBases.includes(base.name)) {
continue;
}
// Get all records from base
const records = await base.getAllRecords();
data.bases[base.name] = {
config: base.config,
records: records
};
console.log(`Backed up ${records.length} records from ${base.name}`);
}
return data;
},
async createJsonBackup(backupPath, backupName, data) {
const filePath = path.join(backupPath, `${backupName}.json`);
await fs.writeFile(
filePath,
JSON.stringify(data, null, 2),
'utf8'
);
return filePath;
},
async createZipBackup(backupPath, backupName, data, includeAttachments) {
const zipPath = path.join(backupPath, `${backupName}.zip`);
return new Promise((resolve, reject) => {
const output = createWriteStream(zipPath);
const archive = archiver('zip', {
zlib: { level: 9 }
});
output.on('close', () => {
console.log(`Backup archive created: ${archive.pointer()} bytes`);
resolve(zipPath);
});
archive.on('error', (err) => {
reject(err);
});
archive.pipe(output);
// Add workspace data
archive.append(
JSON.stringify(data, null, 2),
{ name: 'workspace-data.json' }
);
// Add attachments if enabled
if (includeAttachments) {
// Implementation depends on attachment storage system
// archive.directory('attachments/', 'attachments');
}
archive.finalize();
});
},
async cleanOldBackups(backupPath, maxBackups) {
const files = await fs.readdir(backupPath);
// Filter backup files
const backupFiles = files
.filter(f => f.startsWith('lokud-backup-'))
.map(f => ({
name: f,
path: path.join(backupPath, f),
time: fs.stat(path.join(backupPath, f)).then(s => s.mtime)
}));
// Wait for all stat calls
for (const file of backupFiles) {
file.time = await file.time;
}
// Sort by time (newest first)
backupFiles.sort((a, b) => b.time - a.time);
// Delete old backups
if (backupFiles.length > maxBackups) {
const toDelete = backupFiles.slice(maxBackups);
for (const file of toDelete) {
await fs.unlink(file.path);
console.log(`Deleted old backup: ${file.name}`);
}
}
},
async uploadToCloud(filePath, cloudConfig) {
// Implementation depends on cloud provider
console.log(`Uploading to ${cloudConfig.provider}...`);
// Example for S3:
// const s3 = new AWS.S3(cloudConfig.credentials);
// await s3.upload({
// Bucket: cloudConfig.bucket,
// Key: path.basename(filePath),
// Body: fs.createReadStream(filePath)
// }).promise();
},
commands: {
'backup-now': {
description: 'Manually trigger backup',
async execute(lokud, config) {
await this.performBackup(lokud, config);
}
},
'list-backups': {
description: 'List all available backups',
async execute(lokud, config) {
const files = await fs.readdir(config.backupPath);
const backupFiles = files.filter(f => f.startsWith('lokud-backup-'));
const backups = await Promise.all(
backupFiles.map(async (file) => {
const filePath = path.join(config.backupPath, file);
const stats = await fs.stat(filePath);
return {
name: file,
size: (stats.size / 1024 / 1024).toFixed(2) + ' MB',
created: stats.mtime
};
})
);
return backups;
}
}
}
};Custom Field Validation Plugin
Add custom validation rules to fields.
// plugins/field-validation.js
/**
* Field Validation Plugin
* Add custom validation rules to fields
*/
export default {
name: 'field-validation',
version: '1.0.0',
description: 'Custom field validation rules',
author: 'Lokud Team',
config: {
enabled: true,
// Validation rules by base and field
rules: {
// Example:
// 'Tasks': {
// 'Task Name': {
// minLength: 5,
// maxLength: 100,
// pattern: '^[A-Z].*', // Must start with capital letter
// custom: async (value) => {
// // Custom validation logic
// return value.includes('TODO') ? 'Remove TODO from task name' : null;
// }
// },
// 'Due Date': {
// custom: async (value, record) => {
// // Must be in the future
// if (new Date(value) < new Date()) {
// return 'Due date must be in the future';
// }
// return null;
// }
// }
// }
},
// Show validation errors as notifications
showNotifications: true,
// Prevent saving if validation fails
strictMode: true
},
async initialize(lokud, config) {
console.log('Initializing Field Validation plugin');
// Register validation hooks
lokud.on('record.beforeCreate', async (event) => {
await this.validateRecord(lokud, config, event);
});
lokud.on('record.beforeUpdate', async (event) => {
await this.validateRecord(lokud, config, event);
});
console.log('Field Validation plugin initialized');
},
async validateRecord(lokud, config, event) {
const { base, record, changes } = event;
// Get rules for this base
const baseRules = config.rules[base];
if (!baseRules) return;
const errors = [];
// Get fields to validate
const fieldsToValidate = changes || record;
// Validate each field
for (const [fieldName, value] of Object.entries(fieldsToValidate)) {
const rules = baseRules[fieldName];
if (!rules) continue;
const error = await this.validateField(
fieldName,
value,
rules,
record
);
if (error) {
errors.push(`${fieldName}: ${error}`);
}
}
// Handle errors
if (errors.length > 0) {
const errorMessage = errors.join('\n');
if (config.showNotifications) {
await lokud.notifications.send({
title: 'Validation Error',
message: errorMessage,
priority: 'high'
});
}
if (config.strictMode) {
// Cancel the operation
event.cancel = true;
event.cancelReason = errorMessage;
}
}
},
async validateField(fieldName, value, rules, record) {
// Skip if value is empty and not required
if (!value && !rules.required) {
return null;
}
// Required check
if (rules.required && !value) {
return 'This field is required';
}
// Min length
if (rules.minLength && value.length < rules.minLength) {
return `Minimum length is ${rules.minLength} characters`;
}
// Max length
if (rules.maxLength && value.length > rules.maxLength) {
return `Maximum length is ${rules.maxLength} characters`;
}
// Pattern matching
if (rules.pattern) {
const regex = new RegExp(rules.pattern);
if (!regex.test(value)) {
return rules.patternMessage || 'Invalid format';
}
}
// Min/Max for numbers
if (typeof value === 'number') {
if (rules.min !== undefined && value < rules.min) {
return `Minimum value is ${rules.min}`;
}
if (rules.max !== undefined && value > rules.max) {
return `Maximum value is ${rules.max}`;
}
}
// Custom validation function
if (rules.custom && typeof rules.custom === 'function') {
const error = await rules.custom(value, record);
if (error) {
return error;
}
}
return null;
}
};Installation Instructions
Installing a Plugin
-
Save the plugin file to your workspace’s
plugins/directory -
Add to workspace configuration:
# workspace.yaml
plugins:
- name: plugin-name
enabled: true
config:
# Plugin-specific configuration- Reload workspace to activate the plugin
Development Tips
- Use lokud.logger instead of console.log for better logging
- Handle errors gracefully - always catch and log errors
- Clean up resources in the cleanup() method
- Test thoroughly before deploying to production
- Document configuration options clearly
Plugin Best Practices
- Performance: Avoid blocking operations in event handlers
- Error Handling: Always use try-catch blocks
- Configuration: Provide sensible defaults
- Notifications: Don’t spam users with notifications
- Security: Validate all user inputs
- Testing: Write unit tests for your plugins
Available APIs
Plugins have access to the Lokud API:
// Base operations
lokud.getBases()
lokud.getBase(name)
base.getAllRecords()
base.createRecord(data)
base.updateRecord(id, data)
base.deleteRecord(id)
// Events
lokud.on(event, handler)
lokud.emit(event, data)
// Scheduling
lokud.scheduler.schedule(time, callback)
lokud.scheduler.interval(ms, callback)
lokud.scheduler.cron(expression, callback)
// Notifications
lokud.notifications.send(options)
// Storage
lokud.storage.get(key)
lokud.storage.set(key, value)
// Utilities
lokud.utils.formatDate(date, format)
lokud.utils.getWeekNumber(date)Community Plugins
Check the Lokud plugin repository for more community-contributed plugins:
- Advanced analytics
- Calendar integrations
- Custom import/export formats
- Database synchronization
- And many more!