utim-cli 1.47.4__tar.gz → 1.47.7__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 (89) hide show
  1. {utim_cli-1.47.4 → utim_cli-1.47.7}/CHANGELOG.md +21 -0
  2. {utim_cli-1.47.4/utim_cli.egg-info → utim_cli-1.47.7}/PKG-INFO +1 -1
  3. {utim_cli-1.47.4 → utim_cli-1.47.7}/pyproject.toml +2 -2
  4. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/__init__.py +1 -1
  5. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/_version.py +1 -1
  6. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/bootstrap.py +1 -0
  7. utim_cli-1.47.7/utim_cli/mcp_client.py +343 -0
  8. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/orchestrator.py +71 -47
  9. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/reflection.py +500 -202
  10. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/utim.py +101 -9
  11. {utim_cli-1.47.4 → utim_cli-1.47.7/utim_cli.egg-info}/PKG-INFO +1 -1
  12. utim_cli-1.47.7/utim_cli.egg-info/entry_points.txt +2 -0
  13. utim_cli-1.47.4/utim_cli/mcp_client.py +0 -210
  14. utim_cli-1.47.4/utim_cli.egg-info/entry_points.txt +0 -2
  15. {utim_cli-1.47.4 → utim_cli-1.47.7}/LICENSE +0 -0
  16. {utim_cli-1.47.4 → utim_cli-1.47.7}/MANIFEST.in +0 -0
  17. {utim_cli-1.47.4 → utim_cli-1.47.7}/README.md +0 -0
  18. {utim_cli-1.47.4 → utim_cli-1.47.7}/setup.cfg +0 -0
  19. {utim_cli-1.47.4 → utim_cli-1.47.7}/setup.py +0 -0
  20. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/agent.py +0 -0
  21. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/auth.py +0 -0
  22. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/backup.py +0 -0
  23. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/blender_agent.py +0 -0
  24. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/client_utils.py +0 -0
  25. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/config.py +0 -0
  26. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/constants.py +0 -0
  27. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/context_pruner.py +0 -0
  28. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/doctor.py +0 -0
  29. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/knowledge_graph.py +0 -0
  30. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/local_db.py +0 -0
  31. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/logger.py +0 -0
  32. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/mcp_clean_wrapper.py +0 -0
  33. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/mcp_registry.json +0 -0
  34. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/report.py +0 -0
  35. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/scrapy_search.py +0 -0
  36. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/__init__.py +0 -0
  37. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/auth.py +0 -0
  38. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/db.py +0 -0
  39. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/docs_md/about.md +0 -0
  40. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/docs_md/changelog.md +0 -0
  41. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/docs_md/docs.md +0 -0
  42. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/docs_md/features.md +0 -0
  43. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/docs_md/license.md +0 -0
  44. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/docs_md/pricing.md +0 -0
  45. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/docs_md/privacy.md +0 -0
  46. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/docs_md/refund.md +0 -0
  47. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/docs_md/support.md +0 -0
  48. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/docs_md/terms.md +0 -0
  49. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/email_utils.py +0 -0
  50. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/exchange_rate.py +0 -0
  51. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/firebase.py +0 -0
  52. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/history.py +0 -0
  53. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/logging_config.py +0 -0
  54. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/models.py +0 -0
  55. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/pricing_updater.py +0 -0
  56. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/rate_limit.py +0 -0
  57. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/router.py +0 -0
  58. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/routes/__init__.py +0 -0
  59. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/routes/auth_routes.py +0 -0
  60. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/routes/completion_routes.py +0 -0
  61. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/routes/credit_routes.py +0 -0
  62. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/routes/feedback_routes.py +0 -0
  63. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/routes/quota_routes.py +0 -0
  64. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/routes/referral_routes.py +0 -0
  65. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/routes/session_routes.py +0 -0
  66. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/routes/share_routes.py +0 -0
  67. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/server/server.py +0 -0
  68. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/share.py +0 -0
  69. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/share_tui.py +0 -0
  70. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/situational_scoring.py +0 -0
  71. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/state.py +0 -0
  72. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/tools.py +0 -0
  73. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/tui/__init__.py +0 -0
  74. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/tui/feedback_dialog.py +0 -0
  75. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/tui/history_dialog.py +0 -0
  76. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/tui/mcp_dialog.py +0 -0
  77. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/tui/model_dialog.py +0 -0
  78. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/tui/quota_dialog.py +0 -0
  79. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/tui/resume_dialog.py +0 -0
  80. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/tui/skills_dialog.py +0 -0
  81. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/tui/tools_dialog.py +0 -0
  82. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/tui/update_dialog.py +0 -0
  83. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/utilities.py +0 -0
  84. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/vector_memory.py +0 -0
  85. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli/workspace.py +0 -0
  86. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli.egg-info/SOURCES.txt +0 -0
  87. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli.egg-info/dependency_links.txt +0 -0
  88. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli.egg-info/requires.txt +0 -0
  89. {utim_cli-1.47.4 → utim_cli-1.47.7}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.47.7] - 2026-07-20
4
+
5
+ ### Added
6
+ - **Android Termux Pure-Python Architecture**: Implemented pure-Python line-delimited JSON-RPC stdio client fallback (`PurePythonMCPSession`) for Model Context Protocol (MCP) servers, enabling 100% feature parity on Android Termux without requiring Rust compilation or `pydantic-core`.
7
+ - **Web Support Assistant Disclaimer & Installation Flow**: Added glassmorphism notice bar and 4-step installation visual guide on landing website to distinguish web support chat from local CLI agent.
8
+ - **Enhanced Cognitive Reflection Pipeline**: Replaced single-turn random skill creation with a 5-request interaction buffering engine. Analyzes full interaction trajectories across turns (detecting silent tool calling and file format assumption complaints like image PDFs vs text PDFs), with strict usefulness filtering to prevent storing junk data.
9
+ - **RAG-Based Skill Creation & Intelligent Hold-Back Architecture**: Implemented RAG skill synthesis over ChromaDB Vector Memory. Enforces a minimum threshold ($\ge 3$ accumulated experiences across sessions), an LLM usability/cohesion evaluation (`is_sufficiently_usable`), and cooldown stride tracking to hold back skill creation until technical patterns are mature and genuinely usable.
10
+
11
+ ### Fixed
12
+ - **Device Sign-In Unauthenticated Flow**: Fixed device authorization flow on `https://utim.dev/activate?code=XXXX-XXXX`. Auto-redirects unauthenticated users to `/auth` with a countdown while preserving the device code, bouncing them back to `/activate` automatically after sign-in to complete terminal authorization without broken loops.
13
+
14
+ ## [1.47.6] - 2026-07-19
15
+
16
+ ### Added
17
+ - **Asynchronous Local LLM Setup**: Moved the local Qwen model downloading and setup pipeline into a background daemon thread, allowing UTIM to run completions instantly via Cloud API during initial setup/installation without blocking user queries.
18
+
19
+ ## [1.47.5] - 2026-07-19
20
+
21
+ ### Optimized
22
+ - **Instant startup / rules summarization**: Prioritized Cloud API completion calls (via UTIM completions server or direct OpenRouter) over local Qwen LLM execution in `summarize_experiences_with_model`. This completely eliminates the 1m 49s latency caused by importing PyTorch/Transformers and loading the 0.5B model from scratch, falling back to local execution only in offline mode.
23
+
3
24
  ## [1.47.4] - 2026-07-19
4
25
 
5
26
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 1.47.4
3
+ Version: 1.47.7
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "1.47.4"
7
+ version = "1.47.7"
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"
@@ -79,7 +79,7 @@ legacy-agent = [
79
79
  ]
80
80
 
81
81
  [project.scripts]
82
- utim = "utim_cli.utim:app"
82
+ utim = "utim_cli.utim:main_cli_entry"
83
83
 
84
84
  [tool.setuptools.packages.find]
85
85
  include = ["utim_cli*"]
@@ -5,7 +5,7 @@ try:
5
5
  from utim_cli._version import VERSION as __version__
6
6
  except ImportError:
7
7
  # Fallback during first-install / editable-install edge cases
8
- __version__ = "1.47.4"
8
+ __version__ = "1.47.7"
9
9
 
10
10
  import builtins, os, pathlib, urllib.parse, re
11
11
 
@@ -11,4 +11,4 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "1.47.4"
14
+ VERSION = "1.47.7"
@@ -4,6 +4,7 @@ Auto-creates the .utim folder structure, custom skills, and local database on fi
4
4
  """
5
5
 
6
6
  import os
7
+ from pathlib import Path
7
8
  from utim_cli.config import get_utim_dir
8
9
 
9
10
  UTIM_DIR = get_utim_dir()
@@ -0,0 +1,343 @@
1
+ import os
2
+ import json
3
+ import asyncio
4
+ import threading
5
+ from typing import Dict, List, Any, Optional
6
+
7
+ try:
8
+ from mcp import ClientSession, StdioServerParameters
9
+ from mcp.client.stdio import stdio_client
10
+ HAS_MCP_LIB = True
11
+ except ImportError:
12
+ HAS_MCP_LIB = False
13
+
14
+ # =========================================================================
15
+ # Pure-Python Stdio MCP Client Fallback (Zero C/Rust/pydantic-core dependency)
16
+ # =========================================================================
17
+ class PurePythonMCPTool:
18
+ def __init__(self, name: str, description: str, inputSchema: dict):
19
+ self.name = name
20
+ self.description = description
21
+ self.inputSchema = inputSchema
22
+
23
+ class PurePythonMCPContent:
24
+ def __init__(self, text: str):
25
+ self.text = text
26
+
27
+ class PurePythonMCPResult:
28
+ def __init__(self, content: list, isError: bool = False):
29
+ self.content = content
30
+ self.isError = isError
31
+
32
+ class PurePythonMCPSession:
33
+ """Pure-Python line-delimited JSON-RPC stdio client for MCP servers."""
34
+ def __init__(self, command: str, args: list, env: dict):
35
+ self.command = command
36
+ self.args = args
37
+ self.env = env
38
+ self.proc = None
39
+ self._req_id = 0
40
+
41
+ def _next_id(self):
42
+ self._req_id += 1
43
+ return self._req_id
44
+
45
+ async def start(self):
46
+ import sys
47
+ import shutil
48
+ current_dir = os.path.dirname(os.path.abspath(__file__))
49
+ wrapper_path = os.path.join(current_dir, "mcp_clean_wrapper.py")
50
+ resolved_command = shutil.which(self.command) or self.command
51
+
52
+ cmd_list = [sys.executable, "-u", wrapper_path, resolved_command] + self.args
53
+ self.proc = await asyncio.create_subprocess_exec(
54
+ *cmd_list,
55
+ stdin=asyncio.subprocess.PIPE,
56
+ stdout=asyncio.subprocess.PIPE,
57
+ stderr=asyncio.subprocess.PIPE,
58
+ env=self.env
59
+ )
60
+
61
+ async def _send_request(self, method: str, params: dict) -> dict:
62
+ if not self.proc or not self.proc.stdin:
63
+ raise RuntimeError("MCP process stdin not open")
64
+ req_id = self._next_id()
65
+ payload = json.dumps({
66
+ "jsonrpc": "2.0",
67
+ "id": req_id,
68
+ "method": method,
69
+ "params": params
70
+ }) + "\n"
71
+ self.proc.stdin.write(payload.encode("utf-8"))
72
+ await self.proc.stdin.drain()
73
+
74
+ while True:
75
+ line = await self.proc.stdout.readline()
76
+ if not line:
77
+ raise RuntimeError("MCP server process output ended unexpectedly.")
78
+ line_str = line.decode("utf-8", errors="ignore").strip()
79
+ if not line_str:
80
+ continue
81
+ try:
82
+ msg = json.loads(line_str)
83
+ if msg.get("id") == req_id:
84
+ if "error" in msg:
85
+ err_msg = msg["error"].get("message", "MCP RPC Error")
86
+ raise RuntimeError(err_msg)
87
+ return msg.get("result", {})
88
+ except json.JSONDecodeError:
89
+ continue
90
+
91
+ async def _send_notification(self, method: str, params: dict):
92
+ if not self.proc or not self.proc.stdin:
93
+ return
94
+ payload = json.dumps({
95
+ "jsonrpc": "2.0",
96
+ "method": method,
97
+ "params": params
98
+ }) + "\n"
99
+ self.proc.stdin.write(payload.encode("utf-8"))
100
+ await self.proc.stdin.drain()
101
+
102
+ async def initialize(self):
103
+ await self._send_request("initialize", {
104
+ "protocolVersion": "2024-11-05",
105
+ "capabilities": {},
106
+ "clientInfo": {"name": "utim", "version": "1.0"}
107
+ })
108
+ await self._send_notification("notifications/initialized", {})
109
+
110
+ async def list_tools(self):
111
+ res = await self._send_request("tools/list", {})
112
+ raw_tools = res.get("tools", [])
113
+ tools = []
114
+ for t in raw_tools:
115
+ tools.append(PurePythonMCPTool(
116
+ name=t.get("name", ""),
117
+ description=t.get("description", ""),
118
+ inputSchema=t.get("inputSchema", {})
119
+ ))
120
+ class ToolListResult:
121
+ def __init__(self, t_list):
122
+ self.tools = t_list
123
+ return ToolListResult(tools)
124
+
125
+ async def call_tool(self, name: str, arguments: dict):
126
+ res = await self._send_request("tools/call", {
127
+ "name": name,
128
+ "arguments": arguments
129
+ })
130
+ is_error = res.get("isError", False)
131
+ raw_content = res.get("content", [])
132
+ contents = []
133
+ for c in raw_content:
134
+ if isinstance(c, dict) and "text" in c:
135
+ contents.append(PurePythonMCPContent(c["text"]))
136
+ elif isinstance(c, str):
137
+ contents.append(PurePythonMCPContent(c))
138
+ if not contents:
139
+ contents.append(PurePythonMCPContent(json.dumps(res)))
140
+ return PurePythonMCPResult(contents, isError=is_error)
141
+
142
+ async def close(self):
143
+ if self.proc:
144
+ try:
145
+ self.proc.terminate()
146
+ await self.proc.wait()
147
+ except Exception:
148
+ pass
149
+
150
+
151
+ class MCPManager:
152
+ def __init__(self):
153
+ self.sessions: Dict[str, Any] = {}
154
+ self._exit_stacks = {}
155
+ self._tool_to_server: Dict[str, str] = {}
156
+ self._started = False
157
+ self.cached_tools: List[Dict[str, Any]] = []
158
+ self.server_tools: Dict[str, List[str]] = {} # server_name -> list of tool names
159
+ self._loop = None
160
+ self._thread = None
161
+
162
+ def start_loop(self):
163
+ if self._loop is None:
164
+ self._loop = asyncio.new_event_loop()
165
+ self._thread = threading.Thread(target=self._run_loop, daemon=True)
166
+ self._thread.start()
167
+
168
+ def _run_loop(self):
169
+ asyncio.set_event_loop(self._loop)
170
+ self._loop.run_forever()
171
+
172
+ def run_coro(self, coro):
173
+ self.start_loop()
174
+ future = asyncio.run_coroutine_threadsafe(coro, self._loop)
175
+ return future.result()
176
+
177
+ def start(self):
178
+ if self._started:
179
+ return
180
+
181
+ from utim_cli.config import get_utim_dir
182
+ config_path = os.path.abspath(os.path.join(get_utim_dir(), "mcp.json"))
183
+ if not os.path.exists(config_path):
184
+ os.makedirs(os.path.dirname(config_path), exist_ok=True)
185
+ with open(config_path, "w") as f:
186
+ json.dump({"mcpServers": {}}, f, indent=2)
187
+
188
+ self.start_loop()
189
+ self.run_coro(self._start_async())
190
+
191
+ async def _start_async(self):
192
+ from utim_cli.config import get_utim_dir
193
+ config_path = os.path.abspath(os.path.join(get_utim_dir(), "mcp.json"))
194
+ if not os.path.exists(config_path):
195
+ os.makedirs(os.path.dirname(config_path), exist_ok=True)
196
+ with open(config_path, "w") as f:
197
+ json.dump({"mcpServers": {}}, f, indent=2)
198
+ self._started = True
199
+ return
200
+
201
+ try:
202
+ with open(config_path, "r") as f:
203
+ config = json.load(f)
204
+ except Exception:
205
+ self._started = True
206
+ return
207
+
208
+ servers = config.get("mcpServers") or {}
209
+ if not isinstance(servers, dict):
210
+ servers = {}
211
+
212
+ for name, srv_config in servers.items():
213
+ command = srv_config.get("command")
214
+ args = srv_config.get("args", [])
215
+ env = srv_config.get("env", {})
216
+
217
+ if not command:
218
+ continue
219
+
220
+ cleaned_args = []
221
+ for arg in args:
222
+ if isinstance(arg, str):
223
+ if (arg.startswith('"') and arg.endswith('"')) or (arg.startswith("'") and arg.endswith("'")):
224
+ arg = arg[1:-1]
225
+ cleaned_args.append(arg)
226
+
227
+ try:
228
+ merged_env = os.environ.copy()
229
+ merged_env.update(env)
230
+
231
+ if HAS_MCP_LIB:
232
+ import sys
233
+ import shutil
234
+ current_dir = os.path.dirname(os.path.abspath(__file__))
235
+ wrapper_path = os.path.join(current_dir, "mcp_clean_wrapper.py")
236
+ resolved_command = shutil.which(command) or command
237
+
238
+ server_params = StdioServerParameters(
239
+ command=sys.executable,
240
+ args=["-u", wrapper_path, resolved_command] + cleaned_args,
241
+ env=merged_env
242
+ )
243
+
244
+ from contextlib import AsyncExitStack
245
+ stack = AsyncExitStack()
246
+ self._exit_stacks[name] = stack
247
+
248
+ read, write = await stack.enter_async_context(stdio_client(server_params))
249
+ session = await stack.enter_async_context(ClientSession(read, write))
250
+ await asyncio.wait_for(session.initialize(), timeout=60.0)
251
+ self.sessions[name] = session
252
+ else:
253
+ # Pure-Python stdio client fallback (Android Termux / minimal installs)
254
+ session = PurePythonMCPSession(command, cleaned_args, merged_env)
255
+ await session.start()
256
+ await asyncio.wait_for(session.initialize(), timeout=60.0)
257
+ self.sessions[name] = session
258
+
259
+ # Retrieve tools from the server and cache them
260
+ try:
261
+ res = await session.list_tools()
262
+ self.server_tools[name] = []
263
+ for t in res.tools:
264
+ full_name = f"{name}__{t.name}"
265
+ self._tool_to_server[full_name] = name
266
+ self.server_tools[name].append(t.name)
267
+
268
+ self.cached_tools.append({
269
+ "type": "function",
270
+ "function": {
271
+ "name": full_name,
272
+ "description": f"[{name}] {t.description}",
273
+ "parameters": t.inputSchema
274
+ }
275
+ })
276
+ except Exception as e:
277
+ print(f"Error fetching tools from MCP server {name}: {e}")
278
+
279
+ except Exception as e:
280
+ print(f"Error starting MCP server {name}: {e}")
281
+
282
+ self._started = True
283
+
284
+ def get_tools(self) -> List[Dict[str, Any]]:
285
+ """Synchronously returns the cached list of MCP tools."""
286
+ return self.cached_tools
287
+
288
+ def get_notification_context(self) -> str:
289
+ """Returns the context notification string for connected MCP servers."""
290
+ from utim_cli.config import config
291
+ disabled = config.get("disabled_tools") or []
292
+ if not isinstance(disabled, list):
293
+ disabled = []
294
+ notifications = []
295
+ for server_name, tool_names in self.server_tools.items():
296
+ enabled_tool_names = [t for t in tool_names if f"{server_name}__{t}" not in disabled]
297
+ if enabled_tool_names:
298
+ tools_str = ", ".join(enabled_tool_names)
299
+ notifications.append(f"this mcp server {server_name} is connected and a new set of tools are available for u: {tools_str}")
300
+ return "\n".join(notifications)
301
+
302
+ def call_tool(self, server_name: str, tool_name: str, arguments: Dict[str, Any]) -> str:
303
+ """Synchronously routes tool call execution to the background thread."""
304
+ return self.run_coro(self._call_tool_async(server_name, tool_name, arguments))
305
+
306
+ async def _call_tool_async(self, server_name: str, tool_name: str, arguments: Dict[str, Any]) -> str:
307
+ if server_name not in self.sessions:
308
+ return f"Error: MCP server '{server_name}' not running."
309
+
310
+ session = self.sessions[server_name]
311
+ try:
312
+ res = await session.call_tool(tool_name, arguments)
313
+ if getattr(res, "isError", False):
314
+ return f"Error from {server_name}: " + "\n".join(c.text for c in res.content)
315
+ return "\n".join(c.text for c in res.content)
316
+ except Exception as e:
317
+ return f"Error calling {tool_name} on {server_name}: {str(e)}"
318
+
319
+ def restart(self):
320
+ """Synchronously restarts and reloads all MCP sessions."""
321
+ self.run_coro(self._restart_async())
322
+
323
+ async def _restart_async(self):
324
+ for name, stack in list(self._exit_stacks.items()):
325
+ try:
326
+ await stack.aclose()
327
+ except Exception:
328
+ pass
329
+ for name, session in list(self.sessions.items()):
330
+ if hasattr(session, "close"):
331
+ try:
332
+ await session.close()
333
+ except Exception:
334
+ pass
335
+ self.sessions.clear()
336
+ self._exit_stacks.clear()
337
+ self._tool_to_server.clear()
338
+ self.cached_tools.clear()
339
+ self.server_tools.clear()
340
+ self._started = False
341
+ await self._start_async()
342
+
343
+ mcp_manager = MCPManager()
@@ -350,6 +350,42 @@ def compress_experience(text: str) -> str:
350
350
 
351
351
  _qwen_model = None
352
352
  _qwen_tokenizer = None
353
+ _qwen_downloading = False
354
+
355
+ def _trigger_qwen_download_bg():
356
+ """Trigger background download and caching of Qwen local model if not already loaded or downloading."""
357
+ global _qwen_model, _qwen_tokenizer, _qwen_downloading
358
+ if _qwen_model is None and not _qwen_downloading:
359
+ import sys
360
+ if "pytest" in sys.modules:
361
+ return
362
+
363
+ _qwen_downloading = True
364
+ import threading
365
+
366
+ def download_model_bg():
367
+ global _qwen_model, _qwen_tokenizer, _qwen_downloading
368
+ try:
369
+ import torch
370
+ from transformers import AutoModelForCausalLM, AutoTokenizer
371
+ model_id = "Qwen/Qwen2.5-0.5B-Instruct"
372
+ tok = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True, local_files_only=False)
373
+ mod = AutoModelForCausalLM.from_pretrained(
374
+ model_id,
375
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
376
+ device_map="auto",
377
+ trust_remote_code=True,
378
+ local_files_only=False
379
+ )
380
+ _qwen_tokenizer = tok
381
+ _qwen_model = mod
382
+ except Exception:
383
+ pass
384
+ finally:
385
+ _qwen_downloading = False
386
+
387
+ t = threading.Thread(target=download_model_bg, daemon=True)
388
+ t.start()
353
389
 
354
390
  def summarize_experiences_with_model(experiences: list[dict], existing_cache: str = "") -> str:
355
391
  """Summarize list of experiences into a clean list of rules under 2000 tokens using local Qwen model first, falling back to LLM API."""
@@ -409,25 +445,40 @@ def summarize_experiences_with_model(experiences: list[dict], existing_cache: st
409
445
  "Output ONLY the clean markdown list of rules (do not include introductory or concluding text):"
410
446
  )
411
447
 
412
- # 1. Attempt local execution using Qwen2.5-0.5B-Instruct
413
- try:
414
- import sys
415
- if "pytest" not in sys.modules:
416
- global _qwen_model, _qwen_tokenizer
448
+ # Proactively trigger Qwen local model download in a background thread if needed
449
+ _trigger_qwen_download_bg()
450
+
451
+ # 1. Attempt Cloud API completions (via UTIM completions server or direct OpenRouter)
452
+ import os
453
+ api_key = config.get("api_key")
454
+ llm_key = os.environ.get("OPENROUTER_API_KEY") or os.environ.get("UTIM_API_KEY")
455
+
456
+ if api_key or llm_key:
457
+ from utim_cli.reflection import REFLECTION_MODELS
458
+ from utim_cli.client_utils import proxy_openrouter_request
459
+
460
+ for model in REFLECTION_MODELS:
461
+ try:
462
+ resp = proxy_openrouter_request({
463
+ "model": model,
464
+ "messages": [
465
+ {"role": "system", "content": "You are a context compressor. Output rules list directly."},
466
+ {"role": "user", "content": prompt}
467
+ ],
468
+ "max_tokens": 1000
469
+ })
470
+ if resp.status_code == 200:
471
+ content = resp.json()["choices"][0]["message"]["content"].strip()
472
+ if content:
473
+ return content
474
+ except Exception:
475
+ continue
476
+
477
+ # 2. Fallback to local execution using Qwen2.5-0.5B-Instruct if background loader has completed
478
+ global _qwen_model, _qwen_tokenizer
479
+ if _qwen_model is not None:
480
+ try:
417
481
  import torch
418
- from transformers import AutoModelForCausalLM, AutoTokenizer
419
-
420
- model_id = "Qwen/Qwen2.5-0.5B-Instruct"
421
-
422
- if _qwen_model is None:
423
- _qwen_tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
424
- _qwen_model = AutoModelForCausalLM.from_pretrained(
425
- model_id,
426
- torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
427
- device_map="auto",
428
- trust_remote_code=True
429
- )
430
-
431
482
  messages = [
432
483
  {"role": "system", "content": "You are a context compressor. Output rules list directly."},
433
484
  {"role": "user", "content": prompt}
@@ -454,36 +505,9 @@ def summarize_experiences_with_model(experiences: list[dict], existing_cache: st
454
505
  response = _qwen_tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0].strip()
455
506
  if response:
456
507
  return response
457
-
458
- except Exception:
459
- pass
460
-
461
- # 2. Fallback to OpenRouter Cloud API
462
- import os
463
- llm_key = os.environ.get("OPENROUTER_API_KEY") or os.environ.get("UTIM_API_KEY")
464
- if not llm_key:
465
- return existing_cache or "\n".join(raw_texts[:15])
466
-
467
- from utim_cli.reflection import REFLECTION_MODELS
468
- from utim_cli.client_utils import proxy_openrouter_request
469
-
470
- for model in REFLECTION_MODELS:
471
- try:
472
- resp = proxy_openrouter_request({
473
- "model": model,
474
- "messages": [
475
- {"role": "system", "content": "You are a context compressor. Output rules list directly."},
476
- {"role": "user", "content": prompt}
477
- ],
478
- "max_tokens": 1000
479
- })
480
- if resp.status_code == 200:
481
- content = resp.json()["choices"][0]["message"]["content"].strip()
482
- if content:
483
- return content
484
508
  except Exception:
485
- continue
486
-
509
+ pass
510
+
487
511
  return existing_cache or "\n".join(raw_texts[:15])
488
512
 
489
513
  def update_experience_summary_cache(user_prompt: str) -> str: