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.
- {utim_cli-1.46.28 → utim_cli-1.46.30}/CHANGELOG.md +18 -0
- {utim_cli-1.46.28/utim_cli.egg-info → utim_cli-1.46.30}/PKG-INFO +40 -3
- {utim_cli-1.46.28 → utim_cli-1.46.30}/README.md +38 -1
- {utim_cli-1.46.28 → utim_cli-1.46.30}/pyproject.toml +10 -3
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/__init__.py +7 -1
- utim_cli-1.46.30/utim_cli/_version.py +14 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/agent.py +3 -1
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/client_utils.py +2 -3
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/config.py +23 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/constants.py +6 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/doctor.py +8 -3
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/mcp_client.py +6 -2
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/orchestrator.py +7 -2
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/router.py +8 -2
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/share.py +2 -2
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/share_tui.py +8 -7
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/feedback_dialog.py +10 -6
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/model_dialog.py +51 -31
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/tools_dialog.py +3 -1
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/update_dialog.py +4 -4
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/utim.py +46 -23
- {utim_cli-1.46.28 → utim_cli-1.46.30/utim_cli.egg-info}/PKG-INFO +40 -3
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli.egg-info/SOURCES.txt +1 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/LICENSE +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/MANIFEST.in +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/setup.cfg +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/setup.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/auth.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/backup.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/blender_agent.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/bootstrap.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/context_pruner.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/local_db.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/logger.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/reflection.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/report.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/__init__.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/auth.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/db.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/firebase.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/history.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/models.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/completion_routes.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/server/server.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/state.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tools.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/utilities.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/vector_memory.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli/workspace.py +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-1.46.28 → utim_cli-1.46.30}/utim_cli.egg-info/requires.txt +0 -0
- {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.
|
|
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 ::
|
|
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.
|
|
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 ::
|
|
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
|
-
|
|
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
|
-
|
|
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"]) >
|
|
93
|
-
|
|
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"
|
|
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 = '
|
|
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 = '
|
|
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 = "
|
|
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"
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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
|
-
#
|
|
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"
|
|
217
|
-
max_limit = data.get("max_limit"
|
|
218
|
-
percent = data.get("percent_remaining"
|
|
219
|
-
refills_in = data.get("refills_in_seconds"
|
|
220
|
-
refills_processed = data.get("refills_processed"
|
|
221
|
-
max_refills = data.get("max_refills"
|
|
222
|
-
refill_rate = data.get("refill_rate"
|
|
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.
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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', '
|
|
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]
|
|
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]
|
|
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]
|
|
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:
|
|
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
|
|
3071
|
-
if config.get("auto_update_enabled",
|
|
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.
|
|
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.
|
|
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 ::
|
|
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
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|