feat(ai): on-device RAG over bookmarks + Spotlight indexing + unified logging
All checks were successful
CI / build-and-deploy (push) Successful in 22s

- SpotlightIndexer: actually push BookmarkEntity into the Spotlight index via
  indexAppEntities (IndexedEntity conformance alone indexes nothing). Wired into
  every sync/mutation point. This is what lets Apple Intelligence answer
  free-form Siri questions grounded in the user's bookmarks.
- On-device RAG: BookmarkAssistant (LanguageModelSession) + BookmarkSearchTool
  + SpotlightBookmarkSearch (CSSearchQuery retrieval), surfaced via AskView with
  lightweight Markdown rendering of answers.
- Log: os.Logger facility (com.magicive.marks) replacing ad-hoc print(); shared
  with ShareExtension. ClaudeService now logs server error bodies on non-2xx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krishna Kumar
2026-06-24 12:55:32 -05:00
parent e82e69c5f3
commit 96ea5fe6f6
14 changed files with 499 additions and 18 deletions

View File

@@ -0,0 +1,40 @@
import Foundation
import CoreSpotlight
import AppIntents
/// Pushes Linkding bookmarks into the system Spotlight index via the
/// `IndexedEntity` conformance on `BookmarkEntity`.
///
/// This is the step that actually makes bookmarks discoverable: conforming to
/// `IndexedEntity` only describes *how* an entity would be indexed (its
/// `attributeSet`) nothing reaches Spotlight until the entities are handed to
/// `CSSearchableIndex`. Once indexed, bookmarks show up in Spotlight search and
/// are reachable by Siri / Apple Intelligence.
///
/// All calls are best-effort: failures are logged, never surfaced. If indexing
/// is unavailable or fails, search degrades but nothing else breaks.
enum SpotlightIndexer {
/// Insert or update the given bookmarks in the Spotlight index.
static func index(_ bookmarks: [Bookmark]) async {
guard CSSearchableIndex.isIndexingAvailable(), !bookmarks.isEmpty else { return }
do {
try await CSSearchableIndex.default()
.indexAppEntities(bookmarks.map(BookmarkEntity.init(from:)))
Log.spotlight.info("Indexed \(bookmarks.count, privacy: .public) bookmarks")
} catch {
Log.spotlight.error("Index failed: \(error.localizedDescription, privacy: .public)")
}
}
/// Remove bookmarks from the index by id (e.g. after delete / archive).
static func remove(ids: [Int]) async {
guard CSSearchableIndex.isIndexingAvailable(), !ids.isEmpty else { return }
do {
try await CSSearchableIndex.default()
.deleteAppEntities(identifiedBy: ids, ofType: BookmarkEntity.self)
Log.spotlight.info("Removed \(ids.count, privacy: .public) bookmarks")
} catch {
Log.spotlight.error("Delete failed: \(error.localizedDescription, privacy: .public)")
}
}
}