@mattheworiordan/remi 0.1.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 +207 -0
- package/dist/cli/commands/add.d.ts +7 -0
- package/dist/cli/commands/add.js +36 -0
- package/dist/cli/commands/add.js.map +1 -0
- package/dist/cli/commands/authorize.d.ts +1 -0
- package/dist/cli/commands/authorize.js +75 -0
- package/dist/cli/commands/authorize.js.map +1 -0
- package/dist/cli/commands/complete.d.ts +3 -0
- package/dist/cli/commands/complete.js +9 -0
- package/dist/cli/commands/complete.js.map +1 -0
- package/dist/cli/commands/create-list.d.ts +1 -0
- package/dist/cli/commands/create-list.js +7 -0
- package/dist/cli/commands/create-list.js.map +1 -0
- package/dist/cli/commands/create-section.d.ts +1 -0
- package/dist/cli/commands/create-section.js +7 -0
- package/dist/cli/commands/create-section.js.map +1 -0
- package/dist/cli/commands/delete-list.d.ts +3 -0
- package/dist/cli/commands/delete-list.js +11 -0
- package/dist/cli/commands/delete-list.js.map +1 -0
- package/dist/cli/commands/delete-section.d.ts +1 -0
- package/dist/cli/commands/delete-section.js +7 -0
- package/dist/cli/commands/delete-section.js.map +1 -0
- package/dist/cli/commands/delete.d.ts +4 -0
- package/dist/cli/commands/delete.js +14 -0
- package/dist/cli/commands/delete.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +4 -0
- package/dist/cli/commands/doctor.js +180 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/list.d.ts +4 -0
- package/dist/cli/commands/list.js +11 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/lists.d.ts +1 -0
- package/dist/cli/commands/lists.js +7 -0
- package/dist/cli/commands/lists.js.map +1 -0
- package/dist/cli/commands/move.d.ts +3 -0
- package/dist/cli/commands/move.js +11 -0
- package/dist/cli/commands/move.js.map +1 -0
- package/dist/cli/commands/overdue.d.ts +1 -0
- package/dist/cli/commands/overdue.js +11 -0
- package/dist/cli/commands/overdue.js.map +1 -0
- package/dist/cli/commands/search.d.ts +1 -0
- package/dist/cli/commands/search.js +11 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/sections.d.ts +1 -0
- package/dist/cli/commands/sections.js +7 -0
- package/dist/cli/commands/sections.js.map +1 -0
- package/dist/cli/commands/today.d.ts +1 -0
- package/dist/cli/commands/today.js +11 -0
- package/dist/cli/commands/today.js.map +1 -0
- package/dist/cli/commands/upcoming.d.ts +3 -0
- package/dist/cli/commands/upcoming.js +12 -0
- package/dist/cli/commands/upcoming.js.map +1 -0
- package/dist/cli/commands/update.d.ts +7 -0
- package/dist/cli/commands/update.js +17 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +232 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.d.ts +26 -0
- package/dist/cli/output.js +234 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/core/checksum.d.ts +9 -0
- package/dist/core/checksum.js +13 -0
- package/dist/core/checksum.js.map +1 -0
- package/dist/core/dateparse.d.ts +7 -0
- package/dist/core/dateparse.js +27 -0
- package/dist/core/dateparse.js.map +1 -0
- package/dist/core/errors.d.ts +26 -0
- package/dist/core/errors.js +34 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/eventkit.d.ts +38 -0
- package/dist/core/eventkit.js +127 -0
- package/dist/core/eventkit.js.map +1 -0
- package/dist/core/lookup.d.ts +10 -0
- package/dist/core/lookup.js +32 -0
- package/dist/core/lookup.js.map +1 -0
- package/dist/core/membership.d.ts +30 -0
- package/dist/core/membership.js +92 -0
- package/dist/core/membership.js.map +1 -0
- package/dist/core/recurrence.d.ts +14 -0
- package/dist/core/recurrence.js +90 -0
- package/dist/core/recurrence.js.map +1 -0
- package/dist/core/reminderkit.d.ts +33 -0
- package/dist/core/reminderkit.js +154 -0
- package/dist/core/reminderkit.js.map +1 -0
- package/dist/core/sqlite.d.ts +65 -0
- package/dist/core/sqlite.js +175 -0
- package/dist/core/sqlite.js.map +1 -0
- package/dist/core/tokenmap.d.ts +29 -0
- package/dist/core/tokenmap.js +52 -0
- package/dist/core/tokenmap.js.map +1 -0
- package/dist/reminders-helper +0 -0
- package/dist/section-helper +0 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
- package/src/swift/Info.plist +16 -0
- package/src/swift/build.sh +47 -0
- package/src/swift/reminders-helper.swift +697 -0
- package/src/swift/section-helper.swift +794 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite access for Apple Reminders database.
|
|
3
|
+
*
|
|
4
|
+
* Uses better-sqlite3 for proper transactions and prepared statements.
|
|
5
|
+
* This is a key improvement over the predecessor which shelled out to /usr/bin/sqlite3
|
|
6
|
+
* for each statement, making the 3-step membership write non-atomic.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import Database from "better-sqlite3";
|
|
12
|
+
import { ErrorCode, RemiCommandError } from "./errors.js";
|
|
13
|
+
const STORES_DIR = join(homedir(), "Library/Group Containers/group.com.apple.reminders/Container_v1/Stores");
|
|
14
|
+
let cachedDbPath = null;
|
|
15
|
+
let cachedDb = null;
|
|
16
|
+
/**
|
|
17
|
+
* Find the active Apple Reminders SQLite database.
|
|
18
|
+
* Scans all .sqlite files and returns the one with actual reminder data.
|
|
19
|
+
*/
|
|
20
|
+
export function findRemindersDbPath() {
|
|
21
|
+
if (cachedDbPath)
|
|
22
|
+
return cachedDbPath;
|
|
23
|
+
if (!existsSync(STORES_DIR)) {
|
|
24
|
+
throw new RemiCommandError(ErrorCode.DB_NOT_FOUND, `Reminders data directory not found at ${STORES_DIR}`, "Is Apple Reminders set up on this Mac?");
|
|
25
|
+
}
|
|
26
|
+
let dirContents;
|
|
27
|
+
try {
|
|
28
|
+
dirContents = readdirSync(STORES_DIR);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32
|
+
if (msg.includes("EPERM") || msg.includes("operation not permitted")) {
|
|
33
|
+
throw new RemiCommandError(ErrorCode.PERMISSION_DENIED, "Cannot access Reminders database — permission denied", "Section features require filesystem access. Grant Full Disk Access to your terminal app in System Settings > Privacy & Security > Full Disk Access. Basic reminder operations (without sections) work with just Reminders access.");
|
|
34
|
+
}
|
|
35
|
+
throw new RemiCommandError(ErrorCode.DB_NOT_FOUND, `Cannot read Reminders data directory: ${msg}`);
|
|
36
|
+
}
|
|
37
|
+
const sqliteFiles = dirContents.filter((f) => f.endsWith(".sqlite") && !f.includes("-wal") && !f.includes("-shm"));
|
|
38
|
+
for (const file of sqliteFiles) {
|
|
39
|
+
const dbPath = join(STORES_DIR, file);
|
|
40
|
+
try {
|
|
41
|
+
const db = new Database(dbPath, { readonly: true });
|
|
42
|
+
const row = db.prepare("SELECT COUNT(*) as cnt FROM ZREMCDREMINDER").get();
|
|
43
|
+
db.close();
|
|
44
|
+
if (row.cnt > 0) {
|
|
45
|
+
cachedDbPath = dbPath;
|
|
46
|
+
return dbPath;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Skip files that don't have the expected schema
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new RemiCommandError(ErrorCode.DB_NOT_FOUND, `No active Reminders database found in ${STORES_DIR}`, `Checked ${sqliteFiles.length} files. Ensure you have reminders in Apple Reminders.`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get a database connection. Caches the connection for reuse.
|
|
57
|
+
* Uses WAL mode and busy_timeout for safe concurrent access with remindd.
|
|
58
|
+
*/
|
|
59
|
+
export function getDb() {
|
|
60
|
+
if (cachedDb)
|
|
61
|
+
return cachedDb;
|
|
62
|
+
const dbPath = findRemindersDbPath();
|
|
63
|
+
const db = new Database(dbPath);
|
|
64
|
+
db.pragma("journal_mode = WAL");
|
|
65
|
+
db.pragma("busy_timeout = 5000");
|
|
66
|
+
cachedDb = db;
|
|
67
|
+
return db;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Close the cached database connection.
|
|
71
|
+
*/
|
|
72
|
+
export function closeDb() {
|
|
73
|
+
if (cachedDb) {
|
|
74
|
+
cachedDb.close();
|
|
75
|
+
cachedDb = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Find a list's Z_PK by name.
|
|
80
|
+
*/
|
|
81
|
+
export function findListPk(listName) {
|
|
82
|
+
const db = getDb();
|
|
83
|
+
const row = db
|
|
84
|
+
.prepare("SELECT Z_PK FROM ZREMCDBASELIST WHERE ZNAME = ? AND ZMARKEDFORDELETION = 0")
|
|
85
|
+
.get(listName);
|
|
86
|
+
if (!row) {
|
|
87
|
+
throw new RemiCommandError(ErrorCode.LIST_NOT_FOUND, `List "${listName}" not found in database`);
|
|
88
|
+
}
|
|
89
|
+
return row.Z_PK;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Find a reminder's identifier (hex UUID) by title and list name.
|
|
93
|
+
*/
|
|
94
|
+
export function findReminderIdentifier(title, listName) {
|
|
95
|
+
const db = getDb();
|
|
96
|
+
const row = db
|
|
97
|
+
.prepare(`SELECT r.Z_PK, hex(r.ZIDENTIFIER) as identifier
|
|
98
|
+
FROM ZREMCDREMINDER r
|
|
99
|
+
JOIN ZREMCDBASELIST l ON r.ZLIST = l.Z_PK
|
|
100
|
+
WHERE r.ZTITLE = ? AND l.ZNAME = ? AND r.ZCOMPLETED = 0 AND r.ZMARKEDFORDELETION = 0
|
|
101
|
+
ORDER BY r.ZCREATIONDATE DESC LIMIT 1`)
|
|
102
|
+
.get(title, listName);
|
|
103
|
+
if (!row) {
|
|
104
|
+
throw new RemiCommandError(ErrorCode.REMINDER_NOT_FOUND, `Reminder "${title}" not found in "${listName}"`);
|
|
105
|
+
}
|
|
106
|
+
return { pk: row.Z_PK, identifier: row.identifier };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Find a section's identifier (hex UUID) by name and list.
|
|
110
|
+
*/
|
|
111
|
+
export function findSectionIdentifier(sectionName, listName) {
|
|
112
|
+
const db = getDb();
|
|
113
|
+
const row = db
|
|
114
|
+
.prepare(`SELECT s.Z_PK, hex(s.ZIDENTIFIER) as identifier
|
|
115
|
+
FROM ZREMCDBASESECTION s
|
|
116
|
+
JOIN ZREMCDBASELIST l ON s.ZLIST = l.Z_PK
|
|
117
|
+
WHERE s.ZDISPLAYNAME = ? AND l.ZNAME = ? AND s.ZMARKEDFORDELETION = 0`)
|
|
118
|
+
.get(sectionName, listName);
|
|
119
|
+
if (!row) {
|
|
120
|
+
throw new RemiCommandError(ErrorCode.SECTION_NOT_FOUND, `Section "${sectionName}" not found in "${listName}"`);
|
|
121
|
+
}
|
|
122
|
+
return { pk: row.Z_PK, identifier: row.identifier };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Read the current membership data JSON from a list.
|
|
126
|
+
*/
|
|
127
|
+
export function readMembershipData(listPk) {
|
|
128
|
+
const db = getDb();
|
|
129
|
+
const row = db
|
|
130
|
+
.prepare("SELECT cast(ZMEMBERSHIPSOFREMINDERSINSECTIONSASDATA as text) as data FROM ZREMCDBASELIST WHERE Z_PK = ?")
|
|
131
|
+
.get(listPk);
|
|
132
|
+
return row?.data || null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Read the resolution token map JSON from a list.
|
|
136
|
+
* The token map may be stored as a BLOB or TEXT — handle both.
|
|
137
|
+
*/
|
|
138
|
+
export function readTokenMap(listPk) {
|
|
139
|
+
const db = getDb();
|
|
140
|
+
const row = db
|
|
141
|
+
.prepare("SELECT cast(ZRESOLUTIONTOKENMAP_V3_JSONDATA as text) as data FROM ZREMCDBASELIST WHERE Z_PK = ?")
|
|
142
|
+
.get(listPk);
|
|
143
|
+
return row?.data || null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Write membership data, checksum, and token map in a SINGLE ATOMIC TRANSACTION.
|
|
147
|
+
*
|
|
148
|
+
* This is the key improvement over the predecessor which used separate sqlite3 process
|
|
149
|
+
* calls for each statement. A partial write (data updated but checksum not) would cause
|
|
150
|
+
* remindd to detect data corruption.
|
|
151
|
+
*/
|
|
152
|
+
export function writeMembershipSync(listPk, membershipJson, checksumHex, tokenMapJson) {
|
|
153
|
+
const db = getDb();
|
|
154
|
+
const writeAll = db.transaction(() => {
|
|
155
|
+
db.prepare("UPDATE ZREMCDBASELIST SET ZMEMBERSHIPSOFREMINDERSINSECTIONSASDATA = ? WHERE Z_PK = ?").run(membershipJson, listPk);
|
|
156
|
+
db.prepare("UPDATE ZREMCDBASELIST SET ZMEMBERSHIPSOFREMINDERSINSECTIONSCHECKSUM = ? WHERE Z_PK = ?").run(checksumHex, listPk);
|
|
157
|
+
db.prepare("UPDATE ZREMCDBASELIST SET ZRESOLUTIONTOKENMAP_V3_JSONDATA = ? WHERE Z_PK = ?").run(tokenMapJson, listPk);
|
|
158
|
+
});
|
|
159
|
+
writeAll();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Convert a hex blob string (32 chars, no hyphens) to standard UUID format.
|
|
163
|
+
*/
|
|
164
|
+
export function hexToUuid(hex) {
|
|
165
|
+
const h = hex.toUpperCase();
|
|
166
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Convert a JS Date to Core Data timestamp (seconds since 2001-01-01 00:00:00 UTC).
|
|
170
|
+
*/
|
|
171
|
+
export function coreDataTimestamp(date) {
|
|
172
|
+
const d = date || new Date();
|
|
173
|
+
return d.getTime() / 1000 - 978307200;
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=sqlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/core/sqlite.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution token map manipulation.
|
|
3
|
+
*
|
|
4
|
+
* The token map is Apple's CRDT-style vector clock for field-level sync.
|
|
5
|
+
* Each syncable field has a counter and modificationTime. When a field changes
|
|
6
|
+
* locally, its counter must be incremented to tell remindd's sync engine
|
|
7
|
+
* "this field has a local change that needs to be pushed to CloudKit".
|
|
8
|
+
*
|
|
9
|
+
* These are pure functions — no side effects, easy to unit test.
|
|
10
|
+
*/
|
|
11
|
+
import type { TokenMap } from "../types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Parse a token map JSON string into a TokenMap object.
|
|
14
|
+
* Returns an empty map if the input is null/empty/invalid.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseTokenMap(json: string | null): TokenMap;
|
|
17
|
+
/**
|
|
18
|
+
* Increment the membership field counter in the token map and set modificationTime.
|
|
19
|
+
* Returns a new TokenMap (does not mutate the input).
|
|
20
|
+
*/
|
|
21
|
+
export declare function incrementMembershipCounter(tokenMap: TokenMap, timestamp: number): TokenMap;
|
|
22
|
+
/**
|
|
23
|
+
* Serialize a token map to a JSON string with sorted keys for consistency.
|
|
24
|
+
*/
|
|
25
|
+
export declare function serializeTokenMap(tokenMap: TokenMap): string;
|
|
26
|
+
/**
|
|
27
|
+
* Get the current counter value for the membership field.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getMembershipCounter(tokenMap: TokenMap): number;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution token map manipulation.
|
|
3
|
+
*
|
|
4
|
+
* The token map is Apple's CRDT-style vector clock for field-level sync.
|
|
5
|
+
* Each syncable field has a counter and modificationTime. When a field changes
|
|
6
|
+
* locally, its counter must be incremented to tell remindd's sync engine
|
|
7
|
+
* "this field has a local change that needs to be pushed to CloudKit".
|
|
8
|
+
*
|
|
9
|
+
* These are pure functions — no side effects, easy to unit test.
|
|
10
|
+
*/
|
|
11
|
+
const MEMBERSHIP_FIELD = "membershipsOfRemindersInSectionsChecksum";
|
|
12
|
+
/**
|
|
13
|
+
* Parse a token map JSON string into a TokenMap object.
|
|
14
|
+
* Returns an empty map if the input is null/empty/invalid.
|
|
15
|
+
*/
|
|
16
|
+
export function parseTokenMap(json) {
|
|
17
|
+
if (!json || json.trim() === "")
|
|
18
|
+
return {};
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(json);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Increment the membership field counter in the token map and set modificationTime.
|
|
28
|
+
* Returns a new TokenMap (does not mutate the input).
|
|
29
|
+
*/
|
|
30
|
+
export function incrementMembershipCounter(tokenMap, timestamp) {
|
|
31
|
+
const updated = { ...tokenMap };
|
|
32
|
+
const existing = updated[MEMBERSHIP_FIELD];
|
|
33
|
+
const currentCounter = existing?.counter ?? 0;
|
|
34
|
+
updated[MEMBERSHIP_FIELD] = {
|
|
35
|
+
counter: currentCounter + 1,
|
|
36
|
+
modificationTime: timestamp,
|
|
37
|
+
};
|
|
38
|
+
return updated;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Serialize a token map to a JSON string with sorted keys for consistency.
|
|
42
|
+
*/
|
|
43
|
+
export function serializeTokenMap(tokenMap) {
|
|
44
|
+
return JSON.stringify(tokenMap, Object.keys(tokenMap).sort());
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get the current counter value for the membership field.
|
|
48
|
+
*/
|
|
49
|
+
export function getMembershipCounter(tokenMap) {
|
|
50
|
+
return tokenMap[MEMBERSHIP_FIELD]?.counter ?? 0;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=tokenmap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenmap.js","sourceRoot":"","sources":["../../src/core/tokenmap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,gBAAgB,GAAG,0CAA0C,CAAC;AAEpE;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAmB;IAChD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAC3C,IAAI,CAAC;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAa,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;IACX,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,0BAA0B,CAAC,QAAkB,EAAE,SAAiB;IAC/E,MAAM,OAAO,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;IAChC,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC3C,MAAM,cAAc,GAAG,QAAQ,EAAE,OAAO,IAAI,CAAC,CAAC;IAE9C,OAAO,CAAC,gBAAgB,CAAC,GAAG;QAC3B,OAAO,EAAE,cAAc,GAAG,CAAC;QAC3B,gBAAgB,EAAE,SAAS;KAC3B,CAAC;IAEF,OAAO,OAAO,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAkB;IACnD,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAkB;IACtD,OAAO,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC;AACjD,CAAC"}
|
|
Binary file
|
|
Binary file
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** A reminder list (calendar) in Apple Reminders */
|
|
2
|
+
export interface ReminderList {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
reminderCount: number;
|
|
6
|
+
overdueCount: number;
|
|
7
|
+
}
|
|
8
|
+
/** Priority levels matching Apple Reminders */
|
|
9
|
+
export type Priority = "none" | "low" | "medium" | "high";
|
|
10
|
+
/** A single reminder item */
|
|
11
|
+
export interface Reminder {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
isCompleted: boolean;
|
|
15
|
+
listID: string;
|
|
16
|
+
listName: string;
|
|
17
|
+
priority: Priority;
|
|
18
|
+
dueDate?: string;
|
|
19
|
+
completionDate?: string;
|
|
20
|
+
notes?: string;
|
|
21
|
+
section?: string;
|
|
22
|
+
isRecurring?: boolean;
|
|
23
|
+
recurrence?: string;
|
|
24
|
+
flagged?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** A section within a reminder list */
|
|
27
|
+
export interface Section {
|
|
28
|
+
id: string;
|
|
29
|
+
displayName: string;
|
|
30
|
+
listName: string;
|
|
31
|
+
sortOrder: number;
|
|
32
|
+
}
|
|
33
|
+
/** Structured result wrapper for all remi operations */
|
|
34
|
+
export interface RemiResult<T> {
|
|
35
|
+
success: boolean;
|
|
36
|
+
data?: T;
|
|
37
|
+
error?: RemiError;
|
|
38
|
+
}
|
|
39
|
+
/** Structured error with machine-readable code and human-readable suggestion */
|
|
40
|
+
export interface RemiError {
|
|
41
|
+
code: string;
|
|
42
|
+
message: string;
|
|
43
|
+
suggestion?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Options for adding a reminder */
|
|
46
|
+
export interface AddReminderOptions {
|
|
47
|
+
list: string;
|
|
48
|
+
title: string;
|
|
49
|
+
section?: string;
|
|
50
|
+
due?: string;
|
|
51
|
+
priority?: Priority;
|
|
52
|
+
notes?: string;
|
|
53
|
+
}
|
|
54
|
+
/** Options for updating a reminder */
|
|
55
|
+
export interface UpdateReminderOptions {
|
|
56
|
+
list: string;
|
|
57
|
+
title: string;
|
|
58
|
+
newTitle?: string;
|
|
59
|
+
due?: string;
|
|
60
|
+
clearDue?: boolean;
|
|
61
|
+
priority?: Priority;
|
|
62
|
+
notes?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Section membership entry in the SQLite database */
|
|
65
|
+
export interface MembershipEntry {
|
|
66
|
+
reminderID: string;
|
|
67
|
+
sectionID: string;
|
|
68
|
+
}
|
|
69
|
+
/** Membership data structure stored in ZMEMBERSHIPSOFREMINDERSINSECTIONSASDATA */
|
|
70
|
+
export interface MembershipData {
|
|
71
|
+
memberships: MembershipEntry[];
|
|
72
|
+
}
|
|
73
|
+
/** Token map entry for a single syncable field */
|
|
74
|
+
export interface TokenMapEntry {
|
|
75
|
+
counter: number;
|
|
76
|
+
modificationTime: number;
|
|
77
|
+
}
|
|
78
|
+
/** Resolution token map (CRDT vector clocks for sync) */
|
|
79
|
+
export interface TokenMap {
|
|
80
|
+
[fieldName: string]: TokenMapEntry;
|
|
81
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mattheworiordan/remi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Fast, reliable CLI for Apple Reminders with section support and iCloud sync",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"remi": "./dist/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc && chmod +x dist/cli/index.js",
|
|
11
|
+
"dev": "tsx src/cli/index.ts",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest",
|
|
14
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
15
|
+
"test:soak": "echo '\nSoak Test: Cross-device sync verification\n=========================================\nCopy the prompt from tests/soak/SOAK_TEST.md and paste it into\na Claude Code session with computer-use MCP connected.\n\nOr run: cat tests/soak/SOAK_TEST.md\n'",
|
|
16
|
+
"lint": "biome check src/ tests/",
|
|
17
|
+
"lint:fix": "biome check --write src/ tests/",
|
|
18
|
+
"build:swift": "bash src/swift/build.sh",
|
|
19
|
+
"prepublishOnly": "npm run build && npm run build:swift",
|
|
20
|
+
"postinstall": "npm run build:swift || echo 'Swift build skipped (non-macOS or missing Xcode tools)'"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist/",
|
|
24
|
+
"src/swift/",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"keywords": [
|
|
29
|
+
"apple-reminders",
|
|
30
|
+
"reminders",
|
|
31
|
+
"cli",
|
|
32
|
+
"macos",
|
|
33
|
+
"icloud",
|
|
34
|
+
"sections",
|
|
35
|
+
"eventkit",
|
|
36
|
+
"mcp"
|
|
37
|
+
],
|
|
38
|
+
"author": "Matthew O'Riordan",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/mattheworiordan/remi.git"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18",
|
|
46
|
+
"os": "darwin"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"better-sqlite3": "^12.8.0",
|
|
50
|
+
"chalk": "^5.4.1",
|
|
51
|
+
"chrono-node": "^2.9.0",
|
|
52
|
+
"commander": "^14.0.0",
|
|
53
|
+
"dayjs": "^1.11.13"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@biomejs/biome": "^1.9.4",
|
|
57
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
58
|
+
"@types/node": "^22.15.3",
|
|
59
|
+
"tsx": "^4.20.3",
|
|
60
|
+
"typescript": "^5.8.3",
|
|
61
|
+
"vitest": "^3.1.3"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleIdentifier</key>
|
|
6
|
+
<string>com.mattheworiordan.remi</string>
|
|
7
|
+
<key>CFBundleName</key>
|
|
8
|
+
<string>remi</string>
|
|
9
|
+
<key>CFBundlePackageType</key>
|
|
10
|
+
<string>APPL</string>
|
|
11
|
+
<key>CFBundleIconFile</key>
|
|
12
|
+
<string>remi.icns</string>
|
|
13
|
+
<key>NSRemindersUsageDescription</key>
|
|
14
|
+
<string>remi needs access to Apple Reminders to create, read, update, and organize your reminders and sections.</string>
|
|
15
|
+
</dict>
|
|
16
|
+
</plist>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Build Swift helpers for remi
|
|
3
|
+
# Compiles both helpers into binaries with embedded Info.plist
|
|
4
|
+
# (provides NSRemindersUsageDescription for the permission dialog)
|
|
5
|
+
# This requires macOS 13+ and Xcode Command Line Tools.
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
OUTPUT_DIR="${SCRIPT_DIR}/../../dist"
|
|
11
|
+
INFO_PLIST="$SCRIPT_DIR/Info.plist"
|
|
12
|
+
|
|
13
|
+
mkdir -p "$OUTPUT_DIR"
|
|
14
|
+
|
|
15
|
+
# -- reminders-helper (EventKit operations) --
|
|
16
|
+
if [ ! -f "$OUTPUT_DIR/reminders-helper" ] || [ "$SCRIPT_DIR/reminders-helper.swift" -nt "$OUTPUT_DIR/reminders-helper" ]; then
|
|
17
|
+
echo "Compiling reminders-helper..."
|
|
18
|
+
swiftc \
|
|
19
|
+
-framework Foundation \
|
|
20
|
+
-framework EventKit \
|
|
21
|
+
-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker "$INFO_PLIST" \
|
|
22
|
+
-O \
|
|
23
|
+
-o "$OUTPUT_DIR/reminders-helper" \
|
|
24
|
+
"$SCRIPT_DIR/reminders-helper.swift"
|
|
25
|
+
codesign -s - -f "$OUTPUT_DIR/reminders-helper" 2>/dev/null || true
|
|
26
|
+
echo "reminders-helper compiled successfully"
|
|
27
|
+
else
|
|
28
|
+
echo "reminders-helper is up to date"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# -- section-helper (ReminderKit + SQLite database access) --
|
|
32
|
+
if [ ! -f "$OUTPUT_DIR/section-helper" ] || [ "$SCRIPT_DIR/section-helper.swift" -nt "$OUTPUT_DIR/section-helper" ]; then
|
|
33
|
+
echo "Compiling section-helper..."
|
|
34
|
+
swiftc \
|
|
35
|
+
-framework Foundation \
|
|
36
|
+
-framework EventKit \
|
|
37
|
+
-F /System/Library/PrivateFrameworks \
|
|
38
|
+
-framework ReminderKit \
|
|
39
|
+
-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker "$INFO_PLIST" \
|
|
40
|
+
-O \
|
|
41
|
+
-o "$OUTPUT_DIR/section-helper" \
|
|
42
|
+
"$SCRIPT_DIR/section-helper.swift"
|
|
43
|
+
codesign -s - -f "$OUTPUT_DIR/section-helper" 2>/dev/null || true
|
|
44
|
+
echo "section-helper compiled successfully"
|
|
45
|
+
else
|
|
46
|
+
echo "section-helper is up to date"
|
|
47
|
+
fi
|