obsidian-mcp-server 1.5.2 → 1.5.3

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # Obsidian MCP Server
2
2
 
3
3
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.8.3-blue.svg)](https://www.typescriptlang.org/)
4
- [![Model Context Protocol](https://img.shields.io/badge/MCP-1.8.0-green.svg)](https://modelcontextprotocol.io/)
5
- [![Version](https://img.shields.io/badge/Version-1.5.2-blue.svg)]()
4
+ [![Model Context Protocol](https://img.shields.io/badge/MCP-1.9.0-green.svg)](https://modelcontextprotocol.io/)
5
+ [![Version](https://img.shields.io/badge/Version-1.5.3-blue.svg)]()
6
6
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
7
  [![Status](https://img.shields.io/badge/Status-Stable-green.svg)]()
8
8
  [![GitHub](https://img.shields.io/github/stars/cyanheads/obsidian-mcp-server?style=social)](https://github.com/cyanheads/obsidian-mcp-server)
@@ -40,7 +40,7 @@ export const ObsidianPropertiesSchema = z.object({
40
40
  urls: z.array(z.string().url()).optional(),
41
41
  papers: z.array(z.string()).optional(),
42
42
  // Custom Fields
43
- custom: z.record(z.unknown()).optional()
43
+ custom: z.object({}).catchall(z.unknown()).optional() // Ensure 'custom' is an object if present
44
44
  });
45
45
  /**
46
46
  * Schema for validating property updates (excludes timestamps)
@@ -64,6 +64,6 @@ export const PropertyUpdateSchema = z.object({
64
64
  urls: z.array(z.string().url()).optional(),
65
65
  papers: z.array(z.string()).optional(),
66
66
  // Custom Fields
67
- custom: z.record(z.unknown()).optional()
67
+ custom: z.object({}).catchall(z.unknown()).optional() // Ensure 'custom' is an object if present
68
68
  });
69
69
  //# sourceMappingURL=types.js.map
package/debug.js CHANGED
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // Debug script to run the server with verbose logging
4
- import fs from 'fs';
5
- import path from 'path';
6
4
  import { spawn } from 'child_process';
5
+ import fs from 'fs';
6
+ import path, { dirname } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
- import { dirname } from 'path';
9
8
 
10
9
  // Get current directory from import.meta
11
10
  const __filename = fileURLToPath(import.meta.url);
@@ -30,7 +29,7 @@ const env = {
30
29
  DEBUG_JSONRPC: 'true',
31
30
  NODE_ENV: 'development',
32
31
  // Required environment variables from MCP settings
33
- OBSIDIAN_API_KEY: '9bde5e9fb07951daa75819a1704ce75fd597955205141d3b39e5e1423ddbc97c',
32
+ OBSIDIAN_API_KEY: 'your_actual_api_key_here', // Add your actual API key here for debugging
34
33
  VERIFY_SSL: 'false',
35
34
  OBSIDIAN_PROTOCOL: 'https',
36
35
  OBSIDIAN_HOST: '127.0.0.1',
package/docs/tree.md ADDED
@@ -0,0 +1,63 @@
1
+ # obsidian-mcp-server - Directory Structure
2
+
3
+ Generated on: 2025-04-13 20:39:14
4
+
5
+
6
+ ```
7
+ obsidian-mcp-server
8
+ ├── backups
9
+ ├── scripts
10
+ ├── clean.ts
11
+ └── tree.ts
12
+ ├── src
13
+ ├── mcp
14
+ │ ├── handlers.ts
15
+ │ ├── index.ts
16
+ │ ├── server.ts
17
+ │ └── types.ts
18
+ ├── obsidian
19
+ │ ├── client.ts
20
+ │ ├── errors.ts
21
+ │ ├── index.ts
22
+ │ └── types.ts
23
+ ├── resources
24
+ │ ├── index.ts
25
+ │ ├── tags.ts
26
+ │ └── types.ts
27
+ ├── tools
28
+ │ ├── files
29
+ │ │ ├── content.ts
30
+ │ │ ├── index.ts
31
+ │ │ └── list.ts
32
+ │ ├── properties
33
+ │ │ ├── index.ts
34
+ │ │ ├── manager.ts
35
+ │ │ ├── tools.ts
36
+ │ │ └── types.ts
37
+ │ ├── search
38
+ │ │ ├── complex.ts
39
+ │ │ ├── index.ts
40
+ │ │ └── simple.ts
41
+ │ ├── base.ts
42
+ │ └── index.ts
43
+ ├── utils
44
+ │ ├── errors.ts
45
+ │ ├── idGenerator.ts
46
+ │ ├── index.ts
47
+ │ ├── logging.ts
48
+ │ ├── rate-limiting.ts
49
+ │ ├── tokenization.ts
50
+ │ └── validation.ts
51
+ └── index.ts
52
+ ├── debug.js
53
+ ├── LICENSE
54
+ ├── mcp-client-config.example.json
55
+ ├── package-lock.json
56
+ ├── package.json
57
+ ├── README.md
58
+ ├── repomix.config.json
59
+ └── tsconfig.json
60
+
61
+ ```
62
+
63
+ _Note: This tree excludes files and directories matched by .gitignore and common patterns like node_modules._
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obsidian-mcp-server",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "Model Context Protocol (MCP) server designed for LLMs to interact with Obsidian vaults. Provides secure, token-aware tools for seamless knowledge base management through a standardized interface.",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -12,21 +12,19 @@
12
12
  "postbuild": "node -e \"if (process.platform !== 'win32') require('fs').chmodSync('build/index.js', '755')\"",
13
13
  "start": "node build/index.js",
14
14
  "dev": "tsc -w",
15
- "clean": "node -e \"require('fs').rmSync('build', { recursive: true, force: true })\"",
15
+ "clean": "npx ts-node scripts/clean.ts",
16
16
  "rebuild": "npm run clean && npm run build",
17
- "test": "echo \"No tests specified yet\" && exit 0",
18
- "lint": "eslint . --ext .ts",
19
- "format": "prettier --write \"src/**/*.ts\""
17
+ "tree": "npx ts-node --esm scripts/tree.ts"
20
18
  },
21
19
  "dependencies": {
22
- "@modelcontextprotocol/sdk": "^1.8.0",
23
- "@types/node": "^22.14.0",
24
- "@typescript-eslint/eslint-plugin": "^8.29.0",
25
- "@typescript-eslint/parser": "^8.29.0",
20
+ "@modelcontextprotocol/sdk": "^1.9.0",
21
+ "@types/node": "^22.14.1",
22
+ "@typescript-eslint/eslint-plugin": "^8.29.1",
23
+ "@typescript-eslint/parser": "^8.29.1",
26
24
  "axios": "^1.8.4",
27
- "dotenv": "^16.4.7",
25
+ "dotenv": "^16.5.0",
28
26
  "eslint": "^9.24.0",
29
- "eslint-config-prettier": "^10.1.1",
27
+ "eslint-config-prettier": "^10.1.2",
30
28
  "eslint-plugin-prettier": "^5.2.6",
31
29
  "nanoid": "^5.1.5",
32
30
  "p-limit": "^6.2.0",
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Clean Script
5
+ * ============
6
+ *
7
+ * Description:
8
+ * A utility script to clean build artifacts and temporary directories from your project.
9
+ * By default, it removes the 'dist' and 'logs' directories if they exist.
10
+ *
11
+ * Usage:
12
+ * - Add to package.json: "clean": "node dist/scripts/clean.js"
13
+ * - Can be run directly: npm run clean
14
+ * - Often used in rebuild scripts: "rebuild": "npm run clean && npm run build"
15
+ * - Can be used with arguments to specify custom directories: node dist/scripts/clean.js temp coverage
16
+ *
17
+ * Platform compatibility:
18
+ * - Works on all platforms (Windows, macOS, Linux) using Node.js path normalization
19
+ */
20
+
21
+ import { rm, access } from 'fs/promises';
22
+ import { join } from 'path';
23
+
24
+ /**
25
+ * Interface for clean operation result
26
+ */
27
+ interface CleanResult {
28
+ dir: string;
29
+ status: 'success' | 'skipped';
30
+ reason?: string;
31
+ }
32
+
33
+ /**
34
+ * Check if a directory exists without using fs.Stats
35
+ */
36
+ async function directoryExists(dirPath: string): Promise<boolean> {
37
+ try {
38
+ await access(dirPath);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Main clean function
47
+ */
48
+ const clean = async (): Promise<void> => {
49
+ try {
50
+ // Default directories to clean
51
+ let dirsToClean: string[] = ['dist', 'logs'];
52
+
53
+ // If directories are specified as command line arguments, use those instead
54
+ const args = process.argv.slice(2);
55
+ if (args.length > 0) {
56
+ dirsToClean = args;
57
+ }
58
+
59
+ console.log(`Cleaning directories: ${dirsToClean.join(', ')}`);
60
+
61
+ // Process each directory
62
+ const results = await Promise.allSettled(
63
+ dirsToClean.map(async (dir): Promise<CleanResult> => {
64
+ const dirPath = join(process.cwd(), dir);
65
+
66
+ try {
67
+ // Check if directory exists before attempting to remove it
68
+ const exists = await directoryExists(dirPath);
69
+
70
+ if (!exists) {
71
+ return { dir, status: 'skipped', reason: 'does not exist' };
72
+ }
73
+
74
+ // Remove directory if it exists
75
+ await rm(dirPath, { recursive: true, force: true });
76
+ return { dir, status: 'success' };
77
+ } catch (error) {
78
+ throw error;
79
+ }
80
+ })
81
+ );
82
+
83
+ // Report results
84
+ for (const result of results) {
85
+ if (result.status === 'fulfilled') {
86
+ const { dir, status, reason } = result.value;
87
+ if (status === 'success') {
88
+ console.log(`✓ Successfully cleaned ${dir} directory`);
89
+ } else {
90
+ console.log(`- ${dir} directory ${reason}, skipping cleanup`);
91
+ }
92
+ } else {
93
+ console.error(`× Error cleaning directory: ${result.reason}`);
94
+ }
95
+ }
96
+ } catch (error) {
97
+ console.error('× Error during cleanup:', error instanceof Error ? error.message : error);
98
+ process.exit(1);
99
+ }
100
+ };
101
+
102
+ // Execute the clean function
103
+ clean();
@@ -0,0 +1,599 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Directory Tree Generation Operation
5
+ * ==================================
6
+ *
7
+ * A utility for generating visual tree representations of the project's directory
8
+ * structure with configurable depth control and gitignore integration.
9
+ *
10
+ * This operation creates a formatted markdown file containing a hierarchical
11
+ * representation of directories and files, respecting ignore patterns and
12
+ * applying configurable filtering.
13
+ *
14
+ * Features:
15
+ * - Respects .gitignore patterns and common exclusions
16
+ * - Configurable maximum depth traversal
17
+ * - Customizable output location
18
+ * - Sorting with directories first
19
+ * - Cross-platform compatibility
20
+ *
21
+ * @module utilities/generate.directory.tree.operation
22
+ *
23
+ * Usage examples:
24
+ * - Add to package.json: "tree": "ts-node scripts/tree.ts"
25
+ * - Run directly: npm run tree
26
+ * - Custom output: ts-node scripts/tree.ts ./documentation/structure.md
27
+ * - Limit depth: ts-node scripts/tree.ts --depth=3
28
+ * - Show help: ts-node scripts/tree.ts --help
29
+ */
30
+
31
+ import fs from 'fs/promises';
32
+ import path from 'path';
33
+
34
+ // -----------------------------------
35
+ // Type Definitions
36
+ // -----------------------------------
37
+
38
+ /**
39
+ * Standardized error category classification (using type alias instead of enum)
40
+ */
41
+ type ErrorCategoryType =
42
+ | 'VALIDATION'
43
+ | 'FILESYSTEM'
44
+ | 'SYSTEM'
45
+ | 'UNKNOWN';
46
+
47
+ const ErrorCategory = {
48
+ VALIDATION: 'VALIDATION' as ErrorCategoryType,
49
+ FILESYSTEM: 'FILESYSTEM' as ErrorCategoryType,
50
+ SYSTEM: 'SYSTEM' as ErrorCategoryType,
51
+ UNKNOWN: 'UNKNOWN' as ErrorCategoryType,
52
+ };
53
+
54
+ /**
55
+ * Error severity classification (using type alias instead of enum)
56
+ */
57
+ type ErrorSeverityLevel = 0 | 1 | 2 | 3 | 4;
58
+
59
+ const ErrorSeverity = {
60
+ DEBUG: 0 as ErrorSeverityLevel,
61
+ INFO: 1 as ErrorSeverityLevel,
62
+ WARN: 2 as ErrorSeverityLevel,
63
+ ERROR: 3 as ErrorSeverityLevel,
64
+ FATAL: 4 as ErrorSeverityLevel,
65
+ };
66
+
67
+ /**
68
+ * Standardized error structure for consistent error handling
69
+ */
70
+ interface StandardizedApplicationErrorObject {
71
+ errorMessage: string; // Human-readable description
72
+ errorCode: string; // Machine-readable identifier
73
+ errorCategory: ErrorCategoryType; // System area affected (using type alias)
74
+ errorSeverity: ErrorSeverityLevel; // How critical the error is (using type alias)
75
+ errorTimestamp: string; // When the error occurred
76
+ errorContext: Record<string, unknown>; // Additional relevant data
77
+ errorStack?: string; // Stack trace if available
78
+ }
79
+
80
+ /**
81
+ * Successful result from an operation
82
+ */
83
+ interface OperationResultSuccess<DataType> {
84
+ resultSuccessful: true;
85
+ resultData: DataType;
86
+ }
87
+
88
+ /**
89
+ * Failed result from an operation
90
+ */
91
+ interface OperationResultFailure<ErrorType> {
92
+ resultSuccessful: false;
93
+ resultError: ErrorType;
94
+ }
95
+
96
+ /**
97
+ * Combined result type for operations
98
+ */
99
+ type OperationResult<DataType, ErrorType = StandardizedApplicationErrorObject> =
100
+ | OperationResultSuccess<DataType>
101
+ | OperationResultFailure<ErrorType>;
102
+
103
+ /**
104
+ * Configuration options for the tree generation operation
105
+ */
106
+ interface TreeGenerationConfiguration {
107
+ treeOutputFilePath: string;
108
+ maximumDirectoryDepth: number;
109
+ showHelpText: boolean;
110
+ }
111
+
112
+ /**
113
+ * Definition of a gitignore pattern with parsing metadata
114
+ */
115
+ interface GitignorePatternDefinition {
116
+ patternText: string;
117
+ isNegatedPattern: boolean;
118
+ regexPattern: string;
119
+ }
120
+
121
+ /**
122
+ * Result from the tree generation operation
123
+ */
124
+ interface TreeGenerationResult {
125
+ projectName: string;
126
+ treeOutputFilePath: string;
127
+ treeContentLength: number;
128
+ maximumDepthApplied: number;
129
+ generationTimestamp: string;
130
+ }
131
+
132
+ // -----------------------------------
133
+ // Constants
134
+ // -----------------------------------
135
+
136
+ /**
137
+ * Default patterns to always ignore regardless of gitignore contents
138
+ */
139
+ const DEFAULT_IGNORE_PATTERNS: string[] = [
140
+ '.git',
141
+ 'node_modules',
142
+ '.DS_Store',
143
+ 'dist',
144
+ 'build'
145
+ ];
146
+
147
+ /**
148
+ * Default output path for the generated tree
149
+ */
150
+ const DEFAULT_OUTPUT_PATH = 'docs/tree.md';
151
+
152
+ /**
153
+ * Help text displayed when requested
154
+ */
155
+ const HELP_TEXT = `
156
+ Directory Tree Generator - Project structure visualization tool
157
+
158
+ Usage:
159
+ node dist/utilities/generate.directory.tree.operation.js [output-path] [--depth=<number>] [--help]
160
+
161
+ Options:
162
+ output-path Custom file path for the tree output (default: docs/tree.md)
163
+ --depth=<number> Maximum directory depth to display (default: unlimited)
164
+ --help Show this help message
165
+ `;
166
+
167
+ // -----------------------------------
168
+ // Utility Functions
169
+ // -----------------------------------
170
+
171
+ /**
172
+ * Creates a standardized success result
173
+ *
174
+ * @param data - The data to include in the success result
175
+ * @returns A standardized success result object
176
+ */
177
+ function createSuccessResult<DataType>(data: DataType): OperationResultSuccess<DataType> {
178
+ return { resultSuccessful: true, resultData: data };
179
+ }
180
+
181
+ /**
182
+ * Creates a standardized failure result
183
+ *
184
+ * @param error - The error to include in the failure result
185
+ * @returns A standardized failure result object
186
+ */
187
+ function createFailureResult<ErrorType>(error: ErrorType): OperationResultFailure<ErrorType> {
188
+ return { resultSuccessful: false, resultError: error };
189
+ }
190
+
191
+ /**
192
+ * Creates a standardized error object
193
+ *
194
+ * @param message - Human-readable error message
195
+ * @param code - Machine-readable error code
196
+ * @param category - Error category classification
197
+ * @param severity - Error severity level
198
+ * @param context - Additional context data
199
+ * @returns A standardized error object
200
+ */
201
+ function createStandardizedError(
202
+ message: string,
203
+ code: string,
204
+ category: ErrorCategoryType, // Use the type alias
205
+ severity: ErrorSeverityLevel, // Use the type alias
206
+ context: Record<string, unknown> = {}
207
+ ): StandardizedApplicationErrorObject {
208
+ return {
209
+ errorMessage: message,
210
+ errorCode: code,
211
+ errorCategory: category,
212
+ errorSeverity: severity,
213
+ errorTimestamp: new Date().toISOString(),
214
+ errorContext: context
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Converts an exception to a standardized error object
220
+ *
221
+ * @param exception - The caught exception
222
+ * @param defaultMessage - Fallback message if exception is not an Error object
223
+ * @returns A standardized error object
224
+ */
225
+ function wrapExceptionAsStandardizedError(
226
+ exception: unknown,
227
+ defaultMessage: string
228
+ ): StandardizedApplicationErrorObject {
229
+ const errorMessage = exception instanceof Error ? exception.message : defaultMessage;
230
+ const errorStack = exception instanceof Error ? exception.stack : undefined;
231
+
232
+ return {
233
+ errorMessage,
234
+ errorCode: 'UNEXPECTED_ERROR',
235
+ errorCategory: ErrorCategory.UNKNOWN, // Use the constant object
236
+ errorSeverity: ErrorSeverity.ERROR, // Use the constant object
237
+ errorTimestamp: new Date().toISOString(),
238
+ errorContext: { originalException: exception },
239
+ errorStack
240
+ };
241
+ }
242
+
243
+ // -----------------------------------
244
+ // Implementation Functions
245
+ // -----------------------------------
246
+
247
+ /**
248
+ * Parses command line arguments to extract configuration options
249
+ *
250
+ * @param commandLineArguments - Array of arguments from process.argv
251
+ * @returns Configuration object for tree generation
252
+ */
253
+ function parseCommandLineArguments(
254
+ commandLineArguments: string[]
255
+ ): TreeGenerationConfiguration {
256
+ let treeOutputFilePath = DEFAULT_OUTPUT_PATH;
257
+ let maximumDirectoryDepth = Infinity;
258
+ let showHelpText = false;
259
+
260
+ for (const argumentValue of commandLineArguments) {
261
+ if (argumentValue === '--help') {
262
+ showHelpText = true;
263
+ } else if (argumentValue.startsWith('--depth=')) {
264
+ const depthValue = argumentValue.split('=')[1];
265
+ const parsedDepth = parseInt(depthValue, 10);
266
+
267
+ if (isNaN(parsedDepth) || parsedDepth < 1) {
268
+ console.error('Invalid depth value. Using unlimited depth.');
269
+ maximumDirectoryDepth = Infinity;
270
+ } else {
271
+ maximumDirectoryDepth = parsedDepth;
272
+ }
273
+ } else if (!argumentValue.startsWith('--')) {
274
+ // If it's not an option flag, assume it's the output path
275
+ treeOutputFilePath = argumentValue;
276
+ }
277
+ }
278
+
279
+ return {
280
+ treeOutputFilePath,
281
+ maximumDirectoryDepth,
282
+ showHelpText
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Loads and parses patterns from the .gitignore file
288
+ *
289
+ * @returns Promise resolving to an array of parsed gitignore patterns
290
+ */
291
+ async function loadGitignorePatternDefinitions(): Promise<OperationResult<GitignorePatternDefinition[]>> {
292
+ try {
293
+ const gitignoreContent = await fs.readFile('.gitignore', 'utf-8');
294
+
295
+ const patternDefinitions = gitignoreContent
296
+ .split('\n')
297
+ .map(line => line.trim())
298
+ // Remove comments, empty lines, and lines with just whitespace
299
+ .filter(line => line && !line.startsWith('#') && line.trim() !== '')
300
+ // Process each pattern
301
+ .map(pattern => ({
302
+ patternText: pattern.startsWith('!') ? pattern.slice(1) : pattern,
303
+ isNegatedPattern: pattern.startsWith('!'),
304
+ // Convert glob patterns to regex-compatible strings (simplified approach)
305
+ regexPattern: pattern
306
+ .replace(/\./g, '\\.') // Escape dots first
307
+ .replace(/\*/g, '.*') // Convert * to .*
308
+ .replace(/\?/g, '.') // Convert ? to .
309
+ .replace(/\/$/, '(/.*)?') // Handle directory indicators
310
+ }));
311
+
312
+ return createSuccessResult(patternDefinitions);
313
+ } catch (exceptionObject) {
314
+ console.warn('No .gitignore file found, using default patterns only');
315
+ return createSuccessResult([]);
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Checks if a given file path should be ignored based on patterns
321
+ *
322
+ * @param entryPath - The relative path to check
323
+ * @param ignorePatternDefinitions - Array of parsed gitignore patterns
324
+ * @returns Boolean indicating if the path should be ignored
325
+ */
326
+ function checkPathShouldBeIgnored(
327
+ entryPath: string,
328
+ ignorePatternDefinitions: GitignorePatternDefinition[]
329
+ ): boolean {
330
+ // Always check default patterns first
331
+ if (DEFAULT_IGNORE_PATTERNS.some(pattern => entryPath.includes(pattern))) {
332
+ return true;
333
+ }
334
+
335
+ let shouldBeIgnored = false;
336
+
337
+ for (const { patternText, isNegatedPattern, regexPattern } of ignorePatternDefinitions) {
338
+ // Convert the pattern to a proper regex
339
+ const compiledRegexPattern = new RegExp(`^${regexPattern}$|/${regexPattern}$|/${regexPattern}/`);
340
+
341
+ if (compiledRegexPattern.test(entryPath)) {
342
+ // If it's a negation pattern (!pattern), this file should NOT be ignored
343
+ // Otherwise, it should be ignored
344
+ shouldBeIgnored = !isNegatedPattern;
345
+ }
346
+ }
347
+
348
+ return shouldBeIgnored;
349
+ }
350
+
351
+ /**
352
+ * Recursively generates a tree representation of the directory structure
353
+ *
354
+ * @param directoryPath - Path to the directory to process
355
+ * @param ignorePatternDefinitions - Array of gitignore pattern definitions
356
+ * @param prefixString - Prefix string for the current level (used for indentation)
357
+ * @param isLastEntry - Whether this is the last entry at the current level
358
+ * @param relativePathString - Relative path from the root directory
359
+ * @param currentDepthLevel - Current depth level in the traversal
360
+ * @returns Promise resolving to the string representation of the tree
361
+ */
362
+ async function generateDirectoryTreeRepresentation(
363
+ directoryPath: string,
364
+ ignorePatternDefinitions: GitignorePatternDefinition[],
365
+ prefixString = '',
366
+ isLastEntry = true,
367
+ relativePathString = '',
368
+ currentDepthLevel = 0,
369
+ maximumDepthLevel = Infinity
370
+ ): Promise<OperationResult<string>> {
371
+ try {
372
+ const directoryEntries = await fs.readdir(directoryPath, { withFileTypes: true });
373
+ let treeOutputContent = '';
374
+
375
+ // Filter and sort entries
376
+ const filteredEntries = directoryEntries
377
+ .filter(entry => {
378
+ const entryPath = path.join(relativePathString, entry.name);
379
+ return !checkPathShouldBeIgnored(entryPath, ignorePatternDefinitions);
380
+ })
381
+ .sort((a, b) => {
382
+ // Directories first, then files
383
+ if (a.isDirectory() && !b.isDirectory()) return -1;
384
+ if (!a.isDirectory() && b.isDirectory()) return 1;
385
+ return a.name.localeCompare(b.name);
386
+ });
387
+
388
+ for (let entryIndex = 0; entryIndex < filteredEntries.length; entryIndex++) {
389
+ const entryItem = filteredEntries[entryIndex];
390
+ const isLastItem = entryIndex === filteredEntries.length - 1;
391
+ const newPrefixString = prefixString + (isLastEntry ? ' ' : '│ ');
392
+ const newRelativePath = path.join(relativePathString, entryItem.name);
393
+
394
+ treeOutputContent += prefixString + (isLastItem ? '└── ' : '├── ') + entryItem.name + '\n';
395
+
396
+ // Only traverse deeper if we haven't reached maximumDepthLevel
397
+ if (entryItem.isDirectory() && currentDepthLevel < maximumDepthLevel) {
398
+ const subTreeResult = await generateDirectoryTreeRepresentation(
399
+ path.join(directoryPath, entryItem.name),
400
+ ignorePatternDefinitions,
401
+ newPrefixString,
402
+ isLastItem,
403
+ newRelativePath,
404
+ currentDepthLevel + 1,
405
+ maximumDepthLevel
406
+ );
407
+
408
+ if (subTreeResult.resultSuccessful) {
409
+ treeOutputContent += subTreeResult.resultData;
410
+ } else {
411
+ return subTreeResult; // Propagate error
412
+ }
413
+ }
414
+ }
415
+
416
+ return createSuccessResult(treeOutputContent);
417
+ } catch (exceptionObject) {
418
+ return createFailureResult(
419
+ wrapExceptionAsStandardizedError(
420
+ exceptionObject,
421
+ `Failed to generate tree for directory: ${directoryPath}`
422
+ )
423
+ );
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Ensures the directory for the output file exists, creating it if needed
429
+ *
430
+ * @param directoryPath - Path to the directory to check/create
431
+ * @returns Promise resolving to operation result
432
+ */
433
+ async function ensureDirectoryExists(
434
+ directoryPath: string
435
+ ): Promise<OperationResult<boolean>> {
436
+ try {
437
+ await fs.access(directoryPath);
438
+ return createSuccessResult(true);
439
+ } catch {
440
+ try {
441
+ await fs.mkdir(directoryPath, { recursive: true });
442
+ console.log(`Creating directory: ${directoryPath}`);
443
+ return createSuccessResult(true);
444
+ } catch (exceptionObject) {
445
+ return createFailureResult(
446
+ wrapExceptionAsStandardizedError(
447
+ exceptionObject,
448
+ `Failed to create directory: ${directoryPath}`
449
+ )
450
+ );
451
+ }
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Writes the generated tree content to a markdown file
457
+ *
458
+ * @param projectName - Name of the project
459
+ * @param treeContent - Generated tree content
460
+ * @param outputFilePath - Path where the output file should be written
461
+ * @param maximumDepthValue - Maximum depth value that was applied
462
+ * @returns Promise resolving to operation result
463
+ */
464
+ async function writeTreeContentToFile(
465
+ projectName: string,
466
+ treeContent: string,
467
+ outputFilePath: string,
468
+ maximumDepthValue: number
469
+ ): Promise<OperationResult<TreeGenerationResult>> {
470
+ try {
471
+ const rootDirectoryPath = process.cwd();
472
+ const outputDirectoryPath = path.dirname(path.resolve(rootDirectoryPath, outputFilePath));
473
+
474
+ // Ensure output directory exists
475
+ const directoryResult = await ensureDirectoryExists(outputDirectoryPath);
476
+ if (!directoryResult.resultSuccessful) {
477
+ return directoryResult;
478
+ }
479
+
480
+ // Format the timestamp
481
+ const timestamp = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
482
+
483
+ // Format the markdown content
484
+ const markdownContent = `# ${projectName} - Directory Structure
485
+
486
+ Generated on: ${timestamp}
487
+
488
+ ${maximumDepthValue !== Infinity ? `_Depth limited to ${maximumDepthValue} levels_\n\n` : ''}
489
+ \`\`\`
490
+ ${projectName}
491
+ ${treeContent}
492
+ \`\`\`
493
+
494
+ _Note: This tree excludes files and directories matched by .gitignore and common patterns like node_modules._
495
+ `;
496
+
497
+ // Write the content to the file
498
+ await fs.writeFile(
499
+ path.resolve(rootDirectoryPath, outputFilePath),
500
+ markdownContent
501
+ );
502
+
503
+ return createSuccessResult({
504
+ projectName,
505
+ treeOutputFilePath: outputFilePath,
506
+ treeContentLength: treeContent.length,
507
+ maximumDepthApplied: maximumDepthValue,
508
+ generationTimestamp: timestamp
509
+ });
510
+ } catch (exceptionObject) {
511
+ return createFailureResult(
512
+ wrapExceptionAsStandardizedError(
513
+ exceptionObject,
514
+ `Failed to write tree to file: ${outputFilePath}`
515
+ )
516
+ );
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Main operation function that orchestrates the tree generation process
522
+ *
523
+ * @returns Promise that resolves when the operation completes
524
+ */
525
+ async function generateProjectDirectoryTree(): Promise<void> {
526
+ try {
527
+ // Parse command line arguments
528
+ const commandLineArguments = process.argv.slice(2);
529
+ const configurationSettings = parseCommandLineArguments(commandLineArguments);
530
+
531
+ // Display help if requested
532
+ if (configurationSettings.showHelpText) {
533
+ console.log(HELP_TEXT);
534
+ process.exit(0);
535
+ }
536
+
537
+ const rootDirectoryPath = process.cwd();
538
+ const projectName = path.basename(rootDirectoryPath);
539
+
540
+ // Load gitignore patterns
541
+ const ignorePatternResult = await loadGitignorePatternDefinitions();
542
+ if (!ignorePatternResult.resultSuccessful) {
543
+ throw new Error(`Failed to load gitignore patterns: ${ignorePatternResult.resultError.errorMessage}`);
544
+ }
545
+
546
+ const ignorePatternDefinitions = ignorePatternResult.resultData;
547
+
548
+ console.log(`Generating directory tree for: ${projectName}`);
549
+ console.log(`Output path: ${configurationSettings.treeOutputFilePath}`);
550
+
551
+ if (configurationSettings.maximumDirectoryDepth !== Infinity) {
552
+ console.log(`Maximum depth: ${configurationSettings.maximumDirectoryDepth}`);
553
+ }
554
+
555
+ // Generate the tree structure
556
+ const treeGenerationResult = await generateDirectoryTreeRepresentation(
557
+ rootDirectoryPath,
558
+ ignorePatternDefinitions,
559
+ '',
560
+ true,
561
+ '',
562
+ 0,
563
+ configurationSettings.maximumDirectoryDepth
564
+ );
565
+
566
+ if (!treeGenerationResult.resultSuccessful) {
567
+ throw new Error(`Failed to generate tree: ${treeGenerationResult.resultError.errorMessage}`);
568
+ }
569
+
570
+ // Write the tree to a file
571
+ const writeResult = await writeTreeContentToFile(
572
+ projectName,
573
+ treeGenerationResult.resultData,
574
+ configurationSettings.treeOutputFilePath,
575
+ configurationSettings.maximumDirectoryDepth
576
+ );
577
+
578
+ if (!writeResult.resultSuccessful) {
579
+ throw new Error(`Failed to write tree: ${writeResult.resultError.errorMessage}`);
580
+ }
581
+
582
+ console.log(`✓ Successfully generated tree structure in ${configurationSettings.treeOutputFilePath}`);
583
+ } catch (exceptionObject) {
584
+ const standardizedError = wrapExceptionAsStandardizedError(
585
+ exceptionObject,
586
+ 'Unhandled error during tree generation'
587
+ );
588
+
589
+ console.error(`× Error generating tree: ${standardizedError.errorMessage}`);
590
+ process.exit(1);
591
+ }
592
+ }
593
+
594
+ // -----------------------------------
595
+ // Script Execution
596
+ // -----------------------------------
597
+
598
+ // Execute the main operation function
599
+ generateProjectDirectoryTree();
@@ -48,7 +48,7 @@ export const ObsidianPropertiesSchema = z.object({
48
48
  papers: z.array(z.string()).optional(),
49
49
 
50
50
  // Custom Fields
51
- custom: z.record(z.unknown()).optional()
51
+ custom: z.object({}).catchall(z.unknown()).optional() // Ensure 'custom' is an object if present
52
52
  });
53
53
 
54
54
  /**
@@ -78,7 +78,7 @@ export const PropertyUpdateSchema = z.object({
78
78
  papers: z.array(z.string()).optional(),
79
79
 
80
80
  // Custom Fields
81
- custom: z.record(z.unknown()).optional()
81
+ custom: z.object({}).catchall(z.unknown()).optional() // Ensure 'custom' is an object if present
82
82
  });
83
83
 
84
84
  /**
@@ -113,4 +113,4 @@ export interface PropertyManagerResult {
113
113
  message: string;
114
114
  properties?: ObsidianProperties;
115
115
  errors?: string[];
116
- }
116
+ }