@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,755 +0,0 @@
1
- """Memory and heartbeat layer for V2 analysis architecture.
2
-
3
- Per the V2 shot schema spec §9 and the project_v2_architecture memory,
4
- each project's analysis root carries narrative memory alongside the structured DB:
5
-
6
- {analysis_root}/memory/
7
- bin_summary.md - Regenerated post-analysis: machine's first impression of the bin.
8
- session_notes/ - One file per chat session, dated.
9
- corrections.md - Running log of what humans fixed and why.
10
- decisions.md - Load-bearing conclusions reached together.
11
-
12
- {analysis_root}/heartbeat.json
13
- Current-state snapshot updated by the analysis pipeline:
14
- last run timestamp, clip counts, what's pending, recent failures,
15
- what's new since session N. Read at session start so the LLM walks in
16
- knowing what changed overnight.
17
-
18
- The user-scoped Soul layer lives separately at ~/davinci-resolve-mcp/soul/
19
- (see soul scaffolding in P5).
20
- """
21
-
22
- from __future__ import annotations
23
-
24
- import json
25
- import os
26
- import time
27
- from pathlib import Path
28
- from typing import Any, Dict, Iterable, List, Optional, Tuple
29
-
30
-
31
- MEMORY_DIR_NAME = "memory"
32
- HEARTBEAT_FILENAME = "heartbeat.json"
33
- BIN_SUMMARY_FILENAME = "bin_summary.md"
34
- SESSION_NOTES_DIR_NAME = "session_notes"
35
- CORRECTIONS_FILENAME = "corrections.md"
36
- DECISIONS_FILENAME = "decisions.md"
37
-
38
- # V2 B6: panel_state.json — shared state between chat and the control panel.
39
- # Lives next to heartbeat.json at the project's analysis root. Either side can
40
- # write; both read at session start. Single-user model, last-write-wins.
41
- PANEL_STATE_FILENAME = "panel_state.json"
42
- PANEL_STATE_SCHEMA_VERSION = "2.0"
43
-
44
- HEARTBEAT_SCHEMA_VERSION = "2.0"
45
-
46
- # ---- User-scoped soul layer (cross-project) ----
47
- # Soul lives outside any specific project's analysis root, under a `_soul`
48
- # directory sibling to project directories. The underscore prefix marks it
49
- # as user-scoped, not a project. Cross-project continuity: when Sam starts a
50
- # new project the soul still applies; per-project memory + DB are fresh.
51
- SOUL_DIR_NAME = "_soul"
52
- SOUL_PERSPECTIVE_FILENAME = "perspective.md"
53
- SOUL_WORKING_STYLE_FILENAME = "working_style.md"
54
- SOUL_LEARNED_FILENAME = "learned_from_corrections.md"
55
-
56
-
57
- def _now_iso() -> str:
58
- return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
59
-
60
-
61
- def _today_date() -> str:
62
- return time.strftime("%Y-%m-%d", time.gmtime())
63
-
64
-
65
- def memory_dir(project_root: str) -> str:
66
- return os.path.join(project_root, MEMORY_DIR_NAME)
67
-
68
-
69
- def session_notes_dir(project_root: str) -> str:
70
- return os.path.join(memory_dir(project_root), SESSION_NOTES_DIR_NAME)
71
-
72
-
73
- def heartbeat_path(project_root: str) -> str:
74
- return os.path.join(project_root, HEARTBEAT_FILENAME)
75
-
76
-
77
- def bin_summary_path(project_root: str) -> str:
78
- return os.path.join(memory_dir(project_root), BIN_SUMMARY_FILENAME)
79
-
80
-
81
- def corrections_path(project_root: str) -> str:
82
- return os.path.join(memory_dir(project_root), CORRECTIONS_FILENAME)
83
-
84
-
85
- def decisions_path(project_root: str) -> str:
86
- return os.path.join(memory_dir(project_root), DECISIONS_FILENAME)
87
-
88
-
89
- def ensure_memory_structure(project_root: str) -> Dict[str, Any]:
90
- """Create the memory directory structure if it doesn't exist. Idempotent."""
91
- created = []
92
- for d in (memory_dir(project_root), session_notes_dir(project_root)):
93
- if not os.path.isdir(d):
94
- os.makedirs(d, exist_ok=True)
95
- created.append(d)
96
- # Seed the long-running narrative files if absent
97
- for path, header in (
98
- (corrections_path(project_root), "# Corrections log\n\nRunning record of what was corrected, by whom, when, and why.\n\n"),
99
- (decisions_path(project_root), "# Decisions log\n\nLoad-bearing conclusions reached during work on this project.\n\n"),
100
- ):
101
- if not os.path.isfile(path):
102
- with open(path, "w", encoding="utf-8") as handle:
103
- handle.write(header)
104
- created.append(path)
105
- return {
106
- "success": True,
107
- "project_root": project_root,
108
- "memory_dir": memory_dir(project_root),
109
- "session_notes_dir": session_notes_dir(project_root),
110
- "created": created,
111
- }
112
-
113
-
114
- # ============ Heartbeat ============
115
-
116
- def read_heartbeat(project_root: str) -> Optional[Dict[str, Any]]:
117
- """Read heartbeat.json. Returns None if it doesn't exist or is malformed."""
118
- path = heartbeat_path(project_root)
119
- if not os.path.isfile(path):
120
- return None
121
- try:
122
- with open(path, "r", encoding="utf-8") as handle:
123
- return json.load(handle)
124
- except (OSError, json.JSONDecodeError):
125
- return None
126
-
127
-
128
- def write_heartbeat(project_root: str, payload: Dict[str, Any]) -> Dict[str, Any]:
129
- """Write heartbeat.json atomically."""
130
- ensure_memory_structure(project_root)
131
- payload = dict(payload)
132
- payload.setdefault("schema_version", HEARTBEAT_SCHEMA_VERSION)
133
- payload["updated_at"] = _now_iso()
134
- path = heartbeat_path(project_root)
135
- tmp_path = path + ".tmp"
136
- try:
137
- with open(tmp_path, "w", encoding="utf-8") as handle:
138
- json.dump(payload, handle, indent=2, sort_keys=True)
139
- os.replace(tmp_path, path)
140
- return {"success": True, "path": path}
141
- except OSError as exc:
142
- if os.path.isfile(tmp_path):
143
- try:
144
- os.remove(tmp_path)
145
- except OSError:
146
- pass
147
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
148
-
149
-
150
- def update_heartbeat(
151
- project_root: str,
152
- *,
153
- last_run: Optional[Dict[str, Any]] = None,
154
- clip_counts: Optional[Dict[str, int]] = None,
155
- pending: Optional[List[Dict[str, Any]]] = None,
156
- recent_failures: Optional[List[Dict[str, Any]]] = None,
157
- new_since_last_session: Optional[List[Dict[str, Any]]] = None,
158
- notes: Optional[List[str]] = None,
159
- ) -> Dict[str, Any]:
160
- """Update heartbeat fields, preserving keys not explicitly overridden."""
161
- existing = read_heartbeat(project_root) or {}
162
- payload = dict(existing)
163
- if last_run is not None:
164
- payload["last_run"] = last_run
165
- if clip_counts is not None:
166
- payload["clip_counts"] = clip_counts
167
- if pending is not None:
168
- payload["pending"] = pending
169
- if recent_failures is not None:
170
- payload["recent_failures"] = recent_failures
171
- if new_since_last_session is not None:
172
- payload["new_since_last_session"] = new_since_last_session
173
- if notes is not None:
174
- payload["notes"] = notes
175
- return write_heartbeat(project_root, payload)
176
-
177
-
178
- # ============ Bin summary ============
179
-
180
- def regenerate_bin_summary_from_manifest(
181
- project_root: str,
182
- manifest: Dict[str, Any],
183
- *,
184
- project_name: Optional[str] = None,
185
- ) -> Dict[str, Any]:
186
- """Write bin_summary.md from a manifest's per-clip rows.
187
-
188
- V2.0 (this function): a smart aggregator that synthesizes a bin-level
189
- overview from per-clip summaries — clip counts, dominant editorial roles,
190
- top search_tags, recurring locations, select-potential distribution,
191
- energy arc, and a per-clip listing. It's NOT a vision-synthesized
192
- briefing yet.
193
-
194
- V2.1 (deferred): replace with a separate vision pass that reads all
195
- per-clip clip_summary fields + a representative frame per clip, and
196
- produces a true colleague-style briefing paragraph. See C2 in the
197
- gameplan; implementation pattern follows the per-clip vision flow
198
- (deferred payload + commit_bin_summary action).
199
- """
200
- ensure_memory_structure(project_root)
201
- clips = manifest.get("clips") or []
202
- project_name = project_name or manifest.get("project_name") or "(unknown project)"
203
-
204
- # Read per-clip analyses from disk so we can aggregate
205
- enriched: List[Dict[str, Any]] = []
206
- for clip in clips:
207
- info = {
208
- "record": clip.get("record") or {},
209
- "clip": clip,
210
- "report": None,
211
- }
212
- analysis_path = clip.get("analysis_json")
213
- if analysis_path and os.path.isfile(analysis_path):
214
- try:
215
- with open(analysis_path, "r", encoding="utf-8") as handle:
216
- info["report"] = json.load(handle)
217
- except (OSError, json.JSONDecodeError):
218
- pass
219
- enriched.append(info)
220
-
221
- # Aggregate fields from the per-clip visual analyses
222
- primary_use_counts: Dict[str, int] = {}
223
- select_potential_counts: Dict[str, int] = {}
224
- style_counts: Dict[str, int] = {}
225
- energy_counts: Dict[str, int] = {}
226
- tag_counts: Dict[str, int] = {}
227
- location_counts: Dict[str, int] = {}
228
- total_duration = 0.0
229
- longest_clips: List[Tuple[float, str, Optional[str]]] = []
230
- high_select_clips: List[Tuple[str, str, Optional[str]]] = []
231
- pending_vision_count = 0
232
-
233
- def _bump(d: Dict[str, int], key: Optional[str]) -> None:
234
- if key:
235
- d[str(key)] = d.get(str(key), 0) + 1
236
-
237
- for info in enriched:
238
- record = info["record"]
239
- report = info["report"] or {}
240
- visual = report.get("visual") if isinstance(report.get("visual"), dict) else None
241
- duration = _extract_duration_seconds(record)
242
- total_duration += float(duration or 0.0)
243
- clip_name = record.get("clip_name") or "Untitled"
244
- clip_id = record.get("clip_id")
245
- if duration:
246
- longest_clips.append((float(duration), clip_name, clip_id))
247
-
248
- if visual is None:
249
- pending_vision_count += 1
250
- continue
251
- classification = visual.get("editorial_classification") or {}
252
- _bump(primary_use_counts, classification.get("primary_use"))
253
- _bump(select_potential_counts, classification.get("select_potential"))
254
- _bump(style_counts, classification.get("style"))
255
- _bump(energy_counts, classification.get("energy_arc"))
256
- if classification.get("select_potential") == "high":
257
- high_select_clips.append((clip_name, classification.get("primary_use") or "", clip_id))
258
- # Aggregate search tags
259
- editing_notes = visual.get("editing_notes") or {}
260
- for tag in (editing_notes.get("search_tags") or []):
261
- if isinstance(tag, str) and tag.strip():
262
- _bump(tag_counts, tag.strip().lower())
263
- # Aggregate locations from content if structured
264
- content = visual.get("content") or {}
265
- for loc in (content.get("locations") or []):
266
- if isinstance(loc, str) and loc.strip():
267
- _bump(location_counts, loc.strip())
268
-
269
- def _top(d: Dict[str, int], n: int = 5) -> List[Tuple[str, int]]:
270
- return sorted(d.items(), key=lambda kv: (-kv[1], kv[0]))[:n]
271
-
272
- longest_clips.sort(reverse=True)
273
- top_5_longest = longest_clips[:5]
274
-
275
- # Build the markdown
276
- lines: List[str] = []
277
- lines.append(f"# Bin summary — {project_name}\n")
278
- lines.append(f"_Regenerated: {_now_iso()}_\n")
279
- lines.append("_(V2.0 aggregated overview; V2.1 will replace with a vision-synthesized colleague briefing — see C2 in the gameplan.)_\n\n")
280
-
281
- lines.append("## Overview\n\n")
282
- lines.append(f"- **Clip count**: {len(clips)}")
283
- if pending_vision_count:
284
- lines.append(f" ({pending_vision_count} pending vision)")
285
- lines.append("\n")
286
- lines.append(f"- **Total duration**: ~{_fmt_duration(total_duration)}\n")
287
- lines.append(f"- **Analysis schema version**: {manifest.get('analysis_version', 'unknown')}\n\n")
288
-
289
- if primary_use_counts:
290
- lines.append("## Editorial composition\n\n")
291
- lines.append("**Primary use:**\n\n")
292
- for use, n in _top(primary_use_counts, 10):
293
- lines.append(f"- {use}: {n}\n")
294
- lines.append("\n")
295
-
296
- if select_potential_counts:
297
- lines.append("**Select potential distribution:**\n\n")
298
- for level in ("high", "medium", "low"):
299
- if level in select_potential_counts:
300
- lines.append(f"- {level}: {select_potential_counts[level]}\n")
301
- lines.append("\n")
302
-
303
- if style_counts:
304
- lines.append("**Dominant style:**\n\n")
305
- for s, n in _top(style_counts, 5):
306
- lines.append(f"- {s}: {n}\n")
307
- lines.append("\n")
308
-
309
- if energy_counts:
310
- lines.append("**Energy arcs:**\n\n")
311
- for e, n in _top(energy_counts, 5):
312
- lines.append(f"- {e}: {n}\n")
313
- lines.append("\n")
314
-
315
- if tag_counts:
316
- lines.append("## Top tags\n\n")
317
- top_tags = _top(tag_counts, 20)
318
- lines.append("`" + "` `".join(f"{t} ({n})" for t, n in top_tags) + "`\n\n")
319
-
320
- if location_counts:
321
- lines.append("## Recurring locations\n\n")
322
- for loc, n in _top(location_counts, 10):
323
- lines.append(f"- {loc}: {n}\n")
324
- lines.append("\n")
325
-
326
- if high_select_clips:
327
- lines.append("## High select-potential clips\n\n")
328
- for name, use, cid in high_select_clips[:10]:
329
- cid_text = f" `{cid}`" if cid else ""
330
- lines.append(f"- **{name}**{cid_text} — {use}\n")
331
- lines.append("\n")
332
-
333
- if top_5_longest:
334
- lines.append("## Longest clips\n\n")
335
- for dur, name, cid in top_5_longest:
336
- cid_text = f" `{cid}`" if cid else ""
337
- lines.append(f"- **{name}**{cid_text} — {_fmt_duration(dur)}\n")
338
- lines.append("\n")
339
-
340
- lines.append("## Clips\n\n")
341
- for index, info in enumerate(enriched, 1):
342
- record = info["record"]
343
- name = record.get("clip_name") or record.get("file_path") or f"Clip {index}"
344
- clip_id = record.get("clip_id") or "(no id)"
345
- duration = _fmt_duration(_extract_duration_seconds(record))
346
- bin_path = record.get("bin_path") or ""
347
- lines.append(f"### {index}. {name}\n\n")
348
- lines.append(f"- **clip_id**: `{clip_id}`\n")
349
- lines.append(f"- **duration**: {duration}\n")
350
- if bin_path:
351
- lines.append(f"- **bin**: {bin_path}\n")
352
-
353
- summary = _read_clip_summary(info["clip"])
354
- if summary:
355
- lines.append(f"\n{summary}\n")
356
- else:
357
- lines.append("\n_Summary not yet available (vision pending or analysis incomplete)._\n")
358
- lines.append("\n")
359
-
360
- path = bin_summary_path(project_root)
361
- # Atomic write (temp + os.replace): a crash mid-write must not truncate the
362
- # bin summary that session_start_context reads at startup (PS4).
363
- tmp_path = path + ".tmp"
364
- try:
365
- with open(tmp_path, "w", encoding="utf-8") as handle:
366
- handle.writelines(lines)
367
- os.replace(tmp_path, path)
368
- return {
369
- "success": True,
370
- "path": path,
371
- "clip_count": len(clips),
372
- "pending_vision_count": pending_vision_count,
373
- "aggregates": {
374
- "primary_use": primary_use_counts,
375
- "select_potential": select_potential_counts,
376
- "style": style_counts,
377
- "energy_arc": energy_counts,
378
- "top_tags": dict(_top(tag_counts, 20)),
379
- "top_locations": dict(_top(location_counts, 10)),
380
- },
381
- }
382
- except OSError as exc:
383
- if os.path.isfile(tmp_path):
384
- try:
385
- os.remove(tmp_path)
386
- except OSError:
387
- pass
388
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
389
-
390
-
391
- def _extract_duration_seconds(record: Dict[str, Any]) -> float:
392
- # Try a few common shapes the record may carry duration in
393
- for key in ("duration_seconds", "duration", "Duration"):
394
- value = record.get(key)
395
- if isinstance(value, (int, float)):
396
- return float(value)
397
- return 0.0
398
-
399
-
400
- def _fmt_duration(seconds: float) -> str:
401
- if seconds <= 0:
402
- return "0s"
403
- if seconds < 60:
404
- return f"{seconds:.1f}s"
405
- minutes, sec = divmod(int(seconds), 60)
406
- if minutes < 60:
407
- return f"{minutes}m{sec:02d}s"
408
- hours, minutes = divmod(minutes, 60)
409
- return f"{hours}h{minutes:02d}m"
410
-
411
-
412
- def _read_clip_summary(clip: Dict[str, Any]) -> Optional[str]:
413
- """Try to read the clip_summary from the clip's analysis.json if present."""
414
- analysis_json_path = clip.get("analysis_json")
415
- if not analysis_json_path or not os.path.isfile(analysis_json_path):
416
- return None
417
- try:
418
- with open(analysis_json_path, "r", encoding="utf-8") as handle:
419
- report = json.load(handle)
420
- except (OSError, json.JSONDecodeError):
421
- return None
422
- visual = report.get("visual") or {}
423
- summary = visual.get("clip_summary")
424
- if isinstance(summary, str) and summary.strip():
425
- return summary.strip()
426
- if isinstance(summary, dict):
427
- para = summary.get("paragraph") or summary.get("one_line")
428
- if isinstance(para, str) and para.strip():
429
- return para.strip()
430
- return None
431
-
432
-
433
- # ============ Session notes ============
434
-
435
- def record_session_note(project_root: str, note: str, *, session_id: Optional[str] = None) -> Dict[str, Any]:
436
- """Append a session note to today's session_notes file."""
437
- ensure_memory_structure(project_root)
438
- date = _today_date()
439
- path = os.path.join(session_notes_dir(project_root), f"{date}.md")
440
- header = f"# Session notes — {date}\n\n" if not os.path.isfile(path) else ""
441
- entry = f"## {_now_iso()}" + (f" ({session_id})" if session_id else "") + "\n\n" + note.strip() + "\n\n"
442
- try:
443
- with open(path, "a", encoding="utf-8") as handle:
444
- if header:
445
- handle.write(header)
446
- handle.write(entry)
447
- return {"success": True, "path": path}
448
- except OSError as exc:
449
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
450
-
451
-
452
- def record_correction(
453
- project_root: str,
454
- *,
455
- entity_type: str,
456
- entity_uuid: str,
457
- field_path: str,
458
- previous_value: Any,
459
- new_value: Any,
460
- author: str,
461
- reason: Optional[str] = None,
462
- ) -> Dict[str, Any]:
463
- """Append a correction entry to corrections.md."""
464
- ensure_memory_structure(project_root)
465
- path = corrections_path(project_root)
466
- entry_lines = [
467
- f"## {_now_iso()} — {author}\n",
468
- f"- **target**: {entity_type} `{entity_uuid}` field `{field_path}`\n",
469
- f"- **previous**: `{_short_repr(previous_value)}`\n",
470
- f"- **new**: `{_short_repr(new_value)}`\n",
471
- ]
472
- if reason:
473
- entry_lines.append(f"- **why**: {reason}\n")
474
- entry_lines.append("\n")
475
- try:
476
- with open(path, "a", encoding="utf-8") as handle:
477
- handle.write("".join(entry_lines))
478
- return {"success": True, "path": path}
479
- except OSError as exc:
480
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
481
-
482
-
483
- def record_decision(
484
- project_root: str,
485
- *,
486
- title: str,
487
- description: str,
488
- author: str,
489
- tags: Optional[Iterable[str]] = None,
490
- ) -> Dict[str, Any]:
491
- """Append a load-bearing decision to decisions.md."""
492
- ensure_memory_structure(project_root)
493
- path = decisions_path(project_root)
494
- tag_line = ""
495
- if tags:
496
- tag_str = ", ".join(f"`{t}`" for t in tags)
497
- tag_line = f"_Tags: {tag_str}_\n\n"
498
- entry = f"## {_now_iso()} — {title}\n\n_By: {author}_\n\n{tag_line}{description.strip()}\n\n"
499
- try:
500
- with open(path, "a", encoding="utf-8") as handle:
501
- handle.write(entry)
502
- return {"success": True, "path": path}
503
- except OSError as exc:
504
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
505
-
506
-
507
- def _short_repr(value: Any, limit: int = 200) -> str:
508
- if value is None:
509
- return "null"
510
- if isinstance(value, (str, int, float, bool)):
511
- s = str(value)
512
- else:
513
- try:
514
- s = json.dumps(value, sort_keys=True)
515
- except (TypeError, ValueError):
516
- s = repr(value)
517
- return s if len(s) <= limit else s[:limit] + "…"
518
-
519
-
520
- # ============ Panel state (V2 B6 — chat ↔ control panel sync) ============
521
-
522
- def panel_state_path(project_root: str) -> str:
523
- return os.path.join(project_root, PANEL_STATE_FILENAME)
524
-
525
-
526
- def read_panel_state(project_root: str) -> Optional[Dict[str, Any]]:
527
- """Read panel_state.json. Returns None if absent or unreadable.
528
-
529
- The state file holds: current_clip_id, current_shot_index, current_view
530
- ('bin' | 'clip' | 'shot'), focus_history, plus whatever either chat or
531
- the panel writes. Either side can read; either side can write.
532
- """
533
- path = panel_state_path(project_root)
534
- if not os.path.isfile(path):
535
- return None
536
- try:
537
- with open(path, "r", encoding="utf-8") as handle:
538
- return json.load(handle)
539
- except (OSError, json.JSONDecodeError):
540
- return None
541
-
542
-
543
- def write_panel_state(
544
- project_root: str,
545
- updates: Dict[str, Any],
546
- *,
547
- written_by: Optional[str] = None,
548
- merge: bool = True,
549
- ) -> Dict[str, Any]:
550
- """Write panel_state.json atomically.
551
-
552
- `updates` is merged into the existing state by default (merge=True) so
553
- callers can partial-update fields. Pass merge=False to replace.
554
- """
555
- ensure_memory_structure(project_root)
556
- path = panel_state_path(project_root)
557
- if merge:
558
- existing = read_panel_state(project_root) or {}
559
- payload = dict(existing)
560
- payload.update(updates)
561
- else:
562
- payload = dict(updates)
563
- payload["schema_version"] = PANEL_STATE_SCHEMA_VERSION
564
- payload["updated_at"] = _now_iso()
565
- if written_by:
566
- payload["last_written_by"] = written_by
567
- tmp_path = path + ".tmp"
568
- try:
569
- with open(tmp_path, "w", encoding="utf-8") as handle:
570
- json.dump(payload, handle, indent=2, sort_keys=True)
571
- os.replace(tmp_path, path)
572
- return {"success": True, "path": path, "state": payload}
573
- except OSError as exc:
574
- if os.path.isfile(tmp_path):
575
- try:
576
- os.remove(tmp_path)
577
- except OSError:
578
- pass
579
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
580
-
581
-
582
- # ============ Soul (user-scoped, cross-project) ============
583
-
584
- def soul_dir(analysis_base_root: str) -> str:
585
- """Return the user-scoped soul directory path.
586
-
587
- `analysis_base_root` is the parent of all project analysis roots
588
- (typically `~/Documents/davinci-resolve-mcp-analysis`). The soul lives
589
- at `{base}/_soul/` so it's discoverable alongside project data but
590
- clearly marked user-scoped via the underscore prefix.
591
- """
592
- return os.path.join(analysis_base_root, SOUL_DIR_NAME)
593
-
594
-
595
- def ensure_soul_structure(analysis_base_root: str) -> Dict[str, Any]:
596
- """Create the soul directory + initial files if absent. Idempotent."""
597
- d = soul_dir(analysis_base_root)
598
- if not os.path.isdir(d):
599
- os.makedirs(d, exist_ok=True)
600
-
601
- created = []
602
- seeds = {
603
- SOUL_PERSPECTIVE_FILENAME: _SOUL_PERSPECTIVE_TEMPLATE,
604
- SOUL_WORKING_STYLE_FILENAME: _SOUL_WORKING_STYLE_TEMPLATE,
605
- SOUL_LEARNED_FILENAME: _SOUL_LEARNED_TEMPLATE,
606
- }
607
- for filename, template in seeds.items():
608
- path = os.path.join(d, filename)
609
- if not os.path.isfile(path):
610
- with open(path, "w", encoding="utf-8") as handle:
611
- handle.write(template)
612
- created.append(path)
613
-
614
- return {
615
- "success": True,
616
- "soul_dir": d,
617
- "created": created,
618
- }
619
-
620
-
621
- def read_soul(analysis_base_root: str) -> Dict[str, Optional[str]]:
622
- """Read all soul files. Returns dict mapping filename → contents (or None)."""
623
- d = soul_dir(analysis_base_root)
624
- out: Dict[str, Optional[str]] = {}
625
- for name in (SOUL_PERSPECTIVE_FILENAME, SOUL_WORKING_STYLE_FILENAME, SOUL_LEARNED_FILENAME):
626
- path = os.path.join(d, name)
627
- if os.path.isfile(path):
628
- try:
629
- with open(path, "r", encoding="utf-8") as handle:
630
- out[name] = handle.read()
631
- except OSError:
632
- out[name] = None
633
- else:
634
- out[name] = None
635
- return out
636
-
637
-
638
- def append_to_soul(
639
- analysis_base_root: str,
640
- filename: str,
641
- entry: str,
642
- *,
643
- author: str,
644
- why: Optional[str] = None,
645
- ) -> Dict[str, Any]:
646
- """Append an entry to a soul file, with timestamp and author header.
647
-
648
- The soul evolves slowly; entries are added when an observation generalizes
649
- beyond a single project. Pruning / consolidation is V2.1+ work.
650
- """
651
- if filename not in (SOUL_PERSPECTIVE_FILENAME, SOUL_WORKING_STYLE_FILENAME, SOUL_LEARNED_FILENAME):
652
- return {"success": False, "error": f"Unknown soul file: {filename}"}
653
- ensure_soul_structure(analysis_base_root)
654
- path = os.path.join(soul_dir(analysis_base_root), filename)
655
- block = [f"\n## {_now_iso()} — {author}\n"]
656
- if why:
657
- block.append(f"_Why: {why}_\n\n")
658
- block.append(entry.strip() + "\n")
659
- try:
660
- with open(path, "a", encoding="utf-8") as handle:
661
- handle.write("".join(block))
662
- return {"success": True, "path": path}
663
- except OSError as exc:
664
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
665
-
666
-
667
- _SOUL_PERSPECTIVE_TEMPLATE = """# Soul — Perspective
668
-
669
- User-scoped, cross-project. The accumulated aesthetic and editorial perspective
670
- that carries between projects. Slow-evolving. Updated when something
671
- generalizes beyond a single project's specifics.
672
-
673
- This file is one of three soul layers. The others:
674
- - `working_style.md` — how the user and I collaborate
675
- - `learned_from_corrections.md` — patterns from corrections across projects
676
-
677
- ---
678
- """
679
-
680
- _SOUL_WORKING_STYLE_TEMPLATE = """# Soul — Working style
681
-
682
- User-scoped, cross-project. How the user and I work together — pace,
683
- preferences, the rhythm of our collaboration.
684
-
685
- Entries added when a working-style observation feels durable across projects,
686
- not specific to this one.
687
-
688
- ---
689
- """
690
-
691
- _SOUL_LEARNED_TEMPLATE = """# Soul — Learned from corrections
692
-
693
- User-scoped, cross-project. Patterns from corrections the user has made across
694
- projects. Generalized lessons, not specific fixes.
695
-
696
- Example pattern: "User consistently prefers concrete physical description over
697
- interpretation when subject identity is uncertain. Hedge identity claims;
698
- describe what's visible."
699
-
700
- Specific per-project corrections live in `{project}/memory/corrections.md`.
701
- This file is for what generalizes across projects.
702
-
703
- ---
704
- """
705
-
706
-
707
- # ============ Session start protocol ============
708
-
709
- def session_start_context(
710
- project_root: str,
711
- *,
712
- analysis_base_root: Optional[str] = None,
713
- ) -> Dict[str, Any]:
714
- """Assemble the context an LLM should load at session start.
715
-
716
- Read order: soul (cross-project) → project memory → heartbeat → DB on demand.
717
-
718
- Args:
719
- project_root: This project's analysis root.
720
- analysis_base_root: Parent of all project analysis roots, for the soul.
721
- If None, derived as the parent of project_root.
722
- """
723
- ensure_memory_structure(project_root)
724
- if analysis_base_root is None:
725
- analysis_base_root = os.path.dirname(project_root)
726
- ensure_soul_structure(analysis_base_root)
727
-
728
- heartbeat = read_heartbeat(project_root)
729
- summary_path = bin_summary_path(project_root)
730
- notes_dir = session_notes_dir(project_root)
731
- recent_notes: List[str] = []
732
- if os.path.isdir(notes_dir):
733
- try:
734
- files = sorted(os.listdir(notes_dir), reverse=True)
735
- recent_notes = [os.path.join(notes_dir, f) for f in files[:7] if f.endswith(".md")]
736
- except OSError:
737
- pass
738
-
739
- return {
740
- "project_root": project_root,
741
- "analysis_base_root": analysis_base_root,
742
- # Soul (cross-project, user-scoped) — read first; informs perspective.
743
- "soul_dir": soul_dir(analysis_base_root),
744
- "soul_files": read_soul(analysis_base_root),
745
- # Project-scoped narrative
746
- "heartbeat": heartbeat,
747
- "heartbeat_path": heartbeat_path(project_root),
748
- "bin_summary_path": summary_path if os.path.isfile(summary_path) else None,
749
- "recent_session_notes": recent_notes,
750
- "corrections_path": corrections_path(project_root) if os.path.isfile(corrections_path(project_root)) else None,
751
- "decisions_path": decisions_path(project_root) if os.path.isfile(decisions_path(project_root)) else None,
752
- # Shared state with control panel (V2 B6)
753
- "panel_state": read_panel_state(project_root),
754
- "panel_state_path": panel_state_path(project_root),
755
- }