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.
- package/README.md +8 -5
- package/build/index.js +42669 -1077
- package/package.json +3 -3
- package/build/index.test.js +0 -446
- package/build/services/__fixtures__/notesNormalizedHtml.js +0 -32
- package/build/services/appleNotesManager.js +0 -2634
- package/build/services/appleNotesManager.test.js +0 -2416
- package/build/services/attachmentSave.test.js +0 -85
- package/build/services/fileConfig.js +0 -51
- package/build/services/fileConfig.test.js +0 -48
- package/build/services/notesHtmlMarkdown.test.js +0 -55
- package/build/tools/doctor.js +0 -50
- package/build/tools/doctor.test.js +0 -42
- package/build/tools/resourcesAndPrompts.js +0 -70
- package/build/tools/resourcesAndPrompts.test.js +0 -63
- package/build/types.js +0 -13
- package/build/utils/applescript.js +0 -421
- package/build/utils/applescript.test.js +0 -342
- package/build/utils/attachmentFs.js +0 -97
- package/build/utils/attachmentFs.test.js +0 -69
- package/build/utils/checklistParser.js +0 -259
- package/build/utils/checklistParser.test.js +0 -230
- package/build/utils/contentWarnings.js +0 -44
- package/build/utils/contentWarnings.test.js +0 -52
- package/build/utils/hashtags.js +0 -56
- package/build/utils/hashtags.test.js +0 -45
- package/build/utils/jxa.js +0 -139
- package/build/utils/jxa.test.js +0 -134
- package/build/utils/noteMetadata.js +0 -135
- package/build/utils/noteMetadata.test.js +0 -106
- package/build/utils/protobuf.js +0 -151
- package/build/utils/protobuf.test.js +0 -138
- package/build/utils/syncDetection.js +0 -242
- package/build/utils/syncDetection.test.js +0 -228
package/build/utils/protobuf.js
DELETED
|
@@ -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
|
-
}
|