@bitbonsai/mcpvault 0.8.2 → 0.10.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 +6 -6
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +2 -641
- package/dist/src/createServer.d.ts +11 -0
- package/dist/src/createServer.d.ts.map +1 -0
- package/dist/src/createServer.js +383 -0
- package/dist/src/filesystem.d.ts +26 -0
- package/dist/src/filesystem.d.ts.map +1 -0
- package/dist/src/filesystem.js +73 -5
- package/dist/src/frontmatter.d.ts +16 -0
- package/dist/src/frontmatter.d.ts.map +1 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/pathfilter.d.ts +13 -0
- package/dist/src/pathfilter.d.ts.map +1 -0
- package/dist/src/search.d.ts +11 -0
- package/dist/src/search.d.ts.map +1 -0
- package/dist/src/types.d.ts +134 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/uri.d.ts +10 -0
- package/dist/src/uri.d.ts.map +1 -0
- package/package.json +10 -3
- package/dist/src/filesystem.test.js +0 -946
- package/dist/src/frontmatter.test.js +0 -118
- package/dist/src/integration.test.js +0 -168
- package/dist/src/pathfilter.test.js +0 -264
- package/dist/src/search.test.js +0 -212
- package/dist/src/uri.test.js +0 -46
package/dist/src/search.test.js
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { SearchService } from "./search.js";
|
|
3
|
-
import { PathFilter } from "./pathfilter.js";
|
|
4
|
-
import { writeFile, mkdir, mkdtemp, rm } from "fs/promises";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { tmpdir } from "os";
|
|
7
|
-
let testVaultPath;
|
|
8
|
-
let searchService;
|
|
9
|
-
beforeEach(async () => {
|
|
10
|
-
testVaultPath = await mkdtemp(join(tmpdir(), "mcpvault-search-"));
|
|
11
|
-
searchService = new SearchService(testVaultPath, new PathFilter());
|
|
12
|
-
});
|
|
13
|
-
afterEach(async () => {
|
|
14
|
-
try {
|
|
15
|
-
await rm(testVaultPath, { recursive: true });
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
// Ignore cleanup errors
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
// Helper to write a note directly to disk
|
|
22
|
-
async function writeNote(path, content) {
|
|
23
|
-
const fullPath = join(testVaultPath, path);
|
|
24
|
-
const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
|
|
25
|
-
if (dir !== testVaultPath) {
|
|
26
|
-
await mkdir(dir, { recursive: true });
|
|
27
|
-
}
|
|
28
|
-
await writeFile(fullPath, content);
|
|
29
|
-
}
|
|
30
|
-
describe("SearchService", () => {
|
|
31
|
-
// ============================================================================
|
|
32
|
-
// BASIC SEARCH
|
|
33
|
-
// ============================================================================
|
|
34
|
-
test("finds notes matching a query", async () => {
|
|
35
|
-
await writeNote("alpha.md", "# Alpha\n\nThis note has bananas.");
|
|
36
|
-
await writeNote("beta.md", "# Beta\n\nThis note has oranges.");
|
|
37
|
-
const results = await searchService.search({ query: "bananas" });
|
|
38
|
-
expect(results).toHaveLength(1);
|
|
39
|
-
expect(results[0].p).toBe("alpha.md");
|
|
40
|
-
});
|
|
41
|
-
test("returns empty array when no matches", async () => {
|
|
42
|
-
await writeNote("note.md", "# Note\n\nNothing relevant here.");
|
|
43
|
-
const results = await searchService.search({ query: "zzzznotfound" });
|
|
44
|
-
expect(results).toHaveLength(0);
|
|
45
|
-
});
|
|
46
|
-
test("returns empty array for empty vault", async () => {
|
|
47
|
-
const results = await searchService.search({ query: "anything" });
|
|
48
|
-
expect(results).toHaveLength(0);
|
|
49
|
-
});
|
|
50
|
-
test("throws on empty query", async () => {
|
|
51
|
-
await expect(searchService.search({ query: "" }))
|
|
52
|
-
.rejects.toThrow(/empty/);
|
|
53
|
-
});
|
|
54
|
-
test("throws on whitespace-only query", async () => {
|
|
55
|
-
await expect(searchService.search({ query: " " }))
|
|
56
|
-
.rejects.toThrow(/empty/);
|
|
57
|
-
});
|
|
58
|
-
// ============================================================================
|
|
59
|
-
// LIMIT
|
|
60
|
-
// ============================================================================
|
|
61
|
-
test("respects limit parameter", async () => {
|
|
62
|
-
for (let i = 0; i < 5; i++) {
|
|
63
|
-
await writeNote(`note-${i}.md`, `# Note ${i}\n\nkeyword here.`);
|
|
64
|
-
}
|
|
65
|
-
const results = await searchService.search({ query: "keyword", limit: 2 });
|
|
66
|
-
expect(results).toHaveLength(2);
|
|
67
|
-
});
|
|
68
|
-
test("caps limit at 20", async () => {
|
|
69
|
-
for (let i = 0; i < 25; i++) {
|
|
70
|
-
await writeNote(`note-${i}.md`, `# Note ${i}\n\nkeyword here.`);
|
|
71
|
-
}
|
|
72
|
-
const results = await searchService.search({ query: "keyword", limit: 100 });
|
|
73
|
-
expect(results.length).toBeLessThanOrEqual(20);
|
|
74
|
-
});
|
|
75
|
-
test("defaults limit to 5", async () => {
|
|
76
|
-
for (let i = 0; i < 10; i++) {
|
|
77
|
-
await writeNote(`note-${i}.md`, `# Note ${i}\n\nkeyword here.`);
|
|
78
|
-
}
|
|
79
|
-
const results = await searchService.search({ query: "keyword" });
|
|
80
|
-
expect(results).toHaveLength(5);
|
|
81
|
-
});
|
|
82
|
-
// ============================================================================
|
|
83
|
-
// CASE SENSITIVITY
|
|
84
|
-
// ============================================================================
|
|
85
|
-
test("case-insensitive search by default", async () => {
|
|
86
|
-
await writeNote("upper.md", "# Upper\n\nBANANA is great.");
|
|
87
|
-
await writeNote("lower.md", "# Lower\n\nbanana is great.");
|
|
88
|
-
await writeNote("mixed.md", "# Mixed\n\nBanana is great.");
|
|
89
|
-
const results = await searchService.search({ query: "banana", limit: 10 });
|
|
90
|
-
expect(results).toHaveLength(3);
|
|
91
|
-
});
|
|
92
|
-
test("case-sensitive search when enabled", async () => {
|
|
93
|
-
await writeNote("upper.md", "# Upper\n\nBANANA is great.");
|
|
94
|
-
await writeNote("lower.md", "# Lower\n\nbanana is great.");
|
|
95
|
-
const results = await searchService.search({
|
|
96
|
-
query: "BANANA",
|
|
97
|
-
caseSensitive: true,
|
|
98
|
-
limit: 10
|
|
99
|
-
});
|
|
100
|
-
expect(results).toHaveLength(1);
|
|
101
|
-
expect(results[0].p).toBe("upper.md");
|
|
102
|
-
});
|
|
103
|
-
// ============================================================================
|
|
104
|
-
// FRONTMATTER SEARCH
|
|
105
|
-
// ============================================================================
|
|
106
|
-
test("excludes frontmatter from content-only search", async () => {
|
|
107
|
-
await writeNote("note.md", "---\ntags: [uniquetag]\n---\n\n# Note\n\nNo tag here.");
|
|
108
|
-
const results = await searchService.search({
|
|
109
|
-
query: "uniquetag",
|
|
110
|
-
searchContent: true,
|
|
111
|
-
searchFrontmatter: false,
|
|
112
|
-
limit: 10
|
|
113
|
-
});
|
|
114
|
-
expect(results).toHaveLength(0);
|
|
115
|
-
});
|
|
116
|
-
test("searches frontmatter when enabled", async () => {
|
|
117
|
-
await writeNote("note.md", "---\ntags: [uniquetag]\n---\n\n# Note\n\nNo tag here.");
|
|
118
|
-
const results = await searchService.search({
|
|
119
|
-
query: "uniquetag",
|
|
120
|
-
searchFrontmatter: true,
|
|
121
|
-
limit: 10
|
|
122
|
-
});
|
|
123
|
-
expect(results).toHaveLength(1);
|
|
124
|
-
expect(results[0].p).toBe("note.md");
|
|
125
|
-
});
|
|
126
|
-
test("searches both content and frontmatter together", async () => {
|
|
127
|
-
await writeNote("fm-only.md", "---\nstatus: special\n---\n\n# Note\n\nPlain body.");
|
|
128
|
-
await writeNote("content-only.md", "# Note\n\nThis is special content.");
|
|
129
|
-
const results = await searchService.search({
|
|
130
|
-
query: "special",
|
|
131
|
-
searchContent: true,
|
|
132
|
-
searchFrontmatter: true,
|
|
133
|
-
limit: 10
|
|
134
|
-
});
|
|
135
|
-
expect(results).toHaveLength(2);
|
|
136
|
-
});
|
|
137
|
-
// ============================================================================
|
|
138
|
-
// FILENAME MATCHING
|
|
139
|
-
// ============================================================================
|
|
140
|
-
test("matches by filename when content has no match", async () => {
|
|
141
|
-
await writeNote("Recipes.md", "Some unrelated content about cooking.");
|
|
142
|
-
const results = await searchService.search({ query: "recipes", limit: 10 });
|
|
143
|
-
expect(results).toHaveLength(1);
|
|
144
|
-
expect(results[0].p).toBe("Recipes.md");
|
|
145
|
-
expect(results[0].t).toBe("Recipes");
|
|
146
|
-
});
|
|
147
|
-
// ============================================================================
|
|
148
|
-
// MULTI-TERM SEARCH
|
|
149
|
-
// ============================================================================
|
|
150
|
-
test("multi-term search matches notes with any term", async () => {
|
|
151
|
-
await writeNote("cats.md", "# Cats\n\nI love cats.");
|
|
152
|
-
await writeNote("dogs.md", "# Dogs\n\nI love dogs.");
|
|
153
|
-
await writeNote("fish.md", "# Fish\n\nI love fish.");
|
|
154
|
-
const results = await searchService.search({ query: "cats dogs", limit: 10 });
|
|
155
|
-
const paths = results.map(r => r.p);
|
|
156
|
-
expect(paths).toContain("cats.md");
|
|
157
|
-
expect(paths).toContain("dogs.md");
|
|
158
|
-
expect(paths).not.toContain("fish.md");
|
|
159
|
-
});
|
|
160
|
-
// ============================================================================
|
|
161
|
-
// RANKING
|
|
162
|
-
// ============================================================================
|
|
163
|
-
test("ranks notes with more matches higher", async () => {
|
|
164
|
-
await writeNote("few.md", "# Few\n\napple once.");
|
|
165
|
-
await writeNote("many.md", "# Many\n\napple apple apple apple apple.");
|
|
166
|
-
const results = await searchService.search({ query: "apple", limit: 10 });
|
|
167
|
-
expect(results).toHaveLength(2);
|
|
168
|
-
expect(results[0].p).toBe("many.md");
|
|
169
|
-
});
|
|
170
|
-
// ============================================================================
|
|
171
|
-
// RESULT SHAPE
|
|
172
|
-
// ============================================================================
|
|
173
|
-
test("results include expected fields", async () => {
|
|
174
|
-
await writeNote("folder/note.md", "# My Note\n\nSome content with target word.");
|
|
175
|
-
const results = await searchService.search({ query: "target", limit: 10 });
|
|
176
|
-
expect(results).toHaveLength(1);
|
|
177
|
-
const r = results[0];
|
|
178
|
-
expect(r.p).toBe("folder/note.md");
|
|
179
|
-
expect(r.t).toBe("note");
|
|
180
|
-
expect(r.ex).toBeDefined();
|
|
181
|
-
expect(r.mc).toBeGreaterThanOrEqual(1);
|
|
182
|
-
expect(r.ln).toBeGreaterThanOrEqual(1);
|
|
183
|
-
expect(r.uri).toMatch(/^obsidian:\/\//);
|
|
184
|
-
});
|
|
185
|
-
test("excerpt contains context around match", async () => {
|
|
186
|
-
await writeNote("note.md", "# Note\n\nSome words before target some words after.");
|
|
187
|
-
const results = await searchService.search({ query: "target", limit: 10 });
|
|
188
|
-
expect(results[0].ex).toContain("target");
|
|
189
|
-
});
|
|
190
|
-
// ============================================================================
|
|
191
|
-
// PATH FILTERING
|
|
192
|
-
// ============================================================================
|
|
193
|
-
test("excludes notes in filtered directories", async () => {
|
|
194
|
-
await writeNote("visible.md", "# Visible\n\nkeyword here.");
|
|
195
|
-
await mkdir(join(testVaultPath, ".obsidian"), { recursive: true });
|
|
196
|
-
await writeFile(join(testVaultPath, ".obsidian/config.md"), "keyword here.");
|
|
197
|
-
const results = await searchService.search({ query: "keyword", limit: 10 });
|
|
198
|
-
expect(results).toHaveLength(1);
|
|
199
|
-
expect(results[0].p).toBe("visible.md");
|
|
200
|
-
});
|
|
201
|
-
// ============================================================================
|
|
202
|
-
// TRAILING SLASH IN VAULT PATH
|
|
203
|
-
// ============================================================================
|
|
204
|
-
test("vault path with trailing slash does not truncate result paths", async () => {
|
|
205
|
-
const trailingSlashService = new SearchService(testVaultPath + "/", new PathFilter());
|
|
206
|
-
await mkdir(join(testVaultPath, "sessions"), { recursive: true });
|
|
207
|
-
await writeNote("sessions/foo-bar.md", "# Foo Bar\n\nSome content here.");
|
|
208
|
-
const results = await trailingSlashService.search({ query: "foo", limit: 5 });
|
|
209
|
-
expect(results).toHaveLength(1);
|
|
210
|
-
expect(results[0].p).toBe("sessions/foo-bar.md");
|
|
211
|
-
});
|
|
212
|
-
});
|
package/dist/src/uri.test.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { generateObsidianUri } from './uri.js';
|
|
3
|
-
describe('generateObsidianUri', () => {
|
|
4
|
-
it('generates URI with absolute path', () => {
|
|
5
|
-
const vaultPath = '/Users/test/vault';
|
|
6
|
-
const notePath = 'folder/note.md';
|
|
7
|
-
const uri = generateObsidianUri(vaultPath, notePath);
|
|
8
|
-
expect(uri).toBe('obsidian:////Users/test/vault/folder/note');
|
|
9
|
-
});
|
|
10
|
-
it('handles paths with leading slash', () => {
|
|
11
|
-
const vaultPath = '/Users/test/vault';
|
|
12
|
-
const notePath = '/folder/note.md';
|
|
13
|
-
const uri = generateObsidianUri(vaultPath, notePath);
|
|
14
|
-
expect(uri).toBe('obsidian:////Users/test/vault/folder/note');
|
|
15
|
-
});
|
|
16
|
-
it('removes .md extension', () => {
|
|
17
|
-
const vaultPath = '/Users/test/vault';
|
|
18
|
-
const notePath = 'note.md';
|
|
19
|
-
const uri = generateObsidianUri(vaultPath, notePath);
|
|
20
|
-
expect(uri).toBe('obsidian:////Users/test/vault/note');
|
|
21
|
-
});
|
|
22
|
-
it('encodes special characters', () => {
|
|
23
|
-
const vaultPath = '/Users/test/vault';
|
|
24
|
-
const notePath = 'folder/my note with spaces.md';
|
|
25
|
-
const uri = generateObsidianUri(vaultPath, notePath);
|
|
26
|
-
expect(uri).toBe('obsidian:////Users/test/vault/folder/my%20note%20with%20spaces');
|
|
27
|
-
});
|
|
28
|
-
it('handles notes in root directory', () => {
|
|
29
|
-
const vaultPath = '/Users/test/vault';
|
|
30
|
-
const notePath = 'note.md';
|
|
31
|
-
const uri = generateObsidianUri(vaultPath, notePath);
|
|
32
|
-
expect(uri).toBe('obsidian:////Users/test/vault/note');
|
|
33
|
-
});
|
|
34
|
-
it('handles nested directories', () => {
|
|
35
|
-
const vaultPath = '/Users/test/vault';
|
|
36
|
-
const notePath = 'folder1/folder2/folder3/note.md';
|
|
37
|
-
const uri = generateObsidianUri(vaultPath, notePath);
|
|
38
|
-
expect(uri).toBe('obsidian:////Users/test/vault/folder1/folder2/folder3/note');
|
|
39
|
-
});
|
|
40
|
-
it('encodes special characters in directory names', () => {
|
|
41
|
-
const vaultPath = '/Users/test/vault';
|
|
42
|
-
const notePath = 'my folder/sub folder/note.md';
|
|
43
|
-
const uri = generateObsidianUri(vaultPath, notePath);
|
|
44
|
-
expect(uri).toBe('obsidian:////Users/test/vault/my%20folder/sub%20folder/note');
|
|
45
|
-
});
|
|
46
|
-
});
|