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.
@@ -0,0 +1,116 @@
1
+ export function buildCalendarDeleteArgs(args) {
2
+ const deleteArgs = ["delete", "--id", args.id];
3
+ if (args.futureEvents) deleteArgs.push("--future-events");
4
+ return deleteArgs;
5
+ }
6
+
7
+ export function buildCalendarCreateArgs(args, targetCalendar) {
8
+ const cliArgs = ["create", "--title", args.title, "--start", args.start];
9
+ if (args.end) cliArgs.push("--end", args.end);
10
+ if (args.duration) cliArgs.push("--duration", String(args.duration));
11
+ if (targetCalendar) cliArgs.push("--calendar", targetCalendar);
12
+ if (args.location) cliArgs.push("--location", args.location);
13
+ if (args.notes) cliArgs.push("--notes", args.notes);
14
+ if (args.url) cliArgs.push("--url", args.url);
15
+ if (args.allDay) cliArgs.push("--all-day");
16
+ if (args.alarm) {
17
+ for (const minutes of args.alarm) {
18
+ cliArgs.push("--alarm", String(minutes));
19
+ }
20
+ }
21
+ if (args.recurrence) {
22
+ cliArgs.push("--recurrence", JSON.stringify(args.recurrence));
23
+ }
24
+ return cliArgs;
25
+ }
26
+
27
+ export function buildCalendarUpdateArgs(args) {
28
+ const cliArgs = ["update", "--id", args.id];
29
+ if (args.title) cliArgs.push("--title", args.title);
30
+ if (args.start) cliArgs.push("--start", args.start);
31
+ if (args.end) cliArgs.push("--end", args.end);
32
+ if (args.location) cliArgs.push("--location", args.location);
33
+ if (args.notes) cliArgs.push("--notes", args.notes);
34
+ if (args.url) cliArgs.push("--url", args.url);
35
+ if (args.recurrence) cliArgs.push("--recurrence", JSON.stringify(args.recurrence));
36
+ if (args.futureEvents) cliArgs.push("--future-events");
37
+ return cliArgs;
38
+ }
39
+
40
+ export function buildReminderCreateArgs(args, targetList) {
41
+ const cliArgs = ["create", "--title", args.title];
42
+ if (targetList) cliArgs.push("--list", targetList);
43
+ if (args.due) cliArgs.push("--due", args.due);
44
+ if (args.notes) cliArgs.push("--notes", args.notes);
45
+ if (args.priority !== undefined) cliArgs.push("--priority", String(args.priority));
46
+ if (args.url) cliArgs.push("--url", args.url);
47
+ if (args.alarm) {
48
+ for (const minutes of args.alarm) {
49
+ cliArgs.push("--alarm", String(minutes));
50
+ }
51
+ }
52
+ if (args.location) cliArgs.push("--location", JSON.stringify(args.location));
53
+ if (args.recurrence) cliArgs.push("--recurrence", JSON.stringify(args.recurrence));
54
+ return cliArgs;
55
+ }
56
+
57
+ export function buildReminderUpdateArgs(args) {
58
+ const cliArgs = ["update", "--id", args.id];
59
+ if (args.title) cliArgs.push("--title", args.title);
60
+ if (args.due) cliArgs.push("--due", args.due);
61
+ if (args.notes) cliArgs.push("--notes", args.notes);
62
+ if (args.priority !== undefined) cliArgs.push("--priority", String(args.priority));
63
+ if (args.url !== undefined) cliArgs.push("--url", args.url);
64
+ if (args.location) cliArgs.push("--location", JSON.stringify(args.location));
65
+ if (args.recurrence) cliArgs.push("--recurrence", JSON.stringify(args.recurrence));
66
+ return cliArgs;
67
+ }
68
+
69
+ function pushJSONIfNonEmpty(cliArgs, flag, value) {
70
+ if (Array.isArray(value) && value.length > 0) {
71
+ cliArgs.push(flag, JSON.stringify(value));
72
+ }
73
+ }
74
+
75
+ function pushContactSharedFields(cliArgs, args) {
76
+ if (args.firstName) cliArgs.push("--first-name", args.firstName);
77
+ if (args.lastName) cliArgs.push("--last-name", args.lastName);
78
+ if (args.middleName) cliArgs.push("--middle-name", args.middleName);
79
+ if (args.namePrefix) cliArgs.push("--name-prefix", args.namePrefix);
80
+ if (args.nameSuffix) cliArgs.push("--name-suffix", args.nameSuffix);
81
+ if (args.nickname) cliArgs.push("--nickname", args.nickname);
82
+ if (args.previousFamilyName) cliArgs.push("--previous-family-name", args.previousFamilyName);
83
+ if (args.phoneticGivenName) cliArgs.push("--phonetic-given-name", args.phoneticGivenName);
84
+ if (args.phoneticMiddleName) cliArgs.push("--phonetic-middle-name", args.phoneticMiddleName);
85
+ if (args.phoneticFamilyName) cliArgs.push("--phonetic-family-name", args.phoneticFamilyName);
86
+ if (args.phoneticOrganizationName) cliArgs.push("--phonetic-organization-name", args.phoneticOrganizationName);
87
+ if (args.organization) cliArgs.push("--organization", args.organization);
88
+ if (args.jobTitle) cliArgs.push("--job-title", args.jobTitle);
89
+ if (args.department) cliArgs.push("--department", args.department);
90
+ if (args.contactType) cliArgs.push("--contact-type", args.contactType);
91
+ if (args.email) cliArgs.push("--email", args.email);
92
+ if (args.phone) cliArgs.push("--phone", args.phone);
93
+ pushJSONIfNonEmpty(cliArgs, "--emails", args.emails);
94
+ pushJSONIfNonEmpty(cliArgs, "--phones", args.phones);
95
+ pushJSONIfNonEmpty(cliArgs, "--addresses", args.addresses);
96
+ pushJSONIfNonEmpty(cliArgs, "--urls", args.urls);
97
+ pushJSONIfNonEmpty(cliArgs, "--social-profiles", args.socialProfiles);
98
+ pushJSONIfNonEmpty(cliArgs, "--instant-messages", args.instantMessages);
99
+ pushJSONIfNonEmpty(cliArgs, "--relations", args.relations);
100
+ if (args.birthday) cliArgs.push("--birthday", args.birthday);
101
+ pushJSONIfNonEmpty(cliArgs, "--dates", args.dates);
102
+ if (args.notes) cliArgs.push("--notes", args.notes);
103
+ }
104
+
105
+ export function buildContactCreateArgs(args) {
106
+ const cliArgs = ["create"];
107
+ if (args.name) cliArgs.push("--name", args.name);
108
+ pushContactSharedFields(cliArgs, args);
109
+ return cliArgs;
110
+ }
111
+
112
+ export function buildContactUpdateArgs(args) {
113
+ const cliArgs = ["update", "--id", args.id];
114
+ pushContactSharedFields(cliArgs, args);
115
+ return cliArgs;
116
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "id": "apple-pim-cli",
3
+ "configSchema": {
4
+ "type": "object",
5
+ "properties": {
6
+ "binDir": {
7
+ "type": "string",
8
+ "description": "Path to Swift CLI binaries directory"
9
+ },
10
+ "profile": {
11
+ "type": "string",
12
+ "description": "Default PIM config profile name"
13
+ },
14
+ "configDir": {
15
+ "type": "string",
16
+ "description": "Default PIM config directory"
17
+ }
18
+ }
19
+ },
20
+ "uiHints": {
21
+ "binDir": {
22
+ "label": "CLI Binary Directory"
23
+ },
24
+ "profile": {
25
+ "label": "PIM Profile"
26
+ },
27
+ "configDir": {
28
+ "label": "Config Directory"
29
+ }
30
+ }
31
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "apple-pim-cli",
3
+ "version": "3.0.0",
4
+ "description": "OpenClaw plugin for macOS Calendar, Reminders, Contacts, and Mail via native Swift CLIs",
5
+ "type": "module",
6
+ "scripts": {
7
+ "prepack": "rm -rf lib && cp -R ../lib .",
8
+ "postpack": "rm -rf lib && ln -s ../lib ."
9
+ },
10
+ "openclaw": {
11
+ "extensions": ["./src/index.ts"]
12
+ },
13
+ "files": [
14
+ "src/",
15
+ "lib/",
16
+ "skills/",
17
+ "openclaw.plugin.json"
18
+ ],
19
+ "keywords": [
20
+ "openclaw",
21
+ "openclaw-plugin",
22
+ "calendar",
23
+ "reminders",
24
+ "contacts",
25
+ "mail",
26
+ "eventkit",
27
+ "macos",
28
+ "pim"
29
+ ],
30
+ "author": {
31
+ "name": "Omar Shahine",
32
+ "email": "omar@shahine.com"
33
+ },
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/omarshahine/Apple-PIM-Agent-Plugin.git",
38
+ "directory": "openclaw"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "os": ["darwin"],
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
47
+ "dependencies": {
48
+ "mailparser": "^3.9.3",
49
+ "turndown": "^7.2.2"
50
+ }
51
+ }
@@ -0,0 +1,166 @@
1
+ ---
2
+ name: apple-pim
3
+ description: |
4
+ Native macOS personal information management for calendars, reminders, contacts, and local Mail.app. Use when the user wants to schedule meetings, create events, check their calendar, create or complete reminders, look up contacts, find someone's phone number or email, manage tasks and to-do lists, triage local Mail.app messages, or troubleshoot EventKit, Contacts, or Mail.app permissions on macOS.
5
+ license: MIT
6
+ compatibility: |
7
+ macOS only. Requires TCC permissions for Calendars, Reminders, and Contacts via Privacy & Security settings. Mail features require Mail.app running with Automation permission granted.
8
+ metadata:
9
+ author: Omar Shahine
10
+ version: 3.0.0
11
+ openclaw:
12
+ os: [darwin]
13
+ requires:
14
+ bins: [calendar-cli]
15
+ ---
16
+
17
+ # Apple PIM (EventKit, Contacts & Mail)
18
+
19
+ ## Overview
20
+
21
+ Apple provides frameworks and scripting interfaces for personal information management:
22
+ - **EventKit**: Calendars and Reminders
23
+ - **Contacts**: Address book management
24
+ - **Mail.app**: Local email via JXA (JavaScript for Automation)
25
+
26
+ EventKit and Contacts require explicit user permission via privacy prompts. Mail.app requires Automation permission and must be running.
27
+
28
+ ## Tools
29
+
30
+ This plugin provides 5 tools:
31
+
32
+ | Tool | Actions | Domain |
33
+ |------|---------|--------|
34
+ | `apple_pim_calendar` | `list`, `events`, `get`, `search`, `create`, `update`, `delete`, `batch_create` | Calendar events via EventKit |
35
+ | `apple_pim_reminder` | `lists`, `items`, `get`, `search`, `create`, `complete`, `update`, `delete`, `batch_create`, `batch_complete`, `batch_delete` | Reminders via EventKit |
36
+ | `apple_pim_contact` | `groups`, `list`, `search`, `get`, `create`, `update`, `delete` | Contacts framework |
37
+ | `apple_pim_mail` | `accounts`, `mailboxes`, `messages`, `get`, `search`, `update`, `move`, `delete`, `batch_update`, `batch_delete` | Mail.app via JXA |
38
+ | `apple_pim_system` | `status`, `authorize`, `config_show`, `config_init` | Authorization & configuration |
39
+
40
+ ## Authorization & Permissions
41
+
42
+ ### Permission Model
43
+
44
+ Each PIM domain requires separate macOS authorization:
45
+
46
+ | Domain | Framework | Permission Section |
47
+ |--------|-----------|-------------------|
48
+ | Calendars | EventKit | Privacy & Security > Calendars |
49
+ | Reminders | EventKit | Privacy & Security > Reminders |
50
+ | Contacts | Contacts | Privacy & Security > Contacts |
51
+ | Mail | Automation (JXA) | Privacy & Security > Automation |
52
+
53
+ ### Authorization States
54
+
55
+ | State | Meaning | Action |
56
+ |-------|---------|--------|
57
+ | `notDetermined` | Never requested | Use `apple_pim_system` with action `authorize` to trigger prompt |
58
+ | `authorized` | Full access granted | Ready to use |
59
+ | `denied` | User refused access | Must enable in System Settings manually |
60
+ | `restricted` | System policy (MDM, parental) | Cannot override |
61
+ | `writeOnly` | Limited write access (macOS 17+) | Upgrade to Full Access in Settings |
62
+
63
+ ## Configuration (PIMConfig)
64
+
65
+ The PIM CLIs share a configuration system for filtering calendars/reminder lists and setting defaults.
66
+
67
+ ### Config File Locations
68
+
69
+ | Path | Purpose |
70
+ |------|---------|
71
+ | `~/.config/apple-pim/config.json` | Base configuration |
72
+ | `~/.config/apple-pim/profiles/{name}.json` | Named profile overrides |
73
+
74
+ ### Example Config
75
+
76
+ ```json
77
+ {
78
+ "calendars": {
79
+ "enabled": true,
80
+ "mode": "blocklist",
81
+ "items": ["US Holidays", "Birthdays"],
82
+ "default": "Personal"
83
+ },
84
+ "reminders": {
85
+ "enabled": true,
86
+ "mode": "allowlist",
87
+ "items": ["Tasks", "Shopping", "Work"],
88
+ "default": "Tasks"
89
+ },
90
+ "contacts": { "enabled": true },
91
+ "mail": { "enabled": true }
92
+ }
93
+ ```
94
+
95
+ ### Filter Modes
96
+
97
+ | Mode | Behavior |
98
+ |------|----------|
99
+ | `all` | No filtering — all calendars/lists are visible (default) |
100
+ | `allowlist` | Only calendars/lists named in `items` are visible |
101
+ | `blocklist` | All calendars/lists are visible EXCEPT those named in `items` |
102
+
103
+ ### Multi-Agent Isolation
104
+
105
+ All 5 tools accept optional `configDir` and `profile` parameters for per-call workspace isolation:
106
+
107
+ ```
108
+ apple_pim_calendar({ action: "list", configDir: "~/agents/travel/apple-pim" })
109
+ apple_pim_calendar({ action: "list", profile: "work" })
110
+ ```
111
+
112
+ **Priority chain**: Tool parameter > plugin config > env var > default (`~/.config/apple-pim/`)
113
+
114
+ ## Best Practices
115
+
116
+ ### Calendar Management
117
+ 1. **Use default calendar for new events** when user doesn't specify
118
+ 2. **Preserve recurrence rules** when updating recurring events
119
+ 3. **Handle `.thisEvent` vs `.futureEvents`** span for recurring event edits
120
+ 4. **Use `batch_create`** when creating multiple events for efficiency
121
+
122
+ ### EKSpan for Recurring Events
123
+
124
+ | Span | Effect | When to Use |
125
+ |------|--------|-------------|
126
+ | `.thisEvent` | Affects only the single occurrence | Default for delete and update |
127
+ | `.futureEvents` | Affects this and all future occurrences | Use when ending a series |
128
+
129
+ - **Delete**: Default is `.thisEvent`. Pass `futureEvents: true` to use `.futureEvents`.
130
+ - **Update**: Default is `.thisEvent`. Pass `futureEvents: true` to apply to future occurrences.
131
+ - **Remove recurrence**: Pass `recurrence: { frequency: "none" }` with `futureEvents: true`.
132
+
133
+ ### Reminder Management
134
+ 1. **Default to incomplete reminders** when listing
135
+ 2. **Use filters**: `overdue` for urgent, `today` for daily planning, `week` for review
136
+ 3. **Use batch operations** (`batch_complete`, `batch_delete`) for multiple items
137
+
138
+ ### Contact Management
139
+ 1. **Preserve existing data** when updating (only modify changed fields)
140
+ 2. **Handle labeled values carefully** — don't lose non-primary entries
141
+
142
+ ### Mail Management
143
+ 1. **Mail.app must be running** for all operations
144
+ 2. **Use batch operations** (`batch_update`, `batch_delete`) for inbox triage
145
+ 3. **Message IDs are RFC 2822** — stable across mailbox moves
146
+ 4. **Use mailbox/account hints** for faster lookups
147
+
148
+ ### Error Handling
149
+ 1. **Check authorization first** with `apple_pim_system` action `status`
150
+ 2. **Use `apple_pim_system` action `authorize`** for `notDetermined` domains
151
+ 3. **Guide users to System Settings** for `denied` domains
152
+
153
+ ## Troubleshooting
154
+
155
+ ### Permission Issues
156
+ - Use `apple_pim_system` with action `status` to check all domains
157
+ - Use `apple_pim_system` with action `authorize` to trigger prompts
158
+ - Check System Settings > Privacy & Security
159
+
160
+ ### Binary Not Found
161
+ - Run `./setup.sh --install` to build and install CLIs to `~/.local/bin/`
162
+ - Or set `binDir` in plugin config to point to your build directory
163
+
164
+ ### Configuration Issues
165
+ - **Unexpected filtering**: Use `apple_pim_system` action `config_show` to verify active config
166
+ - **Missing calendars/lists**: Use `apple_pim_system` action `config_init` to discover available items
package/src/index.ts ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * OpenClaw plugin entry for Apple PIM CLI Tools.
3
+ *
4
+ * Registers 5 tools that spawn the Swift CLIs directly (no MCP server).
5
+ * Supports per-call environment isolation via configDir/profile parameters
6
+ * for multi-agent workspaces.
7
+ */
8
+
9
+ import { createCLIRunner, findSwiftBinDir } from "../lib/cli-runner.js";
10
+ import { tools } from "../lib/schemas.js";
11
+ import { markToolResult, getDatamarkingPreamble } from "../lib/sanitize.js";
12
+ import { handleCalendar } from "../lib/handlers/calendar.js";
13
+ import { handleReminder } from "../lib/handlers/reminder.js";
14
+ import { handleContact } from "../lib/handlers/contact.js";
15
+ import { handleMail } from "../lib/handlers/mail.js";
16
+ import { handleApplePim } from "../lib/handlers/apple-pim.js";
17
+ import { existsSync } from "fs";
18
+ import { join, dirname } from "path";
19
+ import { execFileSync } from "child_process";
20
+ import { homedir } from "os";
21
+
22
+ // OpenClaw plugin config (set by the gateway from openclaw.plugin.json configSchema)
23
+ interface PluginConfig {
24
+ binDir?: string;
25
+ profile?: string;
26
+ configDir?: string;
27
+ }
28
+
29
+ // Tool args always include optional isolation params
30
+ interface ToolArgs {
31
+ action: string;
32
+ configDir?: string;
33
+ profile?: string;
34
+ [key: string]: unknown;
35
+ }
36
+
37
+ // OpenClaw tool registration interface
38
+ interface OpenClawContext {
39
+ config?: PluginConfig;
40
+ registerTool(definition: {
41
+ name: string;
42
+ description: string;
43
+ inputSchema: Record<string, unknown>;
44
+ execute: (args: Record<string, unknown>) => Promise<{ content: string }>;
45
+ }): void;
46
+ }
47
+
48
+ // Map MCP tool names to OpenClaw snake_case names
49
+ const TOOL_NAME_MAP: Record<string, string> = {
50
+ "calendar": "apple_pim_calendar",
51
+ "reminder": "apple_pim_reminder",
52
+ "contact": "apple_pim_contact",
53
+ "mail": "apple_pim_mail",
54
+ "apple-pim": "apple_pim_system",
55
+ };
56
+
57
+ // Map MCP tool names to handler functions
58
+ const HANDLERS: Record<string, (args: ToolArgs, runCLI: (cli: string, args: string[]) => Promise<object>) => Promise<object>> = {
59
+ "calendar": handleCalendar,
60
+ "reminder": handleReminder,
61
+ "contact": handleContact,
62
+ "mail": handleMail,
63
+ "apple-pim": handleApplePim,
64
+ };
65
+
66
+ /**
67
+ * Resolve the binary directory using a discovery chain:
68
+ * 1. Plugin config binDir
69
+ * 2. Env var APPLE_PIM_BIN_DIR
70
+ * 3. PATH lookup (which calendar-cli)
71
+ * 4. ~/.local/bin/ (setup.sh --install target)
72
+ */
73
+ function resolveBinDir(config?: PluginConfig): string {
74
+ // 1. Plugin config
75
+ if (config?.binDir && existsSync(join(config.binDir, "calendar-cli"))) {
76
+ return config.binDir;
77
+ }
78
+
79
+ // 2. Env var
80
+ const envBinDir = process.env.APPLE_PIM_BIN_DIR;
81
+ if (envBinDir && existsSync(join(envBinDir, "calendar-cli"))) {
82
+ return envBinDir;
83
+ }
84
+
85
+ // 3. PATH lookup (execFileSync avoids shell injection)
86
+ try {
87
+ const whichResult = execFileSync("which", ["calendar-cli"], { encoding: "utf8" }).trim();
88
+ if (whichResult) {
89
+ return dirname(whichResult);
90
+ }
91
+ } catch {
92
+ // Not on PATH, continue
93
+ }
94
+
95
+ // 4-5. Standard locations via findSwiftBinDir
96
+ return findSwiftBinDir();
97
+ }
98
+
99
+ /**
100
+ * Resolve per-call environment overrides for workspace isolation.
101
+ *
102
+ * Priority chain (per parameter):
103
+ * 1. Tool parameter (per-call override)
104
+ * 2. Plugin config (gateway-level default)
105
+ * 3. Process env (APPLE_PIM_CONFIG_DIR / APPLE_PIM_PROFILE)
106
+ * 4. Default (~/.config/apple-pim/)
107
+ */
108
+ function resolveEnvOverrides(args: ToolArgs, config?: PluginConfig): Record<string, string> {
109
+ const env: Record<string, string> = {};
110
+
111
+ // Resolve configDir
112
+ const configDir = args.configDir || config?.configDir || process.env.APPLE_PIM_CONFIG_DIR;
113
+ if (configDir) {
114
+ env.APPLE_PIM_CONFIG_DIR = configDir.replace(/^~/, homedir());
115
+ }
116
+
117
+ // Resolve profile
118
+ const profile = args.profile || config?.profile || process.env.APPLE_PIM_PROFILE;
119
+ if (profile) {
120
+ env.APPLE_PIM_PROFILE = profile;
121
+ }
122
+
123
+ return env;
124
+ }
125
+
126
+ /**
127
+ * OpenClaw plugin activation function.
128
+ * Called by the OpenClaw gateway when the plugin is loaded.
129
+ */
130
+ export default function activate(context: OpenClawContext): void {
131
+ const config = context.config;
132
+ const binDir = resolveBinDir(config);
133
+
134
+ for (const tool of tools) {
135
+ const openclawName = TOOL_NAME_MAP[tool.name];
136
+ const handler = HANDLERS[tool.name];
137
+
138
+ if (!openclawName || !handler) continue;
139
+
140
+ context.registerTool({
141
+ name: openclawName,
142
+ description: tool.description,
143
+ inputSchema: tool.inputSchema as Record<string, unknown>,
144
+
145
+ async execute(args: Record<string, unknown>) {
146
+ // Runtime validation — ensure required 'action' field is present and valid
147
+ if (typeof args.action !== "string" || !args.action) {
148
+ return {
149
+ content: JSON.stringify({ success: false, error: "Missing required 'action' parameter" }, null, 2),
150
+ };
151
+ }
152
+ const toolArgs = args as ToolArgs;
153
+
154
+ // Per-call environment isolation — never mutates process.env
155
+ const envOverrides = resolveEnvOverrides(toolArgs, config);
156
+ const { runCLI } = createCLIRunner(binDir, envOverrides);
157
+
158
+ try {
159
+ const result = await handler(toolArgs, runCLI);
160
+
161
+ // Apply datamarking for prompt injection defense
162
+ const markedResult = markToolResult(result, tool.name);
163
+ const preamble = getDatamarkingPreamble(tool.name);
164
+
165
+ return {
166
+ content: `${preamble}\n\n${JSON.stringify(markedResult, null, 2)}`,
167
+ };
168
+ } catch (error: unknown) {
169
+ const message = error instanceof Error ? error.message : String(error);
170
+ return {
171
+ content: JSON.stringify({ success: false, error: message }, null, 2),
172
+ };
173
+ }
174
+ },
175
+ });
176
+ }
177
+ }