From b61aaf80a6c6abe3a61d236b884839f0cdda26f3 Mon Sep 17 00:00:00 2001 From: Sreeram Sreedhar Date: Thu, 21 May 2026 00:00:36 -0400 Subject: [PATCH 1/8] initial commit --- .../components/add-document/connections.tsx | 24 +++++++++++++++---- apps/web/components/document-icon.tsx | 10 ++++++++ packages/ui/assets/icons.tsx | 19 +++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/apps/web/components/add-document/connections.tsx b/apps/web/components/add-document/connections.tsx index 7aa0250d5..cf80edcd1 100644 --- a/apps/web/components/add-document/connections.tsx +++ b/apps/web/components/add-document/connections.tsx @@ -4,7 +4,7 @@ import { $fetch } from "@lib/api" import { hasActivePlan } from "@lib/queries" import type { ConnectionResponseSchema } from "@repo/validation/api" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" -import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons" +import { GoogleDrive, Notion, Obsidian, OneDrive } from "@ui/assets/icons" import { useCustomer } from "autumn-js/react" import { Check, @@ -48,7 +48,7 @@ const GDRIVE_SCOPE_LABELS: Record = { type Connection = z.infer -type ConnectorProvider = "google-drive" | "notion" | "onedrive" +type ConnectorProvider = "google-drive" | "notion" | "onedrive" | "obsidian" const CONNECTORS: Record< ConnectorProvider, @@ -77,6 +77,12 @@ const CONNECTORS: Record< documentLabel: "documents", icon: OneDrive, }, + obsidian: { + title: "Obsidian", + description: "Sync your Obsidian vault via the community plugin", + documentLabel: "notes", + icon: Obsidian, + }, } as const /** Extract typed metadata from a connection, with runtime validation. */ @@ -394,7 +400,9 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { } const response = await $fetch("@post/connections/:provider", { - params: { provider }, + params: { + provider: provider as Exclude, + }, body: { redirectUrl: window.location.href, containerTags: [selectedProject], @@ -436,7 +444,9 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { containerTags: string[] | undefined }) => { const response = await $fetch("@post/connections/:provider", { - params: { provider }, + params: { + provider: provider as Exclude, + }, body: { redirectUrl: window.location.href, containerTags: containerTags ?? [selectedProject], @@ -491,6 +501,12 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { }) const handleConnect = (provider: ConnectorProvider) => { + if (provider === "obsidian") { + toast.info( + "Install the Supermemory plugin in Obsidian → Settings → Community plugins → Browse → search 'Supermemory'. Once installed, paste your API key into the plugin settings.", + ) + return + } setConnectingProvider(provider) addConnectionMutation.mutate({ provider, diff --git a/apps/web/components/document-icon.tsx b/apps/web/components/document-icon.tsx index 202ead224..518df0e8f 100644 --- a/apps/web/components/document-icon.tsx +++ b/apps/web/components/document-icon.tsx @@ -13,6 +13,7 @@ import { MicrosoftOneNote, OneDrive, NotionDoc, + Obsidian, PDF, } from "@ui/assets/icons" import { Globe, FileText, Image } from "lucide-react" @@ -50,6 +51,8 @@ const BRAND_COLORS: Record = { microsoft_onenote: "#7719AA", onenote: "#7719AA", onedrive: "#0078D4", + obsidian: "#7C3AED", + obsidian_note: "#7C3AED", pdf: "#FF7673", text: "#FAFAFA", note: "#FAFAFA", @@ -255,6 +258,10 @@ export function DocumentIcon({ ) + case "obsidian": + case "obsidian_note": + return + case "pdf": return @@ -316,6 +323,9 @@ export function getDocumentTypeLabel(type: string | null | undefined): string { return "OneNote" case "onedrive": return "OneDrive" + case "obsidian": + case "obsidian_note": + return "Obsidian" case "pdf": return "PDF" case "youtube": diff --git a/packages/ui/assets/icons.tsx b/packages/ui/assets/icons.tsx index 9c30d2e3e..d6f666923 100644 --- a/packages/ui/assets/icons.tsx +++ b/packages/ui/assets/icons.tsx @@ -1,3 +1,22 @@ +export const Obsidian = ({ className }: { className?: string }) => ( + + Obsidian + + + + + + + + + + +) + export const OneDrive = ({ className }: { className?: string }) => ( Date: Thu, 21 May 2026 00:11:32 -0400 Subject: [PATCH 2/8] scaffold Obsidian connector plugin --- apps/obsidian-connector/.gitignore | 4 + apps/obsidian-connector/esbuild.config.mjs | 26 +++++ apps/obsidian-connector/manifest.json | 10 ++ apps/obsidian-connector/package.json | 18 +++ apps/obsidian-connector/src/main.ts | 57 ++++++++++ apps/obsidian-connector/src/settings.ts | 96 ++++++++++++++++ apps/obsidian-connector/src/types.ts | 9 ++ apps/obsidian-connector/tsconfig.json | 21 ++++ apps/obsidian-connector/versions.json | 3 + bun.lock | 122 ++++++++++++++++++++- 10 files changed, 362 insertions(+), 4 deletions(-) create mode 100644 apps/obsidian-connector/.gitignore create mode 100644 apps/obsidian-connector/esbuild.config.mjs create mode 100644 apps/obsidian-connector/manifest.json create mode 100644 apps/obsidian-connector/package.json create mode 100644 apps/obsidian-connector/src/main.ts create mode 100644 apps/obsidian-connector/src/settings.ts create mode 100644 apps/obsidian-connector/src/types.ts create mode 100644 apps/obsidian-connector/tsconfig.json create mode 100644 apps/obsidian-connector/versions.json diff --git a/apps/obsidian-connector/.gitignore b/apps/obsidian-connector/.gitignore new file mode 100644 index 000000000..79a0f4ee5 --- /dev/null +++ b/apps/obsidian-connector/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +main.js +*.js.map +dist/ diff --git a/apps/obsidian-connector/esbuild.config.mjs b/apps/obsidian-connector/esbuild.config.mjs new file mode 100644 index 000000000..6c80e2f2f --- /dev/null +++ b/apps/obsidian-connector/esbuild.config.mjs @@ -0,0 +1,26 @@ +import esbuild from "esbuild" +import { readFileSync } from "fs" + +const manifest = JSON.parse(readFileSync("manifest.json", "utf8")) +const isProduction = process.argv[2] === "production" + +const context = await esbuild.context({ + entryPoints: ["src/main.ts"], + bundle: true, + external: ["obsidian", "electron", "@codemirror/view", "@codemirror/state"], + format: "cjs", + target: "es2020", + outfile: "main.js", + sourcemap: isProduction ? false : "inline", + define: { + "process.env.PLUGIN_VERSION": JSON.stringify(manifest.version), + }, + logLevel: "info", +}) + +if (isProduction) { + await context.rebuild() + await context.dispose() +} else { + await context.watch() +} diff --git a/apps/obsidian-connector/manifest.json b/apps/obsidian-connector/manifest.json new file mode 100644 index 000000000..65d9ea020 --- /dev/null +++ b/apps/obsidian-connector/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "supermemory", + "name": "Supermemory", + "version": "0.1.0", + "minAppVersion": "1.5.0", + "description": "Sync your Obsidian vault to Supermemory for AI-powered search and memory extraction.", + "author": "Supermemory", + "authorUrl": "https://supermemory.ai", + "isDesktopOnly": false +} diff --git a/apps/obsidian-connector/package.json b/apps/obsidian-connector/package.json new file mode 100644 index 000000000..960719e79 --- /dev/null +++ b/apps/obsidian-connector/package.json @@ -0,0 +1,18 @@ +{ + "name": "@repo/obsidian-plugin", + "version": "0.1.0", + "private": true, + "description": "Supermemory plugin for Obsidian", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "node esbuild.config.mjs production" + }, + "dependencies": { + "obsidian": "^1.7.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "esbuild": "^0.25.0", + "typescript": "^5.8.0" + } +} diff --git a/apps/obsidian-connector/src/main.ts b/apps/obsidian-connector/src/main.ts new file mode 100644 index 000000000..27ce3cf99 --- /dev/null +++ b/apps/obsidian-connector/src/main.ts @@ -0,0 +1,57 @@ +import { Notice, Plugin } from "obsidian" +import { SupermemorySettingTab } from "./settings" +import type { SupermemorySettings } from "./types" + +const DEFAULT_SETTINGS: SupermemorySettings = { + apiKey: "", + apiBaseUrl: "https://api.supermemory.ai", + containerTag: "sm_project_obsidian_default", + vaultName: "", + connectionId: "", + syncOnSave: true, + syncOnStartup: true, +} + +export default class SupermemoryPlugin extends Plugin { + settings: SupermemorySettings = DEFAULT_SETTINGS + + async onload() { + await this.loadSettings() + + 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.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...") + }, + }) + } + + async loadSettings() { + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData(), + ) + } + + async saveSettings() { + await this.saveData(this.settings) + } +} diff --git a/apps/obsidian-connector/src/settings.ts b/apps/obsidian-connector/src/settings.ts new file mode 100644 index 000000000..15e5b3964 --- /dev/null +++ b/apps/obsidian-connector/src/settings.ts @@ -0,0 +1,96 @@ +import { App, PluginSettingTab, Setting } from "obsidian" +import type SupermemoryPlugin from "./main" + +export class SupermemorySettingTab extends PluginSettingTab { + plugin: SupermemoryPlugin + + constructor(app: App, plugin: SupermemoryPlugin) { + super(app, plugin) + this.plugin = plugin + } + + display(): void { + const { containerEl } = this + containerEl.empty() + + new Setting(containerEl) + .setName("API Key") + .setDesc("Your Supermemory API key. Get one at supermemory.ai/api-keys") + .addText((text) => + text + .setPlaceholder("sm_...") + .setValue(this.plugin.settings.apiKey) + .onChange(async (value) => { + this.plugin.settings.apiKey = value.trim() + await this.plugin.saveSettings() + }), + ) + + new Setting(containerEl) + .setName("API Base URL") + .setDesc("Only change this if you self-host Supermemory.") + .addText((text) => + text + .setPlaceholder("https://api.supermemory.ai") + .setValue(this.plugin.settings.apiBaseUrl) + .onChange(async (value) => { + this.plugin.settings.apiBaseUrl = value.trim() + await this.plugin.saveSettings() + }), + ) + + new Setting(containerEl) + .setName("Container tag") + .setDesc("Project / space to sync notes into.") + .addText((text) => + text + .setPlaceholder("sm_project_default") + .setValue(this.plugin.settings.containerTag) + .onChange(async (value) => { + this.plugin.settings.containerTag = value.trim() + await this.plugin.saveSettings() + }), + ) + + new Setting(containerEl) + .setName("Vault display name") + .setDesc( + "How this vault appears in Supermemory. Defaults to the vault folder name.", + ) + .addText((text) => + text + .setPlaceholder(this.app.vault.getName()) + .setValue(this.plugin.settings.vaultName) + .onChange(async (value) => { + this.plugin.settings.vaultName = value.trim() + await this.plugin.saveSettings() + }), + ) + + new Setting(containerEl) + .setName("Sync on file save") + .setDesc("Push changes to Supermemory whenever you save a note.") + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.syncOnSave) + .onChange(async (value) => { + this.plugin.settings.syncOnSave = value + await this.plugin.saveSettings() + }), + ) + + new Setting(containerEl) + .setName("Sync on startup") + .setDesc( + "Reconcile any changes made while Obsidian was closed.", + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.syncOnStartup) + .onChange(async (value) => { + this.plugin.settings.syncOnStartup = value + await this.plugin.saveSettings() + }), + ) + } +} diff --git a/apps/obsidian-connector/src/types.ts b/apps/obsidian-connector/src/types.ts new file mode 100644 index 000000000..360dbc371 --- /dev/null +++ b/apps/obsidian-connector/src/types.ts @@ -0,0 +1,9 @@ +export interface SupermemorySettings { + apiKey: string + apiBaseUrl: string + containerTag: string + vaultName: string + connectionId: string + syncOnSave: boolean + syncOnStartup: boolean +} diff --git a/apps/obsidian-connector/tsconfig.json b/apps/obsidian-connector/tsconfig.json new file mode 100644 index 000000000..3d6859f07 --- /dev/null +++ b/apps/obsidian-connector/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "sourceMap": true, + "declaration": false, + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/obsidian-connector/versions.json b/apps/obsidian-connector/versions.json new file mode 100644 index 000000000..3c0826560 --- /dev/null +++ b/apps/obsidian-connector/versions.json @@ -0,0 +1,3 @@ +{ + "0.1.0": "1.5.0" +} diff --git a/bun.lock b/bun.lock index f1d890ca0..114be045e 100644 --- a/bun.lock +++ b/bun.lock @@ -125,6 +125,18 @@ "typescript": "^5", }, }, + "apps/obsidian-plugin": { + "name": "@repo/obsidian-plugin", + "version": "0.1.0", + "dependencies": { + "obsidian": "^1.7.2", + }, + "devDependencies": { + "@types/node": "^22.0.0", + "esbuild": "^0.25.0", + "typescript": "^5.8.0", + }, + }, "apps/web": { "name": "@repo/web", "version": "0.1.0", @@ -734,6 +746,10 @@ "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260305.1", "", {}, "sha512-835BZaIcgjuYIUqgOWJSpwQxFSJ8g/X1OCZFLO7bmirM6TGmVgIGwiGItBgkjUXXCPrYzJEldsJkuFuK7ePuMw=="], + "@codemirror/state": ["@codemirror/state@6.5.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw=="], + + "@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="], + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], @@ -1018,6 +1034,8 @@ "@lukeed/uuid": ["@lukeed/uuid@2.0.1", "", { "dependencies": { "@lukeed/csprng": "^1.1.0" } }, "sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w=="], + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], + "@mastra/core": ["@mastra/core@1.9.0", "", { "dependencies": { "@a2a-js/sdk": "~0.2.4", "@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.20", "@ai-sdk/provider-utils-v6": "npm:@ai-sdk/provider-utils@4.0.0", "@ai-sdk/provider-v5": "npm:@ai-sdk/provider@2.0.0", "@ai-sdk/provider-v6": "npm:@ai-sdk/provider@3.0.0", "@ai-sdk/ui-utils-v5": "npm:@ai-sdk/ui-utils@1.2.11", "@isaacs/ttlcache": "^2.1.4", "@lukeed/uuid": "^2.0.1", "@mastra/schema-compat": "1.1.3", "@modelcontextprotocol/sdk": "^1.17.5", "@sindresorhus/slugify": "^2.2.1", "dotenv": "^17.2.3", "execa": "^9.6.1", "gray-matter": "^4.0.3", "hono": "^4.11.9", "hono-openapi": "^1.1.1", "ignore": "^7.0.5", "js-tiktoken": "^1.0.21", "json-schema": "^0.4.0", "lru-cache": "^11.2.6", "p-map": "^7.0.3", "p-retry": "^7.1.0", "picomatch": "^4.0.3", "radash": "^12.1.1", "ws": "^8.19.0", "xxhash-wasm": "^1.1.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-wEMsWj/8WhDRLlv1oWPf2ss6FiNzQXluOd+dCQl9fge/Dk3MoIVDhAPBRAVHNhYLTyor8NQMJMkiCGyDZMPWHg=="], "@mastra/schema-compat": ["@mastra/schema-compat@1.1.3", "", { "dependencies": { "json-schema-to-zod": "^2.7.0", "zod-from-json-schema": "^0.5.0", "zod-from-json-schema-v3": "npm:zod-from-json-schema@^0.0.5", "zod-to-json-schema": "^3.24.6" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-szLMJhqfnEn4VctFLKRZ2NIpfg+3UTghQWgy8Fcdchj2HvHxB2uilJxRybM9ugMmvyE+W48tVdz4Xi2Z1P3pFA=="], @@ -1456,6 +1474,8 @@ "@repo/lib": ["@repo/lib@workspace:packages/lib"], + "@repo/obsidian-plugin": ["@repo/obsidian-plugin@workspace:apps/obsidian-plugin"], + "@repo/ui": ["@repo/ui@workspace:packages/ui"], "@repo/validation": ["@repo/validation@workspace:packages/validation"], @@ -1984,6 +2004,8 @@ "@types/chrome": ["@types/chrome@0.1.37", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-IJE4ceuDO7lrEuua7Pow47zwNcI8E6qqkowRP7aFPaZ0lrjxh6y836OPqqkIZeTX64FTogbw+4RNH0+QrweCTQ=="], + "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], "@types/cookie": ["@types/cookie@0.4.1", "", {}, "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="], @@ -2120,7 +2142,7 @@ "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], - "@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + "@types/node": ["@types/node@22.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -2146,6 +2168,8 @@ "@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="], + "@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="], + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], @@ -3858,6 +3882,8 @@ "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + "moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], + "morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="], "motion": ["motion@12.35.0", "", { "dependencies": { "framer-motion": "^12.35.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-BQUhNUIGvUcwXCzwmnT1JpjUqab34lIwxHnXUyWRht1WC1vAyp7/4qgMiUXxN3K6hgUhyoR+HNnLeQMwUZjVjw=="], @@ -3952,6 +3978,8 @@ "obliterator": ["obliterator@1.6.1", "", {}, "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig=="], + "obsidian": ["obsidian@1.12.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw=="], + "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], @@ -4622,6 +4650,8 @@ "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], + "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -4810,7 +4840,7 @@ "undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], @@ -5448,8 +5478,12 @@ "@repo/lib/better-auth": ["better-auth@1.3.3", "", { "dependencies": { "@better-auth/utils": "0.2.5", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.8.0", "@simplewebauthn/browser": "^13.0.0", "@simplewebauthn/server": "^13.0.0", "better-call": "^1.0.12", "defu": "^6.1.4", "jose": "^5.9.6", "kysely": "^0.28.1", "nanostores": "^0.11.3", "zod": "^4.0.5" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-q1aD2nNpGfEI2ckYu+pBjN+23CIRctOpmREkWyJDJdoYW1q9EPs1Xdb+KhFztg2rMmsoUN8I9Xm5mUWMxiWuLw=="], + "@repo/obsidian-plugin/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "@repo/web/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="], + "@repo/web/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + "@repo/web/ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], "@repo/web/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -5534,8 +5568,34 @@ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "@types/body-parser/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + + "@types/connect/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + + "@types/cors/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + + "@types/es-aggregate-error/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + + "@types/express-serve-static-core/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + + "@types/mysql/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + + "@types/node-fetch/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + + "@types/pg/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + + "@types/send/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + + "@types/serve-static/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + "@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "@types/tedious/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + + "@types/ws/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + + "@types/yauzl/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + "@vanilla-extract/css/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], @@ -5604,6 +5664,8 @@ "body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + "bun-types/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "c12/dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], @@ -5612,6 +5674,8 @@ "cacheable-request/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + "chrome-launcher/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + "chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "chromium-bidi/urlpattern-polyfill": ["urlpattern-polyfill@10.0.0", "", {}, "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="], @@ -5678,6 +5742,8 @@ "eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "engine.io/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + "engine.io/cookie": ["cookie@0.4.2", "", {}, "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="], "engine.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], @@ -5750,6 +5816,8 @@ "gray-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "happy-dom/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + "hast-util-to-html/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], "html-to-text/htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="], @@ -5770,6 +5838,8 @@ "is-online/got": ["got@12.6.1", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ=="], + "jest-worker/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "jsonwebtoken/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -5870,6 +5940,8 @@ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "protobufjs/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + "proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "proxy-agent/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -6452,6 +6524,8 @@ "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g=="], + "@opentelemetry/instrumentation-pg/@types/pg/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="], + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], @@ -6482,6 +6556,8 @@ "@repo/web/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], + "@repo/web/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "@repo/web/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], "@repo/web/ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], @@ -6504,6 +6580,32 @@ "@supermemory/tools/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], + "@types/body-parser/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/connect/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/cors/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/es-aggregate-error/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/express-serve-static-core/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/mysql/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/node-fetch/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/pg/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/send/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/serve-static/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/tedious/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/yauzl/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "@voltagent/core/@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], "@voltagent/core/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], @@ -6568,10 +6670,14 @@ "body-parser/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "c12/giget/citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "chrome-launcher/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "chrome-launcher/is-wsl/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], "cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], @@ -6600,6 +6706,8 @@ "docs-test/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], + "engine.io/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -6662,6 +6770,8 @@ "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "happy-dom/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "html-to-text/htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "ink/cli-cursor/restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], @@ -6674,14 +6784,14 @@ "is-online/got/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + "jest-worker/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "listr2/cli-truncate/slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], "listr2/cli-truncate/string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], "listr2/wrap-ansi/string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], - "memory-graph-playground/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "memory-graph-playground/next/@next/env": ["@next/env@16.0.3", "", {}, "sha512-IqgtY5Vwsm14mm/nmQaRMmywCU+yyMIYfk3/MHZ2ZTJvwVbBn3usZnjMi1GacrMVzVcAxJShTCpZlPs26EdEjQ=="], "memory-graph-playground/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MOnbd92+OByu0p6QBAzq1ahVWzF6nyfiH07dQDez4/Nku7G249NjxDVyEfVhz8WkLiOEU+KFVnqtgcsfP2nLXg=="], @@ -6716,6 +6826,8 @@ "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "protobufjs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "public-ip/got/@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="], "public-ip/got/form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="], @@ -7078,6 +7190,8 @@ "@opennextjs/cloudflare/glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + "@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "@puppeteer/browsers/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "@puppeteer/browsers/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], From 346670825c31f42135420e2b610ac7439dcf87fd Mon Sep 17 00:00:00 2001 From: Sreeram Sreedhar Date: Thu, 21 May 2026 00:56:35 -0400 Subject: [PATCH 3/8] 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 000000000..692281ed3 --- /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 27ce3cf99..6b919981d 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) } } From b371ee1f64cf41137860975821882c870a811e01 Mon Sep 17 00:00:00 2001 From: Sreeram Sreedhar Date: Thu, 21 May 2026 01:24:27 -0400 Subject: [PATCH 4/8] added syncing logic --- apps/obsidian-connector/src/main.ts | 42 ++++++- apps/obsidian-connector/src/sync.ts | 174 ++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 apps/obsidian-connector/src/sync.ts diff --git a/apps/obsidian-connector/src/main.ts b/apps/obsidian-connector/src/main.ts index 6b919981d..1fd8eb061 100644 --- a/apps/obsidian-connector/src/main.ts +++ b/apps/obsidian-connector/src/main.ts @@ -1,6 +1,7 @@ -import { Notice, Plugin } from "obsidian" +import { Notice, Plugin, type TAbstractFile, TFile } from "obsidian" import { configure, createConnection } from "./api" import { SupermemorySettingTab } from "./settings" +import { SyncEngine } from "./sync" import type { SupermemorySettings } from "./types" const DEFAULT_SETTINGS: SupermemorySettings = { @@ -15,6 +16,7 @@ const DEFAULT_SETTINGS: SupermemorySettings = { export default class SupermemoryPlugin extends Plugin { settings: SupermemorySettings = DEFAULT_SETTINGS + private syncEngine: SyncEngine | null = null async onload() { await this.loadSettings() @@ -31,6 +33,37 @@ export default class SupermemoryPlugin extends Plugin { name: "Sync vault now", callback: () => this.syncVault(), }) + + if (this.settings.syncOnSave) { + this.registerVaultEvents() + } + + if (this.settings.syncOnStartup && this.settings.apiKey) { + this.app.workspace.onLayoutReady(() => this.syncVault()) + } + } + + private registerVaultEvents() { + this.registerEvent( + this.app.vault.on("modify", (file: TAbstractFile) => { + if (file instanceof TFile) this.syncEngine?.onFileChange(file) + }), + ) + this.registerEvent( + this.app.vault.on("create", (file: TAbstractFile) => { + if (file instanceof TFile) this.syncEngine?.onFileChange(file) + }), + ) + this.registerEvent( + this.app.vault.on("delete", (file: TAbstractFile) => { + if (file instanceof TFile) this.syncEngine?.onFileDelete(file) + }), + ) + this.registerEvent( + this.app.vault.on("rename", (file: TAbstractFile, oldPath: string) => { + if (file instanceof TFile) this.syncEngine?.onFileRename(file, oldPath) + }), + ) } async ensureConnection(): Promise { @@ -66,7 +99,12 @@ export default class SupermemoryPlugin extends Plugin { private async syncVault() { const connectionId = await this.ensureConnection() if (!connectionId) return - new Notice("Supermemory: sync starting...") + + if (!this.syncEngine) { + this.syncEngine = new SyncEngine(this.app, connectionId) + } + + await this.syncEngine.fullSync() } async loadSettings() { diff --git a/apps/obsidian-connector/src/sync.ts b/apps/obsidian-connector/src/sync.ts new file mode 100644 index 000000000..aca979714 --- /dev/null +++ b/apps/obsidian-connector/src/sync.ts @@ -0,0 +1,174 @@ +import { type App, Notice, TFile, parseFrontMatterEntry, parseFrontMatterTags } from "obsidian" +import { pushDeletions, pushNotes, type NotePayload } from "./api" + +const BATCH_SIZE = 50 +const DEBOUNCE_MS = 3000 + +interface SyncState { + syncing: boolean + lastFullSync: number +} + +export class SyncEngine { + private app: App + private connectionId: string + private state: SyncState = { syncing: false, lastFullSync: 0 } + private pendingChanges: Map = new Map() + private debounceTimer: ReturnType | null = null + + constructor(app: App, connectionId: string) { + this.app = app + this.connectionId = connectionId + } + + async fullSync(): Promise<{ queued: number; failed: number }> { + if (this.state.syncing) { + new Notice("Supermemory: sync already in progress.") + return { queued: 0, failed: 0 } + } + + this.state.syncing = true + let totalQueued = 0 + let totalFailed = 0 + + try { + const files = this.app.vault.getMarkdownFiles() + const batches = this.chunk(files, BATCH_SIZE) + + for (const batch of batches) { + const notes = await Promise.all(batch.map((f) => this.fileToPayload(f))) + const valid = notes.filter((n): n is NotePayload => n !== null) + if (valid.length === 0) continue + + try { + const result = await pushNotes(this.connectionId, valid) + totalQueued += result.queuedCount + } catch { + totalFailed += valid.length + } + } + + this.state.lastFullSync = Date.now() + new Notice( + `Supermemory: synced ${totalQueued} note${totalQueued !== 1 ? "s" : ""}${totalFailed > 0 ? `, ${totalFailed} failed` : ""}.`, + ) + } finally { + this.state.syncing = false + } + + return { queued: totalQueued, failed: totalFailed } + } + + onFileChange(file: TFile) { + if (!(file instanceof TFile) || file.extension !== "md") return + this.pendingChanges.set(file.path, "upsert") + this.schedulePush() + } + + onFileDelete(file: TFile) { + if (!(file instanceof TFile) || file.extension !== "md") return + this.pendingChanges.set(file.path, "delete") + this.schedulePush() + } + + onFileRename(file: TFile, oldPath: string) { + if (!(file instanceof TFile) || file.extension !== "md") return + this.pendingChanges.set(oldPath, "delete") + this.pendingChanges.set(file.path, "upsert") + this.schedulePush() + } + + private schedulePush() { + if (this.debounceTimer) clearTimeout(this.debounceTimer) + this.debounceTimer = setTimeout(() => this.flushPending(), DEBOUNCE_MS) + } + + private async flushPending() { + if (this.pendingChanges.size === 0) return + if (this.state.syncing) { + this.schedulePush() + return + } + + this.state.syncing = true + const changes = new Map(this.pendingChanges) + this.pendingChanges.clear() + + try { + const upserts: string[] = [] + const deletes: string[] = [] + + for (const [path, action] of changes) { + if (action === "upsert") upserts.push(path) + else deletes.push(path) + } + + if (deletes.length > 0) { + await pushDeletions( + this.connectionId, + deletes.map((path) => ({ path })), + ) + } + + if (upserts.length > 0) { + const files = upserts + .map((path) => this.app.vault.getAbstractFileByPath(path)) + .filter((f): f is TFile => f instanceof TFile) + + const batches = this.chunk(files, BATCH_SIZE) + for (const batch of batches) { + const notes = await Promise.all( + batch.map((f) => this.fileToPayload(f)), + ) + const valid = notes.filter((n): n is NotePayload => n !== null) + if (valid.length > 0) { + await pushNotes(this.connectionId, valid) + } + } + } + } catch { + // Re-queue on failure so the next debounce retries + for (const [path, action] of changes) { + if (!this.pendingChanges.has(path)) { + this.pendingChanges.set(path, action) + } + } + this.schedulePush() + } finally { + this.state.syncing = false + } + } + + private async fileToPayload(file: TFile): Promise { + try { + const content = await this.app.vault.cachedRead(file) + const cache = this.app.metadataCache.getFileCache(file) + let frontmatter: Record | undefined + + if (cache?.frontmatter) { + frontmatter = { ...cache.frontmatter } + delete frontmatter.position + const tags = parseFrontMatterTags(cache.frontmatter) + if (tags) frontmatter.tags = tags + } + + return { + path: file.path, + content, + title: file.basename, + mtime: file.stat.mtime, + frontmatter, + } + } catch { + return null + } + } + + private chunk(arr: T[], size: number): T[][] { + const result: T[][] = [] + for (let i = 0; i < arr.length; i += size) { + result.push(arr.slice(i, i + size)) + } + return result + } +} From d5aa92167de484b1aa8f33a04b6de04c2b750cf9 Mon Sep 17 00:00:00 2001 From: Sreeram Sreedhar Date: Thu, 21 May 2026 17:36:15 -0400 Subject: [PATCH 5/8] updated obsidian plugin settings page --- apps/obsidian-connector/src/settings.ts | 61 ++----------------------- 1 file changed, 4 insertions(+), 57 deletions(-) diff --git a/apps/obsidian-connector/src/settings.ts b/apps/obsidian-connector/src/settings.ts index 15e5b3964..3b1d82600 100644 --- a/apps/obsidian-connector/src/settings.ts +++ b/apps/obsidian-connector/src/settings.ts @@ -16,28 +16,16 @@ export class SupermemorySettingTab extends PluginSettingTab { new Setting(containerEl) .setName("API Key") .setDesc("Your Supermemory API key. Get one at supermemory.ai/api-keys") - .addText((text) => + .addText((text) => { + text.inputEl.type = "password" text .setPlaceholder("sm_...") .setValue(this.plugin.settings.apiKey) .onChange(async (value) => { this.plugin.settings.apiKey = value.trim() await this.plugin.saveSettings() - }), - ) - - new Setting(containerEl) - .setName("API Base URL") - .setDesc("Only change this if you self-host Supermemory.") - .addText((text) => - text - .setPlaceholder("https://api.supermemory.ai") - .setValue(this.plugin.settings.apiBaseUrl) - .onChange(async (value) => { - this.plugin.settings.apiBaseUrl = value.trim() - await this.plugin.saveSettings() - }), - ) + }) + }) new Setting(containerEl) .setName("Container tag") @@ -51,46 +39,5 @@ export class SupermemorySettingTab extends PluginSettingTab { await this.plugin.saveSettings() }), ) - - new Setting(containerEl) - .setName("Vault display name") - .setDesc( - "How this vault appears in Supermemory. Defaults to the vault folder name.", - ) - .addText((text) => - text - .setPlaceholder(this.app.vault.getName()) - .setValue(this.plugin.settings.vaultName) - .onChange(async (value) => { - this.plugin.settings.vaultName = value.trim() - await this.plugin.saveSettings() - }), - ) - - new Setting(containerEl) - .setName("Sync on file save") - .setDesc("Push changes to Supermemory whenever you save a note.") - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.syncOnSave) - .onChange(async (value) => { - this.plugin.settings.syncOnSave = value - await this.plugin.saveSettings() - }), - ) - - new Setting(containerEl) - .setName("Sync on startup") - .setDesc( - "Reconcile any changes made while Obsidian was closed.", - ) - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.syncOnStartup) - .onChange(async (value) => { - this.plugin.settings.syncOnStartup = value - await this.plugin.saveSettings() - }), - ) } } From b6a09f8f886e083d7f045cde7f43da37bdb36c05 Mon Sep 17 00:00:00 2001 From: Sreeram Sreedhar Date: Thu, 21 May 2026 17:50:51 -0400 Subject: [PATCH 6/8] =?UTF-8?q?sync=20mode=20dropdown=20=E2=80=94=20entire?= =?UTF-8?q?=20vault=20or=20selected=20folders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/obsidian-connector/src/main.ts | 7 +++- apps/obsidian-connector/src/settings.ts | 33 ++++++++++++++++++ apps/obsidian-connector/src/sync.ts | 46 ++++++++++++++++++++++--- apps/obsidian-connector/src/types.ts | 5 +++ 4 files changed, 85 insertions(+), 6 deletions(-) diff --git a/apps/obsidian-connector/src/main.ts b/apps/obsidian-connector/src/main.ts index 1fd8eb061..af60c461f 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 3b1d82600..98af1e412 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 aca979714..dca4e0a13 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 360dbc371..914bf94ac 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 } From a6557f584101ac3ecc09cd7ff9bcf0f289beb075 Mon Sep 17 00:00:00 2001 From: Sreeram Sreedhar Date: Thu, 21 May 2026 21:28:41 -0400 Subject: [PATCH 7/8] sync status --- apps/obsidian-connector/src/main.ts | 48 ++++++++++++++++++++++++++++- apps/obsidian-connector/src/sync.ts | 8 ++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/apps/obsidian-connector/src/main.ts b/apps/obsidian-connector/src/main.ts index af60c461f..65a110d01 100644 --- a/apps/obsidian-connector/src/main.ts +++ b/apps/obsidian-connector/src/main.ts @@ -16,14 +16,29 @@ const DEFAULT_SETTINGS: SupermemorySettings = { includedFolders: "", } +function formatTimeAgo(ms: number): string { + const seconds = Math.floor((Date.now() - ms) / 1000) + if (seconds < 60) return "just now" + const minutes = Math.floor(seconds / 60) + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + return `${hours}h ago` +} + export default class SupermemoryPlugin extends Plugin { settings: SupermemorySettings = DEFAULT_SETTINGS private syncEngine: SyncEngine | null = null + private statusBarEl: HTMLElement | null = null + private lastSyncTime: number | null = null + private lastSyncCount = 0 async onload() { await this.loadSettings() configure(this.settings) + this.statusBarEl = this.addStatusBarItem() + this.updateStatusBar() + this.addSettingTab(new SupermemorySettingTab(this.app, this)) this.addRibbonIcon("cloud-upload", "Sync vault to Supermemory", () => { @@ -45,6 +60,27 @@ export default class SupermemoryPlugin extends Plugin { } } + private updateStatusBar(syncing?: { current: number; total: number }) { + if (!this.statusBarEl) return + + if (syncing) { + this.statusBarEl.setText( + `Supermemory: syncing ${syncing.current}/${syncing.total}...`, + ) + return + } + + if (this.lastSyncTime) { + this.statusBarEl.setText( + `Supermemory: synced ${formatTimeAgo(this.lastSyncTime)} · ${this.lastSyncCount} notes`, + ) + } else if (!this.settings.apiKey) { + this.statusBarEl.setText("Supermemory: no API key") + } else { + this.statusBarEl.setText("Supermemory: ready") + } + } + private registerVaultEvents() { this.registerEvent( this.app.vault.on("modify", (file: TAbstractFile) => { @@ -108,7 +144,16 @@ export default class SupermemoryPlugin extends Plugin { this.syncEngine.updateSettings(this.settings) } - await this.syncEngine.fullSync() + const files = this.app.vault.getMarkdownFiles() + this.updateStatusBar({ current: 0, total: files.length }) + + const result = await this.syncEngine.fullSync((current, total) => { + this.updateStatusBar({ current, total }) + }) + + this.lastSyncTime = Date.now() + this.lastSyncCount = result.queued + this.updateStatusBar() } async loadSettings() { @@ -123,5 +168,6 @@ export default class SupermemoryPlugin extends Plugin { await this.saveData(this.settings) configure(this.settings) this.syncEngine?.updateSettings(this.settings) + this.updateStatusBar() } } diff --git a/apps/obsidian-connector/src/sync.ts b/apps/obsidian-connector/src/sync.ts index dca4e0a13..d45089bc5 100644 --- a/apps/obsidian-connector/src/sync.ts +++ b/apps/obsidian-connector/src/sync.ts @@ -50,7 +50,9 @@ export class SyncEngine { return false } - async fullSync(): Promise<{ queued: number; failed: number }> { + async fullSync( + onProgress?: (current: number, total: number) => void, + ): Promise<{ queued: number; failed: number }> { if (this.state.syncing) { new Notice("Supermemory: sync already in progress.") return { queued: 0, failed: 0 } @@ -65,6 +67,7 @@ export class SyncEngine { .getMarkdownFiles() .filter((f) => this.shouldSync(f)) const batches = this.chunk(files, BATCH_SIZE) + let processed = 0 for (const batch of batches) { const notes = await Promise.all(batch.map((f) => this.fileToPayload(f))) @@ -77,6 +80,9 @@ export class SyncEngine { } catch { totalFailed += valid.length } + + processed += batch.length + onProgress?.(processed, files.length) } this.state.lastFullSync = Date.now() From 696861f4a572a09ffe86b6f879ad29d3cf05fdab Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 01:31:51 +0000 Subject: [PATCH 8/8] update lockfile for CI Co-Authored-By: Claude Opus 4.5 --- bun.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index 114be045e..95ece17bf 100644 --- a/bun.lock +++ b/bun.lock @@ -125,7 +125,7 @@ "typescript": "^5", }, }, - "apps/obsidian-plugin": { + "apps/obsidian-connector": { "name": "@repo/obsidian-plugin", "version": "0.1.0", "dependencies": { @@ -1474,7 +1474,7 @@ "@repo/lib": ["@repo/lib@workspace:packages/lib"], - "@repo/obsidian-plugin": ["@repo/obsidian-plugin@workspace:apps/obsidian-plugin"], + "@repo/obsidian-plugin": ["@repo/obsidian-plugin@workspace:apps/obsidian-connector"], "@repo/ui": ["@repo/ui@workspace:packages/ui"],