@disco_trooper/apple-notes-mcp 1.2.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.
package/README.md CHANGED
@@ -1,21 +1,30 @@
1
1
  # apple-notes-mcp
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@disco_trooper/apple-notes-mcp)](https://www.npmjs.com/package/@disco_trooper/apple-notes-mcp)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![macOS](https://img.shields.io/badge/macOS-000000?logo=apple&logoColor=white)](https://www.apple.com/macos/)
6
+ [![Bun](https://img.shields.io/badge/Bun-000000?logo=bun&logoColor=white)](https://bun.sh)
7
+ [![Claude](https://img.shields.io/badge/Claude-MCP-blueviolet)](https://modelcontextprotocol.io)
8
+
3
9
  MCP server for Apple Notes with semantic search and CRUD operations. Claude searches, reads, creates, updates, and manages your Apple Notes through natural language.
4
10
 
5
11
  ## Features
6
12
 
13
+ - **Chunk-Based Search** - Long notes split into chunks for accurate matching (NEW!)
14
+ - **Query Caching** - 60x faster repeated searches (NEW!)
15
+ - **Knowledge Graph** - Tags, links, and related notes discovery (NEW!)
16
+ - **Hybrid Search** - Vector + keyword search with Reciprocal Rank Fusion
7
17
  - **Semantic Search** - Find notes by meaning, not keywords
8
- - **Hybrid Search** - Combine vector and keyword search for better results
9
18
  - **Full CRUD** - Create, read, update, delete, and move notes
10
19
  - **Incremental Indexing** - Re-embed only changed notes
11
- - **Dual Embedding Support** - Local HuggingFace or OpenRouter API
12
- - **Claude Code Integration** - Works with Claude Code CLI
20
+ - **Dual Embedding** - Local HuggingFace or OpenRouter API
13
21
 
14
- ## Requirements
22
+ ## What's New in 1.3
15
23
 
16
- - macOS (uses Apple Notes via JXA)
17
- - [Bun](https://bun.sh) runtime
18
- - Apple Notes app with notes
24
+ - **Parent Document Retriever** - Splits long notes into 500-char chunks with 100-char overlap. Searches match specific sections, returns full notes.
25
+ - **60x faster cached queries** - Query embedding cache eliminates redundant API calls.
26
+ - **Auto-filters Base64/encoded content** - Skips images and attachments during indexing.
27
+ - **4-6x faster indexing** - Parallel processing and optimized chunk generation.
19
28
 
20
29
  ## Installation
21
30
 
@@ -23,34 +32,43 @@ MCP server for Apple Notes with semantic search and CRUD operations. Claude sear
23
32
 
24
33
  ```bash
25
34
  npm install -g @disco_trooper/apple-notes-mcp
35
+ apple-notes-mcp
26
36
  ```
27
37
 
38
+ The setup wizard guides you through:
39
+ 1. Choosing your embedding provider (local or OpenRouter)
40
+ 2. Configuring API keys if needed
41
+ 3. Setting up Claude Code integration
42
+ 4. Indexing your notes
43
+
28
44
  ### From source
29
45
 
30
46
  ```bash
31
47
  git clone https://github.com/disco-trooper/apple-notes-mcp.git
32
48
  cd apple-notes-mcp
33
49
  bun install
50
+ bun run start
34
51
  ```
35
52
 
53
+ ## Requirements
54
+
55
+ - macOS (uses Apple Notes via JXA)
56
+ - [Bun](https://bun.sh) runtime
57
+ - Apple Notes app with notes
58
+
36
59
  ## Quick Start
37
60
 
38
- Run the setup wizard:
61
+ Run the command after installation:
39
62
 
40
63
  ```bash
41
- bun run setup
64
+ apple-notes-mcp
42
65
  ```
43
66
 
44
- The wizard:
45
- 1. Chooses your embedding provider (local or OpenRouter)
46
- 2. Configures API keys if needed
47
- 3. Sets auto-indexing preferences
48
- 4. Adds to Claude Code configuration
49
- 5. Indexes your notes
67
+ The setup wizard starts automatically on first run. Restart Claude Code after setup to use the MCP tools.
50
68
 
51
69
  ## Configuration
52
70
 
53
- Store configuration in `.env`:
71
+ Configuration stored in `~/.apple-notes-mcp/.env`:
54
72
 
55
73
  | Variable | Description | Default |
56
74
  |----------|-------------|---------|
@@ -61,6 +79,14 @@ Store configuration in `.env`:
61
79
  | `INDEX_TTL` | Auto-reindex interval in seconds | - |
62
80
  | `DEBUG` | Enable debug logging | `false` |
63
81
 
82
+ To reconfigure:
83
+
84
+ ```bash
85
+ apple-notes-mcp setup
86
+ # or from source:
87
+ bun run setup
88
+ ```
89
+
64
90
  ### Embedding Providers
65
91
 
66
92
  **Local (default)**: Uses HuggingFace Transformers with `Xenova/multilingual-e5-small`. Free, runs locally, ~200MB download.
@@ -74,14 +100,14 @@ See [docs/models.md](docs/models.md) for model comparison.
74
100
  ### Search & Discovery
75
101
 
76
102
  #### `search-notes`
77
- Search notes with hybrid vector + fulltext search.
103
+ Hybrid vector + fulltext search.
78
104
 
79
105
  ```
80
106
  query: "meeting notes from last week"
81
107
  folder: "Work" # optional, filter by folder
82
- limit: 10 # default: 20
83
- mode: "hybrid" # hybrid, keyword, or semantic
84
- include_content: false # include full content vs preview
108
+ limit: 10 # default: 20
109
+ mode: "hybrid" # hybrid, keyword, or semantic
110
+ include_content: false # include full content vs preview
85
111
  ```
86
112
 
87
113
  #### `list-notes`
@@ -91,7 +117,7 @@ Count indexed notes.
91
117
  List all Apple Notes folders.
92
118
 
93
119
  #### `get-note`
94
- Get a note's full content by title.
120
+ Get note content by title.
95
121
 
96
122
  ```
97
123
  title: "My Note" # or "Work/My Note" for disambiguation
@@ -100,13 +126,15 @@ title: "My Note" # or "Work/My Note" for disambiguation
100
126
  ### Indexing
101
127
 
102
128
  #### `index-notes`
103
- Index all notes for semantic search.
129
+ Index notes for semantic search.
104
130
 
105
131
  ```
106
132
  mode: "incremental" # incremental (default) or full
107
133
  force: false # force reindex even if TTL hasn't expired
108
134
  ```
109
135
 
136
+ Use `mode: "full"` to create the chunk index for better long-note search. First full index takes longer as it generates chunks, but subsequent searches run fast.
137
+
110
138
  #### `reindex-note`
111
139
  Re-index a single note after manual edits.
112
140
 
@@ -150,16 +178,65 @@ title: "My Note"
150
178
  folder: "Archive"
151
179
  ```
152
180
 
181
+ ### Knowledge Graph
182
+
183
+ #### `list-tags`
184
+ List all tags with occurrence counts.
185
+
186
+ #### `search-by-tag`
187
+ Find notes with a specific tag.
188
+
189
+ ```
190
+ tag: "project"
191
+ folder: "Work" # optional
192
+ limit: 20 # default: 20
193
+ ```
194
+
195
+ #### `related-notes`
196
+ Find notes related to a source note.
197
+
198
+ ```
199
+ title: "My Note"
200
+ types: ["tag", "link", "similar"] # default: all
201
+ limit: 10 # default: 10
202
+ ```
203
+
204
+ #### `export-graph`
205
+ Export knowledge graph for visualization.
206
+
207
+ ```
208
+ format: "json" # json or graphml
209
+ folder: "Work" # optional filter
210
+ ```
211
+
212
+ **Supported Formats:**
213
+ - `json` - For custom visualization (D3.js, web apps)
214
+ - `graphml` - For professional tools (Gephi, yEd, Cytoscape)
215
+
153
216
  ## Claude Code Setup
154
217
 
155
- ### Automatic (via setup wizard)
218
+ ### Automatic (recommended)
156
219
 
157
- Run `bun run setup` and select "Add to Claude Code configuration".
220
+ The setup wizard automatically adds apple-notes-mcp to Claude Code. Run `apple-notes-mcp` after installation.
158
221
 
159
222
  ### Manual
160
223
 
161
224
  Add to `~/.claude.json`:
162
225
 
226
+ For npm installation:
227
+ ```json
228
+ {
229
+ "mcpServers": {
230
+ "apple-notes": {
231
+ "command": "apple-notes-mcp",
232
+ "args": [],
233
+ "env": {}
234
+ }
235
+ }
236
+ }
237
+ ```
238
+
239
+ For source installation:
163
240
  ```json
164
241
  {
165
242
  "mcpServers": {
@@ -205,6 +282,9 @@ Ensure Apple Notes runs and contains notes. Grant automation permissions when pr
205
282
  # Type check
206
283
  bun run check
207
284
 
285
+ # Run tests
286
+ bun run test
287
+
208
288
  # Run with debug logging
209
289
  DEBUG=true bun run start
210
290
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@disco_trooper/apple-notes-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for Apple Notes with semantic search and CRUD operations",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -13,15 +13,16 @@
13
13
  "test:watch": "vitest"
14
14
  },
15
15
  "dependencies": {
16
- "@modelcontextprotocol/sdk": "^1.0.0",
17
- "@lancedb/lancedb": "^0.13.0",
16
+ "@clack/prompts": "^0.8.0",
18
17
  "@huggingface/transformers": "^3.0.0",
19
- "turndown": "^7.2.0",
18
+ "@lancedb/lancedb": "^0.13.0",
19
+ "@modelcontextprotocol/sdk": "^1.0.0",
20
+ "apache-arrow": "^21.1.0",
21
+ "dotenv": "^16.4.0",
20
22
  "marked": "^15.0.0",
21
23
  "run-jxa": "^3.0.0",
22
- "zod": "^3.24.0",
23
- "@clack/prompts": "^0.8.0",
24
- "dotenv": "^16.4.0"
24
+ "turndown": "^7.2.0",
25
+ "zod": "^3.24.0"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@types/bun": "^1.1.0",
@@ -39,7 +40,8 @@
39
40
  "author": "disco-trooper",
40
41
  "license": "MIT",
41
42
  "bin": {
42
- "apple-notes-mcp": "./src/index.ts"
43
+ "apple-notes-mcp": "./src/index.ts",
44
+ "apple-notes-mcp-setup": "./src/setup.ts"
43
45
  },
44
46
  "files": [
45
47
  "src",
@@ -0,0 +1,47 @@
1
+ // src/config/claude.test.ts
2
+ import { describe, it, expect, vi, beforeEach } from "vitest";
3
+
4
+ // Mock paths.js before importing claude.ts
5
+ vi.mock("./paths.js", () => ({
6
+ isNpmInstall: vi.fn(),
7
+ getProjectRoot: vi.fn(() => "/mock/project"),
8
+ }));
9
+
10
+ describe("claude config", () => {
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ vi.resetModules();
14
+ });
15
+
16
+ describe("getClaudeConfigEntry", () => {
17
+ it("should return npm command when npm install", async () => {
18
+ const { isNpmInstall } = await import("./paths.js");
19
+ vi.mocked(isNpmInstall).mockReturnValue(true);
20
+
21
+ const { getClaudeConfigEntry } = await import("./claude.js");
22
+ const entry = getClaudeConfigEntry();
23
+
24
+ expect(entry.command).toBe("apple-notes-mcp");
25
+ expect(entry.args).toEqual([]);
26
+ });
27
+
28
+ it("should return bun command when source install", async () => {
29
+ const { isNpmInstall } = await import("./paths.js");
30
+ vi.mocked(isNpmInstall).mockReturnValue(false);
31
+
32
+ const { getClaudeConfigEntry } = await import("./claude.js");
33
+ const entry = getClaudeConfigEntry();
34
+
35
+ expect(entry.command).toBe("bun");
36
+ expect(entry.args[0]).toBe("run");
37
+ expect(entry.args[1]).toContain("/mock/project/src/index.ts");
38
+ });
39
+ });
40
+
41
+ describe("getClaudeConfigPath", () => {
42
+ it("should return path to ~/.claude.json", async () => {
43
+ const { getClaudeConfigPath } = await import("./claude.js");
44
+ expect(getClaudeConfigPath()).toMatch(/\.claude\.json$/);
45
+ });
46
+ });
47
+ });
@@ -0,0 +1,106 @@
1
+ // src/config/claude.ts
2
+ /**
3
+ * Claude Code configuration management.
4
+ */
5
+
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import * as os from "node:os";
9
+ import { isNpmInstall, getProjectRoot } from "./paths.js";
10
+
11
+ const CLAUDE_CONFIG_PATH = path.join(os.homedir(), ".claude.json");
12
+
13
+ interface ClaudeServerEntry {
14
+ command: string;
15
+ args: string[];
16
+ env: Record<string, string>;
17
+ }
18
+
19
+ interface ClaudeConfig {
20
+ mcpServers?: Record<string, ClaudeServerEntry>;
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ /**
25
+ * Generate the appropriate MCP server entry based on install method.
26
+ */
27
+ export function getClaudeConfigEntry(): ClaudeServerEntry {
28
+ if (isNpmInstall()) {
29
+ return {
30
+ command: "apple-notes-mcp",
31
+ args: [],
32
+ env: {},
33
+ };
34
+ } else {
35
+ const projectRoot = getProjectRoot();
36
+ return {
37
+ command: "bun",
38
+ args: ["run", path.join(projectRoot, "src", "index.ts")],
39
+ env: {},
40
+ };
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Read existing Claude config.
46
+ */
47
+ export function readClaudeConfig(): ClaudeConfig | null {
48
+ if (!fs.existsSync(CLAUDE_CONFIG_PATH)) {
49
+ return null;
50
+ }
51
+
52
+ try {
53
+ const content = fs.readFileSync(CLAUDE_CONFIG_PATH, "utf-8");
54
+ return JSON.parse(content);
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Write Claude config with our MCP server entry.
62
+ */
63
+ export function writeClaudeConfig(entry: ClaudeServerEntry): boolean {
64
+ let config = readClaudeConfig();
65
+
66
+ if (!config) {
67
+ config = {
68
+ mcpServers: {
69
+ "apple-notes": entry,
70
+ },
71
+ };
72
+ } else {
73
+ const mcpServers = (config.mcpServers || {}) as Record<string, ClaudeServerEntry>;
74
+ mcpServers["apple-notes"] = entry;
75
+ config.mcpServers = mcpServers;
76
+ }
77
+
78
+ try {
79
+ fs.writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
80
+ return true;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Check if existing Claude config uses different install method.
88
+ * Returns null if no existing config, otherwise returns the method.
89
+ */
90
+ export function getExistingInstallMethod(): "npm" | "source" | null {
91
+ const config = readClaudeConfig();
92
+ const entry = config?.mcpServers?.["apple-notes"];
93
+
94
+ if (!entry) {
95
+ return null;
96
+ }
97
+
98
+ return entry.command === "apple-notes-mcp" ? "npm" : "source";
99
+ }
100
+
101
+ /**
102
+ * Get Claude config path for display.
103
+ */
104
+ export function getClaudeConfigPath(): string {
105
+ return CLAUDE_CONFIG_PATH;
106
+ }
@@ -32,10 +32,19 @@ export const ERROR_MESSAGE_MAX_LENGTH = 200;
32
32
  export const MAX_RETRIES = 3;
33
33
  export const RATE_LIMIT_BACKOFF_BASE_MS = 2000;
34
34
 
35
- // Indexing
36
- export const EMBEDDING_DELAY_MS = 300;
35
+ // Indexing (EMBEDDING_DELAY_MS removed - not needed with batch processing)
37
36
 
38
37
  // Search tuning
39
38
  export const HYBRID_SEARCH_MIN_FETCH = 40;
40
39
  export const FOLDER_FILTER_MULTIPLIER = 3;
41
40
  export const PREVIEW_TRUNCATE_RATIO = 0.7;
41
+
42
+ // Knowledge Graph
43
+ export const DEFAULT_RELATED_NOTES_LIMIT = 10;
44
+ export const GRAPH_TAG_WEIGHT = 0.8;
45
+ export const GRAPH_LINK_WEIGHT = 1.0;
46
+ export const GRAPH_SIMILAR_WEIGHT = 0.5;
47
+
48
+ // Chunking settings
49
+ export const DEFAULT_CHUNK_SIZE = 500;
50
+ export const DEFAULT_CHUNK_OVERLAP = 100;
@@ -0,0 +1,40 @@
1
+ // src/config/paths.test.ts
2
+ import { describe, it, expect } from "vitest";
3
+
4
+ describe("config paths", () => {
5
+ describe("getConfigDir", () => {
6
+ it("should return ~/.apple-notes-mcp", async () => {
7
+ const { getConfigDir } = await import("./paths.js");
8
+ expect(getConfigDir()).toMatch(/\.apple-notes-mcp$/);
9
+ });
10
+ });
11
+
12
+ describe("getEnvPath", () => {
13
+ it("should return config dir + .env", async () => {
14
+ const { getEnvPath, getConfigDir } = await import("./paths.js");
15
+ expect(getEnvPath()).toBe(`${getConfigDir()}/.env`);
16
+ });
17
+ });
18
+
19
+ describe("getDataDir", () => {
20
+ it("should return config dir + data", async () => {
21
+ const { getDataDir, getConfigDir } = await import("./paths.js");
22
+ expect(getDataDir()).toBe(`${getConfigDir()}/data`);
23
+ });
24
+ });
25
+
26
+ describe("isNpmInstall", () => {
27
+ it("should detect npm install from path", async () => {
28
+ const { isNpmInstall } = await import("./paths.js");
29
+ // Running from source, should be false
30
+ expect(isNpmInstall()).toBe(false);
31
+ });
32
+ });
33
+
34
+ describe("getProjectRoot", () => {
35
+ it("should return project root directory", async () => {
36
+ const { getProjectRoot } = await import("./paths.js");
37
+ expect(getProjectRoot()).toMatch(/apple-notes-mcp$/);
38
+ });
39
+ });
40
+ });
@@ -0,0 +1,86 @@
1
+ // src/config/paths.ts
2
+ /**
3
+ * Unified configuration paths for both source and npm installations.
4
+ * All user config lives in ~/.apple-notes-mcp/ regardless of install method.
5
+ */
6
+
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import * as os from "node:os";
10
+
11
+ /**
12
+ * Get the config directory path.
13
+ * Always ~/.apple-notes-mcp/ for consistency across install methods.
14
+ */
15
+ export function getConfigDir(): string {
16
+ return path.join(os.homedir(), ".apple-notes-mcp");
17
+ }
18
+
19
+ /**
20
+ * Get the .env file path.
21
+ */
22
+ export function getEnvPath(): string {
23
+ return path.join(getConfigDir(), ".env");
24
+ }
25
+
26
+ /**
27
+ * Get the data directory path (for LanceDB).
28
+ */
29
+ export function getDataDir(): string {
30
+ return path.join(getConfigDir(), "data");
31
+ }
32
+
33
+ /**
34
+ * Check if configuration exists.
35
+ */
36
+ export function hasConfig(): boolean {
37
+ return fs.existsSync(getEnvPath());
38
+ }
39
+
40
+ /**
41
+ * Ensure config directory exists.
42
+ */
43
+ export function ensureConfigDir(): void {
44
+ const configDir = getConfigDir();
45
+ if (!fs.existsSync(configDir)) {
46
+ fs.mkdirSync(configDir, { recursive: true });
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Detect if running from npm global install vs source.
52
+ * Used to generate appropriate Claude Code config.
53
+ */
54
+ export function isNpmInstall(): boolean {
55
+ const scriptPath = new URL(import.meta.url).pathname;
56
+ return (
57
+ scriptPath.includes("/node_modules/") ||
58
+ scriptPath.includes("/.npm/") ||
59
+ // Global npm install on macOS
60
+ scriptPath.includes("/lib/node_modules/")
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Get the project root directory (for source installs).
66
+ */
67
+ export function getProjectRoot(): string {
68
+ // Navigate up from src/config/paths.ts to project root
69
+ return path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
70
+ }
71
+
72
+ /**
73
+ * Check if legacy config exists in project root.
74
+ * Used for migration from old setup.
75
+ */
76
+ export function hasLegacyConfig(): boolean {
77
+ const legacyPath = path.join(getProjectRoot(), ".env");
78
+ return fs.existsSync(legacyPath) && !isNpmInstall();
79
+ }
80
+
81
+ /**
82
+ * Get legacy config path for migration.
83
+ */
84
+ export function getLegacyEnvPath(): string {
85
+ return path.join(getProjectRoot(), ".env");
86
+ }
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import { LanceDBStore } from "./lancedb.js";
6
+
7
+ describe("Arrow type inference fix", () => {
8
+ let tempDir: string;
9
+ let store: LanceDBStore;
10
+
11
+ beforeEach(() => {
12
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "arrow-test-"));
13
+ store = new LanceDBStore(tempDir);
14
+ });
15
+
16
+ afterEach(() => {
17
+ fs.rmSync(tempDir, { recursive: true, force: true });
18
+ });
19
+
20
+ it("handles records where ALL tags and outlinks are empty", async () => {
21
+ // This is the exact scenario that triggers the Arrow bug
22
+ const records = [
23
+ {
24
+ id: "1",
25
+ title: "Note 1",
26
+ content: "Hello world",
27
+ vector: new Array(384).fill(0.1),
28
+ folder: "Notes",
29
+ created: new Date().toISOString(),
30
+ modified: new Date().toISOString(),
31
+ indexed_at: new Date().toISOString(),
32
+ tags: [], // Empty!
33
+ outlinks: [], // Empty!
34
+ },
35
+ {
36
+ id: "2",
37
+ title: "Note 2",
38
+ content: "Test content",
39
+ vector: new Array(384).fill(0.2),
40
+ folder: "Notes",
41
+ created: new Date().toISOString(),
42
+ modified: new Date().toISOString(),
43
+ indexed_at: new Date().toISOString(),
44
+ tags: [], // Empty!
45
+ outlinks: [], // Empty!
46
+ },
47
+ ];
48
+
49
+ // This should NOT throw "Cannot infer list vector from empty array"
50
+ await store.index(records);
51
+
52
+ // Verify data was stored
53
+ const count = await store.count();
54
+ expect(count).toBe(2);
55
+
56
+ // Verify tags are empty (placeholders removed)
57
+ const all = await store.getAll();
58
+ expect(all[0].tags).toEqual([]);
59
+ expect(all[0].outlinks).toEqual([]);
60
+ });
61
+
62
+ it("handles mixed empty and non-empty arrays", async () => {
63
+ const records = [
64
+ {
65
+ id: "1",
66
+ title: "Note with tags",
67
+ content: "Hello",
68
+ vector: new Array(384).fill(0.1),
69
+ folder: "Notes",
70
+ created: new Date().toISOString(),
71
+ modified: new Date().toISOString(),
72
+ indexed_at: new Date().toISOString(),
73
+ tags: ["tag1", "tag2"],
74
+ outlinks: [],
75
+ },
76
+ {
77
+ id: "2",
78
+ title: "Note without tags",
79
+ content: "World",
80
+ vector: new Array(384).fill(0.2),
81
+ folder: "Notes",
82
+ created: new Date().toISOString(),
83
+ modified: new Date().toISOString(),
84
+ indexed_at: new Date().toISOString(),
85
+ tags: [],
86
+ outlinks: ["Note 1"],
87
+ },
88
+ ];
89
+
90
+ await store.index(records);
91
+
92
+ const all = await store.getAll();
93
+ const note1 = all.find(n => n.id === "1");
94
+ const note2 = all.find(n => n.id === "2");
95
+
96
+ expect(note1?.tags).toEqual(["tag1", "tag2"]);
97
+ expect(note1?.outlinks).toEqual([]);
98
+ expect(note2?.tags).toEqual([]);
99
+ expect(note2?.outlinks).toEqual(["Note 1"]);
100
+ });
101
+ });