utim-cli 1.44.1__tar.gz → 1.44.3__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 (50) hide show
  1. {utim_cli-1.44.1 → utim_cli-1.44.3}/CHANGELOG.md +37 -29
  2. {utim_cli-1.44.1/utim_cli.egg-info → utim_cli-1.44.3}/PKG-INFO +2 -1
  3. {utim_cli-1.44.1 → utim_cli-1.44.3}/pyproject.toml +2 -1
  4. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/agent.py +13 -13
  5. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/auth.py +11 -2
  6. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/blender_agent.py +29 -8
  7. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/config.py +94 -35
  8. utim_cli-1.44.3/utim_cli/constants.py +13 -0
  9. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/context_pruner.py +2 -1
  10. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/mcp_client.py +2 -1
  11. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/mcp_registry.json +1 -0
  12. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/orchestrator.py +25 -14
  13. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/reflection.py +2 -1
  14. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/tools.py +149 -66
  15. utim_cli-1.44.3/utim_cli/tui/__init__.py +1 -0
  16. utim_cli-1.44.3/utim_cli/tui/history_dialog.py +242 -0
  17. utim_cli-1.44.3/utim_cli/tui/mcp_dialog.py +417 -0
  18. utim_cli-1.44.3/utim_cli/tui/model_dialog.py +1235 -0
  19. utim_cli-1.44.3/utim_cli/tui/resume_dialog.py +137 -0
  20. utim_cli-1.44.3/utim_cli/tui/tools_dialog.py +112 -0
  21. utim_cli-1.44.3/utim_cli/tui/update_dialog.py +152 -0
  22. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/utim.py +122 -2129
  23. {utim_cli-1.44.1 → utim_cli-1.44.3/utim_cli.egg-info}/PKG-INFO +2 -1
  24. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli.egg-info/SOURCES.txt +9 -2
  25. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli.egg-info/requires.txt +1 -0
  26. utim_cli-1.44.1/utim_cli/billing.py +0 -40
  27. {utim_cli-1.44.1 → utim_cli-1.44.3}/LICENSE +0 -0
  28. {utim_cli-1.44.1 → utim_cli-1.44.3}/MANIFEST.in +0 -0
  29. {utim_cli-1.44.1 → utim_cli-1.44.3}/README.md +0 -0
  30. {utim_cli-1.44.1 → utim_cli-1.44.3}/setup.cfg +0 -0
  31. {utim_cli-1.44.1 → utim_cli-1.44.3}/setup.py +0 -0
  32. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/__init__.py +0 -0
  33. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/backup.py +0 -0
  34. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/bootstrap.py +0 -0
  35. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/client_utils.py +0 -0
  36. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/doctor.py +0 -0
  37. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/knowledge_graph.py +0 -0
  38. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/logger.py +0 -0
  39. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/mcp_clean_wrapper.py +0 -0
  40. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/report.py +0 -0
  41. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/scrapy_search.py +0 -0
  42. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/share.py +0 -0
  43. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/share_tui.py +0 -0
  44. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/situational_scoring.py +0 -0
  45. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/state.py +0 -0
  46. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/vector_memory.py +0 -0
  47. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli/workspace.py +0 -0
  48. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli.egg-info/dependency_links.txt +0 -0
  49. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli.egg-info/entry_points.txt +0 -0
  50. {utim_cli-1.44.1 → utim_cli-1.44.3}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,29 +1,37 @@
1
- # Changelog
2
-
3
- All notable changes to the UTIM CLI Agent will be documented in this file.
4
-
5
- ## [1.43.20] - 2026-07-01
6
-
7
- ### Added
8
- - **Local-Global Experience Vector DB Sync**: UTIM now carries the ChromaDB experience database across project folders automatically. On startup, the previous project's `experiences.json`, `vector_db`, and vector metadata files are copied to the current folder deleted from the old folder only after a successful paste. Last active folder is persisted server-side so the sync survives PC restarts.
9
- - **BYOK Provider Disconnection (key `x`)**: Press `x` in the model selector to view a list of connected custom providers and disconnect them. All associated models are removed from the model list instantly.
10
- - **Bring Your Own Key (BYOK) for Free Users**: Free-tier users can now connect any OpenAI-compatible provider using their own API key. BYOK models bypass UTIM quota limits and plan gating entirely.
11
- - **Dynamic Changelog & Version API**: `/api/changelog` parses `CHANGELOG.md` at request time and returns structured JSON. `/health` version field is also derived from `CHANGELOG.md` dynamically — deploying a new changelog auto-updates both the website changelog page and version badge.
12
- - **Changelog Tab on Website**: The terminal website now includes a `↻ Changelog` tab that fetches live release notes from the Railway server on load.
13
- - **Features Page Redesign**: The Features tab now displays a 9-card animated grid with per-feature glow icons, numbered labels, colored tags, and spring entrance animations — matching the full terminal aesthetic of the site.
14
- - **Last Project Folder Persistence**: Server stores each user's last active project directory in the database. Survives PC restarts and is used to drive the cross-folder experience DB sync.
15
- - **`GET /api/auth/last-folder` and `POST /api/auth/last-folder` Endpoints**: New authenticated REST endpoints to read/write a user's last active project folder path.
16
-
17
- ### Changed
18
- - **Plan Upgrade Credit Transition**: Free plan credit allowance ($1.00) is zeroed out instead of rolling over to the new paid plan's quota bank on upgrade.
19
- - **Hobby Plan Model Access**: Hobby plan `allowed_models` corrected to `"all"` so users can access the full model registry (not just free-tier models).
20
- - **BYOK Model Gating Bypass**: Orchestrator pre-flight checks now skip plan gating and UTIM credit checks for custom BYOK models since they use user-owned keys.
21
- - **Pricing Page Claims Cleaned**: Removed unsubstantiated marketing claims (`Dedicated elite endpoints`, `Autonomous team agents access`, `High-speed dedicated endpoints`, `All premium models (GPT-4o, Claude)`, `Synthetic Eye & VSIX tools`, `Claude Sonnet 4.6 (1 RPM)`, `10-session history storage`) from all pricing tier feature lists.
22
-
23
- ### Fixed
24
- - **BYOK Flow Display Bug**: Rewrote Add/Delete/BYOK-import dialogs to run on `sys.__stdout__` with `prompt_toolkit.prompt()` so they are never swallowed by the alt-screen buffer.
25
- - **Double-Exit Crash (TUI)**: Wrapped all keyboard exit callbacks (`ENTER`/`ESC`/`q`/`CTRL+C`) in `try...except` blocks to prevent double-fire tracebacks.
26
- - **`/changelog` Route No Longer Shows Legacy Page**: All website routes (including `/changelog`) now correctly map to the `<PowershellUI />` terminal component. Legacy standalone page removed.
27
-
28
- ---
29
-
1
+ # Changelog
2
+
3
+ All notable changes to the UTIM CLI Agent will be documented in this file.
4
+
5
+ ## [1.44.2] - 2026-07-02
6
+
7
+ ### Fixed
8
+ - **MCP Server Hang:** Fixed an issue where installing MCP servers (like Figma) without the `--stdio` flag would hang the CLI indefinitely. Added a 60-second initialization timeout and updated the preset registry to include required flags automatically.
9
+ - **Model Selection Crash:** Fixed an issue where the CLI would crash instantly when trying to open the `/model` selector on a production npm install due to an excluded server module.
10
+ - **Image Subagent Config:** Separated the Image Generation model and the Prompt Expander LLM in the subagent configuration UI, allowing users to configure both independently.
11
+ - **Minor:** Minor Bug Fixes
12
+
13
+ ## [1.43.20] - 2026-07-01
14
+
15
+ ### Added
16
+ - **Local-Global Experience Vector DB Sync**: UTIM now carries the ChromaDB experience database across project folders automatically. On startup, the previous project's `experiences.json`, `vector_db`, and vector metadata files are copied to the current folder — deleted from the old folder only after a successful paste. Last active folder is persisted server-side so the sync survives PC restarts.
17
+ - **BYOK Provider Disconnection (key `x`)**: Press `x` in the model selector to view a list of connected custom providers and disconnect them. All associated models are removed from the model list instantly.
18
+ - **Bring Your Own Key (BYOK) for Free Users**: Free-tier users can now connect any OpenAI-compatible provider using their own API key. BYOK models bypass UTIM quota limits and plan gating entirely.
19
+ - **Dynamic Changelog & Version API**: `/api/changelog` parses `CHANGELOG.md` at request time and returns structured JSON. `/health` version field is also derived from `CHANGELOG.md` dynamically deploying a new changelog auto-updates both the website changelog page and version badge.
20
+ - **Changelog Tab on Website**: The terminal website now includes a `↻ Changelog` tab that fetches live release notes from the Railway server on load.
21
+ - **Features Page Redesign**: The Features tab now displays a 9-card animated grid with per-feature glow icons, numbered labels, colored tags, and spring entrance animations matching the full terminal aesthetic of the site.
22
+ - **Last Project Folder Persistence**: Server stores each user's last active project directory in the database. Survives PC restarts and is used to drive the cross-folder experience DB sync.
23
+ - **`GET /api/auth/last-folder` and `POST /api/auth/last-folder` Endpoints**: New authenticated REST endpoints to read/write a user's last active project folder path.
24
+
25
+ ### Changed
26
+ - **Plan Upgrade Credit Transition**: Free plan credit allowance ($1.00) is zeroed out instead of rolling over to the new paid plan's quota bank on upgrade.
27
+ - **Hobby Plan Model Access**: Hobby plan `allowed_models` corrected to `"all"` so users can access the full model registry (not just free-tier models).
28
+ - **BYOK Model Gating Bypass**: Orchestrator pre-flight checks now skip plan gating and UTIM credit checks for custom BYOK models since they use user-owned keys.
29
+ - **Pricing Page Claims Cleaned**: Removed unsubstantiated marketing claims (`Dedicated elite endpoints`, `Autonomous team agents access`, `High-speed dedicated endpoints`, `All premium models (GPT-4o, Claude)`, `Synthetic Eye & VSIX tools`, `Claude Sonnet 4.6 (1 RPM)`, `10-session history storage`) from all pricing tier feature lists.
30
+
31
+ ### Fixed
32
+ - **BYOK Flow Display Bug**: Rewrote Add/Delete/BYOK-import dialogs to run on `sys.__stdout__` with `prompt_toolkit.prompt()` so they are never swallowed by the alt-screen buffer.
33
+ - **Double-Exit Crash (TUI)**: Wrapped all keyboard exit callbacks (`ENTER`/`ESC`/`q`/`CTRL+C`) in `try...except` blocks to prevent double-fire tracebacks.
34
+ - **`/changelog` Route No Longer Shows Legacy Page**: All website routes (including `/changelog`) now correctly map to the `<PowershellUI />` terminal component. Legacy standalone page removed.
35
+
36
+ ---
37
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: utim-cli
3
- Version: 1.44.1
3
+ Version: 1.44.3
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
@@ -37,6 +37,7 @@ Requires-Dist: tree-sitter-typescript>=0.21.0
37
37
  Requires-Dist: mcp>=0.1.0
38
38
  Requires-Dist: nest-asyncio>=1.5.0
39
39
  Requires-Dist: python-multipart>=0.0.7
40
+ Requires-Dist: slowapi>=0.1.9
40
41
  Provides-Extra: search
41
42
  Requires-Dist: scrapy>=2.11.0; extra == "search"
42
43
  Requires-Dist: scrapy-playwright>=0.0.34; extra == "search"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "1.44.1"
7
+ version = "1.44.3"
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"
@@ -39,6 +39,7 @@ dependencies = [
39
39
  "mcp>=0.1.0",
40
40
  "nest-asyncio>=1.5.0",
41
41
  "python-multipart>=0.0.7",
42
+ "slowapi>=0.1.9",
42
43
  ]
43
44
 
44
45
  [project.urls]
@@ -3,7 +3,7 @@ import json
3
3
  import sys
4
4
  import time
5
5
  from openai import OpenAI
6
- from .tools import UTIM_TOOLS, TOOL_FUNCTIONS
6
+ from .tools import get_tools
7
7
  from rich.console import Console
8
8
  from rich.live import Live
9
9
  from rich.spinner import Spinner
@@ -193,11 +193,15 @@ class ReActAgent:
193
193
  except Exception as e:
194
194
  return f"Error executing MCP tool {tool_name}: {str(e)}"
195
195
 
196
- if tool_name not in TOOL_FUNCTIONS:
197
- return f"Unknown tool: {tool_name}"
198
-
196
+ utim_tools, tool_functions = get_tools()
197
+ if tool_name not in tool_functions:
198
+ # If not a standard tool, check MCP
199
+ if hasattr(self, 'orchestrator') and self.orchestrator and tool_name in self.orchestrator.mcp_tool_names:
200
+ return self.orchestrator._execute_mcp_tool(tool_name, arguments)
201
+ return f"Error: Tool '{tool_name}' not found."
202
+
199
203
  try:
200
- result = TOOL_FUNCTIONS[tool_name](**arguments)
204
+ result = tool_functions[tool_name](**arguments)
201
205
  self._render_tool_panel(tool_name, arguments, result)
202
206
  return str(result)
203
207
  except Exception as e:
@@ -212,17 +216,12 @@ class ReActAgent:
212
216
  tool_calls = []
213
217
  current_tool_call = None
214
218
 
215
- mcp_tools = []
216
- try:
217
- from utim_cli.mcp_client import mcp_manager
218
- mcp_tools = mcp_manager.get_tools()
219
- except Exception:
220
- pass
219
+ utim_tools, tool_functions = get_tools()
221
220
 
222
221
  # Filter disabled tools
223
222
  from utim_cli.config import config
224
223
  disabled = config.get("disabled_tools", [])
225
- all_tools = [t for t in (UTIM_TOOLS + mcp_tools) if t["function"]["name"] not in disabled]
224
+ all_tools = [t for t in utim_tools if t["function"]["name"] not in disabled]
226
225
 
227
226
  kwargs = {
228
227
  "model": self.model_id,
@@ -314,7 +313,8 @@ class ReActAgent:
314
313
 
315
314
  headers = ["Tool", "Description"]
316
315
  all_tools = []
317
- for tool_def in UTIM_TOOLS:
316
+ utim_tools, _ = get_tools()
317
+ for tool_def in utim_tools:
318
318
  fn = tool_def["function"]
319
319
  all_tools.append((fn["name"], fn["description"], "standard"))
320
320
 
@@ -112,8 +112,17 @@ def login() -> None:
112
112
  console.print(f" [bold {BLUE}]{auth_url}[/bold {BLUE}]\n")
113
113
  webbrowser.open(auth_url)
114
114
 
115
- # Block until the callback arrives
116
- httpd.serve_forever()
115
+ # Block until the callback arrives with a 5-minute timeout
116
+ import time
117
+ httpd.timeout = 1.0
118
+ start_time = time.time()
119
+ while not _AuthCallbackHandler.received:
120
+ if time.time() - start_time > 300:
121
+ console.print(f" [bold red]✗ Authentication timed out (5 minutes).[/bold red]\n")
122
+ httpd.server_close()
123
+ return
124
+ httpd.handle_request()
125
+
117
126
  httpd.server_close()
118
127
 
119
128
  data = _AuthCallbackHandler.received
@@ -46,6 +46,7 @@ import subprocess
46
46
  import time
47
47
  import uuid
48
48
  from typing import Any, Dict, List, Optional
49
+ from utim_cli.constants import DEFAULT_MODEL
49
50
 
50
51
  # ---------------------------------------------------------------------------
51
52
  # Configuration
@@ -56,8 +57,6 @@ _OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
56
57
  # Vision models (must support image_url content parts)
57
58
  _VISION_MODELS: List[str] = [
58
59
  "google/gemma-4-31b-it:free",
59
- "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
60
- "nvidia/nemotron-nano-12b-v2-vl:free",
61
60
  "google/gemma-4-26b-a4b-it:free",
62
61
  "nvidia/llama-nemotron-embed-vl-1b-v2:free",
63
62
  "nvidia/llama-nemotron-rerank-vl-1b-v2:free",
@@ -66,12 +65,34 @@ _VISION_MODELS: List[str] = [
66
65
 
67
66
  _CODE_MODELS: List[str] = [
68
67
  "poolside/laguna-m.1:free",
69
- "cohere/north-mini-code:free",
70
- "qwen/qwen3-coder:free",
71
- "nvidia/nemotron-3-ultra-550b-a55b:free",
68
+ DEFAULT_MODEL,
72
69
  "openrouter/free"
73
70
  ]
74
71
 
72
+
73
+ def _get_vision_models() -> List[str]:
74
+ """Return vision model list, with user override prepended if configured."""
75
+ try:
76
+ from utim_cli.config import config
77
+ override = config.get("subagent_model_blender_vision")
78
+ if override and override not in ("__non_agent__", "__none__"):
79
+ return [override] + [m for m in _VISION_MODELS if m != override]
80
+ except Exception:
81
+ pass
82
+ return _VISION_MODELS
83
+
84
+
85
+ def _get_code_models() -> List[str]:
86
+ """Return code model list, with user override prepended if configured."""
87
+ try:
88
+ from utim_cli.config import config
89
+ override = config.get("subagent_model_blender_code")
90
+ if override and override not in ("__non_agent__", "__none__"):
91
+ return [override] + [m for m in _CODE_MODELS if m != override]
92
+ except Exception:
93
+ pass
94
+ return _CODE_MODELS
95
+
75
96
  MAX_RETRIES = 3 # Script fix-and-retry attempts
76
97
 
77
98
  # ---------------------------------------------------------------------------
@@ -562,7 +583,7 @@ def _phase1_vision(image_path: str, scene_brief: Dict[str, Any]) -> Dict[str, An
562
583
  raw = _llm_call(
563
584
  system=_VISION_SYSTEM_PROMPT,
564
585
  user_text=brief_text,
565
- models=_VISION_MODELS,
586
+ models=_get_vision_models(),
566
587
  image_b64=image_b64,
567
588
  image_mime=mime_type,
568
589
  max_tokens=8192,
@@ -674,7 +695,7 @@ def _phase2_generate_script(
674
695
  raw_script = _llm_call(
675
696
  system=_CODE_SYSTEM_PROMPT,
676
697
  user_text=user_prompt,
677
- models=_CODE_MODELS,
698
+ models=_get_code_models(),
678
699
  max_tokens=16384,
679
700
  )
680
701
 
@@ -854,7 +875,7 @@ def _phase3_execute(
854
875
  raw_fixed = _llm_call(
855
876
  system=_FIX_SYSTEM_PROMPT,
856
877
  user_text=fix_prompt,
857
- models=_CODE_MODELS,
878
+ models=_get_code_models(),
858
879
  max_tokens=16384,
859
880
  )
860
881
  current_script = _clean_and_patch_script(
@@ -1,35 +1,48 @@
1
1
  import json
2
- import shutil
2
+ import logging
3
3
  import os
4
4
  import pathlib
5
5
  from typing import Any, Dict, Optional
6
6
 
7
+ logger = logging.getLogger("utim.config")
8
+
7
9
  class Config:
8
10
  def __init__(self):
9
11
  self.global_dir = pathlib.Path.home() / ".utim"
10
12
  self.local_dir = pathlib.Path(".utim").resolve()
11
- self._data: Dict[str, Any] = self._load()
13
+ self._global_data: Dict[str, Any] = {}
14
+ self._local_data: Dict[str, Any] = {}
15
+ self._load()
12
16
 
13
- def _load(self) -> Dict[str, Any]:
14
- data = {}
17
+ def _load(self):
15
18
  # Load global config
16
19
  global_config = self.global_dir / "config.json"
17
20
  if global_config.exists():
18
21
  try:
19
22
  with open(global_config, "r", encoding="utf-8") as f:
20
- data.update(json.load(f))
21
- except Exception:
22
- pass
23
+ self._global_data = json.load(f)
24
+ except json.JSONDecodeError as e:
25
+ logger.warning(f"Global config corrupted at {global_config}: {e}. Using defaults.")
26
+ except Exception as e:
27
+ logger.error(f"Cannot read global config: {e}")
23
28
 
24
29
  # Load local config if exists
25
30
  local_config = self.local_dir / "config.json"
26
31
  if local_config.exists():
27
32
  try:
28
33
  with open(local_config, "r", encoding="utf-8") as f:
29
- data.update(json.load(f))
30
- except Exception:
31
- pass
32
- return data
34
+ self._local_data = json.load(f)
35
+ except json.JSONDecodeError as e:
36
+ logger.warning(f"Local config corrupted at {local_config}: {e}. Using defaults.")
37
+ except Exception as e:
38
+ logger.error(f"Cannot read local config: {e}")
39
+
40
+ @property
41
+ def _data(self) -> Dict[str, Any]:
42
+ """Returns a merged view (local overrides global)."""
43
+ merged = dict(self._global_data)
44
+ merged.update(self._local_data)
45
+ return merged
33
46
 
34
47
  def save(self):
35
48
  # Save to global config directory
@@ -37,24 +50,28 @@ class Config:
37
50
  global_config = self.global_dir / "config.json"
38
51
  try:
39
52
  with open(global_config, "w", encoding="utf-8") as f:
40
- json.dump(self._data, f, indent=4)
41
- except Exception:
42
- pass
53
+ json.dump(self._global_data, f, indent=4)
54
+ except Exception as e:
55
+ logger.error(f"Cannot write global config: {e}")
43
56
 
44
- # Save to local config directory if it exists
45
- if self.local_dir.exists():
57
+ # Save to local config directory if we have local data
58
+ if self._local_data:
59
+ self.local_dir.mkdir(parents=True, exist_ok=True)
46
60
  local_config = self.local_dir / "config.json"
47
61
  try:
48
62
  with open(local_config, "w", encoding="utf-8") as f:
49
- json.dump(self._data, f, indent=4)
50
- except Exception:
51
- pass
63
+ json.dump(self._local_data, f, indent=4)
64
+ except Exception as e:
65
+ logger.error(f"Cannot write local config: {e}")
52
66
 
53
67
  def get(self, key: str, default: Any = None) -> Any:
54
68
  return self._data.get(key, default)
55
69
 
56
- def set(self, key: str, value: Any):
57
- self._data[key] = value
70
+ def set(self, key: str, value: Any, local: bool = False):
71
+ if local:
72
+ self._local_data[key] = value
73
+ else:
74
+ self._global_data[key] = value
58
75
  self.save()
59
76
 
60
77
  @property
@@ -70,7 +87,8 @@ class Config:
70
87
  return self.get("name")
71
88
 
72
89
  def clear(self):
73
- self._data = {}
90
+ self._global_data = {}
91
+ self._local_data = {}
74
92
  self.save()
75
93
 
76
94
  # ── Custom / Bring-Your-Own-Model support ─────────────────────────────────
@@ -92,14 +110,28 @@ class Config:
92
110
 
93
111
  @custom_models.setter
94
112
  def custom_models(self, value: list):
95
- self._data["custom_models"] = value
96
- self.save()
113
+ self.set("custom_models", value)
97
114
 
98
115
  def add_custom_model(self, entry: dict) -> None:
99
116
  """Append or replace a custom model entry keyed on model_id."""
100
117
  models = self.custom_models
101
118
  models = [m for m in models if m.get("model_id") != entry["model_id"]]
102
- models.append(entry)
119
+
120
+ api_key = entry.get("api_key")
121
+ use_keyring = False
122
+ if api_key:
123
+ try:
124
+ import keyring
125
+ keyring.set_password("utim_cli", entry["model_id"], api_key)
126
+ use_keyring = True
127
+ except Exception as e:
128
+ logger.warning(f"Keyring not available, storing key in plain text: {e}")
129
+
130
+ entry_to_save = dict(entry)
131
+ if use_keyring:
132
+ entry_to_save.pop("api_key", None)
133
+
134
+ models.append(entry_to_save)
103
135
  self.custom_models = models
104
136
 
105
137
  def remove_custom_model(self, model_id: str) -> bool:
@@ -108,26 +140,55 @@ class Config:
108
140
  new = [m for m in models if m.get("model_id") != model_id]
109
141
  if len(new) == len(models):
110
142
  return False
143
+
144
+ try:
145
+ import keyring
146
+ keyring.delete_password("utim_cli", model_id)
147
+ except Exception:
148
+ pass
149
+
111
150
  self.custom_models = new
112
151
  return True
113
152
 
114
153
  def remove_custom_provider(self, provider_name: str, base_url: str) -> int:
115
154
  """Remove all custom models associated with a provider. Returns count removed."""
116
155
  models = self.custom_models
117
- new = [
118
- m for m in models
119
- if m.get("provider_name") != provider_name or m.get("base_url") != base_url
120
- ]
121
- removed_count = len(models) - len(new)
122
- if removed_count > 0:
156
+ new = []
157
+ removed = 0
158
+ try:
159
+ import keyring
160
+ has_keyring = True
161
+ except ImportError:
162
+ has_keyring = False
163
+
164
+ for m in models:
165
+ if m.get("provider_name") == provider_name and m.get("base_url") == base_url:
166
+ removed += 1
167
+ if has_keyring:
168
+ try:
169
+ keyring.delete_password("utim_cli", m["model_id"])
170
+ except Exception:
171
+ pass
172
+ else:
173
+ new.append(m)
174
+
175
+ if removed > 0:
123
176
  self.custom_models = new
124
- return removed_count
177
+ return removed
125
178
 
126
179
  def get_custom_model(self, model_id: str) -> Optional[dict]:
127
180
  """Return the custom model entry for *model_id*, or None."""
128
181
  for m in self.custom_models:
129
182
  if m.get("model_id") == model_id:
130
- return m
183
+ m_ret = dict(m)
184
+ try:
185
+ import keyring
186
+ key = keyring.get_password("utim_cli", model_id)
187
+ if key:
188
+ m_ret["api_key"] = key
189
+ except Exception:
190
+ pass
191
+ return m_ret
131
192
  return None
132
193
 
133
194
  @property
@@ -147,8 +208,6 @@ class Config:
147
208
  return [m.strip() for m in models.split(",")]
148
209
  return [
149
210
  "poolside/laguna-m.1:free",
150
- "qwen/qwen3-coder:free",
151
- "nvidia/nemotron-3-ultra-550b-a55b:free",
152
211
  "openrouter/free"
153
212
  ]
154
213
 
@@ -0,0 +1,13 @@
1
+ import os
2
+
3
+ DEFAULT_MODEL = "cohere/north-mini-code:free"
4
+ SERVER_URL = os.getenv("UTIM_SERVER_URL", "https://api.utim.dev")
5
+ OPENROUTER_BASE_URL = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
6
+
7
+ SUBAGENT_DEFAULTS = {
8
+ "web_search": DEFAULT_MODEL,
9
+ "plan_project": DEFAULT_MODEL,
10
+ "project_res": DEFAULT_MODEL,
11
+ "generate_image": "nvidia/nemotron-3-nano-30b-a3b:free",
12
+ "blender_create_object": DEFAULT_MODEL,
13
+ }
@@ -11,12 +11,13 @@ from typing import List, Dict, Tuple, Optional, Set
11
11
  import requests
12
12
  import json
13
13
  import hashlib
14
+ from utim_cli.constants import DEFAULT_MODEL
14
15
 
15
16
  # Fallback models for compression operations - ordered by reliability and quality
16
17
  COMPRESSION_FALLBACK_MODELS = [
17
18
  "openai/gpt-oss-20b:free",
18
19
  "poolside/laguna-xs.2:free",
19
- "cohere/north-mini-code:free",
20
+ DEFAULT_MODEL,
20
21
  "openrouter/free"
21
22
  ]
22
23
 
@@ -114,9 +114,10 @@ class MCPManager:
114
114
  stack = AsyncExitStack()
115
115
  self._exit_stacks[name] = stack
116
116
 
117
+ import asyncio
117
118
  read, write = await stack.enter_async_context(stdio_client(server_params))
118
119
  session = await stack.enter_async_context(ClientSession(read, write))
119
- await session.initialize()
120
+ await asyncio.wait_for(session.initialize(), timeout=60.0)
120
121
 
121
122
  self.sessions[name] = session
122
123
 
@@ -357,6 +357,7 @@
357
357
  "name": "Figma MCP Server",
358
358
  "desc": "The Figma MCP server brings Figma design context directly into your AI workflow.",
359
359
  "pkg": "figma-developer-mcp",
360
+ "args": ["--stdio"],
360
361
  "author": "Community",
361
362
  "fields": [
362
363
  {
@@ -22,10 +22,12 @@ import subprocess
22
22
  import sys
23
23
  import threading
24
24
  import time
25
+ from datetime import datetime
25
26
  from concurrent.futures import ThreadPoolExecutor, as_completed
26
27
  from typing import Any, Dict, List, Optional, Tuple
27
28
 
28
29
  import requests
30
+ from utim_cli.constants import DEFAULT_MODEL
29
31
  # openai SDK removed — we call OpenRouter directly via requests (no Rust/jiter needed)
30
32
  from rich.console import Console
31
33
  from rich.live import Live
@@ -35,8 +37,7 @@ from rich.rule import Rule
35
37
  from rich.spinner import Spinner
36
38
  from rich.text import Text
37
39
 
38
- from .billing import CreditManager
39
- from .tools import TOOL_FUNCTIONS, UTIM_TOOLS
40
+ from .tools import get_tools
40
41
  import utim_cli.tools as _tools_module # for injecting cancel_event
41
42
  from .config import config
42
43
 
@@ -261,11 +262,10 @@ class Orchestrator:
261
262
  mcp_manager.start()
262
263
  except Exception:
263
264
  pass
264
- self.credits = CreditManager()
265
265
  self.server_url = "https://api.utim.dev"
266
266
  self.session_id: Optional[str] = None
267
267
  # Primary model — falls back through config.fallback_models on failure
268
- self.model_id: str = "cohere/north-mini-code:free"
268
+ self.model_id: str = DEFAULT_MODEL
269
269
  self._current_line_len = 0
270
270
  self.tool_results: List[Dict[str, Any]] = []
271
271
  self.turn_step_timings: List[Dict[str, Any]] = []
@@ -499,6 +499,7 @@ class Orchestrator:
499
499
  timeout=5,
500
500
  )
501
501
  if resp.status_code == 200:
502
+ quota = resp.json()
502
503
  chosen_model = override_model if override_model else self.model_id
503
504
  is_custom = bool(config.get_custom_model(chosen_model))
504
505
 
@@ -521,7 +522,7 @@ class Orchestrator:
521
522
  is_custom = bool(config.get_custom_model(chosen_model))
522
523
 
523
524
  if not is_custom and models_allowed != ["all"] and chosen_model not in models_allowed:
524
- fallback_model = "cohere/north-mini-code:free" # default free fallback
525
+ fallback_model = DEFAULT_MODEL # default free fallback
525
526
  self.console.print(f"\n[bold yellow]⚠ Model '{chosen_model}' is gated under your current '{quota['display_name']}' plan.[/bold yellow]")
526
527
  self.console.print(f" Downgrading to default allowed model: '{fallback_model}' for this request.")
527
528
  if override_model:
@@ -568,7 +569,9 @@ class Orchestrator:
568
569
  except Exception:
569
570
  pass
570
571
 
571
- all_tools = (override_tools if override_tools is not None else UTIM_TOOLS) + mcp_tools
572
+ from utim_cli.tools import get_tools
573
+ utim_tools, _ = get_tools()
574
+ all_tools = (override_tools if override_tools is not None else utim_tools) + mcp_tools
572
575
  # Filter disabled tools
573
576
  disabled = config.get("disabled_tools", [])
574
577
  all_tools = [t for t in all_tools if t["function"]["name"] not in disabled]
@@ -1362,12 +1365,13 @@ class Orchestrator:
1362
1365
  ))
1363
1366
  return f"Error executing MCP tool {func_name}: {str(e)}"
1364
1367
 
1365
- if func_name not in TOOL_FUNCTIONS:
1366
- self.console.print(Panel(
1367
- Text(f"Unknown tool: {func_name}", style="red"),
1368
- border_style="red", padding=(0, 1),
1369
- ))
1370
- return f"Unknown tool: {func_name}"
1368
+ from utim_cli.tools import get_tools
1369
+ utim_tools, tool_functions = get_tools()
1370
+ if func_name not in tool_functions:
1371
+ # Check if it's an MCP tool
1372
+ if func_name in self.mcp_tool_names:
1373
+ return self._execute_mcp_tool(func_name, call_args)
1374
+ return f"Error: The tool '{func_name}' does not exist or is not available in the current context."
1371
1375
 
1372
1376
  display_arg = self._get_display_arg(func_name, arguments)
1373
1377
 
@@ -1508,7 +1512,8 @@ class Orchestrator:
1508
1512
  _SILENT_TOOLS = {"manage_memory", "recall_experience", "store_experience"}
1509
1513
  if func_name in _SILENT_TOOLS:
1510
1514
  try:
1511
- result = TOOL_FUNCTIONS[func_name](**call_args)
1515
+ utim_tools, tool_functions = get_tools()
1516
+ result = tool_functions[func_name](**call_args)
1512
1517
  except Exception as exc:
1513
1518
  result = f"Error executing {func_name}: {exc}"
1514
1519
  self.tool_results.append({
@@ -1560,7 +1565,13 @@ class Orchestrator:
1560
1565
  except Exception:
1561
1566
  pass
1562
1567
 
1563
- result = TOOL_FUNCTIONS[func_name](**call_args)
1568
+ try:
1569
+ utim_tools, tool_functions = get_tools()
1570
+ result = tool_functions[func_name](**call_args)
1571
+ return result
1572
+ except Exception as e:
1573
+ import traceback
1574
+ result = f"Error executing tool {func_name}: {e}\n{traceback.format_exc()}"
1564
1575
 
1565
1576
  # Restore the indicator to evaluating logic
1566
1577
  try: