utim-cli 2.3.19b0__tar.gz → 2.3.20__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 (171) hide show
  1. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/CHANGELOG.md +3 -6
  2. {utim_cli-2.3.19b0/utim_cli.egg-info → utim_cli-2.3.20}/PKG-INFO +1 -1
  3. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/pyproject.toml +1 -1
  4. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim/__init__.py +1 -1
  5. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/__init__.py +1 -1
  6. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/_version.py +1 -5
  7. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/orchestrator.py +46 -0
  8. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/orchestrator_llm_transport.py +45 -122
  9. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/orchestrator_turn_history.py +5 -1
  10. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/hooks.py +11 -3
  11. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/admin_auth.py +194 -163
  12. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/auth.py +106 -5
  13. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/db.py +71 -3
  14. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/key_rotator.py +264 -53
  15. utim_cli-2.3.20/utim_cli/server/plan_integrity.py +1064 -0
  16. utim_cli-2.3.20/utim_cli/server/rate_limit.py +38 -0
  17. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/router.py +66 -9
  18. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/admin_bonus_routes.py +8 -0
  19. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/admin_db_routes.py +422 -4
  20. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/auth_routes.py +47 -13
  21. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/completion_routes.py +114 -29
  22. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/credit_routes.py +399 -357
  23. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/quota_routes.py +158 -48
  24. utim_cli-2.3.20/utim_cli/server/routes/webhook_routes.py +99 -0
  25. utim_cli-2.3.20/utim_cli/server/security_alerts.py +82 -0
  26. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/share_tui.py +1 -1
  27. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/quota_share_dialog.py +458 -446
  28. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/utim.py +28 -3
  29. {utim_cli-2.3.19b0 → utim_cli-2.3.20/utim_cli.egg-info}/PKG-INFO +1 -1
  30. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli.egg-info/SOURCES.txt +3 -0
  31. utim_cli-2.3.19b0/utim_cli/server/rate_limit.py +0 -27
  32. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/LICENSE +0 -0
  33. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/MANIFEST.in +0 -0
  34. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/README.md +0 -0
  35. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/setup.cfg +0 -0
  36. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/setup.py +0 -0
  37. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/_verify_repro.py +0 -0
  38. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/_verify_syntax.py +0 -0
  39. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/_wizard_smoke_test.py +0 -0
  40. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/agent.py +0 -0
  41. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/ask_helper.py +0 -0
  42. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/auth.py +0 -0
  43. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/backup.py +0 -0
  44. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/billing.py +0 -0
  45. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/blender_agent.py +0 -0
  46. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/bootstrap.py +0 -0
  47. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/brain.py +0 -0
  48. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
  49. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/client_utils.py +0 -0
  50. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/config.py +0 -0
  51. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/constants.py +0 -0
  52. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/context_pruner.py +0 -0
  53. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/crawler/__init__.py +0 -0
  54. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/crawler/engine.py +0 -0
  55. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/crawler/markdown_distiller.py +0 -0
  56. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/dependency_manager.py +0 -0
  57. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/doctor.py +0 -0
  58. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/evidence_registry.py +0 -0
  59. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/harbor.py +0 -0
  60. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/knowledge_graph.py +0 -0
  61. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/local_db.py +0 -0
  62. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/logger.py +0 -0
  63. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/mcp_clean_wrapper.py +0 -0
  64. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/mcp_client.py +0 -0
  65. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/mcp_registry.json +0 -0
  66. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/models.txt +0 -0
  67. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/orchestrator_helpers.py +0 -0
  68. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/orchestrator_tool_exec.py +0 -0
  69. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/personalities.py +0 -0
  70. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/platform_installer.py +0 -0
  71. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/__init__.py +0 -0
  72. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/loader.py +0 -0
  73. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/manager.py +0 -0
  74. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/registry.py +0 -0
  75. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/runtime.py +0 -0
  76. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/schema.py +0 -0
  77. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/plugins/tool_loader.py +0 -0
  78. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/reflection.py +0 -0
  79. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/report.py +0 -0
  80. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/scrapy_search.py +0 -0
  81. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/__init__.py +0 -0
  82. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/config.py +0 -0
  83. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/context.py +0 -0
  84. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/events.py +0 -0
  85. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/handlers.py +0 -0
  86. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/session.py +0 -0
  87. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/sdk/sidecar.py +0 -0
  88. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/__init__.py +0 -0
  89. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/attribution.py +0 -0
  90. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/audit_log.py +0 -0
  91. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/batch_processor.py +0 -0
  92. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/captcha.py +0 -0
  93. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/cli_auth.py +0 -0
  94. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/concurrency.py +0 -0
  95. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  96. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/about.md +0 -0
  97. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/changelog.md +0 -0
  98. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/docs.md +0 -0
  99. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/features.md +0 -0
  100. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/license.md +0 -0
  101. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/pricing.md +0 -0
  102. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/privacy.md +0 -0
  103. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/refund.md +0 -0
  104. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/support.md +0 -0
  105. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/docs_md/terms.md +0 -0
  106. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/email_utils.py +0 -0
  107. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/exchange_rate.py +0 -0
  108. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/firebase.py +0 -0
  109. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/fix_duplicate_users.py +0 -0
  110. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/history.py +0 -0
  111. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/local_llm.py +0 -0
  112. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/logging_config.py +0 -0
  113. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/micro_batcher.py +0 -0
  114. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/model_agent.py +0 -0
  115. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/models.py +0 -0
  116. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/pricing_updater.py +0 -0
  117. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/provision_build.py +0 -0
  118. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/rewards_engine.py +0 -0
  119. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/__init__.py +0 -0
  120. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/billing_routes.py +0 -0
  121. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/feedback_routes.py +0 -0
  122. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/marketplace_routes.py +0 -0
  123. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/quota_share_routes.py +0 -0
  124. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/referral_routes.py +0 -0
  125. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/rewards_routes.py +0 -0
  126. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/security_routes.py +0 -0
  127. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/session_routes.py +0 -0
  128. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/routes/share_routes.py +0 -0
  129. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/server.py +0 -0
  130. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/storage_nodes.py +0 -0
  131. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/server/utils.py +0 -0
  132. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/share.py +0 -0
  133. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/situational_scoring.py +0 -0
  134. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/state.py +0 -0
  135. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/subagent_manager.py +0 -0
  136. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/task_dispatcher.py +0 -0
  137. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/task_logger.py +0 -0
  138. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tools.py +0 -0
  139. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tools_grep.py +0 -0
  140. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/__init__.py +0 -0
  141. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/feedback_dialog.py +0 -0
  142. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/history_dialog.py +0 -0
  143. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/marketplace_app_state.py +0 -0
  144. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/marketplace_dialog.py +0 -0
  145. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  146. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/mcp_dialog.py +0 -0
  147. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/miniagents_dialog.py +0 -0
  148. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/model_dialog.py +0 -0
  149. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/personality_dialog.py +0 -0
  150. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/plugins_dialog.py +0 -0
  151. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/publish_dialog.py +0 -0
  152. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/quota_dialog.py +0 -0
  153. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  154. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/resume_dialog.py +0 -0
  155. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/rewards_tui.py +0 -0
  156. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/skills_dialog.py +0 -0
  157. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/subagents_dialog.py +0 -0
  158. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/tasks_dialog.py +0 -0
  159. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/thinking_display.py +0 -0
  160. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/tools_dialog.py +0 -0
  161. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/tui/update_dialog.py +0 -0
  162. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/update_checker.py +0 -0
  163. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/utilities.py +0 -0
  164. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/utimmodel.txt +0 -0
  165. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/vector_memory.py +0 -0
  166. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/wheel.py +0 -0
  167. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli/workspace.py +0 -0
  168. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli.egg-info/dependency_links.txt +0 -0
  169. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli.egg-info/entry_points.txt +0 -0
  170. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli.egg-info/requires.txt +0 -0
  171. {utim_cli-2.3.19b0 → utim_cli-2.3.20}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,12 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## [2.3.19-beta] - 2026-09-03
3
+ ## [2.3.20] - 2026-09-04
4
4
 
5
- ### 🚀 Beta Release: Tier Enforcement & Model Access Fixes
6
- - **Websearch tier gate**: agentic websearch (LLM query synthesis + synthesis) is now paid-plan only; free-tier users always get the fast non-agentic path
7
- - **Tool registry cleanup**: removed `analyzeimage` from callable tools — vision models receive image bytes directly via `readfile`, non-vision models get internal auto-descriptions
8
- - **APINEX models ungated**: APINEX models are now available on every plan (paid included) as free models
9
- - Version tagged as **beta** (`2.3.19-beta`) for beta-channel upload
5
+ ### 🛠️ Minor Fixes and Enhancement
6
+ - Minor fixes and enhancement
10
7
 
11
8
  ## [2.3.19] - 2026-09-02
12
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.3.19b0
3
+ Version: 2.3.20
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.3.19-beta"
7
+ version = "2.3.20"
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.3.19-beta"
17
+ __version__ = "2.3.20"
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.3.19-beta"
8
+ __version__ = "2.3.20"
9
9
 
10
10
 
11
11
  import builtins, os, pathlib, urllib.parse, re
@@ -11,10 +11,6 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- # Beta release: "-beta" is the machine-valid spelling of "2.3.19 beta"
15
- # (npm semver prerelease tag + PEP 440 pre-release segment; a space would
16
- # break npm publish and pip). parse_version() strips the suffix so update
17
- # checks still compare on the numeric core 2.3.19.
18
- VERSION = "2.3.19-beta"
14
+ VERSION = "2.3.20"
19
15
 
20
16
 
@@ -594,6 +594,30 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
594
594
  except Exception:
595
595
  pass
596
596
 
597
+ # ── Sanitize: strip base64-encoded binary blobs ─────────────────
598
+ # On Termux, copying an image file can place raw base64 data into the
599
+ # clipboard. If any paste-chip expansion path leaks this binary noise
600
+ # into the user message, we detect and truncate it here before it
601
+ # reaches the model context.
602
+ _B64_RUN_RE = re.compile(r'[A-Za-z0-9+/=]{400,}')
603
+ if _B64_RUN_RE.search(user_message):
604
+ # Count total base64-characters vs meaningful content
605
+ b64_chars = sum(1 for c in user_message if 'A' <= c <= 'Z' or 'a' <= c <= 'z' or '0' <= c <= '9' or c in '+/=')
606
+ total_chars = max(len(user_message), 1)
607
+ if total_chars > 600 and (b64_chars / total_chars) > 0.85:
608
+ # The message is dominated by binary noise — keep only
609
+ # non-base64 lines and any embedded file paths.
610
+ clean_lines = []
611
+ for line in user_message.split('\n'):
612
+ line_stripped = line.strip()
613
+ b64_ratio = sum(1 for c in line_stripped if 'A' <= c <= 'Z' or 'a' <= c <= 'z' or '0' <= c <= '9' or c in '+/=') / max(len(line_stripped), 1)
614
+ # Keep lines that are paths or have low base64 ratio
615
+ if b64_ratio < 0.70 or re.search(r'[A-Za-z]:[\\/]|/[^\s]+\.\w{2,4}', line_stripped):
616
+ clean_lines.append(line)
617
+ if clean_lines:
618
+ user_message = '\n'.join(clean_lines)
619
+ user_message += f"\n\n[Pasted binary clipboard data stripped — {total_chars} chars of base64 noise removed]"
620
+
597
621
  # Auto-detect image paths in user_message and send direct multimodal vision payload
598
622
  try:
599
623
  img_matches = re.findall(r'(?:[A-Za-z]:[\\/][^\s"\']+\.(?:png|jpg|jpeg|webp|gif)|/[^\s"\']+\.(?:png|jpg|jpeg|webp|gif))', user_message, re.IGNORECASE)
@@ -829,6 +853,17 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
829
853
 
830
854
  break # success
831
855
  except _ServerUnavailableError as exc:
856
+ _err_text = str(exc).lower()
857
+ if "rate limit" in _err_text or "rate-limit" in _err_text or "429" in _err_text or "too many requests" in _err_text:
858
+ self.console.print(f"\n[bold yellow]⏳ Model is temporarily rate-limited by the provider.[/bold yellow]")
859
+ self.console.print(" [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n")
860
+ if len(self.messages) <= turn_msg_start + 1:
861
+ self.messages.append({
862
+ "role": "assistant",
863
+ "content": "⏳ **Model Rate Limited**: The model is temporarily rate-limited by the provider. Please wait a short moment and retry, or use `/model` to switch models."
864
+ })
865
+ self._persist_messages()
866
+ return
832
867
  if self.cancel_event.is_set():
833
868
  del self.messages[turn_msg_start + 1:]
834
869
  self._persist_messages()
@@ -875,6 +910,17 @@ class Orchestrator(LLMTransportMixin, TurnHistoryMixin, ToolExecutionMixin):
875
910
  self.messages.append({"role": "assistant", "content": "[Aborted by user]"})
876
911
  break
877
912
  except Exception as exc:
913
+ _err_text = str(exc).lower()
914
+ if "rate limit" in _err_text or "rate-limit" in _err_text or "429" in _err_text or "too many requests" in _err_text:
915
+ self.console.print(f"\n[bold yellow]⏳ Model is temporarily rate-limited by the provider.[/bold yellow]")
916
+ self.console.print(" [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n")
917
+ if len(self.messages) <= turn_msg_start + 1:
918
+ self.messages.append({
919
+ "role": "assistant",
920
+ "content": "⏳ **Model Rate Limited**: The model is temporarily rate-limited by the provider. Please wait a short moment and retry, or use `/model` to switch models."
921
+ })
922
+ self._persist_messages()
923
+ return
878
924
  if self.cancel_event.is_set():
879
925
  if len(self.messages) <= turn_msg_start + 1:
880
926
  self.messages.append({"role": "assistant", "content": "[Aborted by user]"})
@@ -133,6 +133,11 @@ Contains: _get_configured_model_source, _is_custom_model_selection,
133
133
  _resolve_model_endpoint, _call_llm. Behavior-identical; Orchestrator inherits.
134
134
  """
135
135
  class LLMTransportMixin:
136
+ _PRE_THINK_PATTERNS = re.compile(
137
+ r"^\s*(\*\s*Thinking\.\.\.?|\.\.\.(\s*thinking)?|thinking\.\.\.?)\s*$",
138
+ re.IGNORECASE | re.MULTILINE,
139
+ )
140
+
136
141
  def _get_configured_model_source(self, model_id: str, *, is_primary: bool = False) -> Optional[str]:
137
142
  """Return the selected source for a model id when the picker recorded one."""
138
143
  if is_primary:
@@ -736,7 +741,9 @@ class LLMTransportMixin:
736
741
  # 1. Check if quota is exhausted (only for non-custom models)
737
742
  is_5h_exhausted = bool(quota.get("five_hour_quota_exhausted", False))
738
743
  is_monthly_exhausted = bool(quota.get("is_exhausted", False))
739
- is_exhausted = is_5h_exhausted or is_monthly_exhausted
744
+ bonus_bal = float(quota.get("bonus_balance", quota.get("free_bonus_balance", 0.0)) or 0.0)
745
+ # If the user has available bonus credits, inference is funded by bonus quota
746
+ is_exhausted = (is_5h_exhausted or is_monthly_exhausted) and bonus_bal <= 0.0
740
747
 
741
748
  if not is_custom and is_exhausted:
742
749
  reset_raw = quota.get("five_hour_reset_at") if is_5h_exhausted else (quota.get("reset_at") or quota.get("five_hour_reset_at", ""))
@@ -771,7 +778,7 @@ class LLMTransportMixin:
771
778
 
772
779
  # 2. Check if chosen model is allowed — skip entirely for BYOK/custom models
773
780
  models_allowed = quota.get("models_allowed", [])
774
- free_bonus_balance = quota.get("free_bonus_balance", 0.0) or 0.0
781
+ free_bonus_balance = float(quota.get("bonus_balance", quota.get("free_bonus_balance", 0.0)) or 0.0)
775
782
  chosen_model = override_model if override_model else self.model_id
776
783
  is_custom = self._is_custom_model_selection(chosen_model, is_primary=not override_model)
777
784
  is_free_user = (quota.get("plan") == "free" or str(quota.get("display_name", "")).lower() == "free")
@@ -1348,10 +1355,15 @@ class LLMTransportMixin:
1348
1355
  if t == "queue_status":
1349
1356
  pos = chunk.get("position", 1)
1350
1357
  status = chunk.get("status", "queued")
1358
+ tier = chunk.get("tier", "")
1359
+ prio = chunk.get("priority", 0)
1351
1360
  if status == "queued":
1352
1361
  try:
1353
1362
  from utim_cli.state import STATE
1354
- STATE["thinking_topic"] = f"Request Queued Position {pos}"
1363
+ if prio > 0 and tier:
1364
+ STATE["thinking_topic"] = f"Request Queued Position {pos} ({str(tier).title()} Priority)"
1365
+ else:
1366
+ STATE["thinking_topic"] = f"Request Queued Position {pos}"
1355
1367
  except Exception:
1356
1368
  pass
1357
1369
  elif status == "processing":
@@ -1514,12 +1526,23 @@ class LLMTransportMixin:
1514
1526
  "tool_calls": None,
1515
1527
  }, False
1516
1528
  elif _is_rate_limit:
1529
+ rate_limit_display = (
1530
+ f"⏳ **Model Rate Limited**: `{current_model}` is currently experiencing high demand and is temporarily rate-limited by the provider.\n\n"
1531
+ f"- **What to do:** Please wait a moment before retrying, or switch to a different model with `/model` or `Ctrl+M`.\n"
1532
+ f"- **Retry:** You can resend your prompt anytime."
1533
+ )
1517
1534
  if not silent:
1518
1535
  self.console.print(
1519
- f"\n[dim yellow]↪ {_friendly_name} is currently rate-limited — "
1520
- f"switching to the next available model…[/dim yellow]"
1536
+ f"\n[bold yellow]⏳ Model '{current_model}' is temporarily rate-limited by the provider.[/bold yellow]"
1537
+ )
1538
+ self.console.print(
1539
+ " [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n"
1521
1540
  )
1522
- raise RuntimeError(f"__RATE_LIMITED__:{current_model}")
1541
+ return {
1542
+ "role": "assistant",
1543
+ "content": rate_limit_display,
1544
+ "tool_calls": None,
1545
+ }, False
1523
1546
  elif _is_provider_upstream:
1524
1547
  if not silent:
1525
1548
  self.console.print(
@@ -1940,7 +1963,8 @@ class LLMTransportMixin:
1940
1963
  timeout=5,
1941
1964
  )
1942
1965
  if _qr.status_code == 200:
1943
- _has_bonus = (_qr.json().get("free_bonus_balance") or 0.0) > 0.0
1966
+ _data = _qr.json()
1967
+ _has_bonus = (float(_data.get("bonus_balance", _data.get("free_bonus_balance", 0.0)) or 0.0) > 0.0)
1944
1968
  except Exception:
1945
1969
  pass
1946
1970
  if _has_bonus:
@@ -2059,124 +2083,23 @@ class LLMTransportMixin:
2059
2083
  "tool_calls": None,
2060
2084
  }, False
2061
2085
 
2062
- if not msg_str:
2063
- msg_str = "Provider is temporarily rate-limiting this model."
2064
-
2065
- # Stash the original message so the final-exhaustion branch can use it
2066
- _rate_limit_detail = msg_str
2067
- _rate_limit_upgrade_url = upgrade_url
2068
-
2069
- # Are there more models in the fallback chain to try?
2070
- _has_more_models = model_idx < len(models_to_try) - 1
2071
-
2072
- if _has_more_models:
2073
- # Soft, dim notice — let the next model in the chain take over.
2074
- # The user doesn't need to (and shouldn't) see provider jargon.
2075
- try:
2076
- self.console.print(
2077
- f"\n[dim yellow]↪ {current_model} is busy right now — "
2078
- f"switching to the next available model…[/dim yellow]"
2079
- )
2080
- except Exception:
2081
- pass
2082
- # Break the inner retry loop → outer loop will pick the next model
2083
- last_exc = exc
2084
- break
2085
- else:
2086
- # Last model in chain also rate-limited — show a CLEAN, friendly
2087
- # banner. Mask raw OpenRouter jargon. If the model name looks
2088
- # like a free/OpenRouter endpoint, frame it as "all free
2089
- # models are momentarily busy" instead of a paid-upgrade CTA.
2090
- _is_free_tier_model = (
2091
- ":free" in current_model
2092
- or "/free" in current_model
2093
- or "openrouter" in current_model.lower()
2094
- )
2095
- _display_msg = _rate_limit_detail
2096
- # Strip provider-specific keywords that leak through to users
2097
- import re as _re
2098
- for _jargon in (
2099
- "tokens per minute", "requests per minute",
2100
- "free model", "free tier", "provider",
2101
- ):
2102
- _display_msg = _re.sub(
2103
- _re.escape(_jargon),
2104
- "",
2105
- _display_msg,
2106
- flags=_re.IGNORECASE,
2107
- )
2108
- # Word-boundary stripping for short tokens (tpm, rpm)
2109
- for _jargon in ("tpm", "rpm"):
2110
- _display_msg = _re.sub(
2111
- rf"\b{_jargon}\b",
2112
- "",
2113
- _display_msg,
2114
- flags=_re.IGNORECASE,
2115
- )
2116
- # Collapse leftover whitespace + dangling punctuation
2117
- _display_msg = _re.sub(r"\s+", " ", _display_msg).strip(" .,;:—-")
2118
- # Drop the generic "returned error:" prefix — it's provider
2119
- # boilerplate that adds no value to a human reader.
2120
- _display_msg = _re.sub(
2121
- r"^(?:returned error|error)\s*[:\-—]?\s*",
2122
- "",
2123
- _display_msg,
2124
- flags=_re.IGNORECASE,
2125
- ).strip(" .,;:—-")
2126
- # If the sanitized message is too short or doesn't really say
2127
- # anything useful, swap in a polished default.
2128
- _low = _display_msg.lower()
2129
- # Count substantive words (>= 3 chars, alpha) to detect
2130
- # sentences that were gutted by jargon stripping.
2131
- _substantive = sum(
2132
- 1 for _w in _re.findall(r"[A-Za-z]{3,}", _display_msg)
2133
- )
2134
- # Detect a "stranded period" artifact: a period that's now
2135
- # surrounded by whitespace because its surrounding words
2136
- # were stripped. This is a strong signal that the message
2137
- # was gutted by jargon removal and is no longer readable.
2138
- _has_stranded_period = bool(_re.search(r"\s\.\s", _display_msg))
2139
- _is_uninformative = (
2140
- len(_display_msg) < 12
2141
- or _substantive < 3
2142
- or _has_stranded_period
2143
- or _low in ("returned error", "error", "rate limit", "limit")
2144
- or _low in ("", ".", "..", "...")
2145
- )
2146
- if _is_uninformative:
2147
- if _is_free_tier_model:
2148
- _display_msg = (
2149
- "All free models are momentarily busy. "
2150
- "This clears automatically within a minute or two."
2151
- )
2152
- else:
2153
- _display_msg = "Rate limit reached for this model."
2154
-
2086
+ rate_limit_display = (
2087
+ f"⏳ **Model Rate Limited**: `{current_model}` is currently experiencing high demand and is temporarily rate-limited by the provider.\n\n"
2088
+ f"- **What to do:** Please wait a moment before retrying, or switch to a different model with `/model` or `Ctrl+M`.\n"
2089
+ f"- **Retry:** You can resend your prompt anytime."
2090
+ )
2091
+ if not silent:
2155
2092
  self.console.print(
2156
- f"\n[bold yellow]⏳ {_display_msg}[/bold yellow]"
2093
+ f"\n[bold yellow]⏳ Model '{current_model}' is temporarily rate-limited by the provider.[/bold yellow]"
2157
2094
  )
2158
- if _is_free_tier_model:
2159
- # Free tier → no upgrade CTA, just helpful next steps
2160
- self.console.print(
2161
- " [dim]Free models rotate through a shared capacity pool — "
2162
- "this usually clears in under a minute.[/dim]"
2163
- )
2164
- self.console.print(
2165
- " [dim]You can also pick a different model with "
2166
- "[bold]/model[/bold] or press Ctrl+M.[/dim]\n"
2167
- )
2168
- else:
2169
- if not upgrade_url:
2170
- upgrade_url = "https://utim.dev/pricing"
2171
- self.console.print(
2172
- f" [dim]Top up at [bold]{upgrade_url}[/bold] or run "
2173
- f"[bold]/balance[/bold] to check your remaining quota.[/dim]\n"
2174
- )
2175
-
2176
- raise _FatalClientError(
2177
- f"All models in the fallback chain are rate-limited: "
2178
- f"{_rate_limit_detail}"
2095
+ self.console.print(
2096
+ " [dim]Please wait a moment before retrying, or switch models using [bold]/model[/bold] (Ctrl+M).[/dim]\n"
2179
2097
  )
2098
+ return {
2099
+ "role": "assistant",
2100
+ "content": rate_limit_display,
2101
+ "tool_calls": None,
2102
+ }, False
2180
2103
  if attempt < model_retries:
2181
2104
  delay = 3 * (attempt + 1)
2182
2105
  # Interruptible wait — so Ctrl+C can abort immediately
@@ -1381,7 +1381,11 @@ class TurnHistoryMixin:
1381
1381
  with open("package.json", "r", encoding="utf-8") as f:
1382
1382
  pkg = json.load(f)
1383
1383
  if "scripts" in pkg and "test" in pkg["scripts"]:
1384
- res = subprocess.run(["npm", "test"], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=60, shell=True)
1384
+ from shutil import which as _which_npm
1385
+ _npm = _which_npm("npm")
1386
+ if _npm is None:
1387
+ return "npm not found on PATH"
1388
+ res = subprocess.run([_npm, "test"], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=60)
1385
1389
  if res.returncode != 0:
1386
1390
  return _compact_test_err("npm test", res.stdout, res.stderr)
1387
1391
  return None
@@ -107,11 +107,19 @@ def run_single_hook(
107
107
 
108
108
  argv = _resolve_interpreter(command, Path.cwd())
109
109
  stdin_data = json.dumps(payload)
110
+ # Clamp per-hook timeout: a misconfigured plugin manifest must not be
111
+ # able to hold the dispatch thread pool hostage (DoS guard).
112
+ h_timeout = min(max(float(h_timeout), 0.1), 60.0)
110
113
  if argv is None:
111
- # Shell mode
112
- argv = command
114
+ # Shell mode — run through an explicit shell argv instead of
115
+ # shell=True, which is injection-prone and resolves slower. On
116
+ # Windows, /d also skips AutoRun registry scripts (hijack guard).
117
+ if os.name == "nt":
118
+ shell_argv = ["cmd.exe", "/d", "/s", "/c", command]
119
+ else:
120
+ shell_argv = ["/bin/sh", "-c", command]
113
121
  result = subprocess.run(
114
- argv, shell=True, input=stdin_data, capture_output=True,
122
+ shell_argv, shell=False, input=stdin_data, capture_output=True,
115
123
  text=True, timeout=h_timeout, env=_build_env(plugin_name, event, payload),
116
124
  )
117
125
  else: