@disco_trooper/apple-notes-mcp 1.2.0 → 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.
Files changed (39) hide show
  1. package/README.md +136 -24
  2. package/package.json +13 -9
  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 +209 -2
  10. package/src/db/lancedb.ts +373 -7
  11. package/src/embeddings/cache.test.ts +150 -0
  12. package/src/embeddings/cache.ts +204 -0
  13. package/src/embeddings/index.ts +21 -2
  14. package/src/embeddings/local.ts +61 -10
  15. package/src/embeddings/openrouter.ts +233 -11
  16. package/src/graph/export.test.ts +81 -0
  17. package/src/graph/export.ts +163 -0
  18. package/src/graph/extract.test.ts +90 -0
  19. package/src/graph/extract.ts +52 -0
  20. package/src/graph/queries.test.ts +156 -0
  21. package/src/graph/queries.ts +224 -0
  22. package/src/index.ts +376 -10
  23. package/src/notes/crud.test.ts +148 -3
  24. package/src/notes/crud.ts +250 -5
  25. package/src/notes/read.ts +83 -68
  26. package/src/search/chunk-indexer.test.ts +353 -0
  27. package/src/search/chunk-indexer.ts +254 -0
  28. package/src/search/chunk-search.test.ts +327 -0
  29. package/src/search/chunk-search.ts +298 -0
  30. package/src/search/indexer.ts +151 -109
  31. package/src/search/refresh.test.ts +173 -0
  32. package/src/search/refresh.ts +151 -0
  33. package/src/setup.ts +46 -67
  34. package/src/utils/chunker.test.ts +182 -0
  35. package/src/utils/chunker.ts +170 -0
  36. package/src/utils/content-filter.test.ts +225 -0
  37. package/src/utils/content-filter.ts +275 -0
  38. package/src/utils/runtime.test.ts +70 -0
  39. package/src/utils/runtime.ts +40 -0
package/README.md CHANGED
@@ -1,21 +1,32 @@
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.4
15
23
 
16
- - macOS (uses Apple Notes via JXA)
17
- - [Bun](https://bun.sh) runtime
18
- - Apple Notes app with notes
24
+ - **Smart Refresh** - Search auto-reindexes changed notes. No manual `index-notes` needed.
25
+ - **Batch Operations** - Delete or move multiple notes by title or folder.
26
+ - **Purge Index** - Clear all indexed data when switching models or fixing corruption.
27
+ - **Parent Document Retriever** - Splits long notes into 500-char chunks with 100-char overlap.
28
+ - **60x faster cached queries** - Query embedding cache eliminates redundant API calls.
29
+ - **4-6x faster indexing** - Parallel processing and batch embeddings.
19
30
 
20
31
  ## Installation
21
32
 
@@ -23,34 +34,43 @@ MCP server for Apple Notes with semantic search and CRUD operations. Claude sear
23
34
 
24
35
  ```bash
25
36
  npm install -g @disco_trooper/apple-notes-mcp
37
+ apple-notes-mcp
26
38
  ```
27
39
 
40
+ The setup wizard guides you through:
41
+ 1. Choosing your embedding provider (local or OpenRouter)
42
+ 2. Configuring API keys if needed
43
+ 3. Setting up Claude Code integration
44
+ 4. Indexing your notes
45
+
28
46
  ### From source
29
47
 
30
48
  ```bash
31
49
  git clone https://github.com/disco-trooper/apple-notes-mcp.git
32
50
  cd apple-notes-mcp
33
51
  bun install
52
+ bun run start
34
53
  ```
35
54
 
55
+ ## Requirements
56
+
57
+ - macOS (uses Apple Notes via JXA)
58
+ - [Bun](https://bun.sh) runtime
59
+ - Apple Notes app with notes
60
+
36
61
  ## Quick Start
37
62
 
38
- Run the setup wizard:
63
+ Run the command after installation:
39
64
 
40
65
  ```bash
41
- bun run setup
66
+ apple-notes-mcp
42
67
  ```
43
68
 
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
69
+ The setup wizard starts automatically on first run. Restart Claude Code after setup to use the MCP tools.
50
70
 
51
71
  ## Configuration
52
72
 
53
- Store configuration in `.env`:
73
+ Configuration stored in `~/.apple-notes-mcp/.env`:
54
74
 
55
75
  | Variable | Description | Default |
56
76
  |----------|-------------|---------|
@@ -61,6 +81,14 @@ Store configuration in `.env`:
61
81
  | `INDEX_TTL` | Auto-reindex interval in seconds | - |
62
82
  | `DEBUG` | Enable debug logging | `false` |
63
83
 
84
+ To reconfigure:
85
+
86
+ ```bash
87
+ apple-notes-mcp setup
88
+ # or from source:
89
+ bun run setup
90
+ ```
91
+
64
92
  ### Embedding Providers
65
93
 
66
94
  **Local (default)**: Uses HuggingFace Transformers with `Xenova/multilingual-e5-small`. Free, runs locally, ~200MB download.
@@ -74,14 +102,14 @@ See [docs/models.md](docs/models.md) for model comparison.
74
102
  ### Search & Discovery
75
103
 
76
104
  #### `search-notes`
77
- Search notes with hybrid vector + fulltext search.
105
+ Hybrid vector + fulltext search.
78
106
 
79
107
  ```
80
108
  query: "meeting notes from last week"
81
109
  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
110
+ limit: 10 # default: 20
111
+ mode: "hybrid" # hybrid, keyword, or semantic
112
+ include_content: false # include full content vs preview
85
113
  ```
86
114
 
87
115
  #### `list-notes`
@@ -91,7 +119,7 @@ Count indexed notes.
91
119
  List all Apple Notes folders.
92
120
 
93
121
  #### `get-note`
94
- Get a note's full content by title.
122
+ Get note content by title.
95
123
 
96
124
  ```
97
125
  title: "My Note" # or "Work/My Note" for disambiguation
@@ -100,13 +128,15 @@ title: "My Note" # or "Work/My Note" for disambiguation
100
128
  ### Indexing
101
129
 
102
130
  #### `index-notes`
103
- Index all notes for semantic search.
131
+ Index notes for semantic search.
104
132
 
105
133
  ```
106
134
  mode: "incremental" # incremental (default) or full
107
135
  force: false # force reindex even if TTL hasn't expired
108
136
  ```
109
137
 
138
+ 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.
139
+
110
140
  #### `reindex-note`
111
141
  Re-index a single note after manual edits.
112
142
 
@@ -150,16 +180,92 @@ title: "My Note"
150
180
  folder: "Archive"
151
181
  ```
152
182
 
183
+ #### `batch-delete`
184
+ Delete multiple notes at once.
185
+
186
+ ```
187
+ titles: ["Note 1", "Note 2"] # OR folder: "Old Project"
188
+ confirm: true # required for safety
189
+ ```
190
+
191
+ #### `batch-move`
192
+ Move multiple notes to a target folder.
193
+
194
+ ```
195
+ titles: ["Note 1", "Note 2"] # OR sourceFolder: "Old"
196
+ targetFolder: "Archive" # required
197
+ ```
198
+
199
+ ### Index Management
200
+
201
+ #### `purge-index`
202
+ Clear all indexed data. Use when switching embedding models or to fix corrupted index.
203
+
204
+ ```
205
+ confirm: true # required for safety
206
+ ```
207
+
208
+ After purging, run `index-notes` to rebuild.
209
+
210
+ ### Knowledge Graph
211
+
212
+ #### `list-tags`
213
+ List all tags with occurrence counts.
214
+
215
+ #### `search-by-tag`
216
+ Find notes with a specific tag.
217
+
218
+ ```
219
+ tag: "project"
220
+ folder: "Work" # optional
221
+ limit: 20 # default: 20
222
+ ```
223
+
224
+ #### `related-notes`
225
+ Find notes related to a source note.
226
+
227
+ ```
228
+ title: "My Note"
229
+ types: ["tag", "link", "similar"] # default: all
230
+ limit: 10 # default: 10
231
+ ```
232
+
233
+ #### `export-graph`
234
+ Export knowledge graph for visualization.
235
+
236
+ ```
237
+ format: "json" # json or graphml
238
+ folder: "Work" # optional filter
239
+ ```
240
+
241
+ **Supported Formats:**
242
+ - `json` - For custom visualization (D3.js, web apps)
243
+ - `graphml` - For professional tools (Gephi, yEd, Cytoscape)
244
+
153
245
  ## Claude Code Setup
154
246
 
155
- ### Automatic (via setup wizard)
247
+ ### Automatic (recommended)
156
248
 
157
- Run `bun run setup` and select "Add to Claude Code configuration".
249
+ The setup wizard automatically adds apple-notes-mcp to Claude Code. Run `apple-notes-mcp` after installation.
158
250
 
159
251
  ### Manual
160
252
 
161
253
  Add to `~/.claude.json`:
162
254
 
255
+ For npm installation:
256
+ ```json
257
+ {
258
+ "mcpServers": {
259
+ "apple-notes": {
260
+ "command": "apple-notes-mcp",
261
+ "args": [],
262
+ "env": {}
263
+ }
264
+ }
265
+ }
266
+ ```
267
+
268
+ For source installation:
163
269
  ```json
164
270
  {
165
271
  "mcpServers": {
@@ -205,6 +311,12 @@ Ensure Apple Notes runs and contains notes. Grant automation permissions when pr
205
311
  # Type check
206
312
  bun run check
207
313
 
314
+ # Run tests
315
+ bun run test
316
+
317
+ # Run with coverage
318
+ bun run test:coverage
319
+
208
320
  # Run with debug logging
209
321
  DEBUG=true bun run start
210
322
 
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.4.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",
@@ -10,22 +10,25 @@
10
10
  "dev": "bun --watch run src/index.ts",
11
11
  "check": "bun run tsc --noEmit",
12
12
  "test": "vitest run",
13
- "test:watch": "vitest"
13
+ "test:watch": "vitest",
14
+ "test:coverage": "vitest run --coverage"
14
15
  },
15
16
  "dependencies": {
16
- "@modelcontextprotocol/sdk": "^1.0.0",
17
- "@lancedb/lancedb": "^0.13.0",
17
+ "@clack/prompts": "^0.8.0",
18
18
  "@huggingface/transformers": "^3.0.0",
19
- "turndown": "^7.2.0",
19
+ "@lancedb/lancedb": "^0.13.0",
20
+ "@modelcontextprotocol/sdk": "^1.0.0",
21
+ "apache-arrow": "^21.1.0",
22
+ "dotenv": "^16.4.0",
20
23
  "marked": "^15.0.0",
21
24
  "run-jxa": "^3.0.0",
22
- "zod": "^3.24.0",
23
- "@clack/prompts": "^0.8.0",
24
- "dotenv": "^16.4.0"
25
+ "turndown": "^7.2.0",
26
+ "zod": "^3.24.0"
25
27
  },
26
28
  "devDependencies": {
27
29
  "@types/bun": "^1.1.0",
28
30
  "@types/turndown": "^5.0.0",
31
+ "@vitest/coverage-v8": "^4.0.16",
29
32
  "typescript": "^5.7.0",
30
33
  "vitest": "^4.0.16"
31
34
  },
@@ -39,7 +42,8 @@
39
42
  "author": "disco-trooper",
40
43
  "license": "MIT",
41
44
  "bin": {
42
- "apple-notes-mcp": "./src/index.ts"
45
+ "apple-notes-mcp": "./src/index.ts",
46
+ "apple-notes-mcp-setup": "./src/setup.ts"
43
47
  },
44
48
  "files": [
45
49
  "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
+ }