Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
883ae25791 | ||
|
|
de80b37c45 | ||
|
|
74beefa96a | ||
|
|
f3cd4eb683 |
54
.github/workflows/release.yml
vendored
54
.github/workflows/release.yml
vendored
@ -83,3 +83,57 @@ jobs:
|
||||
fi
|
||||
|
||||
gh release edit "$TAG" --notes-file "$notes_file"
|
||||
|
||||
update-homebrew-tap:
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
steps:
|
||||
- name: Resolve release tag
|
||||
run: echo "RELEASE_TAG=${{ inputs.tag }}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Dispatch tap formula update
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$GH_TOKEN" ]; then
|
||||
echo "::error::Set HOMEBREW_TAP_TOKEN with workflow access to steipete/homebrew-tap"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
request_id="remindctl-${RELEASE_TAG}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
expected_title="Update remindctl for ${RELEASE_TAG} (${request_id})"
|
||||
|
||||
gh workflow run update-formula.yml \
|
||||
--repo steipete/homebrew-tap \
|
||||
--ref main \
|
||||
-f formula=remindctl \
|
||||
-f tag="$RELEASE_TAG" \
|
||||
-f repository=steipete/remindctl \
|
||||
-f macos_artifact=remindctl-macos.zip \
|
||||
-f request_id="$request_id"
|
||||
|
||||
run_id=""
|
||||
for _ in {1..30}; do
|
||||
run_id=$(gh run list \
|
||||
--repo steipete/homebrew-tap \
|
||||
--workflow update-formula.yml \
|
||||
--branch main \
|
||||
--event workflow_dispatch \
|
||||
--limit 20 \
|
||||
--json databaseId,displayTitle \
|
||||
--jq ".[] | select(.displayTitle == \"$expected_title\") | .databaseId" | head -n1)
|
||||
if [ -n "$run_id" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [ -z "$run_id" ]; then
|
||||
echo "::error::Could not find tap workflow run with title: $expected_title"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh run watch "$run_id" \
|
||||
--repo steipete/homebrew-tap \
|
||||
--exit-status \
|
||||
--interval 10
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
- Resolve numeric edit/complete/delete indexes against the default `show` view instead of unrelated completed reminders.
|
||||
- Add a release helper for Homebrew tap updates; thanks @dinakars777.
|
||||
|
||||
## 0.2.0 - 2026-05-04
|
||||
- Add location-based reminder triggers via `--location`, `--leaving`, and `--radius`
|
||||
- Add simple recurrence support via `--repeat` and `--no-repeat`
|
||||
|
||||
167
SKILL.md
Normal file
167
SKILL.md
Normal file
@ -0,0 +1,167 @@
|
||||
---
|
||||
name: apple-reminders
|
||||
description: Use remindctl to inspect and manage Apple Reminders on macOS 14+, including show/list/add/edit/complete/delete/status/authorize workflows, due dates, alarms, recurrence, and location triggers.
|
||||
homepage: https://github.com/steipete/remindctl
|
||||
---
|
||||
|
||||
# Apple Reminders
|
||||
|
||||
Use `remindctl` for Apple Reminders on macOS. It uses Apple's public EventKit APIs, so changes sync through the normal Reminders/iCloud path.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- macOS 14+ with Reminders.app
|
||||
- `remindctl` installed and available on `PATH`
|
||||
- Install with `brew install steipete/tap/remindctl`
|
||||
- Reminders access for the terminal app that runs `remindctl`
|
||||
- Use `remindctl status` to check permission state before mutating data
|
||||
|
||||
## Use This Skill When
|
||||
|
||||
- The user wants to manage Apple Reminders from the terminal
|
||||
- The user asks for reminders, reminder lists, due dates, completion, deletion, or permission status
|
||||
- The user wants reminders that appear on iPhone, iPad, or Mac via Apple Reminders
|
||||
|
||||
## Do Not Use This Skill When
|
||||
|
||||
- The user wants a non-Reminders agent alert, cron job, or timed chatbot reminder
|
||||
- The user wants calendar events instead of reminders
|
||||
- The user needs native Reminders sections, tags, smart lists, attachments, or Apple's private "Urgent" toggle
|
||||
|
||||
## Current Command Model
|
||||
|
||||
- `remindctl` defaults to `show today`
|
||||
- `show` accepts filters: `today`, `tomorrow`, `week`, `overdue`, `upcoming`, `open`, `completed`, `all`, or a date string
|
||||
- `show --list <name>` limits a view to one list
|
||||
- `list` shows all lists with no arguments, or reminders in one or more named lists
|
||||
- `add` creates a reminder
|
||||
- `edit` updates a reminder by index or ID prefix
|
||||
- `complete` marks reminders complete
|
||||
- `delete` removes reminders
|
||||
- `status` reports Reminders authorization without prompting
|
||||
- `authorize` requests permission when possible
|
||||
|
||||
## Helpful Aliases
|
||||
|
||||
- `lists` and `ls` map to `list`
|
||||
- `rm` maps to `delete`
|
||||
- `done` maps to `complete`
|
||||
|
||||
## Output And Flags
|
||||
|
||||
- `--json`, `-j`, `--json-output`, and `--jsonOutput` emit JSON
|
||||
- `--plain` emits stable tab-separated output
|
||||
- `--quiet` emits minimal output
|
||||
- `--no-color` disables colored output
|
||||
- `--no-input` disables interactive prompts
|
||||
|
||||
Prefer `--json` when another step needs machine-readable data. JSON can include EventKit metadata such as `creationDate`, `lastModifiedDate`, `url`, `alarmDate`, `locationTrigger`, and `recurrenceRule`.
|
||||
|
||||
## Reading Reminder Data
|
||||
|
||||
- Use `remindctl today --json` or `remindctl show --json` to inspect reminders
|
||||
- Use `remindctl open --json` for all incomplete reminders, including reminders without due dates
|
||||
- Use `remindctl list --json` to inspect lists
|
||||
- Use `remindctl list Work Errands --json` to inspect multiple lists together
|
||||
- Use `remindctl status --json` to inspect authorization state
|
||||
- Reminder IDs and display indexes come from `show` output; `complete`, `delete`, and `edit` accept either an index or an ID prefix
|
||||
|
||||
## Creating Reminders
|
||||
|
||||
`add` accepts the title as a positional argument or via `--title`, but not both.
|
||||
|
||||
Important options:
|
||||
|
||||
- `--list <name>` choose the target list
|
||||
- `--due <date>` set the due date
|
||||
- `--alarm <date>` set the alarm date
|
||||
- `--notes <text>` add notes
|
||||
- `--repeat <rule>` set simple recurrence
|
||||
- `--priority <none|low|medium|high>` set priority
|
||||
- `--location <address>` create a location trigger
|
||||
- `--radius <meters>` adjust geofence radius
|
||||
- `--leaving` trigger on leaving instead of arriving
|
||||
|
||||
If no list is provided, `remindctl` uses the Reminders app's default list. Do not assume the default is a specific list name. If the system has no default reminder list, specify `--list`.
|
||||
|
||||
Use `--location` whenever using `--radius` or `--leaving`; those flags are invalid on their own.
|
||||
|
||||
## Editing Reminders
|
||||
|
||||
`edit` can update:
|
||||
|
||||
- `--title`
|
||||
- `--list`
|
||||
- `--due` or `--clear-due`
|
||||
- `--alarm` or `--clear-alarm`
|
||||
- `--notes`
|
||||
- `--repeat` or `--no-repeat`
|
||||
- `--priority`
|
||||
- `--complete` or `--incomplete`
|
||||
|
||||
Reject conflicting combinations such as `--due` with `--clear-due`, `--alarm` with `--clear-alarm`, `--repeat` with `--no-repeat`, or `--complete` with `--incomplete`.
|
||||
|
||||
Use `remindctl edit <id> --list <new-list>` to move a reminder between lists. Do not tell the user to delete and recreate the reminder for a move; the command already supports moving it directly.
|
||||
|
||||
## Dates
|
||||
|
||||
Accepted date inputs:
|
||||
|
||||
- `today`, `tomorrow`, `yesterday`
|
||||
- `YYYY-MM-DD`
|
||||
- `YYYY-MM-DD HH:mm`
|
||||
- ISO 8601 with timezone, such as `2026-01-03T12:34:56Z`
|
||||
- Local ISO 8601 without timezone, such as `2026-01-03T12:34:56`
|
||||
|
||||
Rules:
|
||||
|
||||
- Date-only inputs create all-day reminders
|
||||
- Date-time inputs create timed reminders
|
||||
- Timed due reminders get a notification alarm at the due time unless `--alarm` overrides it
|
||||
|
||||
If the user provides only a calendar date, prefer a date-only value instead of inventing a time.
|
||||
|
||||
Supported repeat values:
|
||||
|
||||
- `daily`, `weekly`, `biweekly`, `monthly`, `yearly`
|
||||
- `every N days/weeks/months/years`
|
||||
|
||||
## Lists
|
||||
|
||||
- `remindctl list` prints all lists with reminder and overdue counts
|
||||
- `remindctl list <name...>` prints reminders in one or more lists
|
||||
- `remindctl list <name> --create` creates the list if missing
|
||||
- `remindctl list <name> --delete` deletes the list
|
||||
- `remindctl list <name> --rename <new-name>` renames the list
|
||||
- `remindctl list <name> --force` skips confirmation for destructive list deletion
|
||||
- Create, delete, and rename accept one list name only
|
||||
|
||||
## Completion And Deletion
|
||||
|
||||
- `complete` and `delete` require one or more IDs or indexes
|
||||
- `delete` prompts for confirmation unless `--force` or `--no-input` suppresses it
|
||||
- `complete` supports `--dry-run`
|
||||
- `delete` supports `--dry-run`
|
||||
|
||||
## Permissions
|
||||
|
||||
Use `remindctl status` first when permission state matters.
|
||||
|
||||
- `status` never prompts
|
||||
- `authorize` triggers the system prompt when the state is `notDetermined`
|
||||
- If access is denied, direct the user to `System Settings > Privacy & Security > Reminders`
|
||||
- If the prompt does not appear, the current workaround is to run:
|
||||
|
||||
```bash
|
||||
osascript -e 'tell application "Reminders" to get name of reminders'
|
||||
```
|
||||
|
||||
When running over SSH, grant access on the Mac that actually runs `remindctl`.
|
||||
|
||||
## Response Discipline
|
||||
|
||||
- Confirm the reminder title, list, and due date before creating it if any of them are ambiguous
|
||||
- Use the exact command syntax shown by the current implementation
|
||||
- Do not reuse stale guidance about deleting and recreating reminders to move them between lists
|
||||
- Do not assume a fixed default list name
|
||||
- Do not promise unsupported private Reminders.app features; `remindctl` intentionally stays on public EventKit APIs
|
||||
@ -5,18 +5,20 @@ public enum IDResolver {
|
||||
|
||||
public static func resolve(
|
||||
_ inputs: [String],
|
||||
from reminders: [ReminderItem]
|
||||
from reminders: [ReminderItem],
|
||||
numericFrom numericReminders: [ReminderItem]? = nil
|
||||
) throws -> [ReminderItem] {
|
||||
let sorted = ReminderFiltering.sort(reminders)
|
||||
let numericSorted = ReminderFiltering.sort(numericReminders ?? reminders)
|
||||
var resolved: [ReminderItem] = []
|
||||
for input in inputs {
|
||||
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let index = Int(trimmed) {
|
||||
let idx = index - 1
|
||||
guard idx >= 0 && idx < sorted.count else {
|
||||
guard idx >= 0 && idx < numericSorted.count else {
|
||||
throw RemindCoreError.invalidIdentifier(trimmed)
|
||||
}
|
||||
resolved.append(sorted[idx])
|
||||
resolved.append(numericSorted[idx])
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@ -42,6 +42,11 @@ enum CommandHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
static func resolveShowIdentifiers(_ inputs: [String], from reminders: [ReminderItem]) throws -> [ReminderItem] {
|
||||
let defaultShowReminders = ReminderFiltering.apply(reminders, filter: .today)
|
||||
return try IDResolver.resolve(inputs, from: reminders, numericFrom: defaultShowReminders)
|
||||
}
|
||||
|
||||
private static func parseCustomRecurrence(_ normalized: String, original: String) throws -> RecurrenceRule {
|
||||
let parts = normalized.split(separator: " ")
|
||||
guard parts.count == 3, parts[0] == "every", let interval = Int(parts[1]), interval > 0 else {
|
||||
|
||||
@ -32,7 +32,7 @@ enum CompleteCommand {
|
||||
let store = RemindersStore()
|
||||
try await store.requestAccess()
|
||||
let reminders = try await store.reminders(in: nil)
|
||||
let resolved = try IDResolver.resolve(inputs, from: reminders)
|
||||
let resolved = try CommandHelpers.resolveShowIdentifiers(inputs, from: reminders)
|
||||
|
||||
if values.flag("dryRun") {
|
||||
OutputRenderer.printReminders(resolved, format: runtime.outputFormat)
|
||||
|
||||
@ -33,7 +33,7 @@ enum DeleteCommand {
|
||||
let store = RemindersStore()
|
||||
try await store.requestAccess()
|
||||
let reminders = try await store.reminders(in: nil)
|
||||
let resolved = try IDResolver.resolve(inputs, from: reminders)
|
||||
let resolved = try CommandHelpers.resolveShowIdentifiers(inputs, from: reminders)
|
||||
|
||||
if values.flag("dryRun") {
|
||||
OutputRenderer.printReminders(resolved, format: runtime.outputFormat)
|
||||
|
||||
@ -57,7 +57,7 @@ enum EditCommand {
|
||||
let store = RemindersStore()
|
||||
try await store.requestAccess()
|
||||
let reminders = try await store.reminders(in: nil)
|
||||
let resolved = try IDResolver.resolve([input], from: reminders)
|
||||
let resolved = try CommandHelpers.resolveShowIdentifiers([input], from: reminders)
|
||||
guard let reminder = resolved.first else {
|
||||
throw RemindCoreError.reminderNotFound(input)
|
||||
}
|
||||
|
||||
@ -38,6 +38,13 @@ struct IDResolverTests {
|
||||
#expect(resolved.first?.title == "First")
|
||||
}
|
||||
|
||||
@Test("Resolve numeric indexes from filtered show output")
|
||||
func resolveIndexFromFilteredShowOutput() throws {
|
||||
let all = sampleReminders()
|
||||
let resolved = try IDResolver.resolve(["1"], from: all, numericFrom: [all[1]])
|
||||
#expect(resolved.first?.title == "Second")
|
||||
}
|
||||
|
||||
@Test("Resolve by prefix")
|
||||
func resolvePrefix() throws {
|
||||
let resolved = try IDResolver.resolve(["abcd"], from: sampleReminders())
|
||||
|
||||
@ -30,8 +30,9 @@
|
||||
```sh
|
||||
gh release create v0.2.0 /tmp/remindctl-macos.zip -t "v0.2.0" -F /tmp/release-notes.txt
|
||||
```
|
||||
5. Homebrew tap
|
||||
- Update `../homebrew-tap/Formula/remindctl.rb` to point at the GitHub release asset.
|
||||
5. Update Homebrew tap
|
||||
- Run `scripts/update-homebrew.sh vX.Y.Z` to trigger and watch the centralized formula updater.
|
||||
- Requires a GitHub token with workflow dispatch access to `steipete/homebrew-tap`.
|
||||
|
||||
## What happens in CI
|
||||
- Release signing + notarization are done locally via `scripts/sign-and-notarize.sh`.
|
||||
|
||||
50
scripts/update-homebrew.sh
Executable file
50
scripts/update-homebrew.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <release-tag>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG="$1"
|
||||
TAP_REPO="steipete/homebrew-tap"
|
||||
WORKFLOW="update-formula.yml"
|
||||
SAFE_TAG=$(printf '%s' "$TAG" | tr -c 'A-Za-z0-9._-' '-')
|
||||
REQUEST_ID="remindctl-${SAFE_TAG}-$(date -u +%Y%m%dT%H%M%SZ)-$$"
|
||||
|
||||
gh workflow run "$WORKFLOW" \
|
||||
--repo "$TAP_REPO" \
|
||||
--ref main \
|
||||
-f formula=remindctl \
|
||||
-f tag="$TAG" \
|
||||
-f repository=steipete/remindctl \
|
||||
-f macos_artifact="remindctl-macos.zip" \
|
||||
-f request_id="$REQUEST_ID"
|
||||
|
||||
echo "Homebrew tap update dispatched: $REQUEST_ID"
|
||||
|
||||
RUN_ID=""
|
||||
for _ in {1..30}; do
|
||||
RUN_ID=$(gh run list \
|
||||
--repo "$TAP_REPO" \
|
||||
--workflow "$WORKFLOW" \
|
||||
--branch main \
|
||||
--limit 20 \
|
||||
--json databaseId,displayTitle \
|
||||
--jq ".[] | select(.displayTitle | contains(\"($REQUEST_ID)\")) | .databaseId" \
|
||||
| head -n 1)
|
||||
|
||||
if [[ -n "$RUN_ID" ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [[ -z "$RUN_ID" ]]; then
|
||||
echo "Timed out waiting for Homebrew tap workflow run: $REQUEST_ID" >&2
|
||||
echo "Monitor: https://github.com/$TAP_REPO/actions/workflows/$WORKFLOW" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh run watch "$RUN_ID" --repo "$TAP_REPO" --exit-status
|
||||
Loading…
Reference in New Issue
Block a user