@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,387 +0,0 @@
1
- """Brain-edit logging + metric capture (C6).
2
-
3
- A *brain edit* is any timeline mutation that should be measured. Two callers:
4
-
5
- 1. **Automatic** — the destructive-op hook in `src/server.py` logs a row for
6
- every destructive action call, even when the caller didn't declare a metric.
7
- Gives history without requiring deliberate intent.
8
- 2. **Declared** — a caller that knows it's making an editorial improvement
9
- (a take swap, a gap close, a pacing tighten) passes `metric`, `direction`,
10
- and `rationale` in the action params, plus computes `before_value` and
11
- `after_value` from the metric vocabulary helpers below. These rows form the
12
- measurement substrate for tuning the brain.
13
-
14
- The cross-project rollup (`brain_edits_registry.json`) is updated by
15
- `update_brain_edits_registry()` after each `log_brain_edit()`. Mirrors the
16
- existing `analysis_registry.json` pattern.
17
- """
18
-
19
- from __future__ import annotations
20
-
21
- import json
22
- import logging
23
- import os
24
- import time
25
- from typing import Any, Callable, Dict, List, Optional
26
-
27
- from src.utils import actor_identity, timeline_brain_db
28
-
29
- logger = logging.getLogger("resolve-mcp.brain-edits")
30
-
31
- # ── Metric vocabulary ────────────────────────────────────────────────────────
32
- #
33
- # Starter set. Add new metrics here; the brain (and the dashboard) consult this
34
- # registry to know how to render and aggregate them. `direction` is the
35
- # default-better direction; an `direction` override at log-time wins.
36
-
37
- METRIC_DURATION_SECONDS = "duration_seconds"
38
- METRIC_AVG_PERFORMANCE = "avg_performance_score"
39
- METRIC_CLIP_COUNT = "clip_count"
40
- METRIC_GAP_COUNT = "gap_count"
41
- METRIC_TOTAL_GAP_SECONDS = "total_gap_seconds"
42
- METRIC_REDUNDANCY_SCORE = "redundancy_score"
43
- METRIC_COLOR_INTENT = "color_intent"
44
-
45
- METRIC_REGISTRY: Dict[str, Dict[str, Any]] = {
46
- METRIC_DURATION_SECONDS: {
47
- "label": "Duration (seconds)",
48
- "unit": "s",
49
- "default_direction": "target_value",
50
- },
51
- METRIC_AVG_PERFORMANCE: {
52
- "label": "Avg performance score",
53
- "unit": "score 0-1",
54
- "default_direction": "increase",
55
- },
56
- METRIC_CLIP_COUNT: {
57
- "label": "Clip count",
58
- "unit": "clips",
59
- "default_direction": "target_value",
60
- },
61
- METRIC_GAP_COUNT: {
62
- "label": "Gap count",
63
- "unit": "gaps",
64
- "default_direction": "decrease",
65
- },
66
- METRIC_TOTAL_GAP_SECONDS: {
67
- "label": "Total gap duration (seconds)",
68
- "unit": "s",
69
- "default_direction": "decrease",
70
- },
71
- METRIC_REDUNDANCY_SCORE: {
72
- "label": "Redundancy score",
73
- "unit": "score 0-1",
74
- "default_direction": "decrease",
75
- },
76
- METRIC_COLOR_INTENT: {
77
- "label": "Color intent",
78
- "unit": "qualitative",
79
- "default_direction": "target_value",
80
- },
81
- }
82
-
83
- DIRECTIONS = {"increase", "decrease", "target_value"}
84
-
85
- # Cross-project registry filename (one level above project root)
86
- REGISTRY_FILENAME = "brain_edits_registry.json"
87
-
88
-
89
- def _now_iso() -> str:
90
- return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
91
-
92
-
93
- # ── Metric capture helpers ────────────────────────────────────────────────────
94
-
95
-
96
- def capture_timeline_duration_seconds(timeline: Any) -> Optional[float]:
97
- """Compute duration in seconds from a Resolve Timeline handle."""
98
- try:
99
- start = int(timeline.GetStartFrame())
100
- end = int(timeline.GetEndFrame())
101
- fps_setting = timeline.GetSetting("timelineFrameRate")
102
- fps = float(fps_setting) if fps_setting else 24.0
103
- if fps <= 0:
104
- return None
105
- return max(0.0, (end - start) / fps)
106
- except Exception as exc: # pragma: no cover — Resolve API surface
107
- logger.debug("capture_timeline_duration_seconds failed: %s", exc)
108
- return None
109
-
110
-
111
- def capture_timeline_clip_count(timeline: Any) -> Optional[int]:
112
- """Sum of video + audio + subtitle clips across all tracks."""
113
- try:
114
- total = 0
115
- for tt in ("video", "audio", "subtitle"):
116
- count = timeline.GetTrackCount(tt)
117
- if not count:
118
- continue
119
- for ti in range(1, int(count) + 1):
120
- items = timeline.GetItemListInTrack(tt, ti) or []
121
- total += len(items)
122
- return total
123
- except Exception as exc: # pragma: no cover
124
- logger.debug("capture_timeline_clip_count failed: %s", exc)
125
- return None
126
-
127
-
128
- def capture_timeline_gap_stats(timeline: Any, *, track_types=("video",)) -> Dict[str, Any]:
129
- """Compute gap_count and total_gap_seconds on a timeline.
130
-
131
- Single-track-per-type gap detection: walks each track in order, accumulates
132
- the empty frames between consecutive items. Multi-track overlap is ignored
133
- here (gaps are per-track).
134
- """
135
- out = {"gap_count": 0, "total_gap_frames": 0, "total_gap_seconds": None}
136
- try:
137
- fps_setting = timeline.GetSetting("timelineFrameRate")
138
- fps = float(fps_setting) if fps_setting else 24.0
139
- for tt in track_types:
140
- count = timeline.GetTrackCount(tt)
141
- if not count:
142
- continue
143
- for ti in range(1, int(count) + 1):
144
- items = timeline.GetItemListInTrack(tt, ti) or []
145
- items_sorted = sorted(items, key=lambda it: int(it.GetStart()))
146
- for prev, curr in zip(items_sorted, items_sorted[1:]):
147
- gap_frames = int(curr.GetStart()) - int(prev.GetEnd())
148
- if gap_frames > 0:
149
- out["gap_count"] += 1
150
- out["total_gap_frames"] += gap_frames
151
- if fps > 0:
152
- out["total_gap_seconds"] = round(out["total_gap_frames"] / fps, 3)
153
- except Exception as exc: # pragma: no cover
154
- logger.debug("capture_timeline_gap_stats failed: %s", exc)
155
- return out
156
-
157
-
158
- METRIC_CAPTURERS: Dict[str, Callable[[Any], Any]] = {
159
- METRIC_DURATION_SECONDS: capture_timeline_duration_seconds,
160
- METRIC_CLIP_COUNT: capture_timeline_clip_count,
161
- METRIC_GAP_COUNT: lambda tl: capture_timeline_gap_stats(tl)["gap_count"],
162
- METRIC_TOTAL_GAP_SECONDS: lambda tl: capture_timeline_gap_stats(tl)["total_gap_seconds"],
163
- }
164
-
165
-
166
- def capture_metric(metric: str, timeline: Any) -> Optional[float]:
167
- """Capture a registered metric from a live timeline. None if not capturable."""
168
- fn = METRIC_CAPTURERS.get(metric)
169
- if fn is None:
170
- return None
171
- try:
172
- value = fn(timeline)
173
- except Exception as exc: # pragma: no cover
174
- logger.debug("capture_metric(%s) failed: %s", metric, exc)
175
- return None
176
- if value is None:
177
- return None
178
- try:
179
- return float(value)
180
- except (TypeError, ValueError):
181
- return None
182
-
183
-
184
- # ── Logging ──────────────────────────────────────────────────────────────────
185
-
186
-
187
- def log_brain_edit(
188
- *,
189
- project_root: str,
190
- analysis_run_id: str,
191
- edit_type: str,
192
- tool_name: Optional[str] = None,
193
- action_name: Optional[str] = None,
194
- timeline_before: Optional[str] = None,
195
- timeline_after: Optional[str] = None,
196
- target_metric: Optional[str] = None,
197
- metric_direction: Optional[str] = None,
198
- before_value: Optional[float] = None,
199
- after_value: Optional[float] = None,
200
- rationale: Optional[str] = None,
201
- params: Optional[Dict[str, Any]] = None,
202
- result_summary: Optional[Dict[str, Any]] = None,
203
- project_name: Optional[str] = None,
204
- initiator: Optional[str] = None,
205
- ) -> Dict[str, Any]:
206
- """Persist a brain-edit row + update the cross-project registry."""
207
- if target_metric is not None and target_metric not in METRIC_REGISTRY:
208
- logger.warning("Unknown metric '%s' — logging anyway", target_metric)
209
- if metric_direction is not None and metric_direction not in DIRECTIONS:
210
- return {"success": False, "error": f"Invalid direction: {metric_direction}"}
211
-
212
- params_json = json.dumps(params, default=str) if params is not None else None
213
- result_json = json.dumps(result_summary, default=str) if result_summary is not None else None
214
- created_at = _now_iso()
215
-
216
- with timeline_brain_db.transaction(project_root) as txn:
217
- cursor = txn.execute(
218
- """
219
- INSERT INTO brain_edits(
220
- analysis_run_id, timeline_before, timeline_after, edit_type,
221
- tool_name, action_name, target_metric, metric_direction,
222
- before_value, after_value, rationale, params_json,
223
- result_summary_json, created_at, initiator, actor
224
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
225
- """,
226
- (
227
- analysis_run_id, timeline_before, timeline_after, edit_type,
228
- tool_name, action_name, target_metric, metric_direction,
229
- before_value, after_value, rationale, params_json,
230
- result_json, created_at, initiator,
231
- actor_identity.actor_string(),
232
- ),
233
- )
234
- row_id = cursor.lastrowid
235
-
236
- try:
237
- update_brain_edits_registry(
238
- project_root=project_root,
239
- project_name=project_name,
240
- summary={
241
- "row_id": row_id,
242
- "analysis_run_id": analysis_run_id,
243
- "timeline_before": timeline_before,
244
- "timeline_after": timeline_after,
245
- "edit_type": edit_type,
246
- "tool_name": tool_name,
247
- "action_name": action_name,
248
- "target_metric": target_metric,
249
- "before_value": before_value,
250
- "after_value": after_value,
251
- "delta": _compute_delta(before_value, after_value),
252
- "created_at": created_at,
253
- },
254
- )
255
- except Exception as exc:
256
- logger.warning("update_brain_edits_registry failed (non-fatal): %s", exc)
257
-
258
- return {"success": True, "row_id": row_id, "created_at": created_at}
259
-
260
-
261
- def _compute_delta(before: Optional[float], after: Optional[float]) -> Optional[float]:
262
- if before is None or after is None:
263
- return None
264
- try:
265
- return round(float(after) - float(before), 6)
266
- except (TypeError, ValueError):
267
- return None
268
-
269
-
270
- # ── Query ─────────────────────────────────────────────────────────────────────
271
-
272
-
273
- def get_brain_edit_history(
274
- *,
275
- project_root: str,
276
- timeline_name: Optional[str] = None,
277
- analysis_run_id: Optional[str] = None,
278
- limit: int = 50,
279
- ) -> List[Dict[str, Any]]:
280
- """Return brain_edit rows, optionally filtered by timeline or run, newest first."""
281
- # SQLite treats a negative LIMIT as "no limit"; clamp so a negative/huge
282
- # limit can't silently fetch the whole table (EX8).
283
- try:
284
- limit = max(1, min(1000, int(limit)))
285
- except (TypeError, ValueError):
286
- limit = 50
287
- conn = timeline_brain_db.connect(project_root)
288
- clauses: List[str] = []
289
- args: List[Any] = []
290
- if timeline_name:
291
- clauses.append("(timeline_before = ? OR timeline_after = ?)")
292
- args.extend([timeline_name, timeline_name])
293
- if analysis_run_id:
294
- clauses.append("analysis_run_id = ?")
295
- args.append(analysis_run_id)
296
- where = (" WHERE " + " AND ".join(clauses)) if clauses else ""
297
- rows = conn.execute(
298
- f"""
299
- SELECT id, analysis_run_id, timeline_before, timeline_after, edit_type,
300
- tool_name, action_name, target_metric, metric_direction,
301
- before_value, after_value, rationale, params_json,
302
- result_summary_json, created_at, rolled_back_at
303
- FROM brain_edits
304
- {where}
305
- ORDER BY id DESC
306
- LIMIT ?
307
- """,
308
- (*args, int(limit)),
309
- ).fetchall()
310
-
311
- out: List[Dict[str, Any]] = []
312
- for r in rows:
313
- d = dict(r)
314
- for key in ("params_json", "result_summary_json"):
315
- raw = d.pop(key)
316
- d[key[:-5]] = json.loads(raw) if raw else None
317
- d["delta"] = _compute_delta(d.get("before_value"), d.get("after_value"))
318
- out.append(d)
319
- return out
320
-
321
-
322
- # ── Cross-project rollup ─────────────────────────────────────────────────────
323
-
324
-
325
- def _registry_path_for(project_root: str) -> str:
326
- base_root = os.path.dirname(os.path.normpath(project_root))
327
- return os.path.join(base_root, REGISTRY_FILENAME)
328
-
329
-
330
- def update_brain_edits_registry(
331
- *,
332
- project_root: str,
333
- project_name: Optional[str],
334
- summary: Dict[str, Any],
335
- max_rows: int = 5000,
336
- ) -> Dict[str, Any]:
337
- """Append `summary` to the cross-project registry; trim oldest beyond max_rows.
338
-
339
- The registry sits one level above each project_root (the analysis "base
340
- root"), so it aggregates every project that shares that base. Mirrors how
341
- `analysis_registry.json` is positioned.
342
- """
343
- if not project_root:
344
- return {"success": False, "error": "project_root required"}
345
- path = _registry_path_for(project_root)
346
- os.makedirs(os.path.dirname(path), exist_ok=True)
347
-
348
- payload: Dict[str, Any]
349
- if os.path.isfile(path):
350
- try:
351
- with open(path, "r") as fh:
352
- payload = json.load(fh)
353
- except (OSError, json.JSONDecodeError):
354
- payload = {"entries": []}
355
- else:
356
- payload = {"entries": []}
357
-
358
- entries = payload.setdefault("entries", [])
359
- entry = {
360
- "project_root": project_root,
361
- "project_name": project_name or os.path.basename(project_root.rstrip("/")),
362
- **summary,
363
- }
364
- entries.append(entry)
365
- if len(entries) > max_rows:
366
- del entries[: len(entries) - max_rows]
367
- payload["entries"] = entries
368
- payload["updated_at"] = _now_iso()
369
-
370
- tmp_path = path + ".tmp"
371
- with open(tmp_path, "w") as fh:
372
- json.dump(payload, fh, indent=2, default=str)
373
- os.replace(tmp_path, path)
374
- return {"success": True, "registry_path": path, "entry_count": len(entries)}
375
-
376
-
377
- def read_brain_edits_registry(project_root: str) -> Dict[str, Any]:
378
- path = _registry_path_for(project_root)
379
- if not os.path.isfile(path):
380
- return {"entries": [], "registry_path": path}
381
- try:
382
- with open(path, "r") as fh:
383
- payload = json.load(fh)
384
- except (OSError, json.JSONDecodeError):
385
- return {"entries": [], "registry_path": path}
386
- payload["registry_path"] = path
387
- return payload
@@ -1,76 +0,0 @@
1
- """Instrumentation for counting DaVinci Resolve bridge round-trips.
2
-
3
- Every attribute access and method call on a Resolve object is a COM/socket
4
- round-trip — the dominant cost of most operations. Before optimizing (or adding
5
- a cache), you have to *measure* where the round-trips concentrate. This module
6
- provides an opt-in counting proxy that wraps a Resolve handle and tallies
7
- attribute accesses and method calls, so a hot path's real bridge cost is
8
- visible instead of guessed.
9
-
10
- This is the measurement half of the bridge-perf work. A property cache is only
11
- worth building once measurement shows a clear, repeated-read hot spot — don't
12
- cache blind.
13
-
14
- Example:
15
-
16
- counts = {}
17
- rp = CountingProxy(resolve, counts)
18
- rp.GetProjectManager().GetCurrentProject().GetName()
19
- print(counts) # {'attr_access': N, 'calls': M}
20
- """
21
- from typing import Any, Dict, Optional
22
-
23
-
24
- class CountingProxy:
25
- """Wrap a Resolve object, counting attribute accesses and method calls.
26
-
27
- Method return values that are themselves objects are wrapped too, so a whole
28
- call chain through the bridge is counted. Primitives are returned as-is.
29
- """
30
-
31
- __slots__ = ("_target", "_counter")
32
-
33
- def __init__(self, target: Any, counter: Dict[str, int]):
34
- object.__setattr__(self, "_target", target)
35
- object.__setattr__(self, "_counter", counter)
36
- counter.setdefault("attr_access", 0)
37
- counter.setdefault("calls", 0)
38
-
39
- def __getattr__(self, name: str) -> Any:
40
- counter = object.__getattribute__(self, "_counter")
41
- target = object.__getattribute__(self, "_target")
42
- counter["attr_access"] += 1
43
- attr = getattr(target, name)
44
- if callable(attr):
45
- def wrapped(*args: Any, **kwargs: Any) -> Any:
46
- counter["calls"] += 1
47
- result = attr(*args, **kwargs)
48
- return _maybe_wrap(result, counter)
49
- return wrapped
50
- return _maybe_wrap(attr, counter)
51
-
52
-
53
- _PRIMITIVE = (str, int, float, bool, bytes, type(None))
54
-
55
-
56
- def _maybe_wrap(value: Any, counter: Dict[str, int]) -> Any:
57
- if isinstance(value, _PRIMITIVE):
58
- return value
59
- if isinstance(value, (list, tuple)):
60
- # Wrap object elements so traversals (e.g. clip lists) are counted.
61
- return type(value)(_maybe_wrap(v, counter) for v in value)
62
- if isinstance(value, dict):
63
- return {k: _maybe_wrap(v, counter) for k, v in value.items()}
64
- # Likely a Resolve API object — wrap so its further use is counted.
65
- return CountingProxy(value, counter)
66
-
67
-
68
- def measure(fn, target: Any) -> Dict[str, int]:
69
- """Run ``fn(proxy)`` against a counting proxy of ``target``; return the counts.
70
-
71
- ``fn`` receives the proxy and should exercise the operation under study.
72
- """
73
- counter: Dict[str, int] = {}
74
- proxy = CountingProxy(target, counter)
75
- fn(proxy)
76
- return counter
@@ -1,20 +0,0 @@
1
- """CDL normalization helpers shared by compound and granular servers."""
2
-
3
- from typing import Any, Dict
4
-
5
-
6
- def normalize_cdl_payload(cdl: Any) -> Any:
7
- """Resolve's SetCDL expects strings like ``"1.0 1.0 1.0"`` instead of arrays."""
8
- if not isinstance(cdl, dict):
9
- return cdl
10
- out: Dict[str, Any] = {}
11
- for key, value in cdl.items():
12
- if isinstance(value, (list, tuple)):
13
- out[key] = " ".join(str(item) for item in value)
14
- elif isinstance(value, bool):
15
- out[key] = str(value)
16
- elif isinstance(value, (int, float)):
17
- out[key] = str(value)
18
- else:
19
- out[key] = value
20
- return out
@@ -1,85 +0,0 @@
1
- """Safe declarative clip-query DSL.
2
-
3
- The agent-facing way to ask "which clips match X?" without enumerating a whole
4
- timeline call-by-call. Named filters only — no arbitrary lambdas/code cross the
5
- tool boundary (the brain gets a closed vocabulary, not `eval`).
6
-
7
- Pure: `filter_clips` takes a list of plain clip dicts + a filter dict and returns
8
- the matching subset. The live MCP adapter gathers clip dicts from the timeline
9
- and calls this.
10
-
11
- Each clip dict is expected to carry (best-effort; missing keys are tolerated):
12
- name, track_type, track_index, duration (frames), in_frame, out_frame,
13
- clip_id / media_pool_item_id, clip_hash, marker_color, analyzed (bool),
14
- has_transcription (bool), shot_type (str).
15
- """
16
- from __future__ import annotations
17
-
18
- from typing import Any, Dict, List, Tuple
19
-
20
- # Supported filter keys and a one-line description (also used to validate input
21
- # and to document the surface in the tool docstring).
22
- SUPPORTED_FILTERS: Dict[str, str] = {
23
- "track_type": "exact match: 'video' | 'audio' | 'subtitle'",
24
- "track_index": "exact 1-based track index",
25
- "name_contains": "case-insensitive substring of the clip name",
26
- "duration_lt": "duration (frames) strictly less than",
27
- "duration_gt": "duration (frames) strictly greater than",
28
- "marker_color": "exact clip marker/flag color",
29
- "shot_type": "exact analyzed shot_type",
30
- "analyzed": "bool — clip has an analysis record",
31
- "has_transcription": "bool — clip has transcription",
32
- }
33
-
34
-
35
- def validate_filters(filters: Dict[str, Any]) -> Tuple[bool, List[str]]:
36
- """Return (ok, unknown_keys). Unknown filter keys are rejected, not ignored,
37
- so a typo never silently widens the match set."""
38
- unknown = [k for k in filters if k not in SUPPORTED_FILTERS]
39
- return (not unknown, unknown)
40
-
41
-
42
- def _as_bool(value: Any) -> bool:
43
- if isinstance(value, bool):
44
- return value
45
- if isinstance(value, str):
46
- return value.strip().lower() in {"1", "true", "yes", "y"}
47
- return bool(value)
48
-
49
-
50
- def _matches(clip: Dict[str, Any], filters: Dict[str, Any]) -> bool:
51
- if "track_type" in filters and clip.get("track_type") != filters["track_type"]:
52
- return False
53
- if "track_index" in filters and clip.get("track_index") != int(filters["track_index"]):
54
- return False
55
- if "name_contains" in filters:
56
- needle = str(filters["name_contains"]).lower()
57
- if needle not in str(clip.get("name") or "").lower():
58
- return False
59
- dur = clip.get("duration")
60
- if "duration_lt" in filters:
61
- if dur is None or not (dur < float(filters["duration_lt"])):
62
- return False
63
- if "duration_gt" in filters:
64
- if dur is None or not (dur > float(filters["duration_gt"])):
65
- return False
66
- if "marker_color" in filters and clip.get("marker_color") != filters["marker_color"]:
67
- return False
68
- if "shot_type" in filters and clip.get("shot_type") != filters["shot_type"]:
69
- return False
70
- if "analyzed" in filters and bool(clip.get("analyzed")) != _as_bool(filters["analyzed"]):
71
- return False
72
- if "has_transcription" in filters and bool(clip.get("has_transcription")) != _as_bool(
73
- filters["has_transcription"]
74
- ):
75
- return False
76
- return True
77
-
78
-
79
- def filter_clips(clips: List[Dict[str, Any]], filters: Dict[str, Any]) -> List[Dict[str, Any]]:
80
- """Return the subset of `clips` matching every supplied filter (AND semantics).
81
-
82
- Empty/None filter values are skipped so callers can pass a sparse dict.
83
- """
84
- active = {k: v for k, v in (filters or {}).items() if v is not None and v != ""}
85
- return [c for c in clips if _matches(c, active)]