@xiaotianxt/skills 0.1.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/EXCLUDED.md +42 -0
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/SECURITY.md +23 -0
- package/SOURCES.md +45 -0
- package/bin/skills.mjs +241 -0
- package/package.json +38 -0
- package/skills/1password/SKILL.md +94 -0
- package/skills/1password/agents/openai.yaml +4 -0
- package/skills/1password/references/item-management.md +80 -0
- package/skills/1password/references/op-cli.md +107 -0
- package/skills/apple-calendar-event/SKILL.md +81 -0
- package/skills/apple-calendar-event/agents/openai.yaml +4 -0
- package/skills/apple-calendar-event/scripts/calendar_audit.py +201 -0
- package/skills/apple-calendar-event/scripts/calendar_event.py +164 -0
- package/skills/bro-browser/SKILL.md +118 -0
- package/skills/bro-browser/agents/openai.yaml +4 -0
- package/skills/bro-browser/references/tool-map.md +102 -0
- package/skills/bro-browser/references/workflows.md +146 -0
- package/skills/bro-browser/scripts/bro-call.mjs +189 -0
- package/skills/calendar/SKILL.md +182 -0
- package/skills/calendar/agents/openai.yaml +4 -0
- package/skills/calendar/references/operations.md +255 -0
- package/skills/calendar/scripts/calendar_list_review.py +157 -0
- package/skills/calendar/scripts/event_dedupe_preview.py +155 -0
- package/skills/canvas/SKILL.md +70 -0
- package/skills/canvas/agents/openai.yaml +4 -0
- package/skills/canvas/references/canvas-api.md +76 -0
- package/skills/course-exam-review-planner/SKILL.md +127 -0
- package/skills/cx/SKILL.md +25 -0
- package/skills/gh-fix-ci/LICENSE.txt +201 -0
- package/skills/gh-fix-ci/SKILL.md +81 -0
- package/skills/gh-fix-ci/agents/openai.yaml +6 -0
- package/skills/gh-fix-ci/assets/github-small.svg +3 -0
- package/skills/gh-fix-ci/assets/github.png +0 -0
- package/skills/gh-fix-ci/scripts/inspect_pr_checks.py +509 -0
- package/skills/gh-review-workflow/SKILL.md +61 -0
- package/skills/gh-review-workflow/agents/openai.yaml +4 -0
- package/skills/gh-review-workflow/references/workflow.md +48 -0
- package/skills/gh-review-workflow/scripts/fetch_review_state.py +222 -0
- package/skills/gh-review-workflow/scripts/resolve_review_threads.py +83 -0
- package/skills/github/SKILL.md +74 -0
- package/skills/github/agents/openai.yaml +6 -0
- package/skills/github/assets/github-small.svg +3 -0
- package/skills/github/assets/github.png +0 -0
- package/skills/gws-calendar/SKILL.md +126 -0
- package/skills/gws-calendar-agenda/SKILL.md +52 -0
- package/skills/gws-calendar-insert/SKILL.md +66 -0
- package/skills/gws-docs/SKILL.md +48 -0
- package/skills/gws-docs-write/SKILL.md +49 -0
- package/skills/gws-drive/SKILL.md +137 -0
- package/skills/gws-drive-upload/SKILL.md +52 -0
- package/skills/gws-gmail/SKILL.md +62 -0
- package/skills/gws-gmail-forward/SKILL.md +55 -0
- package/skills/gws-gmail-reply/SKILL.md +58 -0
- package/skills/gws-gmail-reply-all/SKILL.md +62 -0
- package/skills/gws-gmail-send/SKILL.md +57 -0
- package/skills/gws-gmail-triage/SKILL.md +50 -0
- package/skills/gws-gmail-watch/SKILL.md +58 -0
- package/skills/gws-shared/SKILL.md +27 -0
- package/skills/helium-browser-mcp/SKILL.md +137 -0
- package/skills/helium-browser-mcp/agents/openai.yaml +4 -0
- package/skills/helium-browser-mcp/scripts/obmcp.mjs +92 -0
- package/skills/helium-browser-mcp/scripts/openbrowsermcp-stdio-proxy.mjs +170 -0
- package/skills/learn/SKILL.md +122 -0
- package/skills/learn/agents/openai.yaml +7 -0
- package/skills/learn/assets/AGENTS.template.md +33 -0
- package/skills/learn/assets/errorlog.template.typ +61 -0
- package/skills/learn/assets/reading-sequence.template.md +23 -0
- package/skills/learn/assets/source-index.template.md +17 -0
- package/skills/learn/assets/tasklog.template.typ +57 -0
- package/skills/learn/assets/workbook.template.typ +60 -0
- package/skills/learn/references/learning-science.md +103 -0
- package/skills/learn/scripts/init_learning_workspace.py +70 -0
- package/skills/macos-messages/SKILL.md +258 -0
- package/skills/memory/SKILL.md +33 -0
- package/skills/memory/codex.md +186 -0
- package/skills/memory/opencode.md +164 -0
- package/skills/mimestreamctl/SKILL.md +170 -0
- package/skills/mimestreamctl/agents/openai.yaml +4 -0
- package/skills/mimestreamctl/scripts/mimestreamctl +33 -0
- package/skills/mon/SKILL.md +51 -0
- package/skills/mon/scripts/mon_spend_review.py +458 -0
- package/skills/ocr/SKILL.md +136 -0
- package/skills/ocr/agents/openai.yaml +4 -0
- package/skills/ocr/references/local-ocr-best-practices.md +297 -0
- package/skills/ocr/references/mineru-api.md +159 -0
- package/skills/ocr/scripts/ocr-router +22 -0
- package/skills/ocr/scripts/ocr_router.py +741 -0
- package/skills/panopto-mp4-bulk-download/SKILL.md +57 -0
- package/skills/panopto-mp4-bulk-download/agents/openai.yaml +4 -0
- package/skills/panopto-mp4-bulk-download/references/url-patterns.md +26 -0
- package/skills/panopto-mp4-bulk-download/scripts/panopto_bulk_mp4.sh +213 -0
- package/skills/rust-systems-style/SKILL.md +109 -0
- package/skills/rust-systems-style/agents/openai.yaml +4 -0
- package/skills/rust-systems-style/references/rust-review-checklist.md +77 -0
- package/skills/rust-systems-style/references/style-sources.md +68 -0
- package/skills/ship-ai-native-cli/SKILL.md +76 -0
- package/skills/ship-ai-native-cli/agents/openai.yaml +4 -0
- package/skills/ship-ai-native-cli/references/case-notes.md +83 -0
- package/skills/ship-ai-native-cli/references/product-method.md +82 -0
- package/skills/ship-ai-native-cli/references/release-checklist.md +147 -0
- package/skills/ship-ai-native-cli/references/rust-cli-shape.md +111 -0
- package/skills/telegram-mtproto-session/SKILL.md +125 -0
- package/skills/telegram-mtproto-session/agents/openai.yaml +4 -0
- package/skills/telegram-mtproto-session/scripts/telegram_session.py +687 -0
- package/skills/tg/SKILL.md +173 -0
- package/skills/things3-manager/SKILL.md +116 -0
- package/skills/things3-manager/scripts/things +42 -0
- package/skills/things3-manager/scripts/things_cli.py +514 -0
- package/skills/web-artifacts-builder/LICENSE.txt +202 -0
- package/skills/web-artifacts-builder/SKILL.md +74 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +379 -0
- package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/yeet/LICENSE.txt +201 -0
- package/skills/yeet/SKILL.md +71 -0
- package/skills/yeet/agents/openai.yaml +6 -0
- package/skills/yeet/assets/yeet-small.svg +3 -0
- package/skills/yeet/assets/yeet.png +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# 1Password Item Management
|
|
2
|
+
|
|
3
|
+
Use this reference before creating, editing, copying, exporting, or deleting 1Password items.
|
|
4
|
+
|
|
5
|
+
## Creation Principles
|
|
6
|
+
|
|
7
|
+
- Prefer the 1Password app when the user needs to store a new plaintext secret.
|
|
8
|
+
- Do not ask the user to paste a plaintext secret into chat.
|
|
9
|
+
- Do not put sensitive values in command-line assignment statements. The `op item create --help` text warns that command arguments may be logged in shell history or visible to local processes.
|
|
10
|
+
- For CLI creation with secrets, use a short-lived JSON template or stdin, set restrictive permissions, and delete plaintext temporary files immediately.
|
|
11
|
+
- Use `--dry-run` for shape validation when possible.
|
|
12
|
+
- Use clear item titles and field names that match the service's docs.
|
|
13
|
+
|
|
14
|
+
## Field Types
|
|
15
|
+
|
|
16
|
+
Use explicit field types:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
Name[text]=value
|
|
20
|
+
Secret[concealed]=value
|
|
21
|
+
URL[url]=https://example.com
|
|
22
|
+
Old Field[delete]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Only conceal actual secrets: passwords, API keys, bearer tokens, refresh tokens, private keys, recovery codes, and cookies.
|
|
26
|
+
|
|
27
|
+
Use text or URL fields for usernames, account emails, client IDs, hostnames, ports, redirect URLs, docs links, and other identifiers.
|
|
28
|
+
|
|
29
|
+
## API Credential Items
|
|
30
|
+
|
|
31
|
+
For simple API keys in this user's setup, prefer an item category of `API Credential` with the secret in a field named `credential`.
|
|
32
|
+
|
|
33
|
+
Preferred read reference:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
op://Private/<title-or-id>/credential
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Useful non-secret fields:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
Account[text]
|
|
43
|
+
Service[text]
|
|
44
|
+
Documentation[url]
|
|
45
|
+
Developer Portal[url]
|
|
46
|
+
Notes
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
OAuth-style credentials should distinguish non-secret and secret fields:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
Client ID[text]
|
|
53
|
+
Client Secret[concealed]
|
|
54
|
+
Authorization URL[url]
|
|
55
|
+
Token Request URL[url]
|
|
56
|
+
Redirect URL[text]
|
|
57
|
+
Scopes[text]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Listing And Searching
|
|
61
|
+
|
|
62
|
+
Do not list vaults or items for convenience. If discovery is necessary, keep it narrow:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
op item list --vault Private --categories "API Credential" --format json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
When summarizing discovery results, report only item titles, IDs, vault names, categories, and timestamps needed for selection. Do not reveal field values.
|
|
69
|
+
|
|
70
|
+
## Service Accounts And Automation
|
|
71
|
+
|
|
72
|
+
Service accounts are production-impacting credentials. Before creating or modifying one, state:
|
|
73
|
+
|
|
74
|
+
- intended name
|
|
75
|
+
- target vault
|
|
76
|
+
- permissions
|
|
77
|
+
- where the token will be stored
|
|
78
|
+
- how it will be rotated or revoked
|
|
79
|
+
|
|
80
|
+
Do not print service account tokens in final responses. Store them directly in the intended secret manager when possible.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# 1Password CLI Patterns
|
|
2
|
+
|
|
3
|
+
Use this reference for local `op` CLI tasks. Prefer official command help for exact flags:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
op read --help
|
|
7
|
+
op run --help
|
|
8
|
+
op inject --help
|
|
9
|
+
op item --help
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Auth And Accounts
|
|
13
|
+
|
|
14
|
+
Check the CLI without touching secrets:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
op --version
|
|
18
|
+
op whoami
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If `op whoami` fails, run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
op signin
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If the user has multiple accounts, avoid guessing. Use the account explicitly once known:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
op whoami --account <account>
|
|
31
|
+
OP_ACCOUNT=<account> op whoami
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Secret References
|
|
35
|
+
|
|
36
|
+
Standard reference:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
op://<vault>/<item>/<field>
|
|
40
|
+
op://<vault>/<item>/<section>/<field>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Common local convention for API Credential items:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
op://Private/<title-or-id>/credential
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Use item IDs when the item title is unstable or ambiguous.
|
|
50
|
+
|
|
51
|
+
## Prefer `op run`
|
|
52
|
+
|
|
53
|
+
Use `op run` when the target command accepts environment variables. It resolves secret references for the child process and masks secrets in stdout/stderr by default.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
API_KEY='op://Private/Service API/credential' op run -- ./script-that-reads-env
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
With an env template:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# .env.op contains references, not plaintext values.
|
|
63
|
+
API_KEY=op://Private/Service API/credential
|
|
64
|
+
DATABASE_URL=op://Private/App Database/url
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
op run --env-file .env.op -- npm run dev
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Do not use `--no-masking` unless the user explicitly asks to display the secret value.
|
|
72
|
+
|
|
73
|
+
## Use `op inject` For Config Files
|
|
74
|
+
|
|
75
|
+
Use `op inject` when a config format needs inline substitution.
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
# config.yml.tpl
|
|
79
|
+
api_key: "{{ op://Private/Service API/credential }}"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
op inject -i config.yml.tpl -o config.yml
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Delete resolved files once they are no longer needed. Keep template files because they contain references, not secret values.
|
|
87
|
+
|
|
88
|
+
## Direct `op read`
|
|
89
|
+
|
|
90
|
+
Use direct reads only for commands that cannot use `op run` or templates. Keep the value inside the pipeline or command invocation, and do not show it in final responses.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
some-cli login --token "$(op read 'op://Private/Service API/credential')"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Avoid command shapes that expose secrets through process arguments when the command supports stdin or environment variables.
|
|
97
|
+
|
|
98
|
+
## Sensitive Outputs
|
|
99
|
+
|
|
100
|
+
Treat these as secret-bearing output:
|
|
101
|
+
|
|
102
|
+
- `op read`
|
|
103
|
+
- `op item get --reveal`
|
|
104
|
+
- `op run --no-masking`
|
|
105
|
+
- resolved files from `op inject`
|
|
106
|
+
- service account creation output
|
|
107
|
+
- one-time passwords, SSH private keys, cookies, and tokens
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apple-calendar-event
|
|
3
|
+
description: Inspect, create, and verify events in macOS Calendar.app with osascript-backed local automation. Use when Codex needs to audit local Apple Calendar sources/defaults, understand Calendar.sqlitedb cache state, or add an event directly to a specific Apple Calendar calendar on the current Mac. Prefer Google Calendar skills for durable calendar writes unless the user explicitly asks for Apple Calendar.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Apple Calendar Event
|
|
7
|
+
|
|
8
|
+
Use this skill for local `Calendar.app` inspection and direct Apple Calendar writes.
|
|
9
|
+
|
|
10
|
+
## Guardrails
|
|
11
|
+
|
|
12
|
+
- Prefer Google Calendar (`gws-calendar` / `gws-calendar-insert`) for durable user calendar writes unless the user explicitly asks for Apple Calendar.
|
|
13
|
+
- Treat `Calendar.sqlitedb` as a read-only cache for inspection. Never edit it directly.
|
|
14
|
+
- Do not trust Calendar.app's default calendar for automation. It may be `UseLastSelectedAsDefaultCalendar` or point at an unsuitable source.
|
|
15
|
+
- Do not identify a write target by display name alone when names are duplicated. Audit first, then use the exact calendar name only after confirming it is unambiguous enough for the requested write.
|
|
16
|
+
- Expect Apple Calendar to contain mixed stores: Google, iCloud, school CalDAV, subscribed calendars, Reminders, Siri suggestions, birthdays, and local/system stores.
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
### Audit local Calendar.app state
|
|
21
|
+
|
|
22
|
+
Use this before cleanup, migration planning, or any Apple write where the target source is unclear:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python3 scripts/calendar_audit.py
|
|
26
|
+
python3 scripts/calendar_audit.py --json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The audit reads `~/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb` in read-only mode, maps calendars to their stores/accounts, reports duplicate display names, and maps the Calendar.app default calendar UUID when possible.
|
|
30
|
+
|
|
31
|
+
### Create an Apple Calendar event
|
|
32
|
+
|
|
33
|
+
1. Confirm the event title, target calendar name, local date, start time, and end time.
|
|
34
|
+
2. If the target account/source is uncertain, run `scripts/calendar_audit.py` first.
|
|
35
|
+
3. If only the calendar name is uncertain, run `scripts/calendar_event.py list-calendars` and pick the exact calendar name from the output.
|
|
36
|
+
4. Put video links in `--location`. Put meeting numbers, interview notes, and buffer details in `--notes`.
|
|
37
|
+
5. If the user asks to reserve buffer time, reflect that in the final blocked time range before writing the event.
|
|
38
|
+
6. Create the event with `scripts/calendar_event.py create-event ...`.
|
|
39
|
+
7. Verify the write with `scripts/calendar_event.py verify-event ...`.
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
List calendars:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python3 scripts/calendar_event.py list-calendars
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Audit calendar sources and defaults:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python3 scripts/calendar_audit.py
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Create an event:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python3 scripts/calendar_event.py create-event \
|
|
59
|
+
--calendar "重要事件" \
|
|
60
|
+
--title "视频面试" \
|
|
61
|
+
--start "2026-04-15 11:30" \
|
|
62
|
+
--end "2026-04-15 12:45" \
|
|
63
|
+
--location "https://vc.feishu.cn/j/268399244" \
|
|
64
|
+
--notes $'面试时长:1小时,已额外预留缓冲时间至 12:45\n会议号:268399244'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Verify the event:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python3 scripts/calendar_event.py verify-event \
|
|
71
|
+
--calendar "重要事件" \
|
|
72
|
+
--title "视频面试" \
|
|
73
|
+
--start "2026-04-15 11:30"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Notes
|
|
77
|
+
|
|
78
|
+
- Expect macOS to prompt for `Calendar` and `Automation` access the first time `osascript` runs.
|
|
79
|
+
- Keep times explicit in local time. Do not rely on phrases like “tomorrow morning” without converting them to exact timestamps first.
|
|
80
|
+
- Prefer editing the title only when the title itself is user-visible. Put operational detail, meeting IDs, and links in `location` and `notes`.
|
|
81
|
+
- Verify after every write. If verification fails, inspect the target calendar name again before retrying.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Apple Calendar Event"
|
|
3
|
+
short_description: "Audit, create, and verify macOS Calendar.app events."
|
|
4
|
+
default_prompt: "Use this skill to audit local Calendar.app sources/defaults or create and verify an Apple Calendar event in a specific calendar. Prefer Google Calendar for durable writes unless Apple Calendar is explicitly requested."
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import plistlib
|
|
7
|
+
import sqlite3
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_DB = (
|
|
16
|
+
Path.home()
|
|
17
|
+
/ "Library"
|
|
18
|
+
/ "Group Containers"
|
|
19
|
+
/ "group.com.apple.calendar"
|
|
20
|
+
/ "Calendar.sqlitedb"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
STORE_TYPES = {
|
|
24
|
+
0: "local/default",
|
|
25
|
+
2: "caldav/account",
|
|
26
|
+
4: "subscribed",
|
|
27
|
+
5: "other/system",
|
|
28
|
+
6: "reminders",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_command(command: list[str]) -> str | None:
|
|
33
|
+
result = subprocess.run(command, capture_output=True, text=True)
|
|
34
|
+
if result.returncode != 0:
|
|
35
|
+
return None
|
|
36
|
+
return result.stdout
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_ical_defaults() -> dict[str, Any]:
|
|
40
|
+
output = run_command(["defaults", "export", "com.apple.iCal", "-"])
|
|
41
|
+
if not output:
|
|
42
|
+
return {}
|
|
43
|
+
try:
|
|
44
|
+
return plistlib.loads(output.encode())
|
|
45
|
+
except Exception:
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_writable_names() -> dict[str, list[bool]]:
|
|
50
|
+
script = [
|
|
51
|
+
'tell application "Calendar"',
|
|
52
|
+
"set rows to {}",
|
|
53
|
+
"repeat with c in calendars",
|
|
54
|
+
"set end of rows to ((name of c as text) & tab & (writable of c as text))",
|
|
55
|
+
"end repeat",
|
|
56
|
+
"set AppleScript's text item delimiters to linefeed",
|
|
57
|
+
"return rows as text",
|
|
58
|
+
"end tell",
|
|
59
|
+
]
|
|
60
|
+
command: list[str] = ["osascript"]
|
|
61
|
+
for line in script:
|
|
62
|
+
command.extend(["-e", line])
|
|
63
|
+
output = run_command(command)
|
|
64
|
+
writable: dict[str, list[bool]] = {}
|
|
65
|
+
if not output:
|
|
66
|
+
return writable
|
|
67
|
+
for line in output.splitlines():
|
|
68
|
+
if not line.strip():
|
|
69
|
+
continue
|
|
70
|
+
try:
|
|
71
|
+
name, value = line.rsplit("\t", 1)
|
|
72
|
+
except ValueError:
|
|
73
|
+
continue
|
|
74
|
+
writable.setdefault(name, []).append(value.lower() == "true")
|
|
75
|
+
return writable
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def query_calendars(db_path: Path) -> list[dict[str, Any]]:
|
|
79
|
+
if not db_path.exists():
|
|
80
|
+
raise SystemExit(f"Calendar database not found: {db_path}")
|
|
81
|
+
|
|
82
|
+
uri = f"file:{db_path}?mode=ro"
|
|
83
|
+
query = """
|
|
84
|
+
select
|
|
85
|
+
c.ROWID as calendar_rowid,
|
|
86
|
+
c.UUID as calendar_uuid,
|
|
87
|
+
c.title as calendar_title,
|
|
88
|
+
c.external_id as calendar_external_id,
|
|
89
|
+
c.type as calendar_type,
|
|
90
|
+
c.self_identity_email,
|
|
91
|
+
c.owner_identity_email,
|
|
92
|
+
c.shared_owner_address,
|
|
93
|
+
c.subcal_url,
|
|
94
|
+
s.ROWID as store_rowid,
|
|
95
|
+
s.name as store_name,
|
|
96
|
+
s.owner_name as store_owner,
|
|
97
|
+
s.type as store_type,
|
|
98
|
+
s.external_id as store_external_id,
|
|
99
|
+
s.persistent_id as store_persistent_id
|
|
100
|
+
from Calendar c
|
|
101
|
+
left join Store s on c.store_id = s.ROWID
|
|
102
|
+
order by s.name, c.display_order, c.title
|
|
103
|
+
"""
|
|
104
|
+
with sqlite3.connect(uri, uri=True) as conn:
|
|
105
|
+
conn.row_factory = sqlite3.Row
|
|
106
|
+
rows = [dict(row) for row in conn.execute(query)]
|
|
107
|
+
for row in rows:
|
|
108
|
+
row["store_type_label"] = STORE_TYPES.get(row.get("store_type"), "unknown")
|
|
109
|
+
return rows
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build_report(db_path: Path) -> dict[str, Any]:
|
|
113
|
+
calendars = query_calendars(db_path)
|
|
114
|
+
writable_names = load_writable_names()
|
|
115
|
+
defaults = load_ical_defaults()
|
|
116
|
+
|
|
117
|
+
by_uuid = {row["calendar_uuid"]: row for row in calendars if row.get("calendar_uuid")}
|
|
118
|
+
default_uuid = defaults.get("defaultCalendarID")
|
|
119
|
+
default_calendar = by_uuid.get(default_uuid) if default_uuid else None
|
|
120
|
+
|
|
121
|
+
for row in calendars:
|
|
122
|
+
values = writable_names.get(row["calendar_title"], [])
|
|
123
|
+
if not values:
|
|
124
|
+
row["applescript_writable_by_name"] = None
|
|
125
|
+
elif len(values) == 1:
|
|
126
|
+
row["applescript_writable_by_name"] = values[0]
|
|
127
|
+
else:
|
|
128
|
+
row["applescript_writable_by_name"] = "ambiguous"
|
|
129
|
+
|
|
130
|
+
counts = Counter(row["calendar_title"] for row in calendars)
|
|
131
|
+
duplicates = sorted(name for name, count in counts.items() if count > 1)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"database": str(db_path),
|
|
135
|
+
"default_policy": defaults.get("CalDefaultCalendar"),
|
|
136
|
+
"default_calendar_id": default_uuid,
|
|
137
|
+
"default_calendar": default_calendar,
|
|
138
|
+
"duplicate_display_names": duplicates,
|
|
139
|
+
"calendars": calendars,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def print_text(report: dict[str, Any]) -> None:
|
|
144
|
+
print(f"Database: {report['database']}")
|
|
145
|
+
print(f"Default policy: {report.get('default_policy') or '(unknown)'}")
|
|
146
|
+
default_calendar = report.get("default_calendar")
|
|
147
|
+
if default_calendar:
|
|
148
|
+
print(
|
|
149
|
+
"Default calendar: "
|
|
150
|
+
f"{default_calendar['calendar_title']} "
|
|
151
|
+
f"[store={default_calendar.get('store_name') or ''}, "
|
|
152
|
+
f"uuid={default_calendar.get('calendar_uuid') or ''}]"
|
|
153
|
+
)
|
|
154
|
+
elif report.get("default_calendar_id"):
|
|
155
|
+
print(f"Default calendar UUID: {report['default_calendar_id']} (not found)")
|
|
156
|
+
else:
|
|
157
|
+
print("Default calendar: (unknown)")
|
|
158
|
+
|
|
159
|
+
duplicates = report.get("duplicate_display_names") or []
|
|
160
|
+
if duplicates:
|
|
161
|
+
print("Duplicate display names: " + ", ".join(duplicates))
|
|
162
|
+
else:
|
|
163
|
+
print("Duplicate display names: none")
|
|
164
|
+
|
|
165
|
+
print()
|
|
166
|
+
print(
|
|
167
|
+
"Calendar\tStore\tStore type\tStore owner\tSelf identity\tOwner identity\tWritable by name\tUUID"
|
|
168
|
+
)
|
|
169
|
+
for row in report["calendars"]:
|
|
170
|
+
values = [
|
|
171
|
+
row.get("calendar_title") or "",
|
|
172
|
+
row.get("store_name") or "",
|
|
173
|
+
row.get("store_type_label") or "",
|
|
174
|
+
row.get("store_owner") or "",
|
|
175
|
+
row.get("self_identity_email") or "",
|
|
176
|
+
row.get("owner_identity_email") or "",
|
|
177
|
+
str(row.get("applescript_writable_by_name")),
|
|
178
|
+
row.get("calendar_uuid") or "",
|
|
179
|
+
]
|
|
180
|
+
print("\t".join(values))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def main() -> int:
|
|
184
|
+
parser = argparse.ArgumentParser(
|
|
185
|
+
description="Read-only audit of macOS Calendar.app sources and default calendar."
|
|
186
|
+
)
|
|
187
|
+
parser.add_argument("--db", default=str(DEFAULT_DB), help="Calendar.sqlitedb path.")
|
|
188
|
+
parser.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
|
|
189
|
+
args = parser.parse_args()
|
|
190
|
+
|
|
191
|
+
db_path = Path(os.path.expanduser(args.db)).resolve()
|
|
192
|
+
report = build_report(db_path)
|
|
193
|
+
if args.json:
|
|
194
|
+
print(json.dumps(report, indent=2, ensure_ascii=False, default=str))
|
|
195
|
+
else:
|
|
196
|
+
print_text(report)
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
sys.exit(main())
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
MONTH_NAMES = {
|
|
10
|
+
1: "january",
|
|
11
|
+
2: "february",
|
|
12
|
+
3: "march",
|
|
13
|
+
4: "april",
|
|
14
|
+
5: "may",
|
|
15
|
+
6: "june",
|
|
16
|
+
7: "july",
|
|
17
|
+
8: "august",
|
|
18
|
+
9: "september",
|
|
19
|
+
10: "october",
|
|
20
|
+
11: "november",
|
|
21
|
+
12: "december",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_local_timestamp(value: str) -> dt.datetime:
|
|
26
|
+
try:
|
|
27
|
+
return dt.datetime.strptime(value, "%Y-%m-%d %H:%M")
|
|
28
|
+
except ValueError as exc:
|
|
29
|
+
raise SystemExit(f"Invalid timestamp '{value}'. Use 'YYYY-MM-DD HH:MM'.") from exc
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_osascript(lines: list[str], args: list[str] | None = None) -> str:
|
|
33
|
+
command = ["osascript"]
|
|
34
|
+
for line in lines:
|
|
35
|
+
command.extend(["-e", line])
|
|
36
|
+
if args:
|
|
37
|
+
command.extend(args)
|
|
38
|
+
|
|
39
|
+
result = subprocess.run(command, capture_output=True, text=True)
|
|
40
|
+
if result.returncode != 0:
|
|
41
|
+
message = result.stderr.strip() or result.stdout.strip() or "osascript failed"
|
|
42
|
+
raise SystemExit(message)
|
|
43
|
+
return result.stdout.strip()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def applescript_date(var_name: str, value: dt.datetime) -> list[str]:
|
|
47
|
+
return [
|
|
48
|
+
f"set {var_name} to (current date)",
|
|
49
|
+
f"set year of {var_name} to {value.year}",
|
|
50
|
+
f"set month of {var_name} to {MONTH_NAMES[value.month]}",
|
|
51
|
+
f"set day of {var_name} to {value.day}",
|
|
52
|
+
f"set time of {var_name} to ({value.hour} * hours + {value.minute} * minutes + {value.second})",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cmd_list_calendars(_: argparse.Namespace) -> int:
|
|
57
|
+
output = run_osascript(
|
|
58
|
+
[
|
|
59
|
+
'tell application "Calendar"',
|
|
60
|
+
"set calendarNames to name of every calendar",
|
|
61
|
+
"set AppleScript's text item delimiters to linefeed",
|
|
62
|
+
"return calendarNames as text",
|
|
63
|
+
"end tell",
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
if output:
|
|
67
|
+
print(output)
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_create_event(args: argparse.Namespace) -> int:
|
|
72
|
+
start = parse_local_timestamp(args.start)
|
|
73
|
+
end = parse_local_timestamp(args.end)
|
|
74
|
+
if end <= start:
|
|
75
|
+
raise SystemExit("--end must be later than --start.")
|
|
76
|
+
|
|
77
|
+
output = run_osascript(
|
|
78
|
+
[
|
|
79
|
+
"on run argv",
|
|
80
|
+
"set calendarName to item 1 of argv",
|
|
81
|
+
"set eventTitle to item 2 of argv",
|
|
82
|
+
"set eventLocation to item 3 of argv",
|
|
83
|
+
"set eventNotes to item 4 of argv",
|
|
84
|
+
*applescript_date("startDate", start),
|
|
85
|
+
*applescript_date("endDate", end),
|
|
86
|
+
'tell application "Calendar"',
|
|
87
|
+
"set matchingCalendars to every calendar whose name is calendarName",
|
|
88
|
+
'if (count of matchingCalendars) is 0 then error "Calendar not found: " & calendarName',
|
|
89
|
+
"set targetCalendar to first item of matchingCalendars",
|
|
90
|
+
"tell targetCalendar",
|
|
91
|
+
"set newEvent to make new event with properties {summary:eventTitle, start date:startDate, end date:endDate, location:eventLocation, description:eventNotes}",
|
|
92
|
+
'return (summary of newEvent as text) & " | " & (start date of newEvent as text) & " | " & (end date of newEvent as text)',
|
|
93
|
+
"end tell",
|
|
94
|
+
"end tell",
|
|
95
|
+
"end run",
|
|
96
|
+
],
|
|
97
|
+
[args.calendar, args.title, args.location or "", args.notes or ""],
|
|
98
|
+
)
|
|
99
|
+
print(output)
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cmd_verify_event(args: argparse.Namespace) -> int:
|
|
104
|
+
start = parse_local_timestamp(args.start)
|
|
105
|
+
output = run_osascript(
|
|
106
|
+
[
|
|
107
|
+
"on run argv",
|
|
108
|
+
"set calendarName to item 1 of argv",
|
|
109
|
+
"set eventTitle to item 2 of argv",
|
|
110
|
+
*applescript_date("targetStart", start),
|
|
111
|
+
'tell application "Calendar"',
|
|
112
|
+
"set matchingCalendars to every calendar whose name is calendarName",
|
|
113
|
+
'if (count of matchingCalendars) is 0 then error "Calendar not found: " & calendarName',
|
|
114
|
+
"set targetCalendar to first item of matchingCalendars",
|
|
115
|
+
"tell targetCalendar",
|
|
116
|
+
"set matchingEvents to every event whose summary is eventTitle and start date is targetStart",
|
|
117
|
+
'if (count of matchingEvents) is 0 then error "Event not found"',
|
|
118
|
+
"set foundEvent to first item of matchingEvents",
|
|
119
|
+
'return (summary of foundEvent as text) & " | " & (start date of foundEvent as text) & " | " & (end date of foundEvent as text) & " | " & (location of foundEvent as text)',
|
|
120
|
+
"end tell",
|
|
121
|
+
"end tell",
|
|
122
|
+
"end run",
|
|
123
|
+
],
|
|
124
|
+
[args.calendar, args.title],
|
|
125
|
+
)
|
|
126
|
+
print(output)
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
131
|
+
parser = argparse.ArgumentParser(
|
|
132
|
+
description="List, create, and verify events in macOS Calendar.app."
|
|
133
|
+
)
|
|
134
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
135
|
+
|
|
136
|
+
list_parser = subparsers.add_parser("list-calendars", help="Print all calendar names.")
|
|
137
|
+
list_parser.set_defaults(func=cmd_list_calendars)
|
|
138
|
+
|
|
139
|
+
create_parser = subparsers.add_parser("create-event", help="Create a calendar event.")
|
|
140
|
+
create_parser.add_argument("--calendar", required=True, help="Target calendar name.")
|
|
141
|
+
create_parser.add_argument("--title", required=True, help="Event title.")
|
|
142
|
+
create_parser.add_argument("--start", required=True, help="Local start time in 'YYYY-MM-DD HH:MM'.")
|
|
143
|
+
create_parser.add_argument("--end", required=True, help="Local end time in 'YYYY-MM-DD HH:MM'.")
|
|
144
|
+
create_parser.add_argument("--location", default="", help="Event location or meeting URL.")
|
|
145
|
+
create_parser.add_argument("--notes", default="", help="Event notes.")
|
|
146
|
+
create_parser.set_defaults(func=cmd_create_event)
|
|
147
|
+
|
|
148
|
+
verify_parser = subparsers.add_parser("verify-event", help="Verify an event exists.")
|
|
149
|
+
verify_parser.add_argument("--calendar", required=True, help="Target calendar name.")
|
|
150
|
+
verify_parser.add_argument("--title", required=True, help="Event title.")
|
|
151
|
+
verify_parser.add_argument("--start", required=True, help="Local start time in 'YYYY-MM-DD HH:MM'.")
|
|
152
|
+
verify_parser.set_defaults(func=cmd_verify_event)
|
|
153
|
+
|
|
154
|
+
return parser
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main() -> int:
|
|
158
|
+
parser = build_parser()
|
|
159
|
+
args = parser.parse_args()
|
|
160
|
+
return args.func(args)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
sys.exit(main())
|