@hybridlabor-api/aos 4.4.1 → 4.4.2-beta.0

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 (305) hide show
  1. package/.agents/skills/firecrawl/SKILL.md +149 -0
  2. package/.agents/skills/firecrawl/rules/install.md +82 -0
  3. package/.agents/skills/firecrawl/rules/security.md +26 -0
  4. package/.agents/skills/firecrawl-agent/SKILL.md +58 -0
  5. package/.agents/skills/firecrawl-build/SKILL.md +39 -0
  6. package/.agents/skills/firecrawl-build-interact/SKILL.md +68 -0
  7. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +103 -0
  8. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +39 -0
  9. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +20 -0
  10. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +17 -0
  11. package/.agents/skills/firecrawl-build-scrape/SKILL.md +69 -0
  12. package/.agents/skills/firecrawl-build-search/SKILL.md +69 -0
  13. package/.agents/skills/firecrawl-crawl/SKILL.md +59 -0
  14. package/.agents/skills/firecrawl-download/SKILL.md +70 -0
  15. package/.agents/skills/firecrawl-interact/SKILL.md +84 -0
  16. package/.agents/skills/firecrawl-map/SKILL.md +51 -0
  17. package/.agents/skills/firecrawl-scrape/SKILL.md +69 -0
  18. package/.agents/skills/firecrawl-search/SKILL.md +60 -0
  19. package/CLAUDE.md +573 -0
  20. package/README.de.md +0 -8
  21. package/README.md +0 -8
  22. package/README.pt.md +0 -8
  23. package/assets/brand/ao-ant-mark.svg +91 -0
  24. package/assets/brand/bdb-core-mark.svg +34 -0
  25. package/assets/brand/heimdall-guardian-mark.svg +16 -0
  26. package/assets/brand/memb-crystal-mark.svg +18 -0
  27. package/bin/aos-config.mjs +89 -0
  28. package/bin/aos-dashboard.mjs +608 -0
  29. package/bin/aos-uninstall.mjs +227 -0
  30. package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
  31. package/docs/skills_table.md +0 -3
  32. package/installer.js +387 -237
  33. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
  34. package/mcps/after-effects-mcp/build/index.js +840 -0
  35. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
  36. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
  37. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
  38. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
  39. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
  40. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
  41. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
  42. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
  43. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
  44. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
  45. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
  46. package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
  47. package/mcps/computer-use-mcp/dist/client.js +136 -0
  48. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
  49. package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
  50. package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
  51. package/mcps/computer-use-mcp/dist/native.js +50 -0
  52. package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
  53. package/mcps/computer-use-mcp/dist/server.js +342 -0
  54. package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
  55. package/mcps/computer-use-mcp/dist/session.js +2372 -0
  56. package/package.json +13 -4
  57. package/skills/global_config/aos-project-init/SKILL.md +97 -130
  58. package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
  59. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
  60. package/skills/global_config/aos-setup/SKILL.md +98 -8
  61. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +21 -2
  62. package/skills/global_config/ask-tim/SKILL.md +2 -7
  63. package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
  64. package/bin/setup-saas.mjs +0 -792
  65. package/mcps/blender-mcp-server/.dockerignore +0 -19
  66. package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
  67. package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
  68. package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
  69. package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
  70. package/mcps/blender-mcp-server/Dockerfile +0 -19
  71. package/mcps/blender-mcp-server/LICENSE +0 -21
  72. package/mcps/blender-mcp-server/README.md +0 -409
  73. package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
  74. package/mcps/blender-mcp-server/addon/models.py +0 -223
  75. package/mcps/blender-mcp-server/pyproject.toml +0 -120
  76. package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
  77. package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
  78. package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
  79. package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
  80. package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
  81. package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
  82. package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
  83. package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
  84. package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
  85. package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
  86. package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
  87. package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
  88. package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
  89. package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
  90. package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
  91. package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
  92. package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
  93. package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
  94. package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
  95. package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
  96. package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
  97. package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
  98. package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
  99. package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
  100. package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
  101. package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
  102. package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
  103. package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
  104. package/mcps/davinci-mcp-professional/.bandit +0 -35
  105. package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
  106. package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
  107. package/mcps/davinci-mcp-professional/.editorconfig +0 -27
  108. package/mcps/davinci-mcp-professional/.gitattributes +0 -207
  109. package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
  110. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  111. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  112. package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
  113. package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
  114. package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
  115. package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
  116. package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
  117. package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
  118. package/mcps/davinci-mcp-professional/BUGS.md +0 -55
  119. package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
  120. package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
  121. package/mcps/davinci-mcp-professional/COPYING.md +0 -228
  122. package/mcps/davinci-mcp-professional/Doxyfile +0 -379
  123. package/mcps/davinci-mcp-professional/README.md +0 -143
  124. package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
  125. package/mcps/davinci-mcp-professional/USING.md +0 -301
  126. package/mcps/davinci-mcp-professional/VERSION.md +0 -13
  127. package/mcps/davinci-mcp-professional/build.py +0 -169
  128. package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
  129. package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
  130. package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
  131. package/mcps/davinci-mcp-professional/main.py +0 -40
  132. package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
  133. package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
  134. package/mcps/davinci-mcp-professional/security_audit.py +0 -379
  135. package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
  136. package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
  137. package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
  138. package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
  139. package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
  140. package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
  141. package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
  142. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
  143. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
  144. package/mcps/davinci-mcp-professional/test.py +0 -186
  145. package/mcps/davinci-mcp-professional/uv.lock +0 -2073
  146. package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
  147. package/mcps/davinci-resolve-mcp/.clinerules +0 -17
  148. package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
  149. package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
  150. package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
  151. package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
  152. package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
  153. package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
  154. package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
  155. package/mcps/davinci-resolve-mcp/LICENSE +0 -21
  156. package/mcps/davinci-resolve-mcp/README.md +0 -173
  157. package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
  158. package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
  159. package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
  160. package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
  161. package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
  162. package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
  163. package/mcps/davinci-resolve-mcp/install.py +0 -1687
  164. package/mcps/davinci-resolve-mcp/package.json +0 -52
  165. package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
  166. package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
  167. package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
  168. package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
  169. package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
  170. package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
  171. package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
  172. package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
  173. package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
  174. package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
  175. package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
  176. package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
  177. package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
  178. package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
  179. package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
  180. package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
  181. package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
  182. package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
  183. package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
  184. package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
  185. package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
  186. package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
  187. package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
  188. package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
  189. package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
  190. package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
  191. package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
  192. package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
  193. package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
  194. package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
  195. package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
  196. package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
  197. package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
  198. package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
  199. package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
  200. package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
  201. package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
  202. package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
  203. package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
  204. package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
  205. package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
  206. package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
  207. package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
  208. package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
  209. package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
  210. package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
  211. package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
  212. package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
  213. package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
  214. package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
  215. package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
  216. package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
  217. package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
  218. package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
  219. package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
  220. package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
  221. package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
  222. package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
  223. package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
  224. package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
  225. package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
  226. package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
  227. package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
  228. package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
  229. package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
  230. package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
  231. package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
  232. package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
  233. package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
  234. package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
  235. package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
  236. package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
  237. package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
  238. package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
  239. package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
  240. package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
  241. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
  242. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
  243. package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
  244. package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
  245. package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
  246. package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
  247. package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
  248. package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
  249. package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
  250. package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
  251. package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
  252. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
  253. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
  254. package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
  255. package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
  256. package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
  257. package/mcps/vectorworks-mcp/.dockerignore +0 -8
  258. package/mcps/vectorworks-mcp/Dockerfile +0 -28
  259. package/mcps/vectorworks-mcp/README.md +0 -156
  260. package/mcps/vectorworks-mcp/app/__init__.py +0 -2
  261. package/mcps/vectorworks-mcp/app/api.py +0 -52
  262. package/mcps/vectorworks-mcp/app/chunking.py +0 -56
  263. package/mcps/vectorworks-mcp/app/config.py +0 -29
  264. package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
  265. package/mcps/vectorworks-mcp/app/indexer.py +0 -106
  266. package/mcps/vectorworks-mcp/app/main.py +0 -33
  267. package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
  268. package/mcps/vectorworks-mcp/app/search.py +0 -54
  269. package/mcps/vectorworks-mcp/app/utils.py +0 -14
  270. package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
  271. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
  272. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
  273. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
  274. package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
  275. package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
  276. package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
  277. package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
  278. package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
  279. package/mcps/vectorworks-mcp/design.md +0 -57
  280. package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
  281. package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
  282. package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
  283. package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
  284. package/mcps/vectorworks-mcp/requirements.txt +0 -10
  285. package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
  286. package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
  287. package/mcps/vectorworks-mcp/templates/index.html +0 -40
  288. package/scripts/ecosystem-health-audit.js +0 -257
  289. package/skills/bdb-dev-os-skill/SKILL.md +0 -49
  290. package/skills/bdb-dev-os-skill/package.json +0 -11
  291. package/skills/bdbsaastraining/SKILL.md +0 -316
  292. package/skills/bdbsaastraining/references/exam-pool.md +0 -313
  293. package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
  294. package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
  295. package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
  296. package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
  297. package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
  298. package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
  299. package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
  300. package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
  301. package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
  302. package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
  303. package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
  304. package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
  305. package/skills/global_config/bdbsaashost/SKILL.md +0 -125
@@ -1,730 +0,0 @@
1
- """Timeline versioning + version-on-mutate hook (C6).
2
-
3
- Mechanics:
4
-
5
- 1. **archive_current_timeline** — duplicate the current working timeline, move the
6
- duplicate into the `Archive` bin (created on first use), record the row in
7
- `timeline_versions`. The *original* keeps its name; the *duplicate* gets a
8
- `_archived_v0N` suffix. (We don't rename the working timeline because Resolve
9
- loses playhead + UI state on rename, which is hostile to interactive work.)
10
- 2. **ensure_versioned_before_mutation** — called by the destructive-op hook in
11
- `src/server.py`. Idempotent within an `analysis_run_id`: if this run already
12
- archived the current timeline, no-op. Otherwise, archive it.
13
- 3. **list_timeline_versions** — version chain for a timeline name.
14
- 4. **rollback_to_version** — restore an archived version back to the working pool
15
- under the original name. Archives the current working state first (rollback
16
- itself is a versioned op).
17
-
18
- The Resolve API surface used here is intentionally narrow:
19
- `Project.GetCurrentTimeline`, `Project.SetCurrentTimeline`, `MediaPool.GetRootFolder`,
20
- `MediaPool.AddSubFolder`, `MediaPool.SetCurrentFolder`, `MediaPool.MoveClips`,
21
- `Timeline.DuplicateTimeline`, `Timeline.GetName`, `Timeline.GetUniqueId`,
22
- `MediaPool.ExportTimeline` (for `.drt` retention export). All of these are
23
- documented stable scripting calls.
24
- """
25
-
26
- from __future__ import annotations
27
-
28
- import logging
29
- import os
30
- import re
31
- import time
32
- import uuid
33
- from typing import Any, Dict, List, Optional, Tuple
34
-
35
- from src.utils import actor_identity, timeline_brain_db
36
-
37
- logger = logging.getLogger("resolve-mcp.timeline-versioning")
38
-
39
- ARCHIVE_BIN_NAME = "Archive"
40
- ARCHIVED_SUFFIX_PATTERN = re.compile(r"^(?P<base>.+?)(?:_archived_v(?P<v>\d{2,}))?$")
41
-
42
-
43
- def _now_iso() -> str:
44
- return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
45
-
46
-
47
- def new_analysis_run_id() -> str:
48
- """Generate a fresh analysis_run_id. Callers should pass one if they have it."""
49
- return f"run_{uuid.uuid4().hex[:12]}"
50
-
51
-
52
- # ── Bin helpers ──────────────────────────────────────────────────────────────
53
-
54
-
55
- def _find_subfolder_by_name(folder: Any, name: str) -> Optional[Any]:
56
- for sub in (folder.GetSubFolderList() or []):
57
- try:
58
- if sub.GetName() == name:
59
- return sub
60
- except Exception:
61
- continue
62
- return None
63
-
64
-
65
- def _ensure_archive_bin(media_pool: Any) -> Any:
66
- """Return the Archive folder under the media pool root, creating if missing."""
67
- root = media_pool.GetRootFolder()
68
- if root is None:
69
- raise RuntimeError("Cannot resolve media pool root folder")
70
- existing = _find_subfolder_by_name(root, ARCHIVE_BIN_NAME)
71
- if existing is not None:
72
- return existing
73
- created = media_pool.AddSubFolder(root, ARCHIVE_BIN_NAME)
74
- if created is None:
75
- raise RuntimeError(f"Failed to create '{ARCHIVE_BIN_NAME}' bin")
76
- return created
77
-
78
-
79
- def _archive_bin_path(media_pool: Any) -> str:
80
- """Human-readable path to the Archive bin (for the DB record)."""
81
- return f"Master/{ARCHIVE_BIN_NAME}"
82
-
83
-
84
- # ── Timeline lookup ──────────────────────────────────────────────────────────
85
-
86
-
87
- def _list_all_timelines(project: Any) -> List[Any]:
88
- count = project.GetTimelineCount()
89
- if count is None:
90
- return []
91
- out = []
92
- for i in range(1, int(count) + 1):
93
- tl = project.GetTimelineByIndex(i)
94
- if tl is not None:
95
- out.append(tl)
96
- return out
97
-
98
-
99
- def _find_timeline_by_name(project: Any, name: str) -> Optional[Any]:
100
- for tl in _list_all_timelines(project):
101
- try:
102
- if tl.GetName() == name:
103
- return tl
104
- except Exception:
105
- continue
106
- return None
107
-
108
-
109
- # ── Public API ───────────────────────────────────────────────────────────────
110
-
111
-
112
- def archive_current_timeline(
113
- *,
114
- resolve: Any,
115
- project: Any,
116
- project_root: str,
117
- reason: Optional[str] = None,
118
- analysis_run_id: Optional[str] = None,
119
- timeline: Any = None,
120
- initiator: Optional[str] = None,
121
- auto_save: bool = False,
122
- ) -> Dict[str, Any]:
123
- """Duplicate the current timeline and move the duplicate to the Archive bin.
124
-
125
- `timeline` defaults to the current timeline. Returns a dict with the new
126
- version number and the archived timeline's name + DB row id.
127
- """
128
- tl = timeline if timeline is not None else project.GetCurrentTimeline()
129
- if tl is None:
130
- return {"success": False, "error": "No current timeline"}
131
-
132
- media_pool = project.GetMediaPool()
133
- if media_pool is None:
134
- return {"success": False, "error": "Cannot access media pool"}
135
-
136
- working_name = tl.GetName()
137
- conn = timeline_brain_db.connect(project_root)
138
-
139
- next_version = (timeline_brain_db.latest_version(conn, working_name) or 0) + 1
140
- archived_name = f"{working_name}_archived_v{next_version:02d}"
141
-
142
- # Resolve duplication must happen on the live timeline; pin current folder
143
- # to Archive bin first so the duplicate lands there directly.
144
- archive_bin = _ensure_archive_bin(media_pool)
145
- if not media_pool.SetCurrentFolder(archive_bin):
146
- return {"success": False, "error": "Failed to set Archive as current bin"}
147
-
148
- dup = tl.DuplicateTimeline(archived_name)
149
- if dup is None:
150
- return {"success": False, "error": f"DuplicateTimeline('{archived_name}') failed"}
151
-
152
- # DuplicateTimeline drops the duplicate in the *current* folder (Archive),
153
- # but it also switches Resolve's current timeline to the duplicate. Restore
154
- # the working timeline as the active one so downstream mutation hits it.
155
- project.SetCurrentTimeline(tl)
156
-
157
- with timeline_brain_db.transaction(project_root) as txn:
158
- cursor = txn.execute(
159
- """
160
- INSERT INTO timeline_versions(
161
- timeline_name, version, created_at, analysis_run_id,
162
- archived_timeline_name, archived_bin_path, reason, initiator, actor
163
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
164
- """,
165
- (
166
- working_name,
167
- next_version,
168
- _now_iso(),
169
- analysis_run_id,
170
- archived_name,
171
- _archive_bin_path(media_pool),
172
- reason,
173
- initiator,
174
- actor_identity.actor_string(),
175
- ),
176
- )
177
- row_id = cursor.lastrowid
178
-
179
- # Structural snapshot — record every clip on the working timeline so a diff
180
- # between v04 and v05 answers "which clips changed?" not just "how did the
181
- # metric move?". This is the substrate the brain reads to learn what its
182
- # edits actually did.
183
- snapshot_count = 0
184
- try:
185
- snapshot_count = _snapshot_timeline_clip_usage(
186
- project_root=project_root,
187
- timeline=tl,
188
- timeline_name=working_name,
189
- timeline_version=next_version,
190
- analysis_run_id=analysis_run_id,
191
- )
192
- except Exception as exc:
193
- logger.warning(
194
- "structural snapshot failed for '%s' v%d (non-fatal): %s",
195
- working_name, next_version, exc,
196
- )
197
-
198
- # Thumbnail capture — visually identifies a version in the History view
199
- # without having to switch into Resolve. Cheap (Resolve's already
200
- # generating thumbnails), and the visual diff is the most-loved UX.
201
- thumbnail_path: Optional[str] = None
202
- try:
203
- thumbnail_path = _capture_version_thumbnail(
204
- project_root=project_root,
205
- timeline=tl,
206
- timeline_name=working_name,
207
- timeline_version=next_version,
208
- )
209
- if thumbnail_path:
210
- with timeline_brain_db.transaction(project_root) as txn:
211
- txn.execute(
212
- "UPDATE timeline_versions SET thumbnail_path = ? WHERE id = ?",
213
- (thumbnail_path, row_id),
214
- )
215
- except Exception as exc:
216
- logger.warning("thumbnail capture failed for v%d (non-fatal): %s", next_version, exc)
217
-
218
- saved = False
219
- if auto_save:
220
- try:
221
- saved = bool(project.SaveProject())
222
- except Exception as exc:
223
- logger.warning("auto-save after archive failed (non-fatal): %s", exc)
224
-
225
- logger.info(
226
- "Archived timeline '%s' as '%s' (v%d, run=%s, snapshot=%d clips, saved=%s, reason=%s)",
227
- working_name, archived_name, next_version, analysis_run_id, snapshot_count, saved, reason,
228
- )
229
-
230
- return {
231
- "success": True,
232
- "timeline_name": working_name,
233
- "archived_timeline_name": archived_name,
234
- "version": next_version,
235
- "archive_bin": ARCHIVE_BIN_NAME,
236
- "row_id": row_id,
237
- "analysis_run_id": analysis_run_id,
238
- "snapshot_clip_count": snapshot_count,
239
- "project_saved": saved,
240
- }
241
-
242
-
243
- # ── Structural snapshot ──────────────────────────────────────────────────────
244
-
245
-
246
- def _resolve_media_pool_item_id(item: Any) -> Optional[str]:
247
- """Best-effort extraction of the media-pool source ID from a timeline item.
248
-
249
- Resolve exposes `GetMediaPoolItem()` on most timeline-item handles; fall
250
- back to GetUniqueId() if the source isn't reachable (e.g. compound clip).
251
- """
252
- try:
253
- mpi = item.GetMediaPoolItem()
254
- if mpi is not None and hasattr(mpi, "GetUniqueId"):
255
- return str(mpi.GetUniqueId())
256
- except Exception:
257
- pass
258
- try:
259
- return str(item.GetUniqueId()) if hasattr(item, "GetUniqueId") else None
260
- except Exception:
261
- return None
262
-
263
-
264
- def capture_timeline_clip_usage(timeline: Any) -> List[Dict[str, Any]]:
265
- """Walk every track/item on a LIVE timeline into structural-usage rows
266
- (no DB writes). Shared by the version snapshot writer and the cross-name
267
- live diff."""
268
- rows: List[Dict[str, Any]] = []
269
- for tt in ("video", "audio", "subtitle"):
270
- try:
271
- count = timeline.GetTrackCount(tt)
272
- except Exception:
273
- continue
274
- if not count:
275
- continue
276
- for ti in range(1, int(count) + 1):
277
- try:
278
- items = timeline.GetItemListInTrack(tt, ti) or []
279
- except Exception:
280
- continue
281
- for item in items:
282
- mpi_id = _resolve_media_pool_item_id(item)
283
- if not mpi_id:
284
- continue
285
- try:
286
- in_frame = int(item.GetStart())
287
- out_frame = int(item.GetEnd())
288
- except Exception:
289
- continue
290
- rows.append({
291
- "media_pool_item_id": mpi_id,
292
- "track_type": tt,
293
- "track_index": ti,
294
- "in_frame": in_frame,
295
- "out_frame": out_frame,
296
- })
297
- return rows
298
-
299
-
300
- def _snapshot_timeline_clip_usage(
301
- *,
302
- project_root: str,
303
- timeline: Any,
304
- timeline_name: str,
305
- timeline_version: int,
306
- analysis_run_id: Optional[str],
307
- ) -> int:
308
- """Walk every track/item on the timeline and INSERT a row per clip.
309
-
310
- Returns the number of rows written. Safe to call repeatedly for the same
311
- (timeline, version) — version is part of the row, not unique, so diffs
312
- between two versions are a SQL JOIN on media_pool_item_id.
313
- """
314
- observed_at = _now_iso()
315
- rows: List[Tuple[str, str, int, str, int, int, int, Optional[str], str]] = [
316
- (
317
- usage["media_pool_item_id"], timeline_name, timeline_version,
318
- usage["track_type"], usage["track_index"],
319
- usage["in_frame"], usage["out_frame"], analysis_run_id, observed_at,
320
- )
321
- for usage in capture_timeline_clip_usage(timeline)
322
- ]
323
-
324
- if not rows:
325
- return 0
326
-
327
- with timeline_brain_db.transaction(project_root) as txn:
328
- txn.executemany(
329
- """
330
- INSERT INTO timeline_clip_usage(
331
- media_pool_item_id, timeline_name, timeline_version,
332
- track_type, track_index, in_frame, out_frame,
333
- analysis_run_id_at_placement, observed_at
334
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
335
- """,
336
- rows,
337
- )
338
- return len(rows)
339
-
340
-
341
- def diff_versions(
342
- *,
343
- project_root: str,
344
- timeline_name: str,
345
- from_version: int,
346
- to_version: int,
347
- ) -> Dict[str, Any]:
348
- """Compare two structural snapshots — which clips were added, removed, moved?
349
-
350
- Returns {added: [...], removed: [...], moved: [...]} where each entry is
351
- (media_pool_item_id, track_type, track_index, in_frame, out_frame).
352
- """
353
- conn = timeline_brain_db.connect(project_root)
354
-
355
- def _snapshot(version: int) -> List[Dict[str, Any]]:
356
- rows = conn.execute(
357
- """
358
- SELECT media_pool_item_id, track_type, track_index, in_frame, out_frame
359
- FROM timeline_clip_usage
360
- WHERE timeline_name = ? AND timeline_version = ?
361
- ORDER BY track_type, track_index, in_frame
362
- """,
363
- (timeline_name, version),
364
- ).fetchall()
365
- return [dict(r) for r in rows]
366
-
367
- before = _snapshot(from_version)
368
- after = _snapshot(to_version)
369
- return {
370
- "from_version": from_version,
371
- "to_version": to_version,
372
- **compare_usage_snapshots(before, after),
373
- }
374
-
375
-
376
- def compare_usage_snapshots(
377
- before: List[Dict[str, Any]], after: List[Dict[str, Any]],
378
- ) -> Dict[str, Any]:
379
- """added/removed/moved/trimmed between two structural-usage snapshots.
380
-
381
- Position key: (media id, track type, track index, in_frame). Same key in
382
- both snapshots = same placement; a differing out_frame on that same key is
383
- a *trim*. A differing track/in_frame for the same media id is a *move*.
384
- """
385
- def _key(row: Dict[str, Any]) -> Tuple[str, str, int, int]:
386
- return (row["media_pool_item_id"], row["track_type"], row["track_index"], row["in_frame"])
387
-
388
- before_by_key = {_key(r): r for r in before}
389
- after_by_key = {_key(r): r for r in after}
390
- before_keys = set(before_by_key)
391
- after_keys = set(after_by_key)
392
-
393
- added = [r for r in after if _key(r) not in before_keys]
394
- removed = [r for r in before if _key(r) not in after_keys]
395
-
396
- # Trimmed: same placement key in both, but the out_frame differs.
397
- trimmed: List[Dict[str, Any]] = []
398
- for key in before_keys & after_keys:
399
- if before_by_key[key]["out_frame"] != after_by_key[key]["out_frame"]:
400
- row = dict(after_by_key[key])
401
- row["out_frame_before"] = before_by_key[key]["out_frame"]
402
- trimmed.append(row)
403
-
404
- # Moved: same media id present on both sides but at a different placement
405
- # (so it shows up in both `added` and `removed` above). Report it once.
406
- before_by_id: Dict[str, List[Dict[str, Any]]] = {}
407
- for r in before:
408
- before_by_id.setdefault(r["media_pool_item_id"], []).append(r)
409
- moved: List[Dict[str, Any]] = []
410
- for r in added:
411
- prevs = before_by_id.get(r["media_pool_item_id"])
412
- if prevs and any(_key(p) != _key(r) for p in prevs):
413
- moved.append(r)
414
-
415
- return {
416
- "added": added,
417
- "removed": removed,
418
- "moved": moved,
419
- "trimmed": trimmed,
420
- "summary": {
421
- "added": len(added),
422
- "removed": len(removed),
423
- "moved": len(moved),
424
- "trimmed": len(trimmed),
425
- "before_clip_count": len(before),
426
- "after_clip_count": len(after),
427
- },
428
- }
429
-
430
-
431
- def diff_timelines(
432
- *,
433
- project: Any,
434
- from_timeline: str,
435
- to_timeline: str,
436
- ) -> Dict[str, Any]:
437
- """Structural diff between two LIVE timelines by name (read-only).
438
-
439
- Unlike diff_versions this needs no archived snapshots and works across
440
- timeline NAMES — built for edit-engine variants (tighten/selects produce
441
- new-name timelines that have no shared version chain with their source).
442
- For moved/trimmed to mean anything the timelines should share source
443
- clips; for unrelated timelines everything reports as added/removed.
444
- """
445
- from_tl = _find_timeline_by_name(project, from_timeline)
446
- if from_tl is None:
447
- return {"success": False, "error": f"Timeline '{from_timeline}' not found"}
448
- to_tl = _find_timeline_by_name(project, to_timeline)
449
- if to_tl is None:
450
- return {"success": False, "error": f"Timeline '{to_timeline}' not found"}
451
- before = capture_timeline_clip_usage(from_tl)
452
- after = capture_timeline_clip_usage(to_tl)
453
- return {
454
- "success": True,
455
- "from_timeline": from_timeline,
456
- "to_timeline": to_timeline,
457
- **compare_usage_snapshots(before, after),
458
- }
459
-
460
-
461
- def ensure_versioned_before_mutation(
462
- *,
463
- resolve: Any,
464
- project: Any,
465
- project_root: str,
466
- analysis_run_id: str,
467
- reason: Optional[str] = None,
468
- initiator: Optional[str] = None,
469
- auto_save: bool = False,
470
- ) -> Dict[str, Any]:
471
- """Idempotent guard called before any destructive timeline op.
472
-
473
- If this `analysis_run_id` has not yet archived the current timeline, archive
474
- it. If it has, no-op. Returns a dict with `archived: bool` and the version
475
- info if an archive happened.
476
- """
477
- tl = project.GetCurrentTimeline()
478
- if tl is None:
479
- return {"archived": False, "skipped_reason": "no_current_timeline"}
480
-
481
- working_name = tl.GetName()
482
- conn = timeline_brain_db.connect(project_root)
483
- if timeline_brain_db.run_archived_for_run(conn, working_name, analysis_run_id):
484
- return {"archived": False, "skipped_reason": "already_archived_for_run"}
485
-
486
- return {
487
- "archived": True,
488
- **archive_current_timeline(
489
- resolve=resolve,
490
- project=project,
491
- project_root=project_root,
492
- reason=reason or f"version-on-mutate ({analysis_run_id})",
493
- analysis_run_id=analysis_run_id,
494
- timeline=tl,
495
- initiator=initiator,
496
- auto_save=auto_save,
497
- ),
498
- }
499
-
500
-
501
- def list_timeline_versions(
502
- *,
503
- project_root: str,
504
- timeline_name: str,
505
- ) -> List[Dict[str, Any]]:
506
- """Version chain for `timeline_name`, oldest first."""
507
- conn = timeline_brain_db.connect(project_root)
508
- rows = conn.execute(
509
- """
510
- SELECT id, timeline_name, version, created_at, analysis_run_id,
511
- archived_timeline_name, archived_bin_path, drt_export_path, reason
512
- FROM timeline_versions
513
- WHERE timeline_name = ?
514
- ORDER BY version ASC
515
- """,
516
- (timeline_name,),
517
- ).fetchall()
518
- return [dict(r) for r in rows]
519
-
520
-
521
- def rollback_to_version(
522
- *,
523
- resolve: Any,
524
- project: Any,
525
- project_root: str,
526
- timeline_name: str,
527
- version: int,
528
- analysis_run_id: Optional[str] = None,
529
- ) -> Dict[str, Any]:
530
- """Restore `timeline_name` to an archived `version`.
531
-
532
- Steps:
533
- 1. Archive the current working state (rollback is itself a versioned op).
534
- 2. Locate the archived timeline (by name in the Archive bin OR re-import from
535
- `.drt` if it was retention-collapsed).
536
- 3. Duplicate the archived timeline back to the project root, name it
537
- `<timeline_name>_rolled_back_<HHMMSS>` to avoid collision. The caller can
538
- rename via `timeline.set_name` if desired.
539
- """
540
- run = analysis_run_id or new_analysis_run_id()
541
- archive_result = ensure_versioned_before_mutation(
542
- resolve=resolve,
543
- project=project,
544
- project_root=project_root,
545
- analysis_run_id=run,
546
- reason=f"pre-rollback to v{version:02d}",
547
- )
548
-
549
- conn = timeline_brain_db.connect(project_root)
550
- row = conn.execute(
551
- """
552
- SELECT archived_timeline_name, drt_export_path
553
- FROM timeline_versions
554
- WHERE timeline_name = ? AND version = ?
555
- """,
556
- (timeline_name, version),
557
- ).fetchone()
558
- if row is None:
559
- return {"success": False, "error": f"No version v{version} for '{timeline_name}'"}
560
-
561
- archived_name = row["archived_timeline_name"]
562
- drt_path = row["drt_export_path"]
563
-
564
- media_pool = project.GetMediaPool()
565
- if media_pool is None:
566
- return {"success": False, "error": "Cannot access media pool"}
567
-
568
- archived_tl = _find_timeline_by_name(project, archived_name)
569
- if archived_tl is None and drt_path and os.path.isfile(drt_path):
570
- # Retention-collapsed: re-import the .drt back into the project.
571
- imported = media_pool.ImportTimelineFromFile(drt_path)
572
- if imported is None:
573
- return {"success": False, "error": f"Failed to re-import {drt_path}"}
574
- archived_tl = imported
575
- if archived_tl is None:
576
- return {"success": False, "error": f"Archived timeline '{archived_name}' not found"}
577
-
578
- timestamp = time.strftime("%H%M%S", time.gmtime())
579
- restored_name = f"{timeline_name}_rolled_back_{timestamp}"
580
- restored = archived_tl.DuplicateTimeline(restored_name)
581
- if restored is None:
582
- return {"success": False, "error": f"DuplicateTimeline('{restored_name}') failed"}
583
-
584
- project.SetCurrentTimeline(restored)
585
-
586
- return {
587
- "success": True,
588
- "rolled_back_from_version": version,
589
- "restored_timeline_name": restored_name,
590
- "archive_of_previous": archive_result,
591
- "analysis_run_id": run,
592
- }
593
-
594
-
595
- # ── Retention ─────────────────────────────────────────────────────────────────
596
-
597
-
598
- def prune_archived_versions(
599
- *,
600
- resolve: Any,
601
- project: Any,
602
- project_root: str,
603
- timeline_name: str,
604
- keep_n: int = 10,
605
- ) -> Dict[str, Any]:
606
- """Keep the most recent `keep_n` archived versions live in the Archive bin.
607
-
608
- Older versions are exported to `<project>/_soul/timeline_versions/<timeline>/`
609
- as `.drt` files and then removed from the bin. The DB row is preserved and
610
- its `drt_export_path` is populated, so rollback can re-import.
611
- """
612
- if keep_n < 1:
613
- keep_n = 1
614
-
615
- conn = timeline_brain_db.connect(project_root)
616
- rows = conn.execute(
617
- """
618
- SELECT id, version, archived_timeline_name, drt_export_path
619
- FROM timeline_versions
620
- WHERE timeline_name = ?
621
- ORDER BY version DESC
622
- """,
623
- (timeline_name,),
624
- ).fetchall()
625
-
626
- if len(rows) <= keep_n:
627
- return {"success": True, "pruned": 0, "kept": len(rows)}
628
-
629
- media_pool = project.GetMediaPool()
630
- if media_pool is None:
631
- return {"success": False, "error": "Cannot access media pool"}
632
-
633
- export_dir = os.path.join(
634
- project_root, "_soul", "timeline_versions", _slugify(timeline_name)
635
- )
636
- os.makedirs(export_dir, exist_ok=True)
637
-
638
- to_collapse = list(rows[keep_n:])
639
- pruned: List[Dict[str, Any]] = []
640
- for row in to_collapse:
641
- if row["drt_export_path"]:
642
- # Already collapsed previously — nothing to do.
643
- continue
644
- archived_name = row["archived_timeline_name"]
645
- tl = _find_timeline_by_name(project, archived_name)
646
- if tl is None:
647
- logger.warning("Cannot find archived timeline '%s' to collapse", archived_name)
648
- continue
649
-
650
- export_path = os.path.join(export_dir, f"{archived_name}.drt")
651
- # ExportType.DRT is integer 1 in the Resolve API; passing the int avoids
652
- # a runtime dependency on the constant lookup.
653
- EXPORT_DRT = 1
654
- exported = tl.Export(export_path, EXPORT_DRT)
655
- if not exported:
656
- logger.warning("Export(%s) returned False", export_path)
657
- continue
658
-
659
- # Now remove the timeline from the bin. Resolve has no DeleteTimeline on
660
- # a single timeline handle; project.DeleteTimelines() expects a list.
661
- try:
662
- removed = project.DeleteTimelines([tl])
663
- except Exception as exc:
664
- logger.warning("DeleteTimelines failed for '%s': %s", archived_name, exc)
665
- removed = False
666
-
667
- with timeline_brain_db.transaction(project_root) as txn:
668
- txn.execute(
669
- "UPDATE timeline_versions SET drt_export_path = ? WHERE id = ?",
670
- (export_path, row["id"]),
671
- )
672
-
673
- pruned.append({
674
- "version": row["version"],
675
- "archived_name": archived_name,
676
- "exported_to": export_path,
677
- "deleted_from_bin": bool(removed),
678
- })
679
-
680
- return {"success": True, "pruned": len(pruned), "kept": keep_n, "details": pruned}
681
-
682
-
683
- def _slugify(value: str) -> str:
684
- return re.sub(r"[^A-Za-z0-9._-]+", "_", value).strip("_") or "timeline"
685
-
686
-
687
- # ── Thumbnail capture ───────────────────────────────────────────────────────
688
-
689
-
690
- def _capture_version_thumbnail(
691
- *,
692
- project_root: str,
693
- timeline: Any,
694
- timeline_name: str,
695
- timeline_version: int,
696
- ) -> Optional[str]:
697
- """Snapshot the current Color-page thumbnail to disk.
698
-
699
- Returns the file path, or None if Resolve can't produce a thumbnail (e.g.
700
- the playhead isn't on a clip, the Color page isn't active, or the
701
- underlying API call failed). Best-effort — never raises.
702
- """
703
- try:
704
- thumbnail_data = timeline.GetCurrentClipThumbnailImage()
705
- except Exception as exc:
706
- logger.debug("GetCurrentClipThumbnailImage raised: %s", exc)
707
- return None
708
- if not thumbnail_data:
709
- return None
710
-
711
- try:
712
- # Lazy import to avoid a circular module-load with server.py.
713
- from src.server import _thumbnail_data_to_png_bytes
714
- png_bytes = _thumbnail_data_to_png_bytes(thumbnail_data)
715
- except Exception as exc:
716
- logger.debug("thumbnail PNG conversion failed: %s", exc)
717
- return None
718
-
719
- out_dir = os.path.join(
720
- project_root, "_soul", "timeline_versions", _slugify(timeline_name),
721
- )
722
- os.makedirs(out_dir, exist_ok=True)
723
- out_path = os.path.join(out_dir, f"v{timeline_version:02d}.png")
724
- try:
725
- with open(out_path, "wb") as fh:
726
- fh.write(png_bytes)
727
- except OSError as exc:
728
- logger.debug("thumbnail write failed: %s", exc)
729
- return None
730
- return out_path