memos-mcp 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "memos-mcp",
3
3
  "type": "module",
4
- "version": "1.0.1",
4
+ "version": "1.0.2",
5
5
  "description": "MCP server for Memos - create and search memos",
6
6
  "main": "src/index.ts",
7
7
  "bin": {
@@ -1,19 +0,0 @@
1
- {
2
- "[typescript]": {
3
- "editor.formatOnSave": true,
4
- "editor.formatOnSaveMode": "file"
5
- },
6
- "editor.formatOnSave": true,
7
- "editor.formatOnSaveMode": "file",
8
- "editor.codeActionsOnSave": {
9
- "source.fixAll.biome": "explicit",
10
- "source.organizeImports.biome": "explicit"
11
- },
12
- "editor.defaultFormatter": "esbenp.prettier-vscode",
13
- "nodejs-testing.extensions": [
14
- {
15
- "extensions": ["mjs", "cjs", "js", "ts"],
16
- "parameters": []
17
- }
18
- ]
19
- }
package/AGENTS.md DELETED
@@ -1,96 +0,0 @@
1
- # Agent Guidelines for memos-mcp
2
-
3
- ## Overview
4
-
5
- This is an MCP (Model Context Protocol) server for Memos - a lightweight, self-hosted memo hub. The server provides tools to create and search memos via the Memos API.
6
-
7
- ## Required Checks Before Committing
8
-
9
- **IMPORTANT:** Always run these commands before committing changes:
10
-
11
- ```bash
12
- # 1. Type checking - REQUIRED
13
- npm run typecheck
14
-
15
- # 2. Linting - REQUIRED
16
- npm run lint
17
-
18
- # 3. Format check - REQUIRED
19
- npm run format:check
20
-
21
- # 4. Tests - REQUIRED
22
- npm run test
23
- ```
24
-
25
- All checks must pass before committing. You can auto-fix lint and format issues:
26
-
27
- ```bash
28
- npm run lint:fix # Auto-fix lint issues
29
- npm run format # Auto-format code
30
- ```
31
-
32
- ## Project Structure
33
-
34
- ```
35
- src/
36
- ├── index.ts # MCP server entry point
37
- ├── memos-client.ts # Memos API client
38
- ├── memos-client.test.ts # Client tests
39
- ├── cert-manager.ts # SSL certificate auto-fetch & cache
40
- └── cert-manager.test.ts # Cert manager tests
41
- ```
42
-
43
- ## Key Technical Details
44
-
45
- ### SSL Certificate Handling
46
-
47
- The project includes automatic SSL certificate management for servers with incomplete certificate chains (missing intermediates). See `cert-manager.ts`:
48
-
49
- - Auto-fetches Let's Encrypt intermediate certificates via HTTPS
50
- - Caches certificates in `~/.cache/memos-mcp/certs/`
51
- - Auto-refreshes when cache > 30 days or cert expires within 30 days
52
-
53
- ### Dependencies
54
-
55
- - `@modelcontextprotocol/sdk` - MCP SDK for server implementation
56
- - `undici` - HTTP client with custom TLS agent support
57
-
58
- ### Node.js Version
59
-
60
- Requires Node.js >= 22.0.0
61
-
62
- ## Code Style
63
-
64
- - Uses Prettier for formatting (config in package.json)
65
- - Uses ESLint with TypeScript plugin
66
- - Double quotes for strings
67
- - Semicolons required
68
- - 120 character line width
69
-
70
- ## Testing
71
-
72
- Tests use Node.js built-in test runner (`node:test`):
73
-
74
- ```bash
75
- npm run test
76
- ```
77
-
78
- Integration tests in `cert-manager.test.ts` make real network requests to Let's Encrypt servers.
79
-
80
- ## Common Tasks
81
-
82
- ### Adding a new MCP tool
83
-
84
- 1. Add tool definition in `src/index.ts` under `server.setRequestHandler(ListToolsRequestSchema, ...)`
85
- 2. Add tool handler in `server.setRequestHandler(CallToolRequestSchema, ...)`
86
- 3. Implement the method in `src/memos-client.ts`
87
- 4. Add tests in `src/memos-client.test.ts`
88
-
89
- ### Updating certificates
90
-
91
- Certificates are auto-fetched, but you can force refresh:
92
-
93
- ```typescript
94
- import { refreshAllCerts } from "./cert-manager.ts";
95
- await refreshAllCerts();
96
- ```
package/Jenkinsfile DELETED
@@ -1,80 +0,0 @@
1
- pipeline {
2
- agent any
3
- tools {
4
- nodejs 'NodeJS 24.x'
5
- }
6
- stages {
7
- stage('Checkout') {
8
- steps {
9
- checkout scm
10
- }
11
- }
12
-
13
- stage('Install') {
14
- steps {
15
- script {
16
- sh 'npm i'
17
- }
18
- }
19
- }
20
-
21
- stage('Lint') {
22
- steps {
23
- script {
24
- sh 'npm run lint --if-present'
25
- sh 'npm run format:check --if-present'
26
- }
27
- }
28
- }
29
-
30
- stage('Test') {
31
- steps {
32
- script {
33
- sh 'npm test --if-present'
34
- }
35
- }
36
- }
37
-
38
- stage('Build') {
39
- steps {
40
- script {
41
- sh 'npm run build --if-present'
42
- }
43
- }
44
- }
45
-
46
- stage('Publish Official Release') {
47
- when {
48
- expression { env.TAG_NAME != null } // Only run for tag builds
49
- }
50
- environment {
51
- NPM_TOKEN = credentials('npm-verdaccio-publish-token')
52
- }
53
- steps {
54
- sh 'npm config set //verdaccio-verdaccio-68mj54-e6081a-192-168-111-123.traefik.me/:_authToken=${NPM_TOKEN} --location project'
55
- sh 'npm publish --tag latest'
56
- }
57
- }
58
-
59
- // stage('Publish Snapshot Build') {
60
- // when {
61
- // expression { env.TAG_NAME == null } // Only run for non-tag builds
62
- // }
63
- // environment {
64
- // NPM_TOKEN = credentials('npm-verdaccio-publish-token')
65
- // }
66
- // steps {
67
- // sh 'npm config set //verdaccio-verdaccio-68mj54-e6081a-192-168-111-123.traefik.me/:_authToken=${NPM_TOKEN} --location project'
68
- // sh 'npm version 0.0.0-BUILD-${BUILD_NUMBER} --no-git-tag-version' // Apply snapshot version
69
- // sh 'npm publish --tag snapshot' // Publish with snapshot tag
70
- // }
71
- // }
72
-
73
- }
74
-
75
- post {
76
- always {
77
- cleanWs()
78
- }
79
- }
80
- }
package/RELEASE.md DELETED
@@ -1,25 +0,0 @@
1
- ## Release Process
2
-
3
- This project uses a two-pronged release approach:
4
-
5
- ### Official Releases (Versioned)
6
-
7
- Official releases follow Semantic Versioning (MAJOR.MINOR.PATCH) and are published to npm with the `latest` tag.
8
-
9
- **Steps for a Developer to Create an Official Release:**
10
-
11
- 1. **Ensure your local `main` branch is up-to-date** and all changes intended for the release are merged.
12
- 2. **Run `npm version <patch|minor|major>` locally.** This command will:
13
- - Increment the version in `package.json`.
14
- - Create a git commit for the version change.
15
- - Create a corresponding local git tag (e.g., `v1.0.0`).
16
- - Example: `npm version patch`
17
- 3. **Push the commit and the new tag to the remote repository:**
18
- ```bash
19
- git push && git push --tags
20
- ```
21
- 4. **Jenkins will automatically detect the new tag** and trigger a pipeline build. The `Publish Official Release` stage in the `Jenkinsfile` will then publish the package to npm with the `latest` tag.
22
-
23
- ### Snapshot Builds
24
-
25
- For builds on branches that are not triggered by a git tag (e.g., `main` branch builds from regular commits), a snapshot version is automatically applied and published to npm with a `snapshot` tag. These builds are primarily for continuous integration and testing purposes and do not represent official releases.
package/eslint.config.js DELETED
@@ -1,34 +0,0 @@
1
- // eslint.config.js
2
- import js from "@eslint/js";
3
- import typescript from "@typescript-eslint/eslint-plugin";
4
- import typescriptParser from "@typescript-eslint/parser";
5
- import prettier from "eslint-plugin-prettier";
6
- import prettierConfig from "eslint-config-prettier";
7
- import globals from "globals";
8
-
9
- export default [
10
- js.configs.recommended,
11
- {
12
- files: ["**/*.{js,mjs,cjs,ts,tsx}"],
13
- languageOptions: {
14
- ecmaVersion: "latest",
15
- sourceType: "module",
16
- parser: typescriptParser,
17
- globals: {
18
- ...globals.node,
19
- RequestInit: "readonly",
20
- },
21
- },
22
- plugins: {
23
- "@typescript-eslint": typescript,
24
- prettier: prettier,
25
- },
26
- rules: {
27
- ...typescript.configs.recommended.rules,
28
- ...prettierConfig.rules,
29
- "prettier/prettier": "error",
30
- "no-unused-vars": "off",
31
- "@typescript-eslint/no-unused-vars": "error",
32
- },
33
- },
34
- ];
package/memory.md DELETED
@@ -1,51 +0,0 @@
1
- # Memory
2
-
3
- ## Project Purpose
4
-
5
- This project appears to be a TypeScript project template, providing a basic setup for developing and testing TypeScript code. It includes a simple `add` function and a corresponding unit test.
6
-
7
- ## Project Structure
8
-
9
- - `.gitignore`: Specifies intentionally untracked files to ignore.
10
- - `eslint.config.js`: ESLint configuration for linting TypeScript files.
11
- - `Jenkinsfile`: Likely a Jenkins pipeline definition for CI/CD.
12
- - `package-lock.json`: Records the exact dependency tree.
13
- - `package.json`: Project metadata, scripts, and dependencies.
14
- - `README.md`: Project README file.
15
- - `RELEASE.md`: Release notes or guidelines.
16
- - `src/`: Source code directory.
17
- - `src/index.ts`: Contains the main application logic (currently a simple `add` function).
18
- - `test/`: Test code directory.
19
- - `test/index.test.ts`: Contains unit tests for `src/index.ts`.
20
-
21
- ## Project Technical Details
22
-
23
- ### Technical Stack
24
-
25
- - **Language**: Node.js Native TypeScript (means not compiler required and must import module with `.ts`/`.js` suffix)
26
- - **Runtime**: Node.js
27
- - **Linting**: ESLint with `@typescript-eslint/eslint-plugin` and `@typescript-eslint/parser`
28
- - **Formatting**: Prettier with `eslint-config-prettier` and `eslint-plugin-prettier`
29
- - **Testing**: Node.js built-in test runner (`node --test`)
30
-
31
- ### Important Dependent Libraries
32
-
33
- - `@types/node`: Type definitions for Node.js.
34
- - `@typescript-eslint/eslint-plugin`: ESLint plugin for TypeScript.
35
- - `@typescript-eslint/parser`: Parser for ESLint to understand TypeScript.
36
- - `eslint`: Linter.
37
- - `eslint-config-prettier`: Disables ESLint rules that conflict with Prettier.
38
- - `eslint-plugin-prettier`: Runs Prettier as an ESLint rule.
39
- - `prettier`: Code formatter.
40
-
41
- ## Features Implemented
42
-
43
- - A basic `add` function in `src/index.ts`.
44
- - A unit test for the `add` function in `test/index.test.ts`.
45
- - Linting and formatting configurations.
46
- - A test script using Node.js's built-in test runner.
47
-
48
- ## Design Patterns
49
-
50
- - **Modular Design**: The `add` function is exported from `index.ts` and imported into `index.test.ts`, demonstrating modularity.
51
- - **Unit Testing**: The project includes a dedicated test file and uses a unit testing framework to verify the correctness of the `add` function.
@@ -1,322 +0,0 @@
1
- /**
2
- * Tests for Certificate Manager
3
- */
4
-
5
- import { describe, it, beforeEach, afterEach } from "node:test";
6
- import assert from "node:assert";
7
- import { existsSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from "node:fs";
8
- import path from "node:path";
9
- import os from "node:os";
10
- import {
11
- getAllIntermediateCerts,
12
- getIntermediateCerts,
13
- refreshAllCerts,
14
- clearCache,
15
- getCacheStatus,
16
- } from "./cert-manager.ts";
17
-
18
- // Test cache directory (same as in cert-manager.ts)
19
- const CERT_CACHE_DIR = path.join(os.homedir(), ".cache", "memos-mcp", "certs");
20
- const CACHE_FILE = path.join(CERT_CACHE_DIR, "cert-cache.json");
21
-
22
- describe("CertManager", () => {
23
- // Store original cache if exists
24
- let originalCache: string | null = null;
25
-
26
- beforeEach(() => {
27
- // Backup original cache if it exists (sync for beforeEach)
28
- if (existsSync(CACHE_FILE)) {
29
- originalCache = readFileSync(CACHE_FILE, "utf-8");
30
- }
31
- });
32
-
33
- afterEach(async () => {
34
- // Restore original cache
35
- if (originalCache) {
36
- writeFileSync(CACHE_FILE, originalCache);
37
- } else if (existsSync(CACHE_FILE)) {
38
- // If there was no original cache, remove the test one
39
- unlinkSync(CACHE_FILE);
40
- }
41
- originalCache = null;
42
- });
43
-
44
- describe("clearCache", () => {
45
- it("should remove the cache file", async () => {
46
- // Ensure cache exists first
47
- if (!existsSync(CERT_CACHE_DIR)) {
48
- mkdirSync(CERT_CACHE_DIR, { recursive: true });
49
- }
50
- writeFileSync(CACHE_FILE, JSON.stringify({ version: 1, certs: {} }));
51
-
52
- await clearCache();
53
-
54
- assert.strictEqual(existsSync(CACHE_FILE), false);
55
- });
56
-
57
- it("should not throw if cache file does not exist", async () => {
58
- // Ensure cache doesn't exist
59
- if (existsSync(CACHE_FILE)) {
60
- unlinkSync(CACHE_FILE);
61
- }
62
-
63
- await assert.doesNotReject(async () => await clearCache());
64
- });
65
- });
66
-
67
- describe("getCacheStatus", () => {
68
- it("should return empty object when cache is empty", async () => {
69
- await clearCache();
70
-
71
- const status = await getCacheStatus();
72
-
73
- assert.deepStrictEqual(status, {});
74
- });
75
-
76
- it("should return status for cached certificates", async () => {
77
- // Clear in-memory cache first so it reloads from file
78
- await clearCache();
79
-
80
- // Create a mock cache
81
- const mockCache = {
82
- version: 1,
83
- certs: {
84
- E8: {
85
- pem: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
86
- fetchedAt: Date.now(),
87
- expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000, // 1 year from now
88
- },
89
- },
90
- };
91
-
92
- if (!existsSync(CERT_CACHE_DIR)) {
93
- mkdirSync(CERT_CACHE_DIR, { recursive: true });
94
- }
95
- writeFileSync(CACHE_FILE, JSON.stringify(mockCache));
96
-
97
- const status = await getCacheStatus();
98
-
99
- assert.ok(status.E8);
100
- assert.ok(status.E8.fetchedAt instanceof Date);
101
- assert.ok(status.E8.expiresAt instanceof Date);
102
- assert.strictEqual(typeof status.E8.needsRefresh, "boolean");
103
- });
104
-
105
- it("should indicate needsRefresh for old cache entries", async () => {
106
- // Clear in-memory cache first so it reloads from file
107
- await clearCache();
108
-
109
- // Create an old cache entry (40 days ago)
110
- const fortyDaysAgo = Date.now() - 40 * 24 * 60 * 60 * 1000;
111
- const mockCache = {
112
- version: 1,
113
- certs: {
114
- E8: {
115
- pem: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
116
- fetchedAt: fortyDaysAgo,
117
- expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
118
- },
119
- },
120
- };
121
-
122
- if (!existsSync(CERT_CACHE_DIR)) {
123
- mkdirSync(CERT_CACHE_DIR, { recursive: true });
124
- }
125
- writeFileSync(CACHE_FILE, JSON.stringify(mockCache));
126
-
127
- const status = await getCacheStatus();
128
-
129
- assert.strictEqual(status.E8.needsRefresh, true);
130
- });
131
-
132
- it("should indicate needsRefresh for soon-to-expire certificates", async () => {
133
- // Clear in-memory cache first so it reloads from file
134
- await clearCache();
135
-
136
- // Create a cache entry that expires in 20 days
137
- const twentyDaysFromNow = Date.now() + 20 * 24 * 60 * 60 * 1000;
138
- const mockCache = {
139
- version: 1,
140
- certs: {
141
- E8: {
142
- pem: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
143
- fetchedAt: Date.now(),
144
- expiresAt: twentyDaysFromNow,
145
- },
146
- },
147
- };
148
-
149
- if (!existsSync(CERT_CACHE_DIR)) {
150
- mkdirSync(CERT_CACHE_DIR, { recursive: true });
151
- }
152
- writeFileSync(CACHE_FILE, JSON.stringify(mockCache));
153
-
154
- const status = await getCacheStatus();
155
-
156
- assert.strictEqual(status.E8.needsRefresh, true);
157
- });
158
- });
159
-
160
- describe("getIntermediateCerts", () => {
161
- it("should return empty array for unknown certificate names", async () => {
162
- const result = await getIntermediateCerts(["UNKNOWN_CERT"]);
163
-
164
- assert.deepStrictEqual(result, []);
165
- });
166
-
167
- it("should return cached certificate if available and fresh", async () => {
168
- // Clear in-memory cache first so it reloads from file
169
- await clearCache();
170
-
171
- // Create a fresh cache
172
- const mockCache = {
173
- version: 1,
174
- certs: {
175
- E8: {
176
- pem: "-----BEGIN CERTIFICATE-----\nMOCK_E8_CERT\n-----END CERTIFICATE-----",
177
- fetchedAt: Date.now(),
178
- expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
179
- },
180
- },
181
- };
182
-
183
- if (!existsSync(CERT_CACHE_DIR)) {
184
- mkdirSync(CERT_CACHE_DIR, { recursive: true });
185
- }
186
- writeFileSync(CACHE_FILE, JSON.stringify(mockCache));
187
-
188
- const result = await getIntermediateCerts(["E8"]);
189
-
190
- assert.ok(result.length === 1);
191
- assert.ok(result[0].includes("MOCK_E8_CERT"));
192
- });
193
-
194
- it("should return multiple certificates when requested", async () => {
195
- // Clear in-memory cache first so it reloads from file
196
- await clearCache();
197
-
198
- // Create cache with multiple certs
199
- const mockCache = {
200
- version: 1,
201
- certs: {
202
- E8: {
203
- pem: "-----BEGIN CERTIFICATE-----\nMOCK_E8\n-----END CERTIFICATE-----",
204
- fetchedAt: Date.now(),
205
- expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
206
- },
207
- R3: {
208
- pem: "-----BEGIN CERTIFICATE-----\nMOCK_R3\n-----END CERTIFICATE-----",
209
- fetchedAt: Date.now(),
210
- expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
211
- },
212
- },
213
- };
214
-
215
- if (!existsSync(CERT_CACHE_DIR)) {
216
- mkdirSync(CERT_CACHE_DIR, { recursive: true });
217
- }
218
- writeFileSync(CACHE_FILE, JSON.stringify(mockCache));
219
-
220
- const result = await getIntermediateCerts(["E8", "R3"]);
221
-
222
- assert.strictEqual(result.length, 2);
223
- assert.ok(result.some((c) => c.includes("MOCK_E8")));
224
- assert.ok(result.some((c) => c.includes("MOCK_R3")));
225
- });
226
- });
227
-
228
- describe("getAllIntermediateCerts (integration)", () => {
229
- it("should fetch and cache certificates from Let's Encrypt", async () => {
230
- // Clear cache first
231
- await clearCache();
232
-
233
- // This is an integration test - it actually fetches from the network
234
- const certs = await getAllIntermediateCerts();
235
-
236
- // Should have fetched at least some certificates
237
- assert.ok(certs.length > 0, "Should have fetched certificates");
238
- assert.ok(
239
- certs.some((c) => c.includes("-----BEGIN CERTIFICATE-----")),
240
- "Should contain PEM certificates",
241
- );
242
-
243
- // Check cache was populated
244
- const status = await getCacheStatus();
245
- const cachedCerts = Object.keys(status);
246
- assert.ok(cachedCerts.length > 0, "Should have cached certificates");
247
- });
248
- });
249
-
250
- describe("refreshAllCerts (integration)", () => {
251
- it("should refresh all certificates", async () => {
252
- // Clear cache first
253
- await clearCache();
254
-
255
- // Refresh all certs
256
- await refreshAllCerts();
257
-
258
- // Check cache was populated
259
- const status = await getCacheStatus();
260
- const cachedCerts = Object.keys(status);
261
-
262
- // Should have cached multiple certificates
263
- assert.ok(cachedCerts.length >= 5, `Expected at least 5 certs, got ${cachedCerts.length}`);
264
-
265
- // All should have been recently fetched
266
- const now = Date.now();
267
- for (const [name, info] of Object.entries(status)) {
268
- const fetchedAge = now - info.fetchedAt.getTime();
269
- // Should have been fetched within the last minute
270
- assert.ok(fetchedAge < 60000, `${name} should have been fetched recently, age: ${fetchedAge}ms`);
271
- }
272
- });
273
- });
274
-
275
- describe("cache persistence", () => {
276
- it("should persist cache across calls", async () => {
277
- // Clear in-memory cache first so it reloads from file
278
- await clearCache();
279
-
280
- // Create initial cache
281
- const mockCache = {
282
- version: 1,
283
- certs: {
284
- TEST: {
285
- pem: "-----BEGIN CERTIFICATE-----\nTEST_PERSIST\n-----END CERTIFICATE-----",
286
- fetchedAt: Date.now(),
287
- expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000,
288
- },
289
- },
290
- };
291
-
292
- if (!existsSync(CERT_CACHE_DIR)) {
293
- mkdirSync(CERT_CACHE_DIR, { recursive: true });
294
- }
295
- writeFileSync(CACHE_FILE, JSON.stringify(mockCache));
296
-
297
- // Read status - this loads the cache
298
- const status1 = await getCacheStatus();
299
- assert.ok(status1.TEST);
300
-
301
- // Read again - should still have the same data
302
- const status2 = await getCacheStatus();
303
- assert.ok(status2.TEST);
304
- assert.strictEqual(status1.TEST.fetchedAt.getTime(), status2.TEST.fetchedAt.getTime());
305
- });
306
-
307
- it("should handle corrupted cache gracefully", async () => {
308
- // Clear in-memory cache first so it reloads from file
309
- await clearCache();
310
-
311
- // Write corrupted cache
312
- if (!existsSync(CERT_CACHE_DIR)) {
313
- mkdirSync(CERT_CACHE_DIR, { recursive: true });
314
- }
315
- writeFileSync(CACHE_FILE, "not valid json {{{");
316
-
317
- // Should not throw, should return empty
318
- const status = await getCacheStatus();
319
- assert.deepStrictEqual(status, {});
320
- });
321
- });
322
- });
@@ -1,312 +0,0 @@
1
- /**
2
- * Tests for Memos API Client
3
- */
4
-
5
- import { describe, it, mock, beforeEach } from "node:test";
6
- import assert from "node:assert";
7
- import { MemosClient } from "./memos-client.ts";
8
-
9
- // Mock fetch function - cast to any since undici's fetch has slightly different types
10
- const mockFetch = mock.fn<typeof globalThis.fetch>();
11
-
12
- describe("MemosClient", () => {
13
- let client: MemosClient;
14
-
15
- beforeEach(() => {
16
- mockFetch.mock.resetCalls();
17
- client = new MemosClient({
18
- baseUrl: "https://memos.example.com",
19
- accessToken: "test-token",
20
- // Inject mock fetch via config instead of globalThis
21
- fetch: mockFetch as unknown as typeof import("undici").fetch,
22
- });
23
- });
24
-
25
- describe("constructor", () => {
26
- it("should remove trailing slash from baseUrl", () => {
27
- const clientWithSlash = new MemosClient({
28
- baseUrl: "https://memos.example.com/",
29
- accessToken: "test-token",
30
- fetch: mockFetch as unknown as typeof import("undici").fetch,
31
- });
32
- // We can verify this by checking a request URL
33
- mockFetch.mock.mockImplementationOnce(() =>
34
- Promise.resolve(new Response(JSON.stringify({ name: "users/1" }), { status: 200 })),
35
- );
36
-
37
- clientWithSlash.getCurrentUser();
38
-
39
- const callArgs = mockFetch.mock.calls[0];
40
- assert.ok(callArgs);
41
- assert.strictEqual(callArgs.arguments[0], "https://memos.example.com/api/v1/auth/me");
42
- });
43
- });
44
-
45
- describe("createMemo", () => {
46
- it("should create a memo with content only", async () => {
47
- const mockMemo = {
48
- name: "memos/abc123",
49
- content: "Test memo",
50
- visibility: "PRIVATE",
51
- tags: [],
52
- createTime: "2024-01-01T00:00:00Z",
53
- };
54
-
55
- mockFetch.mock.mockImplementationOnce(() =>
56
- Promise.resolve(new Response(JSON.stringify(mockMemo), { status: 200 })),
57
- );
58
-
59
- const result = await client.createMemo({ content: "Test memo" });
60
-
61
- assert.strictEqual(result.name, "memos/abc123");
62
- assert.strictEqual(result.content, "Test memo");
63
-
64
- const callArgs = mockFetch.mock.calls[0];
65
- assert.ok(callArgs);
66
- assert.strictEqual(callArgs.arguments[0], "https://memos.example.com/api/v1/memos");
67
- assert.strictEqual(callArgs.arguments[1]?.method, "POST");
68
- assert.strictEqual(
69
- (callArgs.arguments[1]?.headers as Record<string, string>)?.Authorization,
70
- "Bearer test-token",
71
- );
72
- });
73
-
74
- it("should create a memo with visibility and location", async () => {
75
- const mockMemo = {
76
- name: "memos/xyz789",
77
- content: "Geo memo",
78
- visibility: "PUBLIC",
79
- location: { placeholder: "NYC", latitude: 40.7, longitude: -74.0 },
80
- };
81
-
82
- mockFetch.mock.mockImplementationOnce(() =>
83
- Promise.resolve(new Response(JSON.stringify(mockMemo), { status: 200 })),
84
- );
85
-
86
- const result = await client.createMemo({
87
- content: "Geo memo",
88
- visibility: "PUBLIC",
89
- location: { placeholder: "NYC", latitude: 40.7, longitude: -74.0 },
90
- });
91
-
92
- assert.strictEqual(result.visibility, "PUBLIC");
93
- assert.deepStrictEqual(result.location, { placeholder: "NYC", latitude: 40.7, longitude: -74.0 });
94
-
95
- const callArgs = mockFetch.mock.calls[0];
96
- const body = JSON.parse(callArgs?.arguments[1]?.body as string);
97
- assert.strictEqual(body.content, "Geo memo");
98
- assert.strictEqual(body.visibility, "PUBLIC");
99
- assert.deepStrictEqual(body.location, { placeholder: "NYC", latitude: 40.7, longitude: -74.0 });
100
- });
101
-
102
- it("should throw error on API failure", async () => {
103
- mockFetch.mock.mockImplementationOnce(() =>
104
- Promise.resolve(new Response("Unauthorized", { status: 401, statusText: "Unauthorized" })),
105
- );
106
-
107
- await assert.rejects(async () => await client.createMemo({ content: "Test" }), {
108
- message: /Memos API error: 401/,
109
- });
110
- });
111
- });
112
-
113
- describe("listMemos", () => {
114
- it("should list memos without filters", async () => {
115
- const mockResponse = {
116
- memos: [{ name: "memos/1", content: "Memo 1" }],
117
- nextPageToken: "token123",
118
- };
119
-
120
- mockFetch.mock.mockImplementationOnce(() =>
121
- Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 })),
122
- );
123
-
124
- const result = await client.listMemos();
125
-
126
- assert.strictEqual(result.memos.length, 1);
127
- assert.strictEqual(result.nextPageToken, "token123");
128
-
129
- const callArgs = mockFetch.mock.calls[0];
130
- assert.strictEqual(callArgs?.arguments[0], "https://memos.example.com/api/v1/memos");
131
- });
132
-
133
- it("should list memos with all filters", async () => {
134
- const mockResponse = { memos: [], nextPageToken: undefined };
135
-
136
- mockFetch.mock.mockImplementationOnce(() =>
137
- Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 })),
138
- );
139
-
140
- await client.listMemos({
141
- filter: 'visibility == "PUBLIC"',
142
- pageSize: 50,
143
- pageToken: "abc",
144
- state: "ARCHIVED",
145
- orderBy: "create_time desc",
146
- });
147
-
148
- const callArgs = mockFetch.mock.calls[0];
149
- const url = callArgs?.arguments[0] as string;
150
-
151
- assert.ok(url.includes("filter=visibility"));
152
- assert.ok(url.includes("pageSize=50"));
153
- assert.ok(url.includes("pageToken=abc"));
154
- assert.ok(url.includes("state=ARCHIVED"));
155
- assert.ok(url.includes("orderBy=create_time"));
156
- });
157
- });
158
-
159
- describe("getMemo", () => {
160
- it("should get a memo by ID", async () => {
161
- const mockMemo = {
162
- name: "memos/abc123",
163
- content: "My memo",
164
- visibility: "PRIVATE",
165
- };
166
-
167
- mockFetch.mock.mockImplementationOnce(() =>
168
- Promise.resolve(new Response(JSON.stringify(mockMemo), { status: 200 })),
169
- );
170
-
171
- const result = await client.getMemo("abc123");
172
-
173
- assert.strictEqual(result.name, "memos/abc123");
174
- assert.strictEqual(result.content, "My memo");
175
-
176
- const callArgs = mockFetch.mock.calls[0];
177
- assert.strictEqual(callArgs?.arguments[0], "https://memos.example.com/api/v1/memos/abc123");
178
- });
179
-
180
- it("should throw error when memo not found", async () => {
181
- mockFetch.mock.mockImplementationOnce(() =>
182
- Promise.resolve(new Response("Not Found", { status: 404, statusText: "Not Found" })),
183
- );
184
-
185
- await assert.rejects(async () => await client.getMemo("nonexistent"), {
186
- message: /Memos API error: 404/,
187
- });
188
- });
189
- });
190
-
191
- describe("searchMemos", () => {
192
- it("should search memos by query", async () => {
193
- const mockResponse = {
194
- memos: [{ name: "memos/1", content: "Hello world" }],
195
- };
196
-
197
- mockFetch.mock.mockImplementationOnce(() =>
198
- Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 })),
199
- );
200
-
201
- await client.searchMemos("Hello");
202
-
203
- const callArgs = mockFetch.mock.calls[0];
204
- const url = callArgs?.arguments[0] as string;
205
-
206
- // URL will be encoded, check for the encoded version
207
- assert.ok(url.includes("filter="));
208
- assert.ok(decodeURIComponent(url).includes('content.contains("Hello")'));
209
- });
210
-
211
- it("should escape quotes in search query", async () => {
212
- const mockResponse = { memos: [] };
213
-
214
- mockFetch.mock.mockImplementationOnce(() =>
215
- Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 })),
216
- );
217
-
218
- await client.searchMemos('test "quoted" text');
219
-
220
- const callArgs = mockFetch.mock.calls[0];
221
- const url = callArgs?.arguments[0] as string;
222
-
223
- // Check decoded URL contains escaped quotes
224
- const decodedUrl = decodeURIComponent(url);
225
- assert.ok(decodedUrl.includes('\\"quoted\\"'));
226
- });
227
-
228
- it("should pass additional options to listMemos", async () => {
229
- const mockResponse = { memos: [] };
230
-
231
- mockFetch.mock.mockImplementationOnce(() =>
232
- Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 })),
233
- );
234
-
235
- await client.searchMemos("test", { pageSize: 10, state: "ARCHIVED" });
236
-
237
- const callArgs = mockFetch.mock.calls[0];
238
- const url = callArgs?.arguments[0] as string;
239
-
240
- assert.ok(url.includes("pageSize=10"));
241
- assert.ok(url.includes("state=ARCHIVED"));
242
- });
243
- });
244
-
245
- describe("searchByTag", () => {
246
- it("should search memos by tag", async () => {
247
- const mockResponse = {
248
- memos: [{ name: "memos/1", content: "#important task", tags: ["important"] }],
249
- };
250
-
251
- mockFetch.mock.mockImplementationOnce(() =>
252
- Promise.resolve(new Response(JSON.stringify(mockResponse), { status: 200 })),
253
- );
254
-
255
- await client.searchByTag("important");
256
-
257
- const callArgs = mockFetch.mock.calls[0];
258
- const url = callArgs?.arguments[0] as string;
259
-
260
- // Check URL contains filter parameter with tag
261
- assert.ok(url.includes("filter="));
262
- assert.ok(url.includes("important"));
263
- });
264
- });
265
-
266
- describe("getCurrentUser", () => {
267
- it("should get current user info", async () => {
268
- const mockUser = { name: "users/1", username: "testuser" };
269
-
270
- mockFetch.mock.mockImplementationOnce(() =>
271
- Promise.resolve(new Response(JSON.stringify(mockUser), { status: 200 })),
272
- );
273
-
274
- const result = await client.getCurrentUser();
275
-
276
- assert.deepStrictEqual(result, { name: "users/1", username: "testuser" });
277
-
278
- const callArgs = mockFetch.mock.calls[0];
279
- assert.strictEqual(callArgs?.arguments[0], "https://memos.example.com/api/v1/auth/me");
280
- });
281
- });
282
-
283
- describe("testConnection", () => {
284
- it("should return true on successful connection", async () => {
285
- mockFetch.mock.mockImplementationOnce(() =>
286
- Promise.resolve(new Response(JSON.stringify({ name: "users/1" }), { status: 200 })),
287
- );
288
-
289
- const result = await client.testConnection();
290
-
291
- assert.strictEqual(result, true);
292
- });
293
-
294
- it("should return false on failed connection", async () => {
295
- mockFetch.mock.mockImplementationOnce(() =>
296
- Promise.resolve(new Response("Unauthorized", { status: 401, statusText: "Unauthorized" })),
297
- );
298
-
299
- const result = await client.testConnection();
300
-
301
- assert.strictEqual(result, false);
302
- });
303
-
304
- it("should return false on network error", async () => {
305
- mockFetch.mock.mockImplementationOnce(() => Promise.reject(new Error("Network error")));
306
-
307
- const result = await client.testConnection();
308
-
309
- assert.strictEqual(result, false);
310
- });
311
- });
312
- });
package/tsconfig.json DELETED
@@ -1,25 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "lib": ["ES2022"],
7
- "strict": true,
8
- "esModuleInterop": true,
9
- "skipLibCheck": true,
10
- "forceConsistentCasingInFileNames": true,
11
- "noEmit": true,
12
- "declaration": true,
13
- "declarationMap": true,
14
- "sourceMap": true,
15
- "resolveJsonModule": true,
16
- "isolatedModules": true,
17
- "verbatimModuleSyntax": true,
18
- "noUncheckedIndexedAccess": true,
19
- "noImplicitOverride": true,
20
- "noPropertyAccessFromIndexSignature": false,
21
- "allowImportingTsExtensions": true
22
- },
23
- "include": ["src/**/*.ts"],
24
- "exclude": ["node_modules", "dist", "**/*.test.ts"]
25
- }