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,5 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property manager for Obsidian notes
|
|
3
|
+
*/
|
|
1
4
|
import { parse, stringify } from 'yaml';
|
|
2
|
-
import {
|
|
5
|
+
import { EOL } from 'os';
|
|
6
|
+
import { createLogger } from '../../utils/logging.js';
|
|
7
|
+
import { ObsidianPropertiesSchema, PropertyUpdateSchema } from './types.js';
|
|
8
|
+
// Create a logger for property operations
|
|
9
|
+
const logger = createLogger('PropertyManager');
|
|
10
|
+
/**
|
|
11
|
+
* Manages YAML frontmatter properties in Obsidian notes
|
|
12
|
+
*/
|
|
3
13
|
export class PropertyManager {
|
|
4
14
|
client;
|
|
5
15
|
constructor(client) {
|
|
@@ -7,48 +17,59 @@ export class PropertyManager {
|
|
|
7
17
|
}
|
|
8
18
|
/**
|
|
9
19
|
* Parse YAML frontmatter from note content
|
|
20
|
+
* @param content The note content
|
|
21
|
+
* @returns Extracted properties
|
|
10
22
|
*/
|
|
11
23
|
parseProperties(content) {
|
|
12
24
|
try {
|
|
13
|
-
// Extract frontmatter between --- markers
|
|
14
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
25
|
+
// Extract frontmatter between --- markers (handles both \n and \r\n)
|
|
26
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
15
27
|
if (!match) {
|
|
28
|
+
logger.debug('No frontmatter found in content');
|
|
16
29
|
return {};
|
|
17
30
|
}
|
|
18
31
|
const frontmatter = match[1];
|
|
19
32
|
const properties = parse(frontmatter);
|
|
33
|
+
// Handle tags - don't add # prefix in frontmatter
|
|
34
|
+
if (properties.tags && Array.isArray(properties.tags)) {
|
|
35
|
+
properties.tags = properties.tags.map((tag) => tag.startsWith('#') ? tag.substring(1) : tag);
|
|
36
|
+
}
|
|
20
37
|
// Validate against schema
|
|
21
38
|
const result = ObsidianPropertiesSchema.safeParse(properties);
|
|
22
39
|
if (!result.success) {
|
|
23
|
-
|
|
24
|
-
// Return
|
|
40
|
+
logger.warn('Property validation warnings:', result.error);
|
|
41
|
+
// Return the properties with fixed tags
|
|
25
42
|
return properties;
|
|
26
43
|
}
|
|
27
44
|
return result.data;
|
|
28
45
|
}
|
|
29
46
|
catch (error) {
|
|
30
|
-
|
|
47
|
+
logger.error('Error parsing properties:', error);
|
|
31
48
|
return {};
|
|
32
49
|
}
|
|
33
50
|
}
|
|
34
51
|
/**
|
|
35
52
|
* Generate YAML frontmatter from properties
|
|
53
|
+
* @param properties The properties to convert to YAML
|
|
54
|
+
* @returns YAML frontmatter string
|
|
36
55
|
*/
|
|
37
56
|
generateProperties(properties) {
|
|
38
57
|
try {
|
|
39
58
|
// Remove undefined values
|
|
40
59
|
const cleanProperties = Object.fromEntries(Object.entries(properties).filter(([_, v]) => v !== undefined));
|
|
41
|
-
// Generate YAML
|
|
60
|
+
// Generate YAML with platform-specific line endings
|
|
42
61
|
const yaml = stringify(cleanProperties);
|
|
43
|
-
return
|
|
62
|
+
return `---${EOL}${yaml}---${EOL}`;
|
|
44
63
|
}
|
|
45
64
|
catch (error) {
|
|
46
|
-
|
|
65
|
+
logger.error('Error generating properties:', error);
|
|
47
66
|
throw error;
|
|
48
67
|
}
|
|
49
68
|
}
|
|
50
69
|
/**
|
|
51
|
-
* Validate property values
|
|
70
|
+
* Validate property values against schema
|
|
71
|
+
* @param properties The properties to validate
|
|
72
|
+
* @returns Validation result
|
|
52
73
|
*/
|
|
53
74
|
validateProperties(properties) {
|
|
54
75
|
const result = PropertyUpdateSchema.safeParse(properties);
|
|
@@ -62,6 +83,10 @@ export class PropertyManager {
|
|
|
62
83
|
}
|
|
63
84
|
/**
|
|
64
85
|
* Merge new properties with existing ones
|
|
86
|
+
* @param existing The existing properties
|
|
87
|
+
* @param updates The new properties to merge
|
|
88
|
+
* @param replace Whether to replace arrays instead of merging them
|
|
89
|
+
* @returns The merged properties
|
|
65
90
|
*/
|
|
66
91
|
mergeProperties(existing, updates, replace = false) {
|
|
67
92
|
const merged = { ...existing };
|
|
@@ -94,9 +119,12 @@ export class PropertyManager {
|
|
|
94
119
|
}
|
|
95
120
|
/**
|
|
96
121
|
* Get properties from a note
|
|
122
|
+
* @param filepath Path to the note
|
|
123
|
+
* @returns The properties from the note
|
|
97
124
|
*/
|
|
98
125
|
async getProperties(filepath) {
|
|
99
126
|
try {
|
|
127
|
+
logger.debug(`Getting properties from file: ${filepath}`);
|
|
100
128
|
const content = await this.client.getFileContents(filepath);
|
|
101
129
|
const properties = this.parseProperties(content);
|
|
102
130
|
return {
|
|
@@ -106,6 +134,7 @@ export class PropertyManager {
|
|
|
106
134
|
};
|
|
107
135
|
}
|
|
108
136
|
catch (error) {
|
|
137
|
+
logger.error(`Failed to get properties from ${filepath}:`, error);
|
|
109
138
|
return {
|
|
110
139
|
success: false,
|
|
111
140
|
message: `Failed to get properties: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -115,12 +144,17 @@ export class PropertyManager {
|
|
|
115
144
|
}
|
|
116
145
|
/**
|
|
117
146
|
* Update properties of a note
|
|
147
|
+
* @param filepath Path to the note
|
|
148
|
+
* @param newProperties The new properties to apply
|
|
149
|
+
* @param replace Whether to replace arrays instead of merging them
|
|
150
|
+
* @returns The result of the update operation
|
|
118
151
|
*/
|
|
119
152
|
async updateProperties(filepath, newProperties, replace = false) {
|
|
120
153
|
try {
|
|
121
154
|
// Validate new properties
|
|
122
155
|
const validation = this.validateProperties(newProperties);
|
|
123
156
|
if (!validation.valid) {
|
|
157
|
+
logger.warn(`Invalid properties for ${filepath}:`, validation.errors);
|
|
124
158
|
return {
|
|
125
159
|
success: false,
|
|
126
160
|
message: 'Invalid properties',
|
|
@@ -128,17 +162,19 @@ export class PropertyManager {
|
|
|
128
162
|
};
|
|
129
163
|
}
|
|
130
164
|
// Get existing content and properties
|
|
165
|
+
logger.debug(`Updating properties for file: ${filepath}`);
|
|
131
166
|
const content = await this.client.getFileContents(filepath);
|
|
132
167
|
const existingProperties = this.parseProperties(content);
|
|
133
168
|
// Merge properties
|
|
134
169
|
const mergedProperties = this.mergeProperties(existingProperties, newProperties, replace);
|
|
135
170
|
// Generate new frontmatter
|
|
136
171
|
const newFrontmatter = this.generateProperties(mergedProperties);
|
|
137
|
-
// Replace existing frontmatter or prepend to file
|
|
138
|
-
const newContent = content.replace(/^---[\s\S]*?---\n/, '') || '';
|
|
172
|
+
// Replace existing frontmatter or prepend to file (handles both \n and \r\n)
|
|
173
|
+
const newContent = content.replace(/^---[\s\S]*?---\r?\n/, '') || '';
|
|
139
174
|
const updatedContent = newFrontmatter + newContent;
|
|
140
175
|
// Update file
|
|
141
176
|
await this.client.updateContent(filepath, updatedContent);
|
|
177
|
+
logger.debug(`Successfully updated properties for ${filepath}`);
|
|
142
178
|
return {
|
|
143
179
|
success: true,
|
|
144
180
|
message: 'Properties updated successfully',
|
|
@@ -146,6 +182,7 @@ export class PropertyManager {
|
|
|
146
182
|
};
|
|
147
183
|
}
|
|
148
184
|
catch (error) {
|
|
185
|
+
logger.error(`Failed to update properties for ${filepath}:`, error);
|
|
149
186
|
return {
|
|
150
187
|
success: false,
|
|
151
188
|
message: `Failed to update properties: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -154,4 +191,4 @@ export class PropertyManager {
|
|
|
154
191
|
}
|
|
155
192
|
}
|
|
156
193
|
}
|
|
157
|
-
//# sourceMappingURL=
|
|
194
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
import { BaseToolHandler } from "
|
|
2
|
-
import { PropertyManager } from "./
|
|
3
|
-
|
|
1
|
+
import { BaseToolHandler } from "../base.js";
|
|
2
|
+
import { PropertyManager } from "./manager.js";
|
|
3
|
+
import { createLogger } from "../../utils/logging.js";
|
|
4
|
+
// Create a logger for property tools
|
|
5
|
+
const logger = createLogger('PropertyTools');
|
|
6
|
+
/**
|
|
7
|
+
* Tool names for property operations
|
|
8
|
+
*/
|
|
9
|
+
export const PROPERTY_TOOL_NAMES = {
|
|
4
10
|
GET_PROPERTIES: "obsidian_get_properties",
|
|
5
11
|
UPDATE_PROPERTIES: "obsidian_update_properties"
|
|
6
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Tool handler for getting properties from a note
|
|
15
|
+
*/
|
|
7
16
|
export class GetPropertiesToolHandler extends BaseToolHandler {
|
|
8
17
|
propertyManager;
|
|
9
18
|
constructor(client) {
|
|
10
|
-
super(
|
|
19
|
+
super(PROPERTY_TOOL_NAMES.GET_PROPERTIES, client);
|
|
11
20
|
this.propertyManager = new PropertyManager(client);
|
|
12
21
|
}
|
|
13
22
|
getToolDescription() {
|
|
@@ -43,18 +52,23 @@ export class GetPropertiesToolHandler extends BaseToolHandler {
|
|
|
43
52
|
}
|
|
44
53
|
async runTool(args) {
|
|
45
54
|
try {
|
|
55
|
+
logger.debug(`Getting properties from: ${args.filepath}`);
|
|
46
56
|
const result = await this.propertyManager.getProperties(args.filepath);
|
|
47
57
|
return this.createResponse(result);
|
|
48
58
|
}
|
|
49
59
|
catch (error) {
|
|
60
|
+
logger.error(`Error getting properties from ${args.filepath}:`, error);
|
|
50
61
|
return this.handleError(error);
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Tool handler for updating properties in a note
|
|
67
|
+
*/
|
|
54
68
|
export class UpdatePropertiesToolHandler extends BaseToolHandler {
|
|
55
69
|
propertyManager;
|
|
56
70
|
constructor(client) {
|
|
57
|
-
super(
|
|
71
|
+
super(PROPERTY_TOOL_NAMES.UPDATE_PROPERTIES, client);
|
|
58
72
|
this.propertyManager = new PropertyManager(client);
|
|
59
73
|
}
|
|
60
74
|
getToolDescription() {
|
|
@@ -164,12 +178,14 @@ export class UpdatePropertiesToolHandler extends BaseToolHandler {
|
|
|
164
178
|
}
|
|
165
179
|
async runTool(args) {
|
|
166
180
|
try {
|
|
181
|
+
logger.debug(`Updating properties for: ${args.filepath}`);
|
|
167
182
|
const result = await this.propertyManager.updateProperties(args.filepath, args.properties, args.replace);
|
|
168
183
|
return this.createResponse(result);
|
|
169
184
|
}
|
|
170
185
|
catch (error) {
|
|
186
|
+
logger.error(`Error updating properties for ${args.filepath}:`, error);
|
|
171
187
|
return this.handleError(error);
|
|
172
188
|
}
|
|
173
189
|
}
|
|
174
190
|
}
|
|
175
|
-
//# sourceMappingURL=
|
|
191
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property types and schemas for Obsidian notes
|
|
3
|
+
*/
|
|
1
4
|
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Define validation schemas
|
|
7
|
+
* Allow any string for type to be more flexible
|
|
8
|
+
*/
|
|
4
9
|
export const PropertyType = z.string();
|
|
10
|
+
/**
|
|
11
|
+
* Valid status values for notes
|
|
12
|
+
*/
|
|
5
13
|
export const StatusEnum = z.enum([
|
|
6
14
|
"draft",
|
|
7
15
|
"in-progress",
|
|
8
16
|
"review",
|
|
9
17
|
"complete"
|
|
10
18
|
]);
|
|
11
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Schema for reading properties (includes timestamps)
|
|
21
|
+
*/
|
|
12
22
|
export const ObsidianPropertiesSchema = z.object({
|
|
13
23
|
// Basic Metadata
|
|
14
24
|
// Note: Timestamps are managed automatically
|
|
@@ -18,7 +28,7 @@ export const ObsidianPropertiesSchema = z.object({
|
|
|
18
28
|
// Classification
|
|
19
29
|
type: z.array(PropertyType).optional(),
|
|
20
30
|
// Organization
|
|
21
|
-
tags: z.array(z.string()
|
|
31
|
+
tags: z.array(z.string()).optional(),
|
|
22
32
|
// Technical Metadata
|
|
23
33
|
status: z.array(StatusEnum).optional(),
|
|
24
34
|
version: z.string().optional(),
|
|
@@ -32,7 +42,9 @@ export const ObsidianPropertiesSchema = z.object({
|
|
|
32
42
|
// Custom Fields
|
|
33
43
|
custom: z.record(z.unknown()).optional()
|
|
34
44
|
});
|
|
35
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Schema for validating property updates (excludes timestamps)
|
|
47
|
+
*/
|
|
36
48
|
export const PropertyUpdateSchema = z.object({
|
|
37
49
|
// Basic Metadata
|
|
38
50
|
title: z.string().optional(),
|
|
@@ -40,7 +52,7 @@ export const PropertyUpdateSchema = z.object({
|
|
|
40
52
|
// Classification
|
|
41
53
|
type: z.array(PropertyType).optional(),
|
|
42
54
|
// Organization
|
|
43
|
-
tags: z.array(z.string()
|
|
55
|
+
tags: z.array(z.string()).optional(),
|
|
44
56
|
// Technical Metadata
|
|
45
57
|
status: z.array(StatusEnum).optional(),
|
|
46
58
|
version: z.string().optional(),
|
|
@@ -54,4 +66,4 @@ export const PropertyUpdateSchema = z.object({
|
|
|
54
66
|
// Custom Fields
|
|
55
67
|
custom: z.record(z.unknown()).optional()
|
|
56
68
|
});
|
|
57
|
-
//# sourceMappingURL=
|
|
69
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { PropertyManager } from "../properties/manager.js";
|
|
2
|
+
import { BaseToolHandler } from "../base.js";
|
|
3
|
+
import { createLogger } from "../../utils/logging.js";
|
|
4
|
+
// Create a logger for complex search operations
|
|
5
|
+
const logger = createLogger('ComplexSearchTools');
|
|
6
|
+
/**
|
|
7
|
+
* Tool name for complex search
|
|
8
|
+
*/
|
|
9
|
+
export const COMPLEX_SEARCH_TOOL_NAME = "obsidian_complex_search";
|
|
10
|
+
/**
|
|
11
|
+
* Tool handler for complex JsonLogic searches
|
|
12
|
+
*/
|
|
13
|
+
export class ComplexSearchToolHandler extends BaseToolHandler {
|
|
14
|
+
constructor(client) {
|
|
15
|
+
super(COMPLEX_SEARCH_TOOL_NAME, client);
|
|
16
|
+
}
|
|
17
|
+
getToolDescription() {
|
|
18
|
+
return {
|
|
19
|
+
name: this.name,
|
|
20
|
+
description: "File path pattern matching using JsonLogic queries. Supported operations:\n- glob: Pattern matching for paths (e.g., \"*.md\")\n- Variable access: {\"var\": \"path\"}\n\nNote: For full-text content search, date-based searches, or other advanced queries, use obsidian_find_in_file instead.",
|
|
21
|
+
examples: [
|
|
22
|
+
{
|
|
23
|
+
description: "Find markdown files in Projects folder",
|
|
24
|
+
args: {
|
|
25
|
+
query: {
|
|
26
|
+
"glob": ["Projects/*.md", { "var": "path" }]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
description: "Find files in a specific subfolder",
|
|
32
|
+
args: {
|
|
33
|
+
query: {
|
|
34
|
+
"glob": ["**/Test/*.md", { "var": "path" }]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
query: {
|
|
43
|
+
type: "object",
|
|
44
|
+
description: "JsonLogic query object. Example: {\"glob\": [\"*.md\", {\"var\": \"path\"}]} matches all markdown files"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
required: ["query"]
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async runTool(args) {
|
|
52
|
+
try {
|
|
53
|
+
logger.debug(`Executing complex search with query: ${JSON.stringify(args.query)}`);
|
|
54
|
+
// Perform search
|
|
55
|
+
const results = await this.client.searchJson(args.query);
|
|
56
|
+
// Format response based on result type
|
|
57
|
+
const formattedResults = results.map(result => {
|
|
58
|
+
if ('matches' in result) {
|
|
59
|
+
// SimpleSearchResult
|
|
60
|
+
return {
|
|
61
|
+
filename: result.filename,
|
|
62
|
+
matches: result.matches,
|
|
63
|
+
score: result.score
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// SearchResult
|
|
68
|
+
return {
|
|
69
|
+
filename: result.filename,
|
|
70
|
+
result: result.result
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
logger.debug(`Complex search found ${results.length} results`);
|
|
75
|
+
return this.createResponse({
|
|
76
|
+
message: `Found ${results.length} result(s)`,
|
|
77
|
+
results: formattedResults
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
logger.error(`Complex search error: ${error}`);
|
|
82
|
+
return this.handleError(error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Tool handler for getting all tags used in the vault
|
|
88
|
+
*/
|
|
89
|
+
export class GetTagsToolHandler extends BaseToolHandler {
|
|
90
|
+
propertyManager;
|
|
91
|
+
constructor(client) {
|
|
92
|
+
super("obsidian_get_tags", client);
|
|
93
|
+
this.propertyManager = new PropertyManager(client);
|
|
94
|
+
}
|
|
95
|
+
getToolDescription() {
|
|
96
|
+
return {
|
|
97
|
+
name: this.name,
|
|
98
|
+
description: "Get all tags used across the Obsidian vault with their usage counts. Optionally filter tags within a specific folder.",
|
|
99
|
+
examples: [
|
|
100
|
+
{
|
|
101
|
+
description: "Get all tags in vault",
|
|
102
|
+
args: {}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
description: "Get tags in Projects folder",
|
|
106
|
+
args: {
|
|
107
|
+
path: "Projects"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
description: "Example response",
|
|
112
|
+
args: {},
|
|
113
|
+
response: {
|
|
114
|
+
"tags": [
|
|
115
|
+
{
|
|
116
|
+
"name": "project",
|
|
117
|
+
"count": 15,
|
|
118
|
+
"files": [
|
|
119
|
+
"Projects/ProjectA.md",
|
|
120
|
+
"Projects/ProjectB.md"
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"metadata": {
|
|
125
|
+
"totalOccurrences": 45,
|
|
126
|
+
"uniqueTags": 12,
|
|
127
|
+
"scannedFiles": 30
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
path: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "Optional path to limit tag search to specific folder",
|
|
138
|
+
format: "path"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async runTool(args) {
|
|
145
|
+
try {
|
|
146
|
+
logger.debug(`Getting tags${args.path ? ` in path: ${args.path}` : ' in whole vault'}`);
|
|
147
|
+
const tagMap = new Map();
|
|
148
|
+
// Use searchJson to find files with .md extension
|
|
149
|
+
const query = args.path
|
|
150
|
+
? { "glob": [`${args.path}/**/*.md`.replace(/\\/g, '/'), { "var": "path" }] }
|
|
151
|
+
: { "glob": ["**/*.md", { "var": "path" }] };
|
|
152
|
+
const results = await this.client.searchJson(query);
|
|
153
|
+
let scannedFiles = 0;
|
|
154
|
+
// Process each file to extract tags from frontmatter
|
|
155
|
+
for (const result of results) {
|
|
156
|
+
if (!('filename' in result))
|
|
157
|
+
continue;
|
|
158
|
+
try {
|
|
159
|
+
const content = await this.client.getFileContents(result.filename);
|
|
160
|
+
scannedFiles++;
|
|
161
|
+
// Use PropertyManager to properly parse frontmatter
|
|
162
|
+
const properties = this.propertyManager.parseProperties(content);
|
|
163
|
+
// Process tags if they exist
|
|
164
|
+
if (properties.tags && Array.isArray(properties.tags)) {
|
|
165
|
+
// Process each tag
|
|
166
|
+
for (const tag of properties.tags) {
|
|
167
|
+
const cleanTag = tag.replace(/^#/, ''); // Remove leading # if present
|
|
168
|
+
if (!tagMap.has(cleanTag)) {
|
|
169
|
+
tagMap.set(cleanTag, new Set());
|
|
170
|
+
}
|
|
171
|
+
tagMap.get(cleanTag).add(result.filename);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
logger.error(`Failed to process file ${result.filename}:`, error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Calculate total occurrences
|
|
180
|
+
const totalOccurrences = Array.from(tagMap.values())
|
|
181
|
+
.reduce((sum, files) => sum + files.size, 0);
|
|
182
|
+
return this.createResponse({
|
|
183
|
+
tags: Array.from(tagMap.entries())
|
|
184
|
+
.map(([name, files]) => ({
|
|
185
|
+
name,
|
|
186
|
+
count: files.size,
|
|
187
|
+
files: Array.from(files).sort()
|
|
188
|
+
}))
|
|
189
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
|
|
190
|
+
metadata: {
|
|
191
|
+
totalOccurrences,
|
|
192
|
+
uniqueTags: tagMap.size,
|
|
193
|
+
scannedFiles,
|
|
194
|
+
lastUpdate: Date.now()
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
return this.handleError(error);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=complex.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search tools exports
|
|
3
|
+
*/
|
|
4
|
+
export * from './simple.js';
|
|
5
|
+
export * from './complex.js';
|
|
6
|
+
import { FindInFileToolHandler } from './simple.js';
|
|
7
|
+
import { ComplexSearchToolHandler, GetTagsToolHandler } from './complex.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create all search-related tool handlers
|
|
10
|
+
* @param client The ObsidianClient instance
|
|
11
|
+
* @returns Array of search tool handlers
|
|
12
|
+
*/
|
|
13
|
+
export function createSearchToolHandlers(client) {
|
|
14
|
+
return [
|
|
15
|
+
new FindInFileToolHandler(client),
|
|
16
|
+
new ComplexSearchToolHandler(client),
|
|
17
|
+
new GetTagsToolHandler(client)
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { BaseToolHandler } from "../base.js";
|
|
2
|
+
import { createLogger } from "../../utils/logging.js";
|
|
3
|
+
// Create a logger for search operations
|
|
4
|
+
const logger = createLogger('SimpleSearchTools');
|
|
5
|
+
/**
|
|
6
|
+
* Tool name for simple search
|
|
7
|
+
*/
|
|
8
|
+
export const SIMPLE_SEARCH_TOOL_NAME = "obsidian_find_in_file";
|
|
9
|
+
/**
|
|
10
|
+
* Tool handler for simple text search across files
|
|
11
|
+
*/
|
|
12
|
+
export class FindInFileToolHandler extends BaseToolHandler {
|
|
13
|
+
constructor(client) {
|
|
14
|
+
super(SIMPLE_SEARCH_TOOL_NAME, client);
|
|
15
|
+
}
|
|
16
|
+
getToolDescription() {
|
|
17
|
+
return {
|
|
18
|
+
name: this.name,
|
|
19
|
+
description: "Full-text search across all files in the vault. Returns matching files with surrounding context for each match. For results with more than 5 matching files, returns only file names and match counts to prevent overwhelming responses. Useful for finding specific content, references, or patterns across notes.",
|
|
20
|
+
examples: [
|
|
21
|
+
{
|
|
22
|
+
description: "Search for a specific term",
|
|
23
|
+
args: {
|
|
24
|
+
query: "neural networks",
|
|
25
|
+
contextLength: 20
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
description: "Search with default context",
|
|
30
|
+
args: {
|
|
31
|
+
query: "#todo"
|
|
32
|
+
},
|
|
33
|
+
response: {
|
|
34
|
+
"message": "Found 1 file with matches:",
|
|
35
|
+
"results": [
|
|
36
|
+
{
|
|
37
|
+
"filename": "Projects/AI.md",
|
|
38
|
+
"matches": [
|
|
39
|
+
{
|
|
40
|
+
"context": "Research needed:\n#todo Implement transformer architecture\nDeadline: Next week",
|
|
41
|
+
"match": { "start": 15, "end": 45 }
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
description: "Example response with many matches (file-only format)",
|
|
50
|
+
args: {
|
|
51
|
+
query: "API"
|
|
52
|
+
},
|
|
53
|
+
response: {
|
|
54
|
+
"message": "Found 92 files with matches. Showing file names only:",
|
|
55
|
+
"results": [
|
|
56
|
+
{
|
|
57
|
+
"filename": "Developer/Documentation/API.md",
|
|
58
|
+
"matchCount": 43
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"filename": "Projects/API_Design.md",
|
|
62
|
+
"matchCount": 34
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
query: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Text pattern to search for. Can include tags, keywords, or phrases."
|
|
74
|
+
},
|
|
75
|
+
contextLength: {
|
|
76
|
+
type: "integer",
|
|
77
|
+
description: "Number of characters to include before and after each match for context (default: 10)",
|
|
78
|
+
default: 10
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
required: ["query"]
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async runTool(args) {
|
|
86
|
+
try {
|
|
87
|
+
logger.debug(`Searching for "${args.query}" with context length: ${args.contextLength || 100}`);
|
|
88
|
+
const results = await this.client.search(args.query, args.contextLength ?? 100);
|
|
89
|
+
// If more than 5 results, only return filenames and match counts to prevent overwhelming responses
|
|
90
|
+
if (results.length > 5) {
|
|
91
|
+
const fileOnlyResults = results.map(result => ({
|
|
92
|
+
filename: result.filename,
|
|
93
|
+
matchCount: result.matches.length
|
|
94
|
+
}));
|
|
95
|
+
logger.debug(`Found ${results.length} files with matches, returning file-only format`);
|
|
96
|
+
return this.createResponse({
|
|
97
|
+
message: `Found ${results.length} files with matches. Showing file names only:`,
|
|
98
|
+
results: fileOnlyResults
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Otherwise return full context as before
|
|
102
|
+
const formattedResults = results.map(result => ({
|
|
103
|
+
filename: result.filename,
|
|
104
|
+
matches: result.matches.map(match => ({
|
|
105
|
+
context: match.context,
|
|
106
|
+
match: {
|
|
107
|
+
text: match.context.substring(match.match.start, match.match.end),
|
|
108
|
+
position: {
|
|
109
|
+
start: match.match.start,
|
|
110
|
+
end: match.match.end
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})),
|
|
114
|
+
score: result.score
|
|
115
|
+
}));
|
|
116
|
+
logger.debug(`Found ${results.length} files with matches, returning with context`);
|
|
117
|
+
return this.createResponse({
|
|
118
|
+
message: `Found ${results.length} file(s) with matches:`,
|
|
119
|
+
results: formattedResults
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return this.handleError(error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=simple.js.map
|