apple-tools-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/LICENSE +21 -0
- package/README.md +163 -0
- package/contacts.js +433 -0
- package/index.js +1502 -0
- package/indexer.js +1849 -0
- package/lib/audit.js +1083 -0
- package/lib/shell.js +321 -0
- package/lib/validators.js +432 -0
- package/package.json +116 -0
- package/scripts/audit-index.js +94 -0
- package/search.js +1652 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security validation and escaping utilities for apple-tools-mcp
|
|
3
|
+
*
|
|
4
|
+
* This module centralizes input validation to prevent:
|
|
5
|
+
* - Path traversal attacks
|
|
6
|
+
* - Command injection
|
|
7
|
+
* - SQL injection
|
|
8
|
+
* - Integer overflow
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from "path";
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
|
|
14
|
+
// ============ PATH VALIDATION ============
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validates that a file path is within an allowed directory and has expected extension
|
|
18
|
+
* Prevents path traversal attacks like "../../../etc/passwd"
|
|
19
|
+
*
|
|
20
|
+
* @param {string} filePath - The path to validate
|
|
21
|
+
* @param {string} allowedDir - The directory the path must be within
|
|
22
|
+
* @param {string[]} allowedExtensions - Array of allowed extensions (e.g., ['.emlx'])
|
|
23
|
+
* @returns {string} The resolved, validated path
|
|
24
|
+
* @throws {Error} If validation fails
|
|
25
|
+
*/
|
|
26
|
+
export function validateFilePath(filePath, allowedDir, allowedExtensions = []) {
|
|
27
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
28
|
+
throw new Error('File path is required');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check extension if specified
|
|
32
|
+
if (allowedExtensions.length > 0) {
|
|
33
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
34
|
+
if (!allowedExtensions.includes(ext)) {
|
|
35
|
+
throw new Error(`Invalid file extension. Allowed: ${allowedExtensions.join(', ')}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Resolve to absolute path (handles ../ etc)
|
|
40
|
+
const resolvedPath = path.resolve(filePath);
|
|
41
|
+
const resolvedAllowedDir = path.resolve(allowedDir);
|
|
42
|
+
|
|
43
|
+
// Ensure the resolved path starts with the allowed directory
|
|
44
|
+
if (!resolvedPath.startsWith(resolvedAllowedDir + path.sep) && resolvedPath !== resolvedAllowedDir) {
|
|
45
|
+
throw new Error('Access denied: path outside allowed directory');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return resolvedPath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validates an email file path specifically
|
|
53
|
+
* @param {string} filePath - The email file path
|
|
54
|
+
* @param {string} mailDir - The Mail directory (usually ~/Library/Mail)
|
|
55
|
+
* @returns {string} The validated path
|
|
56
|
+
*/
|
|
57
|
+
export function validateEmailPath(filePath, mailDir) {
|
|
58
|
+
return validateFilePath(filePath, mailDir, ['.emlx']);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============ NUMERIC VALIDATION ============
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validates and constrains a limit parameter
|
|
65
|
+
* Prevents integer overflow and excessively large queries
|
|
66
|
+
*
|
|
67
|
+
* @param {any} value - The value to validate
|
|
68
|
+
* @param {number} defaultValue - Default if invalid (default: 30)
|
|
69
|
+
* @param {number} max - Maximum allowed value (default: 1000)
|
|
70
|
+
* @returns {number} A safe integer within bounds
|
|
71
|
+
*/
|
|
72
|
+
export function validateLimit(value, defaultValue = 30, max = 1000) {
|
|
73
|
+
const parsed = parseInt(value);
|
|
74
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
75
|
+
return defaultValue;
|
|
76
|
+
}
|
|
77
|
+
return Math.min(parsed, max);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validates a days_back parameter
|
|
82
|
+
* @param {any} value - The value to validate
|
|
83
|
+
* @param {number} max - Maximum days back (default: 3650 = ~10 years)
|
|
84
|
+
* @returns {number} A safe integer >= 0
|
|
85
|
+
*/
|
|
86
|
+
export function validateDaysBack(value, max = 3650) {
|
|
87
|
+
const parsed = parseInt(value);
|
|
88
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
return Math.min(parsed, max);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validates a week offset parameter
|
|
96
|
+
* @param {any} value - The value to validate
|
|
97
|
+
* @param {number} max - Maximum weeks ahead (default: 52)
|
|
98
|
+
* @returns {number} A safe integer >= 0
|
|
99
|
+
*/
|
|
100
|
+
export function validateWeekOffset(value, max = 52) {
|
|
101
|
+
const parsed = parseInt(value);
|
|
102
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
return Math.min(parsed, max);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============ STRING ESCAPING ============
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Escapes a string for use in AppleScript double-quoted strings
|
|
112
|
+
* Prevents AppleScript injection attacks
|
|
113
|
+
*
|
|
114
|
+
* @param {string} str - The string to escape
|
|
115
|
+
* @returns {string} Escaped string safe for AppleScript
|
|
116
|
+
*/
|
|
117
|
+
export function escapeAppleScript(str) {
|
|
118
|
+
if (!str || typeof str !== 'string') {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
// Escape backslashes first, then double quotes
|
|
122
|
+
return str
|
|
123
|
+
.replace(/\\/g, '\\\\')
|
|
124
|
+
.replace(/"/g, '\\"');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validates a mailbox name for use in AppleScript
|
|
129
|
+
* Only allows safe characters to prevent injection
|
|
130
|
+
*
|
|
131
|
+
* @param {string} mailbox - The mailbox name
|
|
132
|
+
* @returns {string|null} Validated mailbox name or null if invalid
|
|
133
|
+
*/
|
|
134
|
+
export function validateMailboxName(mailbox) {
|
|
135
|
+
if (!mailbox || typeof mailbox !== 'string') {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Only allow alphanumeric, spaces, hyphens, underscores, and periods
|
|
140
|
+
// This prevents any AppleScript injection
|
|
141
|
+
if (!/^[a-zA-Z0-9\s\-_.]+$/.test(mailbox)) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Also limit length
|
|
146
|
+
if (mailbox.length > 100) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return mailbox;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Escapes a string for use in SQL single-quoted strings
|
|
155
|
+
* Note: Prefer parameterized queries when possible
|
|
156
|
+
*
|
|
157
|
+
* @param {string} str - The string to escape
|
|
158
|
+
* @returns {string} Escaped string safe for SQL
|
|
159
|
+
*/
|
|
160
|
+
export function escapeSQL(str) {
|
|
161
|
+
if (!str || typeof str !== 'string') {
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
// Double single quotes and escape backslashes
|
|
165
|
+
return str
|
|
166
|
+
.replace(/\\/g, '\\\\')
|
|
167
|
+
.replace(/'/g, "''");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Validates and sanitizes an ID string for LanceDB queries
|
|
172
|
+
* @param {string} id - The ID to validate
|
|
173
|
+
* @returns {string|null} Validated ID or null if invalid
|
|
174
|
+
*/
|
|
175
|
+
export function validateLanceDBId(id) {
|
|
176
|
+
if (!id || typeof id !== 'string') {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// IDs should only contain safe characters
|
|
181
|
+
// Allow alphanumeric, spaces, hyphens, colons, commas, periods
|
|
182
|
+
if (!/^[a-zA-Z0-9\s\-:,.]+$/.test(id)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Limit length
|
|
187
|
+
if (id.length > 500) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return id;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ============ SEARCH QUERY VALIDATION ============
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Validates a search query string
|
|
198
|
+
* @param {string} query - The search query
|
|
199
|
+
* @param {number} maxLength - Maximum allowed length (default: 1000)
|
|
200
|
+
* @returns {string} Validated query
|
|
201
|
+
* @throws {Error} If query is invalid
|
|
202
|
+
*/
|
|
203
|
+
export function validateSearchQuery(query, maxLength = 1000) {
|
|
204
|
+
if (!query || typeof query !== 'string') {
|
|
205
|
+
throw new Error('Search query is required');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const trimmed = query.trim();
|
|
209
|
+
if (trimmed.length === 0) {
|
|
210
|
+
throw new Error('Search query cannot be empty');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (trimmed.length > maxLength) {
|
|
214
|
+
return trimmed.substring(0, maxLength);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return trimmed;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Validates a contact/name string
|
|
222
|
+
* @param {string} contact - The contact name or identifier
|
|
223
|
+
* @param {number} maxLength - Maximum length (default: 200)
|
|
224
|
+
* @returns {string|null} Validated contact or null
|
|
225
|
+
*/
|
|
226
|
+
export function validateContact(contact, maxLength = 200) {
|
|
227
|
+
if (!contact || typeof contact !== 'string') {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const trimmed = contact.trim();
|
|
232
|
+
if (trimmed.length === 0 || trimmed.length > maxLength) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return trimmed;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ============ DATE VALIDATION ============
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Validates a date string or timestamp
|
|
243
|
+
* @param {string|number} date - The date to validate
|
|
244
|
+
* @returns {Date|null} Validated Date object or null
|
|
245
|
+
*/
|
|
246
|
+
export function validateDate(date) {
|
|
247
|
+
if (!date) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const d = new Date(date);
|
|
253
|
+
if (isNaN(d.getTime())) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Sanity check: date should be between year 1990 and 2100
|
|
258
|
+
const year = d.getFullYear();
|
|
259
|
+
if (year < 1990 || year > 2100) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return d;
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============ ENVIRONMENT VALIDATION ============
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Gets a validated home directory path
|
|
273
|
+
* @returns {string} The home directory
|
|
274
|
+
* @throws {Error} If HOME is invalid
|
|
275
|
+
*/
|
|
276
|
+
export function getValidatedHome() {
|
|
277
|
+
const home = process.env.HOME;
|
|
278
|
+
|
|
279
|
+
if (!home || typeof home !== 'string') {
|
|
280
|
+
throw new Error('HOME environment variable is not set');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Ensure it's an absolute path
|
|
284
|
+
if (!path.isAbsolute(home)) {
|
|
285
|
+
throw new Error('HOME must be an absolute path');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Ensure it exists and is a directory
|
|
289
|
+
try {
|
|
290
|
+
const stats = fs.statSync(home);
|
|
291
|
+
if (!stats.isDirectory()) {
|
|
292
|
+
throw new Error('HOME is not a directory');
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
if (e.code === 'ENOENT') {
|
|
296
|
+
throw new Error('HOME directory does not exist');
|
|
297
|
+
}
|
|
298
|
+
throw e;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return home;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ============ REGEX SAFETY ============
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Escapes special regex characters in a string
|
|
308
|
+
* Prevents ReDoS when using user input in regex patterns
|
|
309
|
+
*
|
|
310
|
+
* @param {string} str - The string to escape
|
|
311
|
+
* @returns {string} Escaped string safe for use in RegExp
|
|
312
|
+
*/
|
|
313
|
+
export function escapeRegex(str) {
|
|
314
|
+
if (!str || typeof str !== 'string') {
|
|
315
|
+
return '';
|
|
316
|
+
}
|
|
317
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Safely execute a regex match with input length limits
|
|
322
|
+
* Prevents ReDoS by limiting input size
|
|
323
|
+
*
|
|
324
|
+
* @param {string} input - The input string to match against
|
|
325
|
+
* @param {RegExp} pattern - The regex pattern
|
|
326
|
+
* @param {number} maxLength - Maximum input length (default: 10000)
|
|
327
|
+
* @returns {RegExpMatchArray|null} Match result or null
|
|
328
|
+
*/
|
|
329
|
+
export function safeMatch(input, pattern, maxLength = 10000) {
|
|
330
|
+
if (!input || typeof input !== 'string') {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
// Truncate overly long inputs to prevent ReDoS
|
|
334
|
+
const safeInput = input.length > maxLength ? input.substring(0, maxLength) : input;
|
|
335
|
+
try {
|
|
336
|
+
return safeInput.match(pattern);
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Safely execute a regex replace with input length limits
|
|
344
|
+
*
|
|
345
|
+
* @param {string} input - The input string
|
|
346
|
+
* @param {RegExp|string} pattern - The regex pattern or string
|
|
347
|
+
* @param {string|Function} replacement - The replacement string or function
|
|
348
|
+
* @param {number} maxLength - Maximum input length (default: 50000)
|
|
349
|
+
* @returns {string} Replaced string
|
|
350
|
+
*/
|
|
351
|
+
export function safeReplace(input, pattern, replacement, maxLength = 50000) {
|
|
352
|
+
if (!input || typeof input !== 'string') {
|
|
353
|
+
return '';
|
|
354
|
+
}
|
|
355
|
+
// Truncate overly long inputs
|
|
356
|
+
const safeInput = input.length > maxLength ? input.substring(0, maxLength) : input;
|
|
357
|
+
try {
|
|
358
|
+
return safeInput.replace(pattern, replacement);
|
|
359
|
+
} catch {
|
|
360
|
+
return safeInput;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Strip HTML tags safely without ReDoS vulnerability
|
|
366
|
+
* Uses iterative approach instead of complex regex
|
|
367
|
+
*
|
|
368
|
+
* @param {string} html - The HTML string
|
|
369
|
+
* @param {number} maxLength - Maximum input length (default: 100000)
|
|
370
|
+
* @returns {string} Text with HTML tags removed
|
|
371
|
+
*/
|
|
372
|
+
export function stripHtmlTags(html, maxLength = 100000) {
|
|
373
|
+
if (!html || typeof html !== 'string') {
|
|
374
|
+
return '';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Truncate overly long inputs
|
|
378
|
+
let text = html.length > maxLength ? html.substring(0, maxLength) : html;
|
|
379
|
+
|
|
380
|
+
// Simple state machine approach - more predictable than regex
|
|
381
|
+
let result = '';
|
|
382
|
+
let inTag = false;
|
|
383
|
+
|
|
384
|
+
for (let i = 0; i < text.length; i++) {
|
|
385
|
+
const char = text[i];
|
|
386
|
+
if (char === '<') {
|
|
387
|
+
inTag = true;
|
|
388
|
+
} else if (char === '>') {
|
|
389
|
+
inTag = false;
|
|
390
|
+
result += ' '; // Replace tag with space
|
|
391
|
+
} else if (!inTag) {
|
|
392
|
+
result += char;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Normalize whitespace
|
|
397
|
+
return result.replace(/\s+/g, ' ').trim();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ============ DATE UTILITIES ============
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Mac Absolute Time epoch constant
|
|
404
|
+
* Mac epoch is January 1, 2001 00:00:00 UTC
|
|
405
|
+
* Unix epoch is January 1, 1970 00:00:00 UTC
|
|
406
|
+
* Difference is 978307200 seconds
|
|
407
|
+
*/
|
|
408
|
+
const MAC_ABSOLUTE_EPOCH = 978307200;
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Convert Mac Absolute Time to Unix timestamp (milliseconds)
|
|
412
|
+
* Handles both seconds and nanoseconds formats
|
|
413
|
+
*
|
|
414
|
+
* @param {number} macTime - Mac absolute time value
|
|
415
|
+
* @returns {number} Unix timestamp in milliseconds
|
|
416
|
+
*/
|
|
417
|
+
export function macAbsoluteTimeToDate(macTime) {
|
|
418
|
+
if (macTime === null || macTime === undefined || isNaN(macTime)) {
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let seconds = macTime;
|
|
423
|
+
|
|
424
|
+
// Detect nanoseconds (Messages database uses nanoseconds)
|
|
425
|
+
// If the value is larger than reasonable for seconds (> year 3000), assume nanoseconds
|
|
426
|
+
if (macTime > 50000000000) {
|
|
427
|
+
seconds = macTime / 1e9;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Convert to Unix timestamp (milliseconds)
|
|
431
|
+
return (seconds + MAC_ABSOLUTE_EPOCH) * 1000;
|
|
432
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "apple-tools-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for semantic search across Apple Mail, Messages, and Calendar",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"apple-tools-mcp": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"author": "Peter Coates",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/sfls1397/apple-tools-mcp.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/sfls1397/apple-tools-mcp/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/sfls1397/apple-tools-mcp#readme",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"apple",
|
|
24
|
+
"mail",
|
|
25
|
+
"email",
|
|
26
|
+
"calendar",
|
|
27
|
+
"messages",
|
|
28
|
+
"imessage",
|
|
29
|
+
"semantic-search",
|
|
30
|
+
"claude",
|
|
31
|
+
"anthropic",
|
|
32
|
+
"macos"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"index.js",
|
|
39
|
+
"indexer.js",
|
|
40
|
+
"search.js",
|
|
41
|
+
"contacts.js",
|
|
42
|
+
"lib/",
|
|
43
|
+
"scripts/audit-index.js",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"start": "node index.js",
|
|
49
|
+
"build-index": "APPLE_TOOLS_INDEX_DAYS_BACK=30 node -e \"import('./indexer.js').then(i=>i.rebuildIndex()).catch(e=>{console.error(e.message);process.exit(1)})\"",
|
|
50
|
+
"audit": "APPLE_TOOLS_INDEX_DAYS_BACK=30 node scripts/audit-index.js",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"test:coverage": "vitest run --coverage",
|
|
54
|
+
"test:unit": "vitest run tests/unit",
|
|
55
|
+
"test:integration": "vitest run tests/integration",
|
|
56
|
+
"test:integration:periodic": "vitest run tests/integration/periodic-indexing-e2e.test.js --reporter=verbose",
|
|
57
|
+
"test:perf": "vitest run tests/performance",
|
|
58
|
+
"test:fuzz": "vitest run tests/fuzz",
|
|
59
|
+
"test:chaos": "vitest run tests/chaos",
|
|
60
|
+
"test:contract": "vitest run tests/contract",
|
|
61
|
+
"test:concurrency": "vitest run tests/concurrency",
|
|
62
|
+
"test:recovery": "vitest run tests/recovery",
|
|
63
|
+
"test:timezone": "vitest run tests/timezone",
|
|
64
|
+
"test:stress": "vitest run tests/stress",
|
|
65
|
+
"test:snapshot": "vitest run tests/snapshots",
|
|
66
|
+
"test:all": "vitest run",
|
|
67
|
+
"test:idx": "vitest run tests/indexing",
|
|
68
|
+
"test:idx:unit": "vitest run tests/indexing/unit",
|
|
69
|
+
"test:idx:integration": "vitest run tests/indexing/integration",
|
|
70
|
+
"test:idx:accuracy": "vitest run tests/indexing/accuracy",
|
|
71
|
+
"test:idx:perf": "vitest run tests/indexing/performance",
|
|
72
|
+
"test:idx:resource": "vitest run tests/indexing/resource",
|
|
73
|
+
"test:idx:edge": "vitest run tests/indexing/edge-cases",
|
|
74
|
+
"test:idx:security": "vitest run tests/indexing/security",
|
|
75
|
+
"test:idx:contacts": "vitest run tests/indexing/contacts",
|
|
76
|
+
"test:idx:cache": "vitest run tests/indexing/caching",
|
|
77
|
+
"test:idx:negative": "vitest run tests/indexing/negative",
|
|
78
|
+
"test:idx:watch": "vitest tests/indexing",
|
|
79
|
+
"test:idx:build": "node scripts/build-test-index.js",
|
|
80
|
+
"test:idx:clean": "node scripts/clean-test-index.js",
|
|
81
|
+
"perf": "USE_REAL_DATA=1 vitest run tests/perf --reporter=verbose",
|
|
82
|
+
"perf:mock": "vitest run tests/perf --reporter=verbose",
|
|
83
|
+
"perf:watch": "USE_REAL_DATA=1 vitest tests/perf",
|
|
84
|
+
"perf:indexing": "vitest run tests/perf/indexing.perf.test.js --reporter=verbose",
|
|
85
|
+
"perf:search": "vitest run tests/perf/search.perf.test.js --reporter=verbose",
|
|
86
|
+
"perf:tools": "vitest run tests/perf/tools.perf.test.js --reporter=verbose",
|
|
87
|
+
"perf:server": "vitest run tests/perf/mcp-server.perf.test.js --reporter=verbose",
|
|
88
|
+
"perf:embedding": "vitest run tests/perf/embedding.perf.test.js --reporter=verbose",
|
|
89
|
+
"perf:datasources": "vitest run tests/perf/datasources.perf.test.js --reporter=verbose",
|
|
90
|
+
"perf:memory": "vitest run tests/perf/memory.perf.test.js --reporter=verbose",
|
|
91
|
+
"perf:stress": "vitest run tests/perf/stress.perf.test.js --reporter=verbose --testTimeout=120000",
|
|
92
|
+
"perf:mail": "vitest run tests/perf/datasources.perf.test.js -t 'Email' --reporter=verbose",
|
|
93
|
+
"perf:messages": "vitest run tests/perf/datasources.perf.test.js -t 'Messages' --reporter=verbose",
|
|
94
|
+
"perf:calendar": "vitest run tests/perf/datasources.perf.test.js -t 'Calendar' --reporter=verbose",
|
|
95
|
+
"perf:contacts": "vitest run tests/perf/datasources.perf.test.js -t 'Contacts' --reporter=verbose",
|
|
96
|
+
"perf:quick": "vitest run tests/perf/tools.perf.test.js tests/perf/search.perf.test.js --reporter=verbose",
|
|
97
|
+
"perf:negative": "vitest run tests/perf/negative.perf.test.js --reporter=verbose",
|
|
98
|
+
"perf:edge-cases": "vitest run tests/perf/edge-cases.perf.test.js --reporter=verbose",
|
|
99
|
+
"perf:regression": "vitest run tests/perf/regression.perf.test.js --reporter=verbose",
|
|
100
|
+
"perf:lancedb": "vitest run tests/perf/lancedb.perf.test.js --reporter=verbose",
|
|
101
|
+
"perf:background": "vitest run tests/perf/background-indexing.perf.test.js --reporter=verbose",
|
|
102
|
+
"perf:dates": "vitest run tests/perf/date-parsing.perf.test.js --reporter=verbose",
|
|
103
|
+
"perf:full": "vitest run tests/perf --reporter=verbose --testTimeout=120000"
|
|
104
|
+
},
|
|
105
|
+
"dependencies": {
|
|
106
|
+
"@lancedb/lancedb": "^0.22.3",
|
|
107
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
108
|
+
"@xenova/transformers": "^2.17.2",
|
|
109
|
+
"chrono-node": "^2.9.0"
|
|
110
|
+
},
|
|
111
|
+
"devDependencies": {
|
|
112
|
+
"@vitest/coverage-v8": "^4.0.14",
|
|
113
|
+
"fast-check": "^3.15.0",
|
|
114
|
+
"vitest": "^4.0.14"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI Script for Auditing Index
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node scripts/audit-index.js
|
|
8
|
+
* node scripts/audit-index.js --sources emails,messages
|
|
9
|
+
* node scripts/audit-index.js --max-items 50
|
|
10
|
+
* node scripts/audit-index.js --sources calendar --max-items 0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { auditAll, formatAuditReport } from '../lib/audit.js';
|
|
14
|
+
|
|
15
|
+
// Parse command-line arguments
|
|
16
|
+
function parseArgs() {
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const options = {
|
|
19
|
+
sources: ['emails', 'messages', 'calendar'],
|
|
20
|
+
maxItems: 100
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
const arg = args[i];
|
|
25
|
+
|
|
26
|
+
if (arg === '--sources' && i + 1 < args.length) {
|
|
27
|
+
options.sources = args[i + 1].split(',').map(s => s.trim());
|
|
28
|
+
i++;
|
|
29
|
+
} else if (arg === '--max-items' && i + 1 < args.length) {
|
|
30
|
+
options.maxItems = parseInt(args[i + 1], 10);
|
|
31
|
+
i++;
|
|
32
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
33
|
+
printHelp();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return options;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function printHelp() {
|
|
42
|
+
console.log(`
|
|
43
|
+
Audit Index - Check index integrity against source data
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
npm run audit
|
|
47
|
+
npm run audit -- --sources emails,messages
|
|
48
|
+
npm run audit -- --max-items 50
|
|
49
|
+
npm run audit -- --sources calendar --max-items 0
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
--sources <list> Comma-separated list of sources to audit
|
|
53
|
+
Options: emails, messages, calendar
|
|
54
|
+
Default: emails,messages,calendar
|
|
55
|
+
|
|
56
|
+
--max-items <num> Maximum items to show per discrepancy category
|
|
57
|
+
Use 0 for unlimited
|
|
58
|
+
Default: 100
|
|
59
|
+
|
|
60
|
+
--help, -h Show this help message
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
npm run audit # Audit all sources
|
|
64
|
+
npm run audit -- --sources emails # Audit emails only
|
|
65
|
+
npm run audit -- --sources emails,messages # Audit emails and messages
|
|
66
|
+
npm run audit -- --max-items 50 # Show max 50 items per category
|
|
67
|
+
npm run audit -- --sources calendar --max-items 0 # Show all calendar discrepancies
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Main execution
|
|
72
|
+
async function main() {
|
|
73
|
+
try {
|
|
74
|
+
const options = parseArgs();
|
|
75
|
+
|
|
76
|
+
console.error('Starting index audit...');
|
|
77
|
+
console.error(`Sources: ${options.sources.join(', ')}`);
|
|
78
|
+
console.error(`Max items per category: ${options.maxItems === 0 ? 'unlimited' : options.maxItems}`);
|
|
79
|
+
console.error('');
|
|
80
|
+
|
|
81
|
+
const results = await auditAll(options);
|
|
82
|
+
const report = formatAuditReport(results);
|
|
83
|
+
|
|
84
|
+
// Print report to stdout
|
|
85
|
+
console.log(report);
|
|
86
|
+
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Error running audit:', error.message);
|
|
89
|
+
console.error(error.stack);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main();
|