utim-cli 2.5.4__tar.gz → 2.6.0__tar.gz

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 (231) hide show
  1. {utim_cli-2.5.4 → utim_cli-2.6.0}/CHANGELOG.md +27 -0
  2. {utim_cli-2.5.4/utim_cli.egg-info → utim_cli-2.6.0}/PKG-INFO +1 -1
  3. {utim_cli-2.5.4 → utim_cli-2.6.0}/pyproject.toml +1 -1
  4. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim/__init__.py +1 -1
  5. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/__init__.py +1 -1
  6. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/_version.py +1 -1
  7. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/agent.py +40 -32
  8. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/ask_helper.py +9 -8
  9. utim_cli-2.6.0/utim_cli/background_models.py +187 -0
  10. utim_cli-2.6.0/utim_cli/behavior_telemetry.py +207 -0
  11. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/brain.py +11 -11
  12. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/client_utils.py +306 -9
  13. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/config.py +229 -0
  14. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/constants.py +2 -12
  15. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/context_pruner.py +597 -84
  16. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/crawler/markdown_distiller.py +72 -2
  17. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/mcp_client.py +50 -8
  18. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/orchestrator.py +154 -2
  19. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/orchestrator_llm_transport.py +591 -162
  20. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/orchestrator_tool_exec.py +348 -35
  21. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/orchestrator_turn_history.py +117 -0
  22. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/reflection.py +24 -16
  23. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/sdk/__init__.py +1 -1
  24. utim_cli-2.6.0/utim_cli/sdk/config.py +95 -0
  25. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/sdk/sidecar.py +171 -163
  26. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/api_models_db.py +190 -7
  27. utim_cli-2.6.0/utim_cli/server/api_request_logs.py +109 -0
  28. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/auth.py +69 -185
  29. utim_cli-2.6.0/utim_cli/server/bot_detection.py +1032 -0
  30. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/captcha.py +11 -5
  31. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/db.py +186 -4
  32. utim_cli-2.6.0/utim_cli/server/disposable_email_guard.py +329 -0
  33. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/email_utils.py +1 -1
  34. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/firewall.py +12 -38
  35. utim_cli-2.6.0/utim_cli/server/hobby_throttler.py +250 -0
  36. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/key_rotator.py +168 -1
  37. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/models.py +146 -45
  38. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/plan_integrity.py +14 -7
  39. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/platform_billing.py +30 -15
  40. utim_cli-2.6.0/utim_cli/server/provider_batcher.py +470 -0
  41. utim_cli-2.6.0/utim_cli/server/purge_disposable_users.py +174 -0
  42. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/rate_limit.py +11 -0
  43. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/request_queue.py +545 -574
  44. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/router.py +246 -40
  45. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/__init__.py +4 -2
  46. utim_cli-2.6.0/utim_cli/server/routes/activity_routes.py +192 -0
  47. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/admin_bonus_routes.py +70 -0
  48. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/admin_providers_routes.py +695 -640
  49. utim_cli-2.6.0/utim_cli/server/routes/admin_separated_models_routes.py +1551 -0
  50. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/api_gateway_extensions_routes.py +7 -9
  51. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/api_models_admin_routes.py +359 -40
  52. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/api_models_public_routes.py +943 -269
  53. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/auth_routes.py +79 -5
  54. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/billing_routes.py +78 -3
  55. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/completion_routes.py +646 -500
  56. utim_cli-2.6.0/utim_cli/server/routes/compression_routes.py +403 -0
  57. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/credit_routes.py +77 -60
  58. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/discord_routes.py +204 -5
  59. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/marketplace_routes.py +18 -3
  60. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/provider_compat_routes.py +12 -8
  61. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/quota_routes.py +36 -144
  62. utim_cli-2.6.0/utim_cli/server/routes/referral_routes.py +554 -0
  63. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/share_routes.py +7 -1
  64. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/wallet_routes.py +329 -193
  65. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/signup_guard.py +30 -4
  66. utim_cli-2.6.0/utim_cli/server/tier_tables.py +487 -0
  67. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/uptime_monitor.py +21 -1
  68. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/utils.py +186 -0
  69. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/share_tui.py +4 -3
  70. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tools.py +6253 -8710
  71. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/marketplace_dialog.py +5 -13
  72. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/model_dialog.py +1284 -739
  73. utim_cli-2.6.0/utim_cli/tui/personality_dialog.py +642 -0
  74. utim_cli-2.6.0/utim_cli/tui/spinner.py +379 -0
  75. utim_cli-2.6.0/utim_cli/tui/stream_diagnostics.py +243 -0
  76. utim_cli-2.6.0/utim_cli/tui/thinking_phrases.py +375 -0
  77. utim_cli-2.6.0/utim_cli/tui/token_meter.py +612 -0
  78. utim_cli-2.6.0/utim_cli/tui/tool_theme_preview.py +237 -0
  79. utim_cli-2.6.0/utim_cli/tui/tool_themes.py +1182 -0
  80. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/utim.py +940 -88
  81. {utim_cli-2.5.4 → utim_cli-2.6.0/utim_cli.egg-info}/PKG-INFO +1 -1
  82. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli.egg-info/SOURCES.txt +15 -2
  83. utim_cli-2.5.4/utim_cli/sdk/config.py +0 -41
  84. utim_cli-2.5.4/utim_cli/server/api_batcher.py +0 -442
  85. utim_cli-2.5.4/utim_cli/server/hobby_throttler.py +0 -495
  86. utim_cli-2.5.4/utim_cli/server/micro_batcher.py +0 -343
  87. utim_cli-2.5.4/utim_cli/server/routes/activity_routes.py +0 -150
  88. utim_cli-2.5.4/utim_cli/server/routes/admin_separated_models_routes.py +0 -989
  89. utim_cli-2.5.4/utim_cli/server/routes/referral_routes.py +0 -308
  90. utim_cli-2.5.4/utim_cli/tui/personality_dialog.py +0 -192
  91. {utim_cli-2.5.4 → utim_cli-2.6.0}/LICENSE +0 -0
  92. {utim_cli-2.5.4 → utim_cli-2.6.0}/MANIFEST.in +0 -0
  93. {utim_cli-2.5.4 → utim_cli-2.6.0}/README.md +0 -0
  94. {utim_cli-2.5.4 → utim_cli-2.6.0}/setup.cfg +0 -0
  95. {utim_cli-2.5.4 → utim_cli-2.6.0}/setup.py +0 -0
  96. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/_chip_test.py +0 -0
  97. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/_verify_repro.py +0 -0
  98. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/_verify_syntax.py +0 -0
  99. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/_wizard_smoke_test.py +0 -0
  100. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/adaptation_engine.py +0 -0
  101. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/architecture_indexer.py +0 -0
  102. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/auth.py +0 -0
  103. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/backup.py +0 -0
  104. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/billing.py +0 -0
  105. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/blender_agent.py +0 -0
  106. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/bootstrap.py +0 -0
  107. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
  108. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/context_inspector.py +0 -0
  109. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/crawler/__init__.py +0 -0
  110. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/crawler/engine.py +0 -0
  111. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/dependency_manager.py +0 -0
  112. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/doctor.py +0 -0
  113. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/evidence_registry.py +0 -0
  114. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/harbor.py +0 -0
  115. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/knowledge_graph.py +0 -0
  116. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/local_db.py +0 -0
  117. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/logger.py +0 -0
  118. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/mcp_clean_wrapper.py +0 -0
  119. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/mcp_registry.json +0 -0
  120. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/models.txt +0 -0
  121. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/orchestrator_helpers.py +0 -0
  122. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/personalities.py +0 -0
  123. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/platform_installer.py +0 -0
  124. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/plugins/__init__.py +0 -0
  125. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/plugins/hooks.py +0 -0
  126. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/plugins/loader.py +0 -0
  127. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/plugins/manager.py +0 -0
  128. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/plugins/registry.py +0 -0
  129. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/plugins/runtime.py +0 -0
  130. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/plugins/schema.py +0 -0
  131. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/plugins/tool_loader.py +0 -0
  132. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/report.py +0 -0
  133. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/scrapy_search.py +0 -0
  134. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/sdk/context.py +0 -0
  135. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/sdk/events.py +0 -0
  136. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/sdk/handlers.py +0 -0
  137. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/sdk/session.py +0 -0
  138. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/__init__.py +0 -0
  139. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/admin_auth.py +0 -0
  140. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/attribution.py +0 -0
  141. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/audit_log.py +0 -0
  142. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/batch_processor.py +0 -0
  143. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/cli_auth.py +0 -0
  144. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/concurrency.py +0 -0
  145. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/crypto_utils.py +0 -0
  146. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  147. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/docs_md/about.md +0 -0
  148. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/docs_md/changelog.md +0 -0
  149. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/docs_md/docs.md +0 -0
  150. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/docs_md/features.md +0 -0
  151. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/docs_md/license.md +0 -0
  152. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/docs_md/pricing.md +0 -0
  153. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/docs_md/privacy.md +0 -0
  154. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/docs_md/refund.md +0 -0
  155. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/docs_md/support.md +0 -0
  156. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/docs_md/terms.md +0 -0
  157. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/exchange_rate.py +0 -0
  158. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/firebase.py +0 -0
  159. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/fix_duplicate_users.py +0 -0
  160. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/history.py +0 -0
  161. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/local_llm.py +0 -0
  162. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/logging_config.py +0 -0
  163. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/maintenance.py +0 -0
  164. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/model_agent.py +0 -0
  165. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/pricing_updater.py +0 -0
  166. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/provider_balance.py +0 -0
  167. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/provider_discovery.py +0 -0
  168. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/provider_router.py +0 -0
  169. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/provision_build.py +0 -0
  170. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/request_batcher.py +0 -0
  171. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/rewards_engine.py +0 -0
  172. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/admin_db_routes.py +0 -0
  173. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/api_key_routes.py +0 -0
  174. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/feedback_routes.py +0 -0
  175. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/quota_share_routes.py +0 -0
  176. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/rewards_routes.py +0 -0
  177. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/security_routes.py +0 -0
  178. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/session_routes.py +0 -0
  179. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/routes/webhook_routes.py +0 -0
  180. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/security_alerts.py +0 -0
  181. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/server.py +0 -0
  182. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/server/storage_nodes.py +0 -0
  183. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/share.py +0 -0
  184. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/situational_scoring.py +0 -0
  185. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/state.py +0 -0
  186. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/subagent_manager.py +0 -0
  187. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/syntax_fix.py +0 -0
  188. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/task_dispatcher.py +0 -0
  189. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/task_logger.py +0 -0
  190. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tools_grep.py +0 -0
  191. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/__init__.py +0 -0
  192. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/compression_dialog.py +0 -0
  193. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/feedback_dialog.py +0 -0
  194. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/history_dialog.py +0 -0
  195. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/marketplace_app_state.py +0 -0
  196. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  197. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/mcp_dialog.py +0 -0
  198. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/miniagents_dialog.py +0 -0
  199. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/plugins_dialog.py +0 -0
  200. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/publish_dialog.py +0 -0
  201. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/quota_dialog.py +0 -0
  202. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  203. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/quota_share_dialog.py +0 -0
  204. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/resume_dialog.py +0 -0
  205. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/rewards_tui.py +0 -0
  206. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/skills_dialog.py +0 -0
  207. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/subagents_dialog.py +0 -0
  208. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/tasks_dialog.py +0 -0
  209. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/thinking_display.py +0 -0
  210. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/tools_dialog.py +0 -0
  211. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/update_dialog.py +0 -0
  212. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/tui/verbosity_dialog.py +0 -0
  213. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/update_checker.py +0 -0
  214. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/utilities.py +0 -0
  215. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/utimmodel.txt +0 -0
  216. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/vector_memory.py +0 -0
  217. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/voice.py +0 -0
  218. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/voice_coding.py +0 -0
  219. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/voice_events.py +0 -0
  220. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/voice_handler.py +0 -0
  221. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/voice_instant.py +0 -0
  222. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/voice_orchestrator.py +0 -0
  223. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/voice_session.py +0 -0
  224. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/voice_stt.py +0 -0
  225. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/voice_tts.py +0 -0
  226. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/wheel.py +0 -0
  227. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli/workspace.py +0 -0
  228. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli.egg-info/dependency_links.txt +0 -0
  229. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli.egg-info/entry_points.txt +0 -0
  230. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli.egg-info/requires.txt +0 -0
  231. {utim_cli-2.5.4 → utim_cli-2.6.0}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.0] - 2026-11-03
4
+
5
+ ### ✨ New Theme
6
+ - **Fresh UI Theme**: A brand-new visual theme for the CLI and website, with refreshed colours, cleaner spacing, and a sharper overall look.
7
+ - **UI Enhancements**: Polish across menus, panels, and prompts for a more consistent and modern feel.
8
+
9
+ ### 🐛 Fixes
10
+ - **Bug Fixes**: A round of stability and reliability fixes across the app.
11
+
12
+ ## [2.5.5] - 2026-10-29
13
+
14
+ ### 🐛 Fixes
15
+ - **Free Models No Longer Gated On The Free Plan**: Added `is_free_zero_cost_model()` as the single source of truth for $0.00 pricing. Any model priced 0.00 is now always usable on the Free plan, regardless of whether a priced copy of the same slug lives in `hobby_models` / `pro_models` or whether the static registry has an entry for it. Fixes `nvidia/nemotron-3.5-lightning:free` being reported as unavailable on the Free plan.
16
+ - **Registry Gate Bypass For Zero-Price Models**: The non-paid-plan registry check no longer rejects unregistered or `dynamic`-tagged models when their real price is 0.00.
17
+ - **Picker And Endpoint Now Agree**: The public catalog's free classifier resolves explicit free slugs (`:free`, `/free`, `-free`, `_free`, `free/` prefix) *before* consulting the separated tier-table override, so a priced duplicate can no longer hide a free model from the website or the `/model` picker.
18
+ - **Compression Pipeline Routed Through The Server**: New `POST /completions/compression` endpoint resolves the model the user is actually running plus the provider that actually serves it, eliminating the client-side `openrouter/free` fallback that kept returning empty content.
19
+
20
+ ## [2.5.4] - 2026-10-24
21
+
22
+ ### 🐛 Fixes
23
+ - **Streamed Output Now Always Displays**: Fixed responses being received by the server but never printed — pending code-block buffers are now force-flushed at end-of-stream on all streaming paths, so content ending inside an unclosed code fence is no longer silently dropped.
24
+ - **Adaptation Engine Over-Firing**: The truncation detector no longer re-fires continuation nudges on already-complete responses. False-positive heuristics (unclosed fence, mid-sentence) now fire at most once per task, while genuine token-limit continuations keep their full budget.
25
+ - **Fence Detection Hardening**: Unclosed code-block detection now only counts real markdown fences (line-start markers), not inline backtick references in prose.
26
+
27
+ ### ✨ Improvements
28
+ - **Dynamic Provider Adaptation Engine**: Enhanced reliability of provider health tracking, stitching, and continuation logic.
29
+
3
30
  ## [2.5.3] - 2026-10-20
4
31
 
5
32
  ### ✨ Improvements & Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.5.4
3
+ Version: 2.6.0
4
4
  Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
5
5
  License: Emend AI Proprietary EULA
6
6
  Project-URL: Homepage, https://utim.dev
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "2.5.4"
7
+ version = "2.6.0"
8
8
 
9
9
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
10
10
  readme = "README.md"
@@ -14,7 +14,7 @@ Quickstart:
14
14
  try:
15
15
  from utim_cli._version import VERSION as __version__
16
16
  except ImportError:
17
- __version__ = "2.5.4"
17
+ __version__ = "2.6.0"
18
18
 
19
19
  # Developer-friendly core classes (similar to `import genai` / `import openai`)
20
20
  from utim_cli.sdk.session import UtimSession as Session
@@ -5,7 +5,7 @@ try:
5
5
  from utim_cli._version import VERSION as __version__
6
6
  except ImportError:
7
7
  # Fallback during first-install / editable-install edge cases
8
- __version__ = "2.5.4"
8
+ __version__ = "2.6.0"
9
9
 
10
10
 
11
11
  import builtins, os, pathlib, urllib.parse, re
@@ -11,6 +11,6 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "2.5.4"
14
+ VERSION = "2.6.0"
15
15
 
16
16
 
@@ -228,6 +228,42 @@ class ReActAgent:
228
228
  self.console.print(_pre)
229
229
  pre_plain = f"{meta['verb']}({primary_arg}) (ctrl+o to expand)" if primary_arg else f"{meta['verb']} (ctrl+o to expand)"
230
230
  self._last_running_indicator_len = len(pre_plain)
231
+ # Remember that a provisional line is on screen, and the paint
232
+ # generation at the moment we wrote it. _erase_running_line() uses
233
+ # both: a blind CSI-1F erase is only safe while nothing has painted
234
+ # since, otherwise it corrupts the screen or leaves a duplicate header
235
+ # (the "Verb(x) · running" + "Verb(x)" double print).
236
+ self._running_line_live = True
237
+ try:
238
+ from utim_cli.state import STATE as _STG
239
+ self._running_line_paint_gen = _STG.get("_paint_gen", 0)
240
+ except Exception:
241
+ self._running_line_paint_gen = None
242
+
243
+ def _erase_running_line() -> None:
244
+ """Guarded replacement for the blind CSI-1F erase loops below."""
245
+ if not getattr(self, "_running_line_live", False):
246
+ return
247
+ self._running_line_live = False
248
+ try:
249
+ from utim_cli.state import STATE as _ST
250
+ if _ST.get("_paint_gen", 0) != getattr(self, "_running_line_paint_gen", None):
251
+ # Something repainted while the tool ran; the cursor is no
252
+ # longer under our header. Do not blind-erase.
253
+ return
254
+ except Exception:
255
+ pass
256
+ try:
257
+ width = self.console.size.width
258
+ except Exception:
259
+ width = 80
260
+ import math
261
+ num_lines = math.ceil(
262
+ getattr(self, "_last_running_indicator_len", 0) / max(1, width)
263
+ )
264
+ num_lines = max(0, min(num_lines, 5)) # never wipe more than 5 rows
265
+ for _ in range(num_lines):
266
+ self.console.print("\033[F\033[K", end="")
231
267
 
232
268
  if "__" in tool_name:
233
269
  server_name, actual_tool_name = tool_name.split("__", 1)
@@ -240,25 +276,11 @@ class ReActAgent:
240
276
  # Temporarily register metadata for render
241
277
  TOOL_META[tool_name] = {"icon": "", "verb": f"Calling {server_name} ➔ {actual_tool_name}", "color": "#cba6f7"}
242
278
  # Erase the running line first
243
- try:
244
- width = self.console.size.width
245
- except Exception:
246
- width = 80
247
- import math
248
- num_lines = math.ceil(getattr(self, "_last_running_indicator_len", 40) / max(1, width))
249
- for _ in range(num_lines):
250
- self.console.print("\033[F\033[K", end="")
279
+ _erase_running_line()
251
280
  self._render_tool_panel(tool_name, arguments, result)
252
281
  return str(result)
253
282
  except Exception as e:
254
- try:
255
- width = self.console.size.width
256
- except Exception:
257
- width = 80
258
- import math
259
- num_lines = math.ceil(getattr(self, "_last_running_indicator_len", 40) / max(1, width))
260
- for _ in range(num_lines):
261
- self.console.print("\033[F\033[K", end="")
283
+ _erase_running_line()
262
284
  return f"Error executing MCP tool {tool_name}: {str(e)}"
263
285
 
264
286
  utim_tools, tool_functions = get_tools()
@@ -275,25 +297,11 @@ class ReActAgent:
275
297
  filtered_args = dict(arguments) if has_var_kw else {k: v for k, v in arguments.items() if k in sig.parameters}
276
298
  result = tool_functions[tool_name](**filtered_args)
277
299
  # Erase the running line first
278
- try:
279
- width = self.console.size.width
280
- except Exception:
281
- width = 80
282
- import math
283
- num_lines = math.ceil(getattr(self, "_last_running_indicator_len", 40) / max(1, width))
284
- for _ in range(num_lines):
285
- self.console.print("\033[F\033[K", end="")
300
+ _erase_running_line()
286
301
  self._render_tool_panel(tool_name, arguments, result)
287
302
  return str(result)
288
303
  except Exception as e:
289
- try:
290
- width = self.console.size.width
291
- except Exception:
292
- width = 80
293
- import math
294
- num_lines = math.ceil(getattr(self, "_last_running_indicator_len", 40) / max(1, width))
295
- for _ in range(num_lines):
296
- self.console.print("\033[F\033[K", end="")
304
+ _erase_running_line()
297
305
 
298
306
  # Auto-repair: if the tool failed due to a syntax error in the
299
307
  # generated code, attempt an "on the go" syntax fix before
@@ -5,8 +5,8 @@ A dedicated, lightweight helper miniagent sharing the core UTIM LLM execution ar
5
5
  Provides real-time project insight, chat explanation, and conversational companionship
6
6
  without interrupting the main agent's active execution loop.
7
7
 
8
- Uses 'nvidia/nemotron-3.5-lightning:free' as the primary model (first fallback), then falls
9
- back through all registered free models from the server model_registry.
8
+ Uses a spare model from the user's current plan (chosen dynamically, never the main
9
+ agent's model and never a hardcoded slug).
10
10
  Has access to exactly one tool: 'read_chat'.
11
11
  """
12
12
 
@@ -26,12 +26,10 @@ from rich import box
26
26
 
27
27
  # ── Sidecar Helper Model ───────────────────────────────────────────
28
28
 
29
- PRIMARY_HELPER_MODEL = "nvidia/nemotron-3.5-lightning:free"
30
-
31
-
32
29
  def get_free_models_fallback_chain() -> List[str]:
33
- """Return single primary helper model (fallback chain disabled)."""
34
- return [PRIMARY_HELPER_MODEL]
30
+ """Spare plan models for the sidecar helper (dynamic; main agent's model excluded)."""
31
+ from utim_cli.background_models import get_background_models
32
+ return get_background_models(limit=3)
35
33
 
36
34
 
37
35
 
@@ -340,7 +338,10 @@ def execute_ask_helper(
340
338
  if not console:
341
339
  console = Console(highlight=False, markup=True)
342
340
 
343
- model = PRIMARY_HELPER_MODEL
341
+ from utim_cli.background_models import get_background_model
342
+ model = get_background_model()
343
+ if not model:
344
+ return "No spare model is available on your plan for the sidecar helper (the main agent's model is never shared)."
344
345
 
345
346
  # Pre-extract live session chat history and context
346
347
  chat_summary = read_chat_history(orchestrator, max_turns=12)
@@ -0,0 +1,187 @@
1
+ """Dynamic, plan-aware model selection for background (non-main-agent) work.
2
+
3
+ No model slugs are hardcoded here. Background tasks (compression, reflection,
4
+ sidecar helper, decision/judge calls, miniagents, ...) pick from the models the
5
+ server says the *current plan* may use, and they NEVER pick a model that the main
6
+ agent is occupying. If the plan has no spare model, an empty list is returned and
7
+ callers must degrade gracefully (local heuristics / skip) instead of stealing the
8
+ main agent's model.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import threading
14
+ import time
15
+ from typing import Iterable, List, Optional
16
+
17
+ _CACHE_TTL = 300.0
18
+ _lock = threading.Lock()
19
+ _cache: dict = {"ts": 0.0, "tier": None, "models": []}
20
+
21
+ _NON_TEXT_KEYWORDS = (
22
+ "lyria", "music", "audio", "whisper", "tts", "elevenlabs", "suno", "udio",
23
+ "video", "bge-m3", "rerank", "embed", "image", "krea",
24
+ )
25
+
26
+
27
+ def _current_plan() -> str:
28
+ try:
29
+ from utim_cli.config import config
30
+ return str(config.get("user_plan") or "free").lower().strip() or "free"
31
+ except Exception:
32
+ return "free"
33
+
34
+
35
+ def _is_text_model(m: dict) -> bool:
36
+ mid = str(m.get("model_id") or m.get("id") or "").lower()
37
+ if not mid or any(k in mid for k in _NON_TEXT_KEYWORDS):
38
+ return False
39
+ caps = [str(c).lower() for c in (m.get("capabilities") or [])]
40
+ if m.get("is_image_gen") or "image_generation" in caps or "imagegen" in caps:
41
+ return False
42
+ return True
43
+
44
+
45
+ def _cost(m: dict) -> float:
46
+ try:
47
+ return float(m.get("cost_input_per_1m") or 0.0) + float(m.get("cost_output_per_1m") or 0.0)
48
+ except Exception:
49
+ return 0.0
50
+
51
+
52
+ def get_plan_models(force: bool = False) -> List[str]:
53
+ """Text models available to the current plan, cheapest first (server-driven)."""
54
+ plan = _current_plan()
55
+ now = time.monotonic()
56
+ with _lock:
57
+ if (not force and _cache["tier"] == plan and _cache["models"]
58
+ and now - _cache["ts"] < _CACHE_TTL):
59
+ return list(_cache["models"])
60
+
61
+ rows: list = []
62
+ try:
63
+ from utim_cli.client_utils import fetch_remote_models_catalog
64
+ cat = fetch_remote_models_catalog() or {}
65
+ rows = list(cat.get("main_agent") or cat.get("all_text") or [])
66
+ except Exception:
67
+ rows = []
68
+
69
+ rows = [m for m in rows if isinstance(m, dict) and _is_text_model(m)]
70
+ rows.sort(key=_cost)
71
+ ids: List[str] = []
72
+ for m in rows:
73
+ mid = str(m.get("model_id") or m.get("id") or "").strip()
74
+ if mid and mid not in ids:
75
+ ids.append(mid)
76
+
77
+ with _lock:
78
+ if ids:
79
+ _cache.update(ts=now, tier=plan, models=ids)
80
+ elif _cache["tier"] == plan:
81
+ # Network failure: keep serving the last known plan list.
82
+ return list(_cache["models"])
83
+ return ids
84
+
85
+
86
+ def occupied_main_models(extra: Iterable[Optional[str]] = ()) -> set:
87
+ """Model ids the main agent is using right now (never used for background)."""
88
+ used = {str(x).strip() for x in extra if x}
89
+ try:
90
+ from utim_cli.config import config
91
+ mm = config.get("main_model")
92
+ if mm:
93
+ used.add(str(mm).strip())
94
+ except Exception:
95
+ pass
96
+ try:
97
+ from utim_cli import tools as _tools
98
+ mid = getattr(_tools, "_active_model_id", "") or ""
99
+ if mid:
100
+ used.add(str(mid).strip())
101
+ except Exception:
102
+ pass
103
+ return used
104
+
105
+
106
+ def get_background_models(main_model: Optional[str] = None, limit: int = 3) -> List[str]:
107
+ """Ordered background-capable models for the current plan, excluding main agent's."""
108
+ busy = occupied_main_models([main_model])
109
+ out = [m for m in get_plan_models() if m not in busy]
110
+ return out[:limit] if limit else out
111
+
112
+
113
+ def get_background_model(main_model: Optional[str] = None) -> Optional[str]:
114
+ """Single best background model, or None when the plan has no spare model."""
115
+ models = get_background_models(main_model, limit=1)
116
+ return models[0] if models else None
117
+
118
+
119
+ _gated: dict = {} # model_id -> expiry (monotonic)
120
+ _GATED_TTL = 600.0
121
+
122
+
123
+ def mark_gated(model_id: Optional[str]) -> None:
124
+ """Remember a model the server refused so it is never chosen as a fallback again."""
125
+ if model_id:
126
+ _gated[str(model_id).strip()] = time.monotonic() + _GATED_TTL
127
+
128
+
129
+ def _gated_now() -> set:
130
+ now = time.monotonic()
131
+ for k in [k for k, v in _gated.items() if v < now]:
132
+ _gated.pop(k, None)
133
+ return set(_gated)
134
+
135
+
136
+ def _tier_catalog_rows() -> list:
137
+ """Plan-filtered catalog (server applies tier gating) — not the unfiltered /models list."""
138
+ try:
139
+ import requests
140
+ from utim_cli.auth import SERVER_URL, get_auth_headers
141
+ resp = requests.get(
142
+ f"{SERVER_URL.rstrip('/')}/models/catalog",
143
+ params={"tier": _current_plan()},
144
+ headers=get_auth_headers(),
145
+ timeout=4.0,
146
+ )
147
+ if resp.status_code == 200:
148
+ data = resp.json()
149
+ if isinstance(data, dict):
150
+ return list(data.get("main_agent") or data.get("all_text") or [])
151
+ except Exception:
152
+ pass
153
+ return []
154
+
155
+
156
+ def get_plan_fallback_model(exclude: Iterable[Optional[str]] = ()) -> Optional[str]:
157
+ """Model to auto-switch the MAIN agent to when its current model is gated.
158
+
159
+ Dynamic and loop-safe: marks every excluded model as gated, then picks a free
160
+ model from the server's plan-filtered catalog that has not been gated before.
161
+ Returns None when nothing is left so the caller stops instead of ping-ponging.
162
+ """
163
+ for x in exclude:
164
+ mark_gated(x)
165
+ skip = _gated_now()
166
+
167
+ rows = [m for m in _tier_catalog_rows() if isinstance(m, dict) and _is_text_model(m)]
168
+ if not rows:
169
+ try:
170
+ from utim_cli.client_utils import fetch_remote_models_catalog
171
+ cat = fetch_remote_models_catalog() or {}
172
+ rows = [m for m in (cat.get("main_agent") or cat.get("all_text") or [])
173
+ if isinstance(m, dict) and _is_text_model(m)]
174
+ except Exception:
175
+ rows = []
176
+ rows.sort(key=_cost)
177
+
178
+ def _id(m):
179
+ return str(m.get("model_id") or m.get("id") or "").strip()
180
+
181
+ def _free(m):
182
+ return bool(m.get("is_free") or str(m.get("tier", "")).lower() == "free" or _cost(m) == 0.0)
183
+
184
+ for m in rows:
185
+ if _id(m) and _id(m) not in skip and _free(m):
186
+ return _id(m)
187
+ return None
@@ -0,0 +1,207 @@
1
+ """
2
+ utim_cli/behavior_telemetry.py
3
+ ==============================
4
+ Client-side interaction telemetry for the server's bot detector.
5
+
6
+ Why this exists
7
+ ---------------
8
+ The server can observe *what* an account sends but not *how* it was typed.
9
+ Typing cadence and error-correction behaviour are strong, cheap human signals:
10
+ a script producing pristine 40-char-per-second input is trivially separable
11
+ from a person who types ~6 CPS and backspaces twice a sentence.
12
+
13
+ Design constraints
14
+ ------------------
15
+ 1. **Privacy-first.** We never record prompt content, keystrokes themselves,
16
+ clipboard data, or anything identifying. Only *aggregate counters* over a
17
+ rolling window: characters typed, seconds spent typing, total keystrokes,
18
+ backspace count, paste events. No text ever leaves the machine.
19
+ 2. **Zero latency.** Counting is a few integer increments on the prompt_toolkit
20
+ input path. Nothing blocks, nothing is hashed, no I/O.
21
+ 3. **Advisory only.** The server treats this as corroborating evidence that may
22
+ raise suspicion but never lower it, and never convict on its own. A bug here
23
+ cannot get a legitimate user throttled.
24
+ 4. **Cheap to disable.** Set ``UTIM_TELEMETRY=0`` to stop sending entirely; the
25
+ server renormalises onto its own server-side signals.
26
+
27
+ Window semantics
28
+ ----------------
29
+ Counters accumulate over a sliding window (default 600s). After each request we
30
+ emit the current window snapshot and reset the *typing* counters, so each
31
+ request describes the effort that went into producing that specific prompt.
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import os
37
+ import threading
38
+ import time
39
+ from typing import Any, Dict, Optional
40
+
41
+ # Rolling window for accumulation. Long enough to cover composing a long prompt,
42
+ # short enough that an idle tab does not dilute the rate to zero.
43
+ _WINDOW_SECONDS = 600.0
44
+
45
+ _lock = threading.Lock()
46
+
47
+ _state: Dict[str, Any] = {
48
+ "window_start": 0.0,
49
+ "typing_chars": 0,
50
+ "typing_seconds": 0.0,
51
+ "keystrokes": 0,
52
+ "corrections": 0,
53
+ "paste_events": 0,
54
+ "session_start": 0.0,
55
+ "last_input_at": 0.0,
56
+ "idle_seconds": 0.0,
57
+ }
58
+
59
+ # Idle gaps longer than this are treated as "away from keyboard" and are not
60
+ # counted as typing time (prevents a coffee break from tanking the CPS rate).
61
+ _IDLE_GAP = 8.0
62
+
63
+
64
+ def is_enabled() -> bool:
65
+ return os.environ.get("UTIM_TELEMETRY", "1") not in ("0", "false", "False", "no")
66
+
67
+
68
+ def _now() -> float:
69
+ return time.monotonic()
70
+
71
+
72
+ def _ensure_window(now: float) -> None:
73
+ """Reset accumulators when the rolling window has elapsed."""
74
+ start = _state["window_start"]
75
+ if start <= 0.0 or (now - start) > _WINDOW_SECONDS:
76
+ _state["window_start"] = now
77
+ _state["typing_chars"] = 0
78
+ _state["typing_seconds"] = 0.0
79
+ _state["keystrokes"] = 0
80
+ _state["corrections"] = 0
81
+ _state["paste_events"] = 0
82
+
83
+
84
+ def _start_session() -> None:
85
+ if _state["session_start"] <= 0.0:
86
+ _state["session_start"] = _now()
87
+
88
+
89
+ def record_keystroke(chars: int = 1) -> None:
90
+ """Record printable input. Call from the prompt input handler.
91
+
92
+ Kept deliberately trivial: this runs on every keystroke, so it must not
93
+ allocate, hash, or touch the filesystem.
94
+ """
95
+ if not is_enabled():
96
+ return
97
+ now = _now()
98
+ with _lock:
99
+ _start_session()
100
+ _ensure_window(now)
101
+
102
+ last = _state["last_input_at"]
103
+ if last > 0.0:
104
+ gap = now - last
105
+ if gap <= _IDLE_GAP:
106
+ _state["typing_seconds"] += gap
107
+ else:
108
+ _state["idle_seconds"] += gap
109
+ _state["last_input_at"] = now
110
+
111
+ if chars > 0:
112
+ _state["typing_chars"] += chars
113
+ _state["keystrokes"] += chars
114
+
115
+
116
+ def record_correction() -> None:
117
+ """Record a backspace / delete. Humans correct; scripts rarely do."""
118
+ if not is_enabled():
119
+ return
120
+ now = _now()
121
+ with _lock:
122
+ _start_session()
123
+ _ensure_window(now)
124
+ _state["corrections"] += 1
125
+ _state["keystrokes"] += 1
126
+
127
+
128
+ def record_paste(chars: int = 0) -> None:
129
+ """Record a paste event (bulk input is machine-assisted, not typed)."""
130
+ if not is_enabled():
131
+ return
132
+ now = _now()
133
+ with _lock:
134
+ _start_session()
135
+ _ensure_window(now)
136
+ _state["paste_events"] += 1
137
+ # Pasted characters are NOT counted as typing_chars — that is the whole
138
+ # point: they inflate volume without demonstrating human typing effort.
139
+ _state["keystrokes"] += max(0, chars)
140
+
141
+
142
+ def snapshot(reset: bool = True) -> Optional[Dict[str, Any]]:
143
+ """Return the current window snapshot for transmission.
144
+
145
+ Returns None when telemetry is disabled or there is nothing meaningful to
146
+ report (so we do not send empty headers on every request).
147
+ """
148
+ if not is_enabled():
149
+ return None
150
+
151
+ now = _now()
152
+ with _lock:
153
+ _ensure_window(now)
154
+
155
+ typing_chars = _state["typing_chars"]
156
+ typing_seconds = _state["typing_seconds"]
157
+ keystrokes = _state["keystrokes"]
158
+ corrections = _state["corrections"]
159
+ paste_events = _state["paste_events"]
160
+ session_start = _state["session_start"]
161
+ idle_seconds = _state["idle_seconds"]
162
+
163
+ if reset:
164
+ _state["typing_chars"] = 0
165
+ _state["typing_seconds"] = 0.0
166
+ _state["keystrokes"] = 0
167
+ _state["corrections"] = 0
168
+ _state["paste_events"] = 0
169
+ _state["window_start"] = now
170
+
171
+ # Nothing worth reporting — avoid sending a misleading all-zero header,
172
+ # which the server would score as "pristine input".
173
+ if keystrokes <= 0 and typing_chars <= 0 and paste_events <= 0:
174
+ return None
175
+
176
+ session_seconds = (now - session_start) if session_start > 0.0 else 0.0
177
+
178
+ return {
179
+ "typing_chars": int(typing_chars),
180
+ "typing_seconds": round(float(typing_seconds), 2),
181
+ "keystrokes": int(keystrokes),
182
+ "corrections": int(corrections),
183
+ "paste_events": int(paste_events),
184
+ "session_seconds": round(float(session_seconds), 2),
185
+ "idle_seconds": round(float(idle_seconds), 2),
186
+ }
187
+
188
+
189
+ def telemetry_header_value() -> Optional[str]:
190
+ """Compact JSON header value, or None when disabled/empty."""
191
+ snap = snapshot(reset=True)
192
+ if snap is None:
193
+ return None
194
+ try:
195
+ return json.dumps(snap, separators=(",", ":"))
196
+ except Exception:
197
+ return None
198
+
199
+
200
+ def reset() -> None:
201
+ """Clear all counters (used by tests)."""
202
+ with _lock:
203
+ for k in ("typing_chars", "keystrokes", "corrections", "paste_events"):
204
+ _state[k] = 0
205
+ for k in ("typing_seconds", "idle_seconds", "window_start",
206
+ "session_start", "last_input_at"):
207
+ _state[k] = 0.0
@@ -81,13 +81,14 @@ BRAIN_TOP_K_FORWARD = 12 # MiniLM candidates from forward pass
81
81
  BRAIN_TOP_K_REVERSE = 6 # MiniLM candidates per Qwen sub-query
82
82
  BRAIN_FINAL_LIMIT = 6 # max memories injected into system prompt
83
83
  BRAIN_RELEVANCE_FLOOR = 0.30 # cosine similarity floor for injection
84
- QWEN_BRAIN_MODEL = "qwen/qwen2.5-1.5b-instruct" # fast reasoning model on server
85
84
 
86
- # Reuse the reflection model list so brain calls use the same fallback chain
87
- try:
88
- from utim_cli.reflection import REFLECTION_MODELS
89
- except Exception:
90
- REFLECTION_MODELS = ["openrouter/free", QWEN_BRAIN_MODEL]
85
+ def _background_models():
86
+ """Dynamic, plan-aware background models (never hardcoded, never the main agent's)."""
87
+ try:
88
+ from utim_cli.background_models import get_background_models
89
+ return get_background_models(limit=3)
90
+ except Exception:
91
+ return []
91
92
 
92
93
  # ── Paths ──────────────────────────────────────────────────────────────────────
93
94
 
@@ -253,7 +254,7 @@ def _call_qwen_brain(prompt: str, system: str = "", max_tokens: int = 400) -> st
253
254
  import requests as _req
254
255
  _system = system or "You are a precise AI assistant. Be concise and accurate."
255
256
 
256
- for model in REFLECTION_MODELS:
257
+ for model in _background_models():
257
258
  try:
258
259
  resp = _req.post(
259
260
  "https://openrouter.ai/api/v1/chat/completions",
@@ -933,10 +934,9 @@ def _call_decision_model(
933
934
  if not prompt:
934
935
  return ""
935
936
 
936
- candidates = list(models) if models else [
937
- "respan/span-01-lite:free",
938
- "openai/gpt-4o-mini",
939
- ]
937
+ candidates = list(models) if models else _background_models()
938
+ if not candidates:
939
+ return ""
940
940
 
941
941
  # 1) Prefer the in-process proxy (session reuse / rate-limit headroom).
942
942
  try: