@hybridlabor-api/aos 4.4.1 → 4.4.2-beta.1
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.
- package/.agents/skills/firecrawl/SKILL.md +149 -0
- package/.agents/skills/firecrawl/rules/install.md +82 -0
- package/.agents/skills/firecrawl/rules/security.md +26 -0
- package/.agents/skills/firecrawl-agent/SKILL.md +58 -0
- package/.agents/skills/firecrawl-build/SKILL.md +39 -0
- package/.agents/skills/firecrawl-build-interact/SKILL.md +68 -0
- package/.agents/skills/firecrawl-build-onboarding/SKILL.md +103 -0
- package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +39 -0
- package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +20 -0
- package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +17 -0
- package/.agents/skills/firecrawl-build-scrape/SKILL.md +69 -0
- package/.agents/skills/firecrawl-build-search/SKILL.md +69 -0
- package/.agents/skills/firecrawl-crawl/SKILL.md +59 -0
- package/.agents/skills/firecrawl-download/SKILL.md +70 -0
- package/.agents/skills/firecrawl-interact/SKILL.md +84 -0
- package/.agents/skills/firecrawl-map/SKILL.md +51 -0
- package/.agents/skills/firecrawl-scrape/SKILL.md +69 -0
- package/.agents/skills/firecrawl-search/SKILL.md +60 -0
- package/CLAUDE.md +573 -0
- package/README.de.md +0 -8
- package/README.md +0 -8
- package/README.pt.md +0 -8
- package/assets/brand/ao-ant-mark.svg +91 -0
- package/assets/brand/bdb-core-mark.svg +34 -0
- package/assets/brand/heimdall-guardian-mark.svg +16 -0
- package/assets/brand/memb-crystal-mark.svg +18 -0
- package/bin/aos-config.mjs +89 -0
- package/bin/aos-dashboard.mjs +608 -0
- package/bin/aos-uninstall.mjs +227 -0
- package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
- package/docs/skills_table.md +0 -3
- package/installer.js +471 -237
- package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
- package/mcps/after-effects-mcp/build/index.js +840 -0
- package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
- package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
- package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
- package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
- package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
- package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
- package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
- package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
- package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
- package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
- package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
- package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
- package/mcps/computer-use-mcp/dist/client.js +136 -0
- package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
- package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
- package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
- package/mcps/computer-use-mcp/dist/native.js +50 -0
- package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
- package/mcps/computer-use-mcp/dist/server.js +342 -0
- package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
- package/mcps/computer-use-mcp/dist/session.js +2372 -0
- package/package.json +13 -4
- package/skills/global_config/aos-project-init/SKILL.md +97 -130
- package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
- package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
- package/skills/global_config/aos-setup/SKILL.md +98 -8
- package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +21 -2
- package/skills/global_config/ask-tim/SKILL.md +2 -7
- package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
- package/bin/setup-saas.mjs +0 -792
- package/mcps/blender-mcp-server/.dockerignore +0 -19
- package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
- package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
- package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
- package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
- package/mcps/blender-mcp-server/Dockerfile +0 -19
- package/mcps/blender-mcp-server/LICENSE +0 -21
- package/mcps/blender-mcp-server/README.md +0 -409
- package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
- package/mcps/blender-mcp-server/addon/models.py +0 -223
- package/mcps/blender-mcp-server/pyproject.toml +0 -120
- package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
- package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
- package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
- package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
- package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
- package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
- package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
- package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
- package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
- package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
- package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
- package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
- package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
- package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
- package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
- package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
- package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
- package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
- package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
- package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
- package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
- package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
- package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
- package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
- package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
- package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
- package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
- package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
- package/mcps/davinci-mcp-professional/.bandit +0 -35
- package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
- package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
- package/mcps/davinci-mcp-professional/.editorconfig +0 -27
- package/mcps/davinci-mcp-professional/.gitattributes +0 -207
- package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
- package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
- package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
- package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
- package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
- package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
- package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
- package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
- package/mcps/davinci-mcp-professional/BUGS.md +0 -55
- package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
- package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
- package/mcps/davinci-mcp-professional/COPYING.md +0 -228
- package/mcps/davinci-mcp-professional/Doxyfile +0 -379
- package/mcps/davinci-mcp-professional/README.md +0 -143
- package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
- package/mcps/davinci-mcp-professional/USING.md +0 -301
- package/mcps/davinci-mcp-professional/VERSION.md +0 -13
- package/mcps/davinci-mcp-professional/build.py +0 -169
- package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
- package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
- package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
- package/mcps/davinci-mcp-professional/main.py +0 -40
- package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
- package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
- package/mcps/davinci-mcp-professional/security_audit.py +0 -379
- package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
- package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
- package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
- package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
- package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
- package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
- package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
- package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
- package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
- package/mcps/davinci-mcp-professional/test.py +0 -186
- package/mcps/davinci-mcp-professional/uv.lock +0 -2073
- package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
- package/mcps/davinci-resolve-mcp/.clinerules +0 -17
- package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
- package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
- package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
- package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
- package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
- package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
- package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
- package/mcps/davinci-resolve-mcp/LICENSE +0 -21
- package/mcps/davinci-resolve-mcp/README.md +0 -173
- package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
- package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
- package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
- package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
- package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
- package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
- package/mcps/davinci-resolve-mcp/install.py +0 -1687
- package/mcps/davinci-resolve-mcp/package.json +0 -52
- package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
- package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
- package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
- package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
- package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
- package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
- package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
- package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
- package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
- package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
- package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
- package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
- package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
- package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
- package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
- package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
- package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
- package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
- package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
- package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
- package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
- package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
- package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
- package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
- package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
- package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
- package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
- package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
- package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
- package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
- package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
- package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
- package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
- package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
- package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
- package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
- package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
- package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
- package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
- package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
- package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
- package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
- package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
- package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
- package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
- package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
- package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
- package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
- package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
- package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
- package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
- package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
- package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
- package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
- package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
- package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
- package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
- package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
- package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
- package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
- package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
- package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
- package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
- package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
- package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
- package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
- package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
- package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
- package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
- package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
- package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
- package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
- package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
- package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
- package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
- package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
- package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
- package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
- package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
- package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
- package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
- package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
- package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
- package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
- package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
- package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
- package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
- package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
- package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
- package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
- package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
- package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
- package/mcps/vectorworks-mcp/.dockerignore +0 -8
- package/mcps/vectorworks-mcp/Dockerfile +0 -28
- package/mcps/vectorworks-mcp/README.md +0 -156
- package/mcps/vectorworks-mcp/app/__init__.py +0 -2
- package/mcps/vectorworks-mcp/app/api.py +0 -52
- package/mcps/vectorworks-mcp/app/chunking.py +0 -56
- package/mcps/vectorworks-mcp/app/config.py +0 -29
- package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
- package/mcps/vectorworks-mcp/app/indexer.py +0 -106
- package/mcps/vectorworks-mcp/app/main.py +0 -33
- package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
- package/mcps/vectorworks-mcp/app/search.py +0 -54
- package/mcps/vectorworks-mcp/app/utils.py +0 -14
- package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
- package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
- package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
- package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
- package/mcps/vectorworks-mcp/design.md +0 -57
- package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
- package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
- package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
- package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
- package/mcps/vectorworks-mcp/requirements.txt +0 -10
- package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
- package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
- package/mcps/vectorworks-mcp/templates/index.html +0 -40
- package/scripts/ecosystem-health-audit.js +0 -257
- package/skills/bdb-dev-os-skill/SKILL.md +0 -49
- package/skills/bdb-dev-os-skill/package.json +0 -11
- package/skills/bdbsaastraining/SKILL.md +0 -316
- package/skills/bdbsaastraining/references/exam-pool.md +0 -313
- package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
- package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
- package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
- package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
- package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
- package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
- package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
- package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
- package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
- package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
- package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
- package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
- 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
|