diff --git a/apps/obsidian-connector/src/main.ts b/apps/obsidian-connector/src/main.ts index 1fd8eb06..af60c461 100644 --- a/apps/obsidian-connector/src/main.ts +++ b/apps/obsidian-connector/src/main.ts @@ -12,6 +12,8 @@ const DEFAULT_SETTINGS: SupermemorySettings = { connectionId: "", syncOnSave: true, syncOnStartup: true, + syncMode: "all", + includedFolders: "", } export default class SupermemoryPlugin extends Plugin { @@ -101,7 +103,9 @@ export default class SupermemoryPlugin extends Plugin { if (!connectionId) return if (!this.syncEngine) { - this.syncEngine = new SyncEngine(this.app, connectionId) + this.syncEngine = new SyncEngine(this.app, connectionId, this.settings) + } else { + this.syncEngine.updateSettings(this.settings) } await this.syncEngine.fullSync() @@ -118,5 +122,6 @@ export default class SupermemoryPlugin extends Plugin { async saveSettings() { await this.saveData(this.settings) configure(this.settings) + this.syncEngine?.updateSettings(this.settings) } } diff --git a/apps/obsidian-connector/src/settings.ts b/apps/obsidian-connector/src/settings.ts index 3b1d8260..98af1e41 100644 --- a/apps/obsidian-connector/src/settings.ts +++ b/apps/obsidian-connector/src/settings.ts @@ -1,5 +1,6 @@ import { App, PluginSettingTab, Setting } from "obsidian" import type SupermemoryPlugin from "./main" +import type { SyncMode } from "./types" export class SupermemorySettingTab extends PluginSettingTab { plugin: SupermemoryPlugin @@ -39,5 +40,37 @@ export class SupermemorySettingTab extends PluginSettingTab { await this.plugin.saveSettings() }), ) + + new Setting(containerEl) + .setName("Sync mode") + .setDesc("Which notes in your vault to sync to Supermemory.") + .addDropdown((dropdown) => + dropdown + .addOption("all", "Entire vault") + .addOption("folders", "Only selected folders") + .setValue(this.plugin.settings.syncMode) + .onChange(async (value) => { + this.plugin.settings.syncMode = value as SyncMode + await this.plugin.saveSettings() + this.display() + }), + ) + + if (this.plugin.settings.syncMode === "folders") { + new Setting(containerEl) + .setName("Folders to sync") + .setDesc( + "Comma-separated folder paths (e.g. Projects, Daily). Subfolders are included.", + ) + .addText((text) => + text + .setPlaceholder("Projects, Daily") + .setValue(this.plugin.settings.includedFolders) + .onChange(async (value) => { + this.plugin.settings.includedFolders = value + await this.plugin.saveSettings() + }), + ) + } } } diff --git a/apps/obsidian-connector/src/sync.ts b/apps/obsidian-connector/src/sync.ts index aca97971..dca4e0a1 100644 --- a/apps/obsidian-connector/src/sync.ts +++ b/apps/obsidian-connector/src/sync.ts @@ -1,5 +1,6 @@ -import { type App, Notice, TFile, parseFrontMatterEntry, parseFrontMatterTags } from "obsidian" +import { type App, Notice, TFile, parseFrontMatterTags } from "obsidian" import { pushDeletions, pushNotes, type NotePayload } from "./api" +import type { SupermemorySettings } from "./types" const BATCH_SIZE = 50 const DEBOUNCE_MS = 3000 @@ -12,13 +13,41 @@ interface SyncState { export class SyncEngine { private app: App private connectionId: string + private settings: SupermemorySettings private state: SyncState = { syncing: false, lastFullSync: 0 } private pendingChanges: Map = new Map() private debounceTimer: ReturnType | null = null - constructor(app: App, connectionId: string) { + constructor(app: App, connectionId: string, settings: SupermemorySettings) { this.app = app this.connectionId = connectionId + this.settings = settings + } + + updateSettings(settings: SupermemorySettings) { + this.settings = settings + } + + private shouldSync(file: TFile): boolean { + if (file.extension !== "md") return false + + const mode = this.settings.syncMode + if (mode === "all") return true + + if (mode === "folders") { + const folders = this.settings.includedFolders + .split(",") + .map((f) => f.trim().replace(/^\/+|\/+$/g, "")) + .filter((f) => f.length > 0) + if (folders.length === 0) return false + return folders.some( + (folder) => + file.path === `${folder}.md` || + file.path.startsWith(`${folder}/`), + ) + } + + return false } async fullSync(): Promise<{ queued: number; failed: number }> { @@ -32,7 +61,9 @@ export class SyncEngine { let totalFailed = 0 try { - const files = this.app.vault.getMarkdownFiles() + const files = this.app.vault + .getMarkdownFiles() + .filter((f) => this.shouldSync(f)) const batches = this.chunk(files, BATCH_SIZE) for (const batch of batches) { @@ -60,13 +91,16 @@ export class SyncEngine { } onFileChange(file: TFile) { - if (!(file instanceof TFile) || file.extension !== "md") return + if (!(file instanceof TFile) || !this.shouldSync(file)) return this.pendingChanges.set(file.path, "upsert") this.schedulePush() } onFileDelete(file: TFile) { if (!(file instanceof TFile) || file.extension !== "md") return + // Always allow deletes through (file may have been previously synced + // before its folder was excluded). The server safely no-ops on unknown + // customIds. this.pendingChanges.set(file.path, "delete") this.schedulePush() } @@ -74,7 +108,9 @@ export class SyncEngine { onFileRename(file: TFile, oldPath: string) { if (!(file instanceof TFile) || file.extension !== "md") return this.pendingChanges.set(oldPath, "delete") - this.pendingChanges.set(file.path, "upsert") + if (this.shouldSync(file)) { + this.pendingChanges.set(file.path, "upsert") + } this.schedulePush() } diff --git a/apps/obsidian-connector/src/types.ts b/apps/obsidian-connector/src/types.ts index 360dbc37..914bf94a 100644 --- a/apps/obsidian-connector/src/types.ts +++ b/apps/obsidian-connector/src/types.ts @@ -1,3 +1,5 @@ +export type SyncMode = "all" | "folders" + export interface SupermemorySettings { apiKey: string apiBaseUrl: string @@ -6,4 +8,7 @@ export interface SupermemorySettings { connectionId: string syncOnSave: boolean syncOnStartup: boolean + syncMode: SyncMode + /** Comma-separated folder paths; only used when syncMode === "folders" */ + includedFolders: string }