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

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