utim-cli 1.44.2__tar.gz → 1.44.4__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.2/utim_cli.egg-info → utim_cli-1.44.4}/PKG-INFO +2 -1
  2. {utim_cli-1.44.2 → utim_cli-1.44.4}/pyproject.toml +2 -1
  3. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/agent.py +13 -13
  4. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/auth.py +11 -2
  5. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/blender_agent.py +29 -8
  6. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/config.py +97 -35
  7. utim_cli-1.44.4/utim_cli/constants.py +13 -0
  8. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/context_pruner.py +2 -1
  9. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/orchestrator.py +25 -14
  10. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/reflection.py +2 -1
  11. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/tools.py +144 -66
  12. utim_cli-1.44.4/utim_cli/tui/__init__.py +1 -0
  13. utim_cli-1.44.4/utim_cli/tui/history_dialog.py +242 -0
  14. utim_cli-1.44.4/utim_cli/tui/mcp_dialog.py +417 -0
  15. utim_cli-1.44.4/utim_cli/tui/model_dialog.py +1235 -0
  16. utim_cli-1.44.4/utim_cli/tui/resume_dialog.py +137 -0
  17. utim_cli-1.44.4/utim_cli/tui/tools_dialog.py +122 -0
  18. utim_cli-1.44.4/utim_cli/tui/update_dialog.py +152 -0
  19. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/utim.py +121 -2131
  20. {utim_cli-1.44.2 → utim_cli-1.44.4/utim_cli.egg-info}/PKG-INFO +2 -1
  21. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli.egg-info/SOURCES.txt +9 -2
  22. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli.egg-info/requires.txt +1 -0
  23. utim_cli-1.44.2/utim_cli/billing.py +0 -40
  24. {utim_cli-1.44.2 → utim_cli-1.44.4}/CHANGELOG.md +0 -0
  25. {utim_cli-1.44.2 → utim_cli-1.44.4}/LICENSE +0 -0
  26. {utim_cli-1.44.2 → utim_cli-1.44.4}/MANIFEST.in +0 -0
  27. {utim_cli-1.44.2 → utim_cli-1.44.4}/README.md +0 -0
  28. {utim_cli-1.44.2 → utim_cli-1.44.4}/setup.cfg +0 -0
  29. {utim_cli-1.44.2 → utim_cli-1.44.4}/setup.py +0 -0
  30. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/__init__.py +0 -0
  31. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/backup.py +0 -0
  32. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/bootstrap.py +0 -0
  33. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/client_utils.py +0 -0
  34. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/doctor.py +0 -0
  35. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/knowledge_graph.py +0 -0
  36. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/logger.py +0 -0
  37. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/mcp_clean_wrapper.py +0 -0
  38. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/mcp_client.py +0 -0
  39. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/mcp_registry.json +0 -0
  40. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/report.py +0 -0
  41. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/scrapy_search.py +0 -0
  42. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/share.py +0 -0
  43. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/share_tui.py +0 -0
  44. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/situational_scoring.py +0 -0
  45. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/state.py +0 -0
  46. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/vector_memory.py +0 -0
  47. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli/workspace.py +0 -0
  48. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli.egg-info/dependency_links.txt +0 -0
  49. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli.egg-info/entry_points.txt +0 -0
  50. {utim_cli-1.44.2 → utim_cli-1.44.4}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: utim-cli
3
- Version: 1.44.2
3
+ Version: 1.44.4
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.2"
7
+ version = "1.44.4"
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,49 @@
1
1
  import json
2
- import shutil
2
+ import logging
3
3
  import os
4
4
  import pathlib
5
+ import shutil
5
6
  from typing import Any, Dict, Optional
6
7
 
8
+ logger = logging.getLogger("utim.config")
9
+
7
10
  class Config:
8
11
  def __init__(self):
9
12
  self.global_dir = pathlib.Path.home() / ".utim"
10
13
  self.local_dir = pathlib.Path(".utim").resolve()
11
- self._data: Dict[str, Any] = self._load()
14
+ self._global_data: Dict[str, Any] = {}
15
+ self._local_data: Dict[str, Any] = {}
16
+ self._load()
12
17
 
13
- def _load(self) -> Dict[str, Any]:
14
- data = {}
18
+ def _load(self):
15
19
  # Load global config
16
20
  global_config = self.global_dir / "config.json"
17
21
  if global_config.exists():
18
22
  try:
19
23
  with open(global_config, "r", encoding="utf-8") as f:
20
- data.update(json.load(f))
21
- except Exception:
22
- pass
24
+ self._global_data = json.load(f)
25
+ except json.JSONDecodeError as e:
26
+ logger.warning(f"Global config corrupted at {global_config}: {e}. Using defaults.")
27
+ except Exception as e:
28
+ logger.error(f"Cannot read global config: {e}")
23
29
 
24
30
  # Load local config if exists
25
31
  local_config = self.local_dir / "config.json"
26
32
  if local_config.exists():
27
33
  try:
28
34
  with open(local_config, "r", encoding="utf-8") as f:
29
- data.update(json.load(f))
30
- except Exception:
31
- pass
32
- return data
35
+ self._local_data = json.load(f)
36
+ except json.JSONDecodeError as e:
37
+ logger.warning(f"Local config corrupted at {local_config}: {e}. Using defaults.")
38
+ except Exception as e:
39
+ logger.error(f"Cannot read local config: {e}")
40
+
41
+ @property
42
+ def _data(self) -> Dict[str, Any]:
43
+ """Returns a merged view (local overrides global)."""
44
+ merged = dict(self._global_data)
45
+ merged.update(self._local_data)
46
+ return merged
33
47
 
34
48
  def save(self):
35
49
  # Save to global config directory
@@ -37,24 +51,30 @@ class Config:
37
51
  global_config = self.global_dir / "config.json"
38
52
  try:
39
53
  with open(global_config, "w", encoding="utf-8") as f:
40
- json.dump(self._data, f, indent=4)
41
- except Exception:
42
- pass
54
+ json.dump(self._global_data, f, indent=4)
55
+ except Exception as e:
56
+ logger.error(f"Cannot write global config: {e}")
43
57
 
44
- # Save to local config directory if it exists
45
- if self.local_dir.exists():
58
+ # Save to local config directory if we have local data
59
+ if self._local_data:
60
+ self.local_dir.mkdir(parents=True, exist_ok=True)
46
61
  local_config = self.local_dir / "config.json"
47
62
  try:
48
63
  with open(local_config, "w", encoding="utf-8") as f:
49
- json.dump(self._data, f, indent=4)
50
- except Exception:
51
- pass
64
+ json.dump(self._local_data, f, indent=4)
65
+ except Exception as e:
66
+ logger.error(f"Cannot write local config: {e}")
52
67
 
53
68
  def get(self, key: str, default: Any = None) -> Any:
69
+ if key == "disabled_tools" and "disabled_tools" not in self._data:
70
+ return ["blender_create_object", "blender_agent_create_from_image"]
54
71
  return self._data.get(key, default)
55
72
 
56
- def set(self, key: str, value: Any):
57
- self._data[key] = value
73
+ def set(self, key: str, value: Any, local: bool = False):
74
+ if local:
75
+ self._local_data[key] = value
76
+ else:
77
+ self._global_data[key] = value
58
78
  self.save()
59
79
 
60
80
  @property
@@ -70,7 +90,8 @@ class Config:
70
90
  return self.get("name")
71
91
 
72
92
  def clear(self):
73
- self._data = {}
93
+ self._global_data = {}
94
+ self._local_data = {}
74
95
  self.save()
75
96
 
76
97
  # ── Custom / Bring-Your-Own-Model support ─────────────────────────────────
@@ -92,14 +113,28 @@ class Config:
92
113
 
93
114
  @custom_models.setter
94
115
  def custom_models(self, value: list):
95
- self._data["custom_models"] = value
96
- self.save()
116
+ self.set("custom_models", value)
97
117
 
98
118
  def add_custom_model(self, entry: dict) -> None:
99
119
  """Append or replace a custom model entry keyed on model_id."""
100
120
  models = self.custom_models
101
121
  models = [m for m in models if m.get("model_id") != entry["model_id"]]
102
- models.append(entry)
122
+
123
+ api_key = entry.get("api_key")
124
+ use_keyring = False
125
+ if api_key:
126
+ try:
127
+ import keyring
128
+ keyring.set_password("utim_cli", entry["model_id"], api_key)
129
+ use_keyring = True
130
+ except Exception as e:
131
+ logger.warning(f"Keyring not available, storing key in plain text: {e}")
132
+
133
+ entry_to_save = dict(entry)
134
+ if use_keyring:
135
+ entry_to_save.pop("api_key", None)
136
+
137
+ models.append(entry_to_save)
103
138
  self.custom_models = models
104
139
 
105
140
  def remove_custom_model(self, model_id: str) -> bool:
@@ -108,26 +143,55 @@ class Config:
108
143
  new = [m for m in models if m.get("model_id") != model_id]
109
144
  if len(new) == len(models):
110
145
  return False
146
+
147
+ try:
148
+ import keyring
149
+ keyring.delete_password("utim_cli", model_id)
150
+ except Exception:
151
+ pass
152
+
111
153
  self.custom_models = new
112
154
  return True
113
155
 
114
156
  def remove_custom_provider(self, provider_name: str, base_url: str) -> int:
115
157
  """Remove all custom models associated with a provider. Returns count removed."""
116
158
  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:
159
+ new = []
160
+ removed = 0
161
+ try:
162
+ import keyring
163
+ has_keyring = True
164
+ except ImportError:
165
+ has_keyring = False
166
+
167
+ for m in models:
168
+ if m.get("provider_name") == provider_name and m.get("base_url") == base_url:
169
+ removed += 1
170
+ if has_keyring:
171
+ try:
172
+ keyring.delete_password("utim_cli", m["model_id"])
173
+ except Exception:
174
+ pass
175
+ else:
176
+ new.append(m)
177
+
178
+ if removed > 0:
123
179
  self.custom_models = new
124
- return removed_count
180
+ return removed
125
181
 
126
182
  def get_custom_model(self, model_id: str) -> Optional[dict]:
127
183
  """Return the custom model entry for *model_id*, or None."""
128
184
  for m in self.custom_models:
129
185
  if m.get("model_id") == model_id:
130
- return m
186
+ m_ret = dict(m)
187
+ try:
188
+ import keyring
189
+ key = keyring.get_password("utim_cli", model_id)
190
+ if key:
191
+ m_ret["api_key"] = key
192
+ except Exception:
193
+ pass
194
+ return m_ret
131
195
  return None
132
196
 
133
197
  @property
@@ -147,8 +211,6 @@ class Config:
147
211
  return [m.strip() for m in models.split(",")]
148
212
  return [
149
213
  "poolside/laguna-m.1:free",
150
- "qwen/qwen3-coder:free",
151
- "nvidia/nemotron-3-ultra-550b-a55b:free",
152
214
  "openrouter/free"
153
215
  ]
154
216
 
@@ -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
 
@@ -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:
@@ -13,6 +13,7 @@ import sqlite3
13
13
  from typing import Dict, List, Optional
14
14
  from datetime import datetime
15
15
  import requests
16
+ from utim_cli.constants import DEFAULT_MODEL
16
17
 
17
18
 
18
19
  MEMORY_FILE = ".utim_tmp/task_reflections.json"
@@ -74,7 +75,7 @@ Return ONLY a JSON object with the following structure:
74
75
  resp = requests.post(
75
76
  apiUrl,
76
77
  json={
77
- "model": "cohere/north-mini-code:free",
78
+ "model": DEFAULT_MODEL,
78
79
  "messages": [
79
80
  {"role": "system", "content": "You are a reflection engine. Extract technical learnings, logical corrections, and time adaptations from completed work."},
80
81
  {"role": "user", "content": prompt}