@hraness/slopcamera 3.2.4

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 (441) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +60 -0
  3. package/PRIVACY.md +46 -0
  4. package/README.md +311 -0
  5. package/SECURITY.md +21 -0
  6. package/apps/desktop/README.md +966 -0
  7. package/apps/desktop/analysis/protocol.ts +223 -0
  8. package/apps/desktop/application/capability-binding.ts +594 -0
  9. package/apps/desktop/application/context.ts +97 -0
  10. package/apps/desktop/application/creative-iteration.ts +1385 -0
  11. package/apps/desktop/application/default-registry.ts +146 -0
  12. package/apps/desktop/application/directing-contract.ts +218 -0
  13. package/apps/desktop/application/directing-plan.ts +178 -0
  14. package/apps/desktop/application/errors.ts +18 -0
  15. package/apps/desktop/application/gateway-port.ts +867 -0
  16. package/apps/desktop/application/html-overlay-browser-runtime.ts +687 -0
  17. package/apps/desktop/application/html-overlay-integrity.ts +337 -0
  18. package/apps/desktop/application/html-overlay-renderer.ts +45 -0
  19. package/apps/desktop/application/index.ts +13 -0
  20. package/apps/desktop/application/operation-completion-checkpoint.ts +333 -0
  21. package/apps/desktop/application/operation-effects.ts +94 -0
  22. package/apps/desktop/application/operation.ts +224 -0
  23. package/apps/desktop/application/operations/analysis/faces.ts +244 -0
  24. package/apps/desktop/application/operations/analysis/music.ts +248 -0
  25. package/apps/desktop/application/operations/analysis/project-auto-zooms.ts +302 -0
  26. package/apps/desktop/application/operations/analysis/project-inactivity.ts +381 -0
  27. package/apps/desktop/application/operations/analysis/scenes.ts +272 -0
  28. package/apps/desktop/application/operations/analysis/shared.ts +202 -0
  29. package/apps/desktop/application/operations/derive/edit-batch.ts +967 -0
  30. package/apps/desktop/application/operations/derive/follow-faces.ts +454 -0
  31. package/apps/desktop/application/operations/edit/bind-candidate-revision.ts +91 -0
  32. package/apps/desktop/application/operations/edit/create-candidate-revision.ts +377 -0
  33. package/apps/desktop/application/operations/edit/create-revision.ts +255 -0
  34. package/apps/desktop/application/operations/edit/freeze-revision.ts +182 -0
  35. package/apps/desktop/application/operations/gateway/image.ts +41 -0
  36. package/apps/desktop/application/operations/gateway/index.ts +17 -0
  37. package/apps/desktop/application/operations/gateway/shared.ts +122 -0
  38. package/apps/desktop/application/operations/gateway/speech.ts +41 -0
  39. package/apps/desktop/application/operations/gateway/transcription.ts +42 -0
  40. package/apps/desktop/application/operations/gateway/video.ts +41 -0
  41. package/apps/desktop/application/operations/index.ts +31 -0
  42. package/apps/desktop/application/operations/iteration/create-candidate.ts +240 -0
  43. package/apps/desktop/application/operations/iteration/create-matrix.ts +117 -0
  44. package/apps/desktop/application/operations/iteration/select.ts +155 -0
  45. package/apps/desktop/application/operations/iteration/shared.ts +250 -0
  46. package/apps/desktop/application/operations/media/audio-effects.ts +376 -0
  47. package/apps/desktop/application/operations/media/binding.ts +79 -0
  48. package/apps/desktop/application/operations/media/capabilities.ts +112 -0
  49. package/apps/desktop/application/operations/media/color-grade.ts +343 -0
  50. package/apps/desktop/application/operations/media/html-overlay.ts +802 -0
  51. package/apps/desktop/application/operations/media/index.ts +8 -0
  52. package/apps/desktop/application/operations/media/ingest.ts +303 -0
  53. package/apps/desktop/application/operations/media/overlay.ts +729 -0
  54. package/apps/desktop/application/operations/media/shared.ts +845 -0
  55. package/apps/desktop/application/operations/media/transform-platform.ts +120 -0
  56. package/apps/desktop/application/operations/project/commit-edits.ts +2054 -0
  57. package/apps/desktop/application/operations/project/promote-selection.ts +495 -0
  58. package/apps/desktop/application/operations/project/snapshot.ts +95 -0
  59. package/apps/desktop/application/operations/recording/index.ts +5 -0
  60. package/apps/desktop/application/operations/recording/pause.ts +34 -0
  61. package/apps/desktop/application/operations/recording/resume.ts +34 -0
  62. package/apps/desktop/application/operations/recording/shared.ts +199 -0
  63. package/apps/desktop/application/operations/recording/start.ts +169 -0
  64. package/apps/desktop/application/operations/recording/stop.ts +34 -0
  65. package/apps/desktop/application/operations/render/bind-candidate-output.ts +324 -0
  66. package/apps/desktop/application/operations/render/materialize-selection.ts +322 -0
  67. package/apps/desktop/application/operations/render/project-caption-plan.ts +287 -0
  68. package/apps/desktop/application/operations/render/project-plan.ts +939 -0
  69. package/apps/desktop/application/operations/render/project-spatial-receipt.ts +70 -0
  70. package/apps/desktop/application/operations/render/project.ts +1922 -0
  71. package/apps/desktop/application/operations/shared.ts +67 -0
  72. package/apps/desktop/application/operations/slopcamera-portable.ts +1188 -0
  73. package/apps/desktop/application/operations/slopcamera-visuals.ts +784 -0
  74. package/apps/desktop/application/operations/spatial-project.ts +209 -0
  75. package/apps/desktop/application/operations/spatial-render.ts +415 -0
  76. package/apps/desktop/application/operations/spatial-scene.ts +97 -0
  77. package/apps/desktop/application/operations/studio.ts +143 -0
  78. package/apps/desktop/application/output-publication-lease.ts +163 -0
  79. package/apps/desktop/application/project-publication-lease.ts +233 -0
  80. package/apps/desktop/application/project-store.ts +196 -0
  81. package/apps/desktop/application/receipts.ts +533 -0
  82. package/apps/desktop/application/registry.ts +182 -0
  83. package/apps/desktop/application/spatial-assets.ts +675 -0
  84. package/apps/desktop/application/spatial-compositor-cadence.ts +23 -0
  85. package/apps/desktop/application/spatial-project-authority.ts +35 -0
  86. package/apps/desktop/application/spatial-project-lease.ts +126 -0
  87. package/apps/desktop/application/spatial-project-render-preparation.ts +225 -0
  88. package/apps/desktop/application/spatial-project-store.ts +286 -0
  89. package/apps/desktop/application/spatial-render-projection.ts +201 -0
  90. package/apps/desktop/application/spatial-render.ts +677 -0
  91. package/apps/desktop/application/spatial-spz.ts +44 -0
  92. package/apps/desktop/application/spatial-world-import.ts +135 -0
  93. package/apps/desktop/application/spatial-world-provenance.ts +46 -0
  94. package/apps/desktop/application/studio-port.ts +58 -0
  95. package/apps/desktop/application/verified-receipt-reconciliation.ts +1574 -0
  96. package/apps/desktop/capture/protocol.ts +689 -0
  97. package/apps/desktop/cli/analyzer.ts +406 -0
  98. package/apps/desktop/cli/args.ts +3181 -0
  99. package/apps/desktop/cli/asset-ingest.ts +747 -0
  100. package/apps/desktop/cli/atomic-render-effects.ts +210 -0
  101. package/apps/desktop/cli/atomic-render-platform.ts +205 -0
  102. package/apps/desktop/cli/atomic-render.ts +50 -0
  103. package/apps/desktop/cli/audio-analysis.ts +252 -0
  104. package/apps/desktop/cli/bounded-map.ts +39 -0
  105. package/apps/desktop/cli/bundle-service.ts +256 -0
  106. package/apps/desktop/cli/capabilities.ts +212 -0
  107. package/apps/desktop/cli/capture-bundle.ts +1117 -0
  108. package/apps/desktop/cli/command-host-resources.ts +251 -0
  109. package/apps/desktop/cli/commands.ts +6836 -0
  110. package/apps/desktop/cli/directing-blob.ts +291 -0
  111. package/apps/desktop/cli/directing-media.ts +517 -0
  112. package/apps/desktop/cli/directing-quote.ts +89 -0
  113. package/apps/desktop/cli/directing-service.ts +288 -0
  114. package/apps/desktop/cli/emoji-assets.ts +538 -0
  115. package/apps/desktop/cli/errors.ts +29 -0
  116. package/apps/desktop/cli/face-analysis-service.ts +940 -0
  117. package/apps/desktop/cli/filter-script.ts +76 -0
  118. package/apps/desktop/cli/gateway-application-port.ts +1142 -0
  119. package/apps/desktop/cli/gateway-credential.ts +81 -0
  120. package/apps/desktop/cli/gateway-media-artifacts.ts +723 -0
  121. package/apps/desktop/cli/gateway-media-catalog.ts +922 -0
  122. package/apps/desktop/cli/gateway-media-service.ts +3106 -0
  123. package/apps/desktop/cli/gateway-media-signature.ts +122 -0
  124. package/apps/desktop/cli/gateway-provider-options.ts +562 -0
  125. package/apps/desktop/cli/gateway-scene-provider.ts +391 -0
  126. package/apps/desktop/cli/help.ts +501 -0
  127. package/apps/desktop/cli/html-overlay-gpu.ts +97 -0
  128. package/apps/desktop/cli/html-overlay-renderer.ts +2478 -0
  129. package/apps/desktop/cli/io.ts +290 -0
  130. package/apps/desktop/cli/main.ts +123 -0
  131. package/apps/desktop/cli/media-effects-service.ts +885 -0
  132. package/apps/desktop/cli/media-ingest-model.ts +364 -0
  133. package/apps/desktop/cli/media-ingest-platform.ts +227 -0
  134. package/apps/desktop/cli/media-ingest-program.ts +200 -0
  135. package/apps/desktop/cli/media-ingest.ts +30 -0
  136. package/apps/desktop/cli/music-analysis-service.ts +434 -0
  137. package/apps/desktop/cli/mutation-lock.ts +530 -0
  138. package/apps/desktop/cli/native-assets.d.ts +12 -0
  139. package/apps/desktop/cli/native-media-runtime.macos.ts +7 -0
  140. package/apps/desktop/cli/overlay-playback.ts +177 -0
  141. package/apps/desktop/cli/paths.ts +265 -0
  142. package/apps/desktop/cli/portable-surface.ts +242 -0
  143. package/apps/desktop/cli/project-camera-receipt.ts +112 -0
  144. package/apps/desktop/cli/project-face-camera.ts +394 -0
  145. package/apps/desktop/cli/project-inactivity-service.ts +552 -0
  146. package/apps/desktop/cli/project-media-integrity.ts +121 -0
  147. package/apps/desktop/cli/project-renderer.ts +1526 -0
  148. package/apps/desktop/cli/project-service.ts +519 -0
  149. package/apps/desktop/cli/project-state-transaction.ts +520 -0
  150. package/apps/desktop/cli/query-limits.ts +1 -0
  151. package/apps/desktop/cli/recording-controller.ts +1567 -0
  152. package/apps/desktop/cli/recording-daemon.ts +931 -0
  153. package/apps/desktop/cli/recording-ref.ts +56 -0
  154. package/apps/desktop/cli/renamed-environment.ts +6 -0
  155. package/apps/desktop/cli/renderer.ts +1439 -0
  156. package/apps/desktop/cli/run-cli-test-helper.ts +60 -0
  157. package/apps/desktop/cli/scene-analysis-service.ts +879 -0
  158. package/apps/desktop/cli/spatial-compositor-verifier.ts +213 -0
  159. package/apps/desktop/cli/spatial-project-service.ts +90 -0
  160. package/apps/desktop/cli/spatial-scene-service.ts +105 -0
  161. package/apps/desktop/cli/spatial-world-service.ts +39 -0
  162. package/apps/desktop/cli/speech-analysis-service.ts +682 -0
  163. package/apps/desktop/cli/studio-assemble.ts +58 -0
  164. package/apps/desktop/cli/studio-command.ts +60 -0
  165. package/apps/desktop/cli/studio-custody.ts +60 -0
  166. package/apps/desktop/cli/studio-encode.ts +216 -0
  167. package/apps/desktop/cli/studio-exr.ts +52 -0
  168. package/apps/desktop/cli/studio-files.ts +179 -0
  169. package/apps/desktop/cli/studio-output-validation.ts +162 -0
  170. package/apps/desktop/cli/studio-process.ts +145 -0
  171. package/apps/desktop/cli/studio-runtime.ts +77 -0
  172. package/apps/desktop/cli/studio-scaffold.ts +70 -0
  173. package/apps/desktop/cli/studio-service.ts +301 -0
  174. package/apps/desktop/cli/studio-spatial-asset.ts +248 -0
  175. package/apps/desktop/cli/studio-template-names.ts +3 -0
  176. package/apps/desktop/cli/studio-workflow.ts +19 -0
  177. package/apps/desktop/cli/time.ts +25 -0
  178. package/apps/desktop/cli/workflow-code.ts +584 -0
  179. package/apps/desktop/cli/workflow-runs.ts +576 -0
  180. package/apps/desktop/code/advanced.ts +139 -0
  181. package/apps/desktop/code/application-node-planner.ts +1288 -0
  182. package/apps/desktop/code/compiler.ts +329 -0
  183. package/apps/desktop/code/contracts.ts +13 -0
  184. package/apps/desktop/code/creative-recipes.ts +94 -0
  185. package/apps/desktop/code/define-workflow.ts +115 -0
  186. package/apps/desktop/code/editing.ts +284 -0
  187. package/apps/desktop/code/file-candidate-provenance.ts +420 -0
  188. package/apps/desktop/code/file-candidate.ts +69 -0
  189. package/apps/desktop/code/graph-builder.ts +10 -0
  190. package/apps/desktop/code/host-resource-policy.ts +55 -0
  191. package/apps/desktop/code/plan-contracts.ts +159 -0
  192. package/apps/desktop/code/planning.ts +361 -0
  193. package/apps/desktop/code/public.ts +134 -0
  194. package/apps/desktop/code/reference-projection.ts +62 -0
  195. package/apps/desktop/code/run-contracts.ts +466 -0
  196. package/apps/desktop/code/run-store.ts +2478 -0
  197. package/apps/desktop/code/runtime-identity.ts +328 -0
  198. package/apps/desktop/code/scheduler.ts +3067 -0
  199. package/apps/desktop/code/semantic-builder.ts +1436 -0
  200. package/apps/desktop/code/source-bundle.ts +1641 -0
  201. package/apps/desktop/code/source-typecheck.ts +211 -0
  202. package/apps/desktop/code/testing.ts +79 -0
  203. package/apps/desktop/code/worker-client.ts +1675 -0
  204. package/apps/desktop/code/worker-effects.ts +141 -0
  205. package/apps/desktop/code/worker-entry.ts +435 -0
  206. package/apps/desktop/code/worker-lease-guardian.ts +179 -0
  207. package/apps/desktop/code/worker-process-identity.ts +484 -0
  208. package/apps/desktop/code/worker-protocol.ts +211 -0
  209. package/apps/desktop/code/workflow-effects.ts +132 -0
  210. package/apps/desktop/contracts/analysis.ts +949 -0
  211. package/apps/desktop/contracts/edit.ts +576 -0
  212. package/apps/desktop/contracts/index.ts +9 -0
  213. package/apps/desktop/contracts/media-effects.ts +139 -0
  214. package/apps/desktop/contracts/project-render.ts +644 -0
  215. package/apps/desktop/contracts/project.ts +740 -0
  216. package/apps/desktop/contracts/recording.ts +1434 -0
  217. package/apps/desktop/contracts/render.ts +334 -0
  218. package/apps/desktop/contracts/runtime.ts +155 -0
  219. package/apps/desktop/contracts/spatial-compositor.ts +58 -0
  220. package/apps/desktop/contracts/spatial-project.ts +159 -0
  221. package/apps/desktop/contracts/spatial-world.ts +48 -0
  222. package/apps/desktop/contracts/video-effects.ts +120 -0
  223. package/apps/desktop/core/alignment-apply.ts +400 -0
  224. package/apps/desktop/core/audio-alignment.ts +466 -0
  225. package/apps/desktop/core/auto-zoom.ts +209 -0
  226. package/apps/desktop/core/canonical-json.ts +14 -0
  227. package/apps/desktop/core/emoji.ts +210 -0
  228. package/apps/desktop/core/events.ts +409 -0
  229. package/apps/desktop/core/face-framing.ts +533 -0
  230. package/apps/desktop/core/face-tracks.ts +326 -0
  231. package/apps/desktop/core/inactivity.ts +137 -0
  232. package/apps/desktop/core/index.ts +30 -0
  233. package/apps/desktop/core/intervals.ts +123 -0
  234. package/apps/desktop/core/montage.ts +236 -0
  235. package/apps/desktop/core/music-analysis.ts +518 -0
  236. package/apps/desktop/core/plan.ts +244 -0
  237. package/apps/desktop/core/project-auto-zoom.ts +75 -0
  238. package/apps/desktop/core/project-camera-spatial.ts +676 -0
  239. package/apps/desktop/core/project-camera.ts +403 -0
  240. package/apps/desktop/core/project-captions.ts +888 -0
  241. package/apps/desktop/core/project-inactivity.ts +125 -0
  242. package/apps/desktop/core/project-metadata.ts +927 -0
  243. package/apps/desktop/core/project-plan.ts +375 -0
  244. package/apps/desktop/core/project-render-plan.ts +303 -0
  245. package/apps/desktop/core/project-time.ts +469 -0
  246. package/apps/desktop/core/render-plan.ts +442 -0
  247. package/apps/desktop/core/scene-cache.ts +174 -0
  248. package/apps/desktop/core/scene-sampling.ts +448 -0
  249. package/apps/desktop/core/spatial-compositor.ts +22 -0
  250. package/apps/desktop/core/spatial-durability.ts +99 -0
  251. package/apps/desktop/core/spatial-project.ts +169 -0
  252. package/apps/desktop/core/spatial-shot-clock.ts +32 -0
  253. package/apps/desktop/core/speech-analysis.ts +333 -0
  254. package/apps/desktop/core/storage.ts +970 -0
  255. package/apps/desktop/core/time-map.ts +91 -0
  256. package/apps/desktop/core/time.ts +61 -0
  257. package/apps/desktop/core/video-effects.ts +660 -0
  258. package/apps/desktop/dist/cli/NebulaSans-Bold-26se8aek.otf +0 -0
  259. package/apps/desktop/dist/cli/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
  260. package/apps/desktop/dist/cli/NebulaSans-Book-5ax05zvn.woff2 +0 -0
  261. package/apps/desktop/dist/cli/NebulaSans-Book-8cenzchw.otf +0 -0
  262. package/apps/desktop/dist/cli/main.js +5086 -0
  263. package/apps/desktop/html-overlay/catalog.ts +171 -0
  264. package/apps/desktop/html-overlay/contracts.ts +372 -0
  265. package/apps/desktop/html-overlay/execution-profile.ts +77 -0
  266. package/apps/desktop/html-overlay/index.ts +98 -0
  267. package/apps/desktop/html-overlay/libraries.ts +263 -0
  268. package/apps/desktop/html-overlay/random.ts +49 -0
  269. package/apps/desktop/html-overlay/runtime.ts +813 -0
  270. package/apps/desktop/html-overlay/scaffolds.ts +792 -0
  271. package/apps/desktop/html-overlay/spark-runtime.ts +88 -0
  272. package/apps/desktop/html-overlay/spatial-splat-runtime.ts +60 -0
  273. package/apps/desktop/html-overlay/spatial.ts +720 -0
  274. package/apps/desktop/studio/assets/README.md +39 -0
  275. package/apps/desktop/studio/assets/contracts.ts +129 -0
  276. package/apps/desktop/studio/assets/network.ts +64 -0
  277. package/apps/desktop/studio/assets/poly-haven.ts +148 -0
  278. package/apps/desktop/studio/drivers/blender_driver.py +466 -0
  279. package/apps/desktop/studio/drivers/cadquery_driver.py +176 -0
  280. package/apps/desktop/studio/education/driver.py +308 -0
  281. package/apps/desktop/studio/education/lesson.py +180 -0
  282. package/apps/desktop/studio/education/toolkit.py +249 -0
  283. package/apps/desktop/workflows/chaptered-demo.ts +48 -0
  284. package/apps/desktop/workflows/creative-iteration.ts +107 -0
  285. package/apps/desktop/workflows/creative-selection.ts +92 -0
  286. package/apps/desktop/workflows/fragments.ts +486 -0
  287. package/apps/desktop/workflows/index.ts +93 -0
  288. package/apps/desktop/workflows/polished-screen-demo.ts +128 -0
  289. package/apps/desktop/workflows/scene-direction.ts +12 -0
  290. package/apps/desktop/workflows/social-variants.ts +75 -0
  291. package/apps/desktop/workflows/talking-head-cleanup.ts +53 -0
  292. package/dist/NebulaSans-Bold-26se8aek.otf +0 -0
  293. package/dist/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
  294. package/dist/NebulaSans-Book-5ax05zvn.woff2 +0 -0
  295. package/dist/NebulaSans-Book-8cenzchw.otf +0 -0
  296. package/dist/cli.js +479 -0
  297. package/dist/code/advanced.js +270 -0
  298. package/dist/code/index.js +2240 -0
  299. package/dist/generate.js +24 -0
  300. package/dist/host-resources.js +26 -0
  301. package/dist/index-42zsesc1.js +2257 -0
  302. package/dist/index-7308egqr.js +48 -0
  303. package/dist/index-8txs6fkn.js +712 -0
  304. package/dist/index-mcy8z0br.js +192 -0
  305. package/dist/index-p63wavx0.js +2029 -0
  306. package/dist/index-r7gdhmsp.js +424 -0
  307. package/dist/index-sh6xbav6.js +1219 -0
  308. package/dist/index-z1w83f81.js +4 -0
  309. package/dist/index-z7239b4h.js +2363 -0
  310. package/dist/index-zfeg9hmw.js +2013 -0
  311. package/dist/index.js +241 -0
  312. package/dist/operations.js +29 -0
  313. package/dist/skill-install-dh0ntqba.js +10 -0
  314. package/dist/vectorize/worker.js +142 -0
  315. package/dist/workflow.js +16 -0
  316. package/docs/README.md +38 -0
  317. package/docs/studio.md +215 -0
  318. package/examples/studio/blender/README.md +26 -0
  319. package/examples/studio/blender/character.py +152 -0
  320. package/examples/studio/blender/cloth.py +101 -0
  321. package/examples/studio/blender/color_chart.py +29 -0
  322. package/examples/studio/blender/fluid.py +78 -0
  323. package/examples/studio/blender/import_model.py +74 -0
  324. package/examples/studio/blender/product.py +53 -0
  325. package/examples/studio/blender/shaded_street.py +481 -0
  326. package/examples/studio/blender/studio_scene.py +147 -0
  327. package/examples/studio/cadquery/README.md +13 -0
  328. package/examples/studio/cadquery/bracket.py +36 -0
  329. package/examples/studio/cadquery/import_step.py +12 -0
  330. package/examples/studio/education/README.md +46 -0
  331. package/examples/studio/education/lesson.json +99 -0
  332. package/examples/studio/education/scene.py +7 -0
  333. package/examples/studio/hybrid-scene.ts +96 -0
  334. package/examples/studio/native-workflow.ts +14 -0
  335. package/examples/studio/vgpu/README.md +64 -0
  336. package/examples/studio/vgpu/heat-field.mjs +290 -0
  337. package/package.json +304 -0
  338. package/packages/scene/src/index.ts +1 -0
  339. package/packages/scene/src/scene.ts +215 -0
  340. package/schema/diagram.schema.json +334 -0
  341. package/skills/slopcamera/SKILL.md +45 -0
  342. package/skills/slopcamera/agents/openai.yaml +4 -0
  343. package/skills/slopcamera/references/customization.md +77 -0
  344. package/skills/slopcamera/references/diagrams.md +153 -0
  345. package/skills/slopcamera/references/directed-scenes.md +42 -0
  346. package/skills/slopcamera/references/directing-video.md +18 -0
  347. package/skills/slopcamera/references/educational-video.md +26 -0
  348. package/skills/slopcamera/references/gateway-media.md +134 -0
  349. package/skills/slopcamera/references/install.md +62 -0
  350. package/skills/slopcamera/references/native-studio.md +31 -0
  351. package/skills/slopcamera/references/reference-led-3d.md +140 -0
  352. package/skills/slopcamera/references/rubber-stamp-examples/poster-example-1.jpg +0 -0
  353. package/skills/slopcamera/references/rubber-stamp-examples/poster-example-2.jpg +0 -0
  354. package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-1.png +0 -0
  355. package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-2.png +0 -0
  356. package/skills/slopcamera/references/rubber-stamp-field-notes.md +144 -0
  357. package/skills/slopcamera/references/vectorization.md +29 -0
  358. package/skills/slopcamera/references/video-projects.md +187 -0
  359. package/skills/slopcamera/references/visual-communication.md +120 -0
  360. package/skills/slopcamera/references/workflows-sdk.md +45 -0
  361. package/skills/slopcamera/scripts/compose-rubber-stamp-field-note.ts +229 -0
  362. package/src/artifacts.ts +106 -0
  363. package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
  364. package/src/assets/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
  365. package/src/assets/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
  366. package/src/assets/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
  367. package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
  368. package/src/assets/fonts/nebula-sans/PROVENANCE.md +21 -0
  369. package/src/assets.d.ts +9 -0
  370. package/src/cli.ts +601 -0
  371. package/src/cloud-errors.ts +26 -0
  372. package/src/code/advanced.ts +10 -0
  373. package/src/code/boundary.ts +21 -0
  374. package/src/code/canonical-json.ts +129 -0
  375. package/src/code/compiler.ts +953 -0
  376. package/src/code/contracts.ts +576 -0
  377. package/src/code/define-workflow.ts +334 -0
  378. package/src/code/errors.ts +41 -0
  379. package/src/code/graph-builder.ts +945 -0
  380. package/src/code/index.ts +121 -0
  381. package/src/code/json-snapshot.ts +899 -0
  382. package/src/code/json-utf8.ts +88 -0
  383. package/src/code/portable-builder.ts +136 -0
  384. package/src/code/projection.ts +378 -0
  385. package/src/code/public-operations.ts +450 -0
  386. package/src/code/runtime.ts +580 -0
  387. package/src/code/sha256.ts +36 -0
  388. package/src/config.ts +180 -0
  389. package/src/desktop.ts +253 -0
  390. package/src/fs.ts +10 -0
  391. package/src/generate.ts +704 -0
  392. package/src/host-resource-posix.ts +161 -0
  393. package/src/host-resources.ts +1749 -0
  394. package/src/icons.ts +58 -0
  395. package/src/index.ts +200 -0
  396. package/src/label-layout.ts +130 -0
  397. package/src/layout.ts +413 -0
  398. package/src/lint.ts +127 -0
  399. package/src/mcp/boundary.ts +354 -0
  400. package/src/mcp/index.ts +33 -0
  401. package/src/mcp/server.ts +272 -0
  402. package/src/mcp/tools.ts +963 -0
  403. package/src/mcp/types.ts +71 -0
  404. package/src/operations.ts +775 -0
  405. package/src/parse.ts +861 -0
  406. package/src/process-environment.ts +17 -0
  407. package/src/render.ts +437 -0
  408. package/src/skill-install.ts +49 -0
  409. package/src/spatial-scene/authoring.ts +27 -0
  410. package/src/spatial-scene/camera-track.ts +83 -0
  411. package/src/spatial-scene/contracts.ts +277 -0
  412. package/src/spatial-scene/evaluate.ts +145 -0
  413. package/src/spatial-scene/gltf.ts +558 -0
  414. package/src/spatial-scene/identity.ts +260 -0
  415. package/src/spatial-scene/index.ts +10 -0
  416. package/src/spatial-scene/inspect.ts +68 -0
  417. package/src/spatial-scene/math.ts +280 -0
  418. package/src/spatial-scene/patch.ts +131 -0
  419. package/src/spatial-scene/time.ts +53 -0
  420. package/src/studio/contracts.ts +129 -0
  421. package/src/studio/index.ts +2 -0
  422. package/src/studio/plan.ts +156 -0
  423. package/src/studio/shared.ts +41 -0
  424. package/src/theme.ts +68 -0
  425. package/src/tldr.ts +474 -0
  426. package/src/types.ts +212 -0
  427. package/src/vectorize/archive.ts +101 -0
  428. package/src/vectorize/command.ts +797 -0
  429. package/src/vectorize/index.ts +27 -0
  430. package/src/vectorize/limits.ts +61 -0
  431. package/src/vectorize/metrics.ts +351 -0
  432. package/src/vectorize/pixels.ts +323 -0
  433. package/src/vectorize/supervisor.ts +339 -0
  434. package/src/vectorize/svg.ts +188 -0
  435. package/src/vectorize/tool.ts +542 -0
  436. package/src/vectorize/types.ts +113 -0
  437. package/src/vectorize/vectorize.ts +634 -0
  438. package/src/vectorize/worker-protocol.ts +40 -0
  439. package/src/vectorize/worker.ts +209 -0
  440. package/src/version.ts +1 -0
  441. package/src/workflow.ts +409 -0
@@ -0,0 +1,1567 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import {
5
+ CAPTURE_PROTOCOL_VERSION,
6
+ CaptureDeviceSelectionSchema,
7
+ CaptureDisplaySelectionSchema,
8
+ CaptureInterruptionSchema,
9
+ CapturePermissionsSchema,
10
+ CaptureSourceInventorySchema,
11
+ encodeCaptureRequest,
12
+ parseCaptureEventLine,
13
+ TypedTextFocusIdentitiesSchema,
14
+ type CaptureEvent,
15
+ type CaptureInterruption,
16
+ type CaptureOptions,
17
+ type CapturePermissions,
18
+ type CaptureRequest,
19
+ type CaptureSourceInventory,
20
+ type SegmentCompletion,
21
+ } from "../capture/protocol";
22
+ import { asCliError, CliError, errorMessage } from "./errors";
23
+ import { environmentWithoutGatewayCredentials, type CliIo } from "./io";
24
+ import { CaptureBundleWriter, CaptureMediaVerifier } from "./capture-bundle";
25
+ import type { ProcessRunner } from "./io";
26
+ import { ensurePrivateDirectory } from "./paths";
27
+
28
+ export type RecordingDeviceSelection =
29
+ | { readonly kind: "disabled" }
30
+ | { readonly kind: "default" }
31
+ | { readonly deviceId: string; readonly kind: "device" };
32
+
33
+ export type RecordingDisplaySelection =
34
+ | { readonly kind: "all" }
35
+ | { readonly displayIds: readonly string[]; readonly kind: "selected" };
36
+
37
+ export interface RecordingStartOptions {
38
+ readonly camera: RecordingDeviceSelection;
39
+ readonly displays: RecordingDisplaySelection;
40
+ readonly interactionEventProcessIdentifier?: number | null;
41
+ readonly microphone: RecordingDeviceSelection;
42
+ readonly strictInputs: boolean;
43
+ readonly systemAudio: boolean;
44
+ readonly typedText: boolean;
45
+ readonly typedTextFocusIdentities?: CaptureOptions["typedTextFocusIdentities"];
46
+ }
47
+
48
+ export interface EffectiveRecordingConfig extends RecordingStartOptions {
49
+ readonly metadata: true;
50
+ }
51
+
52
+ export type RecordingPhase = "idle" | "recording" | "paused";
53
+
54
+ export interface RecordingSnapshot {
55
+ readonly availableSources: CaptureSourceInventory;
56
+ readonly completedSegmentCount: number;
57
+ readonly effectiveConfig: EffectiveRecordingConfig | null;
58
+ readonly lastInterruption: CaptureInterruption | null;
59
+ readonly logicalTimeUs: number;
60
+ readonly permissions: CapturePermissions | null;
61
+ readonly recordingId: string | null;
62
+ readonly recordingRoot: string | null;
63
+ readonly sources: CaptureSourceInventory;
64
+ readonly state: RecordingPhase;
65
+ readonly updatedAt: string;
66
+ }
67
+
68
+ export interface RecordingController {
69
+ close(): Promise<void>;
70
+ pause(): Promise<RecordingSnapshot>;
71
+ resume(): Promise<RecordingSnapshot>;
72
+ start(options: RecordingStartOptions): Promise<RecordingSnapshot>;
73
+ status(): Promise<RecordingSnapshot>;
74
+ stop(): Promise<RecordingSnapshot>;
75
+ }
76
+
77
+ export interface CaptureTransport {
78
+ close(): Promise<void>;
79
+ readLine(timeoutMs: number): Promise<string>;
80
+ stderrTail(): string;
81
+ write(value: string): Promise<void>;
82
+ }
83
+
84
+ export interface CaptureTransportFactory {
85
+ spawn(executable: string): Promise<CaptureTransport>;
86
+ }
87
+
88
+ type CaptureResponseEventName = Exclude<CaptureEvent["event"], "error" | "ready">;
89
+ type CaptureResponseSequence = readonly CaptureResponseEventName[];
90
+
91
+ const CAPTURE_RESPONSE_SEQUENCES: Readonly<
92
+ Record<CaptureRequest["command"], readonly CaptureResponseSequence[]>
93
+ > = {
94
+ configure: [["configured"]],
95
+ pause: [["segment-completed"]],
96
+ resume: [
97
+ ["segment-started"],
98
+ ["segment-completed", "segment-started"],
99
+ ],
100
+ shutdown: [
101
+ ["shutdown"],
102
+ ["session-completed", "shutdown"],
103
+ ["segment-completed", "session-completed", "shutdown"],
104
+ ],
105
+ snapshot: [
106
+ ["status"],
107
+ ["segment-completed", "status"],
108
+ ["session-completed", "status"],
109
+ ["segment-completed", "session-completed", "status"],
110
+ ],
111
+ start: [["segment-started"]],
112
+ status: [
113
+ ["status"],
114
+ ["segment-completed", "status"],
115
+ ["session-completed", "status"],
116
+ ["segment-completed", "session-completed", "status"],
117
+ ],
118
+ stop: [
119
+ ["session-completed"],
120
+ ["segment-completed", "session-completed"],
121
+ ],
122
+ };
123
+ const MAX_FAILURE_RECOVERY_SOURCES_BYTES = 24 * 1024;
124
+
125
+ class NonFatalCaptureCommandError extends CliError {
126
+ constructor(
127
+ code: "conflict" | "unavailable",
128
+ event: Extract<CaptureEvent, { readonly event: "error" }>,
129
+ ) {
130
+ super(
131
+ code,
132
+ `Capture helper ${event.code}: ${event.message}`,
133
+ { helperCode: event.code, recoverable: true, state: event.state },
134
+ );
135
+ this.name = "NonFatalCaptureCommandError";
136
+ }
137
+ }
138
+
139
+ interface CaptureResponseFrontier {
140
+ readonly completedSegmentCount: number;
141
+ readonly lastInterruption: CaptureInterruption | null;
142
+ readonly logicalTimeUs: number;
143
+ readonly priorSnapshotLogicalTimeUs: number;
144
+ readonly sessionCompletionPersisted: boolean;
145
+ readonly sources: CaptureSourceInventory;
146
+ }
147
+
148
+ function isCaptureResponsePrefix(
149
+ sequence: CaptureResponseSequence,
150
+ events: readonly CaptureEvent[],
151
+ ): boolean {
152
+ return events.every((event, index) => sequence[index] === event.event);
153
+ }
154
+
155
+ function captureResponseNames(events: readonly CaptureEvent[]): string {
156
+ return events.map(({ event }) => event).join(" -> ");
157
+ }
158
+
159
+ function invalidCaptureResponse(
160
+ request: CaptureRequest,
161
+ events: readonly CaptureEvent[],
162
+ detail?: string,
163
+ ): CliError {
164
+ const suffix = detail === undefined ? "" : ` ${detail}`;
165
+ return new CliError(
166
+ "invalid-data",
167
+ `Capture helper emitted an invalid ${request.command} response sequence: ${captureResponseNames(events)}.${suffix}`,
168
+ );
169
+ }
170
+
171
+ function segmentCompletionIn(
172
+ events: readonly CaptureEvent[],
173
+ ): Extract<CaptureEvent, { readonly event: "segment-completed" }> | undefined {
174
+ return events.find((
175
+ event,
176
+ ): event is Extract<CaptureEvent, { readonly event: "segment-completed" }> => (
177
+ event.event === "segment-completed"
178
+ ));
179
+ }
180
+
181
+ function sessionCompletionIn(
182
+ events: readonly CaptureEvent[],
183
+ ): Extract<CaptureEvent, { readonly event: "session-completed" }> | undefined {
184
+ return events.find((
185
+ event,
186
+ ): event is Extract<CaptureEvent, { readonly event: "session-completed" }> => (
187
+ event.event === "session-completed"
188
+ ));
189
+ }
190
+
191
+ function sameCaptureInterruption(
192
+ left: CaptureInterruption | null,
193
+ right: CaptureInterruption | null,
194
+ ): boolean {
195
+ if (left === null || right === null) return left === right;
196
+ return left.code === right.code
197
+ && left.nativeTimeUs === right.nativeTimeUs
198
+ && left.recoverable === right.recoverable
199
+ && left.segmentIndex === right.segmentIndex
200
+ && left.source === right.source
201
+ && left.sourceId === right.sourceId
202
+ && left.sourceTimeUs === right.sourceTimeUs;
203
+ }
204
+
205
+ function sameCaptureSourceInventory(
206
+ left: CaptureSourceInventory,
207
+ right: CaptureSourceInventory,
208
+ ): boolean {
209
+ return JSON.stringify(left) === JSON.stringify(right);
210
+ }
211
+
212
+ function selectedSourcesIn(
213
+ event: CaptureEvent,
214
+ ): CaptureSourceInventory | undefined {
215
+ if (
216
+ event.event === "configured"
217
+ || event.event === "segment-started"
218
+ || event.event === "status"
219
+ ) {
220
+ return event.sources;
221
+ }
222
+ return event.event === "segment-completed"
223
+ ? event.segment.sources
224
+ : undefined;
225
+ }
226
+
227
+ function validateCaptureError(
228
+ request: CaptureRequest,
229
+ response: readonly CaptureEvent[],
230
+ error: Extract<CaptureEvent, { readonly event: "error" }>,
231
+ frontier: CaptureResponseFrontier,
232
+ ): "new-frontier" | "none" | "persisted" {
233
+ const interruption = error.interruption;
234
+ if (interruption === null) return "none";
235
+ if (request.command === "configure") {
236
+ throw invalidCaptureResponse(
237
+ request,
238
+ [error],
239
+ "Configure errors cannot carry segment interruption evidence.",
240
+ );
241
+ }
242
+ if (sameCaptureInterruption(interruption, frontier.lastInterruption)) {
243
+ return "persisted";
244
+ }
245
+
246
+ const completion = segmentCompletionIn(response);
247
+ if (
248
+ completion !== undefined
249
+ && interruption.segmentIndex === completion.segment.index
250
+ ) {
251
+ if (
252
+ completion.interruption === null
253
+ || !sameCaptureInterruption(interruption, completion.interruption)
254
+ ) {
255
+ throw invalidCaptureResponse(
256
+ request,
257
+ [...response, error],
258
+ "An error cannot replace same-segment completion interruption evidence.",
259
+ );
260
+ }
261
+ return "persisted";
262
+ }
263
+
264
+ const completedCount = frontier.completedSegmentCount
265
+ + (completion === undefined ? 0 : 1);
266
+ const sourceFrontierUs = completion?.segment.clock.end.sourceTimeUs
267
+ ?? frontier.logicalTimeUs;
268
+ const nativeFrontierUs = completion?.segment.clock.end.nativeTimeUs ?? 0;
269
+ if (
270
+ interruption.segmentIndex !== completedCount
271
+ || interruption.sourceTimeUs !== sourceFrontierUs
272
+ || interruption.nativeTimeUs < nativeFrontierUs
273
+ ) {
274
+ throw invalidCaptureResponse(
275
+ request,
276
+ [...response, error],
277
+ "An unmatched error interruption must identify the next segment and the persisted source-time frontier.",
278
+ );
279
+ }
280
+ return "new-frontier";
281
+ }
282
+
283
+ function validateCaptureResponse(
284
+ request: CaptureRequest,
285
+ events: readonly CaptureEvent[],
286
+ frontier: CaptureResponseFrontier,
287
+ ): void {
288
+ const segment = segmentCompletionIn(events);
289
+ const session = sessionCompletionIn(events);
290
+ if (frontier.sessionCompletionPersisted && session !== undefined) {
291
+ throw invalidCaptureResponse(
292
+ request,
293
+ events,
294
+ "A completed session may not emit session-completed again.",
295
+ );
296
+ }
297
+
298
+ const responseSegmentCount = segment === undefined ? 0 : 1;
299
+ const expectedCompletedSegmentCount = frontier.completedSegmentCount + responseSegmentCount;
300
+ const expectedLogicalTimeUs = segment?.segment.clock.end.sourceTimeUs ?? frontier.logicalTimeUs;
301
+ if (
302
+ segment !== undefined
303
+ && segment.segment.index !== frontier.completedSegmentCount
304
+ ) {
305
+ throw invalidCaptureResponse(
306
+ request,
307
+ events,
308
+ "Segment completion indices must advance from the persisted bundle frontier.",
309
+ );
310
+ }
311
+ if (
312
+ segment !== undefined
313
+ && segment.segment.clock.start.sourceTimeUs !== frontier.logicalTimeUs
314
+ ) {
315
+ throw invalidCaptureResponse(
316
+ request,
317
+ events,
318
+ "Segment completion must begin at the persisted bundle frontier.",
319
+ );
320
+ }
321
+ if (
322
+ session !== undefined
323
+ && (
324
+ session.segmentCount !== expectedCompletedSegmentCount
325
+ || session.durationUs !== expectedLogicalTimeUs
326
+ )
327
+ ) {
328
+ throw invalidCaptureResponse(
329
+ request,
330
+ events,
331
+ "Session completion must equal the persisted bundle plus its optional segment completion.",
332
+ );
333
+ }
334
+
335
+ if (request.command === "resume" || request.command === "start") {
336
+ const started = events.find((
337
+ event,
338
+ ): event is Extract<CaptureEvent, { readonly event: "segment-started" }> => (
339
+ event.event === "segment-started"
340
+ ));
341
+ if (
342
+ started !== undefined
343
+ && (
344
+ started.index !== expectedCompletedSegmentCount
345
+ || started.startUs !== expectedLogicalTimeUs
346
+ )
347
+ ) {
348
+ throw invalidCaptureResponse(
349
+ request,
350
+ events,
351
+ "A started segment must use the next index and begin at the completed bundle frontier.",
352
+ );
353
+ }
354
+ }
355
+
356
+ if (request.command !== "configure") {
357
+ for (const event of events) {
358
+ const selectedSources = selectedSourcesIn(event);
359
+ if (
360
+ selectedSources !== undefined
361
+ && !(
362
+ event.event === "segment-started"
363
+ && (request.command === "start" || request.command === "resume")
364
+ )
365
+ && !sameCaptureSourceInventory(selectedSources, frontier.sources)
366
+ ) {
367
+ throw invalidCaptureResponse(
368
+ request,
369
+ events,
370
+ "Completion and status events must retain the current selected capture sources.",
371
+ );
372
+ }
373
+ }
374
+ }
375
+
376
+ if (request.command !== "snapshot" && request.command !== "status") return;
377
+ const status = events.at(-1);
378
+ if (status?.event !== "status") {
379
+ throw invalidCaptureResponse(request, events, "The response did not end with status.");
380
+ }
381
+ if (
382
+ status.state !== "recording"
383
+ && status.state !== "paused"
384
+ && status.state !== "stopped"
385
+ ) {
386
+ throw invalidCaptureResponse(
387
+ request,
388
+ events,
389
+ "An active recording may report only recording, paused, or proven stopped status.",
390
+ );
391
+ }
392
+ if (frontier.sessionCompletionPersisted && status.state !== "stopped") {
393
+ throw invalidCaptureResponse(
394
+ request,
395
+ events,
396
+ "A completed session may report only stopped status.",
397
+ );
398
+ }
399
+ if (
400
+ (status.state === "stopped" && session === undefined && !frontier.sessionCompletionPersisted)
401
+ || (status.state !== "stopped" && session !== undefined)
402
+ ) {
403
+ throw invalidCaptureResponse(
404
+ request,
405
+ events,
406
+ "A stopped status requires session-completed proof, and no other status may follow same-request completion.",
407
+ );
408
+ }
409
+ if (segment !== undefined && session === undefined && status.state !== "paused") {
410
+ throw invalidCaptureResponse(
411
+ request,
412
+ events,
413
+ "A segment completion without session completion must reconcile to paused.",
414
+ );
415
+ }
416
+ if (
417
+ status.state === "paused"
418
+ && (
419
+ status.completedSegmentCount !== expectedCompletedSegmentCount
420
+ || status.logicalTimeUs !== expectedLogicalTimeUs
421
+ )
422
+ ) {
423
+ throw invalidCaptureResponse(
424
+ request,
425
+ events,
426
+ "Paused status counters must equal the persisted bundle plus its optional segment completion.",
427
+ );
428
+ }
429
+ const expectedActiveSegmentIndex = status.state === "recording"
430
+ ? expectedCompletedSegmentCount
431
+ : null;
432
+ if (status.activeSegmentIndex !== expectedActiveSegmentIndex) {
433
+ throw invalidCaptureResponse(
434
+ request,
435
+ events,
436
+ "Status activeSegmentIndex must match the capture frontier and lifecycle state.",
437
+ );
438
+ }
439
+ const expectedLastInterruption = segment?.interruption
440
+ ?? frontier.lastInterruption;
441
+ if (
442
+ !sameCaptureInterruption(
443
+ status.lastInterruption,
444
+ expectedLastInterruption,
445
+ )
446
+ ) {
447
+ throw invalidCaptureResponse(
448
+ request,
449
+ events,
450
+ "Status lastInterruption must match the latest accepted interruption evidence.",
451
+ );
452
+ }
453
+ if (
454
+ status.state === "recording"
455
+ && (
456
+ status.completedSegmentCount !== frontier.completedSegmentCount
457
+ || status.logicalTimeUs < frontier.logicalTimeUs
458
+ || status.logicalTimeUs < frontier.priorSnapshotLogicalTimeUs
459
+ )
460
+ ) {
461
+ throw invalidCaptureResponse(
462
+ request,
463
+ events,
464
+ "Recording status must preserve the persisted segment count and advance monotonically from both bundle and live frontiers.",
465
+ );
466
+ }
467
+ if (
468
+ status.state === "stopped"
469
+ && (
470
+ status.completedSegmentCount !== expectedCompletedSegmentCount
471
+ || status.logicalTimeUs !== expectedLogicalTimeUs
472
+ )
473
+ ) {
474
+ throw invalidCaptureResponse(
475
+ request,
476
+ events,
477
+ "Stopped status counters must equal the completed bundle frontier.",
478
+ );
479
+ }
480
+ if (
481
+ session !== undefined
482
+ && (
483
+ status.completedSegmentCount !== session.segmentCount
484
+ || status.logicalTimeUs !== session.durationUs
485
+ )
486
+ ) {
487
+ throw invalidCaptureResponse(
488
+ request,
489
+ events,
490
+ "Status counters must match the preceding session completion.",
491
+ );
492
+ }
493
+ }
494
+
495
+ function spawnCaptureProcess(executable: string) {
496
+ return Bun.spawn([executable], {
497
+ env: environmentWithoutGatewayCredentials(process.env),
498
+ stdin: "pipe" as const,
499
+ stdout: "pipe" as const,
500
+ stderr: "pipe" as const,
501
+ });
502
+ }
503
+
504
+ const CAPTURE_HELPER_GRACEFUL_EXIT_TIMEOUT_MS = 30_000;
505
+
506
+ class BunCaptureTransport implements CaptureTransport {
507
+ readonly #child: ReturnType<typeof spawnCaptureProcess>;
508
+ readonly #reader: ReadableStreamDefaultReader<Uint8Array>;
509
+ readonly #decoder = new TextDecoder();
510
+ #buffer = "";
511
+ #closed = false;
512
+ #stderr = "";
513
+
514
+ constructor(executable: string) {
515
+ this.#child = spawnCaptureProcess(executable);
516
+ this.#reader = this.#child.stdout.getReader();
517
+ void this.#collectStderr();
518
+ }
519
+
520
+ async #collectStderr(): Promise<void> {
521
+ try {
522
+ for await (const chunk of this.#child.stderr) {
523
+ this.#stderr = `${this.#stderr}${new TextDecoder().decode(chunk)}`.slice(-16_384);
524
+ }
525
+ } catch {
526
+ // The structured protocol remains authoritative; stderr is best-effort evidence.
527
+ }
528
+ }
529
+
530
+ async readLine(timeoutMs: number): Promise<string> {
531
+ const deadline = Date.now() + timeoutMs;
532
+ while (true) {
533
+ const newline = this.#buffer.indexOf("\n");
534
+ if (newline !== -1) {
535
+ const line = this.#buffer.slice(0, newline);
536
+ this.#buffer = this.#buffer.slice(newline + 1);
537
+ return line;
538
+ }
539
+ const remaining = deadline - Date.now();
540
+ if (remaining <= 0) throw new CliError("subprocess", "Capture helper response timed out.");
541
+ const result = await Promise.race([
542
+ this.#reader.read(),
543
+ new Promise<never>((_, reject) => {
544
+ setTimeout(() => reject(new CliError("subprocess", "Capture helper response timed out.")), remaining);
545
+ }),
546
+ ]);
547
+ if (result.done) {
548
+ throw new CliError(
549
+ "subprocess",
550
+ `Capture helper closed its protocol stream${this.#stderr === "" ? "." : `: ${this.#stderr.trim()}`}`,
551
+ );
552
+ }
553
+ this.#buffer += this.#decoder.decode(result.value, { stream: true });
554
+ if (new TextEncoder().encode(this.#buffer).byteLength > 64 * 1024) {
555
+ throw new CliError("invalid-data", "Capture helper emitted an oversized protocol line.");
556
+ }
557
+ }
558
+ }
559
+
560
+ async write(value: string): Promise<void> {
561
+ if (this.#closed) throw new CliError("conflict", "Capture helper transport is closed.");
562
+ void this.#child.stdin.write(value);
563
+ await this.#child.stdin.flush();
564
+ }
565
+
566
+ stderrTail(): string {
567
+ return this.#stderr;
568
+ }
569
+
570
+ async close(): Promise<void> {
571
+ if (this.#closed) return;
572
+ this.#closed = true;
573
+ void this.#child.stdin.end();
574
+ try {
575
+ await this.#reader.cancel();
576
+ } catch {
577
+ // Closing stdout lets a finalizing helper exit even when its terminal
578
+ // events would otherwise fill the unread protocol pipe.
579
+ }
580
+ let timeout: ReturnType<typeof setTimeout> | undefined;
581
+ const exited = await Promise.race([
582
+ this.#child.exited.then(() => true),
583
+ new Promise<false>((resolve) => {
584
+ timeout = setTimeout(() => resolve(false), CAPTURE_HELPER_GRACEFUL_EXIT_TIMEOUT_MS);
585
+ }),
586
+ ]);
587
+ if (timeout !== undefined) clearTimeout(timeout);
588
+ if (!exited) this.#child.kill();
589
+ }
590
+ }
591
+
592
+ export class BunCaptureTransportFactory implements CaptureTransportFactory {
593
+ spawn(executable: string): Promise<CaptureTransport> {
594
+ return Promise.resolve(new BunCaptureTransport(executable));
595
+ }
596
+ }
597
+
598
+ function emptyCaptureSourceInventory(): CaptureSourceInventory {
599
+ return { audio: [], cameras: [], displays: [] };
600
+ }
601
+
602
+ function idleSnapshot(now: Date): RecordingSnapshot {
603
+ return {
604
+ availableSources: emptyCaptureSourceInventory(),
605
+ completedSegmentCount: 0,
606
+ effectiveConfig: null,
607
+ lastInterruption: null,
608
+ logicalTimeUs: 0,
609
+ permissions: null,
610
+ recordingId: null,
611
+ recordingRoot: null,
612
+ sources: emptyCaptureSourceInventory(),
613
+ state: "idle",
614
+ updatedAt: now.toISOString(),
615
+ };
616
+ }
617
+
618
+ function activeStatePath(artifactRoot: string): string {
619
+ return join(artifactRoot, ".active-recording.json");
620
+ }
621
+
622
+ function parseEffectiveConfig(value: unknown): EffectiveRecordingConfig | null {
623
+ if (value === null) return null;
624
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
625
+ throw new CliError("invalid-data", "Recording effective config is invalid.");
626
+ }
627
+ const config = value as Readonly<Record<string, unknown>>;
628
+ const booleanField = (field: string): boolean => {
629
+ const item = config[field];
630
+ if (typeof item !== "boolean") {
631
+ throw new CliError("invalid-data", `Recording effective config ${field} is invalid.`);
632
+ }
633
+ return item;
634
+ };
635
+ const metadata = booleanField("metadata");
636
+ if (!metadata) throw new CliError("invalid-data", "Recording metadata cannot be disabled.");
637
+ const rawProcessIdentifier = config.interactionEventProcessIdentifier;
638
+ const interactionEventProcessIdentifier = rawProcessIdentifier === undefined
639
+ ? undefined
640
+ : rawProcessIdentifier === null
641
+ ? null
642
+ : (
643
+ typeof rawProcessIdentifier === "number"
644
+ && Number.isInteger(rawProcessIdentifier)
645
+ && rawProcessIdentifier > 0
646
+ && rawProcessIdentifier <= 2_147_483_647
647
+ )
648
+ ? rawProcessIdentifier
649
+ : (() => {
650
+ throw new CliError(
651
+ "invalid-data",
652
+ "Recording effective config interaction event process ID is invalid.",
653
+ );
654
+ })();
655
+ const rawFocusIdentities = config.typedTextFocusIdentities;
656
+ const parsedFocusIdentities = rawFocusIdentities === undefined
657
+ ? undefined
658
+ : TypedTextFocusIdentitiesSchema.nullable().safeParse(rawFocusIdentities);
659
+ if (
660
+ parsedFocusIdentities !== undefined
661
+ && !parsedFocusIdentities.success
662
+ ) {
663
+ throw new CliError(
664
+ "invalid-data",
665
+ "Recording effective config typed-text focus identities are invalid.",
666
+ );
667
+ }
668
+ const typedTextFocusIdentities = parsedFocusIdentities?.data;
669
+ const parsedCamera = CaptureDeviceSelectionSchema.safeParse(config.camera);
670
+ if (!parsedCamera.success) {
671
+ throw new CliError("invalid-data", "Recording effective config camera selection is invalid.");
672
+ }
673
+ const parsedDisplays = CaptureDisplaySelectionSchema.safeParse(config.displays);
674
+ if (!parsedDisplays.success) {
675
+ throw new CliError("invalid-data", "Recording effective config display selection is invalid.");
676
+ }
677
+ const parsedMicrophone = CaptureDeviceSelectionSchema.safeParse(config.microphone);
678
+ if (!parsedMicrophone.success) {
679
+ throw new CliError("invalid-data", "Recording effective config microphone selection is invalid.");
680
+ }
681
+ return {
682
+ camera: parsedCamera.data,
683
+ displays: parsedDisplays.data,
684
+ ...(interactionEventProcessIdentifier === undefined
685
+ ? {}
686
+ : { interactionEventProcessIdentifier }),
687
+ metadata: true,
688
+ microphone: parsedMicrophone.data,
689
+ strictInputs: booleanField("strictInputs"),
690
+ systemAudio: booleanField("systemAudio"),
691
+ typedText: booleanField("typedText"),
692
+ ...(typedTextFocusIdentities === undefined
693
+ ? {}
694
+ : {
695
+ typedTextFocusIdentities: typedTextFocusIdentities === null
696
+ ? null
697
+ : typedTextFocusIdentities.map(identity => ({ ...identity })),
698
+ }),
699
+ };
700
+ }
701
+
702
+ export function parseRecordingSnapshot(value: unknown): RecordingSnapshot {
703
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
704
+ throw new CliError("invalid-data", "Active recording state must be a JSON object.");
705
+ }
706
+ const state = value as Readonly<Record<string, unknown>>;
707
+ if (state.state !== "idle" && state.state !== "recording" && state.state !== "paused") {
708
+ throw new CliError("invalid-data", "Active recording state has an unknown phase.");
709
+ }
710
+ if (
711
+ typeof state.completedSegmentCount !== "number"
712
+ || !Number.isSafeInteger(state.completedSegmentCount)
713
+ || state.completedSegmentCount < 0
714
+ || typeof state.logicalTimeUs !== "number"
715
+ || !Number.isSafeInteger(state.logicalTimeUs)
716
+ || state.logicalTimeUs < 0
717
+ || typeof state.updatedAt !== "string"
718
+ ) {
719
+ throw new CliError("invalid-data", "Active recording state has invalid counters or timestamp.");
720
+ }
721
+ const recordingId = state.recordingId;
722
+ const recordingRoot = state.recordingRoot;
723
+ const permissions = CapturePermissionsSchema.nullable().safeParse(
724
+ state.permissions,
725
+ );
726
+ const sources = CaptureSourceInventorySchema.safeParse(state.sources);
727
+ const availableSources = CaptureSourceInventorySchema.safeParse(
728
+ state.availableSources,
729
+ );
730
+ const lastInterruption = CaptureInterruptionSchema.nullable().safeParse(
731
+ state.lastInterruption,
732
+ );
733
+ if (
734
+ (recordingId !== null && typeof recordingId !== "string")
735
+ || (recordingRoot !== null && typeof recordingRoot !== "string")
736
+ || !permissions.success
737
+ || !sources.success
738
+ || !availableSources.success
739
+ || !lastInterruption.success
740
+ ) {
741
+ throw new CliError(
742
+ "invalid-data",
743
+ "Active recording state has invalid identity, permissions, sources, or interruption evidence.",
744
+ );
745
+ }
746
+ return {
747
+ availableSources: availableSources.data,
748
+ completedSegmentCount: state.completedSegmentCount,
749
+ effectiveConfig: parseEffectiveConfig(state.effectiveConfig),
750
+ lastInterruption: lastInterruption.data,
751
+ logicalTimeUs: state.logicalTimeUs,
752
+ permissions: permissions.data,
753
+ recordingId,
754
+ recordingRoot,
755
+ sources: sources.data,
756
+ state: state.state,
757
+ updatedAt: state.updatedAt,
758
+ };
759
+ }
760
+
761
+ export async function readRepositoryRecordingState(
762
+ artifactRoot: string,
763
+ now: () => Date = () => new Date(),
764
+ ): Promise<RecordingSnapshot> {
765
+ try {
766
+ const bytes = await readFile(activeStatePath(artifactRoot), "utf8");
767
+ let value: unknown;
768
+ try {
769
+ value = JSON.parse(bytes);
770
+ } catch {
771
+ throw new CliError("invalid-data", "Active recording state is not valid JSON.");
772
+ }
773
+ return parseRecordingSnapshot(value);
774
+ } catch (error) {
775
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return idleSnapshot(now());
776
+ throw error;
777
+ }
778
+ }
779
+
780
+ async function writeRepositoryRecordingState(
781
+ artifactRoot: string,
782
+ snapshot: RecordingSnapshot,
783
+ ): Promise<void> {
784
+ await ensurePrivateDirectory(artifactRoot);
785
+ const destination = activeStatePath(artifactRoot);
786
+ try {
787
+ if ((await lstat(destination)).isSymbolicLink()) {
788
+ throw new CliError("unsafe-path", `Active recording state is a symlink: ${destination}`);
789
+ }
790
+ } catch (error) {
791
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
792
+ }
793
+ const temporary = join(artifactRoot, `.active-recording.${randomUUID()}.tmp`);
794
+ try {
795
+ await writeFile(temporary, `${JSON.stringify(snapshot)}\n`, { flag: "wx", mode: 0o600 });
796
+ await rename(temporary, destination);
797
+ } finally {
798
+ await rm(temporary, { force: true });
799
+ }
800
+ }
801
+
802
+ function permissionsFrom(event: Extract<
803
+ CaptureEvent,
804
+ { readonly event: "configured" | "segment-started" | "status" }
805
+ >): CapturePermissions {
806
+ return { ...event.permissions };
807
+ }
808
+
809
+ function notifyCaptureObserver(
810
+ observer: ((event: CaptureEvent) => void) | undefined,
811
+ event: CaptureEvent,
812
+ ): void {
813
+ if (observer === undefined) return;
814
+ try {
815
+ // Observers receive evidence, not authority over the controller's parsed
816
+ // protocol object. Their diagnostics must not fail or mutate capture.
817
+ observer(structuredClone(event));
818
+ } catch {
819
+ // Capture remains authoritative when optional observation fails.
820
+ }
821
+ }
822
+
823
+ export interface CaptureHelperRecordingControllerOptions {
824
+ readonly artifactRoot: string;
825
+ readonly executable: string;
826
+ readonly io: Pick<CliIo, "now">;
827
+ readonly onCaptureEvent?: (event: CaptureEvent) => void;
828
+ readonly requestTimeoutMs?: number;
829
+ readonly ffprobeExecutable?: string;
830
+ readonly runner?: ProcessRunner;
831
+ readonly transportFactory?: CaptureTransportFactory;
832
+ }
833
+
834
+ interface FailedSegmentCompletion {
835
+ readonly failure: unknown;
836
+ readonly interruption: CaptureInterruption | null;
837
+ readonly segment: SegmentCompletion;
838
+ }
839
+
840
+ export class CaptureHelperRecordingController implements RecordingController {
841
+ readonly #artifactRoot: string;
842
+ readonly #executable: string;
843
+ readonly #now: () => Date;
844
+ readonly #onCaptureEvent: ((event: CaptureEvent) => void) | undefined;
845
+ readonly #requestTimeoutMs: number;
846
+ readonly #transportFactory: CaptureTransportFactory;
847
+ #transport: CaptureTransport | undefined;
848
+ #bundleWriter: CaptureBundleWriter | undefined;
849
+ #snapshot: RecordingSnapshot;
850
+ #effectiveConfig: EffectiveRecordingConfig | null = null;
851
+ #closed = false;
852
+ #requestCounter = 0;
853
+ #helperVersion = "unknown";
854
+ #sessionCompletionPersisted = false;
855
+ #failedSegmentCompletion: FailedSegmentCompletion | undefined;
856
+ #pendingFailureInterruption: CaptureInterruption | undefined;
857
+ readonly #verifier: CaptureMediaVerifier;
858
+
859
+ constructor(options: CaptureHelperRecordingControllerOptions) {
860
+ this.#artifactRoot = options.artifactRoot;
861
+ this.#executable = options.executable;
862
+ this.#now = options.io.now;
863
+ this.#onCaptureEvent = options.onCaptureEvent;
864
+ this.#requestTimeoutMs = options.requestTimeoutMs ?? 240_000;
865
+ this.#transportFactory = options.transportFactory ?? new BunCaptureTransportFactory();
866
+ this.#verifier = new CaptureMediaVerifier({
867
+ ...(options.ffprobeExecutable === undefined ? {} : { ffprobe: options.ffprobeExecutable }),
868
+ ...(options.runner === undefined ? {} : { runner: options.runner }),
869
+ });
870
+ this.#snapshot = idleSnapshot(this.#now());
871
+ }
872
+
873
+ async #ensureTransport(): Promise<CaptureTransport> {
874
+ if (this.#closed) throw new CliError("conflict", "Recording controller is closed.");
875
+ if (this.#transport !== undefined) return this.#transport;
876
+ try {
877
+ if (!(await stat(this.#executable)).isFile()) {
878
+ throw new CliError("unavailable", `Capture helper is not a file: ${this.#executable}`);
879
+ }
880
+ } catch (error) {
881
+ if (error instanceof CliError) throw error;
882
+ throw new CliError(
883
+ "unavailable",
884
+ `Capture helper is unavailable at ${this.#executable}. Run: bun run build:desktop:capture:macos`,
885
+ );
886
+ }
887
+ const transport = await this.#transportFactory.spawn(this.#executable);
888
+ this.#transport = transport;
889
+ const ready = parseCaptureEventLine(await transport.readLine(this.#requestTimeoutMs));
890
+ notifyCaptureObserver(this.#onCaptureEvent, ready);
891
+ if (ready.event !== "ready") {
892
+ await transport.close();
893
+ this.#transport = undefined;
894
+ throw new CliError("invalid-data", `Capture helper emitted ${ready.event} before ready.`);
895
+ }
896
+ this.#helperVersion = ready.helperVersion;
897
+ return transport;
898
+ }
899
+
900
+ #requestId(command: string): string {
901
+ this.#requestCounter += 1;
902
+ return `cli:${command}:${this.#requestCounter}`;
903
+ }
904
+
905
+ async #settleSessionFailure(error: unknown): Promise<never> {
906
+ const failedRecordingId = this.#snapshot.recordingId ?? this.#bundleWriter?.manifest.recordingId ?? null;
907
+ const failedRecordingRoot = this.#snapshot.recordingRoot;
908
+ const pendingFailureInterruption = this.#pendingFailureInterruption;
909
+ const failedLastInterruption =
910
+ pendingFailureInterruption ?? this.#snapshot.lastInterruption;
911
+ const failedSources = structuredClone(this.#snapshot.sources);
912
+ const includeFailedSources = Buffer.byteLength(
913
+ JSON.stringify(failedSources),
914
+ ) <= MAX_FAILURE_RECOVERY_SOURCES_BYTES;
915
+ const failedSnapshot = {
916
+ lastInterruption: failedLastInterruption === null
917
+ ? null
918
+ : structuredClone(failedLastInterruption),
919
+ logicalTimeUs: this.#snapshot.logicalTimeUs,
920
+ permissions: this.#snapshot.permissions === null
921
+ ? null
922
+ : structuredClone(this.#snapshot.permissions),
923
+ ...(includeFailedSources ? { sources: failedSources } : {}),
924
+ } as const;
925
+ const recoveryErrors: string[] = [];
926
+ const failedSegmentCompletion = this.#failedSegmentCompletion;
927
+ let failedSegmentCompletionPersisted = failedSegmentCompletion === undefined;
928
+ const persistFailedSegmentCompletion = async (
929
+ errorPrefix: "completion frontier: " | "completion frontier retry: ",
930
+ ): Promise<void> => {
931
+ if (
932
+ failedSegmentCompletion === undefined
933
+ || this.#bundleWriter === undefined
934
+ ) return;
935
+ try {
936
+ await this.#bundleWriter.recordFailedSegmentCompletion(
937
+ failedSegmentCompletion.segment,
938
+ failedSegmentCompletion.interruption,
939
+ failedSegmentCompletion.failure,
940
+ );
941
+ failedSegmentCompletionPersisted = true;
942
+ for (let index = recoveryErrors.length - 1; index >= 0; index -= 1) {
943
+ if (recoveryErrors[index]?.startsWith("completion frontier")) {
944
+ recoveryErrors.splice(index, 1);
945
+ }
946
+ }
947
+ } catch (persistenceError) {
948
+ recoveryErrors.push(`${errorPrefix}${errorMessage(persistenceError)}`);
949
+ }
950
+ };
951
+ await persistFailedSegmentCompletion("completion frontier: ");
952
+ let pendingFailureInterruptionPersisted =
953
+ pendingFailureInterruption === undefined;
954
+ const persistPendingFailureInterruption = async (
955
+ errorPrefix: "failure interruption: " | "failure interruption retry: ",
956
+ ): Promise<void> => {
957
+ if (
958
+ pendingFailureInterruption === undefined
959
+ || this.#bundleWriter === undefined
960
+ ) return;
961
+ try {
962
+ await this.#bundleWriter.recordFailureInterruption(
963
+ pendingFailureInterruption,
964
+ );
965
+ pendingFailureInterruptionPersisted = true;
966
+ for (let index = recoveryErrors.length - 1; index >= 0; index -= 1) {
967
+ if (recoveryErrors[index]?.startsWith("failure interruption")) {
968
+ recoveryErrors.splice(index, 1);
969
+ }
970
+ }
971
+ } catch (persistenceError) {
972
+ recoveryErrors.push(`${errorPrefix}${errorMessage(persistenceError)}`);
973
+ }
974
+ };
975
+ await persistPendingFailureInterruption("failure interruption: ");
976
+ let terminalManifestState: "failed" | "stopped" | "unsettled" = this.#sessionCompletionPersisted
977
+ ? "stopped"
978
+ : "unsettled";
979
+
980
+ if (this.#bundleWriter !== undefined && !this.#sessionCompletionPersisted) {
981
+ const durationUs = this.#bundleWriter.manifest.timeline.durationUs;
982
+ try {
983
+ await this.#bundleWriter.setState("failed", durationUs);
984
+ terminalManifestState = "failed";
985
+ } catch (settlementError) {
986
+ recoveryErrors.push(`manifest: ${errorMessage(settlementError)}`);
987
+ }
988
+ }
989
+
990
+ const transport = this.#transport;
991
+ this.#transport = undefined;
992
+ if (transport !== undefined) {
993
+ try {
994
+ await transport.close();
995
+ } catch (transportError) {
996
+ recoveryErrors.push(`helper: ${errorMessage(transportError)}`);
997
+ }
998
+ }
999
+
1000
+ if (!failedSegmentCompletionPersisted) {
1001
+ await persistFailedSegmentCompletion("completion frontier retry: ");
1002
+ }
1003
+ if (!pendingFailureInterruptionPersisted) {
1004
+ await persistPendingFailureInterruption("failure interruption retry: ");
1005
+ }
1006
+
1007
+ if (terminalManifestState === "unsettled" && this.#bundleWriter !== undefined) {
1008
+ try {
1009
+ await this.#bundleWriter.setState(
1010
+ "failed",
1011
+ this.#bundleWriter.manifest.timeline.durationUs,
1012
+ );
1013
+ terminalManifestState = "failed";
1014
+ const manifestFailureIndex = recoveryErrors.findIndex((item) => item.startsWith("manifest: "));
1015
+ if (manifestFailureIndex !== -1) recoveryErrors.splice(manifestFailureIndex, 1);
1016
+ } catch (settlementError) {
1017
+ const message = `manifest retry: ${errorMessage(settlementError)}`;
1018
+ if (!recoveryErrors.includes(message)) recoveryErrors.push(message);
1019
+ }
1020
+ }
1021
+
1022
+ try {
1023
+ await rm(activeStatePath(this.#artifactRoot), { force: true });
1024
+ } catch (activeStateError) {
1025
+ recoveryErrors.push(`active state: ${errorMessage(activeStateError)}`);
1026
+ }
1027
+
1028
+ this.#bundleWriter = undefined;
1029
+ this.#effectiveConfig = null;
1030
+ this.#failedSegmentCompletion = undefined;
1031
+ this.#pendingFailureInterruption = undefined;
1032
+ this.#sessionCompletionPersisted = false;
1033
+ this.#snapshot = idleSnapshot(this.#now());
1034
+ if (recoveryErrors.length > 0) this.#closed = true;
1035
+
1036
+ const failure = asCliError(error);
1037
+ const recovery = {
1038
+ controllerReusable: recoveryErrors.length === 0,
1039
+ recordingId: failedRecordingId,
1040
+ recordingRoot: failedRecordingRoot,
1041
+ snapshot: failedSnapshot,
1042
+ terminalManifestState,
1043
+ } as const;
1044
+ if (recoveryErrors.length > 0) {
1045
+ throw new CliError(
1046
+ "internal",
1047
+ `${failure.message} Capture recovery was incomplete: ${recoveryErrors.join("; ")}`,
1048
+ {
1049
+ captureFailure: { code: failure.code, details: failure.details ?? null },
1050
+ recovery,
1051
+ recoveryErrors,
1052
+ },
1053
+ );
1054
+ }
1055
+ throw new CliError(failure.code, failure.message, {
1056
+ ...(failure.details ?? {}),
1057
+ recovery,
1058
+ });
1059
+ }
1060
+
1061
+ async #discardUninitializedSession(recordingRoot: string, error: unknown): Promise<never> {
1062
+ const recoveryErrors: string[] = [];
1063
+ const transport = this.#transport;
1064
+ this.#transport = undefined;
1065
+ if (transport !== undefined) {
1066
+ try {
1067
+ await transport.close();
1068
+ } catch (transportError) {
1069
+ recoveryErrors.push(`helper: ${errorMessage(transportError)}`);
1070
+ }
1071
+ }
1072
+ try {
1073
+ await rm(recordingRoot, { force: true, recursive: true });
1074
+ await rm(activeStatePath(this.#artifactRoot), { force: true });
1075
+ } catch (cleanupError) {
1076
+ recoveryErrors.push(`recording cleanup: ${errorMessage(cleanupError)}`);
1077
+ }
1078
+ this.#bundleWriter = undefined;
1079
+ this.#effectiveConfig = null;
1080
+ this.#failedSegmentCompletion = undefined;
1081
+ this.#pendingFailureInterruption = undefined;
1082
+ this.#sessionCompletionPersisted = false;
1083
+ this.#snapshot = idleSnapshot(this.#now());
1084
+ if (recoveryErrors.length > 0) this.#closed = true;
1085
+
1086
+ const failure = asCliError(error);
1087
+ if (recoveryErrors.length > 0) {
1088
+ throw new CliError(
1089
+ "internal",
1090
+ `${failure.message} Capture recovery was incomplete: ${recoveryErrors.join("; ")}`,
1091
+ { captureFailure: { code: failure.code, details: failure.details ?? null }, recoveryErrors },
1092
+ );
1093
+ }
1094
+ throw failure;
1095
+ }
1096
+
1097
+ async #runSessionMutation<Result>(operation: () => Promise<Result>): Promise<Result> {
1098
+ try {
1099
+ return await operation();
1100
+ } catch (error) {
1101
+ if (error instanceof NonFatalCaptureCommandError) throw error;
1102
+ return await this.#settleSessionFailure(error);
1103
+ }
1104
+ }
1105
+
1106
+ #responseFrontier(): CaptureResponseFrontier {
1107
+ const manifest = this.#bundleWriter?.manifest;
1108
+ return {
1109
+ completedSegmentCount:
1110
+ manifest?.timeline.nativeClock.segments.length
1111
+ ?? this.#snapshot.completedSegmentCount,
1112
+ lastInterruption: this.#snapshot.lastInterruption,
1113
+ logicalTimeUs: manifest?.timeline.durationUs ?? this.#snapshot.logicalTimeUs,
1114
+ priorSnapshotLogicalTimeUs: this.#snapshot.logicalTimeUs,
1115
+ sessionCompletionPersisted: this.#sessionCompletionPersisted,
1116
+ sources: this.#snapshot.sources,
1117
+ };
1118
+ }
1119
+
1120
+ async #applyResponseEvent(event: CaptureEvent): Promise<void> {
1121
+ if (event.event === "segment-completed") {
1122
+ try {
1123
+ await this.#bundleWriter?.appendSegment(
1124
+ event.segment,
1125
+ event.interruption,
1126
+ );
1127
+ } catch (failure) {
1128
+ this.#failedSegmentCompletion = {
1129
+ failure,
1130
+ interruption: event.interruption,
1131
+ segment: event.segment,
1132
+ };
1133
+ this.#snapshot = {
1134
+ ...this.#snapshot,
1135
+ completedSegmentCount: this.#snapshot.completedSegmentCount + 1,
1136
+ lastInterruption:
1137
+ event.interruption ?? this.#snapshot.lastInterruption,
1138
+ logicalTimeUs: Math.max(
1139
+ this.#snapshot.logicalTimeUs,
1140
+ event.segment.clock.end.sourceTimeUs,
1141
+ ),
1142
+ updatedAt: this.#now().toISOString(),
1143
+ };
1144
+ throw failure;
1145
+ }
1146
+ this.#snapshot = {
1147
+ ...this.#snapshot,
1148
+ completedSegmentCount: this.#snapshot.completedSegmentCount + 1,
1149
+ lastInterruption:
1150
+ event.interruption ?? this.#snapshot.lastInterruption,
1151
+ logicalTimeUs: Math.max(
1152
+ this.#snapshot.logicalTimeUs,
1153
+ event.segment.clock.end.sourceTimeUs,
1154
+ ),
1155
+ updatedAt: this.#now().toISOString(),
1156
+ };
1157
+ }
1158
+ if (event.event === "session-completed") {
1159
+ if (this.#bundleWriter === undefined) {
1160
+ throw new CliError("internal", "Capture helper completed a session without an initialized recording bundle.");
1161
+ }
1162
+ await this.#bundleWriter.setState("stopped", event.durationUs);
1163
+ this.#sessionCompletionPersisted = true;
1164
+ this.#snapshot = {
1165
+ ...this.#snapshot,
1166
+ completedSegmentCount: event.segmentCount,
1167
+ logicalTimeUs: event.durationUs,
1168
+ state: "idle",
1169
+ updatedAt: this.#now().toISOString(),
1170
+ };
1171
+ }
1172
+ }
1173
+
1174
+ async #send(request: CaptureRequest): Promise<CaptureEvent> {
1175
+ const transport = await this.#ensureTransport();
1176
+ await transport.write(encodeCaptureRequest(request));
1177
+ const response: CaptureEvent[] = [];
1178
+ const sequences = CAPTURE_RESPONSE_SEQUENCES[request.command];
1179
+ const frontier = this.#responseFrontier();
1180
+ while (true) {
1181
+ const event = parseCaptureEventLine(await transport.readLine(this.#requestTimeoutMs));
1182
+ if (!("requestId" in event) || event.requestId !== request.requestId) {
1183
+ throw new CliError(
1184
+ "invalid-data",
1185
+ `Capture helper response ID ${
1186
+ "requestId" in event ? event.requestId : "<missing>"
1187
+ } did not match ${request.requestId}.`,
1188
+ );
1189
+ }
1190
+ notifyCaptureObserver(this.#onCaptureEvent, event);
1191
+ if (event.event === "error") {
1192
+ const mayFollowDeferredResumeCompletion = (
1193
+ request.command === "resume"
1194
+ && response.length === 1
1195
+ && response[0]?.event === "segment-completed"
1196
+ );
1197
+ if (response.length > 0 && !mayFollowDeferredResumeCompletion) {
1198
+ throw invalidCaptureResponse(
1199
+ request,
1200
+ [...response, event],
1201
+ "Errors may not follow completion evidence.",
1202
+ );
1203
+ }
1204
+ const completedResponse = [...response, event];
1205
+ const interruptionDisposition = validateCaptureError(
1206
+ request,
1207
+ response,
1208
+ event,
1209
+ frontier,
1210
+ );
1211
+ if (response.length > 0) {
1212
+ validateCaptureResponse(request, completedResponse, frontier);
1213
+ }
1214
+ const nominallyNonFatal = event.recoverable
1215
+ && (request.command === "resume" || event.code === "invalid-state");
1216
+ const nonFatal = nominallyNonFatal
1217
+ && interruptionDisposition !== "new-frontier";
1218
+ if (!nonFatal && event.interruption !== null) {
1219
+ this.#pendingFailureInterruption = event.interruption;
1220
+ }
1221
+ for (const acceptedEvent of response) {
1222
+ await this.#applyResponseEvent(acceptedEvent);
1223
+ }
1224
+ if (event.interruption !== null) {
1225
+ this.#snapshot = {
1226
+ ...this.#snapshot,
1227
+ lastInterruption: event.interruption,
1228
+ updatedAt: this.#now().toISOString(),
1229
+ };
1230
+ }
1231
+ if (
1232
+ event.interruption !== null
1233
+ && this.#bundleWriter !== undefined
1234
+ ) {
1235
+ const alreadyPersisted = this.#bundleWriter.manifest.interruptions
1236
+ .some(candidate =>
1237
+ sameCaptureInterruption(candidate, event.interruption)
1238
+ );
1239
+ if (nonFatal && !alreadyPersisted) {
1240
+ throw invalidCaptureResponse(
1241
+ request,
1242
+ completedResponse,
1243
+ "A recoverable error cannot introduce interruption evidence that was not persisted by completion.",
1244
+ );
1245
+ }
1246
+ if (!nonFatal) {
1247
+ await this.#bundleWriter.recordFailureInterruption(
1248
+ event.interruption,
1249
+ );
1250
+ }
1251
+ }
1252
+ if (request.command === "resume" && nonFatal && event.state === "paused") {
1253
+ if (this.#bundleWriter === undefined) {
1254
+ throw new CliError("internal", "Capture helper rejected resume without a recording bundle.");
1255
+ }
1256
+ this.#snapshot = {
1257
+ ...this.#snapshot,
1258
+ state: "paused",
1259
+ updatedAt: this.#now().toISOString(),
1260
+ };
1261
+ await this.#bundleWriter.setState("paused", this.#snapshot.logicalTimeUs);
1262
+ await writeRepositoryRecordingState(this.#artifactRoot, this.#snapshot);
1263
+ } else if (
1264
+ nonFatal
1265
+ && this.#snapshot.recordingId !== null
1266
+ ) {
1267
+ await writeRepositoryRecordingState(
1268
+ this.#artifactRoot,
1269
+ this.#snapshot,
1270
+ );
1271
+ }
1272
+ if (nonFatal) {
1273
+ throw new NonFatalCaptureCommandError(
1274
+ event.code === "invalid-state" ? "conflict" : "unavailable",
1275
+ event,
1276
+ );
1277
+ }
1278
+ throw new CliError(
1279
+ event.recoverable ? "unavailable" : "subprocess",
1280
+ `Capture helper ${event.code}: ${event.message}`,
1281
+ {
1282
+ helperCode: event.code,
1283
+ interruption: event.interruption,
1284
+ recoverable: event.recoverable,
1285
+ state: event.state,
1286
+ },
1287
+ );
1288
+ }
1289
+ response.push(event);
1290
+ const candidates = sequences.filter(sequence => isCaptureResponsePrefix(sequence, response));
1291
+ if (candidates.length === 0) {
1292
+ throw invalidCaptureResponse(request, response);
1293
+ }
1294
+ const complete = candidates.find(sequence => sequence.length === response.length);
1295
+ if (complete !== undefined) {
1296
+ validateCaptureResponse(request, response, frontier);
1297
+ for (const acceptedEvent of response) {
1298
+ await this.#applyResponseEvent(acceptedEvent);
1299
+ }
1300
+ return event;
1301
+ }
1302
+ }
1303
+ }
1304
+
1305
+ async start(options: RecordingStartOptions): Promise<RecordingSnapshot> {
1306
+ if (this.#snapshot.state !== "idle") {
1307
+ throw new CliError("conflict", `Cannot start while recording is ${this.#snapshot.state}.`);
1308
+ }
1309
+ const recordingId = `rec_${this.#now().toISOString().replaceAll(/[-:.TZ]/gu, "")}_${randomUUID().slice(0, 8)}`;
1310
+ const recordingRoot = join(this.#artifactRoot, recordingId);
1311
+ this.#bundleWriter = undefined;
1312
+ this.#effectiveConfig = null;
1313
+ this.#failedSegmentCompletion = undefined;
1314
+ this.#pendingFailureInterruption = undefined;
1315
+ this.#sessionCompletionPersisted = false;
1316
+ await ensurePrivateDirectory(this.#artifactRoot);
1317
+ await mkdir(recordingRoot, { mode: 0o700, recursive: false });
1318
+ const effectiveConfig: EffectiveRecordingConfig = { ...options, metadata: true };
1319
+ const captureOptions: CaptureOptions = {
1320
+ camera: { ...options.camera },
1321
+ displays: options.displays.kind === "all"
1322
+ ? { kind: "all" }
1323
+ : { displayIds: [...options.displays.displayIds], kind: "selected" },
1324
+ excludedBundleIdentifiers: ["com.hraness.slopcamera"],
1325
+ interactionEventProcessIdentifier:
1326
+ options.interactionEventProcessIdentifier ?? null,
1327
+ metadata: true,
1328
+ microphone: { ...options.microphone },
1329
+ strictSources: options.strictInputs,
1330
+ systemAudio: options.systemAudio,
1331
+ typedText: options.typedText,
1332
+ typedTextFocusIdentities:
1333
+ options.typedTextFocusIdentities === undefined
1334
+ || options.typedTextFocusIdentities === null
1335
+ ? null
1336
+ : options.typedTextFocusIdentities.map(identity => ({ ...identity })),
1337
+ };
1338
+ try {
1339
+ const configured = await this.#send({
1340
+ command: "configure",
1341
+ options: captureOptions,
1342
+ protocolVersion: CAPTURE_PROTOCOL_VERSION,
1343
+ requestId: this.#requestId("configure"),
1344
+ sessionDirectory: recordingRoot,
1345
+ });
1346
+ if (configured.event !== "configured") {
1347
+ throw new CliError("invalid-data", "Capture helper did not acknowledge configuration.");
1348
+ }
1349
+ this.#effectiveConfig = effectiveConfig;
1350
+ this.#bundleWriter = new CaptureBundleWriter({
1351
+ bundleRoot: recordingRoot,
1352
+ captureOptions,
1353
+ configured,
1354
+ helperVersion: this.#helperVersion,
1355
+ now: this.#now,
1356
+ recordingId,
1357
+ toolVersion: "3.2.4",
1358
+ verifier: this.#verifier,
1359
+ });
1360
+ await this.#bundleWriter.initialize();
1361
+ this.#snapshot = {
1362
+ availableSources: configured.availableSources,
1363
+ completedSegmentCount: 0,
1364
+ effectiveConfig,
1365
+ lastInterruption: configured.lastInterruption,
1366
+ logicalTimeUs: 0,
1367
+ permissions: permissionsFrom(configured),
1368
+ recordingId,
1369
+ recordingRoot,
1370
+ sources: configured.sources,
1371
+ state: "paused",
1372
+ updatedAt: this.#now().toISOString(),
1373
+ };
1374
+ const started = await this.#send({
1375
+ command: "start",
1376
+ protocolVersion: CAPTURE_PROTOCOL_VERSION,
1377
+ requestId: this.#requestId("start"),
1378
+ });
1379
+ if (started.event !== "segment-started") {
1380
+ throw new CliError("invalid-data", "Capture helper did not start a segment.");
1381
+ }
1382
+ await this.#bundleWriter.setCaptureEnvironment(
1383
+ started.permissions,
1384
+ started.sources,
1385
+ );
1386
+ this.#snapshot = {
1387
+ ...this.#snapshot,
1388
+ logicalTimeUs: started.startUs,
1389
+ permissions: permissionsFrom(started),
1390
+ sources: started.sources,
1391
+ state: "recording",
1392
+ updatedAt: this.#now().toISOString(),
1393
+ };
1394
+ await this.#bundleWriter.setState("recording", started.startUs);
1395
+ await writeRepositoryRecordingState(this.#artifactRoot, this.#snapshot);
1396
+ return this.#snapshot;
1397
+ } catch (error) {
1398
+ if (this.#bundleWriter === undefined) {
1399
+ return await this.#discardUninitializedSession(recordingRoot, error);
1400
+ }
1401
+ return await this.#settleSessionFailure(error);
1402
+ }
1403
+ }
1404
+
1405
+ async pause(): Promise<RecordingSnapshot> {
1406
+ if (this.#snapshot.state !== "recording") {
1407
+ throw new CliError("conflict", `Cannot pause while recording is ${this.#snapshot.state}.`);
1408
+ }
1409
+ return await this.#runSessionMutation(async () => {
1410
+ const completed = await this.#send({
1411
+ command: "pause",
1412
+ protocolVersion: CAPTURE_PROTOCOL_VERSION,
1413
+ requestId: this.#requestId("pause"),
1414
+ });
1415
+ if (completed.event !== "segment-completed") {
1416
+ throw new CliError("invalid-data", "Capture helper did not complete the active segment.");
1417
+ }
1418
+ this.#snapshot = { ...this.#snapshot, state: "paused", updatedAt: this.#now().toISOString() };
1419
+ await this.#bundleWriter?.setState("paused", this.#snapshot.logicalTimeUs);
1420
+ await writeRepositoryRecordingState(this.#artifactRoot, this.#snapshot);
1421
+ return this.#snapshot;
1422
+ });
1423
+ }
1424
+
1425
+ async resume(): Promise<RecordingSnapshot> {
1426
+ if (this.#snapshot.state === "idle" || this.#snapshot.recordingId === null) {
1427
+ throw new CliError("conflict", `Cannot resume while recording is ${this.#snapshot.state}.`);
1428
+ }
1429
+ return await this.#runSessionMutation(async () => {
1430
+ const started = await this.#send({
1431
+ command: "resume",
1432
+ protocolVersion: CAPTURE_PROTOCOL_VERSION,
1433
+ requestId: this.#requestId("resume"),
1434
+ });
1435
+ if (started.event !== "segment-started") {
1436
+ throw new CliError("invalid-data", "Capture helper did not resume capture.");
1437
+ }
1438
+ await this.#bundleWriter?.setCaptureEnvironment(
1439
+ started.permissions,
1440
+ started.sources,
1441
+ );
1442
+ this.#snapshot = {
1443
+ ...this.#snapshot,
1444
+ logicalTimeUs: started.startUs,
1445
+ permissions: permissionsFrom(started),
1446
+ sources: started.sources,
1447
+ state: "recording",
1448
+ updatedAt: this.#now().toISOString(),
1449
+ };
1450
+ await this.#bundleWriter?.setState("recording", this.#snapshot.logicalTimeUs);
1451
+ await writeRepositoryRecordingState(this.#artifactRoot, this.#snapshot);
1452
+ return this.#snapshot;
1453
+ });
1454
+ }
1455
+
1456
+ async status(): Promise<RecordingSnapshot> {
1457
+ if (this.#transport === undefined) {
1458
+ this.#snapshot = await readRepositoryRecordingState(this.#artifactRoot, this.#now);
1459
+ return this.#snapshot;
1460
+ }
1461
+ return await this.#runSessionMutation(async () => {
1462
+ const status = await this.#send({
1463
+ command: "status",
1464
+ protocolVersion: CAPTURE_PROTOCOL_VERSION,
1465
+ requestId: this.#requestId("status"),
1466
+ });
1467
+ if (status.event !== "status") throw new CliError("invalid-data", "Capture helper omitted status.");
1468
+ const state = status.state === "recording" ? "recording" : status.state === "paused" ? "paused" : "idle";
1469
+ if (status.state === "paused" || status.state === "stopped") {
1470
+ if (this.#bundleWriter === undefined) {
1471
+ throw new CliError("internal", `Capture helper reported ${status.state} without a recording bundle.`);
1472
+ }
1473
+ await this.#bundleWriter.setState(status.state, status.logicalTimeUs);
1474
+ }
1475
+ this.#snapshot = {
1476
+ ...this.#snapshot,
1477
+ availableSources: status.availableSources,
1478
+ completedSegmentCount: status.completedSegmentCount,
1479
+ effectiveConfig: state === "idle" ? null : this.#effectiveConfig,
1480
+ lastInterruption: status.lastInterruption,
1481
+ logicalTimeUs: status.logicalTimeUs,
1482
+ permissions: permissionsFrom(status),
1483
+ recordingId: state === "idle" ? null : this.#snapshot.recordingId,
1484
+ recordingRoot: state === "idle" ? null : this.#snapshot.recordingRoot,
1485
+ sources: status.sources,
1486
+ state,
1487
+ updatedAt: this.#now().toISOString(),
1488
+ };
1489
+ await writeRepositoryRecordingState(this.#artifactRoot, this.#snapshot);
1490
+ return this.#snapshot;
1491
+ });
1492
+ }
1493
+
1494
+ async stop(): Promise<RecordingSnapshot> {
1495
+ if (this.#snapshot.state === "idle") throw new CliError("conflict", "No recording is active.");
1496
+ return await this.#runSessionMutation(async () => {
1497
+ const completed = await this.#send({
1498
+ command: "stop",
1499
+ protocolVersion: CAPTURE_PROTOCOL_VERSION,
1500
+ requestId: this.#requestId("stop"),
1501
+ });
1502
+ if (completed.event !== "session-completed") {
1503
+ throw new CliError("invalid-data", "Capture helper did not complete the session.");
1504
+ }
1505
+ const stopped: RecordingSnapshot = {
1506
+ ...this.#snapshot,
1507
+ completedSegmentCount: completed.segmentCount,
1508
+ logicalTimeUs: completed.durationUs,
1509
+ state: "idle",
1510
+ updatedAt: this.#now().toISOString(),
1511
+ };
1512
+ this.#snapshot = stopped;
1513
+ await rm(activeStatePath(this.#artifactRoot), { force: true });
1514
+ return stopped;
1515
+ });
1516
+ }
1517
+
1518
+ async close(): Promise<void> {
1519
+ if (this.#closed) return;
1520
+ const transport = this.#transport;
1521
+ if (transport === undefined) {
1522
+ this.#closed = true;
1523
+ return;
1524
+ }
1525
+ try {
1526
+ try {
1527
+ const sessionNeedsCompletion = this.#bundleWriter !== undefined && !this.#sessionCompletionPersisted;
1528
+ const event = await this.#send({
1529
+ command: "shutdown",
1530
+ protocolVersion: CAPTURE_PROTOCOL_VERSION,
1531
+ requestId: this.#requestId("shutdown"),
1532
+ });
1533
+ if (event.event !== "shutdown") {
1534
+ throw new CliError("invalid-data", `Capture helper emitted ${event.event} while shutting down.`);
1535
+ }
1536
+ if (sessionNeedsCompletion && !this.#sessionCompletionPersisted) {
1537
+ throw new CliError("invalid-data", "Capture helper shut down without completing the active session.");
1538
+ }
1539
+ if (this.#sessionCompletionPersisted) {
1540
+ await rm(activeStatePath(this.#artifactRoot), { force: true });
1541
+ }
1542
+ } catch (error) {
1543
+ return await this.#settleSessionFailure(error);
1544
+ }
1545
+ } finally {
1546
+ this.#closed = true;
1547
+ this.#transport = undefined;
1548
+ await transport.close();
1549
+ }
1550
+ }
1551
+ }
1552
+
1553
+ export async function executeRecordingAction(
1554
+ controller: RecordingController,
1555
+ action: "start" | "pause" | "resume" | "stop" | "status",
1556
+ options?: RecordingStartOptions,
1557
+ ): Promise<RecordingSnapshot> {
1558
+ switch (action) {
1559
+ case "start":
1560
+ if (options === undefined) throw new CliError("internal", "Start options are missing.");
1561
+ return await controller.start(options);
1562
+ case "pause": return await controller.pause();
1563
+ case "resume": return await controller.resume();
1564
+ case "stop": return await controller.stop();
1565
+ case "status": return await controller.status();
1566
+ }
1567
+ }