From 346670825c31f42135420e2b610ac7439dcf87fd Mon Sep 17 00:00:00 2001 From: Sreeram Sreedhar Date: Thu, 21 May 2026 00:56:35 -0400 Subject: [PATCH] Plugin settings + API client --- apps/obsidian-connector/src/api.ts | 148 ++++++++++++++++++++++++++++ apps/obsidian-connector/src/main.ts | 55 ++++++++--- 2 files changed, 189 insertions(+), 14 deletions(-) create mode 100644 apps/obsidian-connector/src/api.ts diff --git a/apps/obsidian-connector/src/api.ts b/apps/obsidian-connector/src/api.ts new file mode 100644 index 00000000..692281ed --- /dev/null +++ b/apps/obsidian-connector/src/api.ts @@ -0,0 +1,148 @@ +import { Notice, requestUrl } from "obsidian" +import type { SupermemorySettings } from "./types" + +export interface PushResponse { + accepted: boolean + deletedCount: number + queuedCount: number +} + +export interface ConnectionResponse { + id: string +} + +export interface NotePayload { + path: string + content: string + title?: string + mtime?: number + frontmatter?: Record +} + +class SupermemoryAPIError extends Error { + constructor( + message: string, + public status?: number, + ) { + super(message) + this.name = "SupermemoryAPIError" + } +} + +class AuthenticationError extends Error { + constructor(message: string) { + super(message) + this.name = "AuthenticationError" + } +} + +let _settings: SupermemorySettings | null = null + +export function configure(settings: SupermemorySettings) { + _settings = settings +} + +function getBaseUrl(): string { + if (!_settings) throw new Error("API not configured") + return _settings.apiBaseUrl.replace(/\/+$/, "") +} + +function getApiKey(): string { + if (!_settings?.apiKey) throw new Error("API key not set") + return _settings.apiKey +} + +async function makeAuthenticatedRequest( + endpoint: string, + options: { method?: string; body?: unknown } = {}, +): Promise { + const apiKey = getApiKey() + const url = `${getBaseUrl()}${endpoint}` + + try { + const response = await requestUrl({ + url, + method: options.method ?? "GET", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: options.body ? JSON.stringify(options.body) : undefined, + }) + + if (response.status === 401) { + throw new AuthenticationError( + "Invalid API key. Check your key in plugin settings.", + ) + } + + if (response.status === 429) { + throw new SupermemoryAPIError("Rate limited. Try again in a moment.", 429) + } + + if (response.status >= 400) { + let errorMessage = `API request failed: ${response.status}` + try { + const errorBody = response.json as { error?: string } + if (errorBody.error) errorMessage = errorBody.error + } catch {} + throw new SupermemoryAPIError(errorMessage, response.status) + } + + return response.json as T + } catch (err) { + if (err instanceof AuthenticationError) { + new Notice("Supermemory: invalid API key. Check plugin settings.") + throw err + } + if (err instanceof SupermemoryAPIError) { + if (err.status === 429) { + new Notice("Supermemory: rate limited. Try again in a moment.") + } + throw err + } + throw new SupermemoryAPIError( + `Network error: ${err instanceof Error ? err.message : "Unknown error"}`, + ) + } +} + +export async function createConnection( + vaultId: string, + vaultName: string, + containerTag: string, +): Promise { + return makeAuthenticatedRequest( + "/v3/connections/obsidian", + { + method: "POST", + body: { metadata: { vaultId, vaultName }, containerTag }, + }, + ) +} + +export async function pushNotes( + connectionId: string, + notes: NotePayload[], +): Promise { + return makeAuthenticatedRequest( + "/v3/connections/obsidian/push", + { + method: "POST", + body: { connectionId, notes }, + }, + ) +} + +export async function pushDeletions( + connectionId: string, + deletions: Array<{ path: string }>, +): Promise { + return makeAuthenticatedRequest( + "/v3/connections/obsidian/push", + { + method: "POST", + body: { connectionId, deletions }, + }, + ) +} diff --git a/apps/obsidian-connector/src/main.ts b/apps/obsidian-connector/src/main.ts index 27ce3cf9..6b919981 100644 --- a/apps/obsidian-connector/src/main.ts +++ b/apps/obsidian-connector/src/main.ts @@ -1,4 +1,5 @@ import { Notice, Plugin } from "obsidian" +import { configure, createConnection } from "./api" import { SupermemorySettingTab } from "./settings" import type { SupermemorySettings } from "./types" @@ -17,32 +18,57 @@ export default class SupermemoryPlugin extends Plugin { async onload() { await this.loadSettings() + configure(this.settings) this.addSettingTab(new SupermemorySettingTab(this.app, this)) this.addRibbonIcon("cloud-upload", "Sync vault to Supermemory", () => { - if (!this.settings.apiKey) { - new Notice("Set your Supermemory API key in plugin settings first.") - return - } - new Notice("Supermemory sync starting...") + this.syncVault() }) this.addCommand({ id: "sync-vault", name: "Sync vault now", - callback: () => { - if (!this.settings.apiKey) { - new Notice( - "Set your Supermemory API key in plugin settings first.", - ) - return - } - new Notice("Supermemory sync starting...") - }, + callback: () => this.syncVault(), }) } + async ensureConnection(): Promise { + if (!this.settings.apiKey) { + new Notice("Set your Supermemory API key in plugin settings first.") + return null + } + + if (this.settings.connectionId) { + return this.settings.connectionId + } + + const vaultId = (this.app as unknown as { appId: string }).appId + const vaultName = this.settings.vaultName || this.app.vault.getName() + + try { + const result = await createConnection( + vaultId, + vaultName, + this.settings.containerTag, + ) + this.settings.connectionId = result.id + await this.saveSettings() + return result.id + } catch (e) { + new Notice( + `Supermemory: failed to connect — ${e instanceof Error ? e.message : "unknown error"}`, + ) + return null + } + } + + private async syncVault() { + const connectionId = await this.ensureConnection() + if (!connectionId) return + new Notice("Supermemory: sync starting...") + } + async loadSettings() { this.settings = Object.assign( {}, @@ -53,5 +79,6 @@ export default class SupermemoryPlugin extends Plugin { async saveSettings() { await this.saveData(this.settings) + configure(this.settings) } }