Skip to content

feat: add Notes and Reminders CLI commands#212

Merged
timlaing merged 14 commits intotimlaing:mainfrom
MrJarnould:codex/notes-reminders-cli
Apr 3, 2026
Merged

feat: add Notes and Reminders CLI commands#212
timlaing merged 14 commits intotimlaing:mainfrom
MrJarnould:codex/notes-reminders-cli

Conversation

@MrJarnould
Copy link
Copy Markdown

Breaking change

Proposed change

This PR adds first-class CLI support for the existing Notes and Reminders services.

For Notes, it adds commands to inspect and export data, including:

  • notes recent
  • notes folders
  • notes list
  • notes search
  • notes get
  • notes render
  • notes export
  • notes changes
  • notes sync-cursor

For Reminders, it adds commands for both read and write flows, including:

  • reminders lists
  • reminders list
  • reminders get
  • reminders create
  • reminders update
  • reminders set-status
  • reminders delete
  • reminders snapshot
  • reminders changes
  • reminders sync-cursor

It also adds subcommand groups for reminder alarms, hashtags, attachments, and recurrence rules.

While smoke-testing the new CLI against a live iCloud session, I fixed a few service-level issues that affected the new commands:

  • Reminders hashtag names now round-trip correctly after create/update
  • notes list --all --since <current-cursor> now short-circuits instead of scanning changes when the cursor is already current
  • reminders sync-cursor now uses a cheap current-token path before falling back to the full changes feed
  • reminder list counts now fall back to membership data when CloudKit reports Count=0
  • the Notes folder query path no longer depends on invalid desired-key enum members

This PR also adds normalization/output helpers and targeted tests for the new command groups and the service fixes they rely on.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New service (thank you!)
  • New feature (which adds functionality to an existing service)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests
  • Documentation or code sample

Example of code:

icloud notes recent --limit 5
icloud notes get NOTE_ID --format json
icloud reminders lists --format json
icloud reminders create --list-id LIST_ID --title "Buy milk"

Additional information

  • This PR fixes or closes issue: N/A
  • This PR is related to issue: N/A
  • Local validation included:
    • pytest tests/test_cmdline.py tests/test_output.py tests/test_notes.py tests/services/test_reminders_cloudkit.py -q
    • ruff check on the touched CLI and service files
    • a full live smoke run covering the Notes and Reminders command matrix end to end (102 passes, 1 intentional skip for image-only attachment metadata creation)

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

  • Documentation added/updated to README

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Warning

Rate limit exceeded

@timlaing has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 28 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 14 minutes and 28 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a402fb9c-a694-42af-b792-8eecfc124517

📥 Commits

Reviewing files that changed from the base of the PR and between 3dae369 and da067bf.

📒 Files selected for processing (5)
  • README.md
  • pyicloud/cli/app.py
  • pyicloud/cli/commands/reminders.py
  • pyicloud/cli/normalize.py
  • tests/test_cmdline.py
📝 Walkthrough

Walkthrough

This pull request adds comprehensive CLI support for Apple iCloud Notes and Reminders services. It introduces Typer-based command modules for Notes (recent, folders, list, search, get, render, export, changes, sync-cursor) and Reminders (core commands plus subgroups for alarms, hashtags, attachments, recurrence), alongside service enhancements for sync token caching, CloudKit text encoding/decoding, and folder metadata tri-state handling. Extensive tests validate CLI and service behaviour.

Changes

Cohort / File(s) Summary
CLI Root & Documentation
README.md, pyicloud/cli/app.py
Extended root CLI with --version option and registered notes and reminders subcommand groups; documented both command hierarchies in README.
Notes CLI Commands
pyicloud/cli/commands/notes.py
Implemented 9 Notes subcommands (recent, folders, list, search, get, render, export, changes, sync-cursor) with filtering, output formatting, and error handling.
Reminders CLI Commands
pyicloud/cli/commands/reminders.py
Implemented 20+ Reminders subcommands including core operations (list, get, create, update, set-status, delete, snapshot) and nested subgroups for alarms, hashtags, attachments, and recurrence rules.
Notes Service Enhancements
pyicloud/services/notes/service.py, pyicloud/cli/normalize.py
Added sync token early-exit optimisation, tri-state has_subfolders folder metadata, and helper functions for selecting/searching recent notes with deletion filtering.
Reminders Service Enhancements
pyicloud/services/reminders/_protocol.py, _mappers.py, _reads.py, _writes.py, client.py, service.py
Introduced CloudKit text encoding/decoding helpers, sync token query method, updated hashtag creation/update to use encrypted text fields, and enabled sync token caching.
Output Formatting
pyicloud/cli/output.py
Added format_color_value helper to convert reminder colour representations into human-friendly compact strings.
Tests
tests/test_cmdline.py, tests/test_notes.py, tests/test_output.py, tests/services/test_reminders_cloudkit.py
Expanded CLI and service test coverage with fake service fixtures, command validation, error handling assertions, and sync token behaviour verification.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐰 Notes and reminders hop into the CLI,
Sync tokens cached, no fetches to retry,
Colours encoded, hashtags encrypted tight,
Export those notes—export them right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding first-class CLI support for Notes and Reminders services.
Description check ✅ Passed The description is directly related to the changeset, detailing the new Notes and Reminders CLI commands, service-level fixes, helpers, and testing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
pyicloud/cli/normalize.py (1)

235-237: Cap the recents search window to avoid oversized queries.

window = max(500, limit * 50) grows linearly with user input; very large --limit values can trigger heavy remote queries and high memory use. Add an upper bound.

Proposed refactor
-    window = max(500, limit * 50)
+    window = min(max(500, limit * 50), 5_000)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyicloud/cli/normalize.py` around lines 235 - 237, The recents search window
currently uses window = max(500, limit * 50) which can grow unbounded; change it
to cap the value with an upper bound (e.g., window = min(MAX_WINDOW, max(500,
limit * 50))) and define a sensible constant MAX_WINDOW (for example 5000 or
10000) near the top of the module or function; ensure
notes_service.recents(limit=window) uses this capped window to prevent oversized
remote queries and memory spikes.
tests/test_cmdline.py (1)

286-300: Intentional duplicate for deduplication testing is clear from test usage.

Line 299 includes self.recent_rows[2] (Note/MEETING) twice in all_rows. This is intentional to verify the search deduplication logic in test_notes_search_uses_recents_first_and_fallback. Consider adding a brief inline comment to make this intention explicit for future maintainers.

📝 Optional: Add clarifying comment
         self.all_rows = [
             self.recent_rows[2],
             NoteSummary(
                 id="Note/FOLLOWUP",
                 title="Meeting Follow-up",
                 snippet="Send recap",
                 modified_at=datetime(2026, 3, 2, tzinfo=timezone.utc),
                 folder_id="Folder/WORK",
                 folder_name="Work",
                 is_deleted=False,
                 is_locked=False,
             ),
             self.recent_rows[1],
+            # Duplicate entry to verify deduplication in search_notes_by_title
             self.recent_rows[2],
         ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_cmdline.py` around lines 286 - 300, Add a brief inline comment
next to the duplicate entry in self.all_rows clarifying that the repeated
self.recent_rows[2] (Note/MEETING) is intentional for deduplication testing;
update the list where self.all_rows is constructed to comment on the deliberate
duplicate used by test_notes_search_uses_recents_first_and_fallback so future
maintainers understand the purpose.
pyicloud/cli/commands/reminders.py (1)

131-139: Catch RemindersAuthError and RemindersApiError exceptions.

The reminders service raises RemindersAuthError and RemindersApiError (from pyicloud/services/reminders/client.py), but _reminders_call only catches LookupError and ValidationError. These exceptions should be caught and converted to CLIAbort to match the wrapper's intended purpose of providing reminder-specific user-facing errors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyicloud/cli/commands/reminders.py` around lines 131 - 139, The
_reminders_call wrapper currently only handles LookupError and ValidationError
but must also catch RemindersAuthError and RemindersApiError raised by the
reminders service; update the exception handling in _reminders_call (function
name: _reminders_call) to import and catch RemindersAuthError and
RemindersApiError (from pyicloud.services.reminders.client) and convert them to
CLIAbort the same way as the other exceptions (raise CLIAbort(str(err)) from
err) while preserving the existing catches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 1322: Update the README line that currently reads "_List recent notes,
folders, or one folder’s notes:_" to the clearer wording "_List recent notes,
folders, or notes in one folder:_"; locate the exact string in the README (the
line starting with "List recent notes, folders, or") and replace the possessive
phrasing "one folder’s notes" with "notes in one folder" to improve readability.

---

Nitpick comments:
In `@pyicloud/cli/commands/reminders.py`:
- Around line 131-139: The _reminders_call wrapper currently only handles
LookupError and ValidationError but must also catch RemindersAuthError and
RemindersApiError raised by the reminders service; update the exception handling
in _reminders_call (function name: _reminders_call) to import and catch
RemindersAuthError and RemindersApiError (from
pyicloud.services.reminders.client) and convert them to CLIAbort the same way as
the other exceptions (raise CLIAbort(str(err)) from err) while preserving the
existing catches.

In `@pyicloud/cli/normalize.py`:
- Around line 235-237: The recents search window currently uses window =
max(500, limit * 50) which can grow unbounded; change it to cap the value with
an upper bound (e.g., window = min(MAX_WINDOW, max(500, limit * 50))) and define
a sensible constant MAX_WINDOW (for example 5000 or 10000) near the top of the
module or function; ensure notes_service.recents(limit=window) uses this capped
window to prevent oversized remote queries and memory spikes.

In `@tests/test_cmdline.py`:
- Around line 286-300: Add a brief inline comment next to the duplicate entry in
self.all_rows clarifying that the repeated self.recent_rows[2] (Note/MEETING) is
intentional for deduplication testing; update the list where self.all_rows is
constructed to comment on the deliberate duplicate used by
test_notes_search_uses_recents_first_and_fallback so future maintainers
understand the purpose.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f051ecd1-c3ff-4756-827e-ab0d6be63eba

📥 Commits

Reviewing files that changed from the base of the PR and between 404b0d7 and 0c2e9cf.

📒 Files selected for processing (17)
  • README.md
  • pyicloud/cli/app.py
  • pyicloud/cli/commands/notes.py
  • pyicloud/cli/commands/reminders.py
  • pyicloud/cli/normalize.py
  • pyicloud/cli/output.py
  • pyicloud/services/notes/service.py
  • pyicloud/services/reminders/_mappers.py
  • pyicloud/services/reminders/_protocol.py
  • pyicloud/services/reminders/_reads.py
  • pyicloud/services/reminders/_writes.py
  • pyicloud/services/reminders/client.py
  • pyicloud/services/reminders/service.py
  • tests/services/test_reminders_cloudkit.py
  • tests/test_cmdline.py
  • tests/test_notes.py
  • tests/test_output.py

Comment thread README.md
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
README.md (1)

1322-1322: ⚠️ Potential issue | 🟡 Minor

Use clearer phrasing: “notes in one folder”.

“one folder’s notes” reads awkwardly here; “notes in one folder” is clearer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 1322, Replace the phrase "one folder’s notes" with the
clearer wording "notes in one folder" in the README heading/text that currently
reads "_List recent notes, folders, or one folder’s notes:_", so it becomes
"_List recent notes, folders, or notes in one folder:_; update any identical
occurrences of "one folder’s notes" elsewhere in the file to maintain consistent
phrasing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 1319-1320: The top-level subcommands list in the README is missing
the new namespaces; update that earlier "top-level subcommands" section to
include the `notes` and `reminders` entries so it matches the later
documentation (which describes `icloud notes ...`); locate the list of top-level
commands and add `notes` and `reminders` alongside the other subcommands to keep
the README consistent and avoid confusing first-time users.

---

Duplicate comments:
In `@README.md`:
- Line 1322: Replace the phrase "one folder’s notes" with the clearer wording
"notes in one folder" in the README heading/text that currently reads "_List
recent notes, folders, or one folder’s notes:_", so it becomes "_List recent
notes, folders, or notes in one folder:_; update any identical occurrences of
"one folder’s notes" elsewhere in the file to maintain consistent phrasing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 153c2061-a33b-48b6-a0cf-9bb8f9502c48

📥 Commits

Reviewing files that changed from the base of the PR and between 0c2e9cf and 3dae369.

📒 Files selected for processing (1)
  • README.md

Comment thread README.md
@timlaing timlaing self-assigned this Apr 3, 2026
@timlaing timlaing added the enhancement New feature or request label Apr 3, 2026
timlaing and others added 8 commits April 3, 2026 17:45
Signed-off-by: Tim Laing <11019084+timlaing@users.noreply.github.com>
Co-authored-by: Tim Laing <11019084+timlaing@users.noreply.github.com>
* feat: handle Apple's HSA2 trusted-device prompts

* Trim unrelated Notes PR scope

* Address CodeRabbit review comments

* Add docstrings for auth bridge PR scope

* Harden bridge prover and persistence tests
…#208)

Updates the requirements on [protobuf](https://github.com/protocolbuffers/protobuf) to permit the latest version.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-version: 7.34.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.34.1 to 4.35.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@3869755...c10b806)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: Tim Laing <11019084+timlaing@users.noreply.github.com>
@timlaing timlaing merged commit 0cf2d65 into timlaing:main Apr 3, 2026
11 of 12 checks passed
@timlaing
Copy link
Copy Markdown
Owner

timlaing commented Apr 3, 2026

@MrJarnould - thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants