utim-cli 1.46.28__tar.gz → 1.46.30__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.28 → utim_cli-1.46.30}/CHANGELOG.md +18 -0
  2. {utim_cli-1.46.28/utim_cli.egg-info → utim_cli-1.46.30}/PKG-INFO +40 -3
  3. {utim_cli-1.46.28 → utim_cli-1.46.30}/README.md +38 -1
  4. {utim_cli-1.46.28 → utim_cli-1.46.30}/pyproject.toml +10 -3
  5. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/__init__.py +7 -1
  6. utim_cli-1.46.30/utim_cli/_version.py +14 -0
  7. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/agent.py +3 -1
  8. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/client_utils.py +2 -3
  9. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/config.py +23 -0
  10. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/constants.py +6 -0
  11. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/doctor.py +8 -3
  12. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/mcp_client.py +6 -2
  13. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/orchestrator.py +7 -2
  14. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/router.py +8 -2
  15. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/share.py +2 -2
  16. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/share_tui.py +8 -7
  17. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/feedback_dialog.py +10 -6
  18. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/model_dialog.py +51 -31
  19. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/tools_dialog.py +3 -1
  20. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/update_dialog.py +4 -4
  21. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/utim.py +46 -23
  22. {utim_cli-1.46.28 → utim_cli-1.46.30/utim_cli.egg-info}/PKG-INFO +40 -3
  23. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli.egg-info/SOURCES.txt +1 -0
  24. {utim_cli-1.46.28 → utim_cli-1.46.30}/LICENSE +0 -0
  25. {utim_cli-1.46.28 → utim_cli-1.46.30}/MANIFEST.in +0 -0
  26. {utim_cli-1.46.28 → utim_cli-1.46.30}/setup.cfg +0 -0
  27. {utim_cli-1.46.28 → utim_cli-1.46.30}/setup.py +0 -0
  28. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/auth.py +0 -0
  29. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/backup.py +0 -0
  30. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/blender_agent.py +0 -0
  31. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/bootstrap.py +0 -0
  32. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/context_pruner.py +0 -0
  33. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/knowledge_graph.py +0 -0
  34. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/local_db.py +0 -0
  35. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/logger.py +0 -0
  36. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/mcp_clean_wrapper.py +0 -0
  37. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/mcp_registry.json +0 -0
  38. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/reflection.py +0 -0
  39. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/report.py +0 -0
  40. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/scrapy_search.py +0 -0
  41. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/__init__.py +0 -0
  42. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/auth.py +0 -0
  43. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/db.py +0 -0
  44. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/email_utils.py +0 -0
  45. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/exchange_rate.py +0 -0
  46. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/firebase.py +0 -0
  47. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/history.py +0 -0
  48. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/logging_config.py +0 -0
  49. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/models.py +0 -0
  50. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/pricing_updater.py +0 -0
  51. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/rate_limit.py +0 -0
  52. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/__init__.py +0 -0
  53. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/auth_routes.py +0 -0
  54. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/completion_routes.py +0 -0
  55. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/credit_routes.py +0 -0
  56. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/feedback_routes.py +0 -0
  57. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/quota_routes.py +0 -0
  58. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/session_routes.py +0 -0
  59. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/share_routes.py +0 -0
  60. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/server.py +0 -0
  61. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/situational_scoring.py +0 -0
  62. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/state.py +0 -0
  63. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tools.py +0 -0
  64. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/__init__.py +0 -0
  65. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/history_dialog.py +0 -0
  66. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/mcp_dialog.py +0 -0
  67. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/resume_dialog.py +0 -0
  68. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/utilities.py +0 -0
  69. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/vector_memory.py +0 -0
  70. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/workspace.py +0 -0
  71. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli.egg-info/dependency_links.txt +0 -0
  72. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli.egg-info/entry_points.txt +0 -0
  73. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli.egg-info/requires.txt +0 -0
  74. {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli.egg-info/top_level.txt +0 -0
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to the UTIM CLI Agent will be documented in this file.
4
4
 
5
+ ## [1.46.30] - 2026-07-11
6
+
7
+ ### Added
8
+ - **Dynamic Console Glyph Fallbacks**: Detected legacy Windows terminals (where `WT_SESSION` is not set) and automatically fell back to standard ASCII symbols (`>` and `->`) instead of unicode characters (`❯`, `▶`, and `➔`) to prevent `⍰` or empty box rendering issues.
9
+ - **SSL Verification Control**: Added `verify_ssl` config key and `UTIM_VERIFY_SSL` environment variable to support disabling SSL verification. This resolves "server unreachable" errors caused by corporate firewalls or proxies that intercept HTTPS traffic.
10
+
11
+ ### Fixed
12
+ - **Model Selection Dialog Crash**: Fixed an `AttributeError` crash on the model list dialog screen (main agent or subagent picker) when the user's local `user_plan` config value is missing or explicitly `null`.
13
+ - **Global Config Null-Safety Improvements**: Fixed multiple potential crashes across the tools dialog, share manager, and usage dashboard when keys like `disabled_tools`, `mcpServers`, `role`, or `user_plan` are missing or set to `null` in config files.
14
+ - **Server Upload Limit & Connection Resets**: Increased the `/shares/upload` workspace file sharing size limit from 4MB to 25MB on the server. Added stream consumption middleware handlers to prevent connection termination resets and `SSLEOFError` crashes on the client during larger uploads.
15
+
16
+ ## [1.46.29] - 2026-07-11
17
+
18
+ ### Fixed
19
+ - **Feedbacks Dialog Markup Crash**: Wrapped all dynamic user-provided strings (emails, comments, and chat history) inside `rich.markup.escape` in `feedback_dialog.py` to prevent `MarkupError` crashes when logs contain unescaped brackets or console style tags.
20
+ - **Event Loop run_in_terminal Coroutine Warnings**: Wrapped the main-thread `run_in_terminal` callback in `asyncio.ensure_future` inside `utim.py` to prevent task destruction and `never awaited` coroutine warnings.
21
+ - **Free Node Pricing Description**: Updated the Free tier pricing widget and documentation pages to reflect the correct 5-hour refill limit (100 credits/$0.10 value) and 3,000 monthly credits limit (with no stacking).
22
+
5
23
  ## [1.46.28] - 2026-07-11
6
24
 
7
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 1.46.28
3
+ Version: 1.46.30
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
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Operating System :: OS Independent
17
- Classifier: Development Status :: 4 - Beta
17
+ Classifier: Development Status :: 5 - Production/Stable
18
18
  Classifier: Environment :: Console
19
19
  Classifier: Intended Audience :: Developers
20
20
  Requires-Python: >=3.9
@@ -175,6 +175,43 @@ Inside the interactive chat terminal, type these slash commands for direct works
175
175
  ## 📄 Related Documentation
176
176
 
177
177
  - [📖 Full Documentation](https://utim.dev/docs)
178
- - [🔒 Security Policy](SECURITY.md)
178
+ - [🔒 Security Policy](SECURITY.md) — vulnerability reporting, security@utim.dev
179
+ - [🗄️ Data Handling Reference](DATA_HANDLING.md) — what stays local, what goes to backend, what goes to model providers
179
180
  - [📊 SLA & Support](SLA.md)
180
181
  - [🛡️ Privacy Policy](landing/src/docs_md/privacy.md)
182
+
183
+ ## 📦 Installation Methods
184
+
185
+ ### npm (recommended — works without Python pre-installed)
186
+ ```bash
187
+ npm install -g @emend-ai/utim
188
+ ```
189
+
190
+ ### pip (from PyPI)
191
+ ```bash
192
+ pip install utim-cli
193
+ # Full install with optional features (web search, images, parsers)
194
+ pip install "utim-cli[full]"
195
+ ```
196
+
197
+ ### Source install (development)
198
+ ```bash
199
+ git clone https://github.com/emendai/utim.git
200
+ cd utim
201
+ pip install -e ".[full]"
202
+ ```
203
+
204
+ ### Smoke-test your install
205
+ ```bash
206
+ utim --version # Should print the current version
207
+ utim doctor # Runs diagnostics — all checks should pass
208
+ ```
209
+
210
+ See [INSTALLATION_VERIFICATION.md](INSTALLATION_VERIFICATION.md) for detailed verification steps and troubleshooting.
211
+
212
+ ## 🔢 Version
213
+
214
+ UTIM uses a single canonical version defined in [`utim_cli/_version.py`](utim_cli/_version.py).
215
+ Run `python scripts/sync_version.py` to propagate a version bump to all surfaces
216
+ (`package.json`, `pyproject.toml`, `CHANGELOG.md`) before releasing.
217
+
@@ -119,6 +119,43 @@ Inside the interactive chat terminal, type these slash commands for direct works
119
119
  ## 📄 Related Documentation
120
120
 
121
121
  - [📖 Full Documentation](https://utim.dev/docs)
122
- - [🔒 Security Policy](SECURITY.md)
122
+ - [🔒 Security Policy](SECURITY.md) — vulnerability reporting, security@utim.dev
123
+ - [🗄️ Data Handling Reference](DATA_HANDLING.md) — what stays local, what goes to backend, what goes to model providers
123
124
  - [📊 SLA & Support](SLA.md)
124
125
  - [🛡️ Privacy Policy](landing/src/docs_md/privacy.md)
126
+
127
+ ## 📦 Installation Methods
128
+
129
+ ### npm (recommended — works without Python pre-installed)
130
+ ```bash
131
+ npm install -g @emend-ai/utim
132
+ ```
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]"
146
+ ```
147
+
148
+ ### Smoke-test your install
149
+ ```bash
150
+ utim --version # Should print the current version
151
+ utim doctor # Runs diagnostics — all checks should pass
152
+ ```
153
+
154
+ See [INSTALLATION_VERIFICATION.md](INSTALLATION_VERIFICATION.md) for detailed verification steps and troubleshooting.
155
+
156
+ ## 🔢 Version
157
+
158
+ UTIM uses a single canonical version defined in [`utim_cli/_version.py`](utim_cli/_version.py).
159
+ Run `python scripts/sync_version.py` to propagate a version bump to all surfaces
160
+ (`package.json`, `pyproject.toml`, `CHANGELOG.md`) before releasing.
161
+
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "1.46.28"
7
+ version = "1.46.30"
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"
@@ -17,7 +17,7 @@ classifiers = [
17
17
  "Programming Language :: Python :: 3.12",
18
18
  "License :: OSI Approved :: MIT License",
19
19
  "Operating System :: OS Independent",
20
- "Development Status :: 4 - Beta",
20
+ "Development Status :: 5 - Production/Stable",
21
21
  "Environment :: Console",
22
22
  "Intended Audience :: Developers",
23
23
  ]
@@ -78,4 +78,11 @@ include = ["utim_cli*"]
78
78
  exclude = ["tests*", "landing*", "scripts*"]
79
79
 
80
80
  [tool.setuptools.package-data]
81
- utim_cli = ["mcp_registry.json"]
81
+ utim_cli = ["mcp_registry.json", "_version.py"]
82
+
83
+ [tool.pytest.ini_options]
84
+ markers = [
85
+ "smoke: fast install validation tests (no live credentials required)",
86
+ "slow: tests that make network calls or take more than 10 seconds",
87
+ "integration: tests requiring live credentials or a running UTIM account",
88
+ ]
@@ -1,5 +1,11 @@
1
1
  # UTIM CLI Package
2
- __version__ = "1.46.28"
2
+ # Version is maintained in utim_cli/_version.py (single source of truth).
3
+ # scripts/sync_version.py keeps package.json, pyproject.toml, and CHANGELOG in sync.
4
+ try:
5
+ from utim_cli._version import VERSION as __version__
6
+ except ImportError:
7
+ # Fallback during first-install / editable-install edge cases
8
+ __version__ = "1.46.30"
3
9
 
4
10
  import builtins, os, pathlib, urllib.parse, re
5
11
 
@@ -0,0 +1,14 @@
1
+ # Single source of truth for the UTIM version.
2
+ #
3
+ # BUMP PROCESS
4
+ # ─────────────────────────────────────────────────────────────────────────────
5
+ # 1. Edit VERSION below.
6
+ # 2. Run `python scripts/sync_version.py` — it writes the same string into
7
+ # package.json, pyproject.toml, and the CHANGELOG header for you.
8
+ # 3. Commit as "chore: bump version to X.Y.Z".
9
+ #
10
+ # DO NOT edit __version__ in __init__.py directly — that file now imports
11
+ # from here so every surface stays in sync automatically.
12
+ # ─────────────────────────────────────────────────────────────────────────────
13
+
14
+ VERSION = "1.46.30"
@@ -346,7 +346,9 @@ class ReActAgent:
346
346
 
347
347
  # Load disabled tools config
348
348
  from utim_cli.config import config
349
- disabled = config.get("disabled_tools", [])
349
+ disabled = config.get("disabled_tools") or []
350
+ if not isinstance(disabled, list):
351
+ disabled = []
350
352
 
351
353
  col_widths = [max(len(t[0]) for t in all_tools) + 2, 60]
352
354
 
@@ -63,7 +63,7 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
63
63
  }
64
64
 
65
65
  # UTIM server completions is a streaming endpoint
66
- resp = requests.post(f"{server_url}/completions", json=payload, headers=headers, stream=True, timeout=timeout)
66
+ resp = requests.post(f"{server_url}/completions", json=payload, headers=headers, stream=True, timeout=timeout, verify=config.verify_ssl)
67
67
  resp.raise_for_status()
68
68
 
69
69
  if stream or json_data.get("stream"):
@@ -151,8 +151,7 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
151
151
  "Authorization": f"Bearer {llm_key}",
152
152
  "Content-Type": "application/json"
153
153
  }
154
- # Direct forwarding to OpenRouter
155
- return requests.post("https://openrouter.ai/api/v1/chat/completions", json=json_data, headers=headers, stream=stream or json_data.get("stream"), timeout=timeout)
154
+ return requests.post("https://openrouter.ai/api/v1/chat/completions", json=json_data, headers=headers, stream=stream or json_data.get("stream"), timeout=timeout, verify=config.verify_ssl)
156
155
 
157
156
 
158
157
  def parse_xml_tool_calls(content: str) -> tuple[str | None, list[dict]]:
@@ -22,6 +22,22 @@ class Config:
22
22
  self._global_data: Dict[str, Any] = {}
23
23
  self._local_data: Dict[str, Any] = {}
24
24
  self._load()
25
+ # Suppress warnings and patch requests if SSL verification is disabled
26
+ if not self.verify_ssl:
27
+ try:
28
+ import urllib3
29
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
30
+ except Exception:
31
+ pass
32
+ try:
33
+ import requests
34
+ original_request = requests.Session.request
35
+ def new_request(self, method, url, *args, **kwargs):
36
+ kwargs['verify'] = False
37
+ return original_request(self, method, url, *args, **kwargs)
38
+ requests.Session.request = new_request
39
+ except Exception:
40
+ pass
25
41
 
26
42
  def _load(self):
27
43
  # Load global config
@@ -243,6 +259,13 @@ class Config:
243
259
  # Enable adaptive context compression
244
260
  return os.environ.get("UTIM_COMPRESSION", "true").lower() in ("1", "true", "yes")
245
261
 
262
+ @property
263
+ def verify_ssl(self) -> bool:
264
+ env_val = os.environ.get("UTIM_VERIFY_SSL")
265
+ if env_val is not None:
266
+ return env_val.lower() not in ("false", "0", "no")
267
+ return self.get("verify_ssl", True)
268
+
246
269
  @property
247
270
  def blender_path(self) -> str:
248
271
  """Return the absolute path to the Blender executable.
@@ -11,3 +11,9 @@ SUBAGENT_DEFAULTS = {
11
11
  "generate_image": "nvidia/nemotron-3-nano-30b-a3b:free",
12
12
  "blender_create_object": DEFAULT_MODEL,
13
13
  }
14
+
15
+ import sys
16
+ _IS_LEGACY_WIN = (sys.platform == "win32" and "WT_SESSION" not in os.environ)
17
+ PROMPT_SYMBOL = ">" if _IS_LEGACY_WIN else "❯"
18
+ ARROW_SYMBOL = "->" if _IS_LEGACY_WIN else "➔"
19
+
@@ -62,8 +62,13 @@ def run_diagnostics(console: Console = None):
62
62
 
63
63
  # 4. Model & Connectivity check
64
64
  console.print("\n [bold #42bcf5]Connectivity & API Health:[/bold #42bcf5]")
65
-
66
65
  # 4a. Check UTIM Server Connection
66
+ import ssl
67
+ from utim_cli.config import config
68
+ ssl_context = None
69
+ if not config.verify_ssl:
70
+ ssl_context = ssl._create_unverified_context()
71
+
67
72
  from utim_cli.auth import SERVER_URL
68
73
  console.print(f" * Server Target URL: {SERVER_URL}")
69
74
  try:
@@ -71,7 +76,7 @@ def run_diagnostics(console: Console = None):
71
76
  f"{SERVER_URL}/plans",
72
77
  headers={'User-Agent': 'Mozilla/5.0'}
73
78
  )
74
- with urllib.request.urlopen(req, timeout=5) as r:
79
+ with urllib.request.urlopen(req, timeout=5, context=ssl_context) as r:
75
80
  status = r.getcode()
76
81
  if status == 200:
77
82
  console.print(" [+] UTIM Server API: [green]Healthy & Reachable (HTTP 200)[/green]")
@@ -86,7 +91,7 @@ def run_diagnostics(console: Console = None):
86
91
  "https://openrouter.ai",
87
92
  headers={'User-Agent': 'Mozilla/5.0'}
88
93
  )
89
- urllib.request.urlopen(req, timeout=5)
94
+ urllib.request.urlopen(req, timeout=5, context=ssl_context)
90
95
  console.print(" [+] OpenRouter connection: [green]Successful[/green]")
91
96
  except Exception as e:
92
97
  console.print(f" [-] OpenRouter connection: [red]Failed[/red] ({e})")
@@ -74,7 +74,9 @@ class MCPManager:
74
74
  self._started = True
75
75
  return
76
76
 
77
- servers = config.get("mcpServers", {})
77
+ servers = config.get("mcpServers") or {}
78
+ if not isinstance(servers, dict):
79
+ servers = {}
78
80
 
79
81
  for name, srv_config in servers.items():
80
82
  command = srv_config.get("command")
@@ -155,7 +157,9 @@ class MCPManager:
155
157
  def get_notification_context(self) -> str:
156
158
  """Returns the context notification string for connected MCP servers."""
157
159
  from utim_cli.config import config
158
- disabled = config.get("disabled_tools", [])
160
+ disabled = config.get("disabled_tools") or []
161
+ if not isinstance(disabled, list):
162
+ disabled = []
159
163
  notifications = []
160
164
  for server_name, tool_names in self.server_tools.items():
161
165
  # Only include tools that are NOT in the disabled tools list
@@ -262,7 +262,9 @@ def _get_active_task(user_prompt: str) -> str:
262
262
 
263
263
  def _get_dynamic_directives(is_compressed: bool) -> str:
264
264
  from utim_cli.config import config
265
- disabled = config.get("disabled_tools", [])
265
+ disabled = config.get("disabled_tools") or []
266
+ if not isinstance(disabled, list):
267
+ disabled = []
266
268
 
267
269
  has_query_codebase = "query_codebase" not in disabled
268
270
  has_plan_project = "plan_project" not in disabled
@@ -1106,7 +1108,9 @@ class Orchestrator:
1106
1108
  else:
1107
1109
  all_tools = utim_tools + mcp_tools
1108
1110
  # Filter disabled tools only for the main agent
1109
- disabled = config.get("disabled_tools", [])
1111
+ disabled = config.get("disabled_tools") or []
1112
+ if not isinstance(disabled, list):
1113
+ disabled = []
1110
1114
  all_tools = [t for t in all_tools if t["function"]["name"] not in disabled]
1111
1115
 
1112
1116
  payload = {
@@ -1170,6 +1174,7 @@ class Orchestrator:
1170
1174
  headers=_headers,
1171
1175
  stream=True,
1172
1176
  timeout=(10, 90), # 10s connect, 90s read — resp.close() handles abort mid-stream
1177
+ verify=config.verify_ssl,
1173
1178
  ) as resp:
1174
1179
 
1175
1180
  resp.raise_for_status()
@@ -89,8 +89,14 @@ from fastapi.responses import JSONResponse
89
89
  class LimitUploadSize(BaseHTTPMiddleware):
90
90
  async def dispatch(self, request, call_next):
91
91
  if request.headers.get("content-length"):
92
- if int(request.headers["content-length"]) > 4_000_000: # 4MB limit
93
- return JSONResponse({"detail": "Request too large"}, status_code=413)
92
+ if int(request.headers["content-length"]) > 25_000_000: # 25MB limit
93
+ # Consume stream to prevent abrupt connection closure / SSLEOFError on client
94
+ try:
95
+ async for _ in request.stream():
96
+ pass
97
+ except Exception:
98
+ pass
99
+ return JSONResponse({"detail": "Request too large (max 25MB)"}, status_code=413)
94
100
  return await call_next(request)
95
101
 
96
102
  app.add_middleware(LimitUploadSize)
@@ -259,8 +259,8 @@ class ShareManager:
259
259
 
260
260
  has_messages = False
261
261
  for msg in messages:
262
- role = msg.get("role", "").lower()
263
- content = msg.get("content", "")
262
+ role = str(msg.get("role") or "").lower()
263
+ content = msg.get("content") or ""
264
264
 
265
265
  # Only include actual User prompts and Assistant responses
266
266
  if role not in ("user", "assistant"):
@@ -14,6 +14,7 @@ from rich.panel import Panel
14
14
  from rich.text import Text
15
15
 
16
16
  from utim_cli.share import ShareManager, EXCLUDE_OPTIONS, EXPIRY_OPTIONS, ShareRecord
17
+ from utim_cli.constants import ARROW_SYMBOL
17
18
 
18
19
  import sys
19
20
  theme_console = Console(file=sys.__stdout__)
@@ -129,7 +130,7 @@ def _run_share_type_dialog():
129
130
  for i, opt in enumerate(options):
130
131
  highlighted = i == sel[0]
131
132
  bg = 'bg:#1a3a2a bold #cdd6f4' if highlighted else ''
132
- bullet = ' ' if highlighted else '• '
133
+ bullet = f'{ARROW_SYMBOL} ' if highlighted else '• '
133
134
  bullet_style = 'bold #42bcf5' if highlighted else 'class:dim'
134
135
 
135
136
  out.extend([
@@ -192,7 +193,7 @@ def _run_expiry_dialog(options, title="", legend=""):
192
193
  for i, opt in enumerate(options):
193
194
  highlighted = i == sel[0]
194
195
  bg = 'bg:#1a3a2a bold #cdd6f4' if highlighted else ''
195
- bullet = ' ' if highlighted else '• '
196
+ bullet = f'{ARROW_SYMBOL} ' if highlighted else '• '
196
197
  bullet_style = 'bold #42bcf5' if highlighted else 'class:dim'
197
198
 
198
199
  out.extend([
@@ -267,10 +268,10 @@ def _run_shares_dashboard(manager: ShareManager):
267
268
  filtered_rows.append((idx, row))
268
269
  else:
269
270
  rec = row["record"]
270
- if (q in rec.name.lower() or
271
- q in rec.id.lower() or
272
- q in rec.link.lower() or
273
- any(q in excl.lower() for excl in rec.excluded)):
271
+ if (q in str(rec.name or '').lower() or
272
+ q in str(rec.id or '').lower() or
273
+ q in str(rec.link or '').lower() or
274
+ any(q in str(excl or '').lower() for excl in rec.excluded)):
274
275
  filtered_rows.append((idx, row))
275
276
  N_filtered[0] = len(filtered_rows)
276
277
  if N_filtered[0] > 0:
@@ -360,7 +361,7 @@ def _run_shares_dashboard(manager: ShareManager):
360
361
  excl_part = f"Excluded: {', '.join(rec.excluded) if rec.excluded else 'none'}"
361
362
 
362
363
  if highlighted:
363
- action_bar = " [ Details (Enter) ] [ Delete (D) ]"
364
+ action_bar = f" {ARROW_SYMBOL} [ Details (Enter) ] [ Delete (D) ]"
364
365
  action_style = "bold #89b4fa bg:#1a3a2a"
365
366
  else:
366
367
  action_bar = " [ Details ] [ Delete ]"
@@ -3,6 +3,7 @@ import datetime
3
3
  import json
4
4
  from utim_cli.config import config
5
5
  from utim_cli.auth import SERVER_URL
6
+ from utim_cli.constants import ARROW_SYMBOL
6
7
 
7
8
 
8
9
  # ─── Consent dialog ───────────────────────────────────────────────────────────
@@ -272,6 +273,7 @@ def _dialog_submit_feedback_conditional(orchestrator=None):
272
273
  json={"rating": rating, "comment": comment, "chat_history": chat_history},
273
274
  headers={"X-API-Key": api_key},
274
275
  timeout=10,
276
+ verify=config.verify_ssl,
275
277
  )
276
278
  if resp.status_code == 200:
277
279
  console.print(
@@ -309,6 +311,7 @@ def _dialog_feedbacks(orchestrator=None):
309
311
  f"{SERVER_URL}/feedback/list",
310
312
  headers={"X-API-Key": api_key},
311
313
  timeout=15,
314
+ verify=config.verify_ssl,
312
315
  )
313
316
  if resp.status_code == 403:
314
317
  console.print(
@@ -374,11 +377,12 @@ def _dialog_feedbacks(orchestrator=None):
374
377
  selected_fb = feedbacks[int(rows[idx]["key"])]
375
378
 
376
379
  def print_chat_history(con, fb=selected_fb):
380
+ from rich.markup import escape
377
381
  con.print(f"\n [bold #cba6f7]Feedback details:[/bold #cba6f7]")
378
- con.print(f" [bold]User:[/bold] {fb['user_email']}")
382
+ con.print(f" [bold]User:[/bold] {escape(str(fb['user_email']))}")
379
383
  con.print(f" [bold]Date:[/bold] {fb['created_at'].replace('T', ' ').split('.')[0]}")
380
384
  con.print(f" [bold]Rating:[/bold] {'⭐' * fb['rating']}")
381
- con.print(f" [bold]Comment:[/bold] {fb['comment'] or '(None)'}\n")
385
+ con.print(f" [bold]Comment:[/bold] {escape(str(fb['comment'] or '(None)'))}\n")
382
386
  con.print(Rule(style="dim"))
383
387
  con.print(f"\n [bold #89b4fa]Chat History:[/bold #89b4fa]\n")
384
388
 
@@ -398,20 +402,20 @@ def _dialog_feedbacks(orchestrator=None):
398
402
  if not isinstance(msg, dict):
399
403
  continue
400
404
  role = msg.get("role", "unknown") or "unknown"
401
- content = msg.get("content", "")
405
+ content = msg.get("content") or ""
402
406
  if role == "user":
403
407
  con.print(f" [bold #42bcf5]👤 User:[/bold #42bcf5]")
404
- con.print(f" {content}\n")
408
+ con.print(f" {escape(str(content))}\n")
405
409
  elif role == "assistant":
406
410
  con.print(f" [bold #a6e3a1]🤖 Assistant:[/bold #a6e3a1]")
407
411
  if isinstance(content, list):
408
412
  content = "\n".join(
409
413
  p.get("text", "") for p in content if isinstance(p, dict)
410
414
  )
411
- con.print(f" {content}\n")
415
+ con.print(f" {escape(str(content))}\n")
412
416
  else:
413
417
  con.print(f" [dim]⚙ {role.title()}:[/dim]")
414
- con.print(f" {content}\n")
418
+ con.print(f" {escape(str(content))}\n")
415
419
 
416
420
  from utim_cli.utim import _run_captured_dialog
417
421
  _run_captured_dialog(
@@ -4,6 +4,42 @@ import httpx
4
4
  import re
5
5
  from utim_cli.config import config
6
6
 
7
+ def _safe_prompt(prompt_text: str, color: str = None, is_password: bool = False) -> str:
8
+ """Bulletproof replacement for prompt_toolkit's prompt() when nested inside run_in_terminal context.
9
+ Prevents asyncio / nest_asyncio event loop crashes on Python 3.13.
10
+ """
11
+ import sys
12
+ import getpass
13
+
14
+ # Render with ANSI color if specified
15
+ if color:
16
+ color_clean = color.lstrip('#')
17
+ if len(color_clean) == 6:
18
+ r = int(color_clean[0:2], 16)
19
+ g = int(color_clean[2:4], 16)
20
+ b = int(color_clean[4:6], 16)
21
+ ansi_prefix = f"\x1b[38;2;{r};{g};{b}m"
22
+ ansi_suffix = "\x1b[0m"
23
+ else:
24
+ ansi_prefix = ""
25
+ ansi_suffix = ""
26
+ styled_prompt = f"{ansi_prefix}{prompt_text}{ansi_suffix}"
27
+ else:
28
+ styled_prompt = prompt_text
29
+
30
+ sys.stdout.write(styled_prompt)
31
+ sys.stdout.flush()
32
+
33
+ try:
34
+ if is_password:
35
+ val = getpass.getpass("")
36
+ else:
37
+ val = input()
38
+ return val
39
+ except (KeyboardInterrupt, EOFError):
40
+ raise KeyboardInterrupt
41
+
42
+
7
43
  def _dialog_model(orchestrator):
8
44
  """Interactive settings to choose between Main Agent and Sub-Agents configuration."""
9
45
  from utim_cli.config import config
@@ -119,7 +155,7 @@ def _dialog_model_main(orchestrator, target="main"):
119
155
  from utim_cli.utim import DEFAULT_MODEL
120
156
  from utim_cli.utim import _run_list_dialog, console, custom_theme
121
157
  # ── Define allowed primary and helper models based on target ──────────────
122
- plan_name = config.get("user_plan", "free").lower()
158
+ plan_name = str(config.get("user_plan") or "free").lower()
123
159
  is_paid_plan = (plan_name != "free")
124
160
 
125
161
  # Check if they have active bonus credits live (to unlock premium models)
@@ -780,8 +816,6 @@ def _dialog_byok_import(orchestrator):
780
816
  import sys
781
817
  import time
782
818
  import requests
783
- from prompt_toolkit import prompt as ptk_prompt
784
- from prompt_toolkit.styles import Style as PTStyle
785
819
  from rich.console import Console as RichConsole
786
820
  from utim_cli.utim import custom_theme
787
821
 
@@ -815,10 +849,8 @@ def _dialog_byok_import(orchestrator):
815
849
  url_hint = f"[dim]{url}[/dim]" if url else "[dim]enter manually[/dim]"
816
850
  byok_console.print(f" [bold #cba6f7]{k}[/bold #cba6f7] {name} {url_hint}")
817
851
 
818
- pt_style = PTStyle.from_dict({'': '#cba6f7'})
819
-
820
852
  try:
821
- choice = ptk_prompt("\n Provider number [9]: ", style=pt_style).strip() or "9"
853
+ choice = _safe_prompt("\n Provider number [9]: ", color="#cba6f7").strip() or "9"
822
854
  except (EOFError, KeyboardInterrupt):
823
855
  byok_console.print("\n[dim]Cancelled.[/dim]\n")
824
856
  return
@@ -834,7 +866,7 @@ def _dialog_byok_import(orchestrator):
834
866
  if not base_url_preset:
835
867
  url_placeholder = "https://..."
836
868
  try:
837
- base_url = ptk_prompt(f" Base URL [{url_placeholder}]: ", style=pt_style).strip()
869
+ base_url = _safe_prompt(f" Base URL [{url_placeholder}]: ", color="#cba6f7").strip()
838
870
  except (EOFError, KeyboardInterrupt):
839
871
  byok_console.print("\n[dim]Cancelled.[/dim]\n")
840
872
  return
@@ -845,7 +877,7 @@ def _dialog_byok_import(orchestrator):
845
877
 
846
878
  # Display name override
847
879
  try:
848
- pname_input = ptk_prompt(f" Provider display name [{provider_name}]: ", style=pt_style).strip()
880
+ pname_input = _safe_prompt(f" Provider display name [{provider_name}]: ", color="#cba6f7").strip()
849
881
  if pname_input:
850
882
  provider_name = pname_input
851
883
  except (EOFError, KeyboardInterrupt):
@@ -860,7 +892,7 @@ def _dialog_byok_import(orchestrator):
860
892
  "(stored securely in config.json — input hidden):[/dim]"
861
893
  )
862
894
  try:
863
- api_key = ptk_prompt(" API Key (Enter to skip): ", is_password=True, style=pt_style).strip()
895
+ api_key = _safe_prompt(" API Key (Enter to skip): ", color="#cba6f7", is_password=True).strip()
864
896
  except (EOFError, KeyboardInterrupt):
865
897
  byok_console.print("\n[dim]Cancelled.[/dim]\n")
866
898
  return
@@ -936,8 +968,6 @@ def _dialog_add_custom_model(orchestrator):
936
968
  """Interactive wizard to add a model from any OpenAI-compatible provider."""
937
969
  import sys
938
970
  import time
939
- from prompt_toolkit import prompt as ptk_prompt
940
- from prompt_toolkit.styles import Style as PTStyle
941
971
  from rich.console import Console as RichConsole
942
972
 
943
973
 
@@ -969,10 +999,8 @@ def _dialog_add_custom_model(orchestrator):
969
999
  url_hint = f"[dim]{url}[/dim]" if url else "[dim]enter manually[/dim]"
970
1000
  add_console.print(f" [bold #42bcf5]{k}[/bold #42bcf5] {name} {url_hint}")
971
1001
 
972
- pt_style = PTStyle.from_dict({'': '#42bcf5'})
973
-
974
1002
  try:
975
- choice = ptk_prompt("\n Provider number [8]: ", style=pt_style).strip() or "8"
1003
+ choice = _safe_prompt("\n Provider number [8]: ", color="#42bcf5").strip() or "8"
976
1004
  except (EOFError, KeyboardInterrupt):
977
1005
  add_console.print("\n[dim]Cancelled.[/dim]\n")
978
1006
  return
@@ -988,7 +1016,7 @@ def _dialog_add_custom_model(orchestrator):
988
1016
  if not base_url_preset:
989
1017
  url_placeholder = "https://..."
990
1018
  try:
991
- base_url = ptk_prompt(f" Base URL [{url_placeholder}]: ", style=pt_style).strip()
1019
+ base_url = _safe_prompt(f" Base URL [{url_placeholder}]: ", color="#42bcf5").strip()
992
1020
  except (EOFError, KeyboardInterrupt):
993
1021
  add_console.print("\n[dim]Cancelled.[/dim]\n")
994
1022
  return
@@ -999,7 +1027,7 @@ def _dialog_add_custom_model(orchestrator):
999
1027
 
1000
1028
  # Provider name (allow override)
1001
1029
  try:
1002
- pname_input = ptk_prompt(f" Provider display name [{provider_name}]: ", style=pt_style).strip()
1030
+ pname_input = _safe_prompt(f" Provider display name [{provider_name}]: ", color="#42bcf5").strip()
1003
1031
  if pname_input:
1004
1032
  provider_name = pname_input
1005
1033
  except (EOFError, KeyboardInterrupt):
@@ -1014,7 +1042,7 @@ def _dialog_add_custom_model(orchestrator):
1014
1042
  "(stored in config.json — input hidden):[/dim]"
1015
1043
  )
1016
1044
  try:
1017
- api_key = ptk_prompt(" API Key (Enter to skip): ", is_password=True, style=pt_style).strip()
1045
+ api_key = _safe_prompt(" API Key (Enter to skip): ", color="#42bcf5", is_password=True).strip()
1018
1046
  except (EOFError, KeyboardInterrupt):
1019
1047
  add_console.print("\n[dim]Cancelled.[/dim]\n")
1020
1048
  return
@@ -1025,7 +1053,7 @@ def _dialog_add_custom_model(orchestrator):
1025
1053
  "(e.g. gpt-4o, llama-3.3-70b-versatile, mistral-large-latest)[/dim]"
1026
1054
  )
1027
1055
  try:
1028
- model_id = ptk_prompt(" Model ID: ", style=pt_style).strip()
1056
+ model_id = _safe_prompt(" Model ID: ", color="#42bcf5").strip()
1029
1057
  except (EOFError, KeyboardInterrupt):
1030
1058
  add_console.print("\n[dim]Cancelled.[/dim]\n")
1031
1059
  return
@@ -1036,7 +1064,7 @@ def _dialog_add_custom_model(orchestrator):
1036
1064
 
1037
1065
  # Context window
1038
1066
  try:
1039
- ctx_raw = ptk_prompt(" Context window tokens [128000]: ", style=pt_style).strip()
1067
+ ctx_raw = _safe_prompt(" Context window tokens [128000]: ", color="#42bcf5").strip()
1040
1068
  context_window = int(ctx_raw) if ctx_raw else 128_000
1041
1069
  except (EOFError, KeyboardInterrupt):
1042
1070
  add_console.print("\n[dim]Cancelled.[/dim]\n")
@@ -1064,7 +1092,7 @@ def _dialog_add_custom_model(orchestrator):
1064
1092
 
1065
1093
  # Ask if user wants to switch to this model now
1066
1094
  try:
1067
- switch = ptk_prompt(" Switch to this model now? [Y/n]: ", style=pt_style).strip().lower()
1095
+ switch = _safe_prompt(" Switch to this model now? [Y/n]: ", color="#42bcf5").strip().lower()
1068
1096
  except (EOFError, KeyboardInterrupt):
1069
1097
  switch = "n"
1070
1098
 
@@ -1084,8 +1112,6 @@ def _dialog_delete_custom_model(orchestrator, model_id: str):
1084
1112
  """Confirm and delete a custom model by model_id."""
1085
1113
  import sys
1086
1114
  import time
1087
- from prompt_toolkit import prompt as ptk_prompt
1088
- from prompt_toolkit.styles import Style as PTStyle
1089
1115
  from rich.console import Console as RichConsole
1090
1116
 
1091
1117
 
@@ -1094,10 +1120,7 @@ def _dialog_delete_custom_model(orchestrator, model_id: str):
1094
1120
 
1095
1121
  del_console.print(f"\n[yellow]Delete custom model [bold]{model_id}[/bold]?[/yellow]")
1096
1122
  try:
1097
- confirm = ptk_prompt(
1098
- " Type 'yes' to confirm: ",
1099
- style=PTStyle.from_dict({'': '#f38ba8'})
1100
- ).strip().lower()
1123
+ confirm = _safe_prompt(" Type 'yes' to confirm: ", color="#f38ba8").strip().lower()
1101
1124
  except (EOFError, KeyboardInterrupt):
1102
1125
  confirm = ""
1103
1126
 
@@ -1124,8 +1147,6 @@ def _dialog_disconnect_provider(orchestrator):
1124
1147
  """Confirm and delete all custom models associated with a provider (disconnect provider)."""
1125
1148
  import sys
1126
1149
  import time
1127
- from prompt_toolkit import prompt as ptk_prompt
1128
- from prompt_toolkit.styles import Style as PTStyle
1129
1150
  from rich.console import Console as RichConsole
1130
1151
 
1131
1152
 
@@ -1163,9 +1184,8 @@ def _dialog_disconnect_provider(orchestrator):
1163
1184
  url_hint = f"[dim]{p['url']}[/dim]" if p['url'] else "[dim]local[/dim]"
1164
1185
  disc_console.print(f" [bold #f38ba8]{idx}[/bold #f38ba8] {p['name']} ({p['count']} models) {url_hint}")
1165
1186
 
1166
- pt_style = PTStyle.from_dict({'': '#f38ba8'})
1167
1187
  try:
1168
- choice = ptk_prompt("\n Disconnect provider number: ", style=pt_style).strip()
1188
+ choice = _safe_prompt("\n Disconnect provider number: ", color="#f38ba8").strip()
1169
1189
  except (EOFError, KeyboardInterrupt):
1170
1190
  disc_console.print("\n[dim]Cancelled.[/dim]\n")
1171
1191
  return
@@ -1180,7 +1200,7 @@ def _dialog_disconnect_provider(orchestrator):
1180
1200
  f"and delete all of its {selected['count']} models from UTIM.[/yellow]"
1181
1201
  )
1182
1202
  try:
1183
- confirm = ptk_prompt(" Type 'yes' to confirm: ", style=pt_style).strip().lower()
1203
+ confirm = _safe_prompt(" Type 'yes' to confirm: ", color="#f38ba8").strip().lower()
1184
1204
  except (EOFError, KeyboardInterrupt):
1185
1205
  confirm = ""
1186
1206
 
@@ -45,7 +45,9 @@ def _dialog_tools(orchestrator):
45
45
 
46
46
  while True:
47
47
  # Load disabled tools list
48
- disabled_tools = config.get("disabled_tools", [])
48
+ disabled_tools = config.get("disabled_tools") or []
49
+ if not isinstance(disabled_tools, list):
50
+ disabled_tools = []
49
51
 
50
52
  # Gather all tools
51
53
  mcp_tools = []
@@ -10,13 +10,13 @@ def _dialog_auto_update():
10
10
  import sys, subprocess
11
11
 
12
12
  def _get_status_label():
13
- enabled = config.get("auto_update_enabled", True)
13
+ enabled = config.get("auto_update_enabled", False)
14
14
  if enabled is False:
15
15
  return "[bold red]DISABLED[/bold red]"
16
16
  return "[bold green]ENABLED[/bold green]"
17
17
 
18
18
  while True:
19
- enabled = config.get("auto_update_enabled", True)
19
+ enabled = config.get("auto_update_enabled", False)
20
20
  rows = [
21
21
  {"key": "toggle", "label": f"Auto-Update: {_get_status_label()}",
22
22
  "desc": "Toggle automatic background updates on or off"},
@@ -55,7 +55,7 @@ def _dialog_auto_update():
55
55
  key = rows[idx]["key"]
56
56
 
57
57
  if key == "toggle":
58
- new_val = False if config.get("auto_update_enabled", True) else True
58
+ new_val = False if config.get("auto_update_enabled", False) else True
59
59
  config.set("auto_update_enabled", new_val)
60
60
  state_str = "enabled" if new_val else "disabled"
61
61
  _transient_status([
@@ -82,7 +82,7 @@ def _dialog_auto_update():
82
82
  _result[0] = ("network_error", False)
83
83
  return
84
84
  latest = resp.json().get("version", "")
85
- current = "1.44.2"
85
+ from utim_cli import __version__ as current
86
86
 
87
87
  def pv(v):
88
88
  return [int(x) for x in v.split(".") if x.isdigit()]
@@ -4,7 +4,7 @@ import os
4
4
  import locale
5
5
  import warnings
6
6
  import logging
7
- from utim_cli.constants import DEFAULT_MODEL
7
+ from utim_cli.constants import DEFAULT_MODEL, PROMPT_SYMBOL, ARROW_SYMBOL
8
8
 
9
9
  # Suppress HuggingFace / SentenceTransformers warnings, progress bars, and log messages globally
10
10
  os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
@@ -109,8 +109,8 @@ def _run_in_terminal_safe(func):
109
109
 
110
110
  if on_loop_thread:
111
111
  # We are on the main loop thread. Just call run_in_terminal.
112
- # It schedules the task on the loop. Do not await or block to avoid deadlocks.
113
- run_in_terminal(func)
112
+ # Wrap it in ensure_future so it is scheduled and run on the loop.
113
+ asyncio.ensure_future(run_in_terminal(func))
114
114
  else:
115
115
  # We are on a background thread.
116
116
  # Schedule the coroutine on the main loop thread and block until it finishes.
@@ -211,15 +211,25 @@ console = Console(
211
211
 
212
212
 
213
213
  def render_usage_menu(data: dict, con=console):
214
+ if not isinstance(data, dict):
215
+ data = {}
216
+ data = dict(data)
217
+ for k in ["balance", "max_limit", "percent_remaining", "refill_rate", "bonus_balance", "bonus_quota_percent", "bonus_limit", "free_monthly_cap", "five_hour_quota_percent", "quota_bank_percent"]:
218
+ if data.get(k) is None:
219
+ data[k] = 0.0
220
+ for k in ["refills_in_seconds", "refills_processed", "max_refills"]:
221
+ if data.get(k) is None:
222
+ data[k] = 0
223
+
214
224
  is_subscribed = data.get("is_subscribed", False)
215
225
  plan_name = data.get("plan_name", "Free")
216
- balance = data.get("balance", 0.0)
217
- max_limit = data.get("max_limit", 100.0)
218
- percent = data.get("percent_remaining", 0.0)
219
- refills_in = data.get("refills_in_seconds", 0)
220
- refills_processed = data.get("refills_processed", 0)
221
- max_refills = data.get("max_refills", 0)
222
- refill_rate = data.get("refill_rate", 0.0)
226
+ balance = data.get("balance")
227
+ max_limit = data.get("max_limit")
228
+ percent = data.get("percent_remaining")
229
+ refills_in = data.get("refills_in_seconds")
230
+ refills_processed = data.get("refills_processed")
231
+ max_refills = data.get("max_refills")
232
+ refill_rate = data.get("refill_rate")
223
233
 
224
234
  bar_width = 40
225
235
  filled = int(round((percent / 100.0) * bar_width))
@@ -849,7 +859,7 @@ def _print_animated_banner(animated: bool = True):
849
859
  else:
850
860
  console.print(f"[bold {color}]{BANNER_BIG}[/bold {color}]")
851
861
 
852
- console.print(f" [bold white]U Think I Make[/bold white] [dim]v1.46.28[/dim]")
862
+ console.print(f" [bold white]U Think I Make[/bold white] [dim]v1.46.30[/dim]")
853
863
  user_email = config.email
854
864
  user_type = "UTIM Community"
855
865
  api_key = config.get("api_key")
@@ -987,7 +997,19 @@ def _run_list_dialog(rows, render_row_fn, title="", legend="", extra_keys=None):
987
997
  end_idx = sel[0] + 1
988
998
 
989
999
  for i in range(viewport_start[0], end_idx):
990
- out.extend(render_row_fn(i, rows[i], i == sel[0]))
1000
+ rendered_row = render_row_fn(i, rows[i], i == sel[0])
1001
+ processed_row = []
1002
+ for style, text in rendered_row:
1003
+ if text.startswith(" ➔ "):
1004
+ text = text.replace(" ➔ ", f" {ARROW_SYMBOL} ")
1005
+ elif text.startswith(" "):
1006
+ text = (" " * (5 + len(ARROW_SYMBOL))) + text[6:]
1007
+ elif text.startswith(" "):
1008
+ text = (" " * (3 + len(ARROW_SYMBOL))) + text[4:]
1009
+ elif "➔" in text:
1010
+ text = text.replace("➔", ARROW_SYMBOL)
1011
+ processed_row.append((style, text))
1012
+ out.extend(processed_row)
991
1013
 
992
1014
  # Show indicator for items below the viewport
993
1015
  remaining = N - end_idx
@@ -1829,7 +1851,7 @@ def _print_session_history(orchestrator, messages, topic):
1829
1851
  for role, content in recent:
1830
1852
  if role == 'user':
1831
1853
  preview = content.strip()[:220] + ('\u2026' if len(content.strip()) > 220 else '')
1832
- console.print(f'[bold #42bcf5]\u276f[/bold #42bcf5] {preview}')
1854
+ console.print(f'[bold #42bcf5]{PROMPT_SYMBOL}[/bold #42bcf5] {preview}')
1833
1855
  else:
1834
1856
  preview = content.strip()[:500] + ('\u2026' if len(content.strip()) > 500 else '')
1835
1857
  console.print(Markdown(preview), width=w)
@@ -1864,7 +1886,7 @@ def _print_session_history(orchestrator, messages, topic):
1864
1886
 
1865
1887
  if role == "user":
1866
1888
  preview = _extract_text(content).strip()
1867
- console.print(f'[bold #42bcf5]\u276f[/bold #42bcf5] {preview}')
1889
+ console.print(f'[bold #42bcf5]{PROMPT_SYMBOL}[/bold #42bcf5] {preview}')
1868
1890
  console.print()
1869
1891
 
1870
1892
  elif role == "assistant":
@@ -2502,7 +2524,7 @@ def _build_pt_app(orchestrator, last_sigint):
2502
2524
 
2503
2525
 
2504
2526
  input_field = TextArea(
2505
- prompt=[('class:input-prompt', ' \u25b6 ')],
2527
+ prompt=[('class:input-prompt', f' {PROMPT_SYMBOL} ')],
2506
2528
  style='class:input-field',
2507
2529
  multiline=True,
2508
2530
  wrap_lines=True,
@@ -2836,9 +2858,9 @@ def _build_pt_app(orchestrator, last_sigint):
2836
2858
  if len(lines) > 15 or len(text) > 1000:
2837
2859
  line_count = len(lines)
2838
2860
  char_count = len(text)
2839
- _echo_line = f"[bold #42bcf5]\u276f[/bold #42bcf5] [Pasted Text: {line_count} lines, {char_count} chars]\n{text[:200]}{'...' if len(text) > 200 else ''}"
2861
+ _echo_line = f"[bold #42bcf5]{PROMPT_SYMBOL}[/bold #42bcf5] [Pasted Text: {line_count} lines, {char_count} chars]\n{text[:200]}{'...' if len(text) > 200 else ''}"
2840
2862
  else:
2841
- _echo_line = f"[bold #42bcf5]\u276f[/bold #42bcf5] {text}"
2863
+ _echo_line = f"[bold #42bcf5]{PROMPT_SYMBOL}[/bold #42bcf5] {text}"
2842
2864
 
2843
2865
  def _start_task():
2844
2866
  console.print(_echo_line)
@@ -2862,7 +2884,7 @@ def _build_pt_app(orchestrator, last_sigint):
2862
2884
  queue = STATE.get("queue", [])
2863
2885
  if queue:
2864
2886
  current_text = queue.pop(0)
2865
- console.print(f"\n[bold #42bcf5]\u276f[/bold #42bcf5] {current_text}")
2887
+ console.print(f"\n[bold #42bcf5]{PROMPT_SYMBOL}[/bold #42bcf5] {current_text}")
2866
2888
  STATE["busy_start"] = time.time()
2867
2889
  else:
2868
2890
  current_text = None
@@ -3063,12 +3085,12 @@ def _build_pt_app(orchestrator, last_sigint):
3063
3085
 
3064
3086
  def check_and_update_background():
3065
3087
  """Check npm registry for a newer version of utim-cli and install it silently.
3066
- Respects the auto_update_enabled config flag (default: True).
3088
+ Respects the auto_update_enabled config flag (default: False).
3067
3089
  Updates via both `pip install --upgrade utim-cli` and `npm install -g @emend-ai/utim@latest`.
3068
3090
  """
3069
3091
  from utim_cli.config import config
3070
- # Respect user preference — default ON
3071
- if config.get("auto_update_enabled", True) is False:
3092
+ # Respect user preference — default OFF
3093
+ if config.get("auto_update_enabled", False) is False:
3072
3094
  return
3073
3095
 
3074
3096
  def _run():
@@ -3087,7 +3109,7 @@ def check_and_update_background():
3087
3109
  return
3088
3110
 
3089
3111
  latest_ver = resp.json().get("version", "")
3090
- current_ver = "1.46.28"
3112
+ current_ver = "1.46.30"
3091
3113
 
3092
3114
  def parse_ver(v):
3093
3115
  return [int(x) for x in v.split(".") if x.isdigit()]
@@ -3365,9 +3387,10 @@ def version_callback(value: bool):
3365
3387
  if value:
3366
3388
  import typer
3367
3389
  from utim_cli import __version__
3368
- print(f"UTIM CLI v{__version__}")
3390
+ print(f"UTIM CLI v{__version__} — U Think I Make")
3369
3391
  raise typer.Exit()
3370
3392
 
3393
+
3371
3394
  @app.callback(invoke_without_command=True)
3372
3395
  def main(
3373
3396
  ctx: typer.Context,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 1.46.28
3
+ Version: 1.46.30
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
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Operating System :: OS Independent
17
- Classifier: Development Status :: 4 - Beta
17
+ Classifier: Development Status :: 5 - Production/Stable
18
18
  Classifier: Environment :: Console
19
19
  Classifier: Intended Audience :: Developers
20
20
  Requires-Python: >=3.9
@@ -175,6 +175,43 @@ Inside the interactive chat terminal, type these slash commands for direct works
175
175
  ## 📄 Related Documentation
176
176
 
177
177
  - [📖 Full Documentation](https://utim.dev/docs)
178
- - [🔒 Security Policy](SECURITY.md)
178
+ - [🔒 Security Policy](SECURITY.md) — vulnerability reporting, security@utim.dev
179
+ - [🗄️ Data Handling Reference](DATA_HANDLING.md) — what stays local, what goes to backend, what goes to model providers
179
180
  - [📊 SLA & Support](SLA.md)
180
181
  - [🛡️ Privacy Policy](landing/src/docs_md/privacy.md)
182
+
183
+ ## 📦 Installation Methods
184
+
185
+ ### npm (recommended — works without Python pre-installed)
186
+ ```bash
187
+ npm install -g @emend-ai/utim
188
+ ```
189
+
190
+ ### pip (from PyPI)
191
+ ```bash
192
+ pip install utim-cli
193
+ # Full install with optional features (web search, images, parsers)
194
+ pip install "utim-cli[full]"
195
+ ```
196
+
197
+ ### Source install (development)
198
+ ```bash
199
+ git clone https://github.com/emendai/utim.git
200
+ cd utim
201
+ pip install -e ".[full]"
202
+ ```
203
+
204
+ ### Smoke-test your install
205
+ ```bash
206
+ utim --version # Should print the current version
207
+ utim doctor # Runs diagnostics — all checks should pass
208
+ ```
209
+
210
+ See [INSTALLATION_VERIFICATION.md](INSTALLATION_VERIFICATION.md) for detailed verification steps and troubleshooting.
211
+
212
+ ## 🔢 Version
213
+
214
+ UTIM uses a single canonical version defined in [`utim_cli/_version.py`](utim_cli/_version.py).
215
+ Run `python scripts/sync_version.py` to propagate a version bump to all surfaces
216
+ (`package.json`, `pyproject.toml`, `CHANGELOG.md`) before releasing.
217
+
@@ -5,6 +5,7 @@ README.md
5
5
  pyproject.toml
6
6
  setup.py
7
7
  utim_cli/__init__.py
8
+ utim_cli/_version.py
8
9
  utim_cli/agent.py
9
10
  utim_cli/auth.py
10
11
  utim_cli/backup.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes