Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"name": "apple-pim",
"source": "./",
"description": "Native macOS integration for Calendar, Reminders, Contacts, and Mail using EventKit, Contacts, and JXA frameworks",
"version": "3.7.20",
"version": "3.7.22",
"keywords": [
"calendar",
"reminders",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apple-pim",
"version": "3.7.20",
"version": "3.7.22",
"description": "Native macOS integration for Calendar, Reminders, Contacts, and Mail using EventKit, Contacts, and JXA frameworks",
"author": {
"name": "Omar Shahine",
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,12 @@ Config files are stored at `~/.config/apple-pim/`:

### Filter Modes

- **allowlist**: Only listed calendars/lists are accessible
- **allowlist**: Only listed items are accessible
- **blocklist**: All EXCEPT listed items are accessible
- **all**: No filtering (default if no config file exists)

Filtering applies per domain: calendars filter by calendar name, reminders by list name, contacts by account container name (e.g. "iCloud", "Work Exchange", "Personal Gmail"). Use `contacts-cli containers` to see available account names.

### Profiles

Profiles let you give different agents different access to your PIM data. Each profile overrides specific domain sections from the base config — fields not in the profile are inherited from the base.
Expand All @@ -213,6 +215,11 @@ Profiles let you give different agents different access to your PIM data. Each p
"mode": "allowlist",
"items": ["Work"]
},
"contacts": {
"enabled": true,
"mode": "allowlist",
"items": ["Work Exchange"]
},
"mail": {
"enabled": false
},
Expand Down Expand Up @@ -364,7 +371,9 @@ calendar-cli events --from today --to tomorrow
calendar-cli create --title "Lunch" --start "tomorrow 12pm" --duration 60
reminder-cli lists
reminder-cli items --list "Personal" --filter overdue
contacts-cli containers # List contact account containers
contacts-cli search "John"
contacts-cli search "John" --profile work # Scoped to work contacts only
mail-cli messages --mailbox INBOX --limit 10
mail-cli send --to "user@example.com" --subject "Hello" --body "Message"
mail-cli send --to "user@example.com" --subject "Report" --body "See attached" --attachment ~/report.pdf
Expand Down Expand Up @@ -460,7 +469,7 @@ mail-cli secrets set smtp.icloud.password --store openclaw
|------|---------|--------|
| `calendar` / `apple_pim_calendar` | `list`, `events`, `get`, `search`, `create`, `update`, `delete`, `batch_create` | Calendar events via EventKit |
| `reminder` / `apple_pim_reminder` | `lists`, `items`, `get`, `search`, `create`, `complete`, `update`, `delete`, `batch_create`, `batch_complete`, `batch_delete` | Reminders via EventKit |
| `contact` / `apple_pim_contact` | `groups`, `list`, `search`, `get`, `create`, `update`, `delete` | Contacts framework |
| `contact` / `apple_pim_contact` | `containers`, `groups`, `list`, `search`, `get`, `create`, `update`, `delete` | Contacts framework |
| `mail` / `apple_pim_mail` | `accounts`, `mailboxes`, `messages`, `get`, `search`, `send`, `reply`, `save_attachment`, `update`, `move`, `delete`, `batch_update`, `batch_delete`, `auth_check` | Mail.app via JXA/AppleScript |
| `apple-pim` / `apple_pim_system` | `status`, `authorize`, `config_show`, `config_init` | Authorization & configuration |

Expand Down
12 changes: 11 additions & 1 deletion commands/contacts.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Manage macOS contacts - list, search, get details, create, update, delete
argument-hint: "[groups|list|search|get|create|update|delete] [options]"
argument-hint: "[containers|groups|list|search|get|create|update|delete] [options]"
allowed-tools:
- mcp__apple-pim__contact
---
Expand All @@ -13,6 +13,9 @@ Manage contacts using the Apple Contacts framework.

When the user runs this command, determine which operation they need and use the `contact` tool with the appropriate action:

### List Containers
Use `contact` with action `containers` to show all contact accounts (iCloud, Exchange, Google, etc.).

### List Groups
Use `contact` with action `groups` to show all contact groups.

Expand All @@ -35,6 +38,7 @@ Use `contact` with action `get` to get full details for a contact:
### Create Contact
Use `contact` with action `create` to create a new contact:
- Optional: `name` (full name) OR `firstName`/`lastName`
- Optional: `container` (target account name — use `containers` to see available accounts)
- Optional: `email`, `phone`, `organization`, `jobTitle`, `notes`, `birthday`

### Update Contact
Expand All @@ -54,6 +58,11 @@ Use `contact` with action `delete` to remove a contact:

## Examples

**List contact accounts:**
```
/apple-pim:contacts containers
```

**List contact groups:**
```
/apple-pim:contacts groups
Expand Down Expand Up @@ -82,6 +91,7 @@ Use `contact` with action `delete` to remove a contact:
```
/apple-pim:contacts create --name "Jane Doe" --email "jane@example.com"
/apple-pim:contacts create --first-name "John" --last-name "Smith" --phone "555-1234" --organization "Acme Corp"
/apple-pim:contacts create --name "Jane Doe" --container "iCloud" --email "jane@example.com"
```

**Update a contact:**
Expand Down
25 changes: 22 additions & 3 deletions docs/multi-agent-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ Profiles are the simplest approach. All agents share `~/.config/apple-pim/` but
"mode": "allowlist",
"items": ["Travel"]
},
"mail": { "enabled": false },
"contacts": { "enabled": false },
"mail": { "enabled": false },
"default_calendar": "Travel",
"default_reminder_list": "Travel"
}
```

**`work.json`** — work calendar only:
**`work.json`** — work calendar and work contacts only:
```json
{
"calendars": {
Expand All @@ -72,13 +72,31 @@ Profiles are the simplest approach. All agents share `~/.config/apple-pim/` but
"mode": "allowlist",
"items": ["Work"]
},
"contacts": { "enabled": true },
"contacts": {
"enabled": true,
"mode": "allowlist",
"items": ["Work Exchange"]
},
"mail": { "enabled": false },
"default_calendar": "Work",
"default_reminder_list": "Work"
}
```

**`family.json`** — shared family contacts only:
```json
{
"calendars": { "enabled": false },
"reminders": { "enabled": false },
"contacts": {
"enabled": true,
"mode": "allowlist",
"items": ["Family iCloud"]
},
"mail": { "enabled": false }
}
```

### Assign Profiles

**OpenClaw plugin** (recommended — per-call isolation):
Expand Down Expand Up @@ -114,6 +132,7 @@ Profiles use **whole-section replacement**, not field-level merge:
|-------------|---------|--------|
| `calendars: {mode: "all"}` | `calendars: {mode: "allowlist", items: ["Work"]}` | Profile's calendars config used entirely |
| `reminders: {mode: "allowlist", items: ["A", "B"]}` | *(not specified)* | Base's reminders config inherited |
| `contacts: {mode: "all"}` | `contacts: {mode: "allowlist", items: ["Work Exchange"]}` | Profile's contacts config used entirely |
| `default_calendar: "Personal"` | `default_calendar: "Work"` | Profile's default used |

## Strategy 2: Config Directory Isolation
Expand Down
19 changes: 19 additions & 0 deletions evals/fixtures/contact/containers-results.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"containers": [
{
"name": "iCloud",
"id": "A1B2C3D4-5678-90AB-CDEF-111111111111",
"type": "CardDAV"
},
{
"name": "Gmail",
"id": "E5F6A7B8-9012-34CD-EF56-222222222222",
"type": "CardDAV"
},
{
"name": "Exchange",
"id": "C9D0E1F2-3456-78AB-CD90-333333333333",
"type": "Exchange"
}
]
}
14 changes: 14 additions & 0 deletions evals/scenarios/tool-call-correctness.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ cases:
expect:
throws: "Contact ID is required"

- id: contact-containers
tool: contact
description: "Containers action produces correct CLI args"
input: { action: containers }
expect:
cli_args_contain: ["containers"]

- id: contact-create-with-container
tool: contact
description: "Container param passes --container flag on create"
input: { action: create, name: "Jane Doe", container: "iCloud" }
expect:
cli_args_contain: ["create", "--name", "--container", "iCloud"]

- id: cal-delete-future-events
tool: calendar
description: "futureEvents flag passed on delete"
Expand Down
2 changes: 1 addition & 1 deletion evals/tests/safety.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe("Category 4: Safety Properties", () => {
const contactTool = tools.find((t) => t.name === "contact");
const actions = contactTool.inputSchema.properties.action.enum;
const covered = new Set([
"groups", "list", "search", "get", "create", "update", "delete", "schema",
"containers", "groups", "list", "search", "get", "create", "update", "delete", "schema",
]);
for (const action of actions) {
expect(covered.has(action)).toBe(true);
Expand Down
3 changes: 3 additions & 0 deletions lib/handlers/contact.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export async function handleContact(args, runCLI) {
case "groups":
return await runCLI("contacts-cli", ["groups"]);

case "containers":
return await runCLI("contacts-cli", ["containers"]);

case "list":
cliArgs.push("list");
if (args.group) cliArgs.push("--group", args.group);
Expand Down
5 changes: 3 additions & 2 deletions lib/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,13 @@ export const tools = [
{
name: "contact",
description:
"Manage macOS contacts. Actions: groups (list groups), list (list contacts), search (by name/email/phone), get (by ID with photo), create, update, delete, schema (show input schema).",
"Manage macOS contacts. Actions: containers (list contact accounts), groups (list groups), list (list contacts), search (by name/email/phone), get (by ID with photo), create, update, delete, schema (show input schema).",
inputSchema: {
type: "object",
properties: {
action: {
type: "string",
enum: ["groups", "list", "search", "get", "create", "update", "delete", "schema"],
enum: ["containers", "groups", "list", "search", "get", "create", "update", "delete", "schema"],
description: "Operation to perform",
},
...agentDXProperties,
Expand Down Expand Up @@ -348,6 +348,7 @@ export const tools = [
},
},
notes: { type: "string" },
container: { type: "string", description: "Target container/account name for create" },
configDir: { type: "string", description: "Override PIM config directory (OpenClaw only — ignored by MCP server)" },
profile: { type: "string", description: "Override PIM profile name (OpenClaw only — MCP server uses APPLE_PIM_PROFILE env)" },
},
Expand Down
1 change: 1 addition & 0 deletions lib/tool-args.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ function pushContactSharedFields(cliArgs, args) {
export function buildContactCreateArgs(args) {
const cliArgs = ["create"];
if (args.name) cliArgs.push("--name", args.name);
if (args.container) cliArgs.push("--container", args.container);
pushContactSharedFields(cliArgs, args);
return cliArgs;
}
Expand Down
Loading
Loading