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