utim-cli 1.46.35__tar.gz → 1.46.36__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 (74) hide show
  1. {utim_cli-1.46.35 → utim_cli-1.46.36}/CHANGELOG.md +5 -0
  2. {utim_cli-1.46.35/utim_cli.egg-info → utim_cli-1.46.36}/PKG-INFO +15 -3
  3. {utim_cli-1.46.35 → utim_cli-1.46.36}/README.md +18 -12
  4. {utim_cli-1.46.35 → utim_cli-1.46.36}/pyproject.toml +11 -3
  5. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/__init__.py +1 -1
  6. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/_version.py +1 -1
  7. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/agent.py +16 -9
  8. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/doctor.py +4 -2
  9. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/orchestrator.py +66 -34
  10. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/model_dialog.py +206 -100
  11. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/utim.py +7 -8
  12. {utim_cli-1.46.35 → utim_cli-1.46.36/utim_cli.egg-info}/PKG-INFO +15 -3
  13. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli.egg-info/requires.txt +14 -2
  14. {utim_cli-1.46.35 → utim_cli-1.46.36}/LICENSE +0 -0
  15. {utim_cli-1.46.35 → utim_cli-1.46.36}/MANIFEST.in +0 -0
  16. {utim_cli-1.46.35 → utim_cli-1.46.36}/setup.cfg +0 -0
  17. {utim_cli-1.46.35 → utim_cli-1.46.36}/setup.py +0 -0
  18. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/auth.py +0 -0
  19. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/backup.py +0 -0
  20. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/blender_agent.py +0 -0
  21. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/bootstrap.py +0 -0
  22. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/client_utils.py +0 -0
  23. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/config.py +0 -0
  24. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/constants.py +0 -0
  25. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/context_pruner.py +0 -0
  26. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/knowledge_graph.py +0 -0
  27. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/local_db.py +0 -0
  28. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/logger.py +0 -0
  29. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/mcp_clean_wrapper.py +0 -0
  30. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/mcp_client.py +0 -0
  31. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/mcp_registry.json +0 -0
  32. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/reflection.py +0 -0
  33. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/report.py +0 -0
  34. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/scrapy_search.py +0 -0
  35. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/__init__.py +0 -0
  36. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/auth.py +0 -0
  37. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/db.py +0 -0
  38. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/email_utils.py +0 -0
  39. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/exchange_rate.py +0 -0
  40. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/firebase.py +0 -0
  41. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/history.py +0 -0
  42. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/logging_config.py +0 -0
  43. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/models.py +0 -0
  44. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/pricing_updater.py +0 -0
  45. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/rate_limit.py +0 -0
  46. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/router.py +0 -0
  47. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/__init__.py +0 -0
  48. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/auth_routes.py +0 -0
  49. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/completion_routes.py +0 -0
  50. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/credit_routes.py +0 -0
  51. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/feedback_routes.py +0 -0
  52. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/quota_routes.py +0 -0
  53. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/session_routes.py +0 -0
  54. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/routes/share_routes.py +0 -0
  55. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/server/server.py +0 -0
  56. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/share.py +0 -0
  57. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/share_tui.py +0 -0
  58. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/situational_scoring.py +0 -0
  59. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/state.py +0 -0
  60. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tools.py +0 -0
  61. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/__init__.py +0 -0
  62. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/feedback_dialog.py +0 -0
  63. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/history_dialog.py +0 -0
  64. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/mcp_dialog.py +0 -0
  65. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/resume_dialog.py +0 -0
  66. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/tools_dialog.py +0 -0
  67. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/tui/update_dialog.py +0 -0
  68. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/utilities.py +0 -0
  69. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/vector_memory.py +0 -0
  70. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli/workspace.py +0 -0
  71. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli.egg-info/SOURCES.txt +0 -0
  72. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli.egg-info/dependency_links.txt +0 -0
  73. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli.egg-info/entry_points.txt +0 -0
  74. {utim_cli-1.46.35 → utim_cli-1.46.36}/utim_cli.egg-info/top_level.txt +0 -0
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to the UTIM CLI Agent will be documented in this file.
4
4
 
5
+ ## [1.46.36] - 2026-07-12
6
+
7
+ ### Added
8
+ - **Reciprocal Server URL Fallbacks**: Integrated automatic fallback routing in the request session patcher to dynamically swap and retry calls between `api.utim.dev` and `utim-cli-production.up.railway.app` in case of connection timeouts or proxy blocking errors.
9
+
5
10
  ## [1.46.35] - 2026-07-11
6
11
 
7
12
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 1.46.35
3
+ Version: 1.46.36
4
4
  Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://utim.dev
@@ -20,7 +20,6 @@ Classifier: Intended Audience :: Developers
20
20
  Requires-Python: >=3.9
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: openai>=1.30.0
24
23
  Requires-Dist: requests==2.31.0
25
24
  Requires-Dist: aiohttp>=3.9.5
26
25
  Requires-Dist: python-dotenv==1.0.1
@@ -30,9 +29,10 @@ Requires-Dist: chardet==4.0.0
30
29
  Requires-Dist: typer>=0.9.0
31
30
  Requires-Dist: rich>=13.0.0
32
31
  Requires-Dist: prompt_toolkit>=3.0.0
33
- Requires-Dist: mcp>=0.1.0
34
32
  Requires-Dist: nest-asyncio>=1.5.0
35
33
  Requires-Dist: sqlalchemy>=2.0.0
34
+ Requires-Dist: openai>=1.30.0; platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")
35
+ Requires-Dist: mcp>=0.1.0; platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")
36
36
  Provides-Extra: search
37
37
  Requires-Dist: scrapy>=2.11.0; extra == "search"
38
38
  Requires-Dist: scrapy-playwright>=0.0.34; extra == "search"
@@ -44,6 +44,8 @@ Requires-Dist: tree-sitter>=0.21.0; extra == "parsers"
44
44
  Requires-Dist: tree-sitter-python>=0.21.0; extra == "parsers"
45
45
  Requires-Dist: tree-sitter-javascript>=0.21.0; extra == "parsers"
46
46
  Requires-Dist: tree-sitter-typescript>=0.21.0; extra == "parsers"
47
+ Provides-Extra: mcp
48
+ Requires-Dist: mcp>=0.1.0; extra == "mcp"
47
49
  Provides-Extra: full
48
50
  Requires-Dist: scrapy>=2.11.0; extra == "full"
49
51
  Requires-Dist: scrapy-playwright>=0.0.34; extra == "full"
@@ -53,6 +55,10 @@ Requires-Dist: tree-sitter>=0.21.0; extra == "full"
53
55
  Requires-Dist: tree-sitter-python>=0.21.0; extra == "full"
54
56
  Requires-Dist: tree-sitter-javascript>=0.21.0; extra == "full"
55
57
  Requires-Dist: tree-sitter-typescript>=0.21.0; extra == "full"
58
+ Requires-Dist: mcp>=0.1.0; (platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")) and extra == "full"
59
+ Requires-Dist: openai>=1.30.0; (platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")) and extra == "full"
60
+ Provides-Extra: legacy-agent
61
+ Requires-Dist: openai>=1.30.0; extra == "legacy-agent"
56
62
 
57
63
  # UTIM CLI Agent: Enterprise Coder Assistant
58
64
 
@@ -194,6 +200,12 @@ pip install utim-cli
194
200
  pip install "utim-cli[full]"
195
201
  ```
196
202
 
203
+ Desktop installs include the normal SDK/MCP path automatically. Mobile/Termux
204
+ installs keep those native-heavy pieces optional to avoid `pydantic-core`; use
205
+ `utim-cli[mcp]` or `utim-cli[legacy-agent]` only if you explicitly need them.
206
+ Do not install `utim_cli/server/requirements.txt` unless you are deploying the
207
+ server.
208
+
197
209
  ### Source install (development)
198
210
  ```bash
199
211
  git clone https://github.com/emendai/utim.git
@@ -131,18 +131,24 @@ Inside the interactive chat terminal, type these slash commands for direct works
131
131
  npm install -g @emend-ai/utim
132
132
  ```
133
133
 
134
- ### pip (from PyPI)
135
- ```bash
136
- pip install utim-cli
137
- # Full install with optional features (web search, images, parsers)
138
- pip install "utim-cli[full]"
139
- ```
140
-
141
- ### Source install (development)
142
- ```bash
143
- git clone https://github.com/emendai/utim.git
144
- cd utim
145
- pip install -e ".[full]"
134
+ ### pip (from PyPI)
135
+ ```bash
136
+ pip install utim-cli
137
+ # Full install with optional features (web search, images, parsers)
138
+ pip install "utim-cli[full]"
139
+ ```
140
+
141
+ Desktop installs include the normal SDK/MCP path automatically. Mobile/Termux
142
+ installs keep those native-heavy pieces optional to avoid `pydantic-core`; use
143
+ `utim-cli[mcp]` or `utim-cli[legacy-agent]` only if you explicitly need them.
144
+ Do not install `utim_cli/server/requirements.txt` unless you are deploying the
145
+ server.
146
+
147
+ ### Source install (development)
148
+ ```bash
149
+ git clone https://github.com/emendai/utim.git
150
+ cd utim
151
+ pip install -e ".[full]"
146
152
  ```
147
153
 
148
154
  ### Smoke-test your install
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "1.46.35"
7
+ version = "1.46.36"
8
8
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -22,7 +22,6 @@ classifiers = [
22
22
  "Intended Audience :: Developers",
23
23
  ]
24
24
  dependencies = [
25
- "openai>=1.30.0",
26
25
  "requests==2.31.0",
27
26
  "aiohttp>=3.9.5",
28
27
  "python-dotenv==1.0.1",
@@ -32,9 +31,10 @@ dependencies = [
32
31
  "typer>=0.9.0",
33
32
  "rich>=13.0.0",
34
33
  "prompt_toolkit>=3.0.0",
35
- "mcp>=0.1.0",
36
34
  "nest-asyncio>=1.5.0",
37
35
  "sqlalchemy>=2.0.0",
36
+ "openai>=1.30.0; platform_system != 'Linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64')",
37
+ "mcp>=0.1.0; platform_system != 'Linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64')",
38
38
  ]
39
39
 
40
40
  [project.urls]
@@ -59,6 +59,9 @@ parsers = [
59
59
  "tree-sitter-javascript>=0.21.0",
60
60
  "tree-sitter-typescript>=0.21.0",
61
61
  ]
62
+ mcp = [
63
+ "mcp>=0.1.0",
64
+ ]
62
65
  full = [
63
66
  "scrapy>=2.11.0",
64
67
  "scrapy-playwright>=0.0.34",
@@ -68,6 +71,11 @@ full = [
68
71
  "tree-sitter-python>=0.21.0",
69
72
  "tree-sitter-javascript>=0.21.0",
70
73
  "tree-sitter-typescript>=0.21.0",
74
+ "mcp>=0.1.0; platform_system != 'Linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64')",
75
+ "openai>=1.30.0; platform_system != 'Linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64')",
76
+ ]
77
+ legacy-agent = [
78
+ "openai>=1.30.0",
71
79
  ]
72
80
 
73
81
  [project.scripts]
@@ -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__ = "1.46.35"
8
+ __version__ = "1.46.36"
9
9
 
10
10
  import builtins, os, pathlib, urllib.parse, re
11
11
 
@@ -11,4 +11,4 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "1.46.35"
14
+ VERSION = "1.46.36"
@@ -1,9 +1,8 @@
1
1
  import os
2
- import json
3
- import sys
4
- import time
5
- from openai import OpenAI
6
- from .tools import get_tools
2
+ import json
3
+ import sys
4
+ import time
5
+ from .tools import get_tools
7
6
  from rich.console import Console
8
7
  from rich.live import Live
9
8
  from rich.spinner import Spinner
@@ -52,10 +51,18 @@ class ReActAgent:
52
51
  if not api_key:
53
52
  self.console.print("[dim]⚠ Warning: OPENROUTER_API_KEY is not set.[/dim]")
54
53
 
55
- self.client = OpenAI(
56
- base_url="https://openrouter.ai/api/v1",
57
- api_key=api_key or "sk-fake-key-for-init",
58
- )
54
+ try:
55
+ from openai import OpenAI
56
+ except ImportError as exc:
57
+ raise RuntimeError(
58
+ "The legacy ReActAgent requires the optional OpenAI SDK. "
59
+ "Install it with: pip install 'utim-cli[legacy-agent]'"
60
+ ) from exc
61
+
62
+ self.client = OpenAI(
63
+ base_url="https://openrouter.ai/api/v1",
64
+ api_key=api_key or "sk-fake-key-for-init",
65
+ )
59
66
  self.messages = [{"role": "system", "content": system_prompt}]
60
67
  self.start_time = None
61
68
 
@@ -21,8 +21,8 @@ def run_diagnostics(console: Console = None):
21
21
  # 2. Dependency Check
22
22
  console.print("\n [bold #42bcf5]Core Dependencies:[/bold #42bcf5]")
23
23
  core_dependencies = [
24
- "openai", "requests", "aiohttp", "typer", "rich",
25
- "prompt_toolkit", "mcp", "nest_asyncio"
24
+ "requests", "aiohttp", "typer", "rich",
25
+ "prompt_toolkit", "nest_asyncio"
26
26
  ]
27
27
  for dep in core_dependencies:
28
28
  try:
@@ -40,6 +40,8 @@ def run_diagnostics(console: Console = None):
40
40
  "scrapy": "Scrapy search crawler",
41
41
  "scrapy-playwright": "Playwright JS crawler",
42
42
  "pillow": "Image utilities (generate_image)",
43
+ "openai": "Legacy ReActAgent SDK client",
44
+ "mcp": "Model Context Protocol server integrations",
43
45
  }
44
46
  for dep, feat in optional_dependencies.items():
45
47
  try:
@@ -750,10 +750,11 @@ class Orchestrator:
750
750
  f"or .env file (looked in {_cwd_env!r} then .utim/.env).[/bold yellow]\n"
751
751
  )
752
752
 
753
- self._local_client: bool = bool(self._local_api_key)
754
-
755
- # OpenRouter base URL (can be overridden per-model for custom providers)
756
- self._openrouter_base_url = "https://openrouter.ai/api/v1/chat/completions"
753
+ self._local_client: bool = bool(self._local_api_key)
754
+
755
+ # OpenRouter base URL (can be overridden per-model for custom providers)
756
+ self._openrouter_base_url = "https://openrouter.ai/api/v1/chat/completions"
757
+ self.model_source: Optional[str] = config.get("main_model_source")
757
758
 
758
759
  # Cancellation flag
759
760
  self.cancel_event = threading.Event()
@@ -854,13 +855,14 @@ class Orchestrator:
854
855
  )
855
856
 
856
857
  # Write current active session state to session_state.json
857
- state_data = {
858
- "session_id": self.session_id,
859
- "messages": clean_messages,
860
- "turn_history": clean_turn_history,
861
- "redo_history": clean_redo_history,
862
- "model_id": self.model_id
863
- }
858
+ state_data = {
859
+ "session_id": self.session_id,
860
+ "messages": clean_messages,
861
+ "turn_history": clean_turn_history,
862
+ "redo_history": clean_redo_history,
863
+ "model_id": self.model_id,
864
+ "model_source": self.model_source,
865
+ }
864
866
  os.makedirs(".utim", exist_ok=True)
865
867
  with open(".utim/session_state.json", "w", encoding="utf-8") as f:
866
868
  json.dump(state_data, f, ensure_ascii=False, indent=4)
@@ -880,15 +882,37 @@ class Orchestrator:
880
882
  )
881
883
 
882
884
  # ── Custom-provider endpoint resolution ─────────────────────────────────
883
- def _resolve_model_endpoint(self, model_id: str) -> tuple:
884
- """Return (chat_completions_url, api_key) for *model_id*.
885
-
886
- Custom models (added via /model add) carry their own base_url and
887
- api_key; everything else falls back to OpenRouter.
888
- """
889
- custom = config.get_custom_model(model_id)
890
- if custom:
891
- base = custom.get("base_url", "").rstrip("/")
885
+ def _get_configured_model_source(self, model_id: str, *, is_primary: bool = False) -> Optional[str]:
886
+ """Return the selected source for a model id when the picker recorded one."""
887
+ if is_primary:
888
+ source = getattr(self, "model_source", None) or config.get("main_model_source")
889
+ if source in ("custom", "utim", "openrouter"):
890
+ return source
891
+
892
+ for key, value in config._data.items():
893
+ if key.startswith("subagent_model_") and not key.endswith("_source") and value == model_id:
894
+ source = config.get(f"{key}_source")
895
+ if source in ("custom", "utim", "openrouter"):
896
+ return source
897
+ return None
898
+
899
+ def _is_custom_model_selection(self, model_id: str, *, is_primary: bool = False) -> bool:
900
+ source = self._get_configured_model_source(model_id, is_primary=is_primary)
901
+ if source in ("utim", "openrouter"):
902
+ return False
903
+ if source == "custom":
904
+ return bool(config.get_custom_model(model_id))
905
+ return bool(config.get_custom_model(model_id))
906
+
907
+ def _resolve_model_endpoint(self, model_id: str, *, is_primary: bool = False) -> tuple:
908
+ """Return (chat_completions_url, api_key) for *model_id*.
909
+
910
+ Custom models (added via /model add) carry their own base_url and
911
+ api_key; everything else falls back to OpenRouter.
912
+ """
913
+ custom = config.get_custom_model(model_id) if self._is_custom_model_selection(model_id, is_primary=is_primary) else None
914
+ if custom:
915
+ base = custom.get("base_url", "").rstrip("/")
892
916
  # Append /chat/completions if the caller gave us just the base path
893
917
  if not base.endswith("/chat/completions"):
894
918
  url = base + "/chat/completions"
@@ -923,7 +947,7 @@ class Orchestrator:
923
947
  if resp.status_code == 200:
924
948
  quota = resp.json()
925
949
  chosen_model = override_model if override_model else self.model_id
926
- is_custom = bool(config.get_custom_model(chosen_model))
950
+ is_custom = self._is_custom_model_selection(chosen_model, is_primary=not override_model)
927
951
 
928
952
  # 1. Check if quota is exhausted (only for non-custom models)
929
953
  used = quota.get("credits_used", quota.get("requests_used", 0.0))
@@ -950,7 +974,7 @@ class Orchestrator:
950
974
  models_allowed = quota["models_allowed"]
951
975
  free_bonus_balance = quota.get("free_bonus_balance", 0.0) or 0.0
952
976
  chosen_model = override_model if override_model else self.model_id
953
- is_custom = bool(config.get_custom_model(chosen_model))
977
+ is_custom = self._is_custom_model_selection(chosen_model, is_primary=not override_model)
954
978
 
955
979
  # If user has active bonus credits, all models are unlocked — skip gating entirely
956
980
  if not is_custom and free_bonus_balance <= 0.0 and models_allowed != ["all"] and chosen_model not in models_allowed and not chosen_model.endswith(":free"):
@@ -960,18 +984,22 @@ class Orchestrator:
960
984
  self.console.print(" Top up at [bold]utim.dev/pricing[/bold] or run [bold]utim upgrade[/bold].\n")
961
985
  fallback_model = DEFAULT_MODEL
962
986
  self.console.print(f" Falling back to: [bold]{fallback_model}[/bold]")
963
- if override_model:
964
- override_model = fallback_model
965
- else:
966
- self.model_id = fallback_model
967
- else:
968
- fallback_model = DEFAULT_MODEL # default free fallback
987
+ if override_model:
988
+ override_model = fallback_model
989
+ else:
990
+ self.model_id = fallback_model
991
+ self.model_source = "utim"
992
+ config.set("main_model_source", "utim")
993
+ else:
994
+ fallback_model = DEFAULT_MODEL # default free fallback
969
995
  self.console.print(f"\n[bold yellow]⚠ Model '{chosen_model}' is gated under your current '{quota['display_name']}' plan.[/bold yellow]")
970
996
  self.console.print(f" Downgrading to default allowed model: '{fallback_model}' for this request.")
971
- if override_model:
972
- override_model = fallback_model
973
- else:
974
- self.model_id = fallback_model
997
+ if override_model:
998
+ override_model = fallback_model
999
+ else:
1000
+ self.model_id = fallback_model
1001
+ self.model_source = "utim"
1002
+ config.set("main_model_source", "utim")
975
1003
  except SystemExit:
976
1004
  raise
977
1005
  except Exception:
@@ -994,7 +1022,8 @@ class Orchestrator:
994
1022
  if self.cancel_event.is_set():
995
1023
  break
996
1024
 
997
- current_is_custom = bool(config.get_custom_model(current_model))
1025
+ current_is_primary = model_idx == 0
1026
+ current_is_custom = self._is_custom_model_selection(current_model, is_primary=current_is_primary and not override_model)
998
1027
  # Check for API key only if we need it for this built-in/OpenRouter model
999
1028
  if not current_is_custom and not self._local_api_key and not config.get("api_key"):
1000
1029
  continue
@@ -1061,7 +1090,10 @@ class Orchestrator:
1061
1090
  "session_id": self.session_id,
1062
1091
  }
1063
1092
  else:
1064
- _endpoint_url, _endpoint_key = self._resolve_model_endpoint(current_model)
1093
+ _endpoint_url, _endpoint_key = self._resolve_model_endpoint(
1094
+ current_model,
1095
+ is_primary=current_is_primary and not override_model,
1096
+ )
1065
1097
  _headers = {
1066
1098
  "Authorization": f"Bearer {_endpoint_key}",
1067
1099
  "Content-Type": "application/json",
@@ -1,8 +1,9 @@
1
- import os
2
- import json
3
- import httpx
4
- import re
5
- from utim_cli.config import config
1
+ import os
2
+ import json
3
+ import httpx
4
+ import re
5
+ import textwrap
6
+ from utim_cli.config import config
6
7
 
7
8
  def _safe_prompt(prompt_text: str, color: str = None, is_password: bool = False) -> str:
8
9
  """Bulletproof replacement for prompt_toolkit's prompt() when nested inside run_in_terminal context.
@@ -609,12 +610,12 @@ def _dialog_model_main(orchestrator, target="main"):
609
610
  if "free" not in tags_val:
610
611
  tags_val = list(tags_val) + ["free"]
611
612
 
612
- primary_models.append({
613
- "model_id": mid,
614
- "desc": desc_val,
615
- "tags": tags_val,
616
- "source": "openrouter"
617
- })
613
+ primary_models.append({
614
+ "model_id": mid,
615
+ "desc": desc_val,
616
+ "tags": tags_val,
617
+ "source": "utim"
618
+ })
618
619
 
619
620
  # Sort based on cost hierarchy
620
621
  primary_models = sorted(primary_models, key=lambda x: (cost_hierarchy.get(x["model_id"], (9, ""))[0], x["model_id"]))
@@ -661,62 +662,147 @@ def _dialog_model_main(orchestrator, target="main"):
661
662
  "label": "⬜ None (main agent writes prompt directly)",
662
663
  })
663
664
 
664
- # Hoist the currently selected model to the top of the list so it is highlighted by default
665
- current_model = None
666
- if target == 'main':
667
- current_model = orchestrator.model_id
668
- elif target.startswith('subagent_'):
669
- subkey = target.split('_', 1)[1]
670
- current_model = config.get(f"subagent_model_{subkey}")
671
-
672
- if current_model:
673
- current_item = None
674
- for item in models:
675
- if item["model_id"] == current_model:
676
- current_item = item
677
- break
678
- if current_item:
679
- models.remove(current_item)
680
- models.insert(0, current_item)
681
-
682
- if not models:
683
- console.print("\n[red]No models available.[/red]\n")
684
- return
685
-
686
- def render_model(i, m, sel):
687
- bg = 'bg:#a6e3a1 bold #1e1e2e' if sel else ''
688
- mid = m['model_id']
689
- source = m.get('source', 'openrouter')
690
-
691
- # Sentinel items: Non-Agent Tool / None
692
- if source == 'sentinel':
693
- label = m.get('label', mid)
694
- sentinel_style = 'bg:#f9e2af bold #1e1e2e' if sel else 'bold #f9e2af'
695
- desc_style = bg or 'fg:#585b70'
696
- current_mark = ''
697
- subkey = target.split('_', 1)[1] if target.startswith('subagent_') else ''
698
- if config.get(f"subagent_model_{subkey}") == mid:
699
- current_mark = ' ◀ current'
700
- if is_paid_plan:
701
- return [
702
- (sentinel_style, f" {label:<26}"),
703
- (sentinel_style, f"{'':<12}"),
704
- (sentinel_style, f"{'':<14}"),
705
- (desc_style, f" {m['desc']}{current_mark}\n\n"),
706
- ]
707
- else:
708
- return [
709
- (sentinel_style, f" {label}"),
710
- (sentinel_style, f"{current_mark}\n"),
711
- (desc_style, f" {m['desc']}\n\n"),
712
- ]
713
-
714
- # Clean display ID (remove provider prefix and ':free' suffix)
715
- display_id = mid
716
- if source == 'openrouter':
717
- display_id = mid.split('/', 1)[-1]
718
- if display_id.endswith(':free'):
719
- display_id = display_id[:-5]
665
+
666
+ # Sort based on cost hierarchy
667
+ primary_models = sorted(primary_models, key=lambda x: (cost_hierarchy.get(x["model_id"], (9, ""))[0], x["model_id"]))
668
+
669
+ # ── Prepend user-defined custom models ───────────────────────────────────
670
+ custom_entries = [
671
+ {
672
+ "model_id": m["model_id"],
673
+ "tags": ["custom", m.get("provider_name", "")],
674
+ "source": "custom",
675
+ "desc": f"Custom model via {m.get('provider_name', 'Custom')}."
676
+ }
677
+ for m in config.custom_models
678
+ ]
679
+ models = custom_entries + primary_models
680
+
681
+ # ── Inject sentinel options for subagent targets ───────────────────────────
682
+ # "Non-Agent Tool" — disables the LLM loop, tool runs in simple direct mode
683
+ # "None" — only available for generate_image, so main agent writes the prompt
684
+ _BLENDER_TARGETS = ('subagent_blender_vision', 'subagent_blender_code')
685
+ if target.startswith('subagent_') and target not in ('subagent_image_gen',) + _BLENDER_TARGETS:
686
+ non_agent_desc = (
687
+ "Disable the LLM subagent loop. Tool runs in direct/simple mode (e.g. web search = raw results, "
688
+ "codebase investigator = file content only, planner = task list from main agent)."
689
+ )
690
+ sentinel_label = "🚫 Non-Agent Tool (direct mode)"
691
+ if target in ("subagent_generate_image",):
692
+ sentinel_label = "⬜ None (main agent writes the image prompt)"
693
+ non_agent_desc = "Disable the prompt expander entirely — the main agent writes the image prompt directly."
694
+ models.insert(0, {
695
+ "model_id": "__non_agent__",
696
+ "desc": non_agent_desc,
697
+ "tags": ["sentinel"],
698
+ "source": "sentinel",
699
+ "label": sentinel_label,
700
+ })
701
+ if target == 'subagent_generate_image':
702
+ # Also offer a 2nd sentinel: __none__ = no prompt expander at all
703
+ models.insert(0, {
704
+ "model_id": "__none__",
705
+ "desc": "Disable the prompt expander entirely — the main agent writes the image prompt directly.",
706
+ "tags": ["sentinel"],
707
+ "source": "sentinel",
708
+ "label": "⬜ None (main agent writes prompt directly)",
709
+ })
710
+
711
+ # Hoist the currently selected model to the top of the list so it is highlighted by default
712
+ current_model = None
713
+ current_source = None
714
+ if target == 'main':
715
+ current_model = orchestrator.model_id
716
+ current_source = getattr(orchestrator, 'model_source', None) or config.get("main_model_source")
717
+ elif target.startswith('subagent_'):
718
+ subkey = target.split('_', 1)[1]
719
+ current_model = config.get(f"subagent_model_{subkey}")
720
+ current_source = config.get(f"subagent_model_{subkey}_source")
721
+
722
+ def _normalize_source(source: str | None) -> str:
723
+ return "utim" if source in (None, "", "openrouter") else source
724
+
725
+ if current_model in ("__non_agent__", "__none__"):
726
+ current_source = "sentinel"
727
+ else:
728
+ current_source = _normalize_source(current_source)
729
+
730
+ if current_model:
731
+ current_item = None
732
+ for item in models:
733
+ if item["model_id"] == current_model and _normalize_source(item.get("source")) == current_source:
734
+ current_item = item
735
+ break
736
+ if current_item:
737
+ models.remove(current_item)
738
+ models.insert(0, current_item)
739
+
740
+ if not models:
741
+ console.print("\n[red]No models available.[/red]\n")
742
+ return
743
+
744
+ def render_model(i, m, sel):
745
+ bg = 'bg:#a6e3a1 bold #1e1e2e' if sel else ''
746
+ mid = m['model_id']
747
+ source = _normalize_source(m.get('source'))
748
+
749
+ def _compact_desc(text: str, width: int) -> list[str]:
750
+ text = re.sub(r"\s+", " ", str(text or "Fast, efficient model.")).strip()
751
+ text = re.sub(r"\bThis model\b", "It", text, flags=re.IGNORECASE)
752
+ parts = re.split(r"(?<=[.!?])\s+", text)
753
+ compact = parts[0] if parts and parts[0] else text
754
+ if len(compact) < 48 and len(parts) > 1:
755
+ compact = f"{compact} {parts[1]}"
756
+ max_chars = max(width, min(120, width * 2))
757
+ if len(compact) > max_chars:
758
+ compact = compact[:max_chars].rsplit(" ", 1)[0].rstrip(" ,;:")
759
+ compact += "..."
760
+ return textwrap.wrap(
761
+ compact,
762
+ width=width,
763
+ max_lines=2,
764
+ placeholder="...",
765
+ break_long_words=False,
766
+ ) or [""]
767
+
768
+ # Sentinel items: Non-Agent Tool / None
769
+ if source == 'sentinel':
770
+ label = m.get('label', mid)
771
+ sentinel_style = 'bg:#f9e2af bold #1e1e2e' if sel else 'bold #f9e2af'
772
+ desc_style = bg or 'fg:#585b70'
773
+ current_mark = ''
774
+ subkey = target.split('_', 1)[1] if target.startswith('subagent_') else ''
775
+ if config.get(f"subagent_model_{subkey}") == mid:
776
+ current_mark = ' ◀ current'
777
+ if is_paid_plan:
778
+ desc_width = max(24, min(54, (console.size.width or 100) - 62))
779
+ desc_lines = _compact_desc(m['desc'], desc_width)
780
+ desc_text = f" {desc_lines[0]}{current_mark}\n"
781
+ for extra in desc_lines[1:]:
782
+ desc_text += f"{'':<55}{extra}\n"
783
+ desc_text += "\n"
784
+ return [
785
+ (sentinel_style, f" {label:<26}"),
786
+ (sentinel_style, f"{'':<12}"),
787
+ (sentinel_style, f"{'':<14}"),
788
+ (desc_style, desc_text),
789
+ ]
790
+ else:
791
+ desc_width = max(24, min(72, (console.size.width or 100) - 10))
792
+ desc_lines = _compact_desc(m['desc'], desc_width)
793
+ desc_text = "".join(f" {line}\n" for line in desc_lines) + "\n"
794
+ return [
795
+ (sentinel_style, f" {label}"),
796
+ (sentinel_style, f"{current_mark}\n"),
797
+ (desc_style, desc_text),
798
+ ]
799
+
800
+ # Clean display ID (remove provider prefix and ':free' suffix)
801
+ display_id = mid
802
+ if source == 'utim':
803
+ display_id = mid.split('/', 1)[-1]
804
+ if display_id.endswith(':free'):
805
+ display_id = display_id[:-5]
720
806
 
721
807
  if display_id == "gemini-3.1-pro-preview-customtools":
722
808
  display_id = "gemini-3.1-pro-preview"
@@ -727,9 +813,9 @@ def _dialog_model_main(orchestrator, target="main"):
727
813
 
728
814
 
729
815
  current = ''
730
- if target == 'main' and mid == orchestrator.model_id:
816
+ if target == 'main' and mid == orchestrator.model_id and source == current_source:
731
817
  current = ' ◀ current'
732
- elif target.startswith('subagent_') and mid == config.get(f"subagent_model_{target.split('_', 1)[1]}"):
818
+ elif target.startswith('subagent_') and mid == config.get(f"subagent_model_{target.split('_', 1)[1]}") and source == current_source:
733
819
  current = ' ◀ current'
734
820
 
735
821
  desc = m.get("desc", "Fast, efficient model for daily tasks.")
@@ -759,18 +845,28 @@ def _dialog_model_main(orchestrator, target="main"):
759
845
  else:
760
846
  cost_style = "fg:#f38ba8"
761
847
 
762
- return [
763
- (style, col_model),
764
- (cost_style, col_cost),
765
- (rec_style, col_rec),
766
- (desc_style, f" {desc}{current}\n\n"),
767
- ]
768
- else:
769
- return [
770
- (style, f" {display_id}"),
771
- (style, f"{current}\n"),
772
- (desc_style, f" {desc}\n\n"),
773
- ]
848
+ desc_width = max(24, min(54, (console.size.width or 100) - 62))
849
+ desc_lines = _compact_desc(desc, desc_width)
850
+ desc_text = f" {desc_lines[0]}{current}\n"
851
+ for extra in desc_lines[1:]:
852
+ desc_text += f"{'':<55}{extra}\n"
853
+ desc_text += "\n"
854
+
855
+ return [
856
+ (style, col_model),
857
+ (cost_style, col_cost),
858
+ (rec_style, col_rec),
859
+ (desc_style, desc_text),
860
+ ]
861
+ else:
862
+ desc_width = max(24, min(72, (console.size.width or 100) - 10))
863
+ desc_lines = _compact_desc(desc, desc_width)
864
+ desc_text = "".join(f" {line}\n" for line in desc_lines) + "\n"
865
+ return [
866
+ (style, f" {display_id}"),
867
+ (style, f"{current}\n"),
868
+ (desc_style, desc_text),
869
+ ]
774
870
 
775
871
  title_str = 'Select Model [dim](a=Add Custom b=BYOK Import d=Delete Custom x=Disconnect Provider q=Cancel)[/dim]'
776
872
  if is_paid_plan:
@@ -785,20 +881,23 @@ def _dialog_model_main(orchestrator, target="main"):
785
881
 
786
882
  if action == 'select':
787
883
  selected_model = models[idx]['model_id']
788
- source = models[idx].get('source', 'openrouter')
884
+ source = _normalize_source(models[idx].get('source'))
789
885
  label = '[yellow](custom)[/yellow] ' if source == 'custom' else ''
790
886
 
791
- if target == 'main':
792
- orchestrator.model_id = selected_model
887
+ if target == 'main':
888
+ orchestrator.model_id = selected_model
889
+ orchestrator.model_source = source
890
+ config.set("main_model_source", source)
793
891
  console.print(f"\n[bold #f9e2af]✓ Main Agent model set to {label}{selected_model}[/bold #f9e2af]\n")
794
892
  # Update compression threshold for new model
795
893
  try:
796
894
  orchestrator._update_model_threshold(orchestrator.model_id)
797
895
  except Exception:
798
896
  pass
799
- elif target.startswith('subagent_'):
800
- subkey = target.split('_', 1)[1]
801
- config.set(f"subagent_model_{subkey}", selected_model)
897
+ elif target.startswith('subagent_'):
898
+ subkey = target.split('_', 1)[1]
899
+ config.set(f"subagent_model_{subkey}", selected_model)
900
+ config.set(f"subagent_model_{subkey}_source", source)
802
901
  console.print(f"\n[bold #f9e2af]✓ {subkey} subagent model set to {label}{selected_model}[/bold #f9e2af]\n")
803
902
 
804
903
  elif action == 'add_custom':
@@ -1111,10 +1210,12 @@ def _dialog_add_custom_model(orchestrator):
1111
1210
  except (EOFError, KeyboardInterrupt):
1112
1211
  switch = "n"
1113
1212
 
1114
- if switch in ("", "y", "yes"):
1115
- orchestrator.model_id = model_id
1116
- try:
1117
- orchestrator._update_model_threshold(model_id)
1213
+ if switch in ("", "y", "yes"):
1214
+ orchestrator.model_id = model_id
1215
+ orchestrator.model_source = "custom"
1216
+ config.set("main_model_source", "custom")
1217
+ try:
1218
+ orchestrator._update_model_threshold(model_id)
1118
1219
  except Exception:
1119
1220
  pass
1120
1221
  add_console.print(f"\n[bold #a6e3a1]✓ Now using {model_id}[/bold #a6e3a1]\n")
@@ -1144,10 +1245,12 @@ def _dialog_delete_custom_model(orchestrator, model_id: str):
1144
1245
  if removed:
1145
1246
  del_console.print(f"\n[bold #a6e3a1]✓ Removed {model_id}[/bold #a6e3a1]\n")
1146
1247
  # If the deleted model was active, reset to default
1147
- if orchestrator.model_id == model_id:
1148
- from utim_cli.constants import DEFAULT_MODEL
1149
- orchestrator.model_id = DEFAULT_MODEL
1150
- del_console.print(f"[dim]Switched back to {DEFAULT_MODEL}[/dim]\n")
1248
+ if orchestrator.model_id == model_id and (getattr(orchestrator, "model_source", None) or config.get("main_model_source")) == "custom":
1249
+ from utim_cli.constants import DEFAULT_MODEL
1250
+ orchestrator.model_id = DEFAULT_MODEL
1251
+ orchestrator.model_source = "utim"
1252
+ config.set("main_model_source", "utim")
1253
+ del_console.print(f"[dim]Switched back to {DEFAULT_MODEL}[/dim]\n")
1151
1254
  else:
1152
1255
  del_console.print("[yellow]Model not found in custom list.[/yellow]\n")
1153
1256
  else:
@@ -1226,17 +1329,20 @@ def _dialog_disconnect_provider(orchestrator):
1226
1329
  )
1227
1330
  # If active model was deleted, reset to DEFAULT_MODEL
1228
1331
  active_model = orchestrator.model_id
1229
- active_still_exists = any(m["model_id"] == active_model for m in config.custom_models)
1230
- if not active_still_exists and active_model not in [DEFAULT_MODEL, "anthropic/claude-sonnet-4.6"]:
1332
+ active_source = getattr(orchestrator, "model_source", None) or config.get("main_model_source")
1333
+ active_still_exists = any(m["model_id"] == active_model for m in config.custom_models)
1334
+ if active_source == "custom" and not active_still_exists and active_model not in [DEFAULT_MODEL, "anthropic/claude-sonnet-4.6"]:
1231
1335
  is_custom_removed = True
1232
1336
  # Hardcoded check since we cannot import utim_cli.server in production CLI builds
1233
1337
  # (as the server module is excluded from the package in pyproject.toml)
1234
1338
  if active_model.startswith("openai/") or active_model.startswith("anthropic/") or active_model.startswith("google/") or active_model.startswith("cohere/"):
1235
1339
  is_custom_removed = False
1236
1340
 
1237
- if is_custom_removed:
1238
- orchestrator.model_id = DEFAULT_MODEL
1239
- disc_console.print(f"[dim]Active model removed. Switched back to cohere/north-mini-code:free[/dim]\n")
1341
+ if is_custom_removed:
1342
+ orchestrator.model_id = DEFAULT_MODEL
1343
+ orchestrator.model_source = "utim"
1344
+ config.set("main_model_source", "utim")
1345
+ disc_console.print(f"[dim]Active model removed. Switched back to cohere/north-mini-code:free[/dim]\n")
1240
1346
  else:
1241
1347
  disc_console.print("\n[dim]Cancelled.[/dim]\n")
1242
1348
 
@@ -883,7 +883,7 @@ def _print_animated_banner(animated: bool = True):
883
883
  else:
884
884
  console.print(f"[bold {color}]{BANNER_BIG}[/bold {color}]")
885
885
 
886
- console.print(f" [bold white]U Think I Make[/bold white] [dim]v1.46.35[/dim]")
886
+ console.print(f" [bold white]U Think I Make[/bold white] [dim]v1.46.36[/dim]")
887
887
  user_email = config.email
888
888
  user_type = "UTIM Community"
889
889
  api_key = config.get("api_key")
@@ -1783,10 +1783,7 @@ def _handle_command(cmd: str, orchestrator: Orchestrator, app_ref) -> None:
1783
1783
  # Activate immediately in this session
1784
1784
  import os as _os2
1785
1785
  _os2.environ["OPENROUTER_API_KEY"] = new_key
1786
- from openai import OpenAI as _OAI
1787
- orchestrator._local_client = _OAI(
1788
- base_url="https://openrouter.ai/api/v1", api_key=new_key
1789
- )
1786
+ orchestrator._local_client = True
1790
1787
  console.print("\n[bold #f9e2af]✓ API key saved and active.[/bold #f9e2af]\n")
1791
1788
 
1792
1789
  else:
@@ -3143,7 +3140,7 @@ def check_and_update_background():
3143
3140
  return
3144
3141
 
3145
3142
  latest_ver = resp.json().get("version", "")
3146
- current_ver = "1.46.35"
3143
+ current_ver = "1.46.36"
3147
3144
 
3148
3145
  def parse_ver(v):
3149
3146
  return [int(x) for x in v.split(".") if x.isdigit()]
@@ -3264,8 +3261,10 @@ def start_chat():
3264
3261
  orchestrator.messages = saved.get("messages", [])
3265
3262
  orchestrator.turn_history = saved.get("turn_history", [])
3266
3263
  orchestrator.redo_history = saved.get("redo_history", [])
3267
- if "model_id" in saved:
3268
- orchestrator.model_id = saved["model_id"]
3264
+ if "model_id" in saved:
3265
+ orchestrator.model_id = saved["model_id"]
3266
+ if "model_source" in saved:
3267
+ orchestrator.model_source = saved["model_source"]
3269
3268
 
3270
3269
  STATE["session_id"] = saved.get("session_id")
3271
3270
  if "queue" in saved:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 1.46.35
3
+ Version: 1.46.36
4
4
  Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://utim.dev
@@ -20,7 +20,6 @@ Classifier: Intended Audience :: Developers
20
20
  Requires-Python: >=3.9
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: openai>=1.30.0
24
23
  Requires-Dist: requests==2.31.0
25
24
  Requires-Dist: aiohttp>=3.9.5
26
25
  Requires-Dist: python-dotenv==1.0.1
@@ -30,9 +29,10 @@ Requires-Dist: chardet==4.0.0
30
29
  Requires-Dist: typer>=0.9.0
31
30
  Requires-Dist: rich>=13.0.0
32
31
  Requires-Dist: prompt_toolkit>=3.0.0
33
- Requires-Dist: mcp>=0.1.0
34
32
  Requires-Dist: nest-asyncio>=1.5.0
35
33
  Requires-Dist: sqlalchemy>=2.0.0
34
+ Requires-Dist: openai>=1.30.0; platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")
35
+ Requires-Dist: mcp>=0.1.0; platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")
36
36
  Provides-Extra: search
37
37
  Requires-Dist: scrapy>=2.11.0; extra == "search"
38
38
  Requires-Dist: scrapy-playwright>=0.0.34; extra == "search"
@@ -44,6 +44,8 @@ Requires-Dist: tree-sitter>=0.21.0; extra == "parsers"
44
44
  Requires-Dist: tree-sitter-python>=0.21.0; extra == "parsers"
45
45
  Requires-Dist: tree-sitter-javascript>=0.21.0; extra == "parsers"
46
46
  Requires-Dist: tree-sitter-typescript>=0.21.0; extra == "parsers"
47
+ Provides-Extra: mcp
48
+ Requires-Dist: mcp>=0.1.0; extra == "mcp"
47
49
  Provides-Extra: full
48
50
  Requires-Dist: scrapy>=2.11.0; extra == "full"
49
51
  Requires-Dist: scrapy-playwright>=0.0.34; extra == "full"
@@ -53,6 +55,10 @@ Requires-Dist: tree-sitter>=0.21.0; extra == "full"
53
55
  Requires-Dist: tree-sitter-python>=0.21.0; extra == "full"
54
56
  Requires-Dist: tree-sitter-javascript>=0.21.0; extra == "full"
55
57
  Requires-Dist: tree-sitter-typescript>=0.21.0; extra == "full"
58
+ Requires-Dist: mcp>=0.1.0; (platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")) and extra == "full"
59
+ Requires-Dist: openai>=1.30.0; (platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")) and extra == "full"
60
+ Provides-Extra: legacy-agent
61
+ Requires-Dist: openai>=1.30.0; extra == "legacy-agent"
56
62
 
57
63
  # UTIM CLI Agent: Enterprise Coder Assistant
58
64
 
@@ -194,6 +200,12 @@ pip install utim-cli
194
200
  pip install "utim-cli[full]"
195
201
  ```
196
202
 
203
+ Desktop installs include the normal SDK/MCP path automatically. Mobile/Termux
204
+ installs keep those native-heavy pieces optional to avoid `pydantic-core`; use
205
+ `utim-cli[mcp]` or `utim-cli[legacy-agent]` only if you explicitly need them.
206
+ Do not install `utim_cli/server/requirements.txt` unless you are deploying the
207
+ server.
208
+
197
209
  ### Source install (development)
198
210
  ```bash
199
211
  git clone https://github.com/emendai/utim.git
@@ -1,4 +1,3 @@
1
- openai>=1.30.0
2
1
  requests==2.31.0
3
2
  aiohttp>=3.9.5
4
3
  python-dotenv==1.0.1
@@ -8,10 +7,13 @@ chardet==4.0.0
8
7
  typer>=0.9.0
9
8
  rich>=13.0.0
10
9
  prompt_toolkit>=3.0.0
11
- mcp>=0.1.0
12
10
  nest-asyncio>=1.5.0
13
11
  sqlalchemy>=2.0.0
14
12
 
13
+ [:platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")]
14
+ openai>=1.30.0
15
+ mcp>=0.1.0
16
+
15
17
  [full]
16
18
  scrapy>=2.11.0
17
19
  scrapy-playwright>=0.0.34
@@ -22,9 +24,19 @@ tree-sitter-python>=0.21.0
22
24
  tree-sitter-javascript>=0.21.0
23
25
  tree-sitter-typescript>=0.21.0
24
26
 
27
+ [full:platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64")]
28
+ mcp>=0.1.0
29
+ openai>=1.30.0
30
+
25
31
  [images]
26
32
  pillow>=9.0.0
27
33
 
34
+ [legacy-agent]
35
+ openai>=1.30.0
36
+
37
+ [mcp]
38
+ mcp>=0.1.0
39
+
28
40
  [parsers]
29
41
  tree-sitter>=0.21.0
30
42
  tree-sitter-python>=0.21.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes