Technical documentation for the Obsidian Linear Sync Plugin v1.0.0
The plugin consists of three core services and a settings interface, all built on the Obsidian Plugin API. This document provides comprehensive API documentation for developers.
classDiagram
class LinearSyncPlugin {
+settings: LinearSyncSettings
+linearApi: LinearApiService
+directorySync: DirectorySyncService
+kanbanSync: KanbanSyncService
+onload()
+onunload()
+loadSettings()
+saveSettings()
}
class LinearApiService {
-plugin: LinearSyncPlugin
-cache: Map
-cacheTimeout: number
+query(query, variables)
+fetchAllIssues()
+fetchWorkflowStates()
+updateIssueState(issueId, stateId)
+createIssue(data)
-getCached(key)
-setCached(key, value)
}
class DirectorySyncService {
-plugin: LinearSyncPlugin
-app: App
+syncAll(issues)
+syncIssue(issue)
+generatePhaseFiles(issue, path, files)
+createFileWithTemplater(template, output)
+findIssueDirectory(identifier)
+moveIssueDirectory(identifier, newState)
+updateDirectoryMetadata(path, state, phase)
-preprocessTemplate(content)
}
class KanbanSyncService {
-plugin: LinearSyncPlugin
-app: App
+updateBoard(issues)
+handleKanbanMove(identifier, column)
+parseTrackerFile()
+writeTrackerFile(content)
-generateBoardContent(issues)
}
LinearSyncPlugin --> LinearApiService
LinearSyncPlugin --> DirectorySyncService
LinearSyncPlugin --> KanbanSyncService
Handles all communication with Linear's GraphQL API.
new LinearApiService(plugin: LinearSyncPlugin)Execute a GraphQL query against Linear API.
Parameters:
query- GraphQL query stringvariables- Optional query variables
Returns: Promise resolving to query data
Example:
const data = await linearApi.query(`
query GetIssue($id: String!) {
issue(id: $id) {
identifier
title
state { name }
}
}
`, { id: "abc-123" });Fetch all active issues (excludes canceled/duplicate).
Returns: Promise resolving to array of issues
Cache: Results cached for 5 minutes
Fetch all workflow states for the team.
Returns: Promise resolving to array of workflow states
Update an issue's workflow state.
Parameters:
issueId- Linear issue IDstateId- Target workflow state ID
Returns: Promise resolving to updated issue
Create a new Linear issue.
Parameters:
data- Issue creation datatitle: string- Issue titledescription?: string- Issue description (Markdown)teamId: string- Team IDstateId?: string- Initial state IDpriority?: number- Priority (1-4)assigneeId?: string- Assignee user ID
Returns: Promise resolving to created issue
Manages file system operations and template processing.
new DirectorySyncService(plugin: LinearSyncPlugin, app: App)Synchronize all issues to vault directories.
Parameters:
issues- Array of Linear issues
Process:
- Creates phase directories if missing
- Generates required files per phase
- Updates existing file metadata
Synchronize a single issue.
Parameters:
issue- Linear issue object
Generate template files for an issue phase.
Parameters:
issue- Linear issue datafolderPath- Target directory pathrequiredFiles- Array of template filenames
Example:
await directorySync.generatePhaseFiles(
issue,
"docs/kanban/03-in-progress/ENG-123",
["01-prd.md", "02-architecture.md", "03-task.md"]
);Process a template file using Templater.
Parameters:
templateName- Template filenameoutputPath- Output file path
Process:
- Loads template from configured folder
- Injects Linear data into context
- Processes Templater syntax
- Writes rendered content
Find existing directory for an issue.
Parameters:
identifier- Issue identifier (e.g., "ENG-123")
Returns: Directory path or null if not found
Move issue directory to new phase.
Parameters:
identifier- Issue identifiernewState- Target Linear state name
Update metadata in all files within a directory.
Parameters:
directoryPath- Directory to updatenewState- New Linear statenewPhase- New phase identifier
Manages TRACKER.md kanban board synchronization.
new KanbanSyncService(plugin: LinearSyncPlugin, app: App)Update TRACKER.md with current issues.
Parameters:
issues- Array of Linear issues
Format:
## Backlog
- [ ] [[ENG-123]] Issue Title #tag
## In Progress
- [ ] [[ENG-124]] Another Issue #priority-highHandle issue movement on kanban board.
Parameters:
issueIdentifier- Issue identifiernewColumn- Target column name
Triggers:
- Updates Linear state
- Moves directory to new phase
- Generates new phase files
Parse TRACKER.md content.
Returns: Parsed kanban structure
Write updated content to TRACKER.md.
Parameters:
content- New tracker content
Plugin settings interface.
interface LinearSyncSettings {
// API Configuration
apiKey: string; // Linear API key
// Sync Configuration
syncInterval: number; // Minutes between syncs
realTimeSync: boolean; // Enable auto-sync
syncWhenUnfocused: boolean; // Sync when window unfocused
// UI Configuration
showStatusBar: boolean; // Show sync status
showSyncNotifications: boolean;
// Directory Configuration
syncDirectories: boolean; // Auto-organize files
autoMoveDirectories: boolean;
confirmDirectoryMoves: boolean;
lockDirectoryStructure: boolean;
// Field Sync Configuration
syncPriority: boolean; // Sync priority field
syncEstimate: boolean; // Sync estimate points
syncDueDate: boolean; // Sync due dates
// Template Configuration
templatePath: string; // Template folder path
phaseFileGeneration: {
enabled: boolean;
progressiveTemplateAddition: boolean;
templateInheritance: boolean;
phaseTemplateMapping: PhaseMapping;
};
}interface PhaseMapping {
[key: string]: {
phase: string;
states: string[];
requiredFiles: string[];
addedFiles: string[];
description: string;
}
}interface LinearIssue {
id: string;
identifier: string; // e.g., "ENG-123"
title: string;
description?: string;
url: string;
// State
state: {
id: string;
name: string;
type: string; // "backlog", "unstarted", "started", "completed", "canceled"
};
// Metadata
priority: number; // 0-4 (0=none, 1=urgent, 4=low)
estimate?: number; // Story points
dueDate?: string; // ISO date
// Relations
assignee?: {
id: string;
name: string;
email: string;
};
team: {
id: string;
key: string;
name: string;
};
project?: {
id: string;
name: string;
};
labels: {
nodes: Array<{
id: string;
name: string;
color: string;
}>;
};
// Timestamps
createdAt: string;
updatedAt: string;
completedAt?: string;
}interface FileFrontmatter {
// Linear metadata
linear_id: string;
linear_identifier: string;
linear_url: string;
linear_state: string;
linear_state_id: string;
linear_priority?: number;
linear_estimate?: number;
linear_due_date?: string;
linear_team_id: string;
linear_assignee_id?: string;
// Sync configuration
auto_sync: boolean;
last_sync: string; // ISO timestamp
// Template metadata
template_type: string;
template_version: string;
phase: string;
state: string;
created: string;
modified: string;
}class LinearSyncPlugin extends Plugin {
async onload() {
// 1. Load settings
await this.loadSettings();
// 2. Initialize services
this.linearApi = new LinearApiService(this);
this.directorySync = new DirectorySyncService(this, this.app);
this.kanbanSync = new KanbanSyncService(this, this.app);
// 3. Register commands
this.addCommand({
id: 'sync-from-linear',
name: 'Sync from Linear',
callback: () => this.syncFromLinear()
});
// 4. Setup auto-sync
if (this.settings.realTimeSync) {
this.registerInterval(
window.setInterval(
() => this.autoSync(),
this.settings.syncInterval * 60 * 1000
)
);
}
// 5. Add settings tab
this.addSettingTab(new LinearSyncSettingTab(this.app, this));
}
async onunload() {
// Cleanup resources
}
}// Sync from Linear
this.addCommand({
id: 'sync-from-linear',
name: 'Sync from Linear',
callback: async () => {
const issues = await this.linearApi.fetchAllIssues();
await this.directorySync.syncAll(issues);
await this.kanbanSync.updateBoard(issues);
}
});
// Sync to Linear
this.addCommand({
id: 'sync-to-linear',
name: 'Sync to Linear',
callback: async () => {
await this.syncLocalChangesToLinear();
}
});
// Batch sync
this.addCommand({
id: 'batch-sync',
name: 'Batch sync all kanban files',
callback: async () => {
await this.batchSyncKanbanFiles();
}
});
// Create issue from note
this.addCommand({
id: 'create-linear-issue',
name: 'Create Linear issue from note',
callback: async () => {
const file = this.app.workspace.getActiveFile();
await this.createIssueFromFile(file);
}
});
// Test connection
this.addCommand({
id: 'test-connection',
name: 'Test Linear connection',
callback: async () => {
await this.testLinearConnection();
}
});// File moved
this.registerEvent(
this.app.vault.on('rename', async (file, oldPath) => {
if (file instanceof TFile) {
await this.handleFileMove(file, oldPath);
}
})
);
// File modified
this.registerEvent(
this.app.vault.on('modify', async (file) => {
if (file instanceof TFile && this.shouldSyncFile(file)) {
await this.syncFileToLinear(file);
}
})
);class LinearApiError extends Error {
constructor(message: string, public statusCode?: number) {
super(message);
this.name = 'LinearApiError';
}
}
class TemplaterError extends Error {
constructor(message: string, public template?: string) {
super(message);
this.name = 'TemplaterError';
}
}
class SyncError extends Error {
constructor(message: string, public issueId?: string) {
super(message);
this.name = 'SyncError';
}
}async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
delay = 1000
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
}
}
throw new Error('Max retries exceeded');
}Last Updated: 2024-08-29 | Plugin Version: 3.0.0