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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. package/.agents/skills/firecrawl/SKILL.md +149 -0
  2. package/.agents/skills/firecrawl/rules/install.md +82 -0
  3. package/.agents/skills/firecrawl/rules/security.md +26 -0
  4. package/.agents/skills/firecrawl-agent/SKILL.md +58 -0
  5. package/.agents/skills/firecrawl-build/SKILL.md +39 -0
  6. package/.agents/skills/firecrawl-build-interact/SKILL.md +68 -0
  7. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +103 -0
  8. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +39 -0
  9. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +20 -0
  10. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +17 -0
  11. package/.agents/skills/firecrawl-build-scrape/SKILL.md +69 -0
  12. package/.agents/skills/firecrawl-build-search/SKILL.md +69 -0
  13. package/.agents/skills/firecrawl-crawl/SKILL.md +59 -0
  14. package/.agents/skills/firecrawl-download/SKILL.md +70 -0
  15. package/.agents/skills/firecrawl-interact/SKILL.md +84 -0
  16. package/.agents/skills/firecrawl-map/SKILL.md +51 -0
  17. package/.agents/skills/firecrawl-scrape/SKILL.md +69 -0
  18. package/.agents/skills/firecrawl-search/SKILL.md +60 -0
  19. package/.claude/hooks/memb-inject.mjs +6 -0
  20. package/CLAUDE.md +573 -0
  21. package/README.de.md +0 -8
  22. package/README.md +0 -8
  23. package/README.pt.md +0 -8
  24. package/assets/brand/ao-ant-mark.svg +91 -0
  25. package/assets/brand/bdb-core-mark.svg +34 -0
  26. package/assets/brand/heimdall-guardian-mark.svg +16 -0
  27. package/assets/brand/memb-crystal-mark.svg +18 -0
  28. package/bin/aos-config.mjs +89 -0
  29. package/bin/aos-dashboard.mjs +608 -0
  30. package/bin/aos-uninstall.mjs +227 -0
  31. package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
  32. package/docs/skills_table.md +0 -3
  33. package/installer.js +428 -237
  34. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
  35. package/mcps/after-effects-mcp/build/index.js +840 -0
  36. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
  37. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
  38. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
  39. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
  40. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
  41. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
  42. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
  43. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
  44. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
  45. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
  46. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
  47. package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
  48. package/mcps/computer-use-mcp/dist/client.js +136 -0
  49. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
  50. package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
  51. package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
  52. package/mcps/computer-use-mcp/dist/native.js +50 -0
  53. package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
  54. package/mcps/computer-use-mcp/dist/server.js +342 -0
  55. package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
  56. package/mcps/computer-use-mcp/dist/session.js +2372 -0
  57. package/package.json +13 -4
  58. package/skills/global_config/aos-project-init/SKILL.md +97 -130
  59. package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
  60. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
  61. package/skills/global_config/aos-setup/SKILL.md +110 -11
  62. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +48 -4
  63. package/skills/global_config/ask-tim/SKILL.md +2 -7
  64. package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
  65. package/bin/setup-saas.mjs +0 -792
  66. package/mcps/blender-mcp-server/.dockerignore +0 -19
  67. package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
  68. package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
  69. package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
  70. package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
  71. package/mcps/blender-mcp-server/Dockerfile +0 -19
  72. package/mcps/blender-mcp-server/LICENSE +0 -21
  73. package/mcps/blender-mcp-server/README.md +0 -409
  74. package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
  75. package/mcps/blender-mcp-server/addon/models.py +0 -223
  76. package/mcps/blender-mcp-server/pyproject.toml +0 -120
  77. package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
  78. package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
  79. package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
  80. package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
  81. package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
  82. package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
  83. package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
  84. package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
  85. package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
  86. package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
  87. package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
  88. package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
  89. package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
  90. package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
  91. package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
  92. package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
  93. package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
  94. package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
  95. package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
  96. package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
  97. package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
  98. package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
  99. package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
  100. package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
  101. package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
  102. package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
  103. package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
  104. package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
  105. package/mcps/davinci-mcp-professional/.bandit +0 -35
  106. package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
  107. package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
  108. package/mcps/davinci-mcp-professional/.editorconfig +0 -27
  109. package/mcps/davinci-mcp-professional/.gitattributes +0 -207
  110. package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
  111. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  112. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  113. package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
  114. package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
  115. package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
  116. package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
  117. package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
  118. package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
  119. package/mcps/davinci-mcp-professional/BUGS.md +0 -55
  120. package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
  121. package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
  122. package/mcps/davinci-mcp-professional/COPYING.md +0 -228
  123. package/mcps/davinci-mcp-professional/Doxyfile +0 -379
  124. package/mcps/davinci-mcp-professional/README.md +0 -143
  125. package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
  126. package/mcps/davinci-mcp-professional/USING.md +0 -301
  127. package/mcps/davinci-mcp-professional/VERSION.md +0 -13
  128. package/mcps/davinci-mcp-professional/build.py +0 -169
  129. package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
  130. package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
  131. package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
  132. package/mcps/davinci-mcp-professional/main.py +0 -40
  133. package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
  134. package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
  135. package/mcps/davinci-mcp-professional/security_audit.py +0 -379
  136. package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
  137. package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
  138. package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
  139. package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
  140. package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
  141. package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
  142. package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
  143. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
  144. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
  145. package/mcps/davinci-mcp-professional/test.py +0 -186
  146. package/mcps/davinci-mcp-professional/uv.lock +0 -2073
  147. package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
  148. package/mcps/davinci-resolve-mcp/.clinerules +0 -17
  149. package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
  150. package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
  151. package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
  152. package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
  153. package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
  154. package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
  155. package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
  156. package/mcps/davinci-resolve-mcp/LICENSE +0 -21
  157. package/mcps/davinci-resolve-mcp/README.md +0 -173
  158. package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
  159. package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
  160. package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
  161. package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
  162. package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
  163. package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
  164. package/mcps/davinci-resolve-mcp/install.py +0 -1687
  165. package/mcps/davinci-resolve-mcp/package.json +0 -52
  166. package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
  167. package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
  168. package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
  169. package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
  170. package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
  171. package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
  172. package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
  173. package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
  174. package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
  175. package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
  176. package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
  177. package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
  178. package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
  179. package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
  180. package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
  181. package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
  182. package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
  183. package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
  184. package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
  185. package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
  186. package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
  187. package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
  188. package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
  189. package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
  190. package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
  191. package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
  192. package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
  193. package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
  194. package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
  195. package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
  196. package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
  197. package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
  198. package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
  199. package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
  200. package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
  201. package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
  202. package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
  203. package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
  204. package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
  205. package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
  206. package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
  207. package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
  208. package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
  209. package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
  210. package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
  211. package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
  212. package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
  213. package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
  214. package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
  215. package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
  216. package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
  217. package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
  218. package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
  219. package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
  220. package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
  221. package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
  222. package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
  223. package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
  224. package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
  225. package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
  226. package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
  227. package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
  228. package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
  229. package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
  230. package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
  231. package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
  232. package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
  233. package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
  234. package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
  235. package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
  236. package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
  237. package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
  238. package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
  239. package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
  240. package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
  241. package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
  242. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
  243. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
  244. package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
  245. package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
  246. package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
  247. package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
  248. package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
  249. package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
  250. package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
  251. package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
  252. package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
  253. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
  254. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
  255. package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
  256. package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
  257. package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
  258. package/mcps/vectorworks-mcp/.dockerignore +0 -8
  259. package/mcps/vectorworks-mcp/Dockerfile +0 -28
  260. package/mcps/vectorworks-mcp/README.md +0 -156
  261. package/mcps/vectorworks-mcp/app/__init__.py +0 -2
  262. package/mcps/vectorworks-mcp/app/api.py +0 -52
  263. package/mcps/vectorworks-mcp/app/chunking.py +0 -56
  264. package/mcps/vectorworks-mcp/app/config.py +0 -29
  265. package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
  266. package/mcps/vectorworks-mcp/app/indexer.py +0 -106
  267. package/mcps/vectorworks-mcp/app/main.py +0 -33
  268. package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
  269. package/mcps/vectorworks-mcp/app/search.py +0 -54
  270. package/mcps/vectorworks-mcp/app/utils.py +0 -14
  271. package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
  272. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
  273. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
  274. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
  275. package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
  276. package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
  277. package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
  278. package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
  279. package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
  280. package/mcps/vectorworks-mcp/design.md +0 -57
  281. package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
  282. package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
  283. package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
  284. package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
  285. package/mcps/vectorworks-mcp/requirements.txt +0 -10
  286. package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
  287. package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
  288. package/mcps/vectorworks-mcp/templates/index.html +0 -40
  289. package/scripts/ecosystem-health-audit.js +0 -257
  290. package/skills/bdb-dev-os-skill/SKILL.md +0 -49
  291. package/skills/bdb-dev-os-skill/package.json +0 -11
  292. package/skills/bdbsaastraining/SKILL.md +0 -316
  293. package/skills/bdbsaastraining/references/exam-pool.md +0 -313
  294. package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
  295. package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
  296. package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
  297. package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
  298. package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
  299. package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
  300. package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
  301. package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
  302. package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
  303. package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
  304. package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
  305. package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
  306. package/skills/global_config/bdbsaashost/SKILL.md +0 -125
@@ -1,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