@agent-native/core 0.114.5 → 0.114.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
  5. package/corpus/core/src/cli/mcp.ts +124 -0
  6. package/corpus/core/src/cli/skills-content/help.ts +4 -3
  7. package/corpus/core/src/cli/skills-content/index.ts +1 -0
  8. package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
  9. package/corpus/core/src/cli/skills.ts +115 -13
  10. package/corpus/core/src/client/resources/ResourceTree.tsx +11 -5
  11. package/corpus/core/src/client/resources/ResourcesPanel.tsx +141 -18
  12. package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
  13. package/corpus/core/src/mcp-client/index.ts +9 -0
  14. package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
  15. package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
  16. package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
  17. package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
  18. package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
  19. package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
  20. package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
  21. package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
  22. package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
  23. package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
  24. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
  25. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
  26. package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
  27. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
  28. package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
  29. package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +1 -2
  30. package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
  31. package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
  32. package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
  33. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
  34. package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
  35. package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
  36. package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
  37. package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
  39. package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
  42. package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
  47. package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-20-opening-zoom-meeting-launches-native-app.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
  51. package/corpus/templates/clips/desktop/package.json +1 -0
  52. package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
  53. package/corpus/templates/clips/desktop/src/app.tsx +2478 -223
  54. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
  55. package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
  56. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
  57. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
  58. package/corpus/templates/clips/desktop/src/lib/open-meeting-join-url.ts +22 -0
  59. package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
  60. package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
  61. package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
  62. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  63. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
  64. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -3
  65. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
  66. package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
  67. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
  68. package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
  69. package/corpus/templates/clips/desktop/src/styles.css +956 -134
  70. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
  71. package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
  72. package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
  73. package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
  74. package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
  75. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
  76. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
  77. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
  78. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
  79. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
  80. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
  81. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
  82. package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
  83. package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
  84. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
  86. package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
  87. package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
  88. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
  89. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
  90. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
  91. package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
  92. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
  93. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
  94. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
  95. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
  96. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
  97. package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
  98. package/corpus/templates/clips/shared/meeting-join-url.ts +0 -5
  99. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +9 -1
  100. package/corpus/templates/content/AGENTS.md +4 -1
  101. package/corpus/templates/content/actions/_content-spaces.ts +6 -1
  102. package/corpus/templates/content/actions/_database-row-batch.ts +15 -2
  103. package/corpus/templates/content/actions/_database-utils.ts +83 -15
  104. package/corpus/templates/content/actions/add-database-item.ts +3 -0
  105. package/corpus/templates/content/actions/connect-local-folder-source.ts +2 -1
  106. package/corpus/templates/content/actions/delete-content-database.ts +12 -9
  107. package/corpus/templates/content/actions/delete-database-items.ts +3 -2
  108. package/corpus/templates/content/actions/delete-document.ts +233 -6
  109. package/corpus/templates/content/actions/duplicate-database-item.ts +1 -0
  110. package/corpus/templates/content/actions/get-document.ts +4 -1
  111. package/corpus/templates/content/actions/list-content-databases.ts +1 -0
  112. package/corpus/templates/content/actions/list-documents.ts +1 -0
  113. package/corpus/templates/content/actions/list-trashed-content-databases.ts +8 -2
  114. package/corpus/templates/content/actions/list-trashed-documents.ts +50 -0
  115. package/corpus/templates/content/actions/permanently-delete-document.ts +28 -0
  116. package/corpus/templates/content/actions/pull-document.ts +15 -2
  117. package/corpus/templates/content/actions/restore-content-database.ts +48 -11
  118. package/corpus/templates/content/actions/restore-document.ts +28 -0
  119. package/corpus/templates/content/actions/search-documents.ts +2 -1
  120. package/corpus/templates/content/actions/view-screen.ts +2 -1
  121. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +149 -55
  122. package/corpus/templates/content/app/components/editor/database/GalleryView.tsx +7 -2
  123. package/corpus/templates/content/app/components/editor/database/ListView.tsx +7 -2
  124. package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +3 -0
  125. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +26 -5
  126. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +243 -163
  127. package/corpus/templates/content/app/components/sidebar/WorkspaceSourceMenu.tsx +154 -0
  128. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +11 -0
  129. package/corpus/templates/content/app/hooks/use-content-database.ts +6 -0
  130. package/corpus/templates/content/app/hooks/use-documents.ts +57 -0
  131. package/corpus/templates/content/app/i18n-data.ts +83 -2
  132. package/corpus/templates/content/app/routes/_app.local-files.tsx +8 -1
  133. package/corpus/templates/content/changelog/2026-07-19-pages-now-move-to-a-reversible-trash-and-the-organization-pi.md +6 -0
  134. package/corpus/templates/content/changelog/2026-07-20-trash-actions-now-preserve-independent-archived-pages-enforc.md +6 -0
  135. package/corpus/templates/content/changelog/2026-07-20-workspace-toggles-now-stay-independently-open-or-closed-and-.md +6 -0
  136. package/corpus/templates/content/changelog/2026-07-20-workspaces-can-be-added-from-a-blank-workspace-or-a-connecte.md +6 -0
  137. package/corpus/templates/content/parity/matrix.md +1 -1
  138. package/corpus/templates/content/parity/matrix.ts +3 -0
  139. package/corpus/templates/content/server/db/schema.ts +2 -0
  140. package/corpus/templates/content/server/lib/public-documents.ts +8 -2
  141. package/corpus/templates/content/server/plugins/db.ts +52 -0
  142. package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +2 -2
  143. package/corpus/templates/content/shared/api.ts +10 -0
  144. package/corpus/toolkit/CHANGELOG.md +6 -0
  145. package/corpus/toolkit/package.json +1 -1
  146. package/corpus/toolkit/src/ui/progress.tsx +3 -2
  147. package/dist/cli/mcp-config-writers.d.ts +2 -2
  148. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  149. package/dist/cli/mcp-config-writers.js +6 -6
  150. package/dist/cli/mcp-config-writers.js.map +1 -1
  151. package/dist/cli/mcp.d.ts +14 -0
  152. package/dist/cli/mcp.d.ts.map +1 -1
  153. package/dist/cli/mcp.js +79 -0
  154. package/dist/cli/mcp.js.map +1 -1
  155. package/dist/cli/skills-content/help.d.ts +1 -1
  156. package/dist/cli/skills-content/help.d.ts.map +1 -1
  157. package/dist/cli/skills-content/help.js +4 -3
  158. package/dist/cli/skills-content/help.js.map +1 -1
  159. package/dist/cli/skills-content/index.d.ts +1 -0
  160. package/dist/cli/skills-content/index.d.ts.map +1 -1
  161. package/dist/cli/skills-content/index.js +1 -0
  162. package/dist/cli/skills-content/index.js.map +1 -1
  163. package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
  164. package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
  165. package/dist/cli/skills-content/rewind-skill.js +53 -0
  166. package/dist/cli/skills-content/rewind-skill.js.map +1 -0
  167. package/dist/cli/skills.d.ts +6 -0
  168. package/dist/cli/skills.d.ts.map +1 -1
  169. package/dist/cli/skills.js +90 -14
  170. package/dist/cli/skills.js.map +1 -1
  171. package/dist/client/resources/ResourceTree.d.ts +3 -1
  172. package/dist/client/resources/ResourceTree.d.ts.map +1 -1
  173. package/dist/client/resources/ResourceTree.js +4 -4
  174. package/dist/client/resources/ResourceTree.js.map +1 -1
  175. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  176. package/dist/client/resources/ResourcesPanel.js +55 -6
  177. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  178. package/dist/collab/routes.d.ts +1 -1
  179. package/dist/collab/struct-routes.d.ts +1 -1
  180. package/dist/mcp/screen-memory-stdio.d.ts +365 -0
  181. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  182. package/dist/mcp/screen-memory-stdio.js +1014 -91
  183. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  184. package/dist/mcp-client/index.d.ts +1 -1
  185. package/dist/mcp-client/index.d.ts.map +1 -1
  186. package/dist/mcp-client/index.js +1 -1
  187. package/dist/mcp-client/index.js.map +1 -1
  188. package/dist/mcp-client/screen-memory-local.d.ts +90 -0
  189. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
  190. package/dist/mcp-client/screen-memory-local.js +590 -12
  191. package/dist/mcp-client/screen-memory-local.js.map +1 -1
  192. package/dist/notifications/routes.d.ts +2 -2
  193. package/dist/observability/routes.d.ts +5 -5
  194. package/dist/progress/routes.d.ts +1 -1
  195. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  196. package/dist/resources/handlers.d.ts +1 -1
  197. package/dist/secrets/routes.d.ts +9 -9
  198. package/dist/server/transcribe-voice.d.ts +1 -1
  199. package/package.json +2 -2
  200. package/src/cli/mcp-config-writers.ts +6 -4
  201. package/src/cli/mcp.ts +124 -0
  202. package/src/cli/skills-content/help.ts +4 -3
  203. package/src/cli/skills-content/index.ts +1 -0
  204. package/src/cli/skills-content/rewind-skill.ts +52 -0
  205. package/src/cli/skills.ts +115 -13
  206. package/src/client/resources/ResourceTree.tsx +11 -5
  207. package/src/client/resources/ResourcesPanel.tsx +141 -18
  208. package/src/mcp/screen-memory-stdio.ts +1360 -104
  209. package/src/mcp-client/index.ts +9 -0
  210. package/src/mcp-client/screen-memory-local.ts +847 -13
@@ -0,0 +1,360 @@
1
+ //! Local, sparse OCR for completed Screen Memory video segments.
2
+ //!
3
+ //! This module deliberately returns bounded rows for the caller to persist;
4
+ //! it never writes frame images and never logs recognized text.
5
+ // The capture worker consumes this foundation in the next integration slice.
6
+ #![allow(dead_code)]
7
+
8
+ use chrono::{DateTime, Duration, Utc};
9
+ use serde::{Deserialize, Serialize};
10
+ use std::ffi::{CStr, CString};
11
+ use std::path::Path;
12
+
13
+ pub const OCR_SCHEMA_VERSION: u32 = 1;
14
+ pub const MAX_OCR_FRAMES: usize = 30;
15
+ pub const MAX_OCR_TEXT_BYTES: usize = 8 * 1024;
16
+
17
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18
+ #[serde(rename_all = "kebab-case")]
19
+ pub enum ScreenMemoryOcrIndexState {
20
+ Pending,
21
+ Indexing,
22
+ Ready,
23
+ Failed,
24
+ Skipped,
25
+ }
26
+
27
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28
+ #[serde(rename_all = "camelCase")]
29
+ pub struct ScreenMemoryOcrIndexStatus {
30
+ pub state: ScreenMemoryOcrIndexState,
31
+ pub attempted_frames: usize,
32
+ pub completed_frames: usize,
33
+ pub started_at: Option<String>,
34
+ pub finished_at: Option<String>,
35
+ pub error: Option<String>,
36
+ }
37
+
38
+ impl ScreenMemoryOcrIndexStatus {
39
+ pub fn pending() -> Self {
40
+ Self {
41
+ state: ScreenMemoryOcrIndexState::Pending,
42
+ attempted_frames: 0,
43
+ completed_frames: 0,
44
+ started_at: None,
45
+ finished_at: None,
46
+ error: None,
47
+ }
48
+ }
49
+
50
+ pub fn indexing(started_at: impl Into<String>) -> Self {
51
+ Self {
52
+ state: ScreenMemoryOcrIndexState::Indexing,
53
+ attempted_frames: 0,
54
+ completed_frames: 0,
55
+ started_at: Some(started_at.into()),
56
+ finished_at: None,
57
+ error: None,
58
+ }
59
+ }
60
+
61
+ pub fn ready(
62
+ attempted_frames: usize,
63
+ completed_frames: usize,
64
+ started_at: impl Into<String>,
65
+ finished_at: impl Into<String>,
66
+ ) -> Self {
67
+ Self {
68
+ state: ScreenMemoryOcrIndexState::Ready,
69
+ attempted_frames,
70
+ completed_frames,
71
+ started_at: Some(started_at.into()),
72
+ finished_at: Some(finished_at.into()),
73
+ error: None,
74
+ }
75
+ }
76
+
77
+ pub fn failed(
78
+ attempted_frames: usize,
79
+ completed_frames: usize,
80
+ started_at: impl Into<String>,
81
+ finished_at: impl Into<String>,
82
+ error: impl Into<String>,
83
+ ) -> Self {
84
+ Self {
85
+ state: ScreenMemoryOcrIndexState::Failed,
86
+ attempted_frames,
87
+ completed_frames,
88
+ started_at: Some(started_at.into()),
89
+ finished_at: Some(finished_at.into()),
90
+ error: Some(error.into()),
91
+ }
92
+ }
93
+
94
+ pub fn skipped(finished_at: impl Into<String>) -> Self {
95
+ Self {
96
+ state: ScreenMemoryOcrIndexState::Skipped,
97
+ attempted_frames: 0,
98
+ completed_frames: 0,
99
+ started_at: None,
100
+ finished_at: Some(finished_at.into()),
101
+ error: None,
102
+ }
103
+ }
104
+ }
105
+
106
+ #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107
+ #[serde(rename_all = "camelCase")]
108
+ pub struct ScreenMemoryOcrRow {
109
+ pub schema_version: u32,
110
+ pub segment_id: String,
111
+ pub captured_at: String,
112
+ pub offset_ms: i64,
113
+ pub source: String,
114
+ pub ocr_text: String,
115
+ pub confidence: f32,
116
+ pub frame_width: u32,
117
+ pub frame_height: u32,
118
+ }
119
+
120
+ #[derive(Debug, Clone, Deserialize)]
121
+ #[serde(rename_all = "camelCase")]
122
+ struct NativeOcrFrame {
123
+ offset_ms: i64,
124
+ text: String,
125
+ confidence: f32,
126
+ width: u32,
127
+ height: u32,
128
+ }
129
+
130
+ #[derive(Debug, Deserialize)]
131
+ struct NativeOcrResponse {
132
+ ok: bool,
133
+ frames: Option<Vec<NativeOcrFrame>>,
134
+ error: Option<String>,
135
+ }
136
+
137
+ /// Maps Vision's actual extracted frame time onto the segment timeline.
138
+ pub fn captured_at_for_offset(segment_started_at: DateTime<Utc>, offset_ms: i64) -> String {
139
+ (segment_started_at + Duration::milliseconds(offset_ms.max(0))).to_rfc3339()
140
+ }
141
+
142
+ /// The sparse sampling contract shared with the native helper: start at the
143
+ /// segment boundary, use the configured interval, and never request more than
144
+ /// thirty video frames.
145
+ pub fn requested_frame_offsets(duration_ms: u64, sample_interval_seconds: u64) -> Vec<u64> {
146
+ let interval_ms = sample_interval_seconds.max(1).saturating_mul(1_000);
147
+ (0..MAX_OCR_FRAMES)
148
+ .map(|index| (index as u64).saturating_mul(interval_ms))
149
+ .take_while(|offset_ms| *offset_ms <= duration_ms)
150
+ .collect()
151
+ }
152
+
153
+ /// Retains a bounded, UTF-8-safe prefix without carrying frame images forward.
154
+ pub fn bounded_ocr_text(text: &str) -> String {
155
+ if text.len() <= MAX_OCR_TEXT_BYTES {
156
+ return text.to_owned();
157
+ }
158
+ let mut end = MAX_OCR_TEXT_BYTES;
159
+ while !text.is_char_boundary(end) {
160
+ end -= 1;
161
+ }
162
+ text[..end].to_owned()
163
+ }
164
+
165
+ fn rows_from_frames(
166
+ segment_id: &str,
167
+ segment_started_at: DateTime<Utc>,
168
+ frames: impl IntoIterator<Item = NativeOcrFrame>,
169
+ ) -> Vec<ScreenMemoryOcrRow> {
170
+ frames
171
+ .into_iter()
172
+ .filter(|frame| !frame.text.trim().is_empty())
173
+ .take(MAX_OCR_FRAMES)
174
+ .map(|frame| ScreenMemoryOcrRow {
175
+ schema_version: OCR_SCHEMA_VERSION,
176
+ segment_id: segment_id.to_owned(),
177
+ captured_at: captured_at_for_offset(segment_started_at, frame.offset_ms),
178
+ offset_ms: frame.offset_ms.max(0),
179
+ source: "ocr".to_owned(),
180
+ ocr_text: bounded_ocr_text(&frame.text),
181
+ confidence: frame.confidence.clamp(0.0, 1.0),
182
+ frame_width: frame.width,
183
+ frame_height: frame.height,
184
+ })
185
+ .collect()
186
+ }
187
+
188
+ /// Runs sparse, on-device OCR on macOS. Callers own persistence and should
189
+ /// transition the associated index status from `indexing` to `ready`/`failed`.
190
+ #[cfg(target_os = "macos")]
191
+ pub fn recognize_segment(
192
+ segment_path: &Path,
193
+ segment_id: &str,
194
+ segment_started_at: DateTime<Utc>,
195
+ sample_interval_seconds: u64,
196
+ ) -> Result<Vec<ScreenMemoryOcrRow>, String> {
197
+ let path = CString::new(segment_path.as_os_str().as_encoded_bytes())
198
+ .map_err(|_| "segment path contains an interior NUL byte".to_owned())?;
199
+ let response =
200
+ unsafe { clips_screen_memory_ocr_json(path.as_ptr(), sample_interval_seconds.max(1)) };
201
+ if response.is_null() {
202
+ return Err("macOS OCR helper returned no response".to_owned());
203
+ }
204
+ let response_text = unsafe {
205
+ let text = CStr::from_ptr(response).to_string_lossy().into_owned();
206
+ clips_screen_memory_ocr_free(response);
207
+ text
208
+ };
209
+ let response: NativeOcrResponse = serde_json::from_str(&response_text)
210
+ .map_err(|_| "macOS OCR helper returned an invalid response".to_owned())?;
211
+ if !response.ok {
212
+ return Err(response
213
+ .error
214
+ .unwrap_or_else(|| "macOS OCR failed".to_owned()));
215
+ }
216
+ Ok(rows_from_frames(
217
+ segment_id,
218
+ segment_started_at,
219
+ response.frames.unwrap_or_default(),
220
+ ))
221
+ }
222
+
223
+ /// Platforms without AVFoundation/Vision do not silently provide a different
224
+ /// OCR path. Capture remains usable, while callers can mark OCR as skipped.
225
+ #[cfg(not(target_os = "macos"))]
226
+ pub fn recognize_segment(
227
+ _segment_path: &Path,
228
+ _segment_id: &str,
229
+ _segment_started_at: DateTime<Utc>,
230
+ _sample_interval_seconds: u64,
231
+ ) -> Result<Vec<ScreenMemoryOcrRow>, String> {
232
+ Err("local Screen Memory OCR is supported on macOS only".to_owned())
233
+ }
234
+
235
+ #[cfg(target_os = "macos")]
236
+ unsafe extern "C" {
237
+ fn clips_screen_memory_ocr_json(
238
+ video_path: *const std::ffi::c_char,
239
+ sample_interval_seconds: u64,
240
+ ) -> *mut std::ffi::c_char;
241
+ fn clips_screen_memory_ocr_free(response: *mut std::ffi::c_char);
242
+ }
243
+
244
+ #[cfg(test)]
245
+ mod tests {
246
+ use super::*;
247
+
248
+ fn frame(offset_ms: i64, text: impl Into<String>) -> NativeOcrFrame {
249
+ NativeOcrFrame {
250
+ offset_ms,
251
+ text: text.into(),
252
+ confidence: 1.2,
253
+ width: 1920,
254
+ height: 1080,
255
+ }
256
+ }
257
+
258
+ #[test]
259
+ fn maps_actual_frame_time_to_segment_timestamp() {
260
+ let started_at = "2026-07-14T12:00:00Z".parse::<DateTime<Utc>>().unwrap();
261
+ assert_eq!(
262
+ captured_at_for_offset(started_at, 1_250),
263
+ "2026-07-14T12:00:01.250+00:00"
264
+ );
265
+ assert_eq!(
266
+ captured_at_for_offset(started_at, -1),
267
+ "2026-07-14T12:00:00+00:00"
268
+ );
269
+ }
270
+
271
+ #[test]
272
+ fn caps_rows_and_bounds_text_without_splitting_unicode() {
273
+ let started_at = "2026-07-14T12:00:00Z".parse::<DateTime<Utc>>().unwrap();
274
+ let oversized = "é".repeat(MAX_OCR_TEXT_BYTES);
275
+ let rows = rows_from_frames(
276
+ "segment-1",
277
+ started_at,
278
+ (0..35).map(|index| {
279
+ frame(
280
+ index * 1_000,
281
+ if index == 0 {
282
+ oversized.clone()
283
+ } else {
284
+ "text".to_owned()
285
+ },
286
+ )
287
+ }),
288
+ );
289
+ assert_eq!(rows.len(), MAX_OCR_FRAMES);
290
+ assert!(rows[0].ocr_text.len() <= MAX_OCR_TEXT_BYTES);
291
+ assert!(rows[0].ocr_text.is_char_boundary(rows[0].ocr_text.len()));
292
+ assert_eq!(rows[0].source, "ocr");
293
+ assert_eq!(rows[0].confidence, 1.0);
294
+ }
295
+
296
+ #[test]
297
+ fn samples_the_requested_interval_with_a_thirty_frame_cap() {
298
+ assert_eq!(
299
+ requested_frame_offsets(5 * 60 * 1_000, 10),
300
+ (0..MAX_OCR_FRAMES)
301
+ .map(|index| index as u64 * 10_000)
302
+ .collect::<Vec<_>>()
303
+ );
304
+ assert_eq!(requested_frame_offsets(2_500, 0), vec![0, 1_000, 2_000]);
305
+ }
306
+
307
+ #[test]
308
+ fn status_shape_serializes_stably() {
309
+ assert_eq!(
310
+ serde_json::to_value(ScreenMemoryOcrIndexStatus::pending()).unwrap(),
311
+ serde_json::json!({
312
+ "state": "pending", "attemptedFrames": 0, "completedFrames": 0,
313
+ "startedAt": null, "finishedAt": null, "error": null
314
+ })
315
+ );
316
+ assert_eq!(
317
+ serde_json::to_value(ScreenMemoryOcrIndexStatus::ready(
318
+ 4,
319
+ 3,
320
+ "2026-07-14T12:00:00Z",
321
+ "2026-07-14T12:00:01Z",
322
+ ))
323
+ .unwrap(),
324
+ serde_json::json!({
325
+ "state": "ready", "attemptedFrames": 4, "completedFrames": 3,
326
+ "startedAt": "2026-07-14T12:00:00Z", "finishedAt": "2026-07-14T12:00:01Z", "error": null
327
+ })
328
+ );
329
+ assert_eq!(
330
+ serde_json::to_value(ScreenMemoryOcrIndexStatus::indexing("2026-07-14T12:00:00Z"))
331
+ .unwrap(),
332
+ serde_json::json!({
333
+ "state": "indexing", "attemptedFrames": 0, "completedFrames": 0,
334
+ "startedAt": "2026-07-14T12:00:00Z", "finishedAt": null, "error": null
335
+ })
336
+ );
337
+ assert_eq!(
338
+ serde_json::to_value(ScreenMemoryOcrIndexStatus::failed(
339
+ 4,
340
+ 1,
341
+ "2026-07-14T12:00:00Z",
342
+ "2026-07-14T12:00:01Z",
343
+ "unavailable",
344
+ ))
345
+ .unwrap(),
346
+ serde_json::json!({
347
+ "state": "failed", "attemptedFrames": 4, "completedFrames": 1,
348
+ "startedAt": "2026-07-14T12:00:00Z", "finishedAt": "2026-07-14T12:00:01Z", "error": "unavailable"
349
+ })
350
+ );
351
+ assert_eq!(
352
+ serde_json::to_value(ScreenMemoryOcrIndexStatus::skipped("2026-07-14T12:00:01Z"))
353
+ .unwrap(),
354
+ serde_json::json!({
355
+ "state": "skipped", "attemptedFrames": 0, "completedFrames": 0,
356
+ "startedAt": null, "finishedAt": "2026-07-14T12:00:01Z", "error": null
357
+ })
358
+ );
359
+ }
360
+ }