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.
- package/README.md +54 -10
- 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} +156 -104
- 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} +35 -14
- 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} +50 -13
- package/build/{propertyTools.js → tools/properties/tools.js} +22 -6
- package/build/{propertyTypes.js → tools/properties/types.js} +19 -7
- 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/mcp-client-config.example.json +23 -0
- package/package.json +4 -3
- 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} +174 -134
- 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} +42 -16
- 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} +54 -13
- package/src/{propertyTools.ts → tools/properties/tools.ts} +33 -7
- package/src/{propertyTypes.ts → tools/properties/types.ts} +31 -7
- 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 -238
- package/build/tools.js +0 -863
- package/build/types.js +0 -37
- package/src/server.ts +0 -308
- package/src/tools.ts +0 -946
- 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,
|
|
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);
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
57
|
-
|
|
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 ??
|
|
60
|
-
maxContentLength: config.maxContentLength ??
|
|
61
|
-
maxBodyLength: config.maxBodyLength ??
|
|
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
|
-
|
|
94
|
-
"WARNING: SSL verification is disabled.
|
|
95
|
-
"
|
|
96
|
-
"
|
|
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
|
-
|
|
171
|
+
sanitizeHeader(value)
|
|
119
172
|
])
|
|
120
173
|
);
|
|
121
174
|
}
|
|
122
175
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|