@disco_trooper/apple-notes-mcp 1.0.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/CLAUDE.md +56 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/package.json +61 -0
- package/src/config/constants.ts +41 -0
- package/src/config/env.test.ts +58 -0
- package/src/config/env.ts +25 -0
- package/src/db/lancedb.test.ts +141 -0
- package/src/db/lancedb.ts +263 -0
- package/src/db/validation.test.ts +76 -0
- package/src/db/validation.ts +57 -0
- package/src/embeddings/index.test.ts +54 -0
- package/src/embeddings/index.ts +111 -0
- package/src/embeddings/local.test.ts +70 -0
- package/src/embeddings/local.ts +191 -0
- package/src/embeddings/openrouter.test.ts +21 -0
- package/src/embeddings/openrouter.ts +285 -0
- package/src/index.ts +387 -0
- package/src/notes/crud.test.ts +199 -0
- package/src/notes/crud.ts +257 -0
- package/src/notes/read.test.ts +131 -0
- package/src/notes/read.ts +504 -0
- package/src/search/index.test.ts +52 -0
- package/src/search/index.ts +283 -0
- package/src/search/indexer.test.ts +42 -0
- package/src/search/indexer.ts +335 -0
- package/src/server.ts +386 -0
- package/src/setup.ts +540 -0
- package/src/types/index.ts +39 -0
- package/src/utils/debug.test.ts +41 -0
- package/src/utils/debug.ts +51 -0
- package/src/utils/errors.test.ts +29 -0
- package/src/utils/errors.ts +46 -0
- package/src/utils/text.ts +23 -0
package/src/setup.ts
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Setup wizard for apple-notes-mcp
|
|
4
|
+
*
|
|
5
|
+
* Interactive CLI for configuring:
|
|
6
|
+
* - Embedding provider (Local HuggingFace / OpenRouter)
|
|
7
|
+
* - API keys
|
|
8
|
+
* - Read-only mode
|
|
9
|
+
* - Auto-indexing settings
|
|
10
|
+
* - Claude Code integration
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as p from "@clack/prompts";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
// Paths
|
|
17
|
+
const PROJECT_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
18
|
+
const ENV_FILE = path.join(PROJECT_DIR, "..", ".env");
|
|
19
|
+
const CLAUDE_CONFIG_PATH = path.join(
|
|
20
|
+
process.env.HOME || "~",
|
|
21
|
+
".claude.json"
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
interface Config {
|
|
25
|
+
provider: "local" | "openrouter";
|
|
26
|
+
openrouterApiKey?: string;
|
|
27
|
+
embeddingModel?: string;
|
|
28
|
+
embeddingDims?: number;
|
|
29
|
+
readonlyMode: boolean;
|
|
30
|
+
autoIndex: "none" | "on-search" | "ttl";
|
|
31
|
+
indexTtl?: number;
|
|
32
|
+
debug: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read existing .env file if it exists
|
|
37
|
+
*/
|
|
38
|
+
function readExistingEnv(): Record<string, string> {
|
|
39
|
+
if (!fs.existsSync(ENV_FILE)) {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const content = fs.readFileSync(ENV_FILE, "utf-8");
|
|
44
|
+
const env: Record<string, string> = {};
|
|
45
|
+
|
|
46
|
+
for (const line of content.split("\n")) {
|
|
47
|
+
const trimmed = line.trim();
|
|
48
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
49
|
+
|
|
50
|
+
const eqIndex = trimmed.indexOf("=");
|
|
51
|
+
if (eqIndex === -1) continue;
|
|
52
|
+
|
|
53
|
+
const key = trimmed.slice(0, eqIndex);
|
|
54
|
+
let value = trimmed.slice(eqIndex + 1);
|
|
55
|
+
|
|
56
|
+
// Remove quotes if present
|
|
57
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
58
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
59
|
+
value = value.slice(1, -1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
env[key] = value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return env;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Write configuration to .env file
|
|
70
|
+
*/
|
|
71
|
+
function writeEnvFile(config: Config): void {
|
|
72
|
+
const lines: string[] = [
|
|
73
|
+
"# apple-notes-mcp configuration",
|
|
74
|
+
"# Generated by setup wizard",
|
|
75
|
+
"",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
if (config.provider === "openrouter") {
|
|
79
|
+
lines.push("# Embedding provider: OpenRouter");
|
|
80
|
+
if (config.openrouterApiKey) {
|
|
81
|
+
lines.push(`OPENROUTER_API_KEY="${config.openrouterApiKey}"`);
|
|
82
|
+
}
|
|
83
|
+
if (config.embeddingModel) {
|
|
84
|
+
lines.push(`EMBEDDING_MODEL="${config.embeddingModel}"`);
|
|
85
|
+
}
|
|
86
|
+
if (config.embeddingDims) {
|
|
87
|
+
lines.push(`EMBEDDING_DIMS="${config.embeddingDims}"`);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
lines.push("# Embedding provider: Local HuggingFace");
|
|
91
|
+
lines.push("# No OPENROUTER_API_KEY = uses local embeddings");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
lines.push("");
|
|
95
|
+
|
|
96
|
+
if (config.readonlyMode) {
|
|
97
|
+
lines.push("# Read-only mode (no write operations)");
|
|
98
|
+
lines.push("READONLY_MODE=true");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
lines.push("");
|
|
102
|
+
|
|
103
|
+
if (config.autoIndex === "ttl" && config.indexTtl) {
|
|
104
|
+
lines.push("# Auto-index TTL in seconds");
|
|
105
|
+
lines.push(`INDEX_TTL=${config.indexTtl}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (config.debug) {
|
|
109
|
+
lines.push("");
|
|
110
|
+
lines.push("# Debug logging");
|
|
111
|
+
lines.push("DEBUG=true");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fs.writeFileSync(ENV_FILE, lines.join("\n") + "\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Read Claude Code config if it exists
|
|
119
|
+
*/
|
|
120
|
+
function readClaudeConfig(): Record<string, unknown> | null {
|
|
121
|
+
if (!fs.existsSync(CLAUDE_CONFIG_PATH)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const content = fs.readFileSync(CLAUDE_CONFIG_PATH, "utf-8");
|
|
127
|
+
return JSON.parse(content);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// Config doesn't exist or is invalid JSON
|
|
130
|
+
if (process.env.DEBUG === "true") {
|
|
131
|
+
console.error("[SETUP] Could not read Claude config:", error);
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Add MCP server to Claude Code config
|
|
139
|
+
*/
|
|
140
|
+
function addToClaudeConfig(): boolean {
|
|
141
|
+
const projectPath = path.resolve(PROJECT_DIR, "..");
|
|
142
|
+
const serverEntry = {
|
|
143
|
+
command: "bun",
|
|
144
|
+
args: ["run", path.join(projectPath, "src", "index.ts")],
|
|
145
|
+
env: {},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
let config = readClaudeConfig();
|
|
149
|
+
|
|
150
|
+
if (!config) {
|
|
151
|
+
// Create new config
|
|
152
|
+
config = {
|
|
153
|
+
mcpServers: {
|
|
154
|
+
"apple-notes": serverEntry,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
} else {
|
|
158
|
+
// Add to existing config
|
|
159
|
+
const mcpServers = (config.mcpServers || {}) as Record<string, unknown>;
|
|
160
|
+
mcpServers["apple-notes"] = serverEntry;
|
|
161
|
+
config.mcpServers = mcpServers;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
fs.writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
166
|
+
return true;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (process.env.DEBUG === "true") {
|
|
169
|
+
console.error("[SETUP] Failed to write Claude config:", error);
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Generate config snippet for manual setup
|
|
177
|
+
*/
|
|
178
|
+
function getConfigSnippet(): string {
|
|
179
|
+
const projectPath = path.resolve(PROJECT_DIR, "..");
|
|
180
|
+
return JSON.stringify(
|
|
181
|
+
{
|
|
182
|
+
"apple-notes": {
|
|
183
|
+
command: "bun",
|
|
184
|
+
args: ["run", path.join(projectPath, "src", "index.ts")],
|
|
185
|
+
env: {},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
null,
|
|
189
|
+
2
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Run indexing with specified mode
|
|
195
|
+
*/
|
|
196
|
+
async function runIndexing(mode: "full" | "incremental"): Promise<{ count: number; timeMs: number; skipped?: number }> {
|
|
197
|
+
// Dynamic import to avoid loading embeddings before config is set
|
|
198
|
+
const { indexNotes } = await import("./search/indexer.js");
|
|
199
|
+
const result = await indexNotes(mode);
|
|
200
|
+
return {
|
|
201
|
+
count: result.indexed,
|
|
202
|
+
timeMs: result.timeMs,
|
|
203
|
+
skipped: result.breakdown?.skipped,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Download local model (warm up the pipeline)
|
|
209
|
+
*/
|
|
210
|
+
async function downloadLocalModel(): Promise<void> {
|
|
211
|
+
const { getLocalEmbedding } = await import("./embeddings/local.js");
|
|
212
|
+
// Generate a test embedding to trigger model download
|
|
213
|
+
await getLocalEmbedding("test");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Main setup wizard
|
|
218
|
+
*/
|
|
219
|
+
async function main(): Promise<void> {
|
|
220
|
+
console.clear();
|
|
221
|
+
|
|
222
|
+
p.intro("apple-notes-mcp Setup Wizard");
|
|
223
|
+
|
|
224
|
+
// Check existing configuration
|
|
225
|
+
const existingEnv = readExistingEnv();
|
|
226
|
+
const hasExistingConfig = Object.keys(existingEnv).length > 0;
|
|
227
|
+
|
|
228
|
+
if (hasExistingConfig) {
|
|
229
|
+
p.note(
|
|
230
|
+
"Existing configuration found in .env\n" +
|
|
231
|
+
"Your current settings will be shown as defaults.",
|
|
232
|
+
"Configuration Detected"
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Provider selection
|
|
237
|
+
const provider = await p.select({
|
|
238
|
+
message: "Which embedding provider would you like to use?",
|
|
239
|
+
options: [
|
|
240
|
+
{
|
|
241
|
+
value: "local",
|
|
242
|
+
label: "Local (HuggingFace)",
|
|
243
|
+
hint: "Free, runs on your machine, ~200MB model download",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
value: "openrouter",
|
|
247
|
+
label: "OpenRouter API",
|
|
248
|
+
hint: "Fast, requires API key, pay-per-use",
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
initialValue: existingEnv.OPENROUTER_API_KEY ? "openrouter" : "local",
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (p.isCancel(provider)) {
|
|
255
|
+
p.cancel("Setup cancelled.");
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let openrouterApiKey: string | undefined;
|
|
260
|
+
let embeddingModel: string | undefined;
|
|
261
|
+
let embeddingDims: number | undefined;
|
|
262
|
+
|
|
263
|
+
if (provider === "openrouter") {
|
|
264
|
+
const apiKey = await p.text({
|
|
265
|
+
message: "Enter your OpenRouter API key:",
|
|
266
|
+
placeholder: "sk-or-v1-...",
|
|
267
|
+
initialValue: existingEnv.OPENROUTER_API_KEY || "",
|
|
268
|
+
validate: (value) => {
|
|
269
|
+
if (!value.trim()) return "API key is required";
|
|
270
|
+
if (!value.startsWith("sk-or-")) return "Invalid API key format (should start with sk-or-)";
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (p.isCancel(apiKey)) {
|
|
275
|
+
p.cancel("Setup cancelled.");
|
|
276
|
+
process.exit(0);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
openrouterApiKey = apiKey;
|
|
280
|
+
|
|
281
|
+
const useCustomModel = await p.confirm({
|
|
282
|
+
message: "Use custom embedding model? (default: qwen/qwen3-embedding-8b)",
|
|
283
|
+
initialValue: false,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
if (p.isCancel(useCustomModel)) {
|
|
287
|
+
p.cancel("Setup cancelled.");
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (useCustomModel) {
|
|
292
|
+
const model = await p.text({
|
|
293
|
+
message: "Enter model name:",
|
|
294
|
+
placeholder: "qwen/qwen3-embedding-8b",
|
|
295
|
+
initialValue: existingEnv.EMBEDDING_MODEL || "qwen/qwen3-embedding-8b",
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (p.isCancel(model)) {
|
|
299
|
+
p.cancel("Setup cancelled.");
|
|
300
|
+
process.exit(0);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
embeddingModel = model;
|
|
304
|
+
|
|
305
|
+
const dims = await p.text({
|
|
306
|
+
message: "Enter embedding dimensions:",
|
|
307
|
+
placeholder: "4096",
|
|
308
|
+
initialValue: existingEnv.EMBEDDING_DIMS || "4096",
|
|
309
|
+
validate: (value) => {
|
|
310
|
+
const num = parseInt(value, 10);
|
|
311
|
+
if (isNaN(num) || num <= 0) return "Must be a positive number";
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (p.isCancel(dims)) {
|
|
316
|
+
p.cancel("Setup cancelled.");
|
|
317
|
+
process.exit(0);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
embeddingDims = parseInt(dims, 10);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Read-only mode
|
|
325
|
+
const readonlyMode = await p.confirm({
|
|
326
|
+
message: "Enable read-only mode? (prevents all write operations to Apple Notes)",
|
|
327
|
+
initialValue: existingEnv.READONLY_MODE === "true",
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
if (p.isCancel(readonlyMode)) {
|
|
331
|
+
p.cancel("Setup cancelled.");
|
|
332
|
+
process.exit(0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Auto-indexing
|
|
336
|
+
const autoIndex = await p.select({
|
|
337
|
+
message: "Auto-indexing mode:",
|
|
338
|
+
options: [
|
|
339
|
+
{
|
|
340
|
+
value: "none",
|
|
341
|
+
label: "Manual only",
|
|
342
|
+
hint: "Run index-notes manually when needed",
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
value: "ttl",
|
|
346
|
+
label: "Time-based (TTL)",
|
|
347
|
+
hint: "Auto-reindex after specified time",
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
initialValue: existingEnv.INDEX_TTL ? "ttl" : "none",
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
if (p.isCancel(autoIndex)) {
|
|
354
|
+
p.cancel("Setup cancelled.");
|
|
355
|
+
process.exit(0);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let indexTtl: number | undefined;
|
|
359
|
+
|
|
360
|
+
if (autoIndex === "ttl") {
|
|
361
|
+
const ttl = await p.select({
|
|
362
|
+
message: "Reindex after:",
|
|
363
|
+
options: [
|
|
364
|
+
{ value: "3600", label: "1 hour" },
|
|
365
|
+
{ value: "21600", label: "6 hours" },
|
|
366
|
+
{ value: "86400", label: "24 hours" },
|
|
367
|
+
{ value: "604800", label: "1 week" },
|
|
368
|
+
],
|
|
369
|
+
initialValue: existingEnv.INDEX_TTL || "86400",
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
if (p.isCancel(ttl)) {
|
|
373
|
+
p.cancel("Setup cancelled.");
|
|
374
|
+
process.exit(0);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
indexTtl = parseInt(ttl, 10);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Debug mode
|
|
381
|
+
const debug = await p.confirm({
|
|
382
|
+
message: "Enable debug logging?",
|
|
383
|
+
initialValue: existingEnv.DEBUG === "true",
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
if (p.isCancel(debug)) {
|
|
387
|
+
p.cancel("Setup cancelled.");
|
|
388
|
+
process.exit(0);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Build configuration
|
|
392
|
+
const config: Config = {
|
|
393
|
+
provider: provider as "local" | "openrouter",
|
|
394
|
+
openrouterApiKey,
|
|
395
|
+
embeddingModel,
|
|
396
|
+
embeddingDims,
|
|
397
|
+
readonlyMode,
|
|
398
|
+
autoIndex: autoIndex as "none" | "ttl",
|
|
399
|
+
indexTtl,
|
|
400
|
+
debug,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// Save configuration
|
|
404
|
+
const s = p.spinner();
|
|
405
|
+
|
|
406
|
+
s.start("Saving configuration...");
|
|
407
|
+
writeEnvFile(config);
|
|
408
|
+
s.stop("Configuration saved to .env");
|
|
409
|
+
|
|
410
|
+
// Download local model if needed
|
|
411
|
+
if (provider === "local") {
|
|
412
|
+
const downloadModel = await p.confirm({
|
|
413
|
+
message: "Download local embedding model now? (~200MB, recommended)",
|
|
414
|
+
initialValue: true,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (p.isCancel(downloadModel)) {
|
|
418
|
+
p.cancel("Setup cancelled.");
|
|
419
|
+
process.exit(0);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (downloadModel) {
|
|
423
|
+
s.start("Downloading embedding model (this may take a minute)...");
|
|
424
|
+
try {
|
|
425
|
+
await downloadLocalModel();
|
|
426
|
+
s.stop("Model downloaded successfully");
|
|
427
|
+
} catch (error) {
|
|
428
|
+
s.stop("Model download failed (will download on first use)");
|
|
429
|
+
p.log.warn(
|
|
430
|
+
`Download error: ${error instanceof Error ? error.message : String(error)}`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Claude Code integration
|
|
437
|
+
const addToClaude = await p.confirm({
|
|
438
|
+
message: "Add to Claude Code configuration (~/.claude.json)?",
|
|
439
|
+
initialValue: true,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (p.isCancel(addToClaude)) {
|
|
443
|
+
p.cancel("Setup cancelled.");
|
|
444
|
+
process.exit(0);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (addToClaude) {
|
|
448
|
+
s.start("Updating Claude Code configuration...");
|
|
449
|
+
const success = addToClaudeConfig();
|
|
450
|
+
if (success) {
|
|
451
|
+
s.stop("Added to ~/.claude.json");
|
|
452
|
+
} else {
|
|
453
|
+
s.stop("Failed to update Claude config");
|
|
454
|
+
p.log.warn("You may need to add the server manually.");
|
|
455
|
+
p.note(getConfigSnippet(), "Add this to mcpServers in ~/.claude.json");
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
p.note(getConfigSnippet(), "Add this to mcpServers in ~/.claude.json");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Initial indexing
|
|
462
|
+
const runIndex = await p.confirm({
|
|
463
|
+
message: "Index your Apple Notes now?",
|
|
464
|
+
initialValue: true,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (p.isCancel(runIndex)) {
|
|
468
|
+
p.cancel("Setup cancelled.");
|
|
469
|
+
process.exit(0);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (runIndex) {
|
|
473
|
+
// Ask for indexing mode
|
|
474
|
+
const indexMode = await p.select({
|
|
475
|
+
message: "Indexing mode:",
|
|
476
|
+
options: [
|
|
477
|
+
{ value: "incremental", label: "Incremental", hint: "Only new/changed notes (faster)" },
|
|
478
|
+
{ value: "full", label: "Full", hint: "Reindex everything (slower)" },
|
|
479
|
+
],
|
|
480
|
+
initialValue: "incremental",
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
if (p.isCancel(indexMode)) {
|
|
484
|
+
p.cancel("Setup cancelled.");
|
|
485
|
+
process.exit(0);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const mode = indexMode as "full" | "incremental";
|
|
489
|
+
const indexingMsg = `${mode === "full" ? "Full" : "Incremental"} indexing...`;
|
|
490
|
+
|
|
491
|
+
// Don't use spinner in debug mode - it conflicts with debug output
|
|
492
|
+
if (!debug) {
|
|
493
|
+
s.start(indexingMsg);
|
|
494
|
+
} else {
|
|
495
|
+
p.log.info(indexingMsg);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
// Reload environment with new config
|
|
500
|
+
const dotenv = await import("dotenv");
|
|
501
|
+
dotenv.config({ path: ENV_FILE });
|
|
502
|
+
|
|
503
|
+
const result = await runIndexing(mode);
|
|
504
|
+
const skippedInfo = result.skipped ? `, ${result.skipped} unchanged` : "";
|
|
505
|
+
const doneMsg = `Indexed ${result.count} notes in ${(result.timeMs / 1000).toFixed(1)}s${skippedInfo}`;
|
|
506
|
+
|
|
507
|
+
if (!debug) {
|
|
508
|
+
s.stop(doneMsg);
|
|
509
|
+
} else {
|
|
510
|
+
p.log.success(doneMsg);
|
|
511
|
+
}
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (!debug) {
|
|
514
|
+
s.stop("Indexing failed");
|
|
515
|
+
}
|
|
516
|
+
p.log.error(
|
|
517
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
518
|
+
);
|
|
519
|
+
p.log.info("You can run indexing later with the index-notes tool.");
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Summary
|
|
524
|
+
p.note(
|
|
525
|
+
[
|
|
526
|
+
`Provider: ${provider === "local" ? "Local HuggingFace" : "OpenRouter"}`,
|
|
527
|
+
`Read-only: ${readonlyMode ? "Yes" : "No"}`,
|
|
528
|
+
`Auto-index: ${autoIndex === "ttl" ? `Every ${indexTtl! / 3600}h` : "Manual"}`,
|
|
529
|
+
`Debug: ${debug ? "Enabled" : "Disabled"}`,
|
|
530
|
+
].join("\n"),
|
|
531
|
+
"Configuration Summary"
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
p.outro("Setup complete! Restart Claude Code to use apple-notes-mcp.");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
main().catch((error) => {
|
|
538
|
+
console.error("Setup failed:", error);
|
|
539
|
+
process.exit(1);
|
|
540
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type definitions for Apple Notes MCP.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Search result returned from the database layer.
|
|
7
|
+
* Contains the full content of the note.
|
|
8
|
+
*/
|
|
9
|
+
export interface DBSearchResult {
|
|
10
|
+
/** Note title */
|
|
11
|
+
title: string;
|
|
12
|
+
/** Folder containing the note */
|
|
13
|
+
folder: string;
|
|
14
|
+
/** Full content of the note */
|
|
15
|
+
content: string;
|
|
16
|
+
/** Last modified date (ISO string) */
|
|
17
|
+
modified: string;
|
|
18
|
+
/** Relevance score (higher = more relevant) */
|
|
19
|
+
score: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Search result returned to clients.
|
|
24
|
+
* Contains a preview instead of full content by default.
|
|
25
|
+
*/
|
|
26
|
+
export interface SearchResult {
|
|
27
|
+
/** Note title */
|
|
28
|
+
title: string;
|
|
29
|
+
/** Folder containing the note */
|
|
30
|
+
folder: string;
|
|
31
|
+
/** Preview of content (200 chars) or full content if include_content=true */
|
|
32
|
+
preview: string;
|
|
33
|
+
/** Full content (only when include_content=true) */
|
|
34
|
+
content?: string;
|
|
35
|
+
/** Last modified date (ISO string) */
|
|
36
|
+
modified: string;
|
|
37
|
+
/** Combined relevance score (higher = more relevant) */
|
|
38
|
+
score: number;
|
|
39
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
describe("debug utility", () => {
|
|
4
|
+
const originalEnv = process.env.DEBUG;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.resetModules();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
if (originalEnv !== undefined) {
|
|
12
|
+
process.env.DEBUG = originalEnv;
|
|
13
|
+
} else {
|
|
14
|
+
delete process.env.DEBUG;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("createDebugLogger returns a function", async () => {
|
|
19
|
+
const { createDebugLogger } = await import("./debug.js");
|
|
20
|
+
const logger = createDebugLogger("TEST");
|
|
21
|
+
expect(typeof logger).toBe("function");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("isDebugEnabled returns false when DEBUG not set", async () => {
|
|
25
|
+
delete process.env.DEBUG;
|
|
26
|
+
const { isDebugEnabled } = await import("./debug.js");
|
|
27
|
+
expect(isDebugEnabled()).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("isDebugEnabled returns true when DEBUG is true", async () => {
|
|
31
|
+
process.env.DEBUG = "true";
|
|
32
|
+
const { isDebugEnabled } = await import("./debug.js");
|
|
33
|
+
expect(isDebugEnabled()).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("isDebugEnabled returns false when DEBUG is false", async () => {
|
|
37
|
+
process.env.DEBUG = "false";
|
|
38
|
+
const { isDebugEnabled } = await import("./debug.js");
|
|
39
|
+
expect(isDebugEnabled()).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared debug logging utility.
|
|
3
|
+
* Logs to stderr to avoid polluting stdout/MCP protocol.
|
|
4
|
+
* Uses dim styling to distinguish from errors.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ANSI color codes
|
|
8
|
+
const COLORS = {
|
|
9
|
+
reset: "\x1b[0m",
|
|
10
|
+
dim: "\x1b[2m",
|
|
11
|
+
cyan: "\x1b[36m",
|
|
12
|
+
yellow: "\x1b[33m",
|
|
13
|
+
red: "\x1b[31m",
|
|
14
|
+
green: "\x1b[32m",
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a debug logger with a specific prefix.
|
|
19
|
+
* Checks DEBUG env var at call time for runtime control.
|
|
20
|
+
* Output is dim cyan to distinguish from errors.
|
|
21
|
+
*/
|
|
22
|
+
export function createDebugLogger(prefix: string) {
|
|
23
|
+
return (...args: unknown[]): void => {
|
|
24
|
+
// Check at call time, not load time
|
|
25
|
+
if (process.env.DEBUG === "true") {
|
|
26
|
+
const formattedPrefix = `${COLORS.dim}${COLORS.cyan}[${prefix}]${COLORS.reset}`;
|
|
27
|
+
const formattedArgs = args.map(arg =>
|
|
28
|
+
typeof arg === "string" ? `${COLORS.dim}${arg}${COLORS.reset}` : arg
|
|
29
|
+
);
|
|
30
|
+
console.error(formattedPrefix, ...formattedArgs);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a warning logger (yellow output).
|
|
37
|
+
*/
|
|
38
|
+
export function createWarningLogger(prefix: string) {
|
|
39
|
+
return (...args: unknown[]): void => {
|
|
40
|
+
const formattedPrefix = `${COLORS.yellow}[${prefix}]${COLORS.reset}`;
|
|
41
|
+
console.error(formattedPrefix, ...args);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if debug mode is enabled.
|
|
47
|
+
* Checks at call time for runtime control.
|
|
48
|
+
*/
|
|
49
|
+
export function isDebugEnabled(): boolean {
|
|
50
|
+
return process.env.DEBUG === "true";
|
|
51
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { sanitizeErrorMessage } from "./errors.js";
|
|
3
|
+
|
|
4
|
+
describe("sanitizeErrorMessage", () => {
|
|
5
|
+
it("preserves user-friendly messages", () => {
|
|
6
|
+
expect(sanitizeErrorMessage("Note not found")).toBe("Note not found");
|
|
7
|
+
expect(sanitizeErrorMessage("Invalid title")).toBe("Invalid title");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("removes file paths", () => {
|
|
11
|
+
const error = "ENOENT: no such file at /Users/john/secret/file.ts";
|
|
12
|
+
expect(sanitizeErrorMessage(error)).not.toContain("/Users/john");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("removes stack traces", () => {
|
|
16
|
+
const error = "Error: failed\n at Function.module (/path/to/file.js:123:45)";
|
|
17
|
+
expect(sanitizeErrorMessage(error)).not.toContain("/path/to");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("handles generic errors gracefully", () => {
|
|
21
|
+
const error = "TypeError: Cannot read property 'x' of undefined";
|
|
22
|
+
expect(sanitizeErrorMessage(error)).toBe("An internal error occurred");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("preserves known safe error patterns", () => {
|
|
26
|
+
expect(sanitizeErrorMessage("Title must be a non-empty string")).toBe("Title must be a non-empty string");
|
|
27
|
+
expect(sanitizeErrorMessage("Note not found: \"My Note\"")).toBe("Note not found: \"My Note\"");
|
|
28
|
+
});
|
|
29
|
+
});
|