utim-cli 1.44.2__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.2/utim_cli.egg-info → utim_cli-1.44.3}/PKG-INFO +2 -1
  2. {utim_cli-1.44.2 → utim_cli-1.44.3}/pyproject.toml +2 -1
  3. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/agent.py +13 -13
  4. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/auth.py +11 -2
  5. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/blender_agent.py +29 -8
  6. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/config.py +94 -35
  7. utim_cli-1.44.3/utim_cli/constants.py +13 -0
  8. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/context_pruner.py +2 -1
  9. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/orchestrator.py +25 -14
  10. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/reflection.py +2 -1
  11. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/tools.py +144 -66
  12. utim_cli-1.44.3/utim_cli/tui/__init__.py +1 -0
  13. utim_cli-1.44.3/utim_cli/tui/history_dialog.py +242 -0
  14. utim_cli-1.44.3/utim_cli/tui/mcp_dialog.py +417 -0
  15. utim_cli-1.44.3/utim_cli/tui/model_dialog.py +1235 -0
  16. utim_cli-1.44.3/utim_cli/tui/resume_dialog.py +137 -0
  17. utim_cli-1.44.3/utim_cli/tui/tools_dialog.py +112 -0
  18. utim_cli-1.44.3/utim_cli/tui/update_dialog.py +152 -0
  19. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/utim.py +121 -2131
  20. {utim_cli-1.44.2 → utim_cli-1.44.3/utim_cli.egg-info}/PKG-INFO +2 -1
  21. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli.egg-info/SOURCES.txt +9 -2
  22. {utim_cli-1.44.2 → utim_cli-1.44.3}/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.3}/CHANGELOG.md +0 -0
  25. {utim_cli-1.44.2 → utim_cli-1.44.3}/LICENSE +0 -0
  26. {utim_cli-1.44.2 → utim_cli-1.44.3}/MANIFEST.in +0 -0
  27. {utim_cli-1.44.2 → utim_cli-1.44.3}/README.md +0 -0
  28. {utim_cli-1.44.2 → utim_cli-1.44.3}/setup.cfg +0 -0
  29. {utim_cli-1.44.2 → utim_cli-1.44.3}/setup.py +0 -0
  30. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/__init__.py +0 -0
  31. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/backup.py +0 -0
  32. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/bootstrap.py +0 -0
  33. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/client_utils.py +0 -0
  34. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/doctor.py +0 -0
  35. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/knowledge_graph.py +0 -0
  36. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/logger.py +0 -0
  37. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/mcp_clean_wrapper.py +0 -0
  38. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/mcp_client.py +0 -0
  39. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/mcp_registry.json +0 -0
  40. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/report.py +0 -0
  41. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/scrapy_search.py +0 -0
  42. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/share.py +0 -0
  43. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/share_tui.py +0 -0
  44. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/situational_scoring.py +0 -0
  45. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/state.py +0 -0
  46. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/vector_memory.py +0 -0
  47. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli/workspace.py +0 -0
  48. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli.egg-info/dependency_links.txt +0 -0
  49. {utim_cli-1.44.2 → utim_cli-1.44.3}/utim_cli.egg-info/entry_points.txt +0 -0
  50. {utim_cli-1.44.2 → utim_cli-1.44.3}/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.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.2"
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
 
@@ -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}