@hybridlabor-api/aos 4.4.0 → 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 (306) 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/hooks/memb-inject.mjs +6 -0
  20. package/CLAUDE.md +573 -0
  21. package/README.de.md +0 -8
  22. package/README.md +0 -8
  23. package/README.pt.md +0 -8
  24. package/assets/brand/ao-ant-mark.svg +91 -0
  25. package/assets/brand/bdb-core-mark.svg +34 -0
  26. package/assets/brand/heimdall-guardian-mark.svg +16 -0
  27. package/assets/brand/memb-crystal-mark.svg +18 -0
  28. package/bin/aos-config.mjs +89 -0
  29. package/bin/aos-dashboard.mjs +608 -0
  30. package/bin/aos-uninstall.mjs +227 -0
  31. package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
  32. package/docs/skills_table.md +0 -3
  33. package/installer.js +428 -237
  34. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
  35. package/mcps/after-effects-mcp/build/index.js +840 -0
  36. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
  37. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
  38. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
  39. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
  40. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
  41. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
  42. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
  43. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
  44. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
  45. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
  46. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
  47. package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
  48. package/mcps/computer-use-mcp/dist/client.js +136 -0
  49. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
  50. package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
  51. package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
  52. package/mcps/computer-use-mcp/dist/native.js +50 -0
  53. package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
  54. package/mcps/computer-use-mcp/dist/server.js +342 -0
  55. package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
  56. package/mcps/computer-use-mcp/dist/session.js +2372 -0
  57. package/package.json +13 -4
  58. package/skills/global_config/aos-project-init/SKILL.md +97 -130
  59. package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
  60. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
  61. package/skills/global_config/aos-setup/SKILL.md +110 -11
  62. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +48 -4
  63. package/skills/global_config/ask-tim/SKILL.md +2 -7
  64. package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
  65. package/bin/setup-saas.mjs +0 -792
  66. package/mcps/blender-mcp-server/.dockerignore +0 -19
  67. package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
  68. package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
  69. package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
  70. package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
  71. package/mcps/blender-mcp-server/Dockerfile +0 -19
  72. package/mcps/blender-mcp-server/LICENSE +0 -21
  73. package/mcps/blender-mcp-server/README.md +0 -409
  74. package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
  75. package/mcps/blender-mcp-server/addon/models.py +0 -223
  76. package/mcps/blender-mcp-server/pyproject.toml +0 -120
  77. package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
  78. package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
  79. package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
  80. package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
  81. package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
  82. package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
  83. package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
  84. package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
  85. package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
  86. package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
  87. package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
  88. package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
  89. package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
  90. package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
  91. package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
  92. package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
  93. package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
  94. package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
  95. package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
  96. package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
  97. package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
  98. package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
  99. package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
  100. package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
  101. package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
  102. package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
  103. package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
  104. package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
  105. package/mcps/davinci-mcp-professional/.bandit +0 -35
  106. package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
  107. package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
  108. package/mcps/davinci-mcp-professional/.editorconfig +0 -27
  109. package/mcps/davinci-mcp-professional/.gitattributes +0 -207
  110. package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
  111. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  112. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  113. package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
  114. package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
  115. package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
  116. package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
  117. package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
  118. package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
  119. package/mcps/davinci-mcp-professional/BUGS.md +0 -55
  120. package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
  121. package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
  122. package/mcps/davinci-mcp-professional/COPYING.md +0 -228
  123. package/mcps/davinci-mcp-professional/Doxyfile +0 -379
  124. package/mcps/davinci-mcp-professional/README.md +0 -143
  125. package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
  126. package/mcps/davinci-mcp-professional/USING.md +0 -301
  127. package/mcps/davinci-mcp-professional/VERSION.md +0 -13
  128. package/mcps/davinci-mcp-professional/build.py +0 -169
  129. package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
  130. package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
  131. package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
  132. package/mcps/davinci-mcp-professional/main.py +0 -40
  133. package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
  134. package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
  135. package/mcps/davinci-mcp-professional/security_audit.py +0 -379
  136. package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
  137. package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
  138. package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
  139. package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
  140. package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
  141. package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
  142. package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
  143. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
  144. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
  145. package/mcps/davinci-mcp-professional/test.py +0 -186
  146. package/mcps/davinci-mcp-professional/uv.lock +0 -2073
  147. package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
  148. package/mcps/davinci-resolve-mcp/.clinerules +0 -17
  149. package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
  150. package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
  151. package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
  152. package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
  153. package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
  154. package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
  155. package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
  156. package/mcps/davinci-resolve-mcp/LICENSE +0 -21
  157. package/mcps/davinci-resolve-mcp/README.md +0 -173
  158. package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
  159. package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
  160. package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
  161. package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
  162. package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
  163. package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
  164. package/mcps/davinci-resolve-mcp/install.py +0 -1687
  165. package/mcps/davinci-resolve-mcp/package.json +0 -52
  166. package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
  167. package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
  168. package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
  169. package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
  170. package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
  171. package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
  172. package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
  173. package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
  174. package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
  175. package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
  176. package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
  177. package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
  178. package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
  179. package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
  180. package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
  181. package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
  182. package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
  183. package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
  184. package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
  185. package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
  186. package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
  187. package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
  188. package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
  189. package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
  190. package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
  191. package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
  192. package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
  193. package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
  194. package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
  195. package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
  196. package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
  197. package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
  198. package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
  199. package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
  200. package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
  201. package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
  202. package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
  203. package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
  204. package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
  205. package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
  206. package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
  207. package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
  208. package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
  209. package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
  210. package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
  211. package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
  212. package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
  213. package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
  214. package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
  215. package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
  216. package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
  217. package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
  218. package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
  219. package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
  220. package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
  221. package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
  222. package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
  223. package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
  224. package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
  225. package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
  226. package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
  227. package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
  228. package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
  229. package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
  230. package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
  231. package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
  232. package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
  233. package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
  234. package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
  235. package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
  236. package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
  237. package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
  238. package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
  239. package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
  240. package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
  241. package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
  242. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
  243. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
  244. package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
  245. package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
  246. package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
  247. package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
  248. package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
  249. package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
  250. package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
  251. package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
  252. package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
  253. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
  254. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
  255. package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
  256. package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
  257. package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
  258. package/mcps/vectorworks-mcp/.dockerignore +0 -8
  259. package/mcps/vectorworks-mcp/Dockerfile +0 -28
  260. package/mcps/vectorworks-mcp/README.md +0 -156
  261. package/mcps/vectorworks-mcp/app/__init__.py +0 -2
  262. package/mcps/vectorworks-mcp/app/api.py +0 -52
  263. package/mcps/vectorworks-mcp/app/chunking.py +0 -56
  264. package/mcps/vectorworks-mcp/app/config.py +0 -29
  265. package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
  266. package/mcps/vectorworks-mcp/app/indexer.py +0 -106
  267. package/mcps/vectorworks-mcp/app/main.py +0 -33
  268. package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
  269. package/mcps/vectorworks-mcp/app/search.py +0 -54
  270. package/mcps/vectorworks-mcp/app/utils.py +0 -14
  271. package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
  272. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
  273. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
  274. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
  275. package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
  276. package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
  277. package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
  278. package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
  279. package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
  280. package/mcps/vectorworks-mcp/design.md +0 -57
  281. package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
  282. package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
  283. package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
  284. package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
  285. package/mcps/vectorworks-mcp/requirements.txt +0 -10
  286. package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
  287. package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
  288. package/mcps/vectorworks-mcp/templates/index.html +0 -40
  289. package/scripts/ecosystem-health-audit.js +0 -257
  290. package/skills/bdb-dev-os-skill/SKILL.md +0 -49
  291. package/skills/bdb-dev-os-skill/package.json +0 -11
  292. package/skills/bdbsaastraining/SKILL.md +0 -316
  293. package/skills/bdbsaastraining/references/exam-pool.md +0 -313
  294. package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
  295. package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
  296. package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
  297. package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
  298. package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
  299. package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
  300. package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
  301. package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
  302. package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
  303. package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
  304. package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
  305. package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
  306. 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