apple-notes-mcp 1.2.19 → 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 +24 -1
- package/build/index.js +22 -8
- package/build/services/appleNotesManager.js +111 -9
- package/build/services/appleNotesManager.test.js +150 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -143,6 +143,9 @@ Searches for notes by title or content.
|
|
|
143
143
|
| `query` | string | Yes | Text to search for |
|
|
144
144
|
| `searchContent` | boolean | No | If `true`, searches note body; if `false` (default), searches titles only |
|
|
145
145
|
| `account` | string | No | Account to search in (defaults to iCloud) |
|
|
146
|
+
| `folder` | string | No | Limit search to a specific folder |
|
|
147
|
+
| `modifiedSince` | string | No | ISO 8601 date string to filter notes modified on or after this date (e.g., `"2025-01-01"`) |
|
|
148
|
+
| `limit` | number | No | Maximum number of results to return |
|
|
146
149
|
|
|
147
150
|
**Example - Search titles:**
|
|
148
151
|
```json
|
|
@@ -159,6 +162,16 @@ Searches for notes by title or content.
|
|
|
159
162
|
}
|
|
160
163
|
```
|
|
161
164
|
|
|
165
|
+
**Example - Search recent notes with limit:**
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"query": "todo",
|
|
169
|
+
"searchContent": true,
|
|
170
|
+
"modifiedSince": "2025-01-01",
|
|
171
|
+
"limit": 10
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
162
175
|
**Returns:** List of matching notes with titles, folder names, and IDs. Use the returned ID for subsequent operations like `get-note-content`, `update-note`, etc.
|
|
163
176
|
|
|
164
177
|
---
|
|
@@ -356,12 +369,14 @@ Moves a note to a different folder.
|
|
|
356
369
|
|
|
357
370
|
#### `list-notes`
|
|
358
371
|
|
|
359
|
-
Lists all notes, optionally filtered by folder.
|
|
372
|
+
Lists all notes, optionally filtered by folder, date, and limit.
|
|
360
373
|
|
|
361
374
|
| Parameter | Type | Required | Description |
|
|
362
375
|
|-----------|------|----------|-------------|
|
|
363
376
|
| `account` | string | No | Account to list notes from (defaults to iCloud) |
|
|
364
377
|
| `folder` | string | No | Filter to notes in this folder only |
|
|
378
|
+
| `modifiedSince` | string | No | ISO 8601 date string to filter notes modified on or after this date (e.g., `"2025-01-01"`) |
|
|
379
|
+
| `limit` | number | No | Maximum number of notes to return |
|
|
365
380
|
|
|
366
381
|
**Example - All notes:**
|
|
367
382
|
```json
|
|
@@ -375,6 +390,14 @@ Lists all notes, optionally filtered by folder.
|
|
|
375
390
|
}
|
|
376
391
|
```
|
|
377
392
|
|
|
393
|
+
**Example - Recent notes with limit:**
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"modifiedSince": "2025-06-01",
|
|
397
|
+
"limit": 20
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
378
401
|
**Returns:** List of note titles.
|
|
379
402
|
|
|
380
403
|
---
|
package/build/index.js
CHANGED
|
@@ -132,11 +132,18 @@ server.tool("search-notes", {
|
|
|
132
132
|
searchContent: z.boolean().optional().describe("Search note content instead of titles"),
|
|
133
133
|
account: z.string().optional().describe("Account to search in"),
|
|
134
134
|
folder: z.string().optional().describe("Limit search to a specific folder"),
|
|
135
|
-
|
|
135
|
+
modifiedSince: z
|
|
136
|
+
.string()
|
|
137
|
+
.optional()
|
|
138
|
+
.describe("ISO 8601 date string to filter notes modified on or after this date (e.g., '2025-01-01'). Useful for searching only recent notes in large collections."),
|
|
139
|
+
limit: z.number().int().positive().optional().describe("Maximum number of results to return"),
|
|
140
|
+
}, withErrorHandling(({ query, searchContent = false, account, folder, modifiedSince, limit }) => {
|
|
136
141
|
// Use sync-aware wrapper for this read operation
|
|
137
|
-
const { result: notes, syncBefore, syncInterference, } = withSyncAwarenessSync("search-notes", () => notesManager.searchNotes(query, searchContent, account, folder));
|
|
142
|
+
const { result: notes, syncBefore, syncInterference, } = withSyncAwarenessSync("search-notes", () => notesManager.searchNotes(query, searchContent, account, folder, modifiedSince, limit));
|
|
138
143
|
const searchType = searchContent ? "content" : "titles";
|
|
139
144
|
const folderInfo = folder ? ` in folder "${folder}"` : "";
|
|
145
|
+
const dateInfo = modifiedSince ? ` modified since ${modifiedSince}` : "";
|
|
146
|
+
const limitInfo = limit ? ` (limit: ${limit})` : "";
|
|
140
147
|
// Build sync warning if needed
|
|
141
148
|
const syncWarnings = [];
|
|
142
149
|
if (syncBefore.syncDetected) {
|
|
@@ -147,7 +154,7 @@ server.tool("search-notes", {
|
|
|
147
154
|
}
|
|
148
155
|
const syncNote = syncWarnings.length > 0 ? `\n\n${syncWarnings.join(" ")}` : "";
|
|
149
156
|
if (notes.length === 0) {
|
|
150
|
-
return successResponse(`No notes found matching "${query}" in ${searchType}${folderInfo}${syncNote}`);
|
|
157
|
+
return successResponse(`No notes found matching "${query}" in ${searchType}${folderInfo}${dateInfo}${syncNote}`);
|
|
151
158
|
}
|
|
152
159
|
// Format each note with ID and folder info, highlighting Recently Deleted
|
|
153
160
|
const noteList = notes
|
|
@@ -162,7 +169,7 @@ server.tool("search-notes", {
|
|
|
162
169
|
return ` - ${n.title}${idSuffix}`;
|
|
163
170
|
})
|
|
164
171
|
.join("\n");
|
|
165
|
-
return successResponse(`Found ${notes.length} notes (searched ${searchType}${folderInfo}):\n${noteList}${syncNote}`);
|
|
172
|
+
return successResponse(`Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):\n${noteList}${syncNote}`);
|
|
166
173
|
}, "Error searching notes"));
|
|
167
174
|
// --- get-note-content ---
|
|
168
175
|
server.tool("get-note-content", {
|
|
@@ -388,12 +395,19 @@ server.tool("move-note", {
|
|
|
388
395
|
server.tool("list-notes", {
|
|
389
396
|
account: z.string().optional().describe("Account to list notes from"),
|
|
390
397
|
folder: z.string().optional().describe("Filter to specific folder"),
|
|
391
|
-
|
|
398
|
+
modifiedSince: z
|
|
399
|
+
.string()
|
|
400
|
+
.optional()
|
|
401
|
+
.describe("ISO 8601 date string to filter notes modified on or after this date (e.g., '2025-01-01'). Useful for listing only recent notes in large collections."),
|
|
402
|
+
limit: z.number().int().positive().optional().describe("Maximum number of notes to return"),
|
|
403
|
+
}, withErrorHandling(({ account, folder, modifiedSince, limit }) => {
|
|
392
404
|
// Use sync-aware wrapper for this read operation
|
|
393
|
-
const { result: notes, syncBefore, syncInterference, } = withSyncAwarenessSync("list-notes", () => notesManager.listNotes(account, folder));
|
|
405
|
+
const { result: notes, syncBefore, syncInterference, } = withSyncAwarenessSync("list-notes", () => notesManager.listNotes(account, folder, modifiedSince, limit));
|
|
394
406
|
// Build context string for the response
|
|
395
407
|
const location = folder ? ` in folder "${folder}"` : "";
|
|
396
408
|
const acct = account ? ` (${account})` : "";
|
|
409
|
+
const dateInfo = modifiedSince ? ` modified since ${modifiedSince}` : "";
|
|
410
|
+
const limitInfo = limit ? ` (limit: ${limit})` : "";
|
|
397
411
|
// Build sync warning if needed
|
|
398
412
|
const syncWarnings = [];
|
|
399
413
|
if (syncBefore.syncDetected) {
|
|
@@ -404,10 +418,10 @@ server.tool("list-notes", {
|
|
|
404
418
|
}
|
|
405
419
|
const syncNote = syncWarnings.length > 0 ? `\n\n${syncWarnings.join(" ")}` : "";
|
|
406
420
|
if (notes.length === 0) {
|
|
407
|
-
return successResponse(`No notes found${location}${acct}${syncNote}`);
|
|
421
|
+
return successResponse(`No notes found${location}${acct}${dateInfo}${syncNote}`);
|
|
408
422
|
}
|
|
409
423
|
const noteList = notes.map((t) => ` - ${t}`).join("\n");
|
|
410
|
-
return successResponse(`Found ${notes.length} notes${location}${acct}:\n${noteList}${syncNote}`);
|
|
424
|
+
return successResponse(`Found ${notes.length} notes${location}${acct}${dateInfo}${limitInfo}:\n${noteList}${syncNote}`);
|
|
411
425
|
}, "Error listing notes"));
|
|
412
426
|
// =============================================================================
|
|
413
427
|
// Folder Tools
|
|
@@ -144,6 +144,33 @@ export function parseAppleScriptDate(appleScriptDate) {
|
|
|
144
144
|
// Return parsed date if valid, otherwise current date as fallback
|
|
145
145
|
return isNaN(parsed.getTime()) ? new Date() : parsed;
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Generates AppleScript code that creates a date variable with the given values.
|
|
149
|
+
*
|
|
150
|
+
* This approach is locale-independent, unlike `date "M/D/YYYY"` coercion which
|
|
151
|
+
* depends on the system's date format settings and would fail on non-US locales.
|
|
152
|
+
*
|
|
153
|
+
* @param date - JavaScript Date object
|
|
154
|
+
* @param varName - AppleScript variable name to assign (default: "thresholdDate")
|
|
155
|
+
* @returns AppleScript code that sets up the date variable
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* buildAppleScriptDateVar(new Date("2025-06-15T00:00:00"))
|
|
159
|
+
* // Returns multi-line AppleScript that sets thresholdDate to June 15, 2025 midnight
|
|
160
|
+
*/
|
|
161
|
+
export function buildAppleScriptDateVar(date, varName = "thresholdDate") {
|
|
162
|
+
const year = date.getFullYear();
|
|
163
|
+
const month = date.getMonth() + 1;
|
|
164
|
+
const day = date.getDate();
|
|
165
|
+
const timeInSeconds = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
|
|
166
|
+
return [
|
|
167
|
+
`set ${varName} to current date`,
|
|
168
|
+
`set year of ${varName} to ${year}`,
|
|
169
|
+
`set month of ${varName} to ${month}`,
|
|
170
|
+
`set day of ${varName} to ${day}`,
|
|
171
|
+
`set time of ${varName} to ${timeInSeconds}`,
|
|
172
|
+
].join("\n");
|
|
173
|
+
}
|
|
147
174
|
/**
|
|
148
175
|
* Parses AppleScript note properties output into structured data.
|
|
149
176
|
*
|
|
@@ -433,6 +460,8 @@ export class AppleNotesManager {
|
|
|
433
460
|
* @param searchContent - If true, search note bodies; if false, search titles
|
|
434
461
|
* @param account - Account to search in (defaults to iCloud)
|
|
435
462
|
* @param folder - Optional folder to limit search to
|
|
463
|
+
* @param modifiedSince - Optional ISO 8601 date string to filter notes modified on or after this date
|
|
464
|
+
* @param limit - Optional maximum number of results to return (default: no limit)
|
|
436
465
|
* @returns Array of matching notes (with minimal metadata)
|
|
437
466
|
*
|
|
438
467
|
* @example
|
|
@@ -445,25 +474,53 @@ export class AppleNotesManager {
|
|
|
445
474
|
*
|
|
446
475
|
* // Search within a specific folder
|
|
447
476
|
* const workNotes = manager.searchNotes("deadline", false, "iCloud", "Work");
|
|
477
|
+
*
|
|
478
|
+
* // Search only recently modified notes
|
|
479
|
+
* const recentNotes = manager.searchNotes("todo", true, undefined, undefined, "2025-01-01");
|
|
480
|
+
*
|
|
481
|
+
* // Search with a result limit
|
|
482
|
+
* const topResults = manager.searchNotes("project", false, undefined, undefined, undefined, 10);
|
|
448
483
|
* ```
|
|
449
484
|
*/
|
|
450
|
-
searchNotes(query, searchContent = false, account, folder) {
|
|
485
|
+
searchNotes(query, searchContent = false, account, folder, modifiedSince, limit) {
|
|
451
486
|
const targetAccount = this.resolveAccount(account);
|
|
452
487
|
const safeQuery = escapeForAppleScript(query);
|
|
488
|
+
const safeLimit = limit !== undefined && limit > 0 ? Math.floor(limit) : undefined;
|
|
453
489
|
// Build the where clause based on search type
|
|
454
490
|
// AppleScript uses 'name' for title and 'body' for content
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
491
|
+
const whereParts = [];
|
|
492
|
+
if (searchContent) {
|
|
493
|
+
whereParts.push(`body contains "${safeQuery}"`);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
whereParts.push(`name contains "${safeQuery}"`);
|
|
497
|
+
}
|
|
498
|
+
// Add date filter if specified (uses locale-safe date variable)
|
|
499
|
+
let dateSetup = "";
|
|
500
|
+
if (modifiedSince) {
|
|
501
|
+
const date = new Date(modifiedSince);
|
|
502
|
+
if (!isNaN(date.getTime())) {
|
|
503
|
+
dateSetup = buildAppleScriptDateVar(date) + "\n";
|
|
504
|
+
whereParts.push(`modification date >= thresholdDate`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const whereClause = whereParts.join(" and ");
|
|
458
508
|
// Build the notes source - either all notes or notes in a specific folder
|
|
459
509
|
const notesSource = folder ? `notes of folder "${escapeForAppleScript(folder)}"` : "notes";
|
|
510
|
+
// Build the limit logic for the repeat loop
|
|
511
|
+
// Note: The limit only reduces iteration over already-matched results from the whose clause,
|
|
512
|
+
// not the query itself. It controls output size, not AppleScript query performance.
|
|
513
|
+
const limitCheck = safeLimit !== undefined
|
|
514
|
+
? `
|
|
515
|
+
if (count of resultList) >= ${safeLimit} then exit repeat`
|
|
516
|
+
: "";
|
|
460
517
|
// Get names, IDs, and folder for each matching note
|
|
461
518
|
// We use a repeat loop to get all properties, separated by a delimiter
|
|
462
519
|
// Note: Some notes may have inaccessible containers, so we wrap in try/on error
|
|
463
520
|
const searchCommand = `
|
|
464
|
-
set matchingNotes to ${notesSource} where ${whereClause}
|
|
521
|
+
${dateSetup}set matchingNotes to ${notesSource} where ${whereClause}
|
|
465
522
|
set resultList to {}
|
|
466
|
-
repeat with n in matchingNotes
|
|
523
|
+
repeat with n in matchingNotes${limitCheck}
|
|
467
524
|
try
|
|
468
525
|
set noteName to name of n
|
|
469
526
|
set noteId to id of n
|
|
@@ -785,15 +842,60 @@ export class AppleNotesManager {
|
|
|
785
842
|
return true;
|
|
786
843
|
}
|
|
787
844
|
/**
|
|
788
|
-
* Lists all notes in an account, optionally filtered by folder.
|
|
845
|
+
* Lists all notes in an account, optionally filtered by folder, date, and limit.
|
|
789
846
|
*
|
|
790
847
|
* @param account - Account to list notes from (defaults to iCloud)
|
|
791
848
|
* @param folder - Optional folder to filter by
|
|
849
|
+
* @param modifiedSince - Optional ISO 8601 date string to filter notes modified on or after this date
|
|
850
|
+
* @param limit - Optional maximum number of results to return (default: no limit)
|
|
792
851
|
* @returns Array of note titles
|
|
793
852
|
*/
|
|
794
|
-
listNotes(account, folder) {
|
|
853
|
+
listNotes(account, folder, modifiedSince, limit) {
|
|
795
854
|
const targetAccount = this.resolveAccount(account);
|
|
796
|
-
|
|
855
|
+
const safeLimit = limit !== undefined && limit > 0 ? Math.floor(limit) : undefined;
|
|
856
|
+
// When date or limit filters are needed, use a repeat loop for fine-grained control
|
|
857
|
+
if (modifiedSince || safeLimit !== undefined) {
|
|
858
|
+
const baseNotesSource = folder
|
|
859
|
+
? `notes of folder "${escapeForAppleScript(folder)}"`
|
|
860
|
+
: "notes";
|
|
861
|
+
// Use whose clause for date filtering (locale-safe, no sort order assumption)
|
|
862
|
+
let dateSetup = "";
|
|
863
|
+
let notesSource = baseNotesSource;
|
|
864
|
+
if (modifiedSince) {
|
|
865
|
+
const date = new Date(modifiedSince);
|
|
866
|
+
if (!isNaN(date.getTime())) {
|
|
867
|
+
dateSetup = buildAppleScriptDateVar(date) + "\n";
|
|
868
|
+
notesSource = `(${baseNotesSource} whose modification date >= thresholdDate)`;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// Build the limit check
|
|
872
|
+
const limitCheck = safeLimit !== undefined
|
|
873
|
+
? `
|
|
874
|
+
if (count of resultList) >= ${safeLimit} then exit repeat`
|
|
875
|
+
: "";
|
|
876
|
+
const listCommand = `
|
|
877
|
+
${dateSetup}set resultList to {}
|
|
878
|
+
repeat with n in ${notesSource}${limitCheck}
|
|
879
|
+
set end of resultList to name of n
|
|
880
|
+
end repeat
|
|
881
|
+
set AppleScript's text item delimiters to "|||"
|
|
882
|
+
return resultList as text
|
|
883
|
+
`;
|
|
884
|
+
const script = buildAccountScopedScript({ account: targetAccount }, listCommand);
|
|
885
|
+
const result = executeAppleScript(script);
|
|
886
|
+
if (!result.success) {
|
|
887
|
+
console.error("Failed to list notes:", result.error);
|
|
888
|
+
return [];
|
|
889
|
+
}
|
|
890
|
+
if (!result.output.trim()) {
|
|
891
|
+
return [];
|
|
892
|
+
}
|
|
893
|
+
return result.output
|
|
894
|
+
.split("|||")
|
|
895
|
+
.map((item) => item.trim())
|
|
896
|
+
.filter((item) => item.length > 0);
|
|
897
|
+
}
|
|
898
|
+
// Simple path: no date or limit filters
|
|
797
899
|
let listCommand;
|
|
798
900
|
if (folder) {
|
|
799
901
|
const safeFolder = escapeForAppleScript(folder);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Script generation is verified by checking for expected AppleScript patterns
|
|
12
12
|
*/
|
|
13
13
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
14
|
-
import { AppleNotesManager, escapeForAppleScript, escapeHtmlForAppleScript, parseAppleScriptDate, } from "./appleNotesManager.js";
|
|
14
|
+
import { AppleNotesManager, escapeForAppleScript, escapeHtmlForAppleScript, buildAppleScriptDateVar, parseAppleScriptDate, } from "./appleNotesManager.js";
|
|
15
15
|
// Mock the AppleScript execution module
|
|
16
16
|
// This prevents actual osascript calls during testing
|
|
17
17
|
vi.mock("@/utils/applescript.js", () => ({
|
|
@@ -210,6 +210,41 @@ describe("parseAppleScriptDate", () => {
|
|
|
210
210
|
});
|
|
211
211
|
});
|
|
212
212
|
// =============================================================================
|
|
213
|
+
// buildAppleScriptDateVar Tests
|
|
214
|
+
// =============================================================================
|
|
215
|
+
describe("buildAppleScriptDateVar", () => {
|
|
216
|
+
it("generates locale-safe AppleScript date setup code", () => {
|
|
217
|
+
const date = new Date(2025, 5, 15, 14, 30, 0); // June 15, 2025 2:30 PM
|
|
218
|
+
const result = buildAppleScriptDateVar(date);
|
|
219
|
+
expect(result).toContain("set thresholdDate to current date");
|
|
220
|
+
expect(result).toContain("set year of thresholdDate to 2025");
|
|
221
|
+
expect(result).toContain("set month of thresholdDate to 6");
|
|
222
|
+
expect(result).toContain("set day of thresholdDate to 15");
|
|
223
|
+
// 14*3600 + 30*60 = 52200
|
|
224
|
+
expect(result).toContain("set time of thresholdDate to 52200");
|
|
225
|
+
});
|
|
226
|
+
it("handles midnight (time = 0)", () => {
|
|
227
|
+
const date = new Date(2025, 0, 1, 0, 0, 0); // Jan 1, 2025 midnight
|
|
228
|
+
const result = buildAppleScriptDateVar(date);
|
|
229
|
+
expect(result).toContain("set month of thresholdDate to 1");
|
|
230
|
+
expect(result).toContain("set day of thresholdDate to 1");
|
|
231
|
+
expect(result).toContain("set time of thresholdDate to 0");
|
|
232
|
+
});
|
|
233
|
+
it("uses custom variable name", () => {
|
|
234
|
+
const date = new Date(2025, 0, 1, 0, 0, 0);
|
|
235
|
+
const result = buildAppleScriptDateVar(date, "myDate");
|
|
236
|
+
expect(result).toContain("set myDate to current date");
|
|
237
|
+
expect(result).toContain("set year of myDate to 2025");
|
|
238
|
+
expect(result).toContain("set month of myDate to 1");
|
|
239
|
+
});
|
|
240
|
+
it("calculates time in seconds correctly", () => {
|
|
241
|
+
const date = new Date(2025, 11, 25, 9, 5, 3); // 9:05:03 AM
|
|
242
|
+
const result = buildAppleScriptDateVar(date);
|
|
243
|
+
// 9*3600 + 5*60 + 3 = 32703
|
|
244
|
+
expect(result).toContain("set time of thresholdDate to 32703");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
// =============================================================================
|
|
213
248
|
// AppleNotesManager Tests
|
|
214
249
|
// =============================================================================
|
|
215
250
|
describe("AppleNotesManager", () => {
|
|
@@ -393,6 +428,65 @@ describe("AppleNotesManager", () => {
|
|
|
393
428
|
expect(script).toContain('tell account "Exchange"');
|
|
394
429
|
expect(script).toContain('notes of folder "Projects"');
|
|
395
430
|
});
|
|
431
|
+
it("adds date filter when modifiedSince is provided", () => {
|
|
432
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
433
|
+
success: true,
|
|
434
|
+
output: "Recent Note|||x-coredata://ABC/ICNote/p1|||Notes",
|
|
435
|
+
});
|
|
436
|
+
manager.searchNotes("note", false, undefined, undefined, "2025-06-15T00:00:00");
|
|
437
|
+
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
438
|
+
// Locale-safe: uses variable setup instead of date "string"
|
|
439
|
+
expect(script).toContain("set thresholdDate to current date");
|
|
440
|
+
expect(script).toContain("set year of thresholdDate to 2025");
|
|
441
|
+
expect(script).toContain("set month of thresholdDate to 6");
|
|
442
|
+
expect(script).toContain("set day of thresholdDate to 15");
|
|
443
|
+
expect(script).toContain("modification date >= thresholdDate");
|
|
444
|
+
expect(script).toContain('name contains "note"');
|
|
445
|
+
});
|
|
446
|
+
it("combines date filter with content search", () => {
|
|
447
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
448
|
+
success: true,
|
|
449
|
+
output: "Note|||x-coredata://ABC/ICNote/p1|||Notes",
|
|
450
|
+
});
|
|
451
|
+
manager.searchNotes("keyword", true, undefined, undefined, "2025-01-01");
|
|
452
|
+
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
453
|
+
expect(script).toContain('body contains "keyword"');
|
|
454
|
+
expect(script).toContain("set thresholdDate to current date");
|
|
455
|
+
expect(script).toContain("modification date >= thresholdDate");
|
|
456
|
+
});
|
|
457
|
+
it("ignores invalid modifiedSince date", () => {
|
|
458
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
459
|
+
success: true,
|
|
460
|
+
output: "Note|||x-coredata://ABC/ICNote/p1|||Notes",
|
|
461
|
+
});
|
|
462
|
+
manager.searchNotes("note", false, undefined, undefined, "not-a-date");
|
|
463
|
+
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
464
|
+
expect(script).not.toContain("modification date");
|
|
465
|
+
});
|
|
466
|
+
it("applies limit to search results", () => {
|
|
467
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
468
|
+
success: true,
|
|
469
|
+
output: "Note 1|||x-coredata://ABC/ICNote/p1|||Notes",
|
|
470
|
+
});
|
|
471
|
+
manager.searchNotes("note", false, undefined, undefined, undefined, 5);
|
|
472
|
+
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
473
|
+
expect(script).toContain("(count of resultList) >= 5");
|
|
474
|
+
expect(script).toContain("exit repeat");
|
|
475
|
+
});
|
|
476
|
+
it("combines modifiedSince, limit, folder, and content search", () => {
|
|
477
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
478
|
+
success: true,
|
|
479
|
+
output: "Note|||x-coredata://ABC/ICNote/p1|||Work",
|
|
480
|
+
});
|
|
481
|
+
manager.searchNotes("project", true, "iCloud", "Work", "2025-03-01", 10);
|
|
482
|
+
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
483
|
+
expect(script).toContain('body contains "project"');
|
|
484
|
+
expect(script).toContain("set thresholdDate to current date");
|
|
485
|
+
expect(script).toContain("modification date >= thresholdDate");
|
|
486
|
+
expect(script).toContain('notes of folder "Work"');
|
|
487
|
+
expect(script).toContain("(count of resultList) >= 10");
|
|
488
|
+
expect(script).toContain('tell account "iCloud"');
|
|
489
|
+
});
|
|
396
490
|
});
|
|
397
491
|
// ---------------------------------------------------------------------------
|
|
398
492
|
// Note Content Retrieval
|
|
@@ -742,6 +836,61 @@ describe("AppleNotesManager", () => {
|
|
|
742
836
|
manager.listNotes("iCloud", "Work");
|
|
743
837
|
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('notes of folder "Work"'));
|
|
744
838
|
});
|
|
839
|
+
it("uses whose clause when modifiedSince is provided", () => {
|
|
840
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
841
|
+
success: true,
|
|
842
|
+
output: "Recent Note 1|||Recent Note 2",
|
|
843
|
+
});
|
|
844
|
+
const results = manager.listNotes(undefined, undefined, "2025-06-15T00:00:00");
|
|
845
|
+
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
846
|
+
// Locale-safe: uses variable setup + whose clause (no sort order assumption)
|
|
847
|
+
expect(script).toContain("set thresholdDate to current date");
|
|
848
|
+
expect(script).toContain("set year of thresholdDate to 2025");
|
|
849
|
+
expect(script).toContain("set month of thresholdDate to 6");
|
|
850
|
+
expect(script).toContain("set day of thresholdDate to 15");
|
|
851
|
+
expect(script).toContain("whose modification date >= thresholdDate");
|
|
852
|
+
expect(results).toEqual(["Recent Note 1", "Recent Note 2"]);
|
|
853
|
+
});
|
|
854
|
+
it("uses repeat loop when limit is provided", () => {
|
|
855
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
856
|
+
success: true,
|
|
857
|
+
output: "Note 1|||Note 2|||Note 3",
|
|
858
|
+
});
|
|
859
|
+
const results = manager.listNotes(undefined, undefined, undefined, 3);
|
|
860
|
+
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
861
|
+
expect(script).toContain("(count of resultList) >= 3");
|
|
862
|
+
expect(results).toEqual(["Note 1", "Note 2", "Note 3"]);
|
|
863
|
+
});
|
|
864
|
+
it("combines folder, modifiedSince, and limit", () => {
|
|
865
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
866
|
+
success: true,
|
|
867
|
+
output: "Work Note|||Another Work Note",
|
|
868
|
+
});
|
|
869
|
+
manager.listNotes("iCloud", "Work", "2025-01-01", 10);
|
|
870
|
+
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
871
|
+
expect(script).toContain("whose modification date >= thresholdDate");
|
|
872
|
+
expect(script).toContain('notes of folder "Work"');
|
|
873
|
+
expect(script).toContain("(count of resultList) >= 10");
|
|
874
|
+
});
|
|
875
|
+
it("returns empty array when modifiedSince yields no results", () => {
|
|
876
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
877
|
+
success: true,
|
|
878
|
+
output: "",
|
|
879
|
+
});
|
|
880
|
+
const results = manager.listNotes(undefined, undefined, "2099-01-01");
|
|
881
|
+
expect(results).toEqual([]);
|
|
882
|
+
});
|
|
883
|
+
it("ignores invalid modifiedSince date and falls back to limit-only", () => {
|
|
884
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
885
|
+
success: true,
|
|
886
|
+
output: "Note 1|||Note 2",
|
|
887
|
+
});
|
|
888
|
+
const results = manager.listNotes(undefined, undefined, "not-a-date", 5);
|
|
889
|
+
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
890
|
+
expect(script).not.toContain("thresholdDate");
|
|
891
|
+
expect(script).toContain("(count of resultList) >= 5");
|
|
892
|
+
expect(results).toEqual(["Note 1", "Note 2"]);
|
|
893
|
+
});
|
|
745
894
|
});
|
|
746
895
|
// ---------------------------------------------------------------------------
|
|
747
896
|
// Folder Operations
|