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