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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/.agents/skills/firecrawl/SKILL.md +149 -0
  2. package/.agents/skills/firecrawl/rules/install.md +82 -0
  3. package/.agents/skills/firecrawl/rules/security.md +26 -0
  4. package/.agents/skills/firecrawl-agent/SKILL.md +58 -0
  5. package/.agents/skills/firecrawl-build/SKILL.md +39 -0
  6. package/.agents/skills/firecrawl-build-interact/SKILL.md +68 -0
  7. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +103 -0
  8. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +39 -0
  9. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +20 -0
  10. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +17 -0
  11. package/.agents/skills/firecrawl-build-scrape/SKILL.md +69 -0
  12. package/.agents/skills/firecrawl-build-search/SKILL.md +69 -0
  13. package/.agents/skills/firecrawl-crawl/SKILL.md +59 -0
  14. package/.agents/skills/firecrawl-download/SKILL.md +70 -0
  15. package/.agents/skills/firecrawl-interact/SKILL.md +84 -0
  16. package/.agents/skills/firecrawl-map/SKILL.md +51 -0
  17. package/.agents/skills/firecrawl-scrape/SKILL.md +69 -0
  18. package/.agents/skills/firecrawl-search/SKILL.md +60 -0
  19. package/CLAUDE.md +573 -0
  20. package/README.de.md +0 -8
  21. package/README.md +0 -8
  22. package/README.pt.md +0 -8
  23. package/assets/brand/ao-ant-mark.svg +91 -0
  24. package/assets/brand/bdb-core-mark.svg +34 -0
  25. package/assets/brand/heimdall-guardian-mark.svg +16 -0
  26. package/assets/brand/memb-crystal-mark.svg +18 -0
  27. package/bin/aos-config.mjs +89 -0
  28. package/bin/aos-dashboard.mjs +608 -0
  29. package/bin/aos-uninstall.mjs +227 -0
  30. package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
  31. package/docs/skills_table.md +0 -3
  32. package/installer.js +387 -237
  33. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
  34. package/mcps/after-effects-mcp/build/index.js +840 -0
  35. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
  36. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
  37. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
  38. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
  39. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
  40. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
  41. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
  42. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
  43. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
  44. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
  45. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
  46. package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
  47. package/mcps/computer-use-mcp/dist/client.js +136 -0
  48. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
  49. package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
  50. package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
  51. package/mcps/computer-use-mcp/dist/native.js +50 -0
  52. package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
  53. package/mcps/computer-use-mcp/dist/server.js +342 -0
  54. package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
  55. package/mcps/computer-use-mcp/dist/session.js +2372 -0
  56. package/package.json +13 -4
  57. package/skills/global_config/aos-project-init/SKILL.md +97 -130
  58. package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
  59. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
  60. package/skills/global_config/aos-setup/SKILL.md +98 -8
  61. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +21 -2
  62. package/skills/global_config/ask-tim/SKILL.md +2 -7
  63. package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
  64. package/bin/setup-saas.mjs +0 -792
  65. package/mcps/blender-mcp-server/.dockerignore +0 -19
  66. package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
  67. package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
  68. package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
  69. package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
  70. package/mcps/blender-mcp-server/Dockerfile +0 -19
  71. package/mcps/blender-mcp-server/LICENSE +0 -21
  72. package/mcps/blender-mcp-server/README.md +0 -409
  73. package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
  74. package/mcps/blender-mcp-server/addon/models.py +0 -223
  75. package/mcps/blender-mcp-server/pyproject.toml +0 -120
  76. package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
  77. package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
  78. package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
  79. package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
  80. package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
  81. package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
  82. package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
  83. package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
  84. package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
  85. package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
  86. package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
  87. package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
  88. package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
  89. package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
  90. package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
  91. package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
  92. package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
  93. package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
  94. package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
  95. package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
  96. package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
  97. package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
  98. package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
  99. package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
  100. package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
  101. package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
  102. package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
  103. package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
  104. package/mcps/davinci-mcp-professional/.bandit +0 -35
  105. package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
  106. package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
  107. package/mcps/davinci-mcp-professional/.editorconfig +0 -27
  108. package/mcps/davinci-mcp-professional/.gitattributes +0 -207
  109. package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
  110. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  111. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  112. package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
  113. package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
  114. package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
  115. package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
  116. package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
  117. package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
  118. package/mcps/davinci-mcp-professional/BUGS.md +0 -55
  119. package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
  120. package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
  121. package/mcps/davinci-mcp-professional/COPYING.md +0 -228
  122. package/mcps/davinci-mcp-professional/Doxyfile +0 -379
  123. package/mcps/davinci-mcp-professional/README.md +0 -143
  124. package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
  125. package/mcps/davinci-mcp-professional/USING.md +0 -301
  126. package/mcps/davinci-mcp-professional/VERSION.md +0 -13
  127. package/mcps/davinci-mcp-professional/build.py +0 -169
  128. package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
  129. package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
  130. package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
  131. package/mcps/davinci-mcp-professional/main.py +0 -40
  132. package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
  133. package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
  134. package/mcps/davinci-mcp-professional/security_audit.py +0 -379
  135. package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
  136. package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
  137. package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
  138. package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
  139. package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
  140. package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
  141. package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
  142. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
  143. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
  144. package/mcps/davinci-mcp-professional/test.py +0 -186
  145. package/mcps/davinci-mcp-professional/uv.lock +0 -2073
  146. package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
  147. package/mcps/davinci-resolve-mcp/.clinerules +0 -17
  148. package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
  149. package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
  150. package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
  151. package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
  152. package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
  153. package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
  154. package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
  155. package/mcps/davinci-resolve-mcp/LICENSE +0 -21
  156. package/mcps/davinci-resolve-mcp/README.md +0 -173
  157. package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
  158. package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
  159. package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
  160. package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
  161. package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
  162. package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
  163. package/mcps/davinci-resolve-mcp/install.py +0 -1687
  164. package/mcps/davinci-resolve-mcp/package.json +0 -52
  165. package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
  166. package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
  167. package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
  168. package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
  169. package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
  170. package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
  171. package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
  172. package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
  173. package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
  174. package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
  175. package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
  176. package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
  177. package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
  178. package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
  179. package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
  180. package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
  181. package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
  182. package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
  183. package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
  184. package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
  185. package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
  186. package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
  187. package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
  188. package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
  189. package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
  190. package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
  191. package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
  192. package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
  193. package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
  194. package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
  195. package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
  196. package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
  197. package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
  198. package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
  199. package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
  200. package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
  201. package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
  202. package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
  203. package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
  204. package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
  205. package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
  206. package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
  207. package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
  208. package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
  209. package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
  210. package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
  211. package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
  212. package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
  213. package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
  214. package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
  215. package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
  216. package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
  217. package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
  218. package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
  219. package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
  220. package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
  221. package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
  222. package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
  223. package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
  224. package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
  225. package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
  226. package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
  227. package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
  228. package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
  229. package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
  230. package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
  231. package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
  232. package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
  233. package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
  234. package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
  235. package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
  236. package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
  237. package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
  238. package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
  239. package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
  240. package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
  241. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
  242. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
  243. package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
  244. package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
  245. package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
  246. package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
  247. package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
  248. package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
  249. package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
  250. package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
  251. package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
  252. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
  253. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
  254. package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
  255. package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
  256. package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
  257. package/mcps/vectorworks-mcp/.dockerignore +0 -8
  258. package/mcps/vectorworks-mcp/Dockerfile +0 -28
  259. package/mcps/vectorworks-mcp/README.md +0 -156
  260. package/mcps/vectorworks-mcp/app/__init__.py +0 -2
  261. package/mcps/vectorworks-mcp/app/api.py +0 -52
  262. package/mcps/vectorworks-mcp/app/chunking.py +0 -56
  263. package/mcps/vectorworks-mcp/app/config.py +0 -29
  264. package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
  265. package/mcps/vectorworks-mcp/app/indexer.py +0 -106
  266. package/mcps/vectorworks-mcp/app/main.py +0 -33
  267. package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
  268. package/mcps/vectorworks-mcp/app/search.py +0 -54
  269. package/mcps/vectorworks-mcp/app/utils.py +0 -14
  270. package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
  271. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
  272. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
  273. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
  274. package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
  275. package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
  276. package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
  277. package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
  278. package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
  279. package/mcps/vectorworks-mcp/design.md +0 -57
  280. package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
  281. package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
  282. package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
  283. package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
  284. package/mcps/vectorworks-mcp/requirements.txt +0 -10
  285. package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
  286. package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
  287. package/mcps/vectorworks-mcp/templates/index.html +0 -40
  288. package/scripts/ecosystem-health-audit.js +0 -257
  289. package/skills/bdb-dev-os-skill/SKILL.md +0 -49
  290. package/skills/bdb-dev-os-skill/package.json +0 -11
  291. package/skills/bdbsaastraining/SKILL.md +0 -316
  292. package/skills/bdbsaastraining/references/exam-pool.md +0 -313
  293. package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
  294. package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
  295. package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
  296. package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
  297. package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
  298. package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
  299. package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
  300. package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
  301. package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
  302. package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
  303. package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
  304. package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
  305. package/skills/global_config/bdbsaashost/SKILL.md +0 -125
@@ -1,1687 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- DaVinci Resolve MCP Server — Universal Installer
4
-
5
- Supports: macOS, Windows, Linux
6
- Configures: Claude Desktop, Claude Code, Cursor, VS Code (Copilot),
7
- Windsurf, Cline, Roo Code, Zed, Continue, OpenCode, and manual setup.
8
-
9
- Usage:
10
- python install.py # Interactive mode
11
- python install.py --clients all # Install all clients non-interactively
12
- python install.py --clients cursor,claude-desktop --no-venv
13
- python install.py --help
14
- """
15
-
16
- import argparse
17
- import json
18
- import os
19
- import platform
20
- import shutil
21
- import subprocess
22
- import sys
23
- import textwrap
24
- import time
25
- from pathlib import Path
26
-
27
- from src.utils.update_check import (
28
- clear_update_prompt_preferences,
29
- check_for_updates,
30
- ignore_update_version,
31
- set_update_mode,
32
- snooze_update_prompt,
33
- update_prompt_decision,
34
- )
35
-
36
- # ─── Version ──────────────────────────────────────────────────────────────────
37
-
38
- VERSION = "2.57.5"
39
- # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
40
- # Resolve's scripting bridge loads into newer interpreters on recent builds
41
- # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
42
- # may fail to connect on 3.13+, but the connection check is the real signal,
43
- # so we proceed with a heads-up rather than refusing to run.
44
- SUPPORTED_PYTHON_MIN = (3, 10)
45
- PYTHON_ABI_RISK_MIN = (3, 13)
46
-
47
- # ─── Colors (disabled on Windows cmd without ANSI support) ────────────────────
48
-
49
- def _supports_color():
50
- if os.environ.get("NO_COLOR"):
51
- return False
52
- if platform.system() == "Windows":
53
- return os.environ.get("WT_SESSION") or os.environ.get("TERM_PROGRAM")
54
- return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
55
-
56
- USE_COLOR = _supports_color()
57
-
58
- def _c(code, text):
59
- return f"\033[{code}m{text}\033[0m" if USE_COLOR else text
60
-
61
- def green(t): return _c("32", t)
62
- def yellow(t): return _c("33", t)
63
- def red(t): return _c("31", t)
64
- def bold(t): return _c("1", t)
65
- def dim(t): return _c("2", t)
66
- def cyan(t): return _c("36", t)
67
-
68
- # ─── Platform Detection ──────────────────────────────────────────────────────
69
-
70
- SYSTEM = platform.system() # Darwin, Windows, Linux
71
-
72
- def is_mac(): return SYSTEM == "Darwin"
73
- def is_windows(): return SYSTEM == "Windows"
74
- def is_linux(): return SYSTEM == "Linux"
75
-
76
- def platform_name():
77
- if is_mac(): return "macOS"
78
- if is_windows(): return "Windows"
79
- if is_linux(): return "Linux"
80
- return SYSTEM
81
-
82
-
83
- def is_supported_python_version(version):
84
- major, minor = version[:2]
85
- return major == 3 and minor >= SUPPORTED_PYTHON_MIN[1]
86
-
87
-
88
- def is_abi_risk_python_version(version):
89
- major, minor = version[:2]
90
- return major == 3 and minor >= PYTHON_ABI_RISK_MIN[1]
91
-
92
-
93
- def format_python_version(version):
94
- return ".".join(str(part) for part in version[:3])
95
-
96
-
97
- def python_requirement_text():
98
- return f"Python {SUPPORTED_PYTHON_MIN[0]}.{SUPPORTED_PYTHON_MIN[1]} or newer"
99
-
100
-
101
- _ABI_NOTE_PRINTED = False
102
-
103
-
104
- def print_abi_risk_note_once(version, label="Python"):
105
- """Emit the 3.13+ heads-up at most once per installer run."""
106
- global _ABI_NOTE_PRINTED
107
- if _ABI_NOTE_PRINTED:
108
- return
109
- _ABI_NOTE_PRINTED = True
110
- print(f" {yellow(label + ':')} {python_abi_risk_note(version)}")
111
-
112
-
113
- def python_abi_risk_note(version):
114
- return (
115
- f"Using Python {format_python_version(version)}. Verified working on recent "
116
- f"Resolve builds (Studio 20.3.2). If Resolve fails to connect "
117
- f"(scriptapp(\"Resolve\") returns None), install Python 3.10-3.12 and re-run "
118
- f"with it, e.g.: python3.12 install.py"
119
- )
120
-
121
-
122
- def python_fix_hint():
123
- return (
124
- " How to fix:\n"
125
- " - Install Python 3.12 (the lowest-risk version for Resolve), e.g.:\n"
126
- " macOS: brew install python@3.12 (or: pyenv install 3.12)\n"
127
- " Linux: pyenv install 3.12 (or your distro's python3.12 package)\n"
128
- " Windows: install Python 3.12 from python.org\n"
129
- " - Re-run with that interpreter, e.g.: python3.12 install.py"
130
- )
131
-
132
-
133
- def _version_for_python(python_path):
134
- script = (
135
- "import sys; "
136
- "print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')"
137
- )
138
- result = subprocess.run(
139
- [str(python_path), "-c", script],
140
- capture_output=True,
141
- text=True,
142
- timeout=10,
143
- )
144
- if result.returncode != 0:
145
- output = (result.stderr or result.stdout or "").strip()
146
- raise RuntimeError(output or f"{python_path} exited with code {result.returncode}")
147
- parts = result.stdout.strip().split(".")
148
- if len(parts) < 2:
149
- raise RuntimeError(f"could not parse Python version from {python_path!s}")
150
- return tuple(int(part) for part in parts[:3])
151
-
152
-
153
- def require_supported_python(python_path, label="Python"):
154
- try:
155
- version = _version_for_python(python_path)
156
- except Exception as exc:
157
- print(f" {red(label + ':')} Could not inspect {python_path}: {exc}")
158
- sys.exit(1)
159
- if not is_supported_python_version(version):
160
- print(
161
- f" {red(label + ':')} {python_requirement_text()} is required "
162
- f"(the MCP SDK needs 3.10+); found {format_python_version(version)} "
163
- f"at {python_path}"
164
- )
165
- print(python_fix_hint())
166
- sys.exit(1)
167
- if is_abi_risk_python_version(version):
168
- print_abi_risk_note_once(version, label)
169
- return version
170
-
171
-
172
- def require_current_python(label="Python"):
173
- version = (sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
174
- if not is_supported_python_version(version):
175
- print(
176
- f" {red(label + ':')} {python_requirement_text()} is required "
177
- f"(the MCP SDK needs 3.10+); current interpreter is "
178
- f"{format_python_version(version)} at {sys.executable}"
179
- )
180
- print(python_fix_hint())
181
- sys.exit(1)
182
- if is_abi_risk_python_version(version):
183
- print_abi_risk_note_once(version, label)
184
- return version
185
-
186
- # ─── Resolve Path Detection ──────────────────────────────────────────────────
187
-
188
- RESOLVE_PATHS = {
189
- "Darwin": {
190
- "api": [
191
- "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting",
192
- ],
193
- "lib": [
194
- "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so",
195
- ],
196
- "app": [
197
- "/Applications/DaVinci Resolve/DaVinci Resolve.app",
198
- ],
199
- },
200
- "Windows": {
201
- "api": [
202
- r"C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Developer\Scripting",
203
- r"C:\Program Files\Blackmagic Design\DaVinci Resolve\Developer\Scripting",
204
- ],
205
- "lib": [
206
- r"C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionscript.dll",
207
- ],
208
- "app": [
209
- r"C:\Program Files\Blackmagic Design\DaVinci Resolve\Resolve.exe",
210
- ],
211
- },
212
- "Linux": {
213
- "api": [
214
- "/opt/resolve/Developer/Scripting",
215
- "/opt/resolve/libs/Fusion/Developer/Scripting",
216
- "/home/{user}/.local/share/DaVinciResolve/Developer/Scripting",
217
- ],
218
- "lib": [
219
- "/opt/resolve/libs/Fusion/fusionscript.so",
220
- "/opt/resolve/bin/fusionscript.so",
221
- ],
222
- "app": [
223
- "/opt/resolve/bin/resolve",
224
- ],
225
- },
226
- }
227
-
228
-
229
- def find_resolve_paths():
230
- """Auto-detect DaVinci Resolve installation paths."""
231
- candidates = RESOLVE_PATHS.get(SYSTEM, RESOLVE_PATHS["Linux"])
232
- username = os.environ.get("USER", os.environ.get("USERNAME", ""))
233
-
234
- api_path = None
235
- lib_path = None
236
-
237
- for p in candidates["api"]:
238
- expanded = p.replace("{user}", username)
239
- if os.path.isdir(expanded):
240
- api_path = expanded
241
- break
242
-
243
- for p in candidates["lib"]:
244
- expanded = p.replace("{user}", username)
245
- if os.path.isfile(expanded):
246
- lib_path = expanded
247
- break
248
-
249
- return api_path, lib_path
250
-
251
-
252
- def check_resolve_running():
253
- """Check if DaVinci Resolve is currently running."""
254
- try:
255
- if is_mac():
256
- result = subprocess.run(
257
- ["pgrep", "-f", "DaVinci Resolve"],
258
- capture_output=True, text=True
259
- )
260
- return result.returncode == 0
261
- elif is_windows():
262
- result = subprocess.run(
263
- ["tasklist", "/FI", "IMAGENAME eq Resolve.exe"],
264
- capture_output=True, text=True
265
- )
266
- return "Resolve.exe" in result.stdout
267
- else: # Linux
268
- result = subprocess.run(
269
- ["pgrep", "-f", "resolve"],
270
- capture_output=True, text=True
271
- )
272
- return result.returncode == 0
273
- except Exception:
274
- return False
275
-
276
- # ─── MCP Client Definitions ──────────────────────────────────────────────────
277
-
278
- def home():
279
- return Path.home()
280
-
281
- def appdata():
282
- """Windows %APPDATA% equivalent."""
283
- return Path(os.environ.get("APPDATA", home() / "AppData" / "Roaming"))
284
-
285
- def xdg_config():
286
- """Linux XDG_CONFIG_HOME or default."""
287
- return Path(os.environ.get("XDG_CONFIG_HOME", home() / ".config"))
288
-
289
- def vscode_global_storage():
290
- """VS Code global storage path per platform."""
291
- if is_mac():
292
- return home() / "Library" / "Application Support" / "Code" / "User" / "globalStorage"
293
- elif is_windows():
294
- return appdata() / "Code" / "User" / "globalStorage"
295
- else:
296
- return xdg_config() / "Code" / "User" / "globalStorage"
297
-
298
-
299
- # Each client entry:
300
- # id, name, config_path_fn, config_key, merge_strategy, notes
301
- # config_path_fn returns the path; config_key is the JSON key wrapping the server entry
302
- # merge_strategy: "merge" = add to existing JSON; "create" = create if not exists
303
-
304
- MCP_CLIENTS = [
305
- {
306
- "id": "antigravity",
307
- "name": "Antigravity",
308
- "get_path": lambda: home() / ".gemini" / "antigravity" / "mcp_config.json",
309
- "config_key": "mcpServers",
310
- "notes": "Google's agentic AI coding assistant (VS Code fork)",
311
- },
312
- {
313
- "id": "claude-desktop",
314
- "name": "Claude Desktop",
315
- "get_path": lambda: {
316
- "Darwin": home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json",
317
- "Windows": appdata() / "Claude" / "claude_desktop_config.json",
318
- "Linux": xdg_config() / "Claude" / "claude_desktop_config.json",
319
- }.get(SYSTEM),
320
- "config_key": "mcpServers",
321
- "notes": "Anthropic's desktop app for Claude",
322
- },
323
- {
324
- "id": "claude-code",
325
- "name": "Claude Code",
326
- "get_path": lambda: Path.cwd() / ".mcp.json",
327
- "config_key": "mcpServers",
328
- "notes": "Project-scoped config (committed to repo)",
329
- },
330
- {
331
- "id": "cursor",
332
- "name": "Cursor",
333
- "get_path": lambda: {
334
- "Darwin": home() / ".cursor" / "mcp.json",
335
- "Windows": home() / ".cursor" / "mcp.json",
336
- "Linux": home() / ".cursor" / "mcp.json",
337
- }.get(SYSTEM),
338
- "config_key": "mcpServers",
339
- "notes": "AI-native code editor (VS Code fork)",
340
- },
341
- {
342
- "id": "vscode",
343
- "name": "VS Code (GitHub Copilot)",
344
- "get_path": lambda: Path.cwd() / ".vscode" / "mcp.json",
345
- "config_key": "servers",
346
- "notes": "Workspace-scoped config for Copilot agent mode",
347
- },
348
- {
349
- "id": "windsurf",
350
- "name": "Windsurf",
351
- "get_path": lambda: {
352
- "Darwin": home() / ".codeium" / "windsurf" / "mcp_config.json",
353
- "Windows": appdata() / "windsurf" / "mcp_settings.json",
354
- "Linux": home() / ".codeium" / "windsurf" / "mcp_config.json",
355
- }.get(SYSTEM),
356
- "config_key": "mcpServers",
357
- "notes": "Codeium's AI code editor",
358
- },
359
- {
360
- "id": "cline",
361
- "name": "Cline",
362
- "get_path": lambda: vscode_global_storage() / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
363
- "config_key": "mcpServers",
364
- "notes": "AI coding assistant (VS Code extension)",
365
- },
366
- {
367
- "id": "roo-code",
368
- "name": "Roo Code",
369
- "get_path": lambda: vscode_global_storage() / "rooveterinaryinc.roo-cline" / "settings" / "mcp_settings.json",
370
- "config_key": "mcpServers",
371
- "notes": "Autonomous AI coding assistant (VS Code extension)",
372
- },
373
- {
374
- "id": "zed",
375
- "name": "Zed",
376
- "get_path": lambda: {
377
- "Darwin": home() / ".config" / "zed" / "settings.json",
378
- "Windows": None, # Zed doesn't support Windows yet
379
- "Linux": home() / ".config" / "zed" / "settings.json",
380
- }.get(SYSTEM),
381
- "config_key": "context_servers",
382
- "notes": "High-performance code editor (macOS/Linux only)",
383
- },
384
- {
385
- "id": "continue",
386
- "name": "Continue",
387
- "get_path": lambda: {
388
- "Darwin": home() / ".continue" / "config.json",
389
- "Windows": home() / ".continue" / "config.json",
390
- "Linux": home() / ".continue" / "config.json",
391
- }.get(SYSTEM),
392
- "config_key": "mcpServers",
393
- "notes": "Open-source AI code assistant",
394
- },
395
- {
396
- "id": "opencode",
397
- "name": "OpenCode",
398
- # OpenCode uses ~/.config/opencode/opencode.json on every platform
399
- # (it also reads a project-root opencode.json, but the global file is
400
- # the safe default for an installer). See https://opencode.ai/docs/config/
401
- "get_path": lambda: home() / ".config" / "opencode" / "opencode.json",
402
- "config_key": "mcp",
403
- "notes": "AI coding agent (uses its own type/enabled/command-array format)",
404
- },
405
- ]
406
-
407
- CLIENT_IDS = [c["id"] for c in MCP_CLIENTS]
408
-
409
- # ─── Server Entry Builder ────────────────────────────────────────────────────
410
-
411
- def get_python_base_install(python_path):
412
- """Resolve the base Python install used by the selected interpreter."""
413
- script = "import sys; print(sys.base_prefix or sys.prefix)"
414
- try:
415
- result = subprocess.run(
416
- [str(python_path), "-c", script],
417
- capture_output=True,
418
- text=True,
419
- timeout=5,
420
- )
421
- base_prefix = result.stdout.strip()
422
- if result.returncode == 0 and base_prefix:
423
- return base_prefix
424
- except Exception as exc:
425
- print(f"Warning: Could not query Python base prefix: {exc}")
426
-
427
- resolved = Path(python_path).resolve()
428
- if resolved.parent.name.lower() in {"scripts", "bin"}:
429
- return str(resolved.parent.parent)
430
- return str(resolved.parent)
431
-
432
-
433
- def build_server_env(python_path, api_path, lib_path, system=SYSTEM, python_home=None):
434
- """Build the env block used by all generated stdio MCP configs."""
435
- api_value = str(api_path or "")
436
- lib_value = str(lib_path or "")
437
- env = {
438
- "RESOLVE_SCRIPT_API": api_value,
439
- "RESOLVE_SCRIPT_LIB": lib_value,
440
- "PYTHONPATH": str(Path(api_value) / "Modules") if api_value else "",
441
- }
442
-
443
- if system == "Windows":
444
- env["PYTHONHOME"] = str(python_home or get_python_base_install(python_path))
445
-
446
- return env
447
-
448
-
449
- def build_server_entry(python_path, server_path, api_path, lib_path, system=SYSTEM, python_home=None):
450
- """Build the standard MCP server config entry."""
451
- return {
452
- "command": str(python_path),
453
- "args": [str(server_path)],
454
- "env": build_server_env(python_path, api_path, lib_path, system=system, python_home=python_home),
455
- }
456
-
457
-
458
- def build_zed_entry(python_path, server_path, api_path, lib_path, system=SYSTEM, python_home=None):
459
- """Build Zed-specific server entry (different format)."""
460
- return {
461
- "command": {
462
- "path": str(python_path),
463
- "args": [str(server_path)],
464
- },
465
- "env": build_server_env(python_path, api_path, lib_path, system=system, python_home=python_home),
466
- "settings": {},
467
- }
468
-
469
-
470
- def build_opencode_entry(python_path, server_path, api_path, lib_path, system=SYSTEM, python_home=None):
471
- """Build OpenCode-specific server entry (issue #72).
472
-
473
- OpenCode's schema differs from the standard format in three ways: it wraps
474
- the entry in a ``"mcp"`` key, the interpreter and script are a single
475
- ``"command"`` array (no separate ``args``), and the environment block is
476
- ``"environment"`` rather than ``"env"``. It also expects ``type``/``enabled``
477
- discriminators. See https://opencode.ai/docs/mcp-servers/
478
- """
479
- return {
480
- "type": "local",
481
- "enabled": True,
482
- "command": [str(python_path), str(server_path)],
483
- "environment": build_server_env(python_path, api_path, lib_path, system=system, python_home=python_home),
484
- }
485
-
486
-
487
- def build_entry_for_client(client, python_path, server_path, api_path, lib_path, system=SYSTEM, python_home=None):
488
- """Return the server entry shaped for a specific client's config schema."""
489
- builders = {
490
- "zed": build_zed_entry,
491
- "opencode": build_opencode_entry,
492
- }
493
- builder = builders.get(client["id"], build_server_entry)
494
- return builder(python_path, server_path, api_path, lib_path, system=system, python_home=python_home)
495
-
496
- # ─── Config File Operations ──────────────────────────────────────────────────
497
-
498
- class ConfigParseError(Exception):
499
- """Existing config file has content but could not be parsed.
500
-
501
- Callers must NOT overwrite such a file -- doing so destroys the user's
502
- settings (issue #71).
503
- """
504
-
505
-
506
- def _strip_jsonc(text):
507
- """Best-effort strip of // and /* */ comments and trailing commas.
508
-
509
- Zed's ``settings.json`` (and several other clients) accept JSON-with-comments.
510
- Python's ``json`` module rejects those, so we strip them before parsing.
511
- The walk is string-aware, so a ``//`` or ``/*`` sequence inside a JSON
512
- string value is preserved.
513
- """
514
- out = []
515
- i = 0
516
- n = len(text)
517
- in_string = False
518
- escape = False
519
- while i < n:
520
- ch = text[i]
521
- if in_string:
522
- out.append(ch)
523
- if escape:
524
- escape = False
525
- elif ch == "\\":
526
- escape = True
527
- elif ch == '"':
528
- in_string = False
529
- i += 1
530
- continue
531
- if ch == '"':
532
- in_string = True
533
- out.append(ch)
534
- i += 1
535
- continue
536
- if ch == "/" and i + 1 < n and text[i + 1] == "/":
537
- i += 2
538
- while i < n and text[i] not in "\r\n":
539
- i += 1
540
- continue
541
- if ch == "/" and i + 1 < n and text[i + 1] == "*":
542
- i += 2
543
- while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
544
- i += 1
545
- i += 2
546
- continue
547
- out.append(ch)
548
- i += 1
549
- stripped = "".join(out)
550
- # Drop trailing commas before a closing brace/bracket — string-aware, so a
551
- # comma inside a string value (e.g. "a, }") is never touched. A prior regex
552
- # pass here was NOT string-aware and silently corrupted such values during a
553
- # JSONC merge (follow-up to the issue #71 fix).
554
- result = []
555
- i = 0
556
- n = len(stripped)
557
- in_string = False
558
- escape = False
559
- while i < n:
560
- ch = stripped[i]
561
- if in_string:
562
- result.append(ch)
563
- if escape:
564
- escape = False
565
- elif ch == "\\":
566
- escape = True
567
- elif ch == '"':
568
- in_string = False
569
- i += 1
570
- continue
571
- if ch == '"':
572
- in_string = True
573
- result.append(ch)
574
- i += 1
575
- continue
576
- if ch == ",":
577
- j = i + 1
578
- while j < n and stripped[j] in " \t\r\n":
579
- j += 1
580
- if j < n and stripped[j] in "}]":
581
- i += 1 # trailing comma: drop it
582
- continue
583
- result.append(ch)
584
- i += 1
585
- return "".join(result)
586
-
587
-
588
- def read_json(path):
589
- """Read a JSON/JSONC config file.
590
-
591
- Returns an empty dict when the file is absent or empty (safe to create).
592
- Raises :class:`ConfigParseError` when the file has content that cannot be
593
- parsed even after stripping JSONC comments and trailing commas -- callers
594
- must refuse to overwrite such a file, or they would wipe the user's
595
- existing settings (issue #71: Zed's settings.json ships with comments).
596
- """
597
- try:
598
- with open(path, "r") as f:
599
- raw = f.read()
600
- except FileNotFoundError:
601
- return {}
602
-
603
- if not raw.strip():
604
- return {}
605
-
606
- try:
607
- return json.loads(raw)
608
- except json.JSONDecodeError:
609
- pass
610
-
611
- try:
612
- return json.loads(_strip_jsonc(raw))
613
- except json.JSONDecodeError as exc:
614
- raise ConfigParseError(str(exc)) from exc
615
-
616
-
617
- def write_json(path, data):
618
- """Write JSON to file, creating parent directories."""
619
- path = Path(path)
620
- path.parent.mkdir(parents=True, exist_ok=True)
621
-
622
- # Backup existing file
623
- if path.exists():
624
- backup = path.with_suffix(path.suffix + ".backup")
625
- shutil.copy2(path, backup)
626
-
627
- with open(path, "w") as f:
628
- json.dump(data, f, indent=2)
629
- f.write("\n")
630
-
631
-
632
- def write_client_config(client, python_path, server_path, api_path, lib_path, dry_run=False):
633
- """Write or merge MCP config for a specific client. Returns (success, message)."""
634
- config_path = client["get_path"]()
635
- if config_path is None:
636
- return False, f"{client['name']} is not available on {platform_name()}"
637
-
638
- config_key = client["config_key"]
639
-
640
- # Build the server entry (some clients use a non-standard schema)
641
- server_entry = build_entry_for_client(client, python_path, server_path, api_path, lib_path)
642
-
643
- if dry_run:
644
- preview = {config_key: {"davinci-resolve": server_entry}}
645
- return True, f"Would write to {config_path}:\n{json.dumps(preview, indent=2)}"
646
-
647
- # Read existing config and merge. If the file exists but cannot be parsed,
648
- # refuse to overwrite it -- silently replacing it would wipe the user's
649
- # settings (issue #71, especially Zed's commented settings.json).
650
- try:
651
- existing = read_json(config_path)
652
- except ConfigParseError as exc:
653
- return False, (
654
- f"{config_path} exists but could not be parsed ({exc}). "
655
- f"Refusing to overwrite to avoid data loss. Add the "
656
- f'"{config_key}" entry manually using the manual config output '
657
- f"(run with --manual)."
658
- )
659
-
660
- if config_key not in existing:
661
- existing[config_key] = {}
662
-
663
- existing[config_key]["davinci-resolve"] = server_entry
664
-
665
- write_json(config_path, existing)
666
- return True, str(config_path)
667
-
668
-
669
- def generate_manual_config(python_path, server_path, api_path, lib_path):
670
- """Generate config snippets for manual setup."""
671
- entry = build_server_entry(python_path, server_path, api_path, lib_path)
672
- zed_entry = build_zed_entry(python_path, server_path, api_path, lib_path)
673
- opencode_entry = build_opencode_entry(python_path, server_path, api_path, lib_path)
674
-
675
- standard = json.dumps({"mcpServers": {"davinci-resolve": entry}}, indent=2)
676
- vscode_fmt = json.dumps({"servers": {"davinci-resolve": entry}}, indent=2)
677
- zed_fmt = json.dumps({"context_servers": {"davinci-resolve": zed_entry}}, indent=2)
678
- opencode_fmt = json.dumps({"mcp": {"davinci-resolve": opencode_entry}}, indent=2)
679
-
680
- return standard, vscode_fmt, zed_fmt, opencode_fmt
681
-
682
- # ─── Virtual Environment ─────────────────────────────────────────────────────
683
-
684
- def find_python():
685
- """Find the best Python 3 executable."""
686
- candidates = ["python3", "python"]
687
- for cmd in candidates:
688
- try:
689
- result = subprocess.run(
690
- [cmd, "--version"], capture_output=True, text=True
691
- )
692
- if result.returncode == 0 and "Python 3" in result.stdout:
693
- return cmd
694
- except FileNotFoundError:
695
- continue
696
- return None
697
-
698
-
699
- def create_venv(venv_path):
700
- """Create a Python virtual environment."""
701
- print(f"\n Creating virtual environment at {dim(str(venv_path))}...")
702
- subprocess.run(
703
- [sys.executable, "-m", "venv", str(venv_path)],
704
- check=True
705
- )
706
-
707
-
708
- def get_venv_python(venv_path):
709
- """Get the Python executable inside a venv."""
710
- if is_windows():
711
- return venv_path / "Scripts" / "python.exe"
712
- return venv_path / "bin" / "python"
713
-
714
-
715
- def get_venv_pip(venv_path):
716
- """Get the pip executable inside a venv."""
717
- if is_windows():
718
- return venv_path / "Scripts" / "pip.exe"
719
- return venv_path / "bin" / "pip"
720
-
721
-
722
- def install_dependencies(venv_path, project_dir):
723
- """Install Python dependencies into the venv."""
724
- pip = get_venv_pip(venv_path)
725
- req_file = project_dir / "requirements.txt"
726
-
727
- print(f" Installing dependencies...")
728
-
729
- # Install MCP SDK
730
- subprocess.run(
731
- [str(pip), "install", "-q", "mcp[cli]"],
732
- check=True, capture_output=True
733
- )
734
-
735
- # Install from requirements.txt if it exists
736
- if req_file.exists():
737
- subprocess.run(
738
- [str(pip), "install", "-q", "-r", str(req_file)],
739
- check=True, capture_output=True
740
- )
741
-
742
- # ─── Connection Verification ─────────────────────────────────────────────────
743
-
744
- def verify_resolve_connection(python_path, api_path, lib_path):
745
- """Try to import DaVinciResolveScript and connect."""
746
- if not api_path:
747
- return False, "Resolve API path not found"
748
-
749
- env = {**os.environ, **build_server_env(python_path, api_path, lib_path)}
750
- modules_path = env["PYTHONPATH"]
751
- test_script = textwrap.dedent(f"""\
752
- import sys
753
- sys.path.insert(0, {modules_path!r})
754
- try:
755
- import DaVinciResolveScript as dvr
756
- resolve = dvr.scriptapp('Resolve')
757
- if resolve:
758
- name = resolve.GetProductName()
759
- ver = resolve.GetVersionString()
760
- print(f"CONNECTED: {{name}} {{ver}}")
761
- else:
762
- print("IMPORTED_OK: Module loads but Resolve not running or not responding")
763
- except ImportError as e:
764
- print(f"IMPORT_ERROR: {{e}}")
765
- except Exception as e:
766
- print(f"ERROR: {{e}}")
767
- """)
768
-
769
- try:
770
- result = subprocess.run(
771
- [str(python_path), "-c", test_script],
772
- capture_output=True,
773
- text=True,
774
- timeout=10,
775
- env=env,
776
- )
777
- output = result.stdout.strip() or result.stderr.strip()
778
- if output.startswith("CONNECTED:"):
779
- return True, output.replace("CONNECTED: ", "")
780
- elif output.startswith("IMPORTED_OK:"):
781
- return True, "API module loaded (Resolve not running)"
782
- else:
783
- if output:
784
- return False, output
785
- return False, f"Process exited with code {result.returncode}"
786
- except subprocess.TimeoutExpired:
787
- return False, "Connection timed out"
788
- except Exception as e:
789
- return False, str(e)
790
-
791
- # ─── Interactive UI ───────────────────────────────────────────────────────────
792
-
793
- def print_banner():
794
- title = f"DaVinci Resolve MCP Server — Installer v{VERSION}"
795
- subtitle = "32 compound · 329 full · 3 platforms"
796
- print()
797
- print(bold(" ╔══════════════════════════════════════════════════════╗"))
798
- print(bold(f" ║{title:^54}║"))
799
- print(bold(" ╠══════════════════════════════════════════════════════╣"))
800
- print(bold(f" ║{subtitle:^54}║"))
801
- print(bold(" ╚══════════════════════════════════════════════════════╝"))
802
- print()
803
-
804
-
805
- def print_step(num, total, text):
806
- print(f"\n {cyan(f'[{num}/{total}]')} {bold(text)}")
807
- print(f" {'─' * 50}")
808
-
809
-
810
- def print_update_status(project_dir, *, force=False):
811
- """Best-effort installer update notice."""
812
- result = check_for_updates(VERSION, project_dir, timeout=2.0, force=force)
813
- status = result.get("status")
814
- if status == "update_available":
815
- version_note = dim(
816
- f"v{result.get('current_version')} -> v{result.get('latest_version')}"
817
- )
818
- release_note = dim(f"Latest release: {result.get('release_url')}")
819
- print(
820
- f" MCP Update: {yellow('Available')} "
821
- f"{version_note}"
822
- )
823
- print(f" {release_note}")
824
- elif status == "up_to_date":
825
- print(f" MCP Update: {green('Up to date')} {dim(f'v{VERSION}')}")
826
- elif status == "current_ahead":
827
- print(f" MCP Update: {green('Local build ahead of latest release')} {dim(f'v{VERSION}')}")
828
- elif status == "disabled":
829
- print(f" MCP Update: {dim('Check disabled')}")
830
- elif status == "error":
831
- print(f" MCP Update: {yellow('Could not check')} {dim(str(result.get('error', '')))}")
832
- return result
833
-
834
-
835
- def _run_git(project_dir, args, timeout=45):
836
- try:
837
- return subprocess.run(
838
- ["git", "-C", str(project_dir), *args],
839
- capture_output=True,
840
- text=True,
841
- timeout=timeout,
842
- )
843
- except FileNotFoundError:
844
- return None
845
- except subprocess.TimeoutExpired as exc:
846
- return exc
847
-
848
-
849
- def _git_failure_message(result, fallback):
850
- if result is None:
851
- return "git is not available on PATH"
852
- if isinstance(result, subprocess.TimeoutExpired):
853
- return "git command timed out"
854
- output = (result.stderr or result.stdout or "").strip()
855
- return output or fallback
856
-
857
-
858
- def _record_update_history(project_dir, entry):
859
- """Append `entry` to `<project_dir>/logs/update_history.json` (best-effort)."""
860
- import json as _json
861
- log_dir = os.path.join(project_dir, "logs")
862
- try:
863
- os.makedirs(log_dir, exist_ok=True)
864
- except OSError:
865
- return
866
- path = os.path.join(log_dir, "update_history.json")
867
- history = {"entries": []}
868
- if os.path.isfile(path):
869
- try:
870
- with open(path, "r", encoding="utf-8") as fh:
871
- loaded = _json.load(fh)
872
- if isinstance(loaded, dict) and isinstance(loaded.get("entries"), list):
873
- history = loaded
874
- except (OSError, ValueError):
875
- pass
876
- history.setdefault("entries", []).append(entry)
877
- # Trim to most recent 200 entries — keep history bounded.
878
- if len(history["entries"]) > 200:
879
- history["entries"] = history["entries"][-200:]
880
- history["updated_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
881
- tmp_path = path + ".tmp"
882
- try:
883
- with open(tmp_path, "w", encoding="utf-8") as fh:
884
- _json.dump(history, fh, indent=2)
885
- os.replace(tmp_path, path)
886
- except OSError:
887
- try:
888
- os.remove(tmp_path)
889
- except OSError:
890
- pass
891
-
892
-
893
- def _read_current_version(project_dir):
894
- """Best-effort read of VERSION constant from src/server.py."""
895
- server_path = os.path.join(project_dir, "src", "server.py")
896
- try:
897
- with open(server_path, "r", encoding="utf-8") as fh:
898
- for line in fh:
899
- if line.startswith("VERSION = "):
900
- return line.split("=", 1)[1].strip().strip('"').strip("'")
901
- except OSError:
902
- pass
903
- return None
904
-
905
-
906
- def _record_attempt(project_dir, *, kind, success, reason=None, message=None,
907
- from_version=None, to_version=None, from_sha=None, to_sha=None,
908
- initiator=None, extra=None):
909
- """Append a structured row to update_history.json. `extra` is merged in flat."""
910
- entry = {
911
- "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
912
- "kind": kind, # "update" | "rollback" | "dry_run"
913
- "success": bool(success),
914
- "reason": reason,
915
- "message": message,
916
- "from_version": from_version,
917
- "to_version": to_version,
918
- "from_sha": from_sha,
919
- "to_sha": to_sha,
920
- "initiator": initiator,
921
- }
922
- if isinstance(extra, dict):
923
- entry.update(extra)
924
- _record_update_history(project_dir, entry)
925
-
926
-
927
- def apply_safe_self_update(project_dir, dry_run=False, *, initiator="cli",
928
- strategy="refuse_on_dirty"):
929
- """Apply a guarded git fast-forward update.
930
-
931
- `strategy` controls behavior when the working tree is dirty:
932
- - `"refuse_on_dirty"` (default) — return reason="local_changes" without
933
- touching anything.
934
- - `"stash_if_needed"` — `git stash push`, apply the update, `git stash pop`.
935
- If pop conflicts, leave the stash in place and return reason="stash_pop_conflict"
936
- with the stash ref so the user can resolve.
937
-
938
- Every attempt — success or failure — is recorded in
939
- `<project_dir>/logs/update_history.json` so the dashboard can show what
940
- happened and rollback can find the prior SHA.
941
- """
942
- inside = _run_git(project_dir, ["rev-parse", "--is-inside-work-tree"])
943
- if inside is None or isinstance(inside, subprocess.TimeoutExpired) or inside.returncode != 0:
944
- msg = _git_failure_message(inside, "not a git checkout")
945
- _record_attempt(project_dir, kind="update", success=False, reason="not_git", message=msg, initiator=initiator)
946
- return {"success": False, "reason": "not_git", "message": msg}
947
-
948
- status = _run_git(project_dir, ["status", "--porcelain"])
949
- if status is None or isinstance(status, subprocess.TimeoutExpired) or status.returncode != 0:
950
- msg = _git_failure_message(status, "could not inspect git status")
951
- _record_attempt(project_dir, kind="update", success=False, reason="status_failed", message=msg, initiator=initiator)
952
- return {"success": False, "reason": "status_failed", "message": msg}
953
-
954
- stash_ref = None
955
- if status.stdout.strip():
956
- if strategy != "stash_if_needed":
957
- msg = "local changes are present; continuing with the current build"
958
- _record_attempt(project_dir, kind="update", success=False, reason="local_changes", message=msg, initiator=initiator)
959
- return {"success": False, "reason": "local_changes", "message": msg}
960
- # Auto-stash path: push the working tree onto the stash, remember the
961
- # ref so we can pop (or surface) it after the update.
962
- stash_msg = f"mcp-update-autostash-{time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())}"
963
- stash = _run_git(project_dir, ["stash", "push", "-u", "-m", stash_msg], timeout=30)
964
- if stash is None or stash.returncode != 0 or "No local changes to save" in (stash.stdout or ""):
965
- # If we got here `status` was dirty, so a stash failure means trouble.
966
- msg = _git_failure_message(stash, "git stash push failed")
967
- _record_attempt(project_dir, kind="update", success=False, reason="stash_failed", message=msg, initiator=initiator)
968
- return {"success": False, "reason": "stash_failed", "message": msg}
969
- stash_ref = stash_msg
970
-
971
- upstream = _run_git(project_dir, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
972
- if upstream is None or isinstance(upstream, subprocess.TimeoutExpired) or upstream.returncode != 0:
973
- msg = _git_failure_message(upstream, "current branch has no configured upstream")
974
- _record_attempt(project_dir, kind="update", success=False, reason="no_upstream", message=msg, initiator=initiator)
975
- return {"success": False, "reason": "no_upstream", "message": msg}
976
-
977
- # Capture pre-update SHA + version so rollback knows where to revert to.
978
- head_before = _run_git(project_dir, ["rev-parse", "HEAD"])
979
- from_sha = head_before.stdout.strip() if head_before and head_before.returncode == 0 else None
980
- from_version = _read_current_version(project_dir)
981
-
982
- if dry_run:
983
- msg = f"would fast-forward from {upstream.stdout.strip()}"
984
- _record_attempt(project_dir, kind="dry_run", success=True, message=msg,
985
- from_version=from_version, from_sha=from_sha, initiator=initiator)
986
- return {"success": True, "changed": False, "dry_run": True, "message": msg}
987
-
988
- fetch = _run_git(project_dir, ["fetch", "--tags", "--prune"], timeout=90)
989
- if fetch is None or isinstance(fetch, subprocess.TimeoutExpired) or fetch.returncode != 0:
990
- msg = _git_failure_message(fetch, "git fetch failed")
991
- _record_attempt(project_dir, kind="update", success=False, reason="fetch_failed", message=msg,
992
- from_version=from_version, from_sha=from_sha, initiator=initiator)
993
- return {"success": False, "reason": "fetch_failed", "message": msg}
994
-
995
- pull = _run_git(project_dir, ["pull", "--ff-only"], timeout=120)
996
- if pull is None or isinstance(pull, subprocess.TimeoutExpired) or pull.returncode != 0:
997
- msg = _git_failure_message(pull, "git pull --ff-only failed")
998
- _record_attempt(project_dir, kind="update", success=False, reason="pull_failed", message=msg,
999
- from_version=from_version, from_sha=from_sha, initiator=initiator)
1000
- return {"success": False, "reason": "pull_failed", "message": msg}
1001
-
1002
- head_after = _run_git(project_dir, ["rev-parse", "HEAD"])
1003
- to_sha = head_after.stdout.strip() if head_after and head_after.returncode == 0 else None
1004
- to_version = _read_current_version(project_dir)
1005
-
1006
- output = "\n".join(part.strip() for part in (pull.stdout, pull.stderr) if part and part.strip())
1007
- changed = "Already up to date." not in output
1008
-
1009
- # Integrity verification: confirm our local HEAD matches what GitHub said
1010
- # was the target SHA. Mismatch could mean: (a) user pushed to a fork mid-
1011
- # update, (b) the release we resolved was different from the branch tip
1012
- # (force-pushed), (c) corruption. We don't roll back automatically — the
1013
- # user may have intentional reasons — but we log it loudly.
1014
- integrity = {"verified": None, "expected_sha": None, "actual_sha": to_sha}
1015
- try:
1016
- from src.utils.update_check import check_for_updates as _cfu # type: ignore
1017
- info = _cfu(from_version or "0.0.0", project_dir, force=False)
1018
- expected = info.get("release_target_sha")
1019
- if expected:
1020
- integrity["expected_sha"] = expected
1021
- # Compare prefixes (release SHA may be a tag name or short SHA).
1022
- ok = bool(to_sha) and (
1023
- expected == to_sha
1024
- or (len(expected) >= 7 and to_sha.startswith(expected[:7]))
1025
- or (len(to_sha) >= 7 and expected.startswith(to_sha[:7]))
1026
- )
1027
- integrity["verified"] = ok
1028
- except Exception as exc:
1029
- integrity["error"] = f"{type(exc).__name__}: {exc}"
1030
-
1031
- # If we stashed changes earlier, try to reapply them now.
1032
- stash_pop_conflict = False
1033
- if stash_ref:
1034
- pop = _run_git(project_dir, ["stash", "pop"], timeout=60)
1035
- if pop is None or pop.returncode != 0:
1036
- stash_pop_conflict = True
1037
-
1038
- result = {
1039
- "success": True, "changed": changed,
1040
- "message": output or "update complete",
1041
- "from_version": from_version, "to_version": to_version,
1042
- "from_sha": from_sha, "to_sha": to_sha,
1043
- "stash_ref": stash_ref,
1044
- "stash_pop_conflict": stash_pop_conflict,
1045
- "integrity": integrity,
1046
- }
1047
- if stash_pop_conflict:
1048
- # Update applied successfully but the stash pop hit a conflict; the
1049
- # user's changes are still in the stash. Don't fail the overall update,
1050
- # but surface the conflict prominently.
1051
- result["reason"] = "stash_pop_conflict"
1052
- result["remediation"] = (
1053
- f"Update applied, but your stashed changes ({stash_ref}) conflict with the new build. "
1054
- "Resolve via `git stash list` + `git stash pop` after restarting; "
1055
- "use `git stash drop` if you want to discard them."
1056
- )
1057
-
1058
- _record_attempt(project_dir, kind="update", success=True,
1059
- message=output or "update complete",
1060
- from_version=from_version, to_version=to_version,
1061
- from_sha=from_sha, to_sha=to_sha, initiator=initiator,
1062
- extra={"stash_ref": stash_ref,
1063
- "stash_pop_conflict": stash_pop_conflict,
1064
- "integrity": integrity})
1065
-
1066
- return result
1067
-
1068
-
1069
- def rollback_to_previous_build(project_dir, *, initiator="cli"):
1070
- """git reset --hard to the from_sha of the most recent successful update.
1071
-
1072
- Refuses if local changes exist (same guard as apply_safe_self_update) so we
1073
- never silently lose user work. Records a rollback row in update_history.
1074
- """
1075
- import json as _json
1076
- history_path = os.path.join(project_dir, "logs", "update_history.json")
1077
- if not os.path.isfile(history_path):
1078
- return {"success": False, "reason": "no_history", "message": "no update_history.json found"}
1079
- try:
1080
- with open(history_path, "r", encoding="utf-8") as fh:
1081
- history = _json.load(fh)
1082
- except (OSError, _json.JSONDecodeError) as exc:
1083
- return {"success": False, "reason": "history_read_failed", "message": str(exc)}
1084
-
1085
- # Newest first; find the latest successful "update" entry with a from_sha.
1086
- candidates = [e for e in reversed(history.get("entries") or [])
1087
- if e.get("kind") == "update" and e.get("success") and e.get("from_sha")]
1088
- if not candidates:
1089
- return {"success": False, "reason": "no_target", "message": "no prior successful update to roll back to"}
1090
- target = candidates[0]
1091
- from_sha = target["from_sha"]
1092
-
1093
- status = _run_git(project_dir, ["status", "--porcelain"])
1094
- if status is None or status.returncode != 0:
1095
- return {"success": False, "reason": "status_failed", "message": "could not inspect git status"}
1096
- if status.stdout.strip():
1097
- return {"success": False, "reason": "local_changes",
1098
- "message": "local changes are present; commit or stash before rolling back"}
1099
-
1100
- head_before = _run_git(project_dir, ["rev-parse", "HEAD"])
1101
- pre_rollback_sha = head_before.stdout.strip() if head_before and head_before.returncode == 0 else None
1102
- pre_rollback_version = _read_current_version(project_dir)
1103
-
1104
- reset = _run_git(project_dir, ["reset", "--hard", from_sha], timeout=60)
1105
- if reset is None or reset.returncode != 0:
1106
- msg = _git_failure_message(reset, f"git reset --hard {from_sha} failed")
1107
- _record_attempt(project_dir, kind="rollback", success=False, reason="reset_failed", message=msg,
1108
- from_version=pre_rollback_version, from_sha=pre_rollback_sha,
1109
- to_sha=from_sha, initiator=initiator)
1110
- return {"success": False, "reason": "reset_failed", "message": msg}
1111
-
1112
- to_version = _read_current_version(project_dir)
1113
- _record_attempt(project_dir, kind="rollback", success=True,
1114
- message=f"rolled back to {from_sha[:10]}",
1115
- from_version=pre_rollback_version, to_version=to_version,
1116
- from_sha=pre_rollback_sha, to_sha=from_sha, initiator=initiator)
1117
- return {"success": True, "changed": pre_rollback_sha != from_sha,
1118
- "message": f"rolled back to {from_sha[:10]}",
1119
- "from_version": pre_rollback_version, "to_version": to_version,
1120
- "from_sha": pre_rollback_sha, "to_sha": from_sha}
1121
-
1122
-
1123
- def preview_update(project_dir):
1124
- """Fetch the target release metadata + scan for breaking-change markers.
1125
-
1126
- Returns a dict the UI can render before the user confirms the update:
1127
- {success, latest_version, release_notes, breaking_changes, prerelease,
1128
- channel, current_version, target_sha}
1129
- """
1130
- try:
1131
- from src.utils.update_check import check_for_updates # type: ignore
1132
- except Exception as exc:
1133
- return {"success": False, "error": f"update_check unavailable: {exc}"}
1134
- current = _read_current_version(project_dir) or "0.0.0"
1135
- try:
1136
- result = check_for_updates(current, project_dir, force=True)
1137
- except Exception as exc:
1138
- return {"success": False, "error": str(exc)}
1139
- return {
1140
- "success": True,
1141
- "current_version": current,
1142
- "latest_version": result.get("latest_version"),
1143
- "release_notes": result.get("release_notes") or "",
1144
- "breaking_changes": result.get("release_notes_breaking") or [],
1145
- "prerelease": bool(result.get("prerelease")),
1146
- "channel": result.get("channel"),
1147
- "target_sha": result.get("release_target_sha"),
1148
- "release_url": result.get("release_url"),
1149
- "status": result.get("status"),
1150
- }
1151
-
1152
-
1153
- def read_update_history(project_dir, limit=20):
1154
- """Most-recent-first list of recorded update attempts. For the dashboard."""
1155
- import json as _json
1156
- path = os.path.join(project_dir, "logs", "update_history.json")
1157
- if not os.path.isfile(path):
1158
- return {"success": True, "entries": []}
1159
- try:
1160
- with open(path, "r", encoding="utf-8") as fh:
1161
- history = _json.load(fh)
1162
- except (OSError, _json.JSONDecodeError) as exc:
1163
- return {"success": False, "error": str(exc), "entries": []}
1164
- entries = (history.get("entries") or [])[-int(limit):]
1165
- entries.reverse()
1166
- return {"success": True, "entries": entries}
1167
-
1168
-
1169
- def _restart_installer():
1170
- print(f" {green('Restarting installer with the updated build...')}")
1171
- os.execv(sys.executable, [sys.executable, *sys.argv])
1172
-
1173
-
1174
- def _print_update_apply_result(result, *, restart_on_change=True):
1175
- if result.get("success"):
1176
- if result.get("dry_run"):
1177
- print(f" MCP Update: {yellow('Dry run')} {dim(result.get('message', ''))}")
1178
- return
1179
- print(f" MCP Update: {green('Update command completed')}")
1180
- message = str(result.get("message") or "").strip()
1181
- if message:
1182
- for line in message.splitlines()[:6]:
1183
- print(f" {dim(line)}")
1184
- if result.get("changed") and restart_on_change:
1185
- _restart_installer()
1186
- return
1187
-
1188
- print(f" MCP Update: {yellow('Not applied')} {dim(result.get('message', ''))}")
1189
-
1190
-
1191
- def maybe_prompt_for_update(project_dir, result, *, interactive, force_update=False, dry_run=False):
1192
- """Prompt or auto-apply updates only in this human-facing installer."""
1193
- if not result or result.get("status") != "update_available":
1194
- return
1195
-
1196
- decision = update_prompt_decision(result)
1197
- if force_update or decision.get("action") == "auto":
1198
- print(f" MCP Update: {yellow('Applying safe fast-forward update...')}")
1199
- _print_update_apply_result(apply_safe_self_update(project_dir, dry_run=dry_run))
1200
- return
1201
-
1202
- if decision.get("action") == "notify":
1203
- print(f" {dim('Update policy is notify-only; continuing with the current build.')}")
1204
- return
1205
- if decision.get("reason") == "ignored":
1206
- print(f" {dim('This release was ignored; continuing with the current build.')}")
1207
- return
1208
- if decision.get("reason") == "snoozed":
1209
- snooze_note = f"Update reminder snoozed until {decision.get('snooze_until_iso')}."
1210
- print(f" {dim(snooze_note)}")
1211
- return
1212
- if not interactive:
1213
- return
1214
-
1215
- latest = result.get("latest_version") or result.get("latest_tag") or "latest"
1216
- print()
1217
- print(f" {yellow('A newer DaVinci Resolve MCP is available.')} {dim(f'v{VERSION} -> v{latest}')}")
1218
- print(f" {dim('Safe auto-update only runs for clean git checkouts with a configured upstream.')}")
1219
- print(f" {cyan('1')}. Update now")
1220
- print(f" {cyan('2')}. Continue current build")
1221
- print(f" {cyan('3')}. Remind me tomorrow")
1222
- print(f" {cyan('4')}. Ignore this version")
1223
- print(f" {cyan('5')}. Auto-update this checkout when safe")
1224
- print(f" {cyan('6')}. Never check automatically")
1225
- try:
1226
- choice = input(f" Select {dim('[2]')} ").strip().lower()
1227
- except (EOFError, KeyboardInterrupt):
1228
- print()
1229
- return
1230
-
1231
- if choice in ("1", "u", "update", "update now"):
1232
- _print_update_apply_result(apply_safe_self_update(project_dir, dry_run=dry_run))
1233
- elif choice in ("3", "r", "remind", "later", "snooze"):
1234
- snooze_update_prompt(project_dir)
1235
- print(f" MCP Update: {dim('Reminder snoozed for 24 hours.')}")
1236
- elif choice in ("4", "i", "ignore"):
1237
- ignore_update_version(project_dir, result)
1238
- print(f" MCP Update: {dim(f'Ignored v{latest}. Newer releases will still prompt.')}")
1239
- elif choice in ("5", "a", "auto", "auto-update", "autoupdate"):
1240
- set_update_mode(project_dir, "auto")
1241
- print(f" MCP Update: {green('Safe auto-update enabled for this checkout.')}")
1242
- _print_update_apply_result(apply_safe_self_update(project_dir, dry_run=dry_run))
1243
- elif choice in ("6", "n", "never", "disable", "disabled"):
1244
- set_update_mode(project_dir, "never")
1245
- print(f" MCP Update: {dim('Automatic update checks disabled for this checkout.')}")
1246
-
1247
-
1248
- def prompt_yes_no(question, default=True):
1249
- """Prompt for yes/no with a default."""
1250
- suffix = "[Y/n]" if default else "[y/N]"
1251
- try:
1252
- answer = input(f" {question} {dim(suffix)} ").strip().lower()
1253
- except (EOFError, KeyboardInterrupt):
1254
- print()
1255
- return default
1256
- if not answer:
1257
- return default
1258
- return answer in ("y", "yes")
1259
-
1260
-
1261
- def prompt_clients():
1262
- """Interactive client selection menu."""
1263
- print(f"\n Which MCP client(s) do you want to configure?\n")
1264
-
1265
- for i, client in enumerate(MCP_CLIENTS, 1):
1266
- path = client["get_path"]()
1267
- available = path is not None
1268
- status = ""
1269
- if not available:
1270
- status = dim(f" (not available on {platform_name()})")
1271
- elif path and path.exists():
1272
- status = green(" (config exists)")
1273
-
1274
- num = f"{i:>2}"
1275
- print(f" {cyan(num)}. {client['name']:<28} {dim(client['notes'])}{status}")
1276
-
1277
- all_num = str(len(MCP_CLIENTS) + 1).rjust(2)
1278
- manual_num = str(len(MCP_CLIENTS) + 2).rjust(2)
1279
- print(f"\n {cyan(all_num)}. {bold('All of the above')}")
1280
- print(f" {cyan(manual_num)}. {bold('Manual setup')} {dim('(print config, I will set it up myself)')}")
1281
- print(f" {cyan(' 0')}. {dim('Skip client configuration')}")
1282
-
1283
- print()
1284
- try:
1285
- choice = input(f" Select (comma-separated, e.g. 1,3,5): ").strip()
1286
- except (EOFError, KeyboardInterrupt):
1287
- print()
1288
- return []
1289
-
1290
- if not choice or choice == "0":
1291
- return []
1292
-
1293
- selections = []
1294
- for part in choice.replace(" ", "").split(","):
1295
- try:
1296
- idx = int(part)
1297
- if idx == len(MCP_CLIENTS) + 1: # "All"
1298
- return CLIENT_IDS + ["manual"]
1299
- elif idx == len(MCP_CLIENTS) + 2: # "Manual"
1300
- selections.append("manual")
1301
- elif 1 <= idx <= len(MCP_CLIENTS):
1302
- selections.append(MCP_CLIENTS[idx - 1]["id"])
1303
- except ValueError:
1304
- # Try matching by name/id
1305
- part_lower = part.lower()
1306
- for client in MCP_CLIENTS:
1307
- if part_lower in client["id"] or part_lower in client["name"].lower():
1308
- selections.append(client["id"])
1309
- break
1310
-
1311
- return list(dict.fromkeys(selections)) # deduplicate, preserve order
1312
-
1313
- # ─── Main Install Flow ───────────────────────────────────────────────────────
1314
-
1315
- def main():
1316
- parser = argparse.ArgumentParser(
1317
- description="DaVinci Resolve MCP Server — Universal Installer",
1318
- formatter_class=argparse.RawDescriptionHelpFormatter,
1319
- epilog=textwrap.dedent("""\
1320
- Examples:
1321
- python install.py Interactive mode
1322
- python install.py --clients all Configure all clients
1323
- python install.py --clients cursor,claude-desktop
1324
- python install.py --clients manual Just print the config
1325
- python install.py --no-venv --clients cursor Skip venv, configure Cursor
1326
- python install.py --dry-run --clients all Preview without writing
1327
- python install.py --update-policy auto Enable guarded auto-updates
1328
- python install.py --update-policy never Disable update checks
1329
- """)
1330
- )
1331
- parser.add_argument(
1332
- "--clients", type=str, default=None,
1333
- help="Comma-separated client IDs, or 'all' / 'manual' (skip interactive prompt)"
1334
- )
1335
- parser.add_argument(
1336
- "--no-venv", action="store_true",
1337
- help="Skip virtual environment creation (use system Python)"
1338
- )
1339
- parser.add_argument(
1340
- "--dry-run", action="store_true",
1341
- help="Preview config changes without writing files"
1342
- )
1343
- parser.add_argument(
1344
- "--python", type=str, default=None,
1345
- help="Path to Python executable to use in MCP configs"
1346
- )
1347
- parser.add_argument(
1348
- "--server", type=str, default=None,
1349
- help="Path to the MCP server script"
1350
- )
1351
- parser.add_argument(
1352
- "--update-policy",
1353
- choices=["prompt", "auto", "notify", "never"],
1354
- default=None,
1355
- help="Set local update policy: prompt, auto, notify, or never"
1356
- )
1357
- parser.add_argument(
1358
- "--update-now", action="store_true",
1359
- help="Apply a safe git fast-forward update if a newer release is available"
1360
- )
1361
- parser.add_argument(
1362
- "--clear-update-preferences", action="store_true",
1363
- help="Clear ignored-version and snooze update preferences"
1364
- )
1365
-
1366
- args = parser.parse_args()
1367
- interactive = args.clients is None
1368
-
1369
- # ── Banner ──
1370
- if interactive:
1371
- print_banner()
1372
-
1373
- project_dir = Path(__file__).resolve().parent
1374
- total_steps = 5
1375
-
1376
- if args.clear_update_preferences:
1377
- clear_update_prompt_preferences(project_dir)
1378
- print(f" MCP Update: {green('Cleared ignored-version and snooze preferences')}")
1379
- if args.update_policy:
1380
- set_update_mode(project_dir, args.update_policy)
1381
- print(f" MCP Update: {green('Policy set')} {dim(args.update_policy)}")
1382
-
1383
- # ══════════════════════════════════════════════════════════════════════
1384
- # STEP 1: Platform & Resolve Detection
1385
- # ══════════════════════════════════════════════════════════════════════
1386
-
1387
- if interactive:
1388
- print_step(1, total_steps, "Detecting Platform & DaVinci Resolve")
1389
-
1390
- print(f" Platform: {bold(platform_name())} ({platform.machine()})")
1391
- update_result = print_update_status(project_dir, force=args.update_now)
1392
- maybe_prompt_for_update(
1393
- project_dir,
1394
- update_result,
1395
- interactive=interactive,
1396
- force_update=args.update_now,
1397
- dry_run=args.dry_run,
1398
- )
1399
-
1400
- api_path, lib_path = find_resolve_paths()
1401
-
1402
- if api_path:
1403
- print(f" API Path: {green(api_path)}")
1404
- else:
1405
- print(f" API Path: {red('Not found')}")
1406
- if is_linux():
1407
- print(f" {dim('Tip: DaVinci Resolve typically installs to /opt/resolve/')}")
1408
- print(f" {dim(' Set RESOLVE_SCRIPT_API environment variable if installed elsewhere')}")
1409
-
1410
- # Check environment variable fallback
1411
- env_api = os.environ.get("RESOLVE_SCRIPT_API")
1412
- if env_api and os.path.isdir(env_api):
1413
- api_path = env_api
1414
- print(f" {green('Found via $RESOLVE_SCRIPT_API:')} {api_path}")
1415
-
1416
- if lib_path:
1417
- print(f" Library: {green(lib_path)}")
1418
- else:
1419
- print(f" Library: {yellow('Not found')} {dim('(optional — API path is sufficient)')}")
1420
-
1421
- resolve_running = check_resolve_running()
1422
- if resolve_running:
1423
- print(f" Resolve: {green('Running')}")
1424
- else:
1425
- print(f" Resolve: {yellow('Not running')} {dim('(start Resolve to verify connection)')}")
1426
-
1427
- if not api_path:
1428
- print(f"\n {yellow('Warning:')} Could not auto-detect DaVinci Resolve installation.")
1429
- print(f" The installer will continue, but you may need to set RESOLVE_SCRIPT_API manually.")
1430
- if interactive and not prompt_yes_no("Continue anyway?"):
1431
- print(f"\n {dim('Aborted.')}")
1432
- sys.exit(1)
1433
-
1434
- # ══════════════════════════════════════════════════════════════════════
1435
- # STEP 2: Python Virtual Environment
1436
- # ══════════════════════════════════════════════════════════════════════
1437
-
1438
- venv_path = project_dir / "venv"
1439
- venv_python = get_venv_python(venv_path)
1440
- skip_venv = args.no_venv
1441
-
1442
- if interactive:
1443
- print_step(2, total_steps, "Python Environment")
1444
-
1445
- if skip_venv:
1446
- print(f" Skipping venv (--no-venv)")
1447
- python_path = Path(args.python) if args.python else Path(sys.executable)
1448
- require_supported_python(python_path, "Selected Python")
1449
- print(f" Using: {python_path}")
1450
- elif venv_path.exists() and venv_python.exists():
1451
- print(f" Venv: {green('Already exists')} at {dim(str(venv_path))}")
1452
- python_path = venv_python
1453
- require_supported_python(python_path, "Existing venv")
1454
-
1455
- # Check if deps are installed
1456
- try:
1457
- result = subprocess.run(
1458
- [str(python_path), "-c", "import mcp; print('ok')"],
1459
- capture_output=True, text=True
1460
- )
1461
- if result.stdout.strip() == "ok":
1462
- print(f" MCP SDK: {green('Installed')}")
1463
- else:
1464
- print(f" MCP SDK: {yellow('Missing')} — installing...")
1465
- install_dependencies(venv_path, project_dir)
1466
- print(f" MCP SDK: {green('Installed')}")
1467
- except Exception:
1468
- install_dependencies(venv_path, project_dir)
1469
- print(f" MCP SDK: {green('Installed')}")
1470
- else:
1471
- if interactive:
1472
- create_venv_ok = prompt_yes_no("Create virtual environment?")
1473
- else:
1474
- create_venv_ok = True
1475
-
1476
- if create_venv_ok:
1477
- require_current_python("Virtual environment")
1478
- create_venv(venv_path)
1479
- python_path = venv_python
1480
- require_supported_python(python_path, "Created venv")
1481
- install_dependencies(venv_path, project_dir)
1482
- print(f" Venv: {green('Created')}")
1483
- print(f" MCP SDK: {green('Installed')}")
1484
- else:
1485
- python_path = Path(args.python) if args.python else Path(sys.executable)
1486
- require_supported_python(python_path, "Selected Python")
1487
- print(f" Using system Python: {python_path}")
1488
-
1489
- # Override python path if explicitly provided
1490
- if args.python:
1491
- python_path = Path(args.python)
1492
- require_supported_python(python_path, "Selected --python")
1493
-
1494
- # ══════════════════════════════════════════════════════════════════════
1495
- # STEP 3: Locate Server Script
1496
- # ══════════════════════════════════════════════════════════════════════
1497
-
1498
- if interactive:
1499
- print_step(3, total_steps, "Server Script")
1500
-
1501
- if args.server:
1502
- server_path = Path(args.server).resolve()
1503
- else:
1504
- # Try to find the compound server first.
1505
- candidates = [
1506
- project_dir / "src" / "server.py",
1507
- project_dir / "src" / "resolve_mcp_server.py",
1508
- project_dir / "src" / "main.py",
1509
- project_dir / "resolve_mcp_server.py",
1510
- ]
1511
- server_path = None
1512
- for c in candidates:
1513
- if c.exists():
1514
- server_path = c
1515
- break
1516
- if server_path is None:
1517
- server_path = candidates[0] # default even if not found yet
1518
-
1519
- if server_path.exists():
1520
- print(f" Server: {green(str(server_path))}")
1521
- else:
1522
- print(f" Server: {yellow(str(server_path))} {dim('(file not found — check path)')}")
1523
-
1524
- # ══════════════════════════════════════════════════════════════════════
1525
- # STEP 4: Configure MCP Clients
1526
- # ══════════════════════════════════════════════════════════════════════
1527
-
1528
- if interactive:
1529
- print_step(4, total_steps, "MCP Client Configuration")
1530
-
1531
- # Determine which clients to configure
1532
- if args.clients:
1533
- if args.clients.lower() == "all":
1534
- selected_ids = CLIENT_IDS
1535
- elif args.clients.lower() == "manual":
1536
- selected_ids = ["manual"]
1537
- else:
1538
- selected_ids = [s.strip() for s in args.clients.split(",")]
1539
- elif interactive:
1540
- selected_ids = prompt_clients()
1541
- else:
1542
- selected_ids = []
1543
-
1544
- # Show manual config if requested
1545
- show_manual = "manual" in selected_ids
1546
- client_ids = [c for c in selected_ids if c != "manual"]
1547
-
1548
- configured = []
1549
- skipped = []
1550
-
1551
- for client_id in client_ids:
1552
- client = next((c for c in MCP_CLIENTS if c["id"] == client_id), None)
1553
- if not client:
1554
- print(f" {yellow('Unknown client:')} {client_id}")
1555
- skipped.append(client_id)
1556
- continue
1557
-
1558
- success, message = write_client_config(
1559
- client, python_path, server_path, api_path, lib_path, dry_run=args.dry_run
1560
- )
1561
-
1562
- if success:
1563
- if args.dry_run:
1564
- print(f"\n {cyan(client['name'])} {dim('(dry run)')}")
1565
- for line in message.split("\n"):
1566
- print(f" {line}")
1567
- configured.append(client["name"])
1568
- else:
1569
- print(f" {green('✓')} {client['name']:<28} → {dim(message)}")
1570
- configured.append(client["name"])
1571
- else:
1572
- print(f" {yellow('⊘')} {client['name']:<28} {dim(message)}")
1573
- skipped.append(client["name"])
1574
-
1575
- # Show manual config
1576
- if show_manual:
1577
- standard, vscode_fmt, zed_fmt, opencode_fmt = generate_manual_config(
1578
- python_path, server_path, api_path, lib_path
1579
- )
1580
- env_preview = build_server_env(python_path, api_path, lib_path)
1581
- print(f"\n {bold('Manual Configuration')}")
1582
- print(f" {'─' * 50}")
1583
- print(f"\n {cyan('Standard format')} (Claude Desktop, Cursor, Windsurf, Cline, Roo Code, Continue):")
1584
- print()
1585
- for line in standard.split("\n"):
1586
- print(f" {line}")
1587
- print(f"\n {cyan('VS Code format')} (GitHub Copilot agent mode — save as .vscode/mcp.json):")
1588
- print()
1589
- for line in vscode_fmt.split("\n"):
1590
- print(f" {line}")
1591
- print(f"\n {cyan('Zed format')} (add to ~/.config/zed/settings.json):")
1592
- print()
1593
- for line in zed_fmt.split("\n"):
1594
- print(f" {line}")
1595
- print(f"\n {cyan('OpenCode format')} (add to ~/.config/opencode/opencode.json or a project opencode.json):")
1596
- print()
1597
- for line in opencode_fmt.split("\n"):
1598
- print(f" {line}")
1599
- print(f"\n {cyan('JetBrains IDEs')} (IntelliJ, WebStorm, PyCharm, etc.):")
1600
- print(f" Settings → Tools → AI Assistant → Model Context Protocol (MCP)")
1601
- print(f" Add server with command: {python_path} {server_path}")
1602
- for key, value in env_preview.items():
1603
- print(f" Set env: {key}={value}")
1604
- print()
1605
-
1606
- if not selected_ids:
1607
- print(f" {dim('No clients selected — skipping configuration')}")
1608
-
1609
- # ══════════════════════════════════════════════════════════════════════
1610
- # STEP 5: Verify Connection
1611
- # ══════════════════════════════════════════════════════════════════════
1612
-
1613
- if interactive:
1614
- print_step(5, total_steps, "Verification")
1615
-
1616
- if api_path:
1617
- success, message = verify_resolve_connection(python_path, api_path, lib_path)
1618
- try:
1619
- py_abi_risk = is_abi_risk_python_version(_version_for_python(python_path))
1620
- except Exception:
1621
- py_abi_risk = False
1622
- if success:
1623
- if "not running" in message.lower():
1624
- print(f" API: {green('Module loads OK')}")
1625
- # If Resolve IS running but the bridge still reported no connection
1626
- # on a 3.13+ interpreter, that is the ABI-mismatch signature.
1627
- if resolve_running and py_abi_risk:
1628
- print(
1629
- f" Resolve: {yellow('Running, but the scripting bridge returned no connection')}"
1630
- )
1631
- print(
1632
- f" This can happen on Python 3.13+ with older Resolve builds. "
1633
- f"If MCP tools fail, recreate the venv with Python 3.10-3.12."
1634
- )
1635
- else:
1636
- print(f" Resolve: {yellow('Not running')} — start Resolve to use MCP tools")
1637
- else:
1638
- print(f" Connected: {green(message)}")
1639
- else:
1640
- print(f" Verify: {yellow(message)}")
1641
- if py_abi_risk:
1642
- print(
1643
- f" On Python 3.13+ this may be an ABI mismatch with Resolve's "
1644
- f"scripting library — try Python 3.10-3.12 if it persists."
1645
- )
1646
- else:
1647
- print(f" {yellow('Skipped')} — Resolve API path not detected")
1648
-
1649
- # ══════════════════════════════════════════════════════════════════════
1650
- # Summary
1651
- # ══════════════════════════════════════════════════════════════════════
1652
-
1653
- print(f"\n {'═' * 50}")
1654
- if configured or show_manual:
1655
- print(f" {green(bold('Setup complete!'))}")
1656
- if configured:
1657
- print(f" Configured: {', '.join(configured)}")
1658
- print()
1659
- print(f" {bold('Next steps:')}")
1660
- if not resolve_running:
1661
- print(f" 1. Start DaVinci Resolve")
1662
- print(f" 2. Open your MCP client")
1663
- print(f" 3. Start using natural language to control Resolve!")
1664
- else:
1665
- print(f" 1. Open your MCP client")
1666
- print(f" 2. Start using natural language to control Resolve!")
1667
- print()
1668
- print(f" {dim(f'Server: {server_path}')}")
1669
- print(f" {dim(f'Python: {python_path}')}")
1670
- if api_path:
1671
- print(f" {dim(f'API: {api_path}')}")
1672
- elif not selected_ids:
1673
- print(f" {green(bold('Environment ready!'))}")
1674
- print(f" Run {cyan('python install.py --clients all')} to configure MCP clients later.")
1675
- else:
1676
- print(f" {yellow('No clients configured.')}")
1677
- print(f" Run {cyan('python install.py')} again to retry.")
1678
-
1679
- print()
1680
-
1681
-
1682
- if __name__ == "__main__":
1683
- try:
1684
- main()
1685
- except KeyboardInterrupt:
1686
- print(f"\n\n {dim('Interrupted.')}\n")
1687
- sys.exit(1)