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