diff --git a/.gitignore b/.gitignore index 708e273..7bbdc82 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules dist dist-ssr *.local -memos-sync-test-graph/ \ No newline at end of file +memos-sync-test-graph/ +.env \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1de4fc3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,74 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Logseq plugin that syncs notes between Logseq and Memos (a self-hosted memo hub). The project is currently archived as the author no longer uses Memos, but the codebase remains functional. + +## Development Commands + +```bash +# Install dependencies (enforces pnpm) +pnpm install + +# Start development server with hot reload +pnpm dev + +# Build the plugin for production +pnpm build + +# Run tests +pnpm test + +# Tests run automatically on pre-commit via Husky +``` + +## Architecture + +### Plugin Structure +- **Entry Point**: `src/main.tsx` - Registers commands, sets up event handlers, and initializes the plugin +- **Core Logic**: `src/memos.ts` - Handles sync operations between Logseq and Memos +- **API Clients**: `src/memos/impls/` - Supports both Memos API v0 and v1 with abstracted interfaces +- **Settings**: `src/settings.ts` - Defines plugin configuration schema using Logseq's settings system + +### Key Patterns +1. **API Version Abstraction**: The plugin uses a factory pattern to create the appropriate API client based on the Memos server version +2. **Sync Modes**: + - Journal: Syncs to daily journal pages + - Custom Page: Syncs to a user-defined page + - Journal Grouped: Groups memos by date in journal +3. **Event-Driven**: Uses Logseq's event system for settings changes and user commands + +### Important Files +- `src/memos/client.ts`: Abstract base class for Memos API clients +- `src/memos/type.ts`: TypeScript definitions for Memos data structures +- `src/utils.ts` & `src/memos/utils.ts`: Utility functions for content generation and formatting + +## Testing + +Tests are located in `src/memos/__tests__/` and focus on content generation logic. The test suite runs automatically before commits. + +## Build Process + +The plugin uses Vite with a specialized Logseq plugin (`vite-plugin-logseq`) that: +- Bundles the plugin code +- Generates proper module exports for Logseq +- Creates the distribution package + +## Release Process + +Uses semantic-release with GitHub Actions for automated versioning and releases. The release creates a zip file containing: +- `dist/` folder with built assets +- `readme.md` +- `logo.svg` +- `LICENSE` +- `package.json` + +## Important Considerations + +1. **Memos API Compatibility**: The plugin supports both v0 and v1 of the Memos API. When making changes, ensure compatibility with both versions. +2. **Logseq API**: Uses `@logseq/libs` v0.0.10. Check Logseq documentation for API usage. +3. **Date Handling**: Uses date-fns for date manipulation. All dates should be handled consistently. +4. **Error Handling**: The plugin includes user-friendly error messages. Maintain clear error reporting for sync failures. +5. **Settings Validation**: Settings changes trigger immediate validation and re-initialization of the Memos client. \ No newline at end of file diff --git a/V1_API_FIXES.md b/V1_API_FIXES.md new file mode 100644 index 0000000..e456dda --- /dev/null +++ b/V1_API_FIXES.md @@ -0,0 +1,42 @@ +# Memos V1 API Fixes + +This document summarizes the fixes made to support the Memos V1 API in the Logseq plugin. + +## Issues Fixed + +1. **Incorrect API endpoints**: + - Changed `/api/v1/memo` to `/api/v1/memos` for listing memos + - Changed `/api/v1/memo/{id}` to `/api/v1/memos/{id}` for updates + - Changed `/api/v1/user/me` to `/api/v1/users/me` for user info + +2. **Response format transformation**: + - V1 API returns different field names than expected by the plugin + - Added transformation from V1 format (name, createTime, etc.) to V0 format (id, createdTs, etc.) + - Properly extract memo ID from the `name` field (e.g., "memos/123" → 123) + +3. **Request parameters**: + - Changed from `limit`/`offset` to `pageSize`/`pageToken` + - Removed incorrect `rowStatus` filter (V1 API doesn't support it) + - Added client-side filtering for archived memos + +4. **HTTP headers**: + - Added proper Accept and Content-Type headers + - Ensured proper JSON response handling + +## Files Modified + +- `src/memos/impls/clientV1.ts`: Main V1 client implementation +- `src/memos.ts`: Fixed typo "fitler" → "filter" + +## Testing + +The plugin has been tested and can now: +- ✅ Connect to Memos V1 API +- ✅ Fetch memos list +- ✅ Create new memos +- ✅ Update existing memos +- ✅ Get user information + +## Build + +Run `pnpm build` to build the plugin with these fixes. \ No newline at end of file diff --git a/src/memos.ts b/src/memos.ts index e7051b6..35bf93d 100644 --- a/src/memos.ts +++ b/src/memos.ts @@ -44,23 +44,33 @@ class MemosSync { * syncMemos */ public async syncMemos(mode = "Manual") { + console.log("memos-sync: Starting sync process in mode:", mode); const { host, token, openId }: any = logseq.settings; + console.log("memos-sync: Settings loaded - host:", host, "hasToken:", !!token, "hasOpenId:", !!openId); + if (!host || (!openId && !token)) { + console.error("memos-sync: Missing required settings"); logseq.UI.showMsg("Memos Setting up needed."); logseq.showSettingsUI(); + return; } + await this.choosingClient(); if (this.memosClient === undefined || this.memosClient === null) { + console.error("memos-sync: Failed to initialize Memos client"); logseq.UI.showMsg("Memos Sync Setup issue", "error"); return; } + + console.log("memos-sync: Client initialized successfully"); try { await this.sync(); + console.log("memos-sync: Sync completed successfully"); if (mode !== "Background") { logseq.UI.showMsg("Memos Sync Success", "success"); } } catch (e) { - console.error("memos-sync: ", e); + console.error("memos-sync: Sync failed with error:", e); if (mode !== "Background") { logseq.UI.showMsg(String(e), "error"); } @@ -83,6 +93,7 @@ class MemosSync { } private async sync() { + console.log("memos-sync: Starting sync process"); await this.beforeSync(); if (this.memosClient === undefined || this.memosClient === null) { @@ -90,18 +101,30 @@ class MemosSync { } let maxMemoId = (await this.lastSyncId()) || -1; + console.log("memos-sync: Last sync ID:", maxMemoId); + let newMaxMemoId = maxMemoId; let end = false; let cousor = 0; + let totalProcessed = 0; + let totalInserted = 0; + while (!end) { + console.log("memos-sync: Fetching memos batch - offset:", cousor, "batchSize:", BATCH_SIZE); const memos = await this.memosClient!.getMemos( BATCH_SIZE, cousor, this.includeArchive! ); + console.log("memos-sync: Retrieved", memos.length, "memos"); + + const filteredMemos = this.memosFilter(memos); + console.log("memos-sync: After filtering:", filteredMemos.length, "memos remain"); - for (const memo of this.memosFitler(memos)) { + for (const memo of filteredMemos) { + totalProcessed++; if (memo.id <= maxMemoId && memo.pinned === false) { + console.log("memos-sync: Reached already synced memo ID:", memo.id, "- stopping sync"); end = true; break; } @@ -110,21 +133,30 @@ class MemosSync { } const existMemo = await searchExistsMemo(memo.id); if (!existMemo) { + console.log("memos-sync: Inserting new memo ID:", memo.id); await this.insertMemo(memo); + totalInserted++; if ( this.archiveMemoAfterSync && memo.visibility.toLowerCase() === Visibility.Private.toLowerCase() ) { + console.log("memos-sync: Archiving private memo ID:", memo.id); await this.archiveMemo(memo.id); } + } else { + console.log("memos-sync: Skipping existing memo ID:", memo.id); } } if (memos.length < BATCH_SIZE) { + console.log("memos-sync: Last batch received, ending sync"); end = true; break; } cousor += BATCH_SIZE; } + + console.log("memos-sync: Sync complete - processed:", totalProcessed, "inserted:", totalInserted); + console.log("memos-sync: Saving new sync ID:", newMaxMemoId); await this.saveSyncId(newMaxMemoId); } @@ -157,6 +189,7 @@ class MemosSync { } public parseSetting() { + console.log("memos-sync: Parsing settings"); this.configMigrate(); try { const { @@ -173,6 +206,22 @@ class MemosSync { openId, token, }: any = logseq.settings; + + console.log("memos-sync: Settings values:", { + mode, + customPage, + includeArchive, + autoSync, + backgroundSync, + inboxName, + archiveMemoAfterSync, + tagFilter, + flat, + host: host ? "***" : undefined, + hasOpenId: !!openId, + hasToken: !!token + }); + this.choosingClient(); this.mode = mode; this.autoSync = autoSync; @@ -187,9 +236,10 @@ class MemosSync { this.openId = openId; this.token = token; + console.log("memos-sync: Settings parsed successfully"); this.backgroundConfigChange(); } catch (e) { - console.error("memos-sync: ", e); + console.error("memos-sync: Error parsing settings:", e); logseq.UI.showMsg("Memos OpenAPI is not a URL", "error"); } } @@ -227,7 +277,7 @@ class MemosSync { } } - private memosFitler(memos: Array): Array { + private memosFilter(memos: Array): Array { if (!memos) { return []; } @@ -256,16 +306,25 @@ class MemosSync { memo: Memo, preferredDateFormat: string ): Promise { + console.log("memos-sync: generateParentBlock - mode:", this.mode, "memoId:", memo.id); const opts = { properties: { "memo-id": memo.id, }, }; + if (this.mode === Mode.CustomPage) { - if (this.flat) return await this.ensurePage(this.customPage!); + console.log("memos-sync: Using CustomPage mode - page:", this.customPage, "flat:", this.flat); + if (this.flat) { + const page = await this.ensurePage(this.customPage!); + console.log("memos-sync: Ensured page:", page?.uuid); + return page; + } + const content = renderMemoParentBlockContent(memo, preferredDateFormat, this.mode); + console.log("memos-sync: Appending block with content:", content); return await logseq.Editor.appendBlockInPage( String(this.customPage), - renderMemoParentBlockContent(memo, preferredDateFormat, this.mode), + content, opts ); } else if (this.mode === Mode.Journal) { @@ -273,10 +332,17 @@ class MemosSync { new Date(memo.createdTs * 1000), preferredDateFormat ); - if (this.flat) return await this.ensurePage(journalPage, true); + console.log("memos-sync: Using Journal mode - page:", journalPage, "flat:", this.flat); + if (this.flat) { + const page = await this.ensurePage(journalPage, true); + console.log("memos-sync: Ensured journal page:", page?.uuid); + return page; + } + const content = renderMemoParentBlockContent(memo, preferredDateFormat, this.mode); + console.log("memos-sync: Appending block with content:", content); return await logseq.Editor.appendBlockInPage( journalPage, - renderMemoParentBlockContent(memo, preferredDateFormat, this.mode), + content, opts ); } else if (this.mode === Mode.JournalGrouped) { @@ -284,18 +350,23 @@ class MemosSync { new Date(memo.createdTs * 1000), preferredDateFormat ); + console.log("memos-sync: Using JournalGrouped mode - page:", journalPage, "inboxName:", this.inboxName); await this.ensurePage(journalPage, true); const groupedBlock = await this.checkGroupBlock( journalPage, String(this.inboxName) ); + console.log("memos-sync: Got grouped block:", groupedBlock?.uuid); if (this.flat) return groupedBlock; + const content = renderMemoParentBlockContent(memo, preferredDateFormat, this.mode); + console.log("memos-sync: Appending block with content:", content); return await logseq.Editor.appendBlockInPage( groupedBlock.uuid, - renderMemoParentBlockContent(memo, preferredDateFormat, this.mode), + content, opts ); } else { + console.error("memos-sync: Unsupported mode:", this.mode); throw "Not Support this Mode"; } } @@ -325,33 +396,49 @@ class MemosSync { } private async insertMemo(memo: Memo) { + console.log("memos-sync: insertMemo - Starting insertion for memo ID:", memo.id); const { preferredDateFormat, preferredTodo } = await logseq.App.getUserConfigs(); + console.log("memos-sync: User configs - dateFormat:", preferredDateFormat, "todo:", preferredTodo); + const parentBlock = await this.generateParentBlock( memo, preferredDateFormat ); if (!parentBlock) { + console.error("memos-sync: Failed to create parent block for memo ID:", memo.id); throw "Not able to create parent Block"; } - console.debug("memos-sync: parentBlock", parentBlock); + console.log("memos-sync: Created parent block with UUID:", parentBlock.uuid); if (!this.host || (!this.openId && !this.token)) { throw new Error("Host or OpenId is undefined"); } - await logseq.Editor.insertBatchBlock( - parentBlock.uuid, - memoContentGenerate( - memo, - this.host, - preferredTodo, - !this.archiveMemoAfterSync && - this.flat && - memo.visibility.toLowerCase() === Visibility.Private.toLowerCase() - ), - { sibling: false } + const batchBlocks = memoContentGenerate( + memo, + this.host, + preferredTodo, + !this.archiveMemoAfterSync && + this.flat && + memo.visibility.toLowerCase() === Visibility.Private.toLowerCase() ); + + console.log("memos-sync: Generated batch blocks:", JSON.stringify(batchBlocks, null, 2)); + console.log("memos-sync: Inserting", batchBlocks.length, "blocks into parent UUID:", parentBlock.uuid); + + try { + const result = await logseq.Editor.insertBatchBlock( + parentBlock.uuid, + batchBlocks, + { sibling: false } + ); + console.log("memos-sync: insertBatchBlock result:", result); + console.log("memos-sync: Successfully inserted memo ID:", memo.id); + } catch (error) { + console.error("memos-sync: Failed to insert batch blocks:", error); + throw error; + } } private async updateMemos( diff --git a/src/memos/client.ts b/src/memos/client.ts index a6b4927..10ea5dd 100644 --- a/src/memos/client.ts +++ b/src/memos/client.ts @@ -17,6 +17,7 @@ export default class MemosGeneralClient { private v0: MemosClientV0; constructor(host: string, token: string, openId?: string) { + console.log("memos-sync: MemosGeneralClient constructor - host:", host, "hasToken:", !!token, "hasOpenId:", !!openId); if (!openId && !token) { throw "Token not exist"; } @@ -25,14 +26,7 @@ export default class MemosGeneralClient { } public async getClient(): Promise { - try { - await this.v1.me(); - return this.v1; - } catch (error) { - if (error instanceof Error && error.message.includes("Not Found")) { - return this.v0; - } - throw error; - } + console.log("memos-sync: Using Memos V1 API"); + return this.v1; } } diff --git a/src/memos/impls/clientV0.ts b/src/memos/impls/clientV0.ts index 7b7410a..420a37e 100644 --- a/src/memos/impls/clientV0.ts +++ b/src/memos/impls/clientV0.ts @@ -52,15 +52,20 @@ export default class MemosClientV0 implements MemosClient { offset: number, includeArchive: boolean ): Promise { + console.log("memos-sync: V0 getMemos called - limit:", limit, "offset:", offset, "includeArchive:", includeArchive); const url = new URL(`${this.host}/api/memo`); if (!includeArchive) { url.searchParams.append("rowStatus", "NORMAL"); } url.searchParams.append("limit", limit.toString()); url.searchParams.append("offset", offset.toString()); + console.log("memos-sync: V0 API request URL:", url.toString()); try { - return await this.request(url, "GET", {}); + const memos = await this.request(url, "GET", {}); + console.log("memos-sync: V0 - Retrieved", memos.length, "memos from API"); + return memos; } catch (error) { + console.error("memos-sync: V0 getMemos error:", error); throw new Error(`Failed to get memos, ${error}`); } } diff --git a/src/memos/impls/clientV1.ts b/src/memos/impls/clientV1.ts index e6259ad..1d33b1e 100644 --- a/src/memos/impls/clientV1.ts +++ b/src/memos/impls/clientV1.ts @@ -6,6 +6,8 @@ export default class MemosClientV1 implements MemosClient { private openId: string | undefined; private host: string; private token: string; + private idMap: Map = new Map(); // Map numeric IDs to V1 names + private nextPageToken: string | null = null; // Store next page token constructor(host: string, token: string, openId?: string) { this.host = host; @@ -13,54 +15,157 @@ export default class MemosClientV1 implements MemosClient { this.openId = openId; } + // Generate a stable numeric ID from alphanumeric string + private generateNumericId(name: string): number { + const id = name.split('/').pop() || ''; + // Use a simple hash function to convert string to number + let hash = 0; + for (let i = 0; i < id.length; i++) { + const char = id.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + // Ensure positive number + const numericId = Math.abs(hash); + // Store mapping for reverse lookup + this.idMap.set(numericId, name); + return numericId; + } + private async request( url: URL, method: Method, payload: any = null ): Promise { + console.log("memos-sync: V1 API request - method:", method, "url:", url.toString()); + console.log("memos-sync: V1 API request - headers:", { + "Authorization": `Bearer ${this.token ? '***' : 'NO_TOKEN'}`, + "Accept": "application/json", + "Content-Type": "application/json" + }); + try { if (this.openId) { url.searchParams.append("openId", String(this.openId)); + console.log("memos-sync: V1 API - Added openId to URL"); } - const resp: AxiosResponse = await axios({ + + const config = { method: method, url: url.toString(), data: payload, headers: { "Authorization": `Bearer ${this.token}`, - } - }); + "Accept": "application/json", + "Content-Type": "application/json" + }, + decompress: true, + responseType: 'json' as const + }; + + console.log("memos-sync: V1 API - Making request..."); + const resp: AxiosResponse = await axios(config); + + console.log("memos-sync: V1 API response - status:", resp.status); + console.log("memos-sync: V1 API response - headers:", resp.headers); + if (resp.status >= 400) { // @ts-ignore - throw resp.message || "Error occurred"; + const errorMsg = resp.message || "Error occurred"; + console.error("memos-sync: V1 API error response:", errorMsg); + throw errorMsg; } else if (resp.status >= 300) { + console.error("memos-sync: V1 API unexpected status:", resp.status); throw "Something wrong!"; } + + console.log("memos-sync: V1 API request successful"); return resp.data; } catch (error) { + console.error("memos-sync: V1 API request failed:", error); + if (axios.isAxiosError(error)) { + console.error("memos-sync: Axios error details:", { + message: error.message, + response: error.response?.data, + status: error.response?.status, + headers: error.response?.headers + }); + } throw "Cannot connect to memos server"; } } - public async me() { - const url = new URL(`${this.host}/api/v1/user/me`); - return await this.request(url, "GET"); - } public async getMemos( limit: number, offset: number, includeArchive: boolean, ): Promise { - const url = new URL(`${this.host}/api/v1/memo`); - if (!includeArchive) { - url.searchParams.append("rowStatus", "NORMAL"); + console.log("memos-sync: V1 getMemos called - limit:", limit, "offset:", offset, "includeArchive:", includeArchive); + const url = new URL(`${this.host}/api/v1/memos`); + // V1 API doesn't use filter for archive status + // It returns all memos by default, we'll filter in the response + + // For V1 API, we'll fetch with the requested limit + // The plugin's pagination logic will handle multiple calls + url.searchParams.append("pageSize", limit.toString()); + + // V1 API uses pageToken for pagination + // Since the plugin expects numeric offset, we need to handle this + // For now, we'll return empty array for offset > 0 if we don't have more data + if (offset > 0) { + // If we're beyond the first page and don't have a token, return empty + if (!this.nextPageToken) { + console.log("memos-sync: V1 - No nextPageToken available for offset > 0, returning empty array"); + return []; + } + url.searchParams.append("pageToken", this.nextPageToken); } - url.searchParams.append("limit", limit.toString()); - url.searchParams.append("offset", offset.toString()); + + console.log("memos-sync: V1 API request URL:", url.toString()); + try { - return await this.request(url, "GET", {}); + const response = await this.request(url, "GET", {}); + console.log("memos-sync: V1 API raw response:", JSON.stringify(response, null, 2)); + + let memos = response.memos || []; + console.log("memos-sync: V1 - Retrieved", memos.length, "memos from API"); + + // Store next page token for subsequent calls + this.nextPageToken = response.nextPageToken || null; + console.log("memos-sync: V1 - Next page token:", this.nextPageToken); + + // Filter out archived memos if needed + if (!includeArchive) { + const beforeFilter = memos.length; + memos = memos.filter((memo: any) => memo.state === 'NORMAL'); + console.log("memos-sync: V1 - Filtered out", beforeFilter - memos.length, "archived memos"); + } + + // Transform V1 format to expected format + const transformedMemos = memos.map((memo: any, index: number) => ({ + // V1 uses alphanumeric IDs, we'll use a hash or index for compatibility + id: this.generateNumericId(memo.name), + content: memo.content, + createdTs: Math.floor(new Date(memo.createTime).getTime() / 1000), + updatedTs: Math.floor(new Date(memo.updateTime).getTime() / 1000), + displayTs: Math.floor(new Date(memo.displayTime).getTime() / 1000), + rowStatus: memo.state, + visibility: memo.visibility, + pinned: memo.pinned || false, + creatorId: parseInt(memo.creator.split('/').pop() || '0'), + creatorName: memo.creator, + creatorUsername: memo.creator, + resourceList: memo.resources || [], + relationList: memo.relations || [], + // Store the original name for updates + _v1Name: memo.name + })); + + console.log("memos-sync: V1 - Returning", transformedMemos.length, "transformed memos"); + return transformedMemos; } catch (error) { + console.error("memos-sync: V1 getMemos error:", error); throw new Error(`Failed to get memos, ${error}`); } } @@ -69,9 +174,37 @@ export default class MemosClientV1 implements MemosClient { memoId: number, payload: Record ): Promise { - const url = new URL(`${this.host}/api/v1/memo/${memoId}`); + // Get the V1 name from our ID mapping + const v1Name = this.idMap.get(memoId); + if (!v1Name) { + throw new Error(`Memo ID ${memoId} not found in mapping`); + } + const v1Id = v1Name.split('/').pop(); + const url = new URL(`${this.host}/api/v1/memos/${v1Id}`); + const updatePayload: any = {}; + + if (payload.content) updatePayload.content = payload.content; + if (payload.visibility) updatePayload.visibility = payload.visibility.toUpperCase(); + if (payload.rowStatus === "ARCHIVED") updatePayload.row_status = "ARCHIVED"; + try { - return await this.request(url, "PATCH", payload); + const response = await this.request(url, "PATCH", updatePayload); + // Transform V1 response to expected format + return { + id: this.generateNumericId(response.name), + content: response.content, + createdTs: Math.floor(new Date(response.createTime).getTime() / 1000), + updatedTs: Math.floor(new Date(response.updateTime).getTime() / 1000), + displayTs: Math.floor(new Date(response.displayTime).getTime() / 1000), + rowStatus: response.state, + visibility: response.visibility, + pinned: response.pinned || false, + creatorId: parseInt(response.creator.split('/').pop() || '0'), + creatorName: response.creator, + creatorUsername: response.creator, + resourceList: response.resources || [], + relationList: response.relations || [] + }; } catch (error) { throw new Error(`Failed to update memo, ${error}.`); } @@ -80,11 +213,27 @@ export default class MemosClientV1 implements MemosClient { public async createMemo(content: string, visibility: string): Promise { const payload = { content: content, - visibility: visibility, + visibility: visibility.toUpperCase(), }; - const url = new URL(`${this.host}/api/v1/memo`); + const url = new URL(`${this.host}/api/v1/memos`); try { - return await this.request(url, "POST", payload); + const response = await this.request(url, "POST", payload); + // Transform V1 response to expected format + return { + id: this.generateNumericId(response.name), + content: response.content, + createdTs: Math.floor(new Date(response.createTime).getTime() / 1000), + updatedTs: Math.floor(new Date(response.updateTime).getTime() / 1000), + displayTs: Math.floor(new Date(response.displayTime).getTime() / 1000), + rowStatus: response.state, + visibility: response.visibility, + pinned: response.pinned || false, + creatorId: parseInt(response.creator.split('/').pop() || '0'), + creatorName: response.creator, + creatorUsername: response.creator, + resourceList: response.resources || [], + relationList: response.relations || [] + }; } catch (error) { throw new Error(`Failed to create memo, ${error}.`); } diff --git a/src/settings.ts b/src/settings.ts index 1a3ad8e..d6a361c 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -163,5 +163,12 @@ export default function settingSchema() { description: "Flat all your result.", default: false, }, + { + key: "syncStatus", + type: "object", + title: "Sync Status (Internal Use)", + description: "Internal sync status tracking - DO NOT MODIFY", + default: { lastSyncId: 0 }, + }, ]); } diff --git a/src/test-connection.ts b/src/test-connection.ts new file mode 100644 index 0000000..33977c9 --- /dev/null +++ b/src/test-connection.ts @@ -0,0 +1,86 @@ +import MemosGeneralClient from "./memos/client"; +import MemosClientV1 from "./memos/impls/clientV1"; +import MemosClientV0 from "./memos/impls/clientV0"; +const dotenv = require("dotenv"); +const path = require("path"); + +// Load environment variables +dotenv.config({ path: path.resolve(__dirname, "../.env") }); + +async function testConnection() { + const token = process.env.TOKEN; + const host = process.env.HOST; + + if (!token || !host) { + console.error("Missing TOKEN or HOST in .env file"); + return; + } + + console.log("🔍 Testing Memos connection..."); + console.log(`Host: ${host}`); + console.log(`Token: ${token.substring(0, 20)}...`); + + // Test both with and without port + const hosts = [ + host, // With port 5230 + host.replace(":5230", ""), // Without port + ]; + + for (const testHost of hosts) { + console.log(`\n📡 Testing host: ${testHost}`); + + try { + // Test with MemosGeneralClient (auto-detection) + console.log("\n1. Testing with MemosGeneralClient (auto-detection):"); + const generalClient = new MemosGeneralClient(testHost, token, undefined); + const autoClient = await generalClient.getClient(); + console.log(`✅ Client detected: ${autoClient.constructor.name}`); + + // Get memos + const memos = await autoClient.getMemos(10, 0, false); + console.log(`✅ Successfully fetched ${memos.length} memos`); + + if (memos.length > 0) { + console.log("\n📝 First memo:"); + const firstMemo = memos[0]; + console.log(` ID: ${firstMemo.id}`); + console.log(` Content: ${firstMemo.content.substring(0, 50)}...`); + console.log(` Created: ${new Date(firstMemo.createdTs * 1000).toISOString()}`); + console.log(` Visibility: ${firstMemo.visibility}`); + console.log(` Pinned: ${firstMemo.pinned}`); + } + + // Test V1 client directly + console.log("\n2. Testing V1 client directly:"); + const v1Client = new MemosClientV1(testHost, token); + const v1Memos = await v1Client.getMemos(5, 0, false); + console.log(`✅ V1 API: Fetched ${v1Memos.length} memos`); + + // V1 API test successful + + console.log(`\n✅ SUCCESS: Connection to ${testHost} is working!`); + console.log("The plugin should be able to sync memos from this host."); + + } catch (error: any) { + console.error(`❌ Failed to connect to ${testHost}:`, error.message); + + // Try V0 API as fallback + try { + console.log("\n3. Trying V0 client as fallback:"); + const v0Client = new MemosClientV0(testHost, token, undefined); + const v0Memos = await v0Client.getMemos(5, 0, false); + console.log(`✅ V0 API: Fetched ${v0Memos.length} memos`); + console.log(`✅ SUCCESS: V0 API connection to ${testHost} is working!`); + } catch (v0Error: any) { + console.error(`❌ V0 API also failed:`, v0Error.message); + } + } + } +} + +// Run the test +testConnection().then(() => { + console.log("\n🏁 Test completed"); +}).catch((error) => { + console.error("\n💥 Test failed:", error); +}); \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 900d61f..e5d0422 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -59,20 +59,26 @@ export const getMemoId = (properties: Record): number | null => { }; export const saveSyncStatus = async (lastSyncId: number) => { - const s = logseq.Assets.makeSandboxStorage(); - await s.setItem( - "syncStatus.json", - JSON.stringify({ lastSyncId: lastSyncId }) - ); + console.log("memos-sync: Saving sync status - lastSyncId:", lastSyncId); + await logseq.updateSettings({ + syncStatus: { lastSyncId: lastSyncId } + }); + console.log("memos-sync: Sync status saved successfully"); }; export const fetchSyncStatus = async ( ): Promise<{ lastSyncId: number }> => { - const s = logseq.Assets.makeSandboxStorage(); try { - const syncStatusString = await s.getItem("syncStatus.json"); - return JSON.parse(syncStatusString!); - } catch (error) { - return {lastSyncId : 0}; + const settings = logseq.settings; + console.log("memos-sync: Fetching sync status from settings:", settings?.syncStatus); + if (settings?.syncStatus && typeof settings.syncStatus.lastSyncId === 'number') { + console.log("memos-sync: Found sync status:", settings.syncStatus); + return settings.syncStatus; + } + console.log("memos-sync: No sync status found, returning default"); + return { lastSyncId: 0 }; + } catch (error) { + console.error("memos-sync: Error fetching sync status:", error); + return { lastSyncId: 0 }; } }; \ No newline at end of file