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
package/lib/tool-args.js
ADDED
|
@@ -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
|
+
}
|