apple-notes-mcp 2.5.7 → 2.5.8

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 (34) hide show
  1. package/README.md +8 -5
  2. package/build/index.js +42669 -1077
  3. package/package.json +3 -3
  4. package/build/index.test.js +0 -446
  5. package/build/services/__fixtures__/notesNormalizedHtml.js +0 -32
  6. package/build/services/appleNotesManager.js +0 -2634
  7. package/build/services/appleNotesManager.test.js +0 -2416
  8. package/build/services/attachmentSave.test.js +0 -85
  9. package/build/services/fileConfig.js +0 -51
  10. package/build/services/fileConfig.test.js +0 -48
  11. package/build/services/notesHtmlMarkdown.test.js +0 -55
  12. package/build/tools/doctor.js +0 -50
  13. package/build/tools/doctor.test.js +0 -42
  14. package/build/tools/resourcesAndPrompts.js +0 -70
  15. package/build/tools/resourcesAndPrompts.test.js +0 -63
  16. package/build/types.js +0 -13
  17. package/build/utils/applescript.js +0 -421
  18. package/build/utils/applescript.test.js +0 -342
  19. package/build/utils/attachmentFs.js +0 -97
  20. package/build/utils/attachmentFs.test.js +0 -69
  21. package/build/utils/checklistParser.js +0 -259
  22. package/build/utils/checklistParser.test.js +0 -230
  23. package/build/utils/contentWarnings.js +0 -44
  24. package/build/utils/contentWarnings.test.js +0 -52
  25. package/build/utils/hashtags.js +0 -56
  26. package/build/utils/hashtags.test.js +0 -45
  27. package/build/utils/jxa.js +0 -139
  28. package/build/utils/jxa.test.js +0 -134
  29. package/build/utils/noteMetadata.js +0 -135
  30. package/build/utils/noteMetadata.test.js +0 -106
  31. package/build/utils/protobuf.js +0 -151
  32. package/build/utils/protobuf.test.js +0 -138
  33. package/build/utils/syncDetection.js +0 -242
  34. package/build/utils/syncDetection.test.js +0 -228
@@ -1,151 +0,0 @@
1
- /**
2
- * Minimal Protobuf Wire Format Decoder
3
- *
4
- * Decodes raw protobuf binary data without requiring a .proto schema file.
5
- * Only implements the subset of wire types needed for reading Apple Notes
6
- * checklist state from the NoteStore protobuf format.
7
- *
8
- * Wire types supported:
9
- * - 0: Varint (integers, booleans)
10
- * - 2: Length-delimited (strings, bytes, embedded messages)
11
- *
12
- * @module utils/protobuf
13
- */
14
- /**
15
- * Protobuf wire types used in Apple Notes data.
16
- */
17
- export const WIRE_TYPE = {
18
- VARINT: 0,
19
- LENGTH_DELIMITED: 2,
20
- };
21
- /**
22
- * Decodes a varint from the buffer at the given offset.
23
- *
24
- * Varints use 7 bits per byte with the high bit as a continuation flag.
25
- * Supports up to 64-bit values (though we only need small integers).
26
- *
27
- * @param buf - The protobuf binary data
28
- * @param offset - Starting byte position
29
- * @returns Tuple of [decoded value, new offset after the varint]
30
- */
31
- export function decodeVarint(buf, offset) {
32
- let result = 0;
33
- let shift = 0;
34
- let pos = offset;
35
- while (pos < buf.length) {
36
- const byte = buf[pos];
37
- result |= (byte & 0x7f) << shift;
38
- pos++;
39
- if ((byte & 0x80) === 0) {
40
- return [result, pos];
41
- }
42
- shift += 7;
43
- if (shift > 35) {
44
- // For our use case (small field numbers, small integers),
45
- // values requiring more than 35 bits are unexpected
46
- throw new Error(`Varint too long at offset ${offset}`);
47
- }
48
- }
49
- throw new Error(`Unexpected end of buffer reading varint at offset ${offset}`);
50
- }
51
- /**
52
- * Decodes all fields from a protobuf message buffer.
53
- *
54
- * Iterates through the buffer, decoding tag-value pairs. Unknown wire types
55
- * cause parsing to stop (returns fields decoded so far).
56
- *
57
- * @param buf - The protobuf binary data
58
- * @returns Array of decoded fields in order
59
- */
60
- export function decodeMessage(buf) {
61
- const fields = [];
62
- let offset = 0;
63
- while (offset < buf.length) {
64
- let tag;
65
- [tag, offset] = decodeVarint(buf, offset);
66
- const fieldNumber = tag >>> 3;
67
- const wireType = tag & 0x07;
68
- if (wireType === WIRE_TYPE.VARINT) {
69
- let value;
70
- [value, offset] = decodeVarint(buf, offset);
71
- fields.push({ fieldNumber, wireType, value });
72
- }
73
- else if (wireType === WIRE_TYPE.LENGTH_DELIMITED) {
74
- let length;
75
- [length, offset] = decodeVarint(buf, offset);
76
- if (offset + length > buf.length) {
77
- break; // Truncated data, return what we have
78
- }
79
- const value = buf.slice(offset, offset + length);
80
- fields.push({ fieldNumber, wireType, value });
81
- offset += length;
82
- }
83
- else if (wireType === 5) {
84
- // 32-bit fixed — skip 4 bytes
85
- offset += 4;
86
- }
87
- else if (wireType === 1) {
88
- // 64-bit fixed — skip 8 bytes
89
- offset += 8;
90
- }
91
- else {
92
- // Unknown wire type — stop parsing
93
- break;
94
- }
95
- }
96
- return fields;
97
- }
98
- /**
99
- * Gets all fields with a specific field number from decoded message fields.
100
- *
101
- * @param fields - Decoded protobuf fields
102
- * @param fieldNumber - The field number to filter for
103
- * @returns Matching fields
104
- */
105
- export function getFields(fields, fieldNumber) {
106
- return fields.filter((f) => f.fieldNumber === fieldNumber);
107
- }
108
- /**
109
- * Gets the first field with a specific field number.
110
- *
111
- * @param fields - Decoded protobuf fields
112
- * @param fieldNumber - The field number to find
113
- * @returns The first matching field, or undefined
114
- */
115
- export function getField(fields, fieldNumber) {
116
- return fields.find((f) => f.fieldNumber === fieldNumber);
117
- }
118
- /**
119
- * Extracts the varint value from a field, returning undefined if not a varint.
120
- */
121
- export function varintValue(field) {
122
- if (!field || typeof field.value !== "number")
123
- return undefined;
124
- return field.value;
125
- }
126
- /**
127
- * Extracts the bytes value from a field, returning undefined if not length-delimited.
128
- */
129
- export function bytesValue(field) {
130
- if (!field || !(field.value instanceof Uint8Array))
131
- return undefined;
132
- return field.value;
133
- }
134
- /**
135
- * Decodes a length-delimited field as a UTF-8 string.
136
- */
137
- export function stringValue(field) {
138
- const bytes = bytesValue(field);
139
- if (!bytes)
140
- return undefined;
141
- return new TextDecoder().decode(bytes);
142
- }
143
- /**
144
- * Decodes a length-delimited field as an embedded message.
145
- */
146
- export function embeddedMessage(field) {
147
- const bytes = bytesValue(field);
148
- if (!bytes)
149
- return undefined;
150
- return decodeMessage(bytes);
151
- }
@@ -1,138 +0,0 @@
1
- /**
2
- * Tests for the minimal protobuf wire format decoder.
3
- */
4
- import { describe, it, expect } from "vitest";
5
- import { decodeVarint, decodeMessage, getField, getFields, varintValue, bytesValue, stringValue, embeddedMessage, WIRE_TYPE, } from "./protobuf.js";
6
- describe("decodeVarint", () => {
7
- it("decodes single-byte varint", () => {
8
- // 0x08 = field 1, varint; 0x05 = value 5
9
- const [value, offset] = decodeVarint(new Uint8Array([5]), 0);
10
- expect(value).toBe(5);
11
- expect(offset).toBe(1);
12
- });
13
- it("decodes multi-byte varint", () => {
14
- // 300 = 0xAC 0x02
15
- const [value, offset] = decodeVarint(new Uint8Array([0xac, 0x02]), 0);
16
- expect(value).toBe(300);
17
- expect(offset).toBe(2);
18
- });
19
- it("decodes varint at non-zero offset", () => {
20
- const [value, offset] = decodeVarint(new Uint8Array([0xff, 0x03]), 1);
21
- expect(value).toBe(3);
22
- expect(offset).toBe(2);
23
- });
24
- it("decodes zero", () => {
25
- const [value, offset] = decodeVarint(new Uint8Array([0x00]), 0);
26
- expect(value).toBe(0);
27
- expect(offset).toBe(1);
28
- });
29
- it("throws on truncated varint", () => {
30
- expect(() => decodeVarint(new Uint8Array([0x80]), 0)).toThrow("Unexpected end of buffer");
31
- });
32
- });
33
- describe("decodeMessage", () => {
34
- it("decodes a message with a varint field", () => {
35
- // Field 1, wire type 0 (varint), value 150
36
- // Tag: (1 << 3) | 0 = 0x08
37
- // Value: 150 = 0x96 0x01
38
- const buf = new Uint8Array([0x08, 0x96, 0x01]);
39
- const fields = decodeMessage(buf);
40
- expect(fields).toHaveLength(1);
41
- expect(fields[0].fieldNumber).toBe(1);
42
- expect(fields[0].wireType).toBe(WIRE_TYPE.VARINT);
43
- expect(fields[0].value).toBe(150);
44
- });
45
- it("decodes a message with a length-delimited field", () => {
46
- // Field 2, wire type 2 (length-delimited), value "hi"
47
- // Tag: (2 << 3) | 2 = 0x12
48
- // Length: 2
49
- // Data: 0x68 0x69 = "hi"
50
- const buf = new Uint8Array([0x12, 0x02, 0x68, 0x69]);
51
- const fields = decodeMessage(buf);
52
- expect(fields).toHaveLength(1);
53
- expect(fields[0].fieldNumber).toBe(2);
54
- expect(fields[0].wireType).toBe(WIRE_TYPE.LENGTH_DELIMITED);
55
- expect(fields[0].value).toBeInstanceOf(Uint8Array);
56
- expect(new TextDecoder().decode(fields[0].value)).toBe("hi");
57
- });
58
- it("decodes multiple fields", () => {
59
- // Field 1 varint=1, Field 2 varint=2
60
- const buf = new Uint8Array([0x08, 0x01, 0x10, 0x02]);
61
- const fields = decodeMessage(buf);
62
- expect(fields).toHaveLength(2);
63
- expect(fields[0].fieldNumber).toBe(1);
64
- expect(fields[0].value).toBe(1);
65
- expect(fields[1].fieldNumber).toBe(2);
66
- expect(fields[1].value).toBe(2);
67
- });
68
- it("handles empty buffer", () => {
69
- const fields = decodeMessage(new Uint8Array([]));
70
- expect(fields).toHaveLength(0);
71
- });
72
- it("handles truncated length-delimited field gracefully", () => {
73
- // Tag for field 2 length-delimited, length=10, but only 2 bytes of data
74
- const buf = new Uint8Array([0x12, 0x0a, 0x68, 0x69]);
75
- const fields = decodeMessage(buf);
76
- // Should stop parsing rather than crash
77
- expect(fields).toHaveLength(0);
78
- });
79
- });
80
- describe("field accessors", () => {
81
- // Build a test message with: field 1 = varint 42, field 2 = "hello", field 2 = "world"
82
- const buf = new Uint8Array([
83
- 0x08,
84
- 0x2a, // field 1, varint, value 42
85
- 0x12,
86
- 0x05,
87
- 0x68,
88
- 0x65,
89
- 0x6c,
90
- 0x6c,
91
- 0x6f, // field 2, "hello"
92
- 0x12,
93
- 0x05,
94
- 0x77,
95
- 0x6f,
96
- 0x72,
97
- 0x6c,
98
- 0x64, // field 2, "world"
99
- ]);
100
- const fields = decodeMessage(buf);
101
- it("getField returns first matching field", () => {
102
- const f = getField(fields, 2);
103
- expect(f).toBeDefined();
104
- expect(new TextDecoder().decode(f.value)).toBe("hello");
105
- });
106
- it("getFields returns all matching fields", () => {
107
- const matches = getFields(fields, 2);
108
- expect(matches).toHaveLength(2);
109
- });
110
- it("getField returns undefined for missing field", () => {
111
- expect(getField(fields, 99)).toBeUndefined();
112
- });
113
- it("varintValue extracts number", () => {
114
- expect(varintValue(getField(fields, 1))).toBe(42);
115
- });
116
- it("varintValue returns undefined for non-varint", () => {
117
- expect(varintValue(getField(fields, 2))).toBeUndefined();
118
- });
119
- it("bytesValue extracts Uint8Array", () => {
120
- const bytes = bytesValue(getField(fields, 2));
121
- expect(bytes).toBeInstanceOf(Uint8Array);
122
- expect(bytes.length).toBe(5);
123
- });
124
- it("stringValue decodes UTF-8", () => {
125
- expect(stringValue(getField(fields, 2))).toBe("hello");
126
- });
127
- it("embeddedMessage decodes nested message", () => {
128
- // Create a field 1 containing an embedded message with field 1 = varint 7
129
- const inner = new Uint8Array([0x08, 0x07]); // field 1, varint 7
130
- // Wrap as field 1, length-delimited
131
- const outer = new Uint8Array([0x0a, 0x02, ...inner]);
132
- const outerFields = decodeMessage(outer);
133
- const nested = embeddedMessage(getField(outerFields, 1));
134
- expect(nested).toBeDefined();
135
- expect(nested).toHaveLength(1);
136
- expect(varintValue(getField(nested, 1))).toBe(7);
137
- });
138
- });
@@ -1,242 +0,0 @@
1
- /**
2
- * iCloud Sync Detection Utilities
3
- *
4
- * Detects when iCloud sync is in progress or has pending changes,
5
- * allowing operations to warn users and verify results.
6
- *
7
- * Detection methods:
8
- * 1. Query NoteStore.sqlite for pending sync changes (ZICCLOUDSTATE)
9
- * 2. Check for recent database transaction activity
10
- * 3. Monitor WAL file modification time
11
- *
12
- * @module utils/syncDetection
13
- */
14
- import { execFileSync } from "child_process";
15
- import * as fs from "fs";
16
- import * as path from "path";
17
- import * as os from "os";
18
- const NOTES_DB_PATH = path.join(os.homedir(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
19
- const WAL_PATH = `${NOTES_DB_PATH}-wal`;
20
- // How recent (in seconds) activity must be to be considered "active sync"
21
- const RECENT_ACTIVITY_THRESHOLD_SECONDS = 5;
22
- // Delay between operation and verification read (ms)
23
- const VERIFICATION_DELAY_MS = 500;
24
- // Cache TTL in milliseconds (2 seconds default)
25
- const SYNC_STATUS_CACHE_TTL_MS = 2000;
26
- // Cached sync status
27
- let cachedSyncStatus = null;
28
- let cacheTimestamp = 0;
29
- /**
30
- * Clears the sync status cache.
31
- * Useful for testing or when forcing a fresh check.
32
- */
33
- export function clearSyncStatusCache() {
34
- cachedSyncStatus = null;
35
- cacheTimestamp = 0;
36
- }
37
- /**
38
- * Gets the current iCloud sync status by querying the Notes database.
39
- *
40
- * Results are cached for 2 seconds to avoid excessive database queries
41
- * during rapid successive operations.
42
- *
43
- * @param useCache - Whether to use cached results (default: true)
44
- * @returns Sync status information
45
- */
46
- export function getSyncStatus(useCache = true) {
47
- // Return cached result if valid
48
- if (useCache && cachedSyncStatus && Date.now() - cacheTimestamp < SYNC_STATUS_CACHE_TTL_MS) {
49
- return cachedSyncStatus;
50
- }
51
- const status = {
52
- syncDetected: false,
53
- pendingUpload: 0,
54
- secondsSinceLastChange: Infinity,
55
- recentActivity: false,
56
- };
57
- try {
58
- // Check if database exists
59
- if (!fs.existsSync(NOTES_DB_PATH)) {
60
- status.error = "Notes database not found";
61
- cachedSyncStatus = status;
62
- cacheTimestamp = Date.now();
63
- return status;
64
- }
65
- // Check WAL file modification time for recent activity
66
- if (fs.existsSync(WAL_PATH)) {
67
- const walStats = fs.statSync(WAL_PATH);
68
- const secondsAgo = (Date.now() - walStats.mtimeMs) / 1000;
69
- status.secondsSinceLastChange = Math.round(secondsAgo);
70
- status.recentActivity = secondsAgo < RECENT_ACTIVITY_THRESHOLD_SECONDS;
71
- }
72
- // Query for pending sync changes
73
- // Use a read-only connection and timeout to avoid blocking
74
- const query = `
75
- SELECT COUNT(*) FROM ZICCLOUDSTATE
76
- WHERE ZCURRENTLOCALVERSION > ZLATESTVERSIONSYNCEDTOCLOUD
77
- AND ZLATESTVERSIONSYNCEDTOCLOUD IS NOT NULL;
78
- `;
79
- // Use execFileSync (argv array, no shell) to match the sibling sqlite callers
80
- // (checklistParser.ts, noteMetadata.ts). The values here aren't user-controlled,
81
- // but argv form avoids shell quoting/interpolation entirely.
82
- const result = execFileSync("sqlite3", ["-readonly", NOTES_DB_PATH, query.replace(/\n/g, " ")], {
83
- encoding: "utf8",
84
- timeout: 5000,
85
- stdio: ["pipe", "pipe", "pipe"],
86
- });
87
- status.pendingUpload = parseInt(result.trim(), 10) || 0;
88
- // Determine if sync is detected
89
- status.syncDetected = status.pendingUpload > 0 || status.recentActivity;
90
- // Generate warning message
91
- if (status.syncDetected) {
92
- const reasons = [];
93
- if (status.pendingUpload > 0) {
94
- reasons.push(`${status.pendingUpload} item(s) pending upload`);
95
- }
96
- if (status.recentActivity) {
97
- reasons.push(`database modified ${status.secondsSinceLastChange}s ago`);
98
- }
99
- status.warning = `iCloud sync in progress: ${reasons.join(", ")}. Results may be incomplete or change shortly.`;
100
- }
101
- }
102
- catch (error) {
103
- // Don't fail the operation due to sync detection errors
104
- status.error = error instanceof Error ? error.message : "Failed to check sync status";
105
- }
106
- // Cache the result
107
- cachedSyncStatus = status;
108
- cacheTimestamp = Date.now();
109
- return status;
110
- }
111
- /**
112
- * Logs a sync warning if sync is detected.
113
- *
114
- * @param status - The sync status to check
115
- * @param operation - Name of the operation being performed
116
- */
117
- export function logSyncWarning(status, operation) {
118
- if (status.warning) {
119
- console.error(`[Sync Warning] ${operation}: ${status.warning}`);
120
- }
121
- }
122
- /**
123
- * Wraps an async operation with sync detection and verification.
124
- *
125
- * 1. Checks sync status before the operation
126
- * 2. Logs a warning if sync is detected
127
- * 3. Executes the operation
128
- * 4. Waits briefly and checks sync status again
129
- * 5. Determines if sync may have interfered
130
- *
131
- * @param operation - Name of the operation (for logging)
132
- * @param fn - The async operation to execute
133
- * @returns Result with sync status information
134
- */
135
- export async function withSyncAwareness(operation, fn) {
136
- // Check sync status before operation
137
- const syncBefore = getSyncStatus();
138
- // Log warning if sync detected
139
- if (syncBefore.syncDetected) {
140
- logSyncWarning(syncBefore, operation);
141
- }
142
- // Execute the operation
143
- const result = await fn();
144
- // Wait briefly for any sync activity to settle
145
- await new Promise((resolve) => setTimeout(resolve, VERIFICATION_DELAY_MS));
146
- // Check sync status after operation (bypass cache for fresh data)
147
- const syncAfter = getSyncStatus(false);
148
- // Determine if sync may have interfered
149
- // Interference is likely if:
150
- // 1. Sync was active before and pending count changed
151
- // 2. Database was modified during the operation
152
- const pendingChanged = syncBefore.pendingUpload !== syncAfter.pendingUpload;
153
- const wasRecentBefore = syncBefore.recentActivity;
154
- const isRecentAfter = syncAfter.recentActivity;
155
- const syncInterference = (syncBefore.syncDetected && pendingChanged) || (wasRecentBefore && isRecentAfter);
156
- let interferenceWarning;
157
- if (syncInterference) {
158
- interferenceWarning =
159
- `iCloud sync activity detected during "${operation}". ` +
160
- `Pending items: ${syncBefore.pendingUpload} → ${syncAfter.pendingUpload}. ` +
161
- `Results may have been affected by sync.`;
162
- console.error(`[Sync Interference] ${interferenceWarning}`);
163
- }
164
- return {
165
- result,
166
- syncBefore,
167
- syncAfter,
168
- syncInterference,
169
- interferenceWarning,
170
- };
171
- }
172
- /**
173
- * Wraps a sync operation with sync detection and verification.
174
- * Synchronous version for AppleScript operations.
175
- *
176
- * @param operation - Name of the operation (for logging)
177
- * @param fn - The sync operation to execute
178
- * @returns Result with sync status information
179
- */
180
- export function withSyncAwarenessSync(operation, fn) {
181
- // Check sync status before operation
182
- const syncBefore = getSyncStatus();
183
- // Log warning if sync detected
184
- if (syncBefore.syncDetected) {
185
- logSyncWarning(syncBefore, operation);
186
- }
187
- // Execute the operation
188
- const result = fn();
189
- // Check sync status after operation (bypass cache for fresh data)
190
- const syncAfter = getSyncStatus(false);
191
- // Determine if sync may have interfered
192
- const pendingChanged = syncBefore.pendingUpload !== syncAfter.pendingUpload;
193
- const wasRecentBefore = syncBefore.recentActivity;
194
- const isRecentAfter = syncAfter.recentActivity;
195
- const syncInterference = (syncBefore.syncDetected && pendingChanged) || (wasRecentBefore && isRecentAfter);
196
- let interferenceWarning;
197
- if (syncInterference) {
198
- interferenceWarning =
199
- `iCloud sync activity detected during "${operation}". ` +
200
- `Pending items: ${syncBefore.pendingUpload} → ${syncAfter.pendingUpload}. ` +
201
- `Results may have been affected by sync.`;
202
- console.error(`[Sync Interference] ${interferenceWarning}`);
203
- }
204
- return {
205
- result,
206
- syncBefore,
207
- syncAfter,
208
- syncInterference,
209
- interferenceWarning,
210
- };
211
- }
212
- /**
213
- * Checks if sync is currently active.
214
- * Convenience method for simple sync checks.
215
- *
216
- * @returns true if sync activity is detected
217
- */
218
- export function isSyncActive() {
219
- return getSyncStatus().syncDetected;
220
- }
221
- /**
222
- * Gets a human-readable sync status summary.
223
- *
224
- * @returns Status summary string
225
- */
226
- export function getSyncStatusSummary() {
227
- const status = getSyncStatus();
228
- if (status.error) {
229
- return `Sync status unknown: ${status.error}`;
230
- }
231
- if (!status.syncDetected) {
232
- return "iCloud sync: Idle";
233
- }
234
- const parts = ["iCloud sync: Active"];
235
- if (status.pendingUpload > 0) {
236
- parts.push(`${status.pendingUpload} pending upload(s)`);
237
- }
238
- if (status.recentActivity) {
239
- parts.push(`last activity ${status.secondsSinceLastChange}s ago`);
240
- }
241
- return parts.join(" - ");
242
- }