124 lines
4.3 KiB
Markdown
124 lines
4.3 KiB
Markdown
|
|
# iNotes (`INoteSync`)
|
||
|
|
|
||
|
|
## Purpose
|
||
|
|
|
||
|
|
Sync Notes.app notes with the server, and apply web-initiated note edits back
|
||
|
|
via AppleScript.
|
||
|
|
|
||
|
|
## Direction
|
||
|
|
|
||
|
|
Bidirectional via AppleScript — accepted fragility per
|
||
|
|
`~/.claude/plans/magical-tumbling-peach.md:108-117`.
|
||
|
|
|
||
|
|
## OS surface
|
||
|
|
|
||
|
|
Notes.app via AppleScript (`NSAppleScript`). No linker entry needed; the
|
||
|
|
target has no `linkerSettings` (`Package.swift:90-94`). Requires
|
||
|
|
Automation > Notes.
|
||
|
|
|
||
|
|
## Files
|
||
|
|
|
||
|
|
- `Reader.swift` — `public final class NotesReader` (line 49):
|
||
|
|
`requestAuthorization()` (line 59), `fetchAllNotes()` (line 70). The fetch
|
||
|
|
script reads `id, name, body, folder, modification date` from every note in
|
||
|
|
every account
|
||
|
|
(`@packages/inotes/Sources/INoteSync/Reader.swift:78-95`).
|
||
|
|
- `APIClient.swift`:
|
||
|
|
- `syncNotes(_:)` -> `POST /client/inotes/sync`
|
||
|
|
(`@packages/inotes/Sources/INoteSync/APIClient.swift:79`)
|
||
|
|
- `getStats()` -> `GET /client/inotes/stats` (line 94)
|
||
|
|
- `getPendingSends()` -> `GET /client/inotes/send-queue/pending` (line 109)
|
||
|
|
- `reportSendResult(...)` -> `POST /client/inotes/send-queue/:id/result` (line 132)
|
||
|
|
- `Sender.swift`:
|
||
|
|
- `NoteApplying` protocol (line 65) + production
|
||
|
|
`AppleScriptNoteApplier` (line 110) and a `NoteSender`
|
||
|
|
facade wrapper (line 76).
|
||
|
|
- Scripts assembled by `scriptForCreate`, `scriptForUpdate`,
|
||
|
|
`scriptForDelete` (lines 149, 163, 181) using
|
||
|
|
`AppleScriptEscape.quote` for all interpolation.
|
||
|
|
- `NoteSendTransport` adapts the typed APIClient calls into
|
||
|
|
`SendQueueTransport` (line 231).
|
||
|
|
- `SyncManager.swift` — slow interval (600s) because AppleScript is
|
||
|
|
expensive; `lazy var sendQueueClient: SendQueueClient<NoteSendTransport>`
|
||
|
|
drains every 60s (`SyncManager.swift:54-72`). Batch size 100 (line 51).
|
||
|
|
|
||
|
|
## Timing
|
||
|
|
|
||
|
|
- Read interval: **600s**
|
||
|
|
(`@packages/inotes/Sources/INoteSync/SyncManager.swift:79`).
|
||
|
|
- Outbound poll interval: **60s**
|
||
|
|
(`@packages/inotes/Sources/INoteSync/SyncManager.swift:60`).
|
||
|
|
- Note batch size: 100.
|
||
|
|
|
||
|
|
## Server surface
|
||
|
|
|
||
|
|
- Entity tables: `icloud.notes`, `icloud.note_send_queue`
|
||
|
|
(`src/server/src/app/server.ts:47-48`).
|
||
|
|
- Allowed actions: `create_note`, `update_note`, `delete_note`
|
||
|
|
(`src/server/src/entities/noteSendItem/types.ts:12`).
|
||
|
|
- Client routes (`src/server/src/surfaces/client/inotes.ts`):
|
||
|
|
- `POST /client/inotes/sync` (line 29)
|
||
|
|
- `GET /client/inotes/stats` (line 35)
|
||
|
|
- `GET /client/inotes/send-queue/pending` (line 39)
|
||
|
|
- `POST /client/inotes/send-queue/:id/result` (line 57)
|
||
|
|
- Web routes (`src/server/src/surfaces/my/notes.ts`):
|
||
|
|
- `GET /my/notes/stats` (line 41)
|
||
|
|
- `GET /my/notes/` (line 49)
|
||
|
|
- `POST /my/notes/` (line 66)
|
||
|
|
- `PUT /my/notes/:id` (line 76)
|
||
|
|
- `DELETE /my/notes/:id` (line 88)
|
||
|
|
- Admin enqueue: `POST /admin/note-send-queue/enqueue`
|
||
|
|
(`src/server/src/surfaces/admin/note-send-queue.ts:13`).
|
||
|
|
|
||
|
|
## Web surface
|
||
|
|
|
||
|
|
- Tab: `/notes` (`web/src/App.tsx:62`).
|
||
|
|
- API helpers: `web/src/api/notes.ts`.
|
||
|
|
|
||
|
|
## AppleScript snippets
|
||
|
|
|
||
|
|
Create (`@packages/inotes/Sources/INoteSync/Sender.swift:149-160`):
|
||
|
|
|
||
|
|
```applescript
|
||
|
|
tell application "Notes"
|
||
|
|
set targetFolder to first folder whose name is "<folder>"
|
||
|
|
set newNote to make new note at targetFolder with properties ¬
|
||
|
|
{name:"<name>", body:"<html body>"}
|
||
|
|
return id of newNote
|
||
|
|
end tell
|
||
|
|
```
|
||
|
|
|
||
|
|
Delete (`Sender.swift:181-189`):
|
||
|
|
|
||
|
|
```applescript
|
||
|
|
tell application "Notes"
|
||
|
|
delete (every note whose id is "<id>")
|
||
|
|
end tell
|
||
|
|
```
|
||
|
|
|
||
|
|
All interpolated values pass through
|
||
|
|
`AppleScriptEscape.quote(_)`
|
||
|
|
(`@packages/shared/Sources/MacSyncShared/Util/AppleScriptEscape.swift:13`).
|
||
|
|
|
||
|
|
## Known limitations
|
||
|
|
|
||
|
|
- Attachments and Apple Pencil drawings don't round-trip — title, body,
|
||
|
|
folder, and modification date only. See
|
||
|
|
[known-limitations](../known-limitations.md#applescript-fragility).
|
||
|
|
- Formatting fidelity is partial; basic HTML survives, advanced styles can
|
||
|
|
flatten.
|
||
|
|
- Pinned / locked notes: the AppleScript surface does not expose locking
|
||
|
|
state; locked notes may be unreadable to the reader.
|
||
|
|
|
||
|
|
## Tests (`@packages/inotes/Tests/INoteSyncTests/`)
|
||
|
|
|
||
|
|
- `AppleScriptTests.swift` — exercises `Reader.parse(_:)` against canned
|
||
|
|
AppleScript output (`@packages/inotes/Sources/INoteSync/Reader.swift:104`),
|
||
|
|
and validates the script strings emitted by `scriptForCreate` /
|
||
|
|
`scriptForUpdate` / `scriptForDelete`.
|
||
|
|
- `SenderDispatchTests.swift` — `NoteSender` action routing against a fake
|
||
|
|
`NoteApplying` (hermetic).
|
||
|
|
|
||
|
|
Not covered: real AppleScript execution, Notes.app side effects, Automation
|
||
|
|
prompts.
|