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.
- package/build/index.js +12 -3
- package/build/mcp/handlers.js +138 -0
- package/build/mcp/index.js +7 -0
- package/build/mcp/server.js +131 -0
- package/build/mcp/types.js +7 -0
- package/build/{obsidian.js → obsidian/client.js} +116 -96
- package/build/obsidian/errors.js +75 -0
- package/build/obsidian/index.js +7 -0
- package/build/obsidian/types.js +12 -0
- package/build/resources/index.js +15 -0
- package/build/{resources.js → resources/tags.js} +31 -5
- package/build/resources/types.js +5 -0
- package/build/tools/base.js +78 -0
- package/build/tools/files/content.js +171 -0
- package/build/tools/files/index.js +22 -0
- package/build/tools/files/list.js +133 -0
- package/build/tools/index.js +31 -0
- package/build/tools/properties/index.js +19 -0
- package/build/{properties.js → tools/properties/manager.js} +40 -8
- package/build/{propertyTools.js → tools/properties/tools.js} +22 -6
- package/build/{propertyTypes.js → tools/properties/types.js} +17 -5
- package/build/tools/search/complex.js +203 -0
- package/build/tools/search/index.js +20 -0
- package/build/tools/search/simple.js +127 -0
- package/build/utils/errors.js +59 -0
- package/build/utils/index.js +9 -0
- package/build/utils/logging.js +119 -0
- package/build/utils/rate-limiting.js +94 -0
- package/build/utils/tokenization.js +62 -0
- package/build/utils/validation.js +88 -0
- package/examples/README.md +48 -0
- package/examples/append-content.md +63 -0
- package/examples/complex-search.md +117 -0
- package/examples/find-in-file.md +94 -0
- package/examples/get-file-contents.md +72 -0
- package/examples/get-properties.md +89 -0
- package/examples/list-files-in-dir.md +55 -0
- package/examples/list-files-in-vault.md +53 -0
- package/examples/patch-content.md +60 -0
- package/examples/update-properties.md +126 -0
- package/package.json +1 -1
- package/src/index.ts +13 -3
- package/src/mcp/handlers.ts +183 -0
- package/src/mcp/index.ts +6 -0
- package/src/mcp/server.ts +162 -0
- package/src/mcp/types.ts +46 -0
- package/src/{obsidian.ts → obsidian/client.ts} +132 -125
- package/src/obsidian/errors.ts +105 -0
- package/src/obsidian/index.ts +6 -0
- package/src/obsidian/types.ts +124 -0
- package/src/resources/index.ts +17 -0
- package/src/{resources.ts → resources/tags.ts} +38 -6
- package/src/resources/types.ts +30 -0
- package/src/tools/base.ts +112 -0
- package/src/tools/files/content.ts +206 -0
- package/src/tools/files/index.ts +31 -0
- package/src/tools/files/list.ts +150 -0
- package/src/tools/index.ts +38 -0
- package/src/tools/properties/index.ts +21 -0
- package/src/{properties.ts → tools/properties/manager.ts} +41 -8
- package/src/{propertyTools.ts → tools/properties/tools.ts} +33 -7
- package/src/{propertyTypes.ts → tools/properties/types.ts} +29 -5
- package/src/tools/search/complex.ts +231 -0
- package/src/tools/search/index.ts +22 -0
- package/src/tools/search/simple.ts +147 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logging.ts +146 -0
- package/src/utils/rate-limiting.ts +114 -0
- package/src/utils/tokenization.ts +71 -0
- package/src/utils/validation.ts +95 -0
- package/build/server.js +0 -263
- package/build/tools.js +0 -845
- package/build/types.js +0 -37
- package/src/server.ts +0 -335
- package/src/tools.ts +0 -926
- 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,
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
+
sanitizeHeader(value)
|
|
152
172
|
])
|
|
153
173
|
);
|
|
154
174
|
}
|
|
155
175
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|