utim-cli 2.0.1__tar.gz → 2.0.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.
- {utim_cli-2.0.1 → utim_cli-2.0.3}/CHANGELOG.md +10 -0
- {utim_cli-2.0.1/utim_cli.egg-info → utim_cli-2.0.3}/PKG-INFO +1 -1
- {utim_cli-2.0.1 → utim_cli-2.0.3}/pyproject.toml +1 -1
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/__init__.py +1 -1
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/_version.py +1 -1
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/orchestrator.py +44 -187
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tools.py +170 -28
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/marketplace_dialog.py +4185 -4180
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/model_dialog.py +40 -36
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/utim.py +3 -3
- {utim_cli-2.0.1 → utim_cli-2.0.3/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.0.1 → utim_cli-2.0.3}/LICENSE +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/MANIFEST.in +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/README.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/setup.cfg +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/setup.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/_check_obj.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/_find_mcp.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/_find_plan.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/_find_tool.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/_patch_multimodal.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/_test_regex.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/agent.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/auth.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/backup.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/billing.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/client_utils.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/config.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/constants.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/context_pruner.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/doctor.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/harbor.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/local_db.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/logger.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/reflection.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/report.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/db.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/history.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/models.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/router.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/routes/completion_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/server.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/share.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/state.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/utilities.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli/workspace.py +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli.egg-info/SOURCES.txt +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.0.1 → utim_cli-2.0.3}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.3] - 2026-08-04
|
|
4
|
+
|
|
5
|
+
### 🛠️ Fixes
|
|
6
|
+
- **Fixed `Wrong color format 'dim'` crash in marketplace TUI**: `prompt_toolkit` does not accept the `dim` modifier as a free-floating token before a color, nor `class:dim` as a literal inline style. All `f"dim {_MUTED}"` / `f"dim {_FG}"` style strings in `utim_cli/tui/marketplace_dialog.py` have been rewritten to `f"fg:{_MUTED}"` / `f"fg:{_FG}"`, and the `[dim]…[/dim]` markup parser now drops the `dim` token instead of emitting the unsafe `class:dim` class string. `/marketplace` no longer crashes on Windows when rendering info/hint/status rows.
|
|
7
|
+
|
|
8
|
+
## [2.0.2] - 2026-08-04
|
|
9
|
+
|
|
10
|
+
### 🛠️ Fixes
|
|
11
|
+
- Minor bug fixes.
|
|
12
|
+
|
|
3
13
|
## [2.0.1] - 2026-08-04 - Bugfix & Adaptive Iteration Budget
|
|
4
14
|
|
|
5
15
|
### 🛠️ Fixes
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "utim-cli"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.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"
|
|
@@ -3429,6 +3429,7 @@ class Orchestrator:
|
|
|
3429
3429
|
results[orig_idx] = (tc, "[Aborted by user]")
|
|
3430
3430
|
continue
|
|
3431
3431
|
_tools_module._cancel_event = self.cancel_event
|
|
3432
|
+
_tools_module._active_model_id = self.model_id
|
|
3432
3433
|
result = self._execute_tool_timed(tc)
|
|
3433
3434
|
self._current_line_len = 0
|
|
3434
3435
|
results[orig_idx] = (tc, result)
|
|
@@ -3684,81 +3685,6 @@ class Orchestrator:
|
|
|
3684
3685
|
self._persist_messages()
|
|
3685
3686
|
return res
|
|
3686
3687
|
|
|
3687
|
-
def _filter_active_tools(self, all_tools: List[Dict], messages: List[Dict]) -> List[Dict]:
|
|
3688
|
-
"""Semantic Intent Tool Masking with Self-Healing Task Re-hydration.
|
|
3689
|
-
|
|
3690
|
-
Core coding tools and MCP tools are ALWAYS active. Specialized tools
|
|
3691
|
-
(analyze_image, generate_image, plan_project, create_skill, background_tasks)
|
|
3692
|
-
are activated via broad semantic intent detection (synonyms, implicit requests,
|
|
3693
|
-
file paths) and persistently retained once activated during the task session.
|
|
3694
|
-
"""
|
|
3695
|
-
if not all_tools:
|
|
3696
|
-
return all_tools
|
|
3697
|
-
|
|
3698
|
-
if not hasattr(self, "_activated_specialized_tools"):
|
|
3699
|
-
self._activated_specialized_tools = set()
|
|
3700
|
-
|
|
3701
|
-
CORE_TOOLS = {
|
|
3702
|
-
"read_file", "write_file", "edit_file", "move_file", "delete_file",
|
|
3703
|
-
"run_command", "list_directory", "grep_search", "search", "search_codebase",
|
|
3704
|
-
"recall_experience", "store_experience", "manage_memory"
|
|
3705
|
-
}
|
|
3706
|
-
|
|
3707
|
-
# Broad semantic intent patterns (handles slang, implicit desires, multi-language phrasing)
|
|
3708
|
-
SEMANTIC_INTENT_PATTERNS = {
|
|
3709
|
-
"analyze_image": [
|
|
3710
|
-
r"\.(png|jpg|jpeg|webp|gif|bmp|svg|tiff|ico)\b",
|
|
3711
|
-
r"(?i)\b(image|img|screenshot|photo|picture|diagram|fig|figure|ui|mockup|wireframe|visual|inspect|look at|see|show|crop|icon|logo|asset)\b"
|
|
3712
|
-
],
|
|
3713
|
-
"generate_image": [
|
|
3714
|
-
r"(?i)\b(generate|draw|create|paint|render|make|design|build)\b.*\b(image|picture|photo|illustration|graphic|logo|banner|art|ui|mockup|icon|asset|cover|poster|avatar)\b",
|
|
3715
|
-
r"(?i)\b(text to image|image gen|sd|flux|dall-e|midjourney|prompt expander)\b"
|
|
3716
|
-
],
|
|
3717
|
-
"plan_project": [
|
|
3718
|
-
r"(?i)\b(plan|architect|roadmap|milestone|phases|breakdown|strategy|design system|structure repo|project structure|todo list|task list|multi-step|workflow|steps)\b",
|
|
3719
|
-
r"(?i)\b(how should (i|we)|what is the best (way|architecture|structure)|organize this)\b"
|
|
3720
|
-
],
|
|
3721
|
-
"create_skill": [
|
|
3722
|
-
r"(?i)\b(skill|custom tool|reusable prompt|workflow template|save routine|workspace rule|automation|agent skill)\b"
|
|
3723
|
-
],
|
|
3724
|
-
"background_tasks": [
|
|
3725
|
-
r"(?i)\b(background|task|bg|daemon|long running|process|job|async|monitor|poll|kill task)\b"
|
|
3726
|
-
]
|
|
3727
|
-
}
|
|
3728
|
-
|
|
3729
|
-
# Collect text across recent messages (user + assistant reasoning + tool outputs)
|
|
3730
|
-
recent_text_parts = []
|
|
3731
|
-
for m in (messages[-8:] if messages else []):
|
|
3732
|
-
content = m.get("content")
|
|
3733
|
-
if isinstance(content, str):
|
|
3734
|
-
recent_text_parts.append(content)
|
|
3735
|
-
elif isinstance(content, list):
|
|
3736
|
-
for part in content:
|
|
3737
|
-
if isinstance(part, dict) and part.get("type") == "text":
|
|
3738
|
-
recent_text_parts.append(part.get("text", ""))
|
|
3739
|
-
|
|
3740
|
-
combined_text = "\n".join(recent_text_parts)
|
|
3741
|
-
|
|
3742
|
-
# 1. Semantic intent detection
|
|
3743
|
-
for tool_name, patterns in SEMANTIC_INTENT_PATTERNS.items():
|
|
3744
|
-
for pat in patterns:
|
|
3745
|
-
if re.search(pat, combined_text):
|
|
3746
|
-
self._activated_specialized_tools.add(tool_name)
|
|
3747
|
-
break
|
|
3748
|
-
|
|
3749
|
-
# 2. Re-hydrate if iteration count >= 3 (complex multi-step task automatically gets plan_project)
|
|
3750
|
-
if getattr(self, "current_iteration", 0) >= 3:
|
|
3751
|
-
self._activated_specialized_tools.add("plan_project")
|
|
3752
|
-
|
|
3753
|
-
# 3. Filter all_tools
|
|
3754
|
-
filtered = []
|
|
3755
|
-
for t in all_tools:
|
|
3756
|
-
name = t.get("function", {}).get("name", "")
|
|
3757
|
-
if name in CORE_TOOLS or name not in SEMANTIC_INTENT_PATTERNS or name in self._activated_specialized_tools:
|
|
3758
|
-
filtered.append(t)
|
|
3759
|
-
|
|
3760
|
-
return filtered
|
|
3761
|
-
|
|
3762
3688
|
# ── Context compression ──────────────────────────────────────────────────
|
|
3763
3689
|
|
|
3764
3690
|
@staticmethod
|
|
@@ -4909,30 +4835,6 @@ class Orchestrator:
|
|
|
4909
4835
|
# No extra newline here - let the next block handle it
|
|
4910
4836
|
pass
|
|
4911
4837
|
|
|
4912
|
-
# ── Self-Healing Re-Activation Gate ──────────────────────────────
|
|
4913
|
-
# If native tool_calls are empty and the model expresses inability to find a tool,
|
|
4914
|
-
# or mentions a specialized tool by name, immediately unmask ALL specialized tools
|
|
4915
|
-
# and re-prompt the model in 0ms so it NEVER gets stuck or stops.
|
|
4916
|
-
SPECIALIZED_TOOLS_ALL = {"analyze_image", "generate_image", "plan_project", "create_skill", "background_tasks"}
|
|
4917
|
-
if not tool_calls and content and not _attempted_tool_call:
|
|
4918
|
-
content_lower = content.lower()
|
|
4919
|
-
mentioned_specialized = {t for t in SPECIALIZED_TOOLS_ALL if t in content_lower}
|
|
4920
|
-
lacks_tool_signal = any(phrase in content_lower for phrase in [
|
|
4921
|
-
"don't have", "do not have", "cannot access", "no tool", "tool is missing",
|
|
4922
|
-
"unable to generate", "unable to analyze", "not available", "don't see"
|
|
4923
|
-
])
|
|
4924
|
-
if mentioned_specialized or lacks_tool_signal:
|
|
4925
|
-
if not hasattr(self, "_activated_specialized_tools"):
|
|
4926
|
-
self._activated_specialized_tools = set()
|
|
4927
|
-
self._activated_specialized_tools.update(SPECIALIZED_TOOLS_ALL)
|
|
4928
|
-
|
|
4929
|
-
self.messages.append({"role": "assistant", "content": content})
|
|
4930
|
-
self.messages.append({
|
|
4931
|
-
"role": "user",
|
|
4932
|
-
"content": "[SYSTEM DIRECTIVE] All specialized tools (analyze_image, generate_image, plan_project, create_skill, background_tasks) have been fully unmasked and loaded into your active tool schema. Please re-run immediately using the required tool."
|
|
4933
|
-
})
|
|
4934
|
-
continue
|
|
4935
|
-
|
|
4936
4838
|
# Parse text-based tool calls fallback if native tool calls are empty
|
|
4937
4839
|
if not tool_calls and content:
|
|
4938
4840
|
parsed_calls = []
|
|
@@ -5520,6 +5422,11 @@ class Orchestrator:
|
|
|
5520
5422
|
+ result[-tail_chars:]
|
|
5521
5423
|
+ (f"\n\n{_line_hint}" if _line_hint else "")
|
|
5522
5424
|
)
|
|
5425
|
+
|
|
5426
|
+
# Clear any stale vision images from a previous iteration before
|
|
5427
|
+
# this batch of tools runs (defensive — shouldn't normally be set).
|
|
5428
|
+
_tools_module._pending_vision_images.clear()
|
|
5429
|
+
|
|
5523
5430
|
# Execute tools in parallel when beneficial
|
|
5524
5431
|
if len(tool_calls) > 1:
|
|
5525
5432
|
# Use parallel execution for multiple tools
|
|
@@ -5536,50 +5443,14 @@ class Orchestrator:
|
|
|
5536
5443
|
func_name = tc.get("function", {}).get("name", "")
|
|
5537
5444
|
result = _cap_tool_result(func_name, result)
|
|
5538
5445
|
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
_multimodal = _tool_result_to_multimodal(result, _img_path)
|
|
5548
|
-
self.messages.append(
|
|
5549
|
-
{
|
|
5550
|
-
"role": "tool",
|
|
5551
|
-
"tool_call_id": tc_id,
|
|
5552
|
-
"name": func_name,
|
|
5553
|
-
"content": _multimodal,
|
|
5554
|
-
}
|
|
5555
|
-
)
|
|
5556
|
-
else:
|
|
5557
|
-
self.messages.append(
|
|
5558
|
-
{
|
|
5559
|
-
"role": "tool",
|
|
5560
|
-
"tool_call_id": tc_id,
|
|
5561
|
-
"name": func_name,
|
|
5562
|
-
"content": result,
|
|
5563
|
-
}
|
|
5564
|
-
)
|
|
5565
|
-
except Exception:
|
|
5566
|
-
self.messages.append(
|
|
5567
|
-
{
|
|
5568
|
-
"role": "tool",
|
|
5569
|
-
"tool_call_id": tc_id,
|
|
5570
|
-
"name": func_name,
|
|
5571
|
-
"content": result,
|
|
5572
|
-
}
|
|
5573
|
-
)
|
|
5574
|
-
else:
|
|
5575
|
-
self.messages.append(
|
|
5576
|
-
{
|
|
5577
|
-
"role": "tool",
|
|
5578
|
-
"tool_call_id": tc_id,
|
|
5579
|
-
"name": func_name,
|
|
5580
|
-
"content": result,
|
|
5581
|
-
}
|
|
5582
|
-
)
|
|
5446
|
+
self.messages.append(
|
|
5447
|
+
{
|
|
5448
|
+
"role": "tool",
|
|
5449
|
+
"tool_call_id": tc_id,
|
|
5450
|
+
"name": func_name,
|
|
5451
|
+
"content": result,
|
|
5452
|
+
}
|
|
5453
|
+
)
|
|
5583
5454
|
else:
|
|
5584
5455
|
# Single tool - execute directly
|
|
5585
5456
|
for tc in tool_calls:
|
|
@@ -5588,55 +5459,41 @@ class Orchestrator:
|
|
|
5588
5459
|
|
|
5589
5460
|
func_name = tc.get("function", {}).get("name", "")
|
|
5590
5461
|
_tools_module._cancel_event = self.cancel_event
|
|
5462
|
+
_tools_module._active_model_id = self.model_id
|
|
5591
5463
|
result = self._execute_tool_timed(tc)
|
|
5592
5464
|
self._current_line_len = 0
|
|
5593
5465
|
tc_id = tc.get("id") or str(tc.get("index", "0"))
|
|
5594
5466
|
result = _cap_tool_result(func_name, result)
|
|
5595
5467
|
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
"role": "tool",
|
|
5626
|
-
"tool_call_id": tc_id,
|
|
5627
|
-
"name": func_name,
|
|
5628
|
-
"content": result,
|
|
5629
|
-
}
|
|
5630
|
-
)
|
|
5631
|
-
else:
|
|
5632
|
-
self.messages.append(
|
|
5633
|
-
{
|
|
5634
|
-
"role": "tool",
|
|
5635
|
-
"tool_call_id": tc_id,
|
|
5636
|
-
"name": func_name,
|
|
5637
|
-
"content": result,
|
|
5638
|
-
}
|
|
5639
|
-
)
|
|
5468
|
+
self.messages.append(
|
|
5469
|
+
{
|
|
5470
|
+
"role": "tool",
|
|
5471
|
+
"tool_call_id": tc_id,
|
|
5472
|
+
"name": func_name,
|
|
5473
|
+
"content": result,
|
|
5474
|
+
}
|
|
5475
|
+
)
|
|
5476
|
+
|
|
5477
|
+
# ── Vision image injection ────────────────────────────────────────
|
|
5478
|
+
# read_file encodes image bytes and pushes them to
|
|
5479
|
+
# _tools_module._pending_vision_images so they bypass the tool-result
|
|
5480
|
+
# string pipeline entirely. Drain the queue here (after ALL tools in
|
|
5481
|
+
# this iteration have run) and inject each image as a "user" message
|
|
5482
|
+
# — the only role that providers universally accept image_url content in.
|
|
5483
|
+
_vision_queue = getattr(_tools_module, "_pending_vision_images", [])
|
|
5484
|
+
if _vision_queue:
|
|
5485
|
+
for _vimg in list(_vision_queue):
|
|
5486
|
+
self.messages.append(
|
|
5487
|
+
{
|
|
5488
|
+
"role": "user",
|
|
5489
|
+
"content": [
|
|
5490
|
+
{"type": "text", "text": f"[Image from read_file: {_vimg['meta']}]"},
|
|
5491
|
+
{"type": "image_url", "image_url": {"url": f"data:{_vimg['mime']};base64,{_vimg['b64']}"}},
|
|
5492
|
+
],
|
|
5493
|
+
}
|
|
5494
|
+
)
|
|
5495
|
+
_vision_queue.clear()
|
|
5496
|
+
|
|
5640
5497
|
# --- Repeated Tool Failure Loop Detection ---
|
|
5641
5498
|
if not hasattr(self, "_tool_failure_counts"):
|
|
5642
5499
|
self._tool_failure_counts = {}
|
|
@@ -159,27 +159,36 @@ def resolve_project_path(filepath: str) -> str:
|
|
|
159
159
|
_VISION_MODELS_CACHE = None
|
|
160
160
|
|
|
161
161
|
def is_model_vision_capable(model_id: str = None) -> bool:
|
|
162
|
-
"""Determine if a model is vision-capable based on models.txt
|
|
162
|
+
"""Determine if a model is vision-capable (Vision: True/False) based on MODEL_REGISTRY and models.txt input_modalities."""
|
|
163
163
|
global _VISION_MODELS_CACHE
|
|
164
164
|
if not model_id:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
# Prefer the slot injected by the orchestrator; fall back to config as last resort.
|
|
166
|
+
model_id = _active_model_id or ""
|
|
167
|
+
if not model_id:
|
|
168
|
+
try:
|
|
169
|
+
from utim_cli.config import config
|
|
170
|
+
model_id = config.get("main_model") or config.get("model") or ""
|
|
171
|
+
except Exception:
|
|
172
|
+
model_id = ""
|
|
170
173
|
|
|
171
|
-
model_id_str = str(model_id).
|
|
174
|
+
model_id_str = str(model_id).strip()
|
|
175
|
+
if not model_id_str:
|
|
176
|
+
return False # unknown model → treat as non-vision (safe default)
|
|
172
177
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
model_id_lower = model_id_str.lower()
|
|
179
|
+
|
|
180
|
+
# 1. Check server MODEL_REGISTRY first
|
|
181
|
+
try:
|
|
182
|
+
from utim_cli.server.models import MODEL_REGISTRY
|
|
183
|
+
if model_id_str in MODEL_REGISTRY:
|
|
184
|
+
return MODEL_REGISTRY[model_id_str].vision
|
|
185
|
+
for k, entry in MODEL_REGISTRY.items():
|
|
186
|
+
if k.lower() == model_id_lower:
|
|
187
|
+
return entry.vision
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
181
190
|
|
|
182
|
-
#
|
|
191
|
+
# 2. Build / query models.txt cache (authoritative list from OpenRouter metadata)
|
|
183
192
|
if _VISION_MODELS_CACHE is None:
|
|
184
193
|
_VISION_MODELS_CACHE = {}
|
|
185
194
|
try:
|
|
@@ -190,15 +199,27 @@ def is_model_vision_capable(model_id: str = None) -> bool:
|
|
|
190
199
|
models_txt_path = "models.txt"
|
|
191
200
|
if os.path.exists(models_txt_path):
|
|
192
201
|
with open(models_txt_path, "r", encoding="utf-8") as f:
|
|
193
|
-
data = json.load(f)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
data = json.load(f)
|
|
203
|
+
items = data.get("data", []) if isinstance(data, dict) else data
|
|
204
|
+
for m in items:
|
|
205
|
+
mid = m.get("id", "")
|
|
206
|
+
if not mid:
|
|
207
|
+
continue
|
|
208
|
+
in_mods = m.get("architecture", {}).get("input_modalities", []) or []
|
|
209
|
+
_VISION_MODELS_CACHE[mid.lower()] = ("image" in in_mods)
|
|
198
210
|
except Exception:
|
|
199
211
|
_VISION_MODELS_CACHE = {}
|
|
200
212
|
|
|
201
|
-
|
|
213
|
+
if model_id_lower in _VISION_MODELS_CACHE:
|
|
214
|
+
return _VISION_MODELS_CACHE[model_id_lower]
|
|
215
|
+
|
|
216
|
+
# 3. Fallback for custom / unlisted model names
|
|
217
|
+
vision_keywords = ["vision", "-vl", "llava", "pixtral", "image", "multimodal", "gpt-4o", "gemini", "claude-3", "claude-4", "claude-5", "qwen3"]
|
|
218
|
+
if any(kw in model_id_lower for kw in vision_keywords):
|
|
219
|
+
if not any(non_v in model_id_lower for non_v in ["text-only", "coder-air", "kat-coder"]):
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
return False
|
|
202
223
|
|
|
203
224
|
|
|
204
225
|
def _extract_file_symbols(filepath: str, file_content: str) -> list:
|
|
@@ -305,15 +326,125 @@ def read_file(filepath: str, start_line: int = None, end_line: int = None, symbo
|
|
|
305
326
|
ext = _os.path.splitext(filepath)[1].lower()
|
|
306
327
|
image_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp'}
|
|
307
328
|
if ext in image_extensions:
|
|
308
|
-
#
|
|
309
|
-
# description that clutters the tool indicator. Non-vision models still
|
|
310
|
-
# need the description as their only way to "see" the image.
|
|
329
|
+
# Collect image metadata (size, dimensions) for the text portion of the response.
|
|
311
330
|
try:
|
|
312
|
-
|
|
313
|
-
|
|
331
|
+
file_size = _os.path.getsize(filepath)
|
|
332
|
+
size_kb = file_size / 1024.0
|
|
333
|
+
size_str = f"{size_kb:.1f} KB" if size_kb < 1024 else f"{size_kb/1024:.2f} MB"
|
|
334
|
+
except Exception:
|
|
335
|
+
size_str = "unknown size"
|
|
336
|
+
|
|
337
|
+
meta = f"[Image: {_os.path.basename(filepath)} | {ext.lstrip('.').upper()} | {size_str} | URI: {_make_file_uri(filepath)}]"
|
|
338
|
+
|
|
339
|
+
# Try to also read image dimensions (pure stdlib where possible, else Pillow)
|
|
340
|
+
dim_str = ""
|
|
341
|
+
try:
|
|
342
|
+
with open(filepath, "rb") as _f:
|
|
343
|
+
_head = _f.read(64)
|
|
344
|
+
# PNG: width@16, height@20 (big-endian uint32)
|
|
345
|
+
if _head.startswith(b"\x89PNG\r\n\x1a\n") and len(_head) >= 24:
|
|
346
|
+
import struct
|
|
347
|
+
w, h = struct.unpack(">II", _head[16:24])
|
|
348
|
+
dim_str = f" | {w}x{h}px"
|
|
349
|
+
# GIF: width@6, height@8 (little-endian uint16)
|
|
350
|
+
elif _head.startswith(b"GIF8") and len(_head) >= 10:
|
|
351
|
+
import struct
|
|
352
|
+
w, h = struct.unpack("<HH", _head[6:10])
|
|
353
|
+
dim_str = f" | {w}x{h}px"
|
|
354
|
+
# JPEG: parse SOF marker to find dimensions
|
|
355
|
+
elif _head.startswith(b"\xff\xd8\xff"):
|
|
356
|
+
import struct
|
|
357
|
+
_f2 = open(filepath, "rb")
|
|
358
|
+
try:
|
|
359
|
+
soi = _f2.read(2)
|
|
360
|
+
while True:
|
|
361
|
+
marker = _f2.read(2)
|
|
362
|
+
if len(marker) < 2:
|
|
363
|
+
break
|
|
364
|
+
if marker[0] != 0xFF:
|
|
365
|
+
break
|
|
366
|
+
# Skip SOI / EOI / RST markers
|
|
367
|
+
if marker[1] in (0xD8, 0xD9) or 0xD0 <= marker[1] <= 0xD7:
|
|
368
|
+
continue
|
|
369
|
+
seg_len_bytes = _f2.read(2)
|
|
370
|
+
if len(seg_len_bytes) < 2:
|
|
371
|
+
break
|
|
372
|
+
seg_len = struct.unpack(">H", seg_len_bytes)[0]
|
|
373
|
+
# SOFn markers (Start of Frame): 0xC0-0xCF except 0xC4,0xC8,0xCC
|
|
374
|
+
if 0xC0 <= marker[1] <= 0xCF and marker[1] not in (0xC4, 0xC8, 0xCC):
|
|
375
|
+
_f2.read(1) # precision
|
|
376
|
+
h, w = struct.unpack(">HH", _f2.read(4))
|
|
377
|
+
dim_str = f" | {w}x{h}px"
|
|
378
|
+
break
|
|
379
|
+
else:
|
|
380
|
+
_f2.read(seg_len - 2)
|
|
381
|
+
finally:
|
|
382
|
+
_f2.close()
|
|
383
|
+
# WebP: 'RIFF....WEBP' then VP8 / VP8L / VP8X chunk
|
|
384
|
+
elif _head.startswith(b"RIFF") and _head[8:12] == b"WEBP":
|
|
385
|
+
with open(filepath, "rb") as _f3:
|
|
386
|
+
_f3.read(12)
|
|
387
|
+
chunk_hdr = _f3.read(4)
|
|
388
|
+
if chunk_hdr == b"VP8 ":
|
|
389
|
+
_f3.read(10) # frame tag + start code
|
|
390
|
+
w = int.from_bytes(_f3.read(2), "little") & 0x3FFF
|
|
391
|
+
h = int.from_bytes(_f3.read(2), "little") & 0x3FFF
|
|
392
|
+
dim_str = f" | {w}x{h}px"
|
|
393
|
+
elif chunk_hdr == b"VP8L":
|
|
394
|
+
_f3.read(1) # signature byte
|
|
395
|
+
b1, b2, b3, b4 = _f3.read(4)
|
|
396
|
+
w = ((b2 & 0x3F) << 8 | b1) + 1
|
|
397
|
+
h = (((b4 & 0x0F) << 10) | (b3 << 2) | ((b2 & 0xC0) >> 6)) + 1
|
|
398
|
+
dim_str = f" | {w}x{h}px"
|
|
399
|
+
elif chunk_hdr == b"VP8X":
|
|
400
|
+
_f3.read(8)
|
|
401
|
+
w = int.from_bytes(_f3.read(3), "little") + 1
|
|
402
|
+
h = int.from_bytes(_f3.read(3), "little") + 1
|
|
403
|
+
dim_str = f" | {w}x{h}px"
|
|
314
404
|
except Exception:
|
|
315
405
|
pass
|
|
316
|
-
|
|
406
|
+
|
|
407
|
+
meta += dim_str
|
|
408
|
+
|
|
409
|
+
# Route image to model based on vision capability:
|
|
410
|
+
#
|
|
411
|
+
# Vision-capable model → encode bytes here, push to _pending_vision_images.
|
|
412
|
+
# The orchestrator drains this queue after all tools complete and injects a
|
|
413
|
+
# "user" message with the raw base64 image so the model truly sees it.
|
|
414
|
+
# (image_url blocks are NOT supported inside "tool" role messages by most
|
|
415
|
+
# providers — they must live in a "user" message.)
|
|
416
|
+
#
|
|
417
|
+
# Non-vision model → call analyze_image() in the background and return its
|
|
418
|
+
# text description so the model has useful context.
|
|
419
|
+
if is_model_vision_capable():
|
|
420
|
+
import base64 as _b64mod, mimetypes as _mtmod
|
|
421
|
+
_mime_v, _ = _mtmod.guess_type(filepath)
|
|
422
|
+
_ext_v = _os.path.splitext(filepath)[1].lower()
|
|
423
|
+
if not _mime_v or not _mime_v.startswith("image/"):
|
|
424
|
+
_mime_v = f"image/{_ext_v[1:]}"
|
|
425
|
+
if _ext_v == ".jpg":
|
|
426
|
+
_mime_v = "image/jpeg"
|
|
427
|
+
try:
|
|
428
|
+
with open(filepath, "rb") as _fv:
|
|
429
|
+
_b64_v = _b64mod.b64encode(_fv.read()).decode("utf-8")
|
|
430
|
+
# Push to queue — orchestrator injects the user message after tools complete
|
|
431
|
+
_pending_vision_images.append({"meta": meta, "b64": _b64_v, "mime": _mime_v})
|
|
432
|
+
return meta # return clean metadata text only; no sentinel needed
|
|
433
|
+
except Exception as _venc_err:
|
|
434
|
+
# Encoding failed — fall through to analyze_image text fallback below
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
# Non-vision model (or encoding failed): describe image via external vision sub-model.
|
|
438
|
+
try:
|
|
439
|
+
description = analyze_image(
|
|
440
|
+
filepath,
|
|
441
|
+
"Identify and describe this image in detail, listing any people, actors, characters, "
|
|
442
|
+
"text, UI elements, colors, visual layout, code or terminal output shown, and any "
|
|
443
|
+
"visible error messages or stack traces verbatim."
|
|
444
|
+
)
|
|
445
|
+
return f"{meta}\n{description}"
|
|
446
|
+
except Exception as _e2:
|
|
447
|
+
return f"{meta}\n(Vision description unavailable: {_e2})"
|
|
317
448
|
|
|
318
449
|
try:
|
|
319
450
|
with open(filepath, "rb") as f:
|
|
@@ -967,6 +1098,17 @@ def edit_file(filepath: str, old_str: str = None, new_str: str = None, replaceme
|
|
|
967
1098
|
# threading.Event here before each tool call so run_command can be aborted.
|
|
968
1099
|
_cancel_event = None # type: threading.Event | None
|
|
969
1100
|
|
|
1101
|
+
# Active model ID slot — injected by the orchestrator before each tool call
|
|
1102
|
+
# (same pattern as _cancel_event). read_file reads this to decide whether to
|
|
1103
|
+
# push image bytes to _pending_vision_images or call analyze_image instead.
|
|
1104
|
+
_active_model_id: str = ""
|
|
1105
|
+
|
|
1106
|
+
# Queue for vision images: read_file pushes here when the active model is
|
|
1107
|
+
# vision-capable; the orchestrator drains it after each tool-call batch and
|
|
1108
|
+
# injects a "user" message containing the raw image bytes so the model can
|
|
1109
|
+
# actually see the image (most providers reject image_url in "tool" messages).
|
|
1110
|
+
_pending_vision_images: list = [] # list of {"meta": str, "b64": str, "mime": str}
|
|
1111
|
+
|
|
970
1112
|
# ─── Intelligent Sandbox mode ──────────────────────────────────────────────────
|
|
971
1113
|
# Set to True via --sandbox CLI flag. When active, run_command uses static analysis
|
|
972
1114
|
# to check commands for safety. Risky commands are blocked unless approved.
|