@disco_trooper/apple-notes-mcp 1.1.0 → 1.3.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.
Files changed (49) hide show
  1. package/README.md +104 -24
  2. package/package.json +11 -12
  3. package/src/config/claude.test.ts +47 -0
  4. package/src/config/claude.ts +106 -0
  5. package/src/config/constants.ts +11 -2
  6. package/src/config/paths.test.ts +40 -0
  7. package/src/config/paths.ts +86 -0
  8. package/src/db/arrow-fix.test.ts +101 -0
  9. package/src/db/lancedb.test.ts +254 -2
  10. package/src/db/lancedb.ts +385 -38
  11. package/src/embeddings/cache.test.ts +150 -0
  12. package/src/embeddings/cache.ts +204 -0
  13. package/src/embeddings/index.ts +22 -4
  14. package/src/embeddings/local.ts +57 -17
  15. package/src/embeddings/openrouter.ts +233 -11
  16. package/src/errors/index.test.ts +64 -0
  17. package/src/errors/index.ts +62 -0
  18. package/src/graph/export.test.ts +81 -0
  19. package/src/graph/export.ts +163 -0
  20. package/src/graph/extract.test.ts +90 -0
  21. package/src/graph/extract.ts +52 -0
  22. package/src/graph/queries.test.ts +156 -0
  23. package/src/graph/queries.ts +224 -0
  24. package/src/index.ts +309 -23
  25. package/src/notes/conversion.ts +62 -0
  26. package/src/notes/crud.test.ts +41 -8
  27. package/src/notes/crud.ts +75 -64
  28. package/src/notes/read.test.ts +58 -3
  29. package/src/notes/read.ts +142 -210
  30. package/src/notes/resolve.ts +174 -0
  31. package/src/notes/tables.ts +69 -40
  32. package/src/search/chunk-indexer.test.ts +353 -0
  33. package/src/search/chunk-indexer.ts +207 -0
  34. package/src/search/chunk-search.test.ts +327 -0
  35. package/src/search/chunk-search.ts +298 -0
  36. package/src/search/index.ts +4 -6
  37. package/src/search/indexer.ts +164 -109
  38. package/src/setup.ts +46 -67
  39. package/src/types/index.ts +4 -0
  40. package/src/utils/chunker.test.ts +182 -0
  41. package/src/utils/chunker.ts +170 -0
  42. package/src/utils/content-filter.test.ts +225 -0
  43. package/src/utils/content-filter.ts +275 -0
  44. package/src/utils/debug.ts +0 -2
  45. package/src/utils/runtime.test.ts +70 -0
  46. package/src/utils/runtime.ts +40 -0
  47. package/src/utils/text.test.ts +32 -0
  48. package/CLAUDE.md +0 -56
  49. package/src/server.ts +0 -427
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { LanceDBStore } from "./lancedb.js";
3
- import type { NoteRecord } from "./lancedb.js";
2
+ import { LanceDBStore, ChunkStore } from "./lancedb.js";
3
+ import type { NoteRecord, ChunkRecord } from "./lancedb.js";
4
4
  import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
 
@@ -20,6 +20,7 @@ describe("LanceDBStore", () => {
20
20
  });
21
21
 
22
22
  const createTestRecord = (title: string): NoteRecord => ({
23
+ id: `test-id-${title.toLowerCase().replace(/\s+/g, "-")}`,
23
24
  title,
24
25
  folder: "Test",
25
26
  content: `Content of ${title}`,
@@ -27,6 +28,9 @@ describe("LanceDBStore", () => {
27
28
  created: new Date().toISOString(),
28
29
  indexed_at: new Date().toISOString(),
29
30
  vector: Array(384).fill(0.1),
31
+ // LanceDB requires at least one element to infer the list type
32
+ tags: ["test-tag"],
33
+ outlinks: ["test-link"],
30
34
  });
31
35
 
32
36
  describe("index and getByTitle", () => {
@@ -138,4 +142,252 @@ describe("LanceDBStore", () => {
138
142
  expect(results[0]).toHaveProperty("score");
139
143
  });
140
144
  });
145
+
146
+ describe("rebuildFtsIndex", () => {
147
+ it("rebuilds FTS index without error", async () => {
148
+ await store.index([
149
+ createTestRecord("FTS Note 1"),
150
+ createTestRecord("FTS Note 2"),
151
+ ]);
152
+
153
+ await expect(store.rebuildFtsIndex()).resolves.not.toThrow();
154
+ });
155
+
156
+ it("works after indexing records", async () => {
157
+ await store.index([createTestRecord("Note A")]);
158
+
159
+ // Rebuild should work on existing table
160
+ await expect(store.rebuildFtsIndex()).resolves.not.toThrow();
161
+
162
+ // Index more records and rebuild again
163
+ await store.index([createTestRecord("Note B")]);
164
+ await expect(store.rebuildFtsIndex()).resolves.not.toThrow();
165
+ });
166
+ });
167
+
168
+ describe("searchFTS", () => {
169
+ it("returns results matching query text", async () => {
170
+ await store.index([
171
+ createTestRecord("Meeting notes"),
172
+ createTestRecord("Shopping list"),
173
+ ]);
174
+ await store.rebuildFtsIndex();
175
+
176
+ const results = await store.searchFTS("Meeting", 10);
177
+ expect(results.length).toBeGreaterThanOrEqual(1);
178
+ expect(results[0].title).toBe("Meeting notes");
179
+ });
180
+
181
+ it("returns empty array for no matches", async () => {
182
+ await store.index([createTestRecord("Test note")]);
183
+ await store.rebuildFtsIndex();
184
+
185
+ const results = await store.searchFTS("nonexistentquery12345", 10);
186
+ expect(results).toHaveLength(0);
187
+ });
188
+ });
189
+ });
190
+
191
+ describe("ChunkStore", () => {
192
+ let chunkStore: ChunkStore;
193
+ let testDbPath: string;
194
+
195
+ beforeEach(() => {
196
+ testDbPath = path.join("/tmp", `lancedb-chunk-test-${Date.now()}`);
197
+ chunkStore = new ChunkStore(testDbPath);
198
+ });
199
+
200
+ afterEach(() => {
201
+ if (fs.existsSync(testDbPath)) {
202
+ fs.rmSync(testDbPath, { recursive: true, force: true });
203
+ }
204
+ });
205
+
206
+ const createTestChunk = (
207
+ noteId: string,
208
+ chunkIndex: number,
209
+ totalChunks: number,
210
+ content?: string
211
+ ): ChunkRecord => ({
212
+ chunk_id: `${noteId}_chunk_${chunkIndex}`,
213
+ note_id: noteId,
214
+ note_title: `Note ${noteId}`,
215
+ folder: "Test",
216
+ chunk_index: chunkIndex,
217
+ total_chunks: totalChunks,
218
+ content: content ?? `Chunk ${chunkIndex} content for note ${noteId}`,
219
+ vector: Array(384).fill(0.1),
220
+ created: new Date().toISOString(),
221
+ modified: new Date().toISOString(),
222
+ indexed_at: new Date().toISOString(),
223
+ tags: ["test-tag"],
224
+ outlinks: ["test-link"],
225
+ });
226
+
227
+ describe("indexChunks", () => {
228
+ it("indexes chunks and allows retrieval", async () => {
229
+ const chunks = [
230
+ createTestChunk("note-1", 0, 2),
231
+ createTestChunk("note-1", 1, 2),
232
+ createTestChunk("note-2", 0, 1),
233
+ ];
234
+
235
+ await chunkStore.indexChunks(chunks);
236
+ const count = await chunkStore.count();
237
+
238
+ expect(count).toBe(3);
239
+ });
240
+
241
+ it("handles empty chunks array", async () => {
242
+ await chunkStore.indexChunks([]);
243
+ const count = await chunkStore.count();
244
+ expect(count).toBe(0);
245
+ });
246
+
247
+ it("handles chunks with empty tags and outlinks", async () => {
248
+ const chunks = [
249
+ { ...createTestChunk("note-1", 0, 1), tags: [], outlinks: [] },
250
+ ];
251
+
252
+ await chunkStore.indexChunks(chunks);
253
+ const count = await chunkStore.count();
254
+ expect(count).toBe(1);
255
+ });
256
+ });
257
+
258
+ describe("searchChunks", () => {
259
+ it("returns results based on vector similarity", async () => {
260
+ const chunks = [
261
+ createTestChunk("note-1", 0, 2),
262
+ createTestChunk("note-1", 1, 2),
263
+ ];
264
+ await chunkStore.indexChunks(chunks);
265
+
266
+ const queryVector = Array(384).fill(0.1);
267
+ const results = await chunkStore.searchChunks(queryVector, 2);
268
+
269
+ expect(results).toHaveLength(2);
270
+ expect(results[0]).toHaveProperty("chunk_id");
271
+ expect(results[0]).toHaveProperty("note_id");
272
+ expect(results[0]).toHaveProperty("score");
273
+ });
274
+ });
275
+
276
+ describe("searchChunksFTS", () => {
277
+ it("returns results matching query text", async () => {
278
+ const chunks = [
279
+ createTestChunk("note-1", 0, 1, "Meeting notes about project planning"),
280
+ createTestChunk("note-2", 0, 1, "Shopping list for groceries"),
281
+ ];
282
+ await chunkStore.indexChunks(chunks);
283
+ await chunkStore.rebuildFtsIndex();
284
+
285
+ const results = await chunkStore.searchChunksFTS("Meeting", 10);
286
+ expect(results.length).toBeGreaterThanOrEqual(1);
287
+ expect(results[0].content).toContain("Meeting");
288
+ });
289
+
290
+ it("returns empty array for no matches", async () => {
291
+ const chunks = [createTestChunk("note-1", 0, 1)];
292
+ await chunkStore.indexChunks(chunks);
293
+ await chunkStore.rebuildFtsIndex();
294
+
295
+ const results = await chunkStore.searchChunksFTS("nonexistentquery12345", 10);
296
+ expect(results).toHaveLength(0);
297
+ });
298
+ });
299
+
300
+ describe("getChunksByNoteId", () => {
301
+ it("returns all chunks for a note sorted by chunk_index", async () => {
302
+ const chunks = [
303
+ createTestChunk("note-1", 2, 3),
304
+ createTestChunk("note-1", 0, 3),
305
+ createTestChunk("note-1", 1, 3),
306
+ createTestChunk("note-2", 0, 1),
307
+ ];
308
+ await chunkStore.indexChunks(chunks);
309
+
310
+ const noteChunks = await chunkStore.getChunksByNoteId("note-1");
311
+
312
+ expect(noteChunks).toHaveLength(3);
313
+ expect(noteChunks[0].chunk_index).toBe(0);
314
+ expect(noteChunks[1].chunk_index).toBe(1);
315
+ expect(noteChunks[2].chunk_index).toBe(2);
316
+ });
317
+
318
+ it("returns empty array for non-existent note", async () => {
319
+ await chunkStore.indexChunks([createTestChunk("note-1", 0, 1)]);
320
+
321
+ const chunks = await chunkStore.getChunksByNoteId("non-existent");
322
+ expect(chunks).toHaveLength(0);
323
+ });
324
+ });
325
+
326
+ describe("deleteNoteChunks", () => {
327
+ it("deletes all chunks for a note", async () => {
328
+ const chunks = [
329
+ createTestChunk("note-1", 0, 2),
330
+ createTestChunk("note-1", 1, 2),
331
+ createTestChunk("note-2", 0, 1),
332
+ ];
333
+ await chunkStore.indexChunks(chunks);
334
+
335
+ await chunkStore.deleteNoteChunks("note-1");
336
+
337
+ const remaining = await chunkStore.count();
338
+ expect(remaining).toBe(1);
339
+
340
+ const note1Chunks = await chunkStore.getChunksByNoteId("note-1");
341
+ expect(note1Chunks).toHaveLength(0);
342
+
343
+ const note2Chunks = await chunkStore.getChunksByNoteId("note-2");
344
+ expect(note2Chunks).toHaveLength(1);
345
+ });
346
+
347
+ it("does not throw when deleting non-existent note chunks", async () => {
348
+ await chunkStore.indexChunks([createTestChunk("note-1", 0, 1)]);
349
+ await expect(chunkStore.deleteNoteChunks("non-existent")).resolves.not.toThrow();
350
+ });
351
+ });
352
+
353
+ describe("count", () => {
354
+ it("returns correct count", async () => {
355
+ const chunks = [
356
+ createTestChunk("note-1", 0, 2),
357
+ createTestChunk("note-1", 1, 2),
358
+ createTestChunk("note-2", 0, 1),
359
+ ];
360
+ await chunkStore.indexChunks(chunks);
361
+
362
+ expect(await chunkStore.count()).toBe(3);
363
+ });
364
+
365
+ it("returns 0 for empty store", async () => {
366
+ expect(await chunkStore.count()).toBe(0);
367
+ });
368
+ });
369
+
370
+ describe("clear", () => {
371
+ it("removes all chunks", async () => {
372
+ await chunkStore.indexChunks([
373
+ createTestChunk("note-1", 0, 1),
374
+ createTestChunk("note-2", 0, 1),
375
+ ]);
376
+ expect(await chunkStore.count()).toBe(2);
377
+
378
+ await chunkStore.clear();
379
+ expect(await chunkStore.count()).toBe(0);
380
+ });
381
+ });
382
+
383
+ describe("rebuildFtsIndex", () => {
384
+ it("rebuilds FTS index without error", async () => {
385
+ await chunkStore.indexChunks([
386
+ createTestChunk("note-1", 0, 1),
387
+ createTestChunk("note-2", 0, 1),
388
+ ]);
389
+
390
+ await expect(chunkStore.rebuildFtsIndex()).resolves.not.toThrow();
391
+ });
392
+ });
141
393
  });