@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,678 +0,0 @@
|
|
|
1
|
-
"""Analysis cap layer — token budgets, frame caps, response trimming.
|
|
2
|
-
|
|
3
|
-
Six cap dimensions, each with named presets (minimal/standard/generous/unlimited)
|
|
4
|
-
and per-field overrides. Caps are advisory at the call site: the analysis
|
|
5
|
-
pipeline consults `check_budget()` before vision/transcription calls and
|
|
6
|
-
`record_usage()` after; over-budget calls return a `BudgetDecision` with
|
|
7
|
-
`allowed=False` and the caller is expected to surface a clean error rather
|
|
8
|
-
than silently spending money.
|
|
9
|
-
|
|
10
|
-
Budgets are tracked in `analysis_token_usage` (schema v5 in
|
|
11
|
-
`timeline_brain_db.py`), scoped per-clip / per-job / per-day. The day_bucket
|
|
12
|
-
column makes per-day rollups one indexed scan.
|
|
13
|
-
|
|
14
|
-
Pricing-to-USD conversion is intentionally NOT in this module — the spec
|
|
15
|
-
deferred a "$ cost cap" axis. Token budgets are the unit of currency here.
|
|
16
|
-
|
|
17
|
-
## Preset rationale
|
|
18
|
-
|
|
19
|
-
| Dimension | minimal | standard | generous | unlimited |
|
|
20
|
-
|-----------|--------:|---------:|---------:|----------:|
|
|
21
|
-
| response_chars | 5,000 | 25,000 | 100,000 | None |
|
|
22
|
-
| vision_tokens_per_clip | 16,000 | 100,000 | 250,000 | None |
|
|
23
|
-
| frames_per_clip | 12 | 80 | 200 | None |
|
|
24
|
-
| vision_tokens_per_job | 60,000 | 1,000,000 | 3,000,000 | None |
|
|
25
|
-
| vision_tokens_per_day | 150,000 | 2,000,000 | 6,000,000 | None |
|
|
26
|
-
| wall_clock_seconds_per_call | 30 | 90 | 300 | None |
|
|
27
|
-
| max_frame_dim_pixels | 512 | 768 | 1280 | None |
|
|
28
|
-
|
|
29
|
-
`minimal` = preview/triage mode. `standard` = realistic per-project default.
|
|
30
|
-
`generous` = high-fidelity analysis on a few specific clips. `unlimited` = all
|
|
31
|
-
guards off; use only when you're certain about the input size.
|
|
32
|
-
|
|
33
|
-
NOTE on `frames_per_clip`: this is a *safety ceiling*, not the primary frame
|
|
34
|
-
dial. How many frames a clip actually gets is chosen by the `sampling_mode`
|
|
35
|
-
(Economy/Balanced/Thorough — see media_analysis.SAMPLING_MODES), which is
|
|
36
|
-
duration- and content-aware. `frames_per_clip` only clips the result if the mode
|
|
37
|
-
would exceed it. The standard ceiling (80) matches the default Thorough ceiling
|
|
38
|
-
so the mode is never silently truncated; lower it to hard-cap cost, raise it for
|
|
39
|
-
unusually long/cutty clips. (Before sampling modes existed this defaulted to 8
|
|
40
|
-
and *was* the frame dial — that flat cap is what made long clips under-covered.)
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
from __future__ import annotations
|
|
44
|
-
|
|
45
|
-
import logging
|
|
46
|
-
import time
|
|
47
|
-
from dataclasses import asdict, dataclass, replace
|
|
48
|
-
from typing import Any, Dict, List, Optional
|
|
49
|
-
|
|
50
|
-
from src.utils import timeline_brain_db
|
|
51
|
-
|
|
52
|
-
logger = logging.getLogger("resolve-mcp.analysis-caps")
|
|
53
|
-
|
|
54
|
-
PRESET_MINIMAL = "minimal"
|
|
55
|
-
PRESET_STANDARD = "standard"
|
|
56
|
-
PRESET_GENEROUS = "generous"
|
|
57
|
-
PRESET_UNLIMITED = "unlimited"
|
|
58
|
-
DEFAULT_PRESET = PRESET_STANDARD
|
|
59
|
-
|
|
60
|
-
SCOPE_CLIP = "clip"
|
|
61
|
-
SCOPE_JOB = "job"
|
|
62
|
-
SCOPE_DAY = "day"
|
|
63
|
-
SCOPE_SESSION = "session"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@dataclass(frozen=True)
|
|
67
|
-
class Caps:
|
|
68
|
-
"""Effective cap values. `None` means uncapped on that dimension."""
|
|
69
|
-
response_chars: Optional[int]
|
|
70
|
-
vision_tokens_per_clip: Optional[int]
|
|
71
|
-
frames_per_clip: Optional[int]
|
|
72
|
-
vision_tokens_per_job: Optional[int]
|
|
73
|
-
vision_tokens_per_day: Optional[int]
|
|
74
|
-
wall_clock_seconds_per_call: Optional[int]
|
|
75
|
-
max_frame_dim_pixels: Optional[int]
|
|
76
|
-
preset: str = DEFAULT_PRESET
|
|
77
|
-
|
|
78
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
79
|
-
return asdict(self)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
CAP_PRESETS: Dict[str, Caps] = {
|
|
83
|
-
PRESET_MINIMAL: Caps(
|
|
84
|
-
preset=PRESET_MINIMAL,
|
|
85
|
-
response_chars=5_000,
|
|
86
|
-
vision_tokens_per_clip=16_000,
|
|
87
|
-
frames_per_clip=12,
|
|
88
|
-
vision_tokens_per_job=60_000,
|
|
89
|
-
vision_tokens_per_day=150_000,
|
|
90
|
-
wall_clock_seconds_per_call=30,
|
|
91
|
-
max_frame_dim_pixels=512,
|
|
92
|
-
),
|
|
93
|
-
PRESET_STANDARD: Caps(
|
|
94
|
-
preset=PRESET_STANDARD,
|
|
95
|
-
response_chars=25_000,
|
|
96
|
-
vision_tokens_per_clip=100_000,
|
|
97
|
-
frames_per_clip=80,
|
|
98
|
-
vision_tokens_per_job=1_000_000,
|
|
99
|
-
vision_tokens_per_day=2_000_000,
|
|
100
|
-
wall_clock_seconds_per_call=90,
|
|
101
|
-
max_frame_dim_pixels=768,
|
|
102
|
-
),
|
|
103
|
-
PRESET_GENEROUS: Caps(
|
|
104
|
-
preset=PRESET_GENEROUS,
|
|
105
|
-
response_chars=100_000,
|
|
106
|
-
vision_tokens_per_clip=250_000,
|
|
107
|
-
frames_per_clip=200,
|
|
108
|
-
vision_tokens_per_job=3_000_000,
|
|
109
|
-
vision_tokens_per_day=6_000_000,
|
|
110
|
-
wall_clock_seconds_per_call=300,
|
|
111
|
-
max_frame_dim_pixels=1280,
|
|
112
|
-
),
|
|
113
|
-
PRESET_UNLIMITED: Caps(
|
|
114
|
-
preset=PRESET_UNLIMITED,
|
|
115
|
-
response_chars=None,
|
|
116
|
-
vision_tokens_per_clip=None,
|
|
117
|
-
frames_per_clip=None,
|
|
118
|
-
vision_tokens_per_job=None,
|
|
119
|
-
vision_tokens_per_day=None,
|
|
120
|
-
wall_clock_seconds_per_call=None,
|
|
121
|
-
max_frame_dim_pixels=None,
|
|
122
|
-
),
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
VALID_PRESETS = frozenset(CAP_PRESETS)
|
|
126
|
-
|
|
127
|
-
_OVERRIDE_KEYS = frozenset({
|
|
128
|
-
"response_chars",
|
|
129
|
-
"vision_tokens_per_clip",
|
|
130
|
-
"frames_per_clip",
|
|
131
|
-
"vision_tokens_per_job",
|
|
132
|
-
"vision_tokens_per_day",
|
|
133
|
-
"wall_clock_seconds_per_call",
|
|
134
|
-
"max_frame_dim_pixels",
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
# ── Public API: preset resolution ────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def resolve_caps(
|
|
142
|
-
preset: Optional[str] = None,
|
|
143
|
-
overrides: Optional[Dict[str, Any]] = None,
|
|
144
|
-
) -> Caps:
|
|
145
|
-
"""Materialise the effective Caps for a given preset + optional overrides.
|
|
146
|
-
|
|
147
|
-
Unknown preset falls back to standard (logged as warning). Override values:
|
|
148
|
-
integers replace the preset value; `None` or "unlimited" lifts the cap on
|
|
149
|
-
that dimension; unknown keys are ignored (logged at debug).
|
|
150
|
-
"""
|
|
151
|
-
preset_key = (preset or DEFAULT_PRESET).strip().lower()
|
|
152
|
-
if preset_key not in VALID_PRESETS:
|
|
153
|
-
logger.warning("unknown preset '%s', falling back to %s", preset, DEFAULT_PRESET)
|
|
154
|
-
preset_key = DEFAULT_PRESET
|
|
155
|
-
base = CAP_PRESETS[preset_key]
|
|
156
|
-
|
|
157
|
-
if not overrides:
|
|
158
|
-
return base
|
|
159
|
-
|
|
160
|
-
patch: Dict[str, Any] = {}
|
|
161
|
-
for key, raw in overrides.items():
|
|
162
|
-
if key not in _OVERRIDE_KEYS:
|
|
163
|
-
logger.debug("ignoring unknown cap override: %s", key)
|
|
164
|
-
continue
|
|
165
|
-
if raw is None or (isinstance(raw, str) and raw.strip().lower() == "unlimited"):
|
|
166
|
-
patch[key] = None
|
|
167
|
-
continue
|
|
168
|
-
try:
|
|
169
|
-
patch[key] = int(raw)
|
|
170
|
-
except (TypeError, ValueError):
|
|
171
|
-
logger.debug("ignoring non-integer cap override %s=%r", key, raw)
|
|
172
|
-
continue
|
|
173
|
-
return replace(base, **patch) if patch else base
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def list_presets() -> Dict[str, Dict[str, Any]]:
|
|
177
|
-
"""Render every preset as a plain dict (for dashboard + MCP introspection)."""
|
|
178
|
-
return {key: caps.to_dict() for key, caps in CAP_PRESETS.items()}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
# ── Public API: budget enforcement ───────────────────────────────────────────
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
@dataclass(frozen=True)
|
|
185
|
-
class BudgetDecision:
|
|
186
|
-
allowed: bool
|
|
187
|
-
reason: Optional[str] # e.g. "over_clip_cap", "over_job_cap", "over_day_cap"
|
|
188
|
-
current_usage: Dict[str, int]
|
|
189
|
-
cap: Dict[str, Optional[int]]
|
|
190
|
-
headroom: Dict[str, Optional[int]] # remaining tokens per scope; None if uncapped
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def _day_bucket(when: Optional[float] = None) -> str:
|
|
194
|
-
t = when if when is not None else time.time()
|
|
195
|
-
return time.strftime("%Y-%m-%d", time.gmtime(t))
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def _iso(when: Optional[float] = None) -> str:
|
|
199
|
-
t = when if when is not None else time.time()
|
|
200
|
-
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(t))
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def _sum_usage(
|
|
204
|
-
conn,
|
|
205
|
-
*,
|
|
206
|
-
scope: str,
|
|
207
|
-
scope_key: Optional[str] = None,
|
|
208
|
-
day_bucket: Optional[str] = None,
|
|
209
|
-
) -> Dict[str, int]:
|
|
210
|
-
clauses: List[str] = ["scope = ?"]
|
|
211
|
-
args: List[Any] = [scope]
|
|
212
|
-
if scope_key is not None:
|
|
213
|
-
clauses.append("scope_key = ?")
|
|
214
|
-
args.append(scope_key)
|
|
215
|
-
if day_bucket is not None:
|
|
216
|
-
clauses.append("day_bucket = ?")
|
|
217
|
-
args.append(day_bucket)
|
|
218
|
-
where = " AND ".join(clauses)
|
|
219
|
-
row = conn.execute(
|
|
220
|
-
f"""
|
|
221
|
-
SELECT COALESCE(SUM(vision_tokens), 0) AS vision,
|
|
222
|
-
COALESCE(SUM(transcription_tokens), 0) AS transcription,
|
|
223
|
-
COALESCE(SUM(frames_uploaded), 0) AS frames,
|
|
224
|
-
COALESCE(SUM(wall_clock_ms), 0) AS wall_clock_ms
|
|
225
|
-
FROM analysis_token_usage
|
|
226
|
-
WHERE {where}
|
|
227
|
-
""",
|
|
228
|
-
args,
|
|
229
|
-
).fetchone()
|
|
230
|
-
return {
|
|
231
|
-
"vision_tokens": int(row["vision"] or 0),
|
|
232
|
-
"transcription_tokens": int(row["transcription"] or 0),
|
|
233
|
-
"frames_uploaded": int(row["frames"] or 0),
|
|
234
|
-
"wall_clock_ms": int(row["wall_clock_ms"] or 0),
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def check_budget(
|
|
239
|
-
*,
|
|
240
|
-
project_root: str,
|
|
241
|
-
caps: Caps,
|
|
242
|
-
estimated_vision_tokens: int = 0,
|
|
243
|
-
clip_id: Optional[str] = None,
|
|
244
|
-
job_id: Optional[str] = None,
|
|
245
|
-
) -> BudgetDecision:
|
|
246
|
-
"""Would adding `estimated_vision_tokens` exceed any active cap?
|
|
247
|
-
|
|
248
|
-
Checks the three cumulative budgets: per-clip, per-job, per-day. If any is
|
|
249
|
-
over (or would go over with this call), returns `allowed=False` with the
|
|
250
|
-
reason. Uncapped dimensions never block.
|
|
251
|
-
"""
|
|
252
|
-
conn = timeline_brain_db.connect(project_root)
|
|
253
|
-
day = _day_bucket()
|
|
254
|
-
|
|
255
|
-
clip_usage = (
|
|
256
|
-
_sum_usage(conn, scope=SCOPE_CLIP, scope_key=clip_id)
|
|
257
|
-
if clip_id else {"vision_tokens": 0, "transcription_tokens": 0, "frames_uploaded": 0, "wall_clock_ms": 0}
|
|
258
|
-
)
|
|
259
|
-
job_usage = (
|
|
260
|
-
_sum_usage(conn, scope=SCOPE_JOB, scope_key=job_id)
|
|
261
|
-
if job_id else {"vision_tokens": 0, "transcription_tokens": 0, "frames_uploaded": 0, "wall_clock_ms": 0}
|
|
262
|
-
)
|
|
263
|
-
day_usage = _sum_usage(conn, scope=SCOPE_DAY, day_bucket=day)
|
|
264
|
-
|
|
265
|
-
def _check(usage_value: int, cap_value: Optional[int]) -> Optional[int]:
|
|
266
|
-
if cap_value is None:
|
|
267
|
-
return None
|
|
268
|
-
return cap_value - usage_value
|
|
269
|
-
|
|
270
|
-
cap_map = {
|
|
271
|
-
"clip": caps.vision_tokens_per_clip,
|
|
272
|
-
"job": caps.vision_tokens_per_job,
|
|
273
|
-
"day": caps.vision_tokens_per_day,
|
|
274
|
-
}
|
|
275
|
-
headroom = {
|
|
276
|
-
"clip": _check(clip_usage["vision_tokens"], caps.vision_tokens_per_clip),
|
|
277
|
-
"job": _check(job_usage["vision_tokens"], caps.vision_tokens_per_job),
|
|
278
|
-
"day": _check(day_usage["vision_tokens"], caps.vision_tokens_per_day),
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
# Identify the most-binding constraint. Order: clip → job → day so the
|
|
282
|
-
# smallest scope reports first.
|
|
283
|
-
for scope_key in ("clip", "job", "day"):
|
|
284
|
-
remaining = headroom[scope_key]
|
|
285
|
-
if remaining is None:
|
|
286
|
-
continue
|
|
287
|
-
if remaining - estimated_vision_tokens < 0:
|
|
288
|
-
return BudgetDecision(
|
|
289
|
-
allowed=False,
|
|
290
|
-
reason=f"over_{scope_key}_cap",
|
|
291
|
-
current_usage={
|
|
292
|
-
"clip_vision_tokens": clip_usage["vision_tokens"],
|
|
293
|
-
"job_vision_tokens": job_usage["vision_tokens"],
|
|
294
|
-
"day_vision_tokens": day_usage["vision_tokens"],
|
|
295
|
-
},
|
|
296
|
-
cap=cap_map,
|
|
297
|
-
headroom=headroom,
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
return BudgetDecision(
|
|
301
|
-
allowed=True,
|
|
302
|
-
reason=None,
|
|
303
|
-
current_usage={
|
|
304
|
-
"clip_vision_tokens": clip_usage["vision_tokens"],
|
|
305
|
-
"job_vision_tokens": job_usage["vision_tokens"],
|
|
306
|
-
"day_vision_tokens": day_usage["vision_tokens"],
|
|
307
|
-
},
|
|
308
|
-
cap=cap_map,
|
|
309
|
-
headroom=headroom,
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def record_usage(
|
|
314
|
-
*,
|
|
315
|
-
project_root: str,
|
|
316
|
-
scope: str,
|
|
317
|
-
scope_key: Optional[str],
|
|
318
|
-
vision_tokens: int = 0,
|
|
319
|
-
transcription_tokens: int = 0,
|
|
320
|
-
frames_uploaded: int = 0,
|
|
321
|
-
wall_clock_ms: int = 0,
|
|
322
|
-
preset: Optional[str] = None,
|
|
323
|
-
) -> Dict[str, Any]:
|
|
324
|
-
"""Persist a token-usage row. Called after every vision/transcription call.
|
|
325
|
-
|
|
326
|
-
Insert under each applicable scope. A single call typically writes 2 rows:
|
|
327
|
-
one under `clip` (so per-clip caps are accurate) and one under `day` (so
|
|
328
|
-
cross-clip rollups are accurate). When a job_id is in play, also write
|
|
329
|
-
under `job`. Use separate calls for each scope; this function writes ONE
|
|
330
|
-
row per call.
|
|
331
|
-
"""
|
|
332
|
-
if scope not in (SCOPE_CLIP, SCOPE_JOB, SCOPE_DAY, SCOPE_SESSION):
|
|
333
|
-
raise ValueError(f"unknown scope: {scope}")
|
|
334
|
-
now = time.time()
|
|
335
|
-
with timeline_brain_db.transaction(project_root) as txn:
|
|
336
|
-
cursor = txn.execute(
|
|
337
|
-
"""
|
|
338
|
-
INSERT INTO analysis_token_usage(
|
|
339
|
-
scope, scope_key, vision_tokens, transcription_tokens,
|
|
340
|
-
frames_uploaded, wall_clock_ms, preset_at_call,
|
|
341
|
-
occurred_at, day_bucket
|
|
342
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
343
|
-
""",
|
|
344
|
-
(
|
|
345
|
-
scope, scope_key, int(vision_tokens), int(transcription_tokens),
|
|
346
|
-
int(frames_uploaded), int(wall_clock_ms), preset,
|
|
347
|
-
_iso(now), _day_bucket(now),
|
|
348
|
-
),
|
|
349
|
-
)
|
|
350
|
-
row_id = cursor.lastrowid
|
|
351
|
-
return {"success": True, "row_id": row_id}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
def record_usage_all_scopes(
|
|
355
|
-
*,
|
|
356
|
-
project_root: str,
|
|
357
|
-
clip_id: Optional[str] = None,
|
|
358
|
-
job_id: Optional[str] = None,
|
|
359
|
-
vision_tokens: int = 0,
|
|
360
|
-
transcription_tokens: int = 0,
|
|
361
|
-
frames_uploaded: int = 0,
|
|
362
|
-
wall_clock_ms: int = 0,
|
|
363
|
-
preset: Optional[str] = None,
|
|
364
|
-
) -> Dict[str, Any]:
|
|
365
|
-
"""Record the same usage under every applicable scope in one call.
|
|
366
|
-
|
|
367
|
-
Always writes a `day` row. Writes a `clip` row if clip_id is provided, a
|
|
368
|
-
`job` row if job_id is provided. Atomicity is per-row — partial failure on
|
|
369
|
-
one scope still records the others.
|
|
370
|
-
"""
|
|
371
|
-
rows: List[Dict[str, Any]] = []
|
|
372
|
-
rows.append(record_usage(
|
|
373
|
-
project_root=project_root, scope=SCOPE_DAY, scope_key=None,
|
|
374
|
-
vision_tokens=vision_tokens, transcription_tokens=transcription_tokens,
|
|
375
|
-
frames_uploaded=frames_uploaded, wall_clock_ms=wall_clock_ms, preset=preset,
|
|
376
|
-
))
|
|
377
|
-
if clip_id:
|
|
378
|
-
rows.append(record_usage(
|
|
379
|
-
project_root=project_root, scope=SCOPE_CLIP, scope_key=clip_id,
|
|
380
|
-
vision_tokens=vision_tokens, transcription_tokens=transcription_tokens,
|
|
381
|
-
frames_uploaded=frames_uploaded, wall_clock_ms=wall_clock_ms, preset=preset,
|
|
382
|
-
))
|
|
383
|
-
if job_id:
|
|
384
|
-
rows.append(record_usage(
|
|
385
|
-
project_root=project_root, scope=SCOPE_JOB, scope_key=job_id,
|
|
386
|
-
vision_tokens=vision_tokens, transcription_tokens=transcription_tokens,
|
|
387
|
-
frames_uploaded=frames_uploaded, wall_clock_ms=wall_clock_ms, preset=preset,
|
|
388
|
-
))
|
|
389
|
-
return {"success": True, "rows": rows}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
def get_current_usage(
|
|
393
|
-
*,
|
|
394
|
-
project_root: str,
|
|
395
|
-
scope: str = SCOPE_DAY,
|
|
396
|
-
scope_key: Optional[str] = None,
|
|
397
|
-
day_bucket: Optional[str] = None,
|
|
398
|
-
) -> Dict[str, int]:
|
|
399
|
-
"""Rollup query for the dashboard / get_usage MCP action."""
|
|
400
|
-
conn = timeline_brain_db.connect(project_root)
|
|
401
|
-
day = day_bucket if scope == SCOPE_DAY else None
|
|
402
|
-
return _sum_usage(conn, scope=scope, scope_key=scope_key, day_bucket=day)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
def log_caps_event(
|
|
406
|
-
*,
|
|
407
|
-
project_root: str,
|
|
408
|
-
event_type: str,
|
|
409
|
-
reason: Optional[str] = None,
|
|
410
|
-
preset: Optional[str] = None,
|
|
411
|
-
estimated_vision_tokens: int = 0,
|
|
412
|
-
current_usage: Optional[Dict[str, Any]] = None,
|
|
413
|
-
cap: Optional[Dict[str, Any]] = None,
|
|
414
|
-
headroom: Optional[Dict[str, Any]] = None,
|
|
415
|
-
clip_id: Optional[str] = None,
|
|
416
|
-
job_id: Optional[str] = None,
|
|
417
|
-
) -> Dict[str, Any]:
|
|
418
|
-
"""Persist a caps event row (refusal / timeout / etc) for dashboard visibility."""
|
|
419
|
-
import json as _json
|
|
420
|
-
now = time.time()
|
|
421
|
-
with timeline_brain_db.transaction(project_root) as txn:
|
|
422
|
-
cursor = txn.execute(
|
|
423
|
-
"""
|
|
424
|
-
INSERT INTO caps_events(
|
|
425
|
-
event_type, reason, preset, estimated_vision_tokens,
|
|
426
|
-
current_usage_json, cap_json, headroom_json,
|
|
427
|
-
clip_id, job_id, occurred_at, day_bucket
|
|
428
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
429
|
-
""",
|
|
430
|
-
(
|
|
431
|
-
event_type, reason, preset, int(estimated_vision_tokens),
|
|
432
|
-
_json.dumps(current_usage) if current_usage else None,
|
|
433
|
-
_json.dumps(cap) if cap else None,
|
|
434
|
-
_json.dumps(headroom) if headroom else None,
|
|
435
|
-
clip_id, job_id, _iso(now), _day_bucket(now),
|
|
436
|
-
),
|
|
437
|
-
)
|
|
438
|
-
row_id = cursor.lastrowid
|
|
439
|
-
return {"success": True, "row_id": row_id}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
def get_caps_events(
|
|
443
|
-
*,
|
|
444
|
-
project_root: str,
|
|
445
|
-
event_type: Optional[str] = None,
|
|
446
|
-
limit: int = 20,
|
|
447
|
-
) -> List[Dict[str, Any]]:
|
|
448
|
-
"""Most-recent-first caps events for the dashboard."""
|
|
449
|
-
import json as _json
|
|
450
|
-
conn = timeline_brain_db.connect(project_root)
|
|
451
|
-
if event_type:
|
|
452
|
-
rows = conn.execute(
|
|
453
|
-
"""
|
|
454
|
-
SELECT * FROM caps_events WHERE event_type = ?
|
|
455
|
-
ORDER BY id DESC LIMIT ?
|
|
456
|
-
""",
|
|
457
|
-
(event_type, int(limit)),
|
|
458
|
-
).fetchall()
|
|
459
|
-
else:
|
|
460
|
-
rows = conn.execute(
|
|
461
|
-
"SELECT * FROM caps_events ORDER BY id DESC LIMIT ?",
|
|
462
|
-
(int(limit),),
|
|
463
|
-
).fetchall()
|
|
464
|
-
out: List[Dict[str, Any]] = []
|
|
465
|
-
for r in rows:
|
|
466
|
-
d = dict(r)
|
|
467
|
-
for key in ("current_usage_json", "cap_json", "headroom_json"):
|
|
468
|
-
raw = d.pop(key)
|
|
469
|
-
d[key[:-5]] = _json.loads(raw) if raw else None
|
|
470
|
-
out.append(d)
|
|
471
|
-
return out
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
def get_usage_history(
|
|
475
|
-
*,
|
|
476
|
-
project_root: str,
|
|
477
|
-
days: int = 30,
|
|
478
|
-
) -> List[Dict[str, Any]]:
|
|
479
|
-
"""Per-day vision token totals over the past `days` days. Newest first."""
|
|
480
|
-
conn = timeline_brain_db.connect(project_root)
|
|
481
|
-
rows = conn.execute(
|
|
482
|
-
"""
|
|
483
|
-
SELECT day_bucket,
|
|
484
|
-
COALESCE(SUM(vision_tokens), 0) AS vision_tokens,
|
|
485
|
-
COALESCE(SUM(transcription_tokens), 0) AS transcription_tokens,
|
|
486
|
-
COALESCE(SUM(frames_uploaded), 0) AS frames_uploaded,
|
|
487
|
-
COALESCE(SUM(wall_clock_ms), 0) AS wall_clock_ms
|
|
488
|
-
FROM analysis_token_usage
|
|
489
|
-
WHERE scope = 'day'
|
|
490
|
-
GROUP BY day_bucket
|
|
491
|
-
ORDER BY day_bucket DESC
|
|
492
|
-
LIMIT ?
|
|
493
|
-
""",
|
|
494
|
-
(int(days),),
|
|
495
|
-
).fetchall()
|
|
496
|
-
return [dict(r) for r in rows]
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
def reset_day_usage(
|
|
500
|
-
*,
|
|
501
|
-
project_root: str,
|
|
502
|
-
day_bucket: Optional[str] = None,
|
|
503
|
-
) -> Dict[str, Any]:
|
|
504
|
-
"""Delete day-scope usage rows for the given day (default today). Returns count deleted."""
|
|
505
|
-
day = day_bucket or _day_bucket()
|
|
506
|
-
with timeline_brain_db.transaction(project_root) as txn:
|
|
507
|
-
cursor = txn.execute(
|
|
508
|
-
"DELETE FROM analysis_token_usage WHERE scope = 'day' AND day_bucket = ?",
|
|
509
|
-
(day,),
|
|
510
|
-
)
|
|
511
|
-
deleted = cursor.rowcount
|
|
512
|
-
return {"success": True, "day_bucket": day, "deleted": deleted}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
def get_usage_rollup(
|
|
516
|
-
*,
|
|
517
|
-
project_root: str,
|
|
518
|
-
caps: Caps,
|
|
519
|
-
clip_id: Optional[str] = None,
|
|
520
|
-
job_id: Optional[str] = None,
|
|
521
|
-
) -> Dict[str, Any]:
|
|
522
|
-
"""Combined snapshot of usage vs caps across all three cumulative scopes."""
|
|
523
|
-
day = _day_bucket()
|
|
524
|
-
clip_usage = get_current_usage(project_root=project_root, scope=SCOPE_CLIP, scope_key=clip_id) if clip_id else None
|
|
525
|
-
job_usage = get_current_usage(project_root=project_root, scope=SCOPE_JOB, scope_key=job_id) if job_id else None
|
|
526
|
-
day_usage = get_current_usage(project_root=project_root, scope=SCOPE_DAY, day_bucket=day)
|
|
527
|
-
|
|
528
|
-
def _percent(used: int, cap: Optional[int]) -> Optional[float]:
|
|
529
|
-
if cap is None or cap == 0:
|
|
530
|
-
return None
|
|
531
|
-
return round(min(100.0, 100.0 * used / cap), 1)
|
|
532
|
-
|
|
533
|
-
return {
|
|
534
|
-
"preset": caps.preset,
|
|
535
|
-
"caps": caps.to_dict(),
|
|
536
|
-
"usage": {
|
|
537
|
-
"clip": clip_usage,
|
|
538
|
-
"job": job_usage,
|
|
539
|
-
"day": day_usage,
|
|
540
|
-
},
|
|
541
|
-
"percent_consumed": {
|
|
542
|
-
"clip": _percent(clip_usage["vision_tokens"], caps.vision_tokens_per_clip) if clip_usage else None,
|
|
543
|
-
"job": _percent(job_usage["vision_tokens"], caps.vision_tokens_per_job) if job_usage else None,
|
|
544
|
-
"day": _percent(day_usage["vision_tokens"], caps.vision_tokens_per_day),
|
|
545
|
-
},
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
# ── Response payload trimming ────────────────────────────────────────────────
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
def trim_response_payload(payload: Any, max_chars: Optional[int]) -> Any:
|
|
553
|
-
"""Truncate a JSON-serialisable payload so its string repr fits under max_chars.
|
|
554
|
-
|
|
555
|
-
Strategy:
|
|
556
|
-
1. If under cap or uncapped → return as-is.
|
|
557
|
-
2. Recursively trim the largest list/string fields first (transcripts,
|
|
558
|
-
frame_paths, segments) until under cap.
|
|
559
|
-
3. Add a `_trimmed` marker dict so consumers can see what got dropped.
|
|
560
|
-
"""
|
|
561
|
-
if max_chars is None:
|
|
562
|
-
return payload
|
|
563
|
-
|
|
564
|
-
import json
|
|
565
|
-
raw = json.dumps(payload, default=str)
|
|
566
|
-
if len(raw) <= max_chars:
|
|
567
|
-
return payload
|
|
568
|
-
if not isinstance(payload, dict):
|
|
569
|
-
# Non-dict payloads: stringify + truncate.
|
|
570
|
-
return raw[: max_chars - 32] + "…[truncated]"
|
|
571
|
-
|
|
572
|
-
trimmed = dict(payload)
|
|
573
|
-
drops: List[str] = []
|
|
574
|
-
|
|
575
|
-
# 1) Drop very large list-valued keys first (in priority order).
|
|
576
|
-
LARGE_KEYS = ("transcript_segments", "frame_paths", "sampled_frames", "segments", "tokens")
|
|
577
|
-
for key in LARGE_KEYS:
|
|
578
|
-
if key in trimmed:
|
|
579
|
-
value = trimmed[key]
|
|
580
|
-
if isinstance(value, list) and len(value) > 4:
|
|
581
|
-
trimmed[key] = value[:4] + [{"_trimmed_count": len(value) - 4}]
|
|
582
|
-
drops.append(f"{key}[{len(value) - 4} omitted]")
|
|
583
|
-
elif isinstance(value, str) and len(value) > 1000:
|
|
584
|
-
trimmed[key] = value[:1000] + "…[trimmed]"
|
|
585
|
-
drops.append(f"{key}[len={len(value)}]")
|
|
586
|
-
raw = json.dumps(trimmed, default=str)
|
|
587
|
-
if len(raw) <= max_chars:
|
|
588
|
-
break
|
|
589
|
-
|
|
590
|
-
# 2) If still over, drop any long string values left.
|
|
591
|
-
if len(raw) > max_chars:
|
|
592
|
-
for key, value in list(trimmed.items()):
|
|
593
|
-
if isinstance(value, str) and len(value) > 500:
|
|
594
|
-
trimmed[key] = value[:500] + "…[trimmed]"
|
|
595
|
-
drops.append(f"{key}[len={len(value)}]")
|
|
596
|
-
raw = json.dumps(trimmed, default=str)
|
|
597
|
-
if len(raw) <= max_chars:
|
|
598
|
-
break
|
|
599
|
-
|
|
600
|
-
# 3) Last resort: stringify and truncate.
|
|
601
|
-
if len(raw) > max_chars:
|
|
602
|
-
return raw[: max_chars - 64] + "…[response cap hit; raw truncated]"
|
|
603
|
-
|
|
604
|
-
if drops:
|
|
605
|
-
trimmed["_trimmed"] = {
|
|
606
|
-
"reason": "response_chars cap",
|
|
607
|
-
"max_chars": max_chars,
|
|
608
|
-
"fields": drops,
|
|
609
|
-
}
|
|
610
|
-
return trimmed
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
# ── Wall-clock timeout helper ────────────────────────────────────────────────
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
class WallClockTimeout(Exception):
|
|
617
|
-
"""Raised when a wrapped call exceeds caps.wall_clock_seconds_per_call."""
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
def run_with_timeout(fn, timeout_seconds: Optional[float], *args, **kwargs):
|
|
621
|
-
"""Run `fn` with a wall-clock timeout. Uses a thread + Event for portability.
|
|
622
|
-
|
|
623
|
-
Returns whatever `fn` returns. Raises WallClockTimeout if elapsed > cap.
|
|
624
|
-
Pass `timeout_seconds=None` (or 0) to disable. Accepts fractional seconds
|
|
625
|
-
(handy in tests). The thread keeps running after timeout (we can't kill
|
|
626
|
-
threads in Python); callers should treat the timed-out call as zombie and
|
|
627
|
-
not wait for cleanup.
|
|
628
|
-
"""
|
|
629
|
-
if timeout_seconds is None or timeout_seconds <= 0:
|
|
630
|
-
return fn(*args, **kwargs)
|
|
631
|
-
import threading
|
|
632
|
-
result: Dict[str, Any] = {}
|
|
633
|
-
err: Dict[str, BaseException] = {}
|
|
634
|
-
|
|
635
|
-
def _target():
|
|
636
|
-
try:
|
|
637
|
-
result["value"] = fn(*args, **kwargs)
|
|
638
|
-
except BaseException as exc: # noqa: BLE001
|
|
639
|
-
err["exc"] = exc
|
|
640
|
-
|
|
641
|
-
thread = threading.Thread(target=_target, daemon=True)
|
|
642
|
-
thread.start()
|
|
643
|
-
thread.join(timeout=timeout_seconds)
|
|
644
|
-
if thread.is_alive():
|
|
645
|
-
raise WallClockTimeout(f"call exceeded {timeout_seconds}s wall-clock cap")
|
|
646
|
-
if "exc" in err:
|
|
647
|
-
raise err["exc"]
|
|
648
|
-
return result.get("value")
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
# ── Frame downscale helper ──────────────────────────────────────────────────
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
def downscale_frame_if_needed(image_path: str, max_dim: Optional[int]) -> str:
|
|
655
|
-
"""Downscale `image_path` in place if either dimension > max_dim.
|
|
656
|
-
|
|
657
|
-
Uses PIL if available; silently no-ops if Pillow isn't installed (the
|
|
658
|
-
analysis pipeline degrades to original-resolution uploads). Returns the
|
|
659
|
-
path (may be the same path if not modified).
|
|
660
|
-
"""
|
|
661
|
-
if max_dim is None or max_dim <= 0:
|
|
662
|
-
return image_path
|
|
663
|
-
try:
|
|
664
|
-
from PIL import Image as PILImage
|
|
665
|
-
except ImportError:
|
|
666
|
-
logger.debug("Pillow not installed — frame downscale skipped")
|
|
667
|
-
return image_path
|
|
668
|
-
try:
|
|
669
|
-
with PILImage.open(image_path) as img:
|
|
670
|
-
w, h = img.size
|
|
671
|
-
if w <= max_dim and h <= max_dim:
|
|
672
|
-
return image_path
|
|
673
|
-
img.thumbnail((max_dim, max_dim), PILImage.LANCZOS)
|
|
674
|
-
img.save(image_path)
|
|
675
|
-
logger.debug("downscaled %s from %dx%d to fit %dpx", image_path, w, h, max_dim)
|
|
676
|
-
except Exception as exc:
|
|
677
|
-
logger.debug("frame downscale failed for %s: %s", image_path, exc)
|
|
678
|
-
return image_path
|