icloud-mcp 2.5.0 → 2.7.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/lib/carddav.js CHANGED
@@ -12,7 +12,7 @@ function getCredentials() {
12
12
  return { user, auth: Buffer.from(`${user}:${pass}`).toString('base64') };
13
13
  }
14
14
 
15
- async function davRequest(method, url, opts = {}) {
15
+ async function defaultDavRequest(method, url, opts = {}) {
16
16
  const { auth } = getCredentials();
17
17
  const headers = {
18
18
  Authorization: `Basic ${auth}`,
@@ -25,6 +25,17 @@ async function davRequest(method, url, opts = {}) {
25
25
  return { status: res.status, etag: res.headers.get('etag'), body: text };
26
26
  }
27
27
 
28
+ let davRequest = defaultDavRequest;
29
+
30
+ export function setCardDavRequestForTests(fn) {
31
+ davRequest = fn || defaultDavRequest;
32
+ _discoveryCache = null;
33
+ }
34
+
35
+ export function setCardDavDiscoveryForTests(cache) {
36
+ _discoveryCache = cache;
37
+ }
38
+
28
39
  function propfindBody(props) {
29
40
  return `<?xml version="1.0" encoding="UTF-8"?>\n<A:propfind xmlns:A="DAV:"><A:prop>${props}</A:prop></A:propfind>`;
30
41
  }
@@ -387,7 +398,22 @@ export async function updateContact(contactId, fields) {
387
398
  return { updated: true, contactId, etag: resp.etag };
388
399
  }
389
400
 
390
- export async function deleteContact(contactId) {
401
+ export async function deleteContact(contactId, dryRun = false) {
402
+ if (dryRun) {
403
+ const contact = await getContact(contactId);
404
+ return {
405
+ dryRun: true,
406
+ wouldDelete: true,
407
+ contactId,
408
+ changes: [{
409
+ action: 'delete_contact',
410
+ contactId,
411
+ fullName: contact.fullName || null,
412
+ emails: contact.emails || [],
413
+ phones: contact.phones || [],
414
+ }],
415
+ };
416
+ }
391
417
  const { dataHost, addressBookPath } = await discover();
392
418
  const url = `${dataHost}${addressBookPath}${contactId}.vcf`;
393
419
 
@@ -0,0 +1,13 @@
1
+ // Local state files (rules, move manifest, digest, session log).
2
+ // Defaults to the home directory so nothing is written inside the repo.
3
+ // Override with ICLOUD_MCP_DATA_DIR for tests or a dedicated data folder.
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
6
+
7
+ export function dataDir() {
8
+ return process.env.ICLOUD_MCP_DATA_DIR || homedir();
9
+ }
10
+
11
+ export function dataFile(filename) {
12
+ return join(dataDir(), filename);
13
+ }
package/lib/digest.js CHANGED
@@ -1,32 +1,36 @@
1
1
  // ─── Digest State ─────────────────────────────────────────────────────────────
2
2
  import { readFileSync, writeFileSync, existsSync } from 'fs';
3
- import { homedir } from 'os';
4
- import { join } from 'path';
3
+ import { dataFile } from './data-paths.js';
5
4
 
6
- const DIGEST_FILE = join(homedir(), '.icloud-mcp-digest.json');
5
+ function digestFile() {
6
+ return dataFile('.icloud-mcp-digest.json');
7
+ }
7
8
 
8
9
  const DEFAULT_STATE = {
9
10
  lastRun: null,
10
11
  processedUids: [],
11
12
  pendingActions: [],
12
- skipCounts: {}
13
+ skipCounts: {},
14
+ dismissedReminders: [],
15
+ seenReminders: []
13
16
  };
14
17
 
15
18
  function readDigest() {
16
- if (!existsSync(DIGEST_FILE)) return { ...DEFAULT_STATE };
17
- try { return JSON.parse(readFileSync(DIGEST_FILE, 'utf8')); }
19
+ const file = digestFile();
20
+ if (!existsSync(file)) return { ...DEFAULT_STATE };
21
+ try { return JSON.parse(readFileSync(file, 'utf8')); }
18
22
  catch { return { ...DEFAULT_STATE }; }
19
23
  }
20
24
 
21
25
  function writeDigest(data) {
22
- writeFileSync(DIGEST_FILE, JSON.stringify(data, null, 2));
26
+ writeFileSync(digestFile(), JSON.stringify(data, null, 2));
23
27
  }
24
28
 
25
29
  export function getDigestState() {
26
30
  return readDigest();
27
31
  }
28
32
 
29
- export function updateDigestState({ processedUids, lastRun, pendingActions, skipCounts } = {}) {
33
+ export function updateDigestState({ processedUids, lastRun, pendingActions, skipCounts, dismissedReminders, seenReminders } = {}) {
30
34
  const state = readDigest();
31
35
  if (lastRun !== undefined) state.lastRun = lastRun;
32
36
  if (processedUids !== undefined) {
@@ -41,6 +45,17 @@ export function updateDigestState({ processedUids, lastRun, pendingActions, skip
41
45
  state.skipCounts[sender] = (state.skipCounts[sender] || 0) + count;
42
46
  }
43
47
  }
48
+ if (dismissedReminders !== undefined) {
49
+ // Replace, prune entries older than 30 days, cap at 500
50
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
51
+ state.dismissedReminders = dismissedReminders
52
+ .filter(r => !r.dismissedAt || new Date(r.dismissedAt).getTime() > cutoff)
53
+ .slice(-500);
54
+ }
55
+ if (seenReminders !== undefined) {
56
+ // Replace wholesale — snapshot of current "claude" list, no accumulation
57
+ state.seenReminders = seenReminders;
58
+ }
44
59
  writeDigest(state);
45
60
  return state;
46
61
  }