utim-cli 1.0.0__tar.gz → 1.43.9__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-1.0.0/utim_cli.egg-info → utim_cli-1.43.9}/PKG-INFO +3 -3
- {utim_cli-1.0.0 → utim_cli-1.43.9}/pyproject.toml +75 -75
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/orchestrator.py +1 -14
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/share.py +171 -63
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/share_tui.py +153 -39
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/utim.py +282 -10
- {utim_cli-1.0.0 → utim_cli-1.43.9/utim_cli.egg-info}/PKG-INFO +3 -3
- {utim_cli-1.0.0 → utim_cli-1.43.9}/CHANGELOG.md +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/LICENSE +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/MANIFEST.in +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/README.md +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/setup.cfg +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/setup.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/__init__.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/agent.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/auth.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/backup.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/billing.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/blender_agent.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/bootstrap.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/client_utils.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/config.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/context_pruner.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/doctor.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/logger.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/mcp_client.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/reflection.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/report.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/state.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/tools.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/vector_memory.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/workspace.py +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli.egg-info/SOURCES.txt +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.43.9
|
|
4
4
|
Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
|
|
5
5
|
License: MIT
|
|
6
|
-
Project-URL: Homepage, https://utim.
|
|
7
|
-
Project-URL: Documentation, https://utim.
|
|
6
|
+
Project-URL: Homepage, https://utim.dev
|
|
7
|
+
Project-URL: Documentation, https://utim.dev/docs
|
|
8
8
|
Project-URL: Changelog, https://github.com/emendai/utim/blob/main/CHANGELOG.md
|
|
9
9
|
Project-URL: Issues, https://github.com/emendai/utim/issues
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "utim-cli"
|
|
7
|
-
version = "1.
|
|
8
|
-
description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.9"
|
|
11
|
-
license = { text = "MIT" }
|
|
12
|
-
classifiers = [
|
|
13
|
-
"Programming Language :: Python :: 3",
|
|
14
|
-
"Programming Language :: Python :: 3.9",
|
|
15
|
-
"Programming Language :: Python :: 3.10",
|
|
16
|
-
"Programming Language :: Python :: 3.11",
|
|
17
|
-
"Programming Language :: Python :: 3.12",
|
|
18
|
-
"License :: OSI Approved :: MIT License",
|
|
19
|
-
"Operating System :: OS Independent",
|
|
20
|
-
"Development Status :: 4 - Beta",
|
|
21
|
-
"Environment :: Console",
|
|
22
|
-
"Intended Audience :: Developers",
|
|
23
|
-
]
|
|
24
|
-
dependencies = [
|
|
25
|
-
"openai>=1.30.0",
|
|
26
|
-
"requests==2.31.0",
|
|
27
|
-
"aiohttp>=3.9.5",
|
|
28
|
-
"python-dotenv==1.0.1",
|
|
29
|
-
"urllib3==1.26.18",
|
|
30
|
-
"charset-normalizer==3.3.2",
|
|
31
|
-
"chardet==4.0.0",
|
|
32
|
-
"typer>=0.9.0",
|
|
33
|
-
"rich>=13.0.0",
|
|
34
|
-
"prompt_toolkit>=3.0.0",
|
|
35
|
-
"tree-sitter>=0.21.0",
|
|
36
|
-
"tree-sitter-python>=0.21.0",
|
|
37
|
-
"tree-sitter-javascript>=0.21.0",
|
|
38
|
-
"tree-sitter-typescript>=0.21.0",
|
|
39
|
-
"mcp>=0.1.0",
|
|
40
|
-
"nest-asyncio>=1.5.0",
|
|
41
|
-
"python-multipart>=0.0.7",
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
[project.urls]
|
|
45
|
-
Homepage = "https://utim.
|
|
46
|
-
Documentation = "https://utim.
|
|
47
|
-
Changelog = "https://github.com/emendai/utim/blob/main/CHANGELOG.md"
|
|
48
|
-
Issues = "https://github.com/emendai/utim/issues"
|
|
49
|
-
|
|
50
|
-
[project.optional-dependencies]
|
|
51
|
-
# Optional features
|
|
52
|
-
search = [
|
|
53
|
-
"scrapy>=2.11.0",
|
|
54
|
-
"scrapy-playwright>=0.0.34",
|
|
55
|
-
"beautifulsoup4>=4.12.0",
|
|
56
|
-
]
|
|
57
|
-
images = [
|
|
58
|
-
"pillow>=9.0.0",
|
|
59
|
-
]
|
|
60
|
-
full = [
|
|
61
|
-
"scrapy>=2.11.0",
|
|
62
|
-
"scrapy-playwright>=0.0.34",
|
|
63
|
-
"beautifulsoup4>=4.12.0",
|
|
64
|
-
"pillow>=9.0.0",
|
|
65
|
-
]
|
|
66
|
-
|
|
67
|
-
[project.scripts]
|
|
68
|
-
utim = "utim_cli.utim:app"
|
|
69
|
-
|
|
70
|
-
[tool.setuptools.packages.find]
|
|
71
|
-
include = ["utim_cli*"]
|
|
72
|
-
exclude = ["utim_cli.server*", "tests*", "landing*", "scripts*"]
|
|
73
|
-
|
|
74
|
-
[tool.setuptools.package-data]
|
|
75
|
-
utim_cli = ["mcp_registry.json"]
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "utim-cli"
|
|
7
|
+
version = "1.43.9"
|
|
8
|
+
description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.9",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Environment :: Console",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"openai>=1.30.0",
|
|
26
|
+
"requests==2.31.0",
|
|
27
|
+
"aiohttp>=3.9.5",
|
|
28
|
+
"python-dotenv==1.0.1",
|
|
29
|
+
"urllib3==1.26.18",
|
|
30
|
+
"charset-normalizer==3.3.2",
|
|
31
|
+
"chardet==4.0.0",
|
|
32
|
+
"typer>=0.9.0",
|
|
33
|
+
"rich>=13.0.0",
|
|
34
|
+
"prompt_toolkit>=3.0.0",
|
|
35
|
+
"tree-sitter>=0.21.0",
|
|
36
|
+
"tree-sitter-python>=0.21.0",
|
|
37
|
+
"tree-sitter-javascript>=0.21.0",
|
|
38
|
+
"tree-sitter-typescript>=0.21.0",
|
|
39
|
+
"mcp>=0.1.0",
|
|
40
|
+
"nest-asyncio>=1.5.0",
|
|
41
|
+
"python-multipart>=0.0.7",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://utim.dev"
|
|
46
|
+
Documentation = "https://utim.dev/docs"
|
|
47
|
+
Changelog = "https://github.com/emendai/utim/blob/main/CHANGELOG.md"
|
|
48
|
+
Issues = "https://github.com/emendai/utim/issues"
|
|
49
|
+
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
# Optional features
|
|
52
|
+
search = [
|
|
53
|
+
"scrapy>=2.11.0",
|
|
54
|
+
"scrapy-playwright>=0.0.34",
|
|
55
|
+
"beautifulsoup4>=4.12.0",
|
|
56
|
+
]
|
|
57
|
+
images = [
|
|
58
|
+
"pillow>=9.0.0",
|
|
59
|
+
]
|
|
60
|
+
full = [
|
|
61
|
+
"scrapy>=2.11.0",
|
|
62
|
+
"scrapy-playwright>=0.0.34",
|
|
63
|
+
"beautifulsoup4>=4.12.0",
|
|
64
|
+
"pillow>=9.0.0",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[project.scripts]
|
|
68
|
+
utim = "utim_cli.utim:app"
|
|
69
|
+
|
|
70
|
+
[tool.setuptools.packages.find]
|
|
71
|
+
include = ["utim_cli*"]
|
|
72
|
+
exclude = ["utim_cli.server*", "tests*", "landing*", "scripts*"]
|
|
73
|
+
|
|
74
|
+
[tool.setuptools.package-data]
|
|
75
|
+
utim_cli = ["mcp_registry.json"]
|
|
@@ -550,8 +550,7 @@ class Orchestrator:
|
|
|
550
550
|
if not current_is_custom and not self._local_api_key and not config.get("api_key"):
|
|
551
551
|
continue
|
|
552
552
|
|
|
553
|
-
|
|
554
|
-
self.console.print(f"\n[bold yellow]🔄 Falling back to model: {current_model}...[/bold yellow]")
|
|
553
|
+
pass
|
|
555
554
|
|
|
556
555
|
|
|
557
556
|
model_retries = 2
|
|
@@ -892,8 +891,6 @@ class Orchestrator:
|
|
|
892
891
|
|
|
893
892
|
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as exc:
|
|
894
893
|
last_exc = exc
|
|
895
|
-
if not silent:
|
|
896
|
-
self.console.print(f"\n[yellow]⚠ Model {current_model} failed (Connection/Timeout error: {exc}). Trying next model...[/yellow]")
|
|
897
894
|
break # try next model
|
|
898
895
|
except requests.exceptions.HTTPError as exc:
|
|
899
896
|
last_exc = exc
|
|
@@ -905,28 +902,18 @@ class Orchestrator:
|
|
|
905
902
|
except Exception:
|
|
906
903
|
self.console.print(f"\n[red]HTTP 400 Error Details from {current_model}: {exc.response.text}[/red]")
|
|
907
904
|
if code == 429:
|
|
908
|
-
if not silent:
|
|
909
|
-
self.console.print(f"\n[yellow]⚠ Model {current_model} rate-limited (429). Trying next model...[/yellow]")
|
|
910
905
|
break
|
|
911
906
|
if attempt < model_retries:
|
|
912
907
|
delay = 3 * (attempt + 1)
|
|
913
|
-
if not silent:
|
|
914
|
-
self.console.print(f"\n[dim yellow]⟳ Model {current_model} returned HTTP {code}. Retrying in {delay}s (attempt {attempt+1}/{model_retries})...[/dim yellow]")
|
|
915
908
|
time.sleep(delay)
|
|
916
909
|
continue
|
|
917
|
-
if not silent:
|
|
918
|
-
self.console.print(f"\n[yellow]⚠ Model {current_model} failed (HTTP {code}). Trying next model...[/yellow]")
|
|
919
910
|
break # try next model
|
|
920
911
|
except RuntimeError as exc:
|
|
921
912
|
# Mid-stream API errors (e.g. model overloaded) or custom empty response error
|
|
922
913
|
last_exc = exc
|
|
923
|
-
if not silent:
|
|
924
|
-
self.console.print(f"\n[yellow]⚠ Model {current_model} failed: {exc}. Trying next model...[/yellow]")
|
|
925
914
|
break # try next model
|
|
926
915
|
except Exception as exc:
|
|
927
916
|
last_exc = exc
|
|
928
|
-
if not silent:
|
|
929
|
-
self.console.print(f"\n[yellow]⚠ Model {current_model} failed (unexpected error: {exc}). Trying next model...[/yellow]")
|
|
930
917
|
break # try next model
|
|
931
918
|
|
|
932
919
|
# If we exit the loop, all models failed
|
|
@@ -34,7 +34,7 @@ EXPIRY_OPTIONS = [
|
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
class ShareRecord:
|
|
37
|
-
def __init__(self, id: str, name: str, created_at: str, expires_at: str, link: str, file_path: str, excluded: List[str]):
|
|
37
|
+
def __init__(self, id: str, name: str, created_at: str, expires_at: str, link: str, file_path: str, excluded: List[str], share_type: str = "chat_project"):
|
|
38
38
|
self.id = id
|
|
39
39
|
self.name = name
|
|
40
40
|
self.created_at = created_at
|
|
@@ -42,6 +42,7 @@ class ShareRecord:
|
|
|
42
42
|
self.link = link
|
|
43
43
|
self.file_path = file_path
|
|
44
44
|
self.excluded = excluded
|
|
45
|
+
self.share_type = share_type
|
|
45
46
|
|
|
46
47
|
def to_dict(self) -> Dict:
|
|
47
48
|
return {
|
|
@@ -51,7 +52,8 @@ class ShareRecord:
|
|
|
51
52
|
"expires_at": self.expires_at,
|
|
52
53
|
"link": self.link,
|
|
53
54
|
"file_path": self.file_path,
|
|
54
|
-
"excluded": self.excluded
|
|
55
|
+
"excluded": self.excluded,
|
|
56
|
+
"share_type": self.share_type
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
@classmethod
|
|
@@ -63,7 +65,8 @@ class ShareRecord:
|
|
|
63
65
|
expires_at=data["expires_at"],
|
|
64
66
|
link=data["link"],
|
|
65
67
|
file_path=data["file_path"],
|
|
66
|
-
excluded=data.get("excluded", [])
|
|
68
|
+
excluded=data.get("excluded", []),
|
|
69
|
+
share_type=data.get("share_type", "chat_project")
|
|
67
70
|
)
|
|
68
71
|
|
|
69
72
|
def is_expired(self) -> bool:
|
|
@@ -96,6 +99,40 @@ class ShareRecord:
|
|
|
96
99
|
except Exception:
|
|
97
100
|
return "Unknown"
|
|
98
101
|
|
|
102
|
+
def print_progress_bar(percentage: float, prefix: str = ""):
|
|
103
|
+
import sys
|
|
104
|
+
bar_width = 40
|
|
105
|
+
filled = int(round((percentage / 100.0) * bar_width))
|
|
106
|
+
filled = min(bar_width, max(0, filled))
|
|
107
|
+
|
|
108
|
+
# Check if the output encoding supports the block characters
|
|
109
|
+
encoding = getattr(sys.__stdout__, 'encoding', None) or 'utf-8'
|
|
110
|
+
try:
|
|
111
|
+
"█".encode(encoding)
|
|
112
|
+
fill_char = "█"
|
|
113
|
+
empty_char = "░"
|
|
114
|
+
except UnicodeEncodeError:
|
|
115
|
+
fill_char = "#"
|
|
116
|
+
empty_char = "-"
|
|
117
|
+
|
|
118
|
+
bar = fill_char * filled + empty_char * (bar_width - filled)
|
|
119
|
+
msg = f"\r {prefix} [{bar}] {percentage:.2f}%"
|
|
120
|
+
try:
|
|
121
|
+
sys.__stdout__.write(msg)
|
|
122
|
+
sys.__stdout__.flush()
|
|
123
|
+
except UnicodeEncodeError:
|
|
124
|
+
# If write fails with unicode, fall back to purely ASCII representation
|
|
125
|
+
try:
|
|
126
|
+
ascii_bar = "#" * filled + "-" * (bar_width - filled)
|
|
127
|
+
ascii_msg = f"\r {prefix} [{ascii_bar}] {percentage:.2f}%"
|
|
128
|
+
sys.__stdout__.write(ascii_msg)
|
|
129
|
+
sys.__stdout__.flush()
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
|
|
99
136
|
class ShareManager:
|
|
100
137
|
def __init__(self, workspace_path: str = "."):
|
|
101
138
|
self.workspace_path = Path(workspace_path).resolve()
|
|
@@ -158,7 +195,7 @@ class ShareManager:
|
|
|
158
195
|
return True
|
|
159
196
|
return False
|
|
160
197
|
|
|
161
|
-
def create_share(self, exclude_keys: List[str], expiry_hours: float, chat_messages: List[Dict]) -> ShareRecord:
|
|
198
|
+
def create_share(self, exclude_keys: List[str], expiry_hours: float, chat_messages: List[Dict], share_type: str = "chat_project") -> ShareRecord:
|
|
162
199
|
share_id = f"share_{uuid.uuid4().hex[:12]}"
|
|
163
200
|
created_at = datetime.datetime.now(datetime.timezone.utc)
|
|
164
201
|
expires_at = created_at + datetime.timedelta(hours=expiry_hours)
|
|
@@ -169,7 +206,6 @@ class ShareManager:
|
|
|
169
206
|
zip_filepath = self.shares_dir / zip_filename
|
|
170
207
|
|
|
171
208
|
# Exclude folders
|
|
172
|
-
# Resolve folders/names to exclude
|
|
173
209
|
exclude_names = []
|
|
174
210
|
for key in exclude_keys:
|
|
175
211
|
# Map categories to actual folder names to filter
|
|
@@ -188,10 +224,16 @@ class ShareManager:
|
|
|
188
224
|
chat_history_content = self._format_chat_history(chat_messages)
|
|
189
225
|
|
|
190
226
|
# Zip files
|
|
191
|
-
self._zip_workspace(zip_filepath, exclude_names, chat_history_content)
|
|
227
|
+
self._zip_workspace(zip_filepath, exclude_names, chat_history_content, share_type, exclude_keys)
|
|
192
228
|
|
|
193
229
|
# Upload and get link
|
|
230
|
+
import sys
|
|
231
|
+
size_mb = zip_filepath.stat().st_size / (1024 * 1024)
|
|
232
|
+
sys.__stdout__.write(f" Uploading shared package to server ({size_mb:.2f} MB)...")
|
|
233
|
+
sys.__stdout__.flush()
|
|
194
234
|
link = self._upload_file(zip_filepath, expiry_hours)
|
|
235
|
+
sys.__stdout__.write(" Done!\n")
|
|
236
|
+
sys.__stdout__.flush()
|
|
195
237
|
|
|
196
238
|
rec = ShareRecord(
|
|
197
239
|
id=share_id,
|
|
@@ -200,7 +242,8 @@ class ShareManager:
|
|
|
200
242
|
expires_at=expires_at.isoformat(),
|
|
201
243
|
link=link,
|
|
202
244
|
file_path=str(zip_filepath),
|
|
203
|
-
excluded=exclude_keys
|
|
245
|
+
excluded=exclude_keys,
|
|
246
|
+
share_type=share_type
|
|
204
247
|
)
|
|
205
248
|
|
|
206
249
|
self.records[share_id] = rec
|
|
@@ -214,66 +257,140 @@ class ShareManager:
|
|
|
214
257
|
md.append(f"Export Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
|
215
258
|
md.append("---\n\n")
|
|
216
259
|
|
|
260
|
+
has_messages = False
|
|
217
261
|
for msg in messages:
|
|
218
|
-
role = msg.get("role", "").
|
|
262
|
+
role = msg.get("role", "").lower()
|
|
219
263
|
content = msg.get("content", "")
|
|
220
264
|
|
|
265
|
+
# Only include actual User prompts and Assistant responses
|
|
266
|
+
if role not in ("user", "assistant"):
|
|
267
|
+
continue
|
|
268
|
+
|
|
221
269
|
# Safely extract text if content is a list
|
|
222
270
|
if isinstance(content, list):
|
|
223
271
|
text = "\n".join(p.get("text", "") for p in content if isinstance(p, dict) and "text" in p)
|
|
224
272
|
else:
|
|
225
273
|
text = str(content)
|
|
226
274
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
275
|
+
# Skip empty messages
|
|
276
|
+
if not text.strip():
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
has_messages = True
|
|
280
|
+
if role == "user":
|
|
230
281
|
md.append(f"### 👤 User\n\n{text}\n\n")
|
|
231
|
-
elif role == "
|
|
282
|
+
elif role == "assistant":
|
|
232
283
|
md.append(f"### 🤖 Assistant\n\n{text}\n\n")
|
|
233
|
-
elif role == "Tool":
|
|
234
|
-
name = msg.get("name", "Tool")
|
|
235
|
-
md.append(f"### 🛠️ Tool: {name}\n\n```\n{text}\n```\n\n")
|
|
236
|
-
else:
|
|
237
|
-
md.append(f"### 📝 {role}\n\n{text}\n\n")
|
|
238
284
|
|
|
239
285
|
md.append("---\n\n")
|
|
240
286
|
|
|
287
|
+
if not has_messages:
|
|
288
|
+
md.append("*No conversation history.*")
|
|
289
|
+
|
|
241
290
|
return "".join(md)
|
|
242
291
|
|
|
243
|
-
def
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
292
|
+
def _generate_share_readme(self, exclude_keys: List[str]) -> str:
|
|
293
|
+
md = []
|
|
294
|
+
md.append("# UTIM Shared Workspace Instructions\n\n")
|
|
295
|
+
md.append("This project workspace was packaged and shared via UTIM CLI.\n")
|
|
296
|
+
md.append("To keep the package size minimal and protect sensitive secrets, some files and folders were omitted.\n\n")
|
|
297
|
+
md.append("## 📦 How to Reinstall Omitted Packages & Files\n\n")
|
|
298
|
+
|
|
299
|
+
reinstall_guides = {
|
|
300
|
+
"node_modules": "- **node_modules**: Omitted Node.js package dependencies. Run `npm install` in the project root directory to restore them.",
|
|
301
|
+
"venv": "- **venv / .venv**: Omitted Python virtual environment. Run `python -m venv venv` to recreate, activate it, and run `pip install -r requirements.txt` to restore dependencies.",
|
|
302
|
+
".env": "- **.env**: Omitted environment configuration secrets. Create a new local `.env` file in the root directory and define your API keys and parameters.",
|
|
303
|
+
"__pycache__": "- **__pycache__**: Omitted Python bytecode caches. These will be automatically regenerated by Python when running your scripts.",
|
|
304
|
+
"dist": "- **dist / build**: Omitted compiled distribution/build artifacts. Run your project build command (e.g. `npm run build` or `python -m build`) to rebuild.",
|
|
305
|
+
".next": "- **.next**: Omitted Next.js/Nuxt.js build caches. These will be regenerated automatically during the next project run or build.",
|
|
306
|
+
"target": "- **target**: Omitted Rust compilation build artifacts. Run `cargo build` in the project directory to rebuild.",
|
|
307
|
+
}
|
|
247
308
|
|
|
248
|
-
|
|
309
|
+
for key in exclude_keys:
|
|
310
|
+
guide = reinstall_guides.get(key, f"- **{key}**: Omitted file/directory.")
|
|
311
|
+
md.append(f"{guide}\n")
|
|
312
|
+
|
|
313
|
+
md.append("\n---\n")
|
|
314
|
+
md.append("Shared with UTIM CLI (https://utim.dev)\n")
|
|
315
|
+
return "".join(md)
|
|
316
|
+
|
|
317
|
+
def _zip_workspace(self, output_zip_path: Path, exclude_names: List[str], chat_history: str, share_type: str, exclude_keys: List[str]):
|
|
318
|
+
# Calculate total files for progress calculation
|
|
319
|
+
total_files = 0
|
|
320
|
+
if share_type in ("project", "chat_project"):
|
|
249
321
|
for root, dirs, files in os.walk(self.workspace_path):
|
|
250
|
-
# Filter directories in-place to prevent os.walk recursion
|
|
251
|
-
# Skip .utim directory so we don't zip previous zips
|
|
252
322
|
dirs[:] = [d for d in dirs if d not in exclude_names and d != ".utim"]
|
|
253
|
-
|
|
254
323
|
for file in files:
|
|
255
324
|
if file in exclude_names or file.endswith(('.pyc', '.pyo', '.pyd')):
|
|
256
325
|
continue
|
|
257
|
-
|
|
258
|
-
# Also skip if file path directory component is excluded
|
|
259
326
|
path_parts = Path(root).relative_to(self.workspace_path).parts
|
|
260
327
|
if any(p in exclude_names or p == ".utim" for p in path_parts):
|
|
261
328
|
continue
|
|
262
|
-
|
|
263
|
-
full_path = Path(root) / file
|
|
264
|
-
# Skip the zip itself
|
|
265
|
-
if full_path.resolve() == output_zip_path.resolve():
|
|
329
|
+
if (Path(root) / file).resolve() == output_zip_path.resolve():
|
|
266
330
|
continue
|
|
331
|
+
total_files += 1
|
|
332
|
+
|
|
333
|
+
if share_type in ("chat", "chat_project"):
|
|
334
|
+
total_files += 1
|
|
335
|
+
if share_type in ("project", "chat_project") and exclude_keys:
|
|
336
|
+
total_files += 1
|
|
337
|
+
|
|
338
|
+
total_files = max(1, total_files)
|
|
339
|
+
zipped_count = 0
|
|
340
|
+
|
|
341
|
+
with zipfile.ZipFile(output_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
342
|
+
# 1. Add chat history if requested
|
|
343
|
+
if share_type in ("chat", "chat_project"):
|
|
344
|
+
zipf.writestr("chat_history.md", chat_history)
|
|
345
|
+
zipped_count += 1
|
|
346
|
+
print_progress_bar(100.0 * zipped_count / total_files, "Compressing:")
|
|
347
|
+
|
|
348
|
+
# 2. Add workspace files if requested
|
|
349
|
+
if share_type in ("project", "chat_project"):
|
|
350
|
+
# Write the custom README explaining how to reinstall omitted packages
|
|
351
|
+
if exclude_keys:
|
|
352
|
+
readme_content = self._generate_share_readme(exclude_keys)
|
|
353
|
+
zipf.writestr("README.md", readme_content)
|
|
354
|
+
zipped_count += 1
|
|
355
|
+
print_progress_bar(100.0 * zipped_count / total_files, "Compressing:")
|
|
356
|
+
|
|
357
|
+
for root, dirs, files in os.walk(self.workspace_path):
|
|
358
|
+
# Filter directories in-place to prevent os.walk recursion
|
|
359
|
+
dirs[:] = [d for d in dirs if d not in exclude_names and d != ".utim"]
|
|
360
|
+
|
|
361
|
+
for file in files:
|
|
362
|
+
if file in exclude_names or file.endswith(('.pyc', '.pyo', '.pyd')):
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
# Also skip if file path directory component is excluded
|
|
366
|
+
path_parts = Path(root).relative_to(self.workspace_path).parts
|
|
367
|
+
if any(p in exclude_names or p == ".utim" for p in path_parts):
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
full_path = Path(root) / file
|
|
371
|
+
# Skip the zip itself
|
|
372
|
+
if full_path.resolve() == output_zip_path.resolve():
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
# Safe relative path inside the zip
|
|
376
|
+
rel_path = full_path.relative_to(self.workspace_path)
|
|
377
|
+
# Avoid overwriting our newly generated README.md if they have one
|
|
378
|
+
if rel_path.name.lower() == "readme.md" and exclude_keys:
|
|
379
|
+
rel_path = rel_path.with_name("ORIGINAL_README.md")
|
|
267
380
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
381
|
+
try:
|
|
382
|
+
zipf.write(str(full_path), str(rel_path))
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
zipped_count += 1
|
|
386
|
+
print_progress_bar(100.0 * zipped_count / total_files, "Compressing:")
|
|
387
|
+
|
|
388
|
+
import sys
|
|
389
|
+
sys.__stdout__.write("\n")
|
|
390
|
+
sys.__stdout__.flush()
|
|
274
391
|
|
|
275
392
|
def _upload_file(self, zip_filepath: Path, expiry_hours: float) -> str:
|
|
276
|
-
# Determine expiry string for
|
|
393
|
+
# Determine expiry string for server
|
|
277
394
|
if expiry_hours <= 0.25:
|
|
278
395
|
exp = "15m"
|
|
279
396
|
elif expiry_hours <= 1.0:
|
|
@@ -287,34 +404,25 @@ class ShareManager:
|
|
|
287
404
|
else:
|
|
288
405
|
exp = "7d"
|
|
289
406
|
|
|
290
|
-
# Check UTIM_SERVER_URL env var
|
|
291
|
-
server_url = os.getenv("UTIM_SERVER_URL", "")
|
|
292
|
-
if server_url:
|
|
293
|
-
|
|
294
|
-
# Try uploading to configured server endpoint
|
|
295
|
-
url = f"{server_url}/shares/upload"
|
|
296
|
-
with open(zip_filepath, 'rb') as f:
|
|
297
|
-
response = requests.post(url, files={"file": f}, data={"expires": exp}, timeout=30)
|
|
298
|
-
if response.status_code == 200:
|
|
299
|
-
data = response.json()
|
|
300
|
-
if data.get("link"):
|
|
301
|
-
return data["link"]
|
|
302
|
-
except Exception:
|
|
303
|
-
# Fallback to public file.io if server upload fails
|
|
304
|
-
pass
|
|
407
|
+
# Check UTIM_SERVER_URL env var, defaulting to production Railway server
|
|
408
|
+
server_url = os.getenv("UTIM_SERVER_URL", "https://utim-cli-production.up.railway.app")
|
|
409
|
+
if not server_url:
|
|
410
|
+
raise RuntimeError("No share server URL configured.")
|
|
305
411
|
|
|
306
|
-
|
|
412
|
+
url = f"{server_url}/shares/upload"
|
|
307
413
|
try:
|
|
308
|
-
url = f"https://file.io/?expires={exp}"
|
|
309
414
|
with open(zip_filepath, 'rb') as f:
|
|
310
|
-
response = requests.post(url, files={"file": f}, timeout=
|
|
415
|
+
response = requests.post(url, files={"file": f}, data={"expires": exp}, timeout=60)
|
|
416
|
+
|
|
311
417
|
if response.status_code == 200:
|
|
312
418
|
data = response.json()
|
|
313
|
-
if data.get("
|
|
419
|
+
if data.get("link"):
|
|
314
420
|
return data["link"]
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
421
|
+
else:
|
|
422
|
+
raise RuntimeError("Server response missing download link.")
|
|
423
|
+
else:
|
|
424
|
+
raise RuntimeError(f"Server returned status code {response.status_code}: {response.text}")
|
|
425
|
+
except requests.exceptions.RequestException as e:
|
|
426
|
+
raise RuntimeError(f"Network error contacting Railway server: {e}")
|
|
427
|
+
except Exception as e:
|
|
428
|
+
raise RuntimeError(f"Upload failed: {e}")
|