apple-pim-cli 3.0.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/cli-runner.js +98 -0
- package/lib/handlers/apple-pim.js +101 -0
- package/lib/handlers/calendar.js +69 -0
- package/lib/handlers/contact.js +42 -0
- package/lib/handlers/mail.js +101 -0
- package/lib/handlers/reminder.js +83 -0
- package/lib/mail-format.js +65 -0
- package/lib/sanitize.js +250 -0
- package/lib/schemas.js +396 -0
- package/lib/tool-args.js +116 -0
- package/openclaw.plugin.json +31 -0
- package/package.json +51 -0
- package/skills/apple-pim/SKILL.md +166 -0
- package/src/index.ts +177 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Locate Swift CLI binaries by checking multiple locations in order.
|
|
8
|
+
* @param {string[]} extraLocations - Additional directories to check first.
|
|
9
|
+
* @returns {string} Path to the directory containing CLI binaries.
|
|
10
|
+
*/
|
|
11
|
+
export function findSwiftBinDir(extraLocations = []) {
|
|
12
|
+
const locations = [
|
|
13
|
+
...extraLocations,
|
|
14
|
+
// ~/.local/bin (setup.sh --install target)
|
|
15
|
+
join(homedir(), ".local", "bin"),
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
for (const loc of locations) {
|
|
19
|
+
if (existsSync(join(loc, "calendar-cli"))) {
|
|
20
|
+
return loc;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Return first location as default (will fail with helpful error)
|
|
25
|
+
return locations[0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Helper to calculate relative date string from days offset.
|
|
30
|
+
* @param {number} daysOffset - Number of days to offset from today.
|
|
31
|
+
* @returns {string} Date in YYYY-MM-DD format.
|
|
32
|
+
*/
|
|
33
|
+
export function relativeDateString(daysOffset) {
|
|
34
|
+
const date = new Date();
|
|
35
|
+
date.setDate(date.getDate() + daysOffset);
|
|
36
|
+
return date.toISOString().split("T")[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Default timeout for CLI execution (30 seconds). */
|
|
40
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Factory: creates a runCLI function bound to a specific binary directory.
|
|
44
|
+
* @param {string} binDir - Directory containing the Swift CLI binaries.
|
|
45
|
+
* @param {Object} envOverrides - Extra env vars to pass to every spawn call.
|
|
46
|
+
* @param {{ timeoutMs?: number }} options - Options (e.g. timeout).
|
|
47
|
+
* @returns {{ runCLI: (cli: string, args: string[]) => Promise<object> }}
|
|
48
|
+
*/
|
|
49
|
+
export function createCLIRunner(binDir, envOverrides = {}, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
50
|
+
async function runCLI(cli, args) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const cliPath = join(binDir, cli);
|
|
53
|
+
const proc = spawn(cliPath, args, {
|
|
54
|
+
env: { ...process.env, ...envOverrides },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let stdout = "";
|
|
58
|
+
let stderr = "";
|
|
59
|
+
let killed = false;
|
|
60
|
+
|
|
61
|
+
const timer = setTimeout(() => {
|
|
62
|
+
killed = true;
|
|
63
|
+
proc.kill("SIGTERM");
|
|
64
|
+
reject(new Error(`CLI timed out after ${timeoutMs}ms: ${cli} ${args.join(" ")}`));
|
|
65
|
+
}, timeoutMs);
|
|
66
|
+
|
|
67
|
+
proc.stdout.on("data", (data) => {
|
|
68
|
+
stdout += data.toString();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
proc.stderr.on("data", (data) => {
|
|
72
|
+
stderr += data.toString();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
proc.on("close", (code) => {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
if (killed) return; // already rejected by timeout
|
|
78
|
+
if (code === 0) {
|
|
79
|
+
try {
|
|
80
|
+
resolve(JSON.parse(stdout));
|
|
81
|
+
} catch {
|
|
82
|
+
resolve({ success: true, output: stdout });
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
reject(new Error(stderr || `CLI exited with code ${code}`));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
proc.on("error", (err) => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
if (killed) return;
|
|
92
|
+
reject(new Error(`Failed to run CLI: ${err.message}`));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { runCLI };
|
|
98
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export async function handleApplePim(args, runCLI) {
|
|
2
|
+
switch (args.action) {
|
|
3
|
+
case "status": {
|
|
4
|
+
const status = {};
|
|
5
|
+
const domains = [
|
|
6
|
+
{ name: "calendars", cli: "calendar-cli" },
|
|
7
|
+
{ name: "reminders", cli: "reminder-cli" },
|
|
8
|
+
{ name: "contacts", cli: "contacts-cli" },
|
|
9
|
+
{ name: "mail", cli: "mail-cli" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const statusMessages = {
|
|
13
|
+
authorized: "Full access granted",
|
|
14
|
+
notDetermined: "Permission not yet requested. Run authorize to prompt.",
|
|
15
|
+
denied: "Access denied. Enable in System Settings > Privacy & Security.",
|
|
16
|
+
restricted: "Access restricted by system policy (MDM or parental controls).",
|
|
17
|
+
writeOnly: "Write-only access. Upgrade in System Settings > Privacy & Security.",
|
|
18
|
+
unavailable: "Not available",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (const domain of domains) {
|
|
22
|
+
try {
|
|
23
|
+
const result = await runCLI(domain.cli, ["auth-status"]);
|
|
24
|
+
const auth = result.authorization || "unknown";
|
|
25
|
+
status[domain.name] = {
|
|
26
|
+
enabled: true,
|
|
27
|
+
authorization: auth,
|
|
28
|
+
message: result.message || statusMessages[auth] || `Status: ${auth}`,
|
|
29
|
+
};
|
|
30
|
+
} catch (err) {
|
|
31
|
+
status[domain.name] = {
|
|
32
|
+
enabled: false,
|
|
33
|
+
authorization: "error",
|
|
34
|
+
message: err.message,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { status };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
case "authorize": {
|
|
43
|
+
const targetDomain = args.domain;
|
|
44
|
+
const results = {};
|
|
45
|
+
const domains = [
|
|
46
|
+
{ name: "calendars", cli: "calendar-cli", args: ["list"] },
|
|
47
|
+
{ name: "reminders", cli: "reminder-cli", args: ["lists"] },
|
|
48
|
+
{ name: "contacts", cli: "contacts-cli", args: ["groups"] },
|
|
49
|
+
{ name: "mail", cli: "mail-cli", args: ["accounts"] },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const toAuthorize = targetDomain
|
|
53
|
+
? domains.filter((d) => d.name === targetDomain)
|
|
54
|
+
: domains;
|
|
55
|
+
|
|
56
|
+
for (const domain of toAuthorize) {
|
|
57
|
+
try {
|
|
58
|
+
await runCLI(domain.cli, domain.args);
|
|
59
|
+
results[domain.name] = { success: true, message: "Access authorized" };
|
|
60
|
+
} catch (err) {
|
|
61
|
+
const msg = err.message.toLowerCase();
|
|
62
|
+
if (msg.includes("denied") || msg.includes("not granted")) {
|
|
63
|
+
results[domain.name] = {
|
|
64
|
+
success: false,
|
|
65
|
+
message:
|
|
66
|
+
"Access denied. The user must manually enable access:\n" +
|
|
67
|
+
"1. Open System Settings > Privacy & Security\n" +
|
|
68
|
+
`2. Find the ${domain.name === "mail" ? "Automation" : domain.name.charAt(0).toUpperCase() + domain.name.slice(1)} section\n` +
|
|
69
|
+
"3. Enable access for the terminal application\n" +
|
|
70
|
+
"4. Restart the terminal and try again",
|
|
71
|
+
};
|
|
72
|
+
} else if (msg.includes("not running") && domain.name === "mail") {
|
|
73
|
+
results[domain.name] = {
|
|
74
|
+
success: false,
|
|
75
|
+
message: "Mail.app must be running before authorization can be requested.",
|
|
76
|
+
};
|
|
77
|
+
} else {
|
|
78
|
+
results[domain.name] = { success: false, message: err.message };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { results };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case "config_show": {
|
|
87
|
+
const configArgs = ["config", "show"];
|
|
88
|
+
if (args.profile) configArgs.push("--profile", args.profile);
|
|
89
|
+
return await runCLI("calendar-cli", configArgs);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case "config_init": {
|
|
93
|
+
const configArgs = ["config", "init"];
|
|
94
|
+
if (args.profile) configArgs.push("--profile", args.profile);
|
|
95
|
+
return await runCLI("calendar-cli", configArgs);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
default:
|
|
99
|
+
throw new Error(`Unknown apple-pim action: ${args.action}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { relativeDateString } from "../cli-runner.js";
|
|
2
|
+
import {
|
|
3
|
+
buildCalendarCreateArgs,
|
|
4
|
+
buildCalendarDeleteArgs,
|
|
5
|
+
buildCalendarUpdateArgs,
|
|
6
|
+
} from "../tool-args.js";
|
|
7
|
+
|
|
8
|
+
export async function handleCalendar(args, runCLI) {
|
|
9
|
+
const cliArgs = [];
|
|
10
|
+
|
|
11
|
+
switch (args.action) {
|
|
12
|
+
case "list":
|
|
13
|
+
return await runCLI("calendar-cli", ["list"]);
|
|
14
|
+
|
|
15
|
+
case "events":
|
|
16
|
+
cliArgs.push("events");
|
|
17
|
+
if (args.calendar) cliArgs.push("--calendar", args.calendar);
|
|
18
|
+
if (args.lastDays !== undefined) {
|
|
19
|
+
cliArgs.push("--from", relativeDateString(-args.lastDays));
|
|
20
|
+
} else if (args.from) {
|
|
21
|
+
cliArgs.push("--from", args.from);
|
|
22
|
+
}
|
|
23
|
+
if (args.nextDays !== undefined) {
|
|
24
|
+
cliArgs.push("--to", relativeDateString(args.nextDays));
|
|
25
|
+
} else if (args.to) {
|
|
26
|
+
cliArgs.push("--to", args.to);
|
|
27
|
+
}
|
|
28
|
+
if (args.limit) cliArgs.push("--limit", String(args.limit));
|
|
29
|
+
return await runCLI("calendar-cli", cliArgs);
|
|
30
|
+
|
|
31
|
+
case "get":
|
|
32
|
+
if (!args.id) throw new Error("Event ID is required for calendar get");
|
|
33
|
+
return await runCLI("calendar-cli", ["get", "--id", args.id]);
|
|
34
|
+
|
|
35
|
+
case "search":
|
|
36
|
+
if (!args.query) throw new Error("Search query is required for calendar search");
|
|
37
|
+
cliArgs.push("search", args.query);
|
|
38
|
+
if (args.calendar) cliArgs.push("--calendar", args.calendar);
|
|
39
|
+
if (args.from) cliArgs.push("--from", args.from);
|
|
40
|
+
if (args.to) cliArgs.push("--to", args.to);
|
|
41
|
+
if (args.limit) cliArgs.push("--limit", String(args.limit));
|
|
42
|
+
return await runCLI("calendar-cli", cliArgs);
|
|
43
|
+
|
|
44
|
+
case "create":
|
|
45
|
+
return await runCLI(
|
|
46
|
+
"calendar-cli",
|
|
47
|
+
buildCalendarCreateArgs(args, args.calendar)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
case "update":
|
|
51
|
+
return await runCLI("calendar-cli", buildCalendarUpdateArgs(args));
|
|
52
|
+
|
|
53
|
+
case "delete":
|
|
54
|
+
return await runCLI("calendar-cli", buildCalendarDeleteArgs(args));
|
|
55
|
+
|
|
56
|
+
case "batch_create":
|
|
57
|
+
if (!args.events || !Array.isArray(args.events) || args.events.length === 0) {
|
|
58
|
+
throw new Error("Events array is required and cannot be empty");
|
|
59
|
+
}
|
|
60
|
+
return await runCLI("calendar-cli", [
|
|
61
|
+
"batch-create",
|
|
62
|
+
"--json",
|
|
63
|
+
JSON.stringify(args.events),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
default:
|
|
67
|
+
throw new Error(`Unknown calendar action: ${args.action}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildContactCreateArgs,
|
|
3
|
+
buildContactUpdateArgs,
|
|
4
|
+
} from "../tool-args.js";
|
|
5
|
+
|
|
6
|
+
export async function handleContact(args, runCLI) {
|
|
7
|
+
const cliArgs = [];
|
|
8
|
+
|
|
9
|
+
switch (args.action) {
|
|
10
|
+
case "groups":
|
|
11
|
+
return await runCLI("contacts-cli", ["groups"]);
|
|
12
|
+
|
|
13
|
+
case "list":
|
|
14
|
+
cliArgs.push("list");
|
|
15
|
+
if (args.group) cliArgs.push("--group", args.group);
|
|
16
|
+
if (args.limit) cliArgs.push("--limit", String(args.limit));
|
|
17
|
+
return await runCLI("contacts-cli", cliArgs);
|
|
18
|
+
|
|
19
|
+
case "search":
|
|
20
|
+
if (!args.query) throw new Error("Search query is required for contact search");
|
|
21
|
+
cliArgs.push("search", args.query);
|
|
22
|
+
if (args.limit) cliArgs.push("--limit", String(args.limit));
|
|
23
|
+
return await runCLI("contacts-cli", cliArgs);
|
|
24
|
+
|
|
25
|
+
case "get":
|
|
26
|
+
if (!args.id) throw new Error("Contact ID is required for contact get");
|
|
27
|
+
return await runCLI("contacts-cli", ["get", "--id", args.id]);
|
|
28
|
+
|
|
29
|
+
case "create":
|
|
30
|
+
return await runCLI("contacts-cli", buildContactCreateArgs(args));
|
|
31
|
+
|
|
32
|
+
case "update":
|
|
33
|
+
return await runCLI("contacts-cli", buildContactUpdateArgs(args));
|
|
34
|
+
|
|
35
|
+
case "delete":
|
|
36
|
+
if (!args.id) throw new Error("Contact ID is required for contact delete");
|
|
37
|
+
return await runCLI("contacts-cli", ["delete", "--id", args.id]);
|
|
38
|
+
|
|
39
|
+
default:
|
|
40
|
+
throw new Error(`Unknown contact action: ${args.action}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { formatMailGetResult } from "../mail-format.js";
|
|
2
|
+
|
|
3
|
+
export async function handleMail(args, runCLI) {
|
|
4
|
+
const cliArgs = [];
|
|
5
|
+
|
|
6
|
+
switch (args.action) {
|
|
7
|
+
case "accounts":
|
|
8
|
+
return await runCLI("mail-cli", ["accounts"]);
|
|
9
|
+
|
|
10
|
+
case "mailboxes":
|
|
11
|
+
cliArgs.push("mailboxes");
|
|
12
|
+
if (args.account) cliArgs.push("--account", args.account);
|
|
13
|
+
return await runCLI("mail-cli", cliArgs);
|
|
14
|
+
|
|
15
|
+
case "messages":
|
|
16
|
+
cliArgs.push("messages");
|
|
17
|
+
if (args.mailbox) cliArgs.push("--mailbox", args.mailbox);
|
|
18
|
+
if (args.account) cliArgs.push("--account", args.account);
|
|
19
|
+
if (args.limit) cliArgs.push("--limit", String(args.limit));
|
|
20
|
+
if (args.filter) cliArgs.push("--filter", args.filter);
|
|
21
|
+
return await runCLI("mail-cli", cliArgs);
|
|
22
|
+
|
|
23
|
+
case "get": {
|
|
24
|
+
if (!args.id) throw new Error("Message ID is required for mail get");
|
|
25
|
+
const getArgs = ["get", "--id", args.id];
|
|
26
|
+
if (args.mailbox) getArgs.push("--mailbox", args.mailbox);
|
|
27
|
+
if (args.account) getArgs.push("--account", args.account);
|
|
28
|
+
if (args.format === "markdown") getArgs.push("--include-source");
|
|
29
|
+
const result = await runCLI("mail-cli", getArgs);
|
|
30
|
+
return await formatMailGetResult(result, args.format || "plain");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
case "search":
|
|
34
|
+
if (!args.query) throw new Error("Search query is required for mail search");
|
|
35
|
+
cliArgs.push("search", args.query);
|
|
36
|
+
if (args.field) cliArgs.push("--field", args.field);
|
|
37
|
+
if (args.mailbox) cliArgs.push("--mailbox", args.mailbox);
|
|
38
|
+
if (args.account) cliArgs.push("--account", args.account);
|
|
39
|
+
if (args.limit) cliArgs.push("--limit", String(args.limit));
|
|
40
|
+
return await runCLI("mail-cli", cliArgs);
|
|
41
|
+
|
|
42
|
+
case "update": {
|
|
43
|
+
if (!args.id) throw new Error("Message ID is required for mail update");
|
|
44
|
+
const updateArgs = ["update", "--id", args.id];
|
|
45
|
+
if (args.read !== undefined) updateArgs.push("--read", String(args.read));
|
|
46
|
+
if (args.flagged !== undefined) updateArgs.push("--flagged", String(args.flagged));
|
|
47
|
+
if (args.junk !== undefined) updateArgs.push("--junk", String(args.junk));
|
|
48
|
+
if (args.mailbox) updateArgs.push("--mailbox", args.mailbox);
|
|
49
|
+
if (args.account) updateArgs.push("--account", args.account);
|
|
50
|
+
return await runCLI("mail-cli", updateArgs);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case "move": {
|
|
54
|
+
if (!args.id) throw new Error("Message ID is required for mail move");
|
|
55
|
+
if (!args.toMailbox) throw new Error("Target mailbox (toMailbox) is required for mail move");
|
|
56
|
+
const moveArgs = ["move", "--id", args.id, "--to-mailbox", args.toMailbox];
|
|
57
|
+
if (args.toAccount) moveArgs.push("--to-account", args.toAccount);
|
|
58
|
+
if (args.mailbox) moveArgs.push("--mailbox", args.mailbox);
|
|
59
|
+
if (args.account) moveArgs.push("--account", args.account);
|
|
60
|
+
return await runCLI("mail-cli", moveArgs);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case "delete": {
|
|
64
|
+
if (!args.id) throw new Error("Message ID is required for mail delete");
|
|
65
|
+
const delArgs = ["delete", "--id", args.id];
|
|
66
|
+
if (args.mailbox) delArgs.push("--mailbox", args.mailbox);
|
|
67
|
+
if (args.account) delArgs.push("--account", args.account);
|
|
68
|
+
return await runCLI("mail-cli", delArgs);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case "batch_update": {
|
|
72
|
+
if (!args.ids || !Array.isArray(args.ids) || args.ids.length === 0) {
|
|
73
|
+
throw new Error("IDs array is required and cannot be empty");
|
|
74
|
+
}
|
|
75
|
+
const updates = args.ids.map((id) => {
|
|
76
|
+
const obj = { id };
|
|
77
|
+
if (args.read !== undefined) obj.read = args.read;
|
|
78
|
+
if (args.flagged !== undefined) obj.flagged = args.flagged;
|
|
79
|
+
if (args.junk !== undefined) obj.junk = args.junk;
|
|
80
|
+
return obj;
|
|
81
|
+
});
|
|
82
|
+
const batchArgs = ["batch-update", "--json", JSON.stringify(updates)];
|
|
83
|
+
if (args.mailbox) batchArgs.push("--mailbox", args.mailbox);
|
|
84
|
+
if (args.account) batchArgs.push("--account", args.account);
|
|
85
|
+
return await runCLI("mail-cli", batchArgs);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case "batch_delete": {
|
|
89
|
+
if (!args.ids || !Array.isArray(args.ids) || args.ids.length === 0) {
|
|
90
|
+
throw new Error("IDs array is required and cannot be empty");
|
|
91
|
+
}
|
|
92
|
+
const batchArgs = ["batch-delete", "--json", JSON.stringify(args.ids)];
|
|
93
|
+
if (args.mailbox) batchArgs.push("--mailbox", args.mailbox);
|
|
94
|
+
if (args.account) batchArgs.push("--account", args.account);
|
|
95
|
+
return await runCLI("mail-cli", batchArgs);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
default:
|
|
99
|
+
throw new Error(`Unknown mail action: ${args.action}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildReminderCreateArgs,
|
|
3
|
+
buildReminderUpdateArgs,
|
|
4
|
+
} from "../tool-args.js";
|
|
5
|
+
|
|
6
|
+
export async function handleReminder(args, runCLI) {
|
|
7
|
+
const cliArgs = [];
|
|
8
|
+
|
|
9
|
+
switch (args.action) {
|
|
10
|
+
case "lists":
|
|
11
|
+
return await runCLI("reminder-cli", ["lists"]);
|
|
12
|
+
|
|
13
|
+
case "items":
|
|
14
|
+
cliArgs.push("items");
|
|
15
|
+
if (args.list) cliArgs.push("--list", args.list);
|
|
16
|
+
if (args.filter) cliArgs.push("--filter", args.filter);
|
|
17
|
+
if (!args.filter && args.completed) cliArgs.push("--completed");
|
|
18
|
+
if (args.limit) cliArgs.push("--limit", String(args.limit));
|
|
19
|
+
return await runCLI("reminder-cli", cliArgs);
|
|
20
|
+
|
|
21
|
+
case "get":
|
|
22
|
+
if (!args.id) throw new Error("Reminder ID is required for reminder get");
|
|
23
|
+
return await runCLI("reminder-cli", ["get", "--id", args.id]);
|
|
24
|
+
|
|
25
|
+
case "search":
|
|
26
|
+
if (!args.query) throw new Error("Search query is required for reminder search");
|
|
27
|
+
cliArgs.push("search", args.query);
|
|
28
|
+
if (args.list) cliArgs.push("--list", args.list);
|
|
29
|
+
if (args.completed) cliArgs.push("--completed");
|
|
30
|
+
if (args.limit) cliArgs.push("--limit", String(args.limit));
|
|
31
|
+
return await runCLI("reminder-cli", cliArgs);
|
|
32
|
+
|
|
33
|
+
case "create":
|
|
34
|
+
return await runCLI(
|
|
35
|
+
"reminder-cli",
|
|
36
|
+
buildReminderCreateArgs(args, args.list)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
case "complete":
|
|
40
|
+
if (!args.id) throw new Error("Reminder ID is required for reminder complete");
|
|
41
|
+
cliArgs.push("complete", "--id", args.id);
|
|
42
|
+
if (args.undo) cliArgs.push("--undo");
|
|
43
|
+
return await runCLI("reminder-cli", cliArgs);
|
|
44
|
+
|
|
45
|
+
case "update":
|
|
46
|
+
return await runCLI("reminder-cli", buildReminderUpdateArgs(args));
|
|
47
|
+
|
|
48
|
+
case "delete":
|
|
49
|
+
if (!args.id) throw new Error("Reminder ID is required for reminder delete");
|
|
50
|
+
return await runCLI("reminder-cli", ["delete", "--id", args.id]);
|
|
51
|
+
|
|
52
|
+
case "batch_create":
|
|
53
|
+
if (!args.reminders || !Array.isArray(args.reminders) || args.reminders.length === 0) {
|
|
54
|
+
throw new Error("Reminders array is required and cannot be empty");
|
|
55
|
+
}
|
|
56
|
+
return await runCLI("reminder-cli", [
|
|
57
|
+
"batch-create",
|
|
58
|
+
"--json",
|
|
59
|
+
JSON.stringify(args.reminders),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
case "batch_complete":
|
|
63
|
+
if (!args.ids || !Array.isArray(args.ids) || args.ids.length === 0) {
|
|
64
|
+
throw new Error("IDs array is required and cannot be empty");
|
|
65
|
+
}
|
|
66
|
+
cliArgs.push("batch-complete", "--json", JSON.stringify(args.ids));
|
|
67
|
+
if (args.undo) cliArgs.push("--undo");
|
|
68
|
+
return await runCLI("reminder-cli", cliArgs);
|
|
69
|
+
|
|
70
|
+
case "batch_delete":
|
|
71
|
+
if (!args.ids || !Array.isArray(args.ids) || args.ids.length === 0) {
|
|
72
|
+
throw new Error("IDs array is required and cannot be empty");
|
|
73
|
+
}
|
|
74
|
+
return await runCLI("reminder-cli", [
|
|
75
|
+
"batch-delete",
|
|
76
|
+
"--json",
|
|
77
|
+
JSON.stringify(args.ids),
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
default:
|
|
81
|
+
throw new Error(`Unknown reminder action: ${args.action}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { simpleParser } from "mailparser";
|
|
2
|
+
import TurndownService from "turndown";
|
|
3
|
+
|
|
4
|
+
const turndown = new TurndownService({
|
|
5
|
+
headingStyle: "atx",
|
|
6
|
+
codeBlockStyle: "fenced",
|
|
7
|
+
emDelimiter: "_",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function normalizeMarkdown(markdown) {
|
|
11
|
+
return markdown
|
|
12
|
+
.replace(/\r\n/g, "\n")
|
|
13
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
14
|
+
.trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function markdownFromEmailSource(source) {
|
|
18
|
+
const parsed = await simpleParser(source);
|
|
19
|
+
|
|
20
|
+
if (parsed.html) {
|
|
21
|
+
return normalizeMarkdown(turndown.turndown(parsed.html));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (parsed.textAsHtml) {
|
|
25
|
+
return normalizeMarkdown(turndown.turndown(parsed.textAsHtml));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (parsed.text) {
|
|
29
|
+
return normalizeMarkdown(parsed.text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function formatMailGetResult(result, format) {
|
|
36
|
+
if (format !== "markdown") return result;
|
|
37
|
+
|
|
38
|
+
const message = result?.message;
|
|
39
|
+
if (!message || typeof message !== "object") return result;
|
|
40
|
+
|
|
41
|
+
let markdown = "";
|
|
42
|
+
if (typeof message.source === "string" && message.source.trim().length > 0) {
|
|
43
|
+
try {
|
|
44
|
+
markdown = await markdownFromEmailSource(message.source);
|
|
45
|
+
} catch {
|
|
46
|
+
// Fall back to Mail.app plain-text extraction if MIME parsing fails.
|
|
47
|
+
markdown = "";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!markdown && typeof message.content === "string") {
|
|
52
|
+
markdown = normalizeMarkdown(message.content);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...result,
|
|
57
|
+
message: {
|
|
58
|
+
...message,
|
|
59
|
+
content: markdown,
|
|
60
|
+
contentFormat: "markdown",
|
|
61
|
+
// Source is only needed for conversion, not for normal tool output.
|
|
62
|
+
source: undefined,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|