Compare commits

...

4 Commits
v0.2.0 ... main

Author SHA1 Message Date
Peter Steinberger
883ae25791
fix: resolve reminder indexes from show view
Some checks failed
CI / build (push) Has been cancelled
2026-05-08 15:13:00 +01:00
Peter Steinberger
de80b37c45
ci: update homebrew tap on release
Some checks are pending
CI / build (push) Waiting to run
2026-05-07 03:56:52 +01:00
Dinakar Sarbada
74beefa96a
docs: integrate Homebrew tap update into release flow
Some checks failed
CI / build (push) Has been cancelled
Co-authored-by: Dinakar Sarbada <dinakars777@users.noreply.github.com>
2026-05-04 09:04:27 +01:00
Wenyou Yi
f3cd4eb683
docs: add remindctl skill for agentic tools
Add an apple-reminders skill that documents the current remindctl command surface for agentic tools.

Refined before merge to match the 0.2.0 release: macOS/Homebrew requirements, JSON aliases, open filter, metadata fields, recurrence, alarms, location trigger constraints, permissions, and EventKit limitations.

Co-authored-by: Allen Yi <yiwenyou_allen@outlook.com>
2026-05-04 06:50:51 +01:00
11 changed files with 298 additions and 8 deletions

View File

@ -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

View File

@ -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
View 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

View File

@ -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
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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())

View File

@ -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
View 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