obsidian-mcp-server 1.2.6 → 1.4.0

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.
Files changed (79) hide show
  1. package/README.md +54 -10
  2. package/build/index.js +12 -3
  3. package/build/mcp/handlers.js +138 -0
  4. package/build/mcp/index.js +7 -0
  5. package/build/mcp/server.js +131 -0
  6. package/build/mcp/types.js +7 -0
  7. package/build/{obsidian.js → obsidian/client.js} +156 -104
  8. package/build/obsidian/errors.js +75 -0
  9. package/build/obsidian/index.js +7 -0
  10. package/build/obsidian/types.js +12 -0
  11. package/build/resources/index.js +15 -0
  12. package/build/{resources.js → resources/tags.js} +35 -14
  13. package/build/resources/types.js +5 -0
  14. package/build/tools/base.js +78 -0
  15. package/build/tools/files/content.js +171 -0
  16. package/build/tools/files/index.js +22 -0
  17. package/build/tools/files/list.js +133 -0
  18. package/build/tools/index.js +31 -0
  19. package/build/tools/properties/index.js +19 -0
  20. package/build/{properties.js → tools/properties/manager.js} +50 -13
  21. package/build/{propertyTools.js → tools/properties/tools.js} +22 -6
  22. package/build/{propertyTypes.js → tools/properties/types.js} +19 -7
  23. package/build/tools/search/complex.js +203 -0
  24. package/build/tools/search/index.js +20 -0
  25. package/build/tools/search/simple.js +127 -0
  26. package/build/utils/errors.js +59 -0
  27. package/build/utils/index.js +9 -0
  28. package/build/utils/logging.js +119 -0
  29. package/build/utils/rate-limiting.js +94 -0
  30. package/build/utils/tokenization.js +62 -0
  31. package/build/utils/validation.js +88 -0
  32. package/examples/README.md +48 -0
  33. package/examples/append-content.md +63 -0
  34. package/examples/complex-search.md +117 -0
  35. package/examples/find-in-file.md +94 -0
  36. package/examples/get-file-contents.md +72 -0
  37. package/examples/get-properties.md +89 -0
  38. package/examples/list-files-in-dir.md +55 -0
  39. package/examples/list-files-in-vault.md +53 -0
  40. package/examples/patch-content.md +60 -0
  41. package/examples/update-properties.md +126 -0
  42. package/mcp-client-config.example.json +23 -0
  43. package/package.json +4 -3
  44. package/src/index.ts +13 -3
  45. package/src/mcp/handlers.ts +183 -0
  46. package/src/mcp/index.ts +6 -0
  47. package/src/mcp/server.ts +162 -0
  48. package/src/mcp/types.ts +46 -0
  49. package/src/{obsidian.ts → obsidian/client.ts} +174 -134
  50. package/src/obsidian/errors.ts +105 -0
  51. package/src/obsidian/index.ts +6 -0
  52. package/src/obsidian/types.ts +124 -0
  53. package/src/resources/index.ts +17 -0
  54. package/src/{resources.ts → resources/tags.ts} +42 -16
  55. package/src/resources/types.ts +30 -0
  56. package/src/tools/base.ts +112 -0
  57. package/src/tools/files/content.ts +206 -0
  58. package/src/tools/files/index.ts +31 -0
  59. package/src/tools/files/list.ts +150 -0
  60. package/src/tools/index.ts +38 -0
  61. package/src/tools/properties/index.ts +21 -0
  62. package/src/{properties.ts → tools/properties/manager.ts} +54 -13
  63. package/src/{propertyTools.ts → tools/properties/tools.ts} +33 -7
  64. package/src/{propertyTypes.ts → tools/properties/types.ts} +31 -7
  65. package/src/tools/search/complex.ts +231 -0
  66. package/src/tools/search/index.ts +22 -0
  67. package/src/tools/search/simple.ts +147 -0
  68. package/src/utils/errors.ts +69 -0
  69. package/src/utils/index.ts +8 -0
  70. package/src/utils/logging.ts +146 -0
  71. package/src/utils/rate-limiting.ts +114 -0
  72. package/src/utils/tokenization.ts +71 -0
  73. package/src/utils/validation.ts +95 -0
  74. package/build/server.js +0 -238
  75. package/build/tools.js +0 -863
  76. package/build/types.js +0 -37
  77. package/src/server.ts +0 -308
  78. package/src/tools.ts +0 -946
  79. package/src/types.ts +0 -184
@@ -1,64 +1,102 @@
1
+ /**
2
+ * Obsidian REST API client implementation
3
+ */
1
4
  import axios from "axios";
2
- import type { AxiosInstance, AxiosError, AxiosRequestConfig } from "axios";
3
- import {
4
- ObsidianConfig,
5
- ObsidianError,
5
+ import type { AxiosInstance, AxiosRequestConfig } from "axios";
6
+ import { Agent } from "node:https";
7
+ import { readFileSync } from "fs";
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from "path";
10
+
11
+ import { createLogger } from '../utils/logging.js';
12
+ import { ObsidianError } from '../utils/errors.js';
13
+ import { validateFilePath, sanitizeHeader } from '../utils/validation.js';
14
+ import {
15
+ ObsidianConfig,
16
+ ObsidianServerConfig,
17
+ DEFAULT_OBSIDIAN_CONFIG,
18
+ NoteJson,
6
19
  ObsidianFile,
7
- SearchResult,
8
20
  SimpleSearchResult,
9
21
  SearchResponse,
10
- DEFAULT_OBSIDIAN_CONFIG,
11
- ObsidianServerConfig,
12
22
  JsonLogicQuery,
13
23
  ObsidianStatus,
14
24
  ObsidianCommand,
15
- NoteJson,
16
- PeriodType,
17
- ApiError
18
- } from "./types.js";
19
- import { Agent } from "node:https";
20
- import { readFileSync } from "fs";
21
- import { fileURLToPath } from 'url';
22
- import { dirname, join } from "path";
25
+ PeriodType
26
+ } from './types.js';
27
+ import {
28
+ createMissingAPIKeyMessage,
29
+ handleAxiosError
30
+ } from './errors.js';
31
+
32
+ // Logger for the ObsidianClient
33
+ const logger = createLogger('ObsidianClient');
23
34
 
24
35
  // Get package version for user agent
25
36
  const __filename = fileURLToPath(import.meta.url);
26
37
  const __dirname = dirname(__filename);
27
- const packagePath = join(__dirname, '..', '..', 'package.json');
28
38
  const VERSION = (() => {
29
39
  try {
40
+ // Look for package.json in the same directory as the built files
41
+ const packagePath = join(__dirname, '..', '..', 'package.json');
30
42
  const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
31
43
  return pkg.version;
32
44
  } catch (error) {
33
- console.warn('Could not read package.json version:', error);
34
- return '1.1.0'; // Fallback version
45
+ // Try alternative location for development
46
+ try {
47
+ const devPackagePath = join(__dirname, '..', '..', '..', 'package.json');
48
+ const pkg = JSON.parse(readFileSync(devPackagePath, 'utf-8'));
49
+ return pkg.version;
50
+ } catch (devError) {
51
+ logger.warn('Could not read package.json version, using fallback');
52
+ return '1.1.0'; // Fallback version
53
+ }
35
54
  }
36
55
  })();
37
56
 
57
+ /**
58
+ * Client for interacting with the Obsidian Local REST API
59
+ */
38
60
  export class ObsidianClient {
39
61
  private client: AxiosInstance;
40
62
  private config: Required<ObsidianConfig> & ObsidianServerConfig;
41
63
 
64
+ /**
65
+ * Create a new ObsidianClient
66
+ * @param config Configuration for the client
67
+ */
42
68
  constructor(config: ObsidianConfig) {
43
69
  if (!config.apiKey) {
44
70
  throw new ObsidianError(
45
- "Missing API key. To fix this:\n" +
46
- "1. Install the 'Local REST API' plugin in Obsidian\n" +
47
- "2. Enable the plugin in Obsidian Settings\n" +
48
- "3. Copy your API key from Obsidian Settings > Local REST API\n" +
49
- "4. Provide the API key in your configuration",
71
+ createMissingAPIKeyMessage(),
50
72
  40100 // Unauthorized
51
73
  );
52
74
  }
53
75
 
54
- // Combine defaults with provided config
76
+ // Determine if we're in a development environment
77
+ const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
78
+
79
+ // Read environment variables with fallbacks
80
+ const envConfig = {
81
+ protocol: process.env.OBSIDIAN_PROTOCOL as "http" | "https" || DEFAULT_OBSIDIAN_CONFIG.protocol,
82
+ host: process.env.OBSIDIAN_HOST || DEFAULT_OBSIDIAN_CONFIG.host,
83
+ port: parseInt(process.env.OBSIDIAN_PORT || String(DEFAULT_OBSIDIAN_CONFIG.port)),
84
+ verifySSL: process.env.VERIFY_SSL ? process.env.VERIFY_SSL === 'true' : (isDev ? false : true),
85
+ timeout: parseInt(process.env.REQUEST_TIMEOUT || '5000'),
86
+ maxContentLength: parseInt(process.env.MAX_CONTENT_LENGTH || String(50 * 1024 * 1024)),
87
+ maxBodyLength: parseInt(process.env.MAX_BODY_LENGTH || String(50 * 1024 * 1024))
88
+ };
89
+
90
+ // Combine defaults with provided config and environment variables
55
91
  this.config = {
56
- ...DEFAULT_OBSIDIAN_CONFIG,
57
- verifySSL: config.verifySSL ?? true, // Default to true as required by Obsidian REST API plugin
92
+ protocol: envConfig.protocol,
93
+ host: envConfig.host,
94
+ port: envConfig.port,
95
+ verifySSL: config.verifySSL ?? envConfig.verifySSL,
58
96
  apiKey: config.apiKey,
59
- timeout: config.timeout ?? 5000, // 5 second default timeout
60
- maxContentLength: config.maxContentLength ?? 50 * 1024 * 1024, // 50MB
61
- maxBodyLength: config.maxBodyLength ?? 50 * 1024 * 1024 // 50MB
97
+ timeout: config.timeout ?? envConfig.timeout,
98
+ maxContentLength: config.maxContentLength ?? envConfig.maxContentLength,
99
+ maxBodyLength: config.maxBodyLength ?? envConfig.maxBodyLength
62
100
  };
63
101
 
64
102
  // Configure HTTPS agent
@@ -90,20 +128,35 @@ export class ObsidianClient {
90
128
  };
91
129
 
92
130
  if (!this.config.verifySSL) {
93
- console.warn(
94
- "WARNING: SSL verification is disabled. The Obsidian REST API plugin requires HTTPS by default.\n" +
95
- "Make sure you have configured the certificate as a trusted certificate authority.\n" +
96
- "See Obsidian Settings > Local REST API > 'How to Access' for setup instructions."
131
+ logger.warn(
132
+ "WARNING: SSL verification is disabled. While this works for development, it's not recommended for production.\n" +
133
+ "To properly configure SSL certificates:\n" +
134
+ "1. Go to Obsidian Settings > Local REST API\n" +
135
+ "2. Under 'How to Access', copy the certificate\n" +
136
+ "3. For Windows users:\n" +
137
+ " - Open 'certmgr.msc' (Windows Certificate Manager)\n" +
138
+ " - Go to 'Trusted Root Certification Authorities' > 'Certificates'\n" +
139
+ " - Right-click > 'All Tasks' > 'Import' and follow the wizard\n" +
140
+ " - Select the certificate file you copied from Obsidian\n" +
141
+ "4. For other systems:\n" +
142
+ " - macOS: Add to Keychain Access\n" +
143
+ " - Linux: Add to ca-certificates"
97
144
  );
98
145
  }
99
146
 
100
147
  this.client = axios.create(axiosConfig);
101
148
  }
102
149
 
150
+ /**
151
+ * Get the base URL for the Obsidian API
152
+ */
103
153
  private getBaseUrl(): string {
104
154
  return `${this.config.protocol}://${this.config.host}:${this.config.port}`;
105
155
  }
106
156
 
157
+ /**
158
+ * Get headers for requests to the Obsidian API
159
+ */
107
160
  private getHeaders(): Record<string, string> {
108
161
  const headers: Record<string, string> = {
109
162
  Authorization: `Bearer ${this.config.apiKey}`,
@@ -115,109 +168,20 @@ export class ObsidianClient {
115
168
  return Object.fromEntries(
116
169
  Object.entries(headers).map(([key, value]) => [
117
170
  key,
118
- this.sanitizeHeader(value)
171
+ sanitizeHeader(value)
119
172
  ])
120
173
  );
121
174
  }
122
175
 
123
- private sanitizeHeader(value: string): string {
124
- // Remove any potentially harmful characters from header values
125
- return value.replace(/[^\w\s\-\._~:/?#\[\]@!$&'()*+,;=]/g, '');
126
- }
127
-
128
- private validateFilePath(filepath: string): void {
129
- // Prevent path traversal attacks
130
- const normalizedPath = filepath.replace(/\\/g, '/');
131
- if (normalizedPath.includes('../') || normalizedPath.includes('..\\')) {
132
- throw new ObsidianError('Invalid file path: Path traversal not allowed', 40001);
133
- }
134
-
135
- // Additional path validations
136
- if (normalizedPath.startsWith('/') || /^[a-zA-Z]:/.test(normalizedPath)) {
137
- throw new ObsidianError('Invalid file path: Absolute paths not allowed', 40002);
138
- }
139
- }
140
-
141
- private getErrorCode(status: number): number {
142
- switch (status) {
143
- case 400: return 40000; // Bad request
144
- case 401: return 40100; // Unauthorized
145
- case 403: return 40300; // Forbidden
146
- case 404: return 40400; // Not found
147
- case 405: return 40500; // Method not allowed
148
- case 409: return 40900; // Conflict
149
- case 429: return 42900; // Too many requests
150
- case 500: return 50000; // Internal server error
151
- case 501: return 50100; // Not implemented
152
- case 502: return 50200; // Bad gateway
153
- case 503: return 50300; // Service unavailable
154
- case 504: return 50400; // Gateway timeout
155
- default:
156
- if (status >= 400 && status < 500) return 40000 + (status - 400) * 100;
157
- if (status >= 500 && status < 600) return 50000 + (status - 500) * 100;
158
- return 50000;
159
- }
160
- }
161
-
176
+ /**
177
+ * Safely execute an API request with error handling
178
+ */
162
179
  private async safeRequest<T>(operation: () => Promise<T>): Promise<T> {
163
180
  try {
164
181
  return await operation();
165
182
  } catch (error) {
166
183
  if (axios.isAxiosError(error)) {
167
- const axiosError = error as AxiosError<ApiError>;
168
- const response = axiosError.response;
169
- const errorData = response?.data;
170
-
171
- // Handle common connection errors with helpful messages
172
- if (error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' || error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
173
- throw new ObsidianError(
174
- `SSL certificate verification failed. You have two options:\n\n` +
175
- `Option 1 - Enable HTTP (not recommended for production):\n` +
176
- `1. Go to Obsidian Settings > Local REST API\n` +
177
- `2. Enable "Enable Non-encrypted (HTTP) Server"\n` +
178
- `3. Update your client config to use "http" protocol\n\n` +
179
- `Option 2 - Configure HTTPS (recommended):\n` +
180
- `1. Go to Obsidian Settings > Local REST API\n` +
181
- `2. Under 'How to Access', copy the certificate\n` +
182
- `3. Add the certificate to your system's trusted certificates:\n` +
183
- ` - On macOS: Add to Keychain Access\n` +
184
- ` - On Windows: Add to Certificate Manager\n` +
185
- ` - On Linux: Add to ca-certificates\n` +
186
- ` For development only: Set verifySSL: false in client config\n\n` +
187
- `Original error: ${error.message}`,
188
- 50001, // SSL error code
189
- { code: error.code, config: { verifySSL: this.config.verifySSL } }
190
- );
191
- }
192
-
193
- if (error.code === 'ECONNREFUSED') {
194
- throw new ObsidianError(
195
- `Connection refused. To fix this:\n` +
196
- `1. Ensure Obsidian is running\n` +
197
- `2. Verify the 'Local REST API' plugin is enabled in Obsidian Settings\n` +
198
- `3. Check that you're using the correct host (${this.config.host}) and port (${this.config.port})\n` +
199
- `4. Make sure HTTPS is enabled in the plugin settings`,
200
- 50002, // Connection refused
201
- { code: error.code }
202
- );
203
- }
204
-
205
- if (response?.status === 401) {
206
- throw new ObsidianError(
207
- `Authentication failed. To fix this:\n` +
208
- `1. Go to Obsidian Settings > Local REST API\n` +
209
- `2. Copy your API key from the settings\n` +
210
- `3. Update your configuration with the new API key\n` +
211
- `Note: The API key changes when you regenerate certificates`,
212
- 40100, // Unauthorized
213
- { code: error.code }
214
- );
215
- }
216
-
217
- // For other errors, use API error code if available
218
- const errorCode = errorData?.errorCode ?? this.getErrorCode(response?.status ?? 500);
219
- const message = errorData?.message ?? axiosError.message ?? "Unknown error";
220
- throw new ObsidianError(message, errorCode, errorData);
184
+ throw handleAxiosError(error, this.config.host, this.config.port);
221
185
  }
222
186
 
223
187
  if (error instanceof Error) {
@@ -228,31 +192,47 @@ export class ObsidianClient {
228
192
  }
229
193
  }
230
194
 
195
+ /**
196
+ * List all files in the vault
197
+ */
231
198
  async listFilesInVault(): Promise<ObsidianFile[]> {
232
199
  return this.safeRequest(async () => {
200
+ logger.debug('Listing all files in vault');
233
201
  const response = await this.client.get<{ files: ObsidianFile[] }>("/vault/");
234
202
  return response.data.files;
235
203
  });
236
204
  }
237
205
 
206
+ /**
207
+ * List files in a specific directory
208
+ */
238
209
  async listFilesInDir(dirpath: string): Promise<ObsidianFile[]> {
239
- this.validateFilePath(dirpath);
210
+ validateFilePath(dirpath);
240
211
  return this.safeRequest(async () => {
212
+ logger.debug(`Listing files in directory: ${dirpath}`);
241
213
  const response = await this.client.get<{ files: ObsidianFile[] }>(`/vault/${dirpath}/`);
242
214
  return response.data.files;
243
215
  });
244
216
  }
245
217
 
218
+ /**
219
+ * Get the contents of a file
220
+ */
246
221
  async getFileContents(filepath: string): Promise<string> {
247
- this.validateFilePath(filepath);
222
+ validateFilePath(filepath);
248
223
  return this.safeRequest(async () => {
224
+ logger.debug(`Getting contents of file: ${filepath}`);
249
225
  const response = await this.client.get<string>(`/vault/${filepath}`);
250
226
  return response.data;
251
227
  });
252
228
  }
253
229
 
230
+ /**
231
+ * Search for a string across all files
232
+ */
254
233
  async search(query: string, contextLength: number = 100): Promise<SimpleSearchResult[]> {
255
234
  return this.safeRequest(async () => {
235
+ logger.debug(`Searching for: ${query} with context length: ${contextLength}`);
256
236
  const response = await this.client.post<SimpleSearchResult[]>(
257
237
  "/search/simple/",
258
238
  null,
@@ -262,12 +242,16 @@ export class ObsidianClient {
262
242
  });
263
243
  }
264
244
 
245
+ /**
246
+ * Append content to a file
247
+ */
265
248
  async appendContent(filepath: string, content: string): Promise<void> {
266
- this.validateFilePath(filepath);
249
+ validateFilePath(filepath);
267
250
  if (!content || typeof content !== 'string') {
268
251
  throw new ObsidianError('Invalid content: Content must be a non-empty string', 40003);
269
252
  }
270
253
  return this.safeRequest(async () => {
254
+ logger.debug(`Appending content to file: ${filepath}`);
271
255
  await this.client.post(
272
256
  `/vault/${filepath}`,
273
257
  content,
@@ -280,13 +264,17 @@ export class ObsidianClient {
280
264
  });
281
265
  }
282
266
 
267
+ /**
268
+ * Update the entire content of a file
269
+ */
283
270
  async updateContent(filepath: string, content: string): Promise<void> {
284
- this.validateFilePath(filepath);
271
+ validateFilePath(filepath);
285
272
  if (!content || typeof content !== 'string') {
286
273
  throw new ObsidianError('Invalid content: Content must be a non-empty string', 40003);
287
274
  }
288
275
 
289
276
  return this.safeRequest(async () => {
277
+ logger.debug(`Updating content of file: ${filepath}`);
290
278
  await this.client.put(
291
279
  `/vault/${filepath}`,
292
280
  content,
@@ -299,8 +287,12 @@ export class ObsidianClient {
299
287
  });
300
288
  }
301
289
 
290
+ /**
291
+ * Execute a complex search using JsonLogic query
292
+ */
302
293
  async searchJson(query: JsonLogicQuery): Promise<SearchResponse[]> {
303
294
  return this.safeRequest(async () => {
295
+ logger.debug(`Executing JSON search with query: ${JSON.stringify(query)}`);
304
296
  const isTagSearch = JSON.stringify(query).includes('"contains"') &&
305
297
  JSON.stringify(query).includes('"#"');
306
298
 
@@ -315,41 +307,61 @@ export class ObsidianClient {
315
307
  }
316
308
  );
317
309
 
318
- return isTagSearch ? response.data as SimpleSearchResult[] : response.data as SearchResult[];
310
+ return response.data as SearchResponse[];
319
311
  });
320
312
  }
321
313
 
314
+ /**
315
+ * Get server status
316
+ */
322
317
  async getStatus(): Promise<ObsidianStatus> {
323
318
  return this.safeRequest(async () => {
319
+ logger.debug('Getting server status');
324
320
  const response = await this.client.get<ObsidianStatus>("/");
325
321
  return response.data;
326
322
  });
327
323
  }
328
324
 
325
+ /**
326
+ * List available commands
327
+ */
329
328
  async listCommands(): Promise<ObsidianCommand[]> {
330
329
  return this.safeRequest(async () => {
330
+ logger.debug('Listing commands');
331
331
  const response = await this.client.get<{commands: ObsidianCommand[]}>("/commands/");
332
332
  return response.data.commands;
333
333
  });
334
334
  }
335
335
 
336
+ /**
337
+ * Execute a command by ID
338
+ */
336
339
  async executeCommand(commandId: string): Promise<void> {
337
340
  return this.safeRequest(async () => {
341
+ logger.debug(`Executing command: ${commandId}`);
338
342
  await this.client.post(`/commands/${commandId}/`);
339
343
  });
340
344
  }
341
345
 
346
+ /**
347
+ * Open a file in Obsidian
348
+ */
342
349
  async openFile(filepath: string, newLeaf: boolean = false): Promise<void> {
343
- this.validateFilePath(filepath);
350
+ validateFilePath(filepath);
344
351
  return this.safeRequest(async () => {
352
+ logger.debug(`Opening file: ${filepath}, newLeaf: ${newLeaf}`);
345
353
  await this.client.post(`/open/${filepath}`, null, {
346
354
  params: { newLeaf }
347
355
  });
348
356
  });
349
357
  }
350
358
 
359
+ /**
360
+ * Get the currently active file
361
+ */
351
362
  async getActiveFile(): Promise<NoteJson> {
352
363
  return this.safeRequest(async () => {
364
+ logger.debug('Getting active file');
353
365
  const response = await this.client.get<NoteJson>("/active/", {
354
366
  headers: {
355
367
  "Accept": "application/vnd.olrapi.note+json"
@@ -359,8 +371,12 @@ export class ObsidianClient {
359
371
  });
360
372
  }
361
373
 
374
+ /**
375
+ * Update the active file
376
+ */
362
377
  async updateActiveFile(content: string): Promise<void> {
363
378
  return this.safeRequest(async () => {
379
+ logger.debug('Updating active file');
364
380
  await this.client.put("/active/", content, {
365
381
  headers: {
366
382
  "Content-Type": "text/markdown"
@@ -369,12 +385,19 @@ export class ObsidianClient {
369
385
  });
370
386
  }
371
387
 
388
+ /**
389
+ * Delete the active file
390
+ */
372
391
  async deleteActiveFile(): Promise<void> {
373
392
  return this.safeRequest(async () => {
393
+ logger.debug('Deleting active file');
374
394
  await this.client.delete("/active/");
375
395
  });
376
396
  }
377
397
 
398
+ /**
399
+ * Patch the active file
400
+ */
378
401
  async patchActiveFile(
379
402
  operation: "append" | "prepend" | "replace",
380
403
  targetType: "heading" | "block" | "frontmatter",
@@ -387,6 +410,7 @@ export class ObsidianClient {
387
410
  }
388
411
  ): Promise<void> {
389
412
  return this.safeRequest(async () => {
413
+ logger.debug(`Patching active file: ${operation} ${targetType} "${target}"`);
390
414
  const headers: Record<string, string> = {
391
415
  "Operation": operation,
392
416
  "Target-Type": targetType,
@@ -405,8 +429,12 @@ export class ObsidianClient {
405
429
  });
406
430
  }
407
431
 
432
+ /**
433
+ * Get a periodic note (e.g., daily, weekly)
434
+ */
408
435
  async getPeriodicNote(period: PeriodType["type"]): Promise<NoteJson> {
409
436
  return this.safeRequest(async () => {
437
+ logger.debug(`Getting ${period} periodic note`);
410
438
  const response = await this.client.get<NoteJson>(`/periodic/${period}/`, {
411
439
  headers: {
412
440
  "Accept": "application/vnd.olrapi.note+json"
@@ -416,8 +444,12 @@ export class ObsidianClient {
416
444
  });
417
445
  }
418
446
 
447
+ /**
448
+ * Update a periodic note
449
+ */
419
450
  async updatePeriodicNote(period: PeriodType["type"], content: string): Promise<void> {
420
451
  return this.safeRequest(async () => {
452
+ logger.debug(`Updating ${period} periodic note`);
421
453
  await this.client.put(`/periodic/${period}/`, content, {
422
454
  headers: {
423
455
  "Content-Type": "text/markdown"
@@ -426,12 +458,19 @@ export class ObsidianClient {
426
458
  });
427
459
  }
428
460
 
461
+ /**
462
+ * Delete a periodic note
463
+ */
429
464
  async deletePeriodicNote(period: PeriodType["type"]): Promise<void> {
430
465
  return this.safeRequest(async () => {
466
+ logger.debug(`Deleting ${period} periodic note`);
431
467
  await this.client.delete(`/periodic/${period}/`);
432
468
  });
433
469
  }
434
470
 
471
+ /**
472
+ * Patch a periodic note
473
+ */
435
474
  async patchPeriodicNote(
436
475
  period: PeriodType["type"],
437
476
  operation: "append" | "prepend" | "replace",
@@ -445,6 +484,7 @@ export class ObsidianClient {
445
484
  }
446
485
  ): Promise<void> {
447
486
  return this.safeRequest(async () => {
487
+ logger.debug(`Patching ${period} periodic note: ${operation} ${targetType} "${target}"`);
448
488
  const headers: Record<string, string> = {
449
489
  "Operation": operation,
450
490
  "Target-Type": targetType,
@@ -462,4 +502,4 @@ export class ObsidianClient {
462
502
  await this.client.patch(`/periodic/${period}/`, content, { headers });
463
503
  });
464
504
  }
465
- }
505
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Error handling for Obsidian client
3
+ */
4
+ import { AxiosError } from "axios";
5
+ import { ObsidianError, getErrorCodeFromStatus } from '../utils/errors.js';
6
+ import type { ApiError } from '../utils/errors.js';
7
+
8
+ /**
9
+ * Helper function to create a descriptive error message for SSL certificate issues
10
+ */
11
+ export function createSSLErrorMessage(error: Error, config: { verifySSL: boolean }): string {
12
+ return (
13
+ `SSL certificate verification failed. You have two options:\n\n` +
14
+ `Option 1 - Enable HTTP (not recommended for production):\n` +
15
+ `1. Go to Obsidian Settings > Local REST API\n` +
16
+ `2. Enable "Enable Non-encrypted (HTTP) Server"\n` +
17
+ `3. Update your client config to use "http" protocol\n\n` +
18
+ `Option 2 - Configure HTTPS (recommended):\n` +
19
+ `1. Go to Obsidian Settings > Local REST API\n` +
20
+ `2. Under 'How to Access', copy the certificate\n` +
21
+ `3. Add the certificate to your system's trusted certificates:\n` +
22
+ ` - On macOS: Add to Keychain Access\n` +
23
+ ` - On Windows: Add to Certificate Manager\n` +
24
+ ` - On Linux: Add to ca-certificates\n` +
25
+ ` For development only: Set verifySSL: false in client config\n\n` +
26
+ `Original error: ${error.message}`
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Helper function to create a descriptive error message for connection refused issues
32
+ */
33
+ export function createConnectionRefusedMessage(host: string, port: number): string {
34
+ return (
35
+ `Connection refused. To fix this:\n` +
36
+ `1. Ensure Obsidian is running\n` +
37
+ `2. Verify the 'Local REST API' plugin is enabled in Obsidian Settings\n` +
38
+ `3. Check that you're using the correct host (${host}) and port (${port})\n` +
39
+ `4. Make sure HTTPS is enabled in the plugin settings`
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Helper function to create a descriptive error message for authentication failures
45
+ */
46
+ export function createAuthFailedMessage(): string {
47
+ return (
48
+ `Authentication failed. To fix this:\n` +
49
+ `1. Go to Obsidian Settings > Local REST API\n` +
50
+ `2. Copy your API key from the settings\n` +
51
+ `3. Update your configuration with the new API key\n` +
52
+ `Note: The API key changes when you regenerate certificates`
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Helper function to create a descriptive error message for missing API key
58
+ */
59
+ export function createMissingAPIKeyMessage(): string {
60
+ return (
61
+ `Missing API key. To fix this:\n` +
62
+ `1. Install the 'Local REST API' plugin in Obsidian\n` +
63
+ `2. Enable the plugin in Obsidian Settings\n` +
64
+ `3. Copy your API key from Obsidian Settings > Local REST API\n` +
65
+ `4. Provide the API key in your configuration`
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Helper function to handle Axios errors consistently
71
+ */
72
+ export function handleAxiosError(error: AxiosError<ApiError>, host: string, port: number): ObsidianError {
73
+ const response = error.response;
74
+ const errorData = response?.data;
75
+
76
+ // Handle common connection errors with helpful messages
77
+ if (error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' || error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
78
+ return new ObsidianError(
79
+ createSSLErrorMessage(error, { verifySSL: true }),
80
+ 50001, // SSL error code
81
+ { code: error.code }
82
+ );
83
+ }
84
+
85
+ if (error.code === 'ECONNREFUSED') {
86
+ return new ObsidianError(
87
+ createConnectionRefusedMessage(host, port),
88
+ 50002, // Connection refused
89
+ { code: error.code }
90
+ );
91
+ }
92
+
93
+ if (response?.status === 401) {
94
+ return new ObsidianError(
95
+ createAuthFailedMessage(),
96
+ 40100, // Unauthorized
97
+ { code: error.code }
98
+ );
99
+ }
100
+
101
+ // For other errors, use API error code if available
102
+ const errorCode = errorData?.errorCode ?? getErrorCodeFromStatus(response?.status ?? 500);
103
+ const message = errorData?.message ?? error.message ?? "Unknown error";
104
+ return new ObsidianError(message, errorCode, errorData);
105
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Exports for the Obsidian module
3
+ */
4
+ export * from './client.js';
5
+ export * from './types.js';
6
+ export * from './errors.js';