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.
Files changed (42) hide show
  1. {utim_cli-1.0.0/utim_cli.egg-info → utim_cli-1.43.9}/PKG-INFO +3 -3
  2. {utim_cli-1.0.0 → utim_cli-1.43.9}/pyproject.toml +75 -75
  3. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/orchestrator.py +1 -14
  4. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/share.py +171 -63
  5. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/share_tui.py +153 -39
  6. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/utim.py +282 -10
  7. {utim_cli-1.0.0 → utim_cli-1.43.9/utim_cli.egg-info}/PKG-INFO +3 -3
  8. {utim_cli-1.0.0 → utim_cli-1.43.9}/CHANGELOG.md +0 -0
  9. {utim_cli-1.0.0 → utim_cli-1.43.9}/LICENSE +0 -0
  10. {utim_cli-1.0.0 → utim_cli-1.43.9}/MANIFEST.in +0 -0
  11. {utim_cli-1.0.0 → utim_cli-1.43.9}/README.md +0 -0
  12. {utim_cli-1.0.0 → utim_cli-1.43.9}/setup.cfg +0 -0
  13. {utim_cli-1.0.0 → utim_cli-1.43.9}/setup.py +0 -0
  14. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/__init__.py +0 -0
  15. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/agent.py +0 -0
  16. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/auth.py +0 -0
  17. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/backup.py +0 -0
  18. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/billing.py +0 -0
  19. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/blender_agent.py +0 -0
  20. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/bootstrap.py +0 -0
  21. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/client_utils.py +0 -0
  22. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/config.py +0 -0
  23. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/context_pruner.py +0 -0
  24. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/doctor.py +0 -0
  25. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/knowledge_graph.py +0 -0
  26. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/logger.py +0 -0
  27. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/mcp_clean_wrapper.py +0 -0
  28. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/mcp_client.py +0 -0
  29. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/mcp_registry.json +0 -0
  30. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/reflection.py +0 -0
  31. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/report.py +0 -0
  32. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/scrapy_search.py +0 -0
  33. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/situational_scoring.py +0 -0
  34. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/state.py +0 -0
  35. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/tools.py +0 -0
  36. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/vector_memory.py +0 -0
  37. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli/workspace.py +0 -0
  38. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli.egg-info/SOURCES.txt +0 -0
  39. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli.egg-info/dependency_links.txt +0 -0
  40. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli.egg-info/entry_points.txt +0 -0
  41. {utim_cli-1.0.0 → utim_cli-1.43.9}/utim_cli.egg-info/requires.txt +0 -0
  42. {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.0.0
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.ai
7
- Project-URL: Documentation, https://utim.ai/docs
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.0.0"
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.ai"
46
- Documentation = "https://utim.ai/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"]
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
- if model_idx > 0 and not silent:
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", "").capitalize()
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
- if role == "System":
228
- md.append(f"### ⚙️ System Prompt\n\n```\n{text}\n```\n\n")
229
- elif role == "User":
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 == "Assistant":
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 _zip_workspace(self, output_zip_path: Path, exclude_names: List[str], chat_history: str):
244
- with zipfile.ZipFile(output_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
245
- # 1. Add the chat history first at root as chat_history.md
246
- zipf.writestr("chat_history.md", chat_history)
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
- # 2. Walk directory recursively
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
- # Safe relative path inside the zip
269
- rel_path = full_path.relative_to(self.workspace_path)
270
- try:
271
- zipf.write(str(full_path), str(rel_path))
272
- except Exception:
273
- pass
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 file.io
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
- try:
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
- # Fallback public upload using file.io
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=30)
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("success") and data.get("link"):
419
+ if data.get("link"):
314
420
  return data["link"]
315
- except Exception:
316
- pass
317
-
318
- # Fallback to local link if offline or upload fails
319
- # Return a custom local URI
320
- return f"file:///{zip_filepath.resolve().as_posix()}"
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}")