@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.
- package/README.md +104 -24
- package/package.json +11 -12
- package/src/config/claude.test.ts +47 -0
- package/src/config/claude.ts +106 -0
- package/src/config/constants.ts +11 -2
- package/src/config/paths.test.ts +40 -0
- package/src/config/paths.ts +86 -0
- package/src/db/arrow-fix.test.ts +101 -0
- package/src/db/lancedb.test.ts +254 -2
- package/src/db/lancedb.ts +385 -38
- package/src/embeddings/cache.test.ts +150 -0
- package/src/embeddings/cache.ts +204 -0
- package/src/embeddings/index.ts +22 -4
- package/src/embeddings/local.ts +57 -17
- package/src/embeddings/openrouter.ts +233 -11
- package/src/errors/index.test.ts +64 -0
- package/src/errors/index.ts +62 -0
- package/src/graph/export.test.ts +81 -0
- package/src/graph/export.ts +163 -0
- package/src/graph/extract.test.ts +90 -0
- package/src/graph/extract.ts +52 -0
- package/src/graph/queries.test.ts +156 -0
- package/src/graph/queries.ts +224 -0
- package/src/index.ts +309 -23
- package/src/notes/conversion.ts +62 -0
- package/src/notes/crud.test.ts +41 -8
- package/src/notes/crud.ts +75 -64
- package/src/notes/read.test.ts +58 -3
- package/src/notes/read.ts +142 -210
- package/src/notes/resolve.ts +174 -0
- package/src/notes/tables.ts +69 -40
- package/src/search/chunk-indexer.test.ts +353 -0
- package/src/search/chunk-indexer.ts +207 -0
- package/src/search/chunk-search.test.ts +327 -0
- package/src/search/chunk-search.ts +298 -0
- package/src/search/index.ts +4 -6
- package/src/search/indexer.ts +164 -109
- package/src/setup.ts +46 -67
- package/src/types/index.ts +4 -0
- package/src/utils/chunker.test.ts +182 -0
- package/src/utils/chunker.ts +170 -0
- package/src/utils/content-filter.test.ts +225 -0
- package/src/utils/content-filter.ts +275 -0
- package/src/utils/debug.ts +0 -2
- package/src/utils/runtime.test.ts +70 -0
- package/src/utils/runtime.ts +40 -0
- package/src/utils/text.test.ts +32 -0
- package/CLAUDE.md +0 -56
- package/src/server.ts +0 -427
package/README.md
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
# apple-notes-mcp
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@disco_trooper/apple-notes-mcp)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.apple.com/macos/)
|
|
6
|
+
[](https://bun.sh)
|
|
7
|
+
[](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
|
|
12
|
-
- **Claude Code Integration** - Works with Claude Code CLI
|
|
20
|
+
- **Dual Embedding** - Local HuggingFace or OpenRouter API
|
|
13
21
|
|
|
14
|
-
##
|
|
22
|
+
## What's New in 1.3
|
|
15
23
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
|
61
|
+
Run the command after installation:
|
|
39
62
|
|
|
40
63
|
```bash
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
83
|
-
mode: "hybrid"
|
|
84
|
-
include_content: false
|
|
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
|
|
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
|
|
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 (
|
|
218
|
+
### Automatic (recommended)
|
|
156
219
|
|
|
157
|
-
|
|
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,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@disco_trooper/apple-notes-mcp",
|
|
3
|
-
"version": "1.
|
|
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",
|
|
7
|
-
"module": "src/server.ts",
|
|
8
7
|
"scripts": {
|
|
9
8
|
"start": "bun run src/index.ts",
|
|
10
9
|
"setup": "bun run src/setup.ts",
|
|
@@ -14,18 +13,18 @@
|
|
|
14
13
|
"test:watch": "vitest"
|
|
15
14
|
},
|
|
16
15
|
"dependencies": {
|
|
17
|
-
"@
|
|
18
|
-
"@lancedb/lancedb": "^0.13.0",
|
|
16
|
+
"@clack/prompts": "^0.8.0",
|
|
19
17
|
"@huggingface/transformers": "^3.0.0",
|
|
20
|
-
"
|
|
18
|
+
"@lancedb/lancedb": "^0.13.0",
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
20
|
+
"apache-arrow": "^21.1.0",
|
|
21
|
+
"dotenv": "^16.4.0",
|
|
21
22
|
"marked": "^15.0.0",
|
|
22
23
|
"run-jxa": "^3.0.0",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"dotenv": "^16.4.0"
|
|
24
|
+
"turndown": "^7.2.0",
|
|
25
|
+
"zod": "^3.24.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@smithery/cli": "^3.0.3",
|
|
29
28
|
"@types/bun": "^1.1.0",
|
|
30
29
|
"@types/turndown": "^5.0.0",
|
|
31
30
|
"typescript": "^5.7.0",
|
|
@@ -41,12 +40,12 @@
|
|
|
41
40
|
"author": "disco-trooper",
|
|
42
41
|
"license": "MIT",
|
|
43
42
|
"bin": {
|
|
44
|
-
"apple-notes-mcp": "./src/index.ts"
|
|
43
|
+
"apple-notes-mcp": "./src/index.ts",
|
|
44
|
+
"apple-notes-mcp-setup": "./src/setup.ts"
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
47
47
|
"src",
|
|
48
|
-
"README.md"
|
|
49
|
-
"CLAUDE.md"
|
|
48
|
+
"README.md"
|
|
50
49
|
],
|
|
51
50
|
"engines": {
|
|
52
51
|
"node": ">=18"
|
|
@@ -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
|
+
}
|
package/src/config/constants.ts
CHANGED
|
@@ -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
|
+
});
|