utim-cli 1.44.0__tar.gz → 1.44.1__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 (43) hide show
  1. utim_cli-1.44.1/CHANGELOG.md +29 -0
  2. {utim_cli-1.44.0/utim_cli.egg-info → utim_cli-1.44.1}/PKG-INFO +1 -1
  3. {utim_cli-1.44.0 → utim_cli-1.44.1}/pyproject.toml +1 -1
  4. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/backup.py +8 -32
  5. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/bootstrap.py +14 -38
  6. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/config.py +2 -43
  7. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/orchestrator.py +14 -95
  8. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/situational_scoring.py +4 -24
  9. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/tools.py +4 -6
  10. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/utim.py +244 -315
  11. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/vector_memory.py +7 -29
  12. {utim_cli-1.44.0 → utim_cli-1.44.1/utim_cli.egg-info}/PKG-INFO +1 -1
  13. utim_cli-1.44.0/CHANGELOG.md +0 -51
  14. {utim_cli-1.44.0 → utim_cli-1.44.1}/LICENSE +0 -0
  15. {utim_cli-1.44.0 → utim_cli-1.44.1}/MANIFEST.in +0 -0
  16. {utim_cli-1.44.0 → utim_cli-1.44.1}/README.md +0 -0
  17. {utim_cli-1.44.0 → utim_cli-1.44.1}/setup.cfg +0 -0
  18. {utim_cli-1.44.0 → utim_cli-1.44.1}/setup.py +0 -0
  19. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/__init__.py +0 -0
  20. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/agent.py +0 -0
  21. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/auth.py +0 -0
  22. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/billing.py +0 -0
  23. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/blender_agent.py +0 -0
  24. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/client_utils.py +0 -0
  25. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/context_pruner.py +0 -0
  26. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/doctor.py +0 -0
  27. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/knowledge_graph.py +0 -0
  28. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/logger.py +0 -0
  29. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/mcp_clean_wrapper.py +0 -0
  30. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/mcp_client.py +0 -0
  31. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/mcp_registry.json +0 -0
  32. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/reflection.py +0 -0
  33. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/report.py +0 -0
  34. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/scrapy_search.py +0 -0
  35. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/share.py +0 -0
  36. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/share_tui.py +0 -0
  37. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/state.py +0 -0
  38. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/workspace.py +0 -0
  39. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli.egg-info/SOURCES.txt +0 -0
  40. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli.egg-info/dependency_links.txt +0 -0
  41. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli.egg-info/entry_points.txt +0 -0
  42. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli.egg-info/requires.txt +0 -0
  43. {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli.egg-info/top_level.txt +0 -0
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to the UTIM CLI Agent will be documented in this file.
4
+
5
+ ## [1.43.20] - 2026-07-01
6
+
7
+ ### Added
8
+ - **Local-Global Experience Vector DB Sync**: UTIM now carries the ChromaDB experience database across project folders automatically. On startup, the previous project's `experiences.json`, `vector_db`, and vector metadata files are copied to the current folder — deleted from the old folder only after a successful paste. Last active folder is persisted server-side so the sync survives PC restarts.
9
+ - **BYOK Provider Disconnection (key `x`)**: Press `x` in the model selector to view a list of connected custom providers and disconnect them. All associated models are removed from the model list instantly.
10
+ - **Bring Your Own Key (BYOK) for Free Users**: Free-tier users can now connect any OpenAI-compatible provider using their own API key. BYOK models bypass UTIM quota limits and plan gating entirely.
11
+ - **Dynamic Changelog & Version API**: `/api/changelog` parses `CHANGELOG.md` at request time and returns structured JSON. `/health` version field is also derived from `CHANGELOG.md` dynamically — deploying a new changelog auto-updates both the website changelog page and version badge.
12
+ - **Changelog Tab on Website**: The terminal website now includes a `↻ Changelog` tab that fetches live release notes from the Railway server on load.
13
+ - **Features Page Redesign**: The Features tab now displays a 9-card animated grid with per-feature glow icons, numbered labels, colored tags, and spring entrance animations — matching the full terminal aesthetic of the site.
14
+ - **Last Project Folder Persistence**: Server stores each user's last active project directory in the database. Survives PC restarts and is used to drive the cross-folder experience DB sync.
15
+ - **`GET /api/auth/last-folder` and `POST /api/auth/last-folder` Endpoints**: New authenticated REST endpoints to read/write a user's last active project folder path.
16
+
17
+ ### Changed
18
+ - **Plan Upgrade Credit Transition**: Free plan credit allowance ($1.00) is zeroed out instead of rolling over to the new paid plan's quota bank on upgrade.
19
+ - **Hobby Plan Model Access**: Hobby plan `allowed_models` corrected to `"all"` so users can access the full model registry (not just free-tier models).
20
+ - **BYOK Model Gating Bypass**: Orchestrator pre-flight checks now skip plan gating and UTIM credit checks for custom BYOK models since they use user-owned keys.
21
+ - **Pricing Page Claims Cleaned**: Removed unsubstantiated marketing claims (`Dedicated elite endpoints`, `Autonomous team agents access`, `High-speed dedicated endpoints`, `All premium models (GPT-4o, Claude)`, `Synthetic Eye & VSIX tools`, `Claude Sonnet 4.6 (1 RPM)`, `10-session history storage`) from all pricing tier feature lists.
22
+
23
+ ### Fixed
24
+ - **BYOK Flow Display Bug**: Rewrote Add/Delete/BYOK-import dialogs to run on `sys.__stdout__` with `prompt_toolkit.prompt()` so they are never swallowed by the alt-screen buffer.
25
+ - **Double-Exit Crash (TUI)**: Wrapped all keyboard exit callbacks (`ENTER`/`ESC`/`q`/`CTRL+C`) in `try...except` blocks to prevent double-fire tracebacks.
26
+ - **`/changelog` Route No Longer Shows Legacy Page**: All website routes (including `/changelog`) now correctly map to the `<PowershellUI />` terminal component. Legacy standalone page removed.
27
+
28
+ ---
29
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: utim-cli
3
- Version: 1.44.0
3
+ Version: 1.44.1
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.44.00"
7
+ version = "1.44.1"
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"
@@ -4,27 +4,9 @@ import json
4
4
  import sqlite3
5
5
  from utim_cli.logger import log_info, log_error, log_warning
6
6
 
7
- def _utim_dir() -> str:
8
- """Return the absolute path to the project-local .utim directory."""
9
- try:
10
- from utim_cli.config import config
11
- return str(config.local_dir)
12
- except Exception:
13
- return os.path.join(os.getcwd(), ".utim")
14
-
15
- def _backup_dir() -> str:
16
- """Return the absolute path to the project-local .utim_backup directory."""
17
- try:
18
- from utim_cli.config import config
19
- return str(config.local_dir.parent / ".utim_backup")
20
- except Exception:
21
- return os.path.join(os.getcwd(), ".utim_backup")
22
-
23
- # Legacy constants kept for any external code that might import them
24
7
  BACKUP_DIR = ".utim_backup"
25
8
  UTIM_DIR = ".utim"
26
9
 
27
-
28
10
  def is_json_valid(path: str) -> bool:
29
11
  """Check if JSON file exists and is parseable."""
30
12
  if not os.path.exists(path):
@@ -61,20 +43,18 @@ def is_file_valid(path: str) -> bool:
61
43
  def backup_state():
62
44
  """Backup active configuration and database files to .utim_backup/."""
63
45
  try:
64
- utim_dir = _utim_dir()
65
- backup_dir = _backup_dir()
66
- if not os.path.exists(utim_dir):
46
+ if not os.path.exists(UTIM_DIR):
67
47
  return
68
48
 
69
- os.makedirs(backup_dir, exist_ok=True)
49
+ os.makedirs(BACKUP_DIR, exist_ok=True)
70
50
 
71
51
  files_to_backup = [
72
52
  "config.json", "mcp.json", "utim_local.db",
73
53
  "memory.json", "session_state.json"
74
54
  ]
75
55
  for f in files_to_backup:
76
- src = os.path.join(utim_dir, f)
77
- dst = os.path.join(backup_dir, f)
56
+ src = os.path.join(UTIM_DIR, f)
57
+ dst = os.path.join(BACKUP_DIR, f)
78
58
  if os.path.exists(src):
79
59
  # Verify file is not corrupted before backing it up
80
60
  if not is_file_valid(src):
@@ -87,16 +67,13 @@ def backup_state():
87
67
  except Exception as e:
88
68
  log_error("backup", f"State backup failed: {e}", e)
89
69
 
90
-
91
70
  def restore_state() -> bool:
92
71
  """Restore configuration and database files from backup if corrupt or missing."""
93
72
  try:
94
- utim_dir = _utim_dir()
95
- backup_dir = _backup_dir()
96
- if not os.path.exists(backup_dir):
73
+ if not os.path.exists(BACKUP_DIR):
97
74
  return False
98
75
 
99
- os.makedirs(utim_dir, exist_ok=True)
76
+ os.makedirs(UTIM_DIR, exist_ok=True)
100
77
  restored = False
101
78
 
102
79
  files_to_restore = [
@@ -104,8 +81,8 @@ def restore_state() -> bool:
104
81
  "memory.json", "session_state.json"
105
82
  ]
106
83
  for f in files_to_restore:
107
- src = os.path.join(backup_dir, f)
108
- dst = os.path.join(utim_dir, f)
84
+ src = os.path.join(BACKUP_DIR, f)
85
+ dst = os.path.join(UTIM_DIR, f)
109
86
 
110
87
  # Check if destination is missing or corrupted
111
88
  needs_restore = not os.path.exists(dst) or not is_file_valid(dst)
@@ -122,4 +99,3 @@ def restore_state() -> bool:
122
99
  log_error("backup", f"State restoration failed: {e}", e)
123
100
  return False
124
101
 
125
-
@@ -114,22 +114,7 @@ description: Guidelines and patterns for asynchronous Python programming using a
114
114
  }
115
115
 
116
116
  def initialize_utim() -> str:
117
- """Initialize .utim and .utim_tmp directory structure, local SQLite database,
118
- and auto-create custom skills/rules if they don't exist.
119
-
120
- Returns the path to the local DB file.
121
- """
122
- from utim_cli.config import config as _cfg
123
-
124
- # Ensure BOTH project-local directories exist (and capture first-run state)
125
- utim_first_run, utim_tmp_first_run = _cfg.ensure_project_dirs()
126
- UTIM_DIR_resolved = _cfg.local_dir
127
- UTIM_TMP_DIR_resolved = _cfg.local_tmp_dir
128
-
129
- # Store first-run flag so utim.py can show a welcome hint
130
- initialize_utim._utim_first_run = utim_first_run # type: ignore[attr-defined]
131
- initialize_utim._utim_tmp_first_run = utim_tmp_first_run # type: ignore[attr-defined]
132
-
117
+ """Initialize .utim directory, local SQLite database, and auto-create custom skills/rules if they don't exist."""
133
118
  try:
134
119
  from utim_cli.backup import restore_state
135
120
  restore_state()
@@ -137,6 +122,8 @@ def initialize_utim() -> str:
137
122
  from utim_cli.logger import log_error
138
123
  log_error("bootstrap", "Failed to restore state during initialization", e)
139
124
 
125
+ UTIM_DIR.mkdir(exist_ok=True)
126
+
140
127
  # 1. Initialize local_utim database using SQLAlchemy init_db
141
128
  try:
142
129
  from utim_cli.server.db import init_db
@@ -156,13 +143,14 @@ def initialize_utim() -> str:
156
143
 
157
144
  # 5. Sync global/local experience database across project folders
158
145
  try:
159
- api_key = _cfg.get("api_key")
146
+ from utim_cli.config import config
147
+ api_key = config.get("api_key")
160
148
  if api_key:
161
149
  _sync_global_experience(api_key, os.getcwd())
162
150
  except Exception:
163
151
  pass
164
152
 
165
- db_path = UTIM_DIR_resolved / "utim_local.db"
153
+ db_path = UTIM_DIR / 'utim_local.db'
166
154
  return str(db_path)
167
155
 
168
156
 
@@ -286,16 +274,14 @@ def _sync_global_experience(api_key: str, current_folder: str):
286
274
  def _write_skills_and_agents():
287
275
  """Auto-create skills/ directory, AGENTS.md, and all default SKILL.md files inside .utim/."""
288
276
  try:
289
- from utim_cli.config import config
290
- utim_dir = config.local_dir
291
277
  # Write AGENTS.md
292
- agents_path = utim_dir / 'AGENTS.md'
278
+ agents_path = UTIM_DIR / 'AGENTS.md'
293
279
  if not agents_path.exists():
294
280
  with open(agents_path, 'w', encoding='utf-8') as f:
295
281
  f.write(DEFAULT_AGENTS_MD)
296
282
 
297
283
  # Write skills
298
- skills_dir = utim_dir / 'skills'
284
+ skills_dir = UTIM_DIR / 'skills'
299
285
  for skill_name, content in DEFAULT_SKILLS.items():
300
286
  skill_dir = skills_dir / skill_name
301
287
  skill_dir.mkdir(parents=True, exist_ok=True)
@@ -305,14 +291,13 @@ def _write_skills_and_agents():
305
291
  f.write(content)
306
292
 
307
293
  # Write DESIGN.md
308
- design_path = utim_dir / 'DESIGN.md'
294
+ design_path = UTIM_DIR / 'DESIGN.md'
309
295
  if not design_path.exists():
310
296
  _write_default_design_md(design_path)
311
297
  except Exception as e:
312
298
  from utim_cli.logger import log_error
313
299
  log_error("bootstrap", "Failed to write skills and agents configuration to .utim/", e)
314
300
 
315
-
316
301
  def _write_default_design_md(md_path: Path):
317
302
  content = """# Premium Web Design System (Aesthetics Cheat-Sheet)
318
303
  Use this guide to build modern, beautiful, and "award-winning" web interfaces. Apply these principles, variables, and recipes to wow the user.
@@ -334,8 +319,7 @@ Use this guide to build modern, beautiful, and "award-winning" web interfaces. A
334
319
 
335
320
  def _write_analytical_rules_md():
336
321
  """Write the analytical_rules.md file to .utim folder if not exists."""
337
- from utim_cli.config import config
338
- md_path = config.local_dir / 'analytical_rules.md'
322
+ md_path = UTIM_DIR / 'analytical_rules.md'
339
323
  if md_path.exists():
340
324
  return
341
325
 
@@ -351,11 +335,9 @@ Always start by identifying the TRUE objective, then work backwards to feasible
351
335
  from utim_cli.logger import log_error
352
336
  log_error("bootstrap", f"Failed to write analytical rules markdown to {md_path}", e)
353
337
 
354
-
355
338
  def _write_utim_md():
356
339
  """Write the UTIM.md file to .utim folder if not exists."""
357
- from utim_cli.config import config
358
- md_path = config.local_dir / 'UTIM.md'
340
+ md_path = UTIM_DIR / 'UTIM.md'
359
341
  if md_path.exists():
360
342
  return
361
343
 
@@ -369,13 +351,10 @@ def _write_utim_md():
369
351
  from utim_cli.logger import log_error
370
352
  log_error("bootstrap", f"Failed to write UTIM.md markdown to {md_path}", e)
371
353
 
372
-
373
354
  def get_rag_context(user_prompt: str = "") -> str:
374
355
  """Get RAG context string for system prompt injection by matching user_prompt keywords to skills."""
375
356
  try:
376
- from utim_cli.config import config
377
357
  from utim_cli.state import STATE
378
- utim_dir = config.local_dir
379
358
  STATE["injected_contexts"] = []
380
359
 
381
360
  # Simple keyword matching to select relevant skills
@@ -398,7 +377,7 @@ def get_rag_context(user_prompt: str = "") -> str:
398
377
 
399
378
  # If no keywords matched, default to AGENTS.md
400
379
  if not matched_skills:
401
- agents_path = utim_dir / 'AGENTS.md'
380
+ agents_path = UTIM_DIR / 'AGENTS.md'
402
381
  if agents_path.exists():
403
382
  with open(agents_path, 'r', encoding='utf-8') as f:
404
383
  content = f.read()
@@ -408,7 +387,7 @@ def get_rag_context(user_prompt: str = "") -> str:
408
387
 
409
388
  context = ""
410
389
  for skill_name in matched_skills[:2]: # Load top 2 matched skills
411
- skill_path = utim_dir / 'skills' / skill_name / 'SKILL.md'
390
+ skill_path = UTIM_DIR / 'skills' / skill_name / 'SKILL.md'
412
391
  if skill_path.exists():
413
392
  with open(skill_path, 'r', encoding='utf-8') as f:
414
393
  content = f.read()
@@ -425,12 +404,9 @@ def get_rag_context(user_prompt: str = "") -> str:
425
404
  log_error("bootstrap", "Failed to get RAG context from skills", e)
426
405
  return ""
427
406
 
428
-
429
407
  def get_subagent_rag_context(subagent_name: str, query: str = "") -> str:
430
408
  """Get RAG context string for the specified sub-agent prompt injection by reading from markdown skill files directly."""
431
409
  try:
432
- from utim_cli.config import config
433
- utim_dir = config.local_dir
434
410
  # Map subagent name to the most relevant skill file
435
411
  mapping = {
436
412
  "project_res": "software-architecture",
@@ -442,7 +418,7 @@ def get_subagent_rag_context(subagent_name: str, query: str = "") -> str:
442
418
  if not skill_name:
443
419
  return ""
444
420
 
445
- skill_path = utim_dir / 'skills' / skill_name / 'SKILL.md'
421
+ skill_path = UTIM_DIR / 'skills' / skill_name / 'SKILL.md'
446
422
  if not skill_path.exists():
447
423
  return ""
448
424
 
@@ -2,55 +2,14 @@ import json
2
2
  import shutil
3
3
  import os
4
4
  import pathlib
5
- from typing import Any, Dict, Optional, Tuple
5
+ from typing import Any, Dict, Optional
6
6
 
7
7
  class Config:
8
8
  def __init__(self):
9
9
  self.global_dir = pathlib.Path.home() / ".utim"
10
- # local_dir and local_tmp_dir are always relative to the current working
11
- # directory (the user's project root). They are resolved lazily via
12
- # properties so that tests that change os.getcwd() see the correct path.
10
+ self.local_dir = pathlib.Path(".utim").resolve()
13
11
  self._data: Dict[str, Any] = self._load()
14
12
 
15
- # ── Project-local storage paths ──────────────────────────────────────────
16
-
17
- @property
18
- def local_dir(self) -> pathlib.Path:
19
- """Path to the project-local .utim directory (persistent data)."""
20
- return pathlib.Path(os.getcwd()) / ".utim"
21
-
22
- @property
23
- def local_tmp_dir(self) -> pathlib.Path:
24
- """Path to the project-local .utim_tmp directory (transient data)."""
25
- return pathlib.Path(os.getcwd()) / ".utim_tmp"
26
-
27
- def ensure_project_dirs(self) -> Tuple[bool, bool]:
28
- """Create .utim and .utim_tmp if they do not yet exist.
29
-
30
- Returns (utim_created, utim_tmp_created) — True if the directory was
31
- just created for the first time (useful for showing first-run messages).
32
- """
33
- utim_created = not self.local_dir.exists()
34
- utim_tmp_created = not self.local_tmp_dir.exists()
35
-
36
- self.local_dir.mkdir(parents=True, exist_ok=True)
37
- self.local_tmp_dir.mkdir(parents=True, exist_ok=True)
38
-
39
- # Write a .gitignore inside .utim_tmp so transient files are never
40
- # accidentally committed to version control.
41
- gitignore = self.local_tmp_dir / ".gitignore"
42
- if not gitignore.exists():
43
- try:
44
- gitignore.write_text(
45
- "# Auto-generated by UTIM. These files are transient and safe to delete.\n"
46
- "*\n",
47
- encoding="utf-8",
48
- )
49
- except Exception:
50
- pass
51
-
52
- return utim_created, utim_tmp_created
53
-
54
13
  def _load(self) -> Dict[str, Any]:
55
14
  data = {}
56
15
  # Load global config
@@ -361,7 +361,6 @@ class Orchestrator:
361
361
  # Turn-level file-change tracking
362
362
  self.turn_history: List[Dict[str, Any]] = []
363
363
  self.redo_history: List[Dict[str, Any]] = []
364
- self._post_undo_dirty: bool = False # True if user typed a new prompt while redo_history was non-empty
365
364
  self._turn_changes: List[Dict[str, Any]] = []
366
365
  self._current_turn_start: int = 1
367
366
 
@@ -1683,6 +1682,7 @@ class Orchestrator:
1683
1682
  after = ch.get("after") or ""
1684
1683
  b_lines = before.splitlines()
1685
1684
  a_lines = after.splitlines()
1685
+ # Simple heuristic: added = lines only in after, removed = lines only in before
1686
1686
  b_set = set(b_lines); a_set = set(a_lines)
1687
1687
  add_total += len(a_lines) - len([l for l in a_lines if l in b_set])
1688
1688
  del_total += len(b_lines) - len([l for l in b_lines if l in a_set])
@@ -1696,69 +1696,6 @@ class Orchestrator:
1696
1696
  parts.append(f"[bold red]-{del_total}[/bold red]")
1697
1697
  return " ".join(parts) if parts else "No files changed"
1698
1698
 
1699
- def get_undo_file_diff(self, from_turn_idx: int, mode: str = "undo") -> List[Dict]:
1700
- """Build a per-file diff preview for all turns from from_turn_idx onward (undo)
1701
- or for all redo_history turns (redo).
1702
- Returns a list of dicts: {path, action, before_lines, after_lines, added, removed}
1703
- """
1704
- import difflib
1705
- source_turns: List[Dict] = []
1706
- if mode == "undo":
1707
- source_turns = self.turn_history[from_turn_idx:]
1708
- else: # redo
1709
- source_turns = list(reversed(self.redo_history))
1710
-
1711
- # Collapse per-file: keep the first before and last after across all turns
1712
- file_first_before: Dict[str, Any] = {}
1713
- file_last_after: Dict[str, Any] = {}
1714
- file_action: Dict[str, str] = {}
1715
- for turn in source_turns:
1716
- for ch in turn.get("changes", []):
1717
- path = ch["path"]
1718
- act = ch.get("action", "write")
1719
- if path not in file_first_before:
1720
- file_first_before[path] = ch.get("before")
1721
- file_action[path] = act
1722
- file_last_after[path] = ch.get("after")
1723
-
1724
- results: List[Dict] = []
1725
- for path in file_first_before:
1726
- before_str = file_first_before[path] or ""
1727
- after_str = file_last_after.get(path) or ""
1728
- b_lines = before_str.splitlines(keepends=True)
1729
- a_lines = after_str.splitlines(keepends=True)
1730
-
1731
- if mode == "undo":
1732
- # Undo: show revert direction (after→before)
1733
- diff_lines = list(difflib.unified_diff(
1734
- a_lines, b_lines,
1735
- fromfile=f"{path} (current)",
1736
- tofile=f"{path} (after undo)",
1737
- n=3
1738
- ))
1739
- added = sum(1 for l in diff_lines if l.startswith("+") and not l.startswith("+++"))
1740
- removed = sum(1 for l in diff_lines if l.startswith("-") and not l.startswith("---"))
1741
- else:
1742
- # Redo: show forward direction (before→after)
1743
- diff_lines = list(difflib.unified_diff(
1744
- b_lines, a_lines,
1745
- fromfile=f"{path} (current)",
1746
- tofile=f"{path} (after redo)",
1747
- n=3
1748
- ))
1749
- added = sum(1 for l in diff_lines if l.startswith("+") and not l.startswith("+++"))
1750
- removed = sum(1 for l in diff_lines if l.startswith("-") and not l.startswith("---"))
1751
-
1752
- results.append({
1753
- "path": path,
1754
- "action": file_action[path],
1755
- "diff_lines": diff_lines,
1756
- "added": added,
1757
- "removed": removed,
1758
- })
1759
- return results
1760
-
1761
-
1762
1699
  def rewind_single_turn(self, turn_idx: int, revert_code: bool = True,
1763
1700
  revert_msgs: bool = True) -> Dict[str, Any]:
1764
1701
  """Rewind only a single turn (not all subsequent turns)."""
@@ -1923,37 +1860,24 @@ class Orchestrator:
1923
1860
  turn["msg_start"] = msg_start
1924
1861
  turn["msg_end"] = msg_end
1925
1862
  self.turn_history.append(turn)
1926
- self._post_undo_dirty = False # Redo completed — dirty flag cleared
1927
1863
 
1928
1864
  # Persist messages and redo history to DB
1929
1865
  self._persist_messages()
1930
1866
  return res
1931
1867
 
1932
1868
  def redo_up_to_turn(self, redo_idx: int) -> Dict[str, Any]:
1933
- """Redo undone turns chronologically up to redo_idx turns from the oldest undone.
1934
-
1935
- redo_history stores turns in reverse-chronological order: [C, B, A] where A
1936
- is at the end (most recently undone = earliest chronologically = first to redo).
1937
-
1938
- The dialog shows them in chronological order [A, B, C] so:
1939
- redo_idx=0 → redo just A (the last item in redo_history)
1940
- redo_idx=1 → redo A + B (the last 2 items, applied A first)
1941
- redo_idx=2 → redo A+B+C (all, applied A→B→C)
1942
- """
1869
+ """Redo all undone turns from index 0 up to redo_idx (inclusive)."""
1943
1870
  if not hasattr(self, "redo_history") or not self.redo_history or redo_idx >= len(self.redo_history):
1944
1871
  return {"reverted": [], "errors": ["No undone turns to redo."]}
1945
-
1946
- n = redo_idx + 1 # number of turns to redo
1947
-
1948
- # Take the n chronologically-oldest turns from the END of redo_history
1949
- # redo_history = [C, B, A] → last n=2 = [B, A] → reversed = [A, B] ✓
1950
- turns_to_redo = list(reversed(self.redo_history[-n:]))
1951
- # Remove those n items from the end
1952
- self.redo_history = self.redo_history[:-n] if n < len(self.redo_history) else []
1953
-
1872
+
1873
+ # Get the slice of turns to redo
1874
+ turns_to_redo = self.redo_history[:redo_idx + 1]
1875
+ # Keep the remaining undone turns
1876
+ self.redo_history = self.redo_history[redo_idx + 1:]
1877
+
1954
1878
  res: Dict[str, Any] = {"redone_code": [], "errors": []}
1955
-
1956
- # Apply in chronological order (A → B → C)
1879
+
1880
+ # Redo them in order
1957
1881
  for turn in turns_to_redo:
1958
1882
  # Redo code changes
1959
1883
  for ch in turn.get("changes", []):
@@ -1977,19 +1901,18 @@ class Orchestrator:
1977
1901
  res["redone_code"].append(path)
1978
1902
  except Exception as e:
1979
1903
  res["errors"].append(f"{path}: {e}")
1980
-
1981
- # Redo messages: append them back in order
1904
+
1905
+ # Redo messages: append them back
1982
1906
  msg_start = len(self.messages)
1983
1907
  messages_to_add = turn.get("messages", [])
1984
1908
  self.messages.extend(messages_to_add)
1985
1909
  msg_end = len(self.messages)
1986
-
1910
+
1987
1911
  # Reconstruct the turn entry and append back to turn_history
1988
1912
  turn["msg_start"] = msg_start
1989
1913
  turn["msg_end"] = msg_end
1990
1914
  self.turn_history.append(turn)
1991
-
1992
- self._post_undo_dirty = False # Redo completed — dirty flag cleared
1915
+
1993
1916
  self._persist_messages()
1994
1917
  return res
1995
1918
 
@@ -2697,10 +2620,6 @@ class Orchestrator:
2697
2620
  pass
2698
2621
 
2699
2622
  self.messages.append({"role": "user", "content": user_message})
2700
- # Track dirty redo: if there was redo history when the user typed this new prompt,
2701
- # flag it so /redo can warn them instead of silently losing the redo stack
2702
- if self.redo_history:
2703
- self._post_undo_dirty = True
2704
2623
  self.redo_history = [] # Clear redo history on new user action
2705
2624
  self._persist_messages(in_progress_turn={
2706
2625
  "user_msg": user_message,
@@ -157,17 +157,11 @@ def get_situational_multiplier(content: str, task_type: str, user_prompt: str =
157
157
  except Exception:
158
158
  pass
159
159
 
160
- # 2. Command Chaining & Operator Pattern Boosting ONLY for explicit command-execution tasks
161
- # Must match BOTH the task type AND have specific command-execution words (not just "run")
162
- COMMAND_EXECUTION_WORDS = ["exec", "execute", "command", "shell", "powershell", "cmd", "terminal", "install", "npm run", "pip install", "run tests", "run script"]
163
- is_command_prompt = any(w in user_prompt.lower() for w in COMMAND_EXECUTION_WORDS)
160
+ # 2. Command Chaining & Operator Pattern Boosting (e.g., && vs ; separation)
161
+ is_command_prompt = any(w in user_prompt.lower() for w in ["run", "exec", "command", "shell", "powershell", "cmd", "terminal", "install", "test"])
164
162
  if any(k in c_lower for k in ["&&", "command", "shell", "powershell", "cmd", "operator", "separate"]):
165
- # Only boost if the task is explicitly about running commands AND the prompt is command-related
166
- if task_type in ["setup", "testing"] and is_command_prompt:
163
+ if task_type in ["setup", "testing"] or is_command_prompt:
167
164
  multiplier *= 2.2
168
- elif task_type == "general" or not is_command_prompt:
169
- # Suppress command-operator hints completely for general/conversational prompts
170
- multiplier *= 0.1
171
165
 
172
166
  # 3. Operating System and Platform Pattern Boosting
173
167
  if os.name == "nt": # Windows
@@ -211,19 +205,9 @@ def get_situational_multiplier(content: str, task_type: str, user_prompt: str =
211
205
 
212
206
 
213
207
  def score_and_filter_context(items: List[Dict], user_prompt: str, limit: int = 5) -> List[Dict]:
214
- """Scores a list of retrieved context items and drops any below a minimum relevance threshold.
215
-
216
- Items with a base_score below MIN_BASE_SCORE are discarded before situational scoring,
217
- preventing irrelevant experiences (e.g. PowerShell tips) from being injected on
218
- unrelated conversational prompts like 'hello'.
219
- """
208
+ """Scores a list of retrieved context items based on neural conceptual pattern matching."""
220
209
  if not items:
221
210
  return []
222
-
223
- # Minimum cosine-similarity-derived base score to be eligible for injection.
224
- # base_score = 1/(1+distance), so distance=0.7 → base_score≈0.59.
225
- # Items with lower scores are semantically too far from the query.
226
- MIN_BASE_SCORE = 0.55
227
211
 
228
212
  task_type = classify_task_type(user_prompt)
229
213
  scored_items = []
@@ -232,10 +216,6 @@ def score_and_filter_context(items: List[Dict], user_prompt: str, limit: int = 5
232
216
  content = item.get("content", "")
233
217
  base_score = item.get("base_score", 1.0)
234
218
 
235
- # Hard gate: drop items that are too semantically distant regardless of boosts
236
- if base_score < MIN_BASE_SCORE:
237
- continue
238
-
239
219
  # Calculate neural pattern multiplier
240
220
  multiplier = get_situational_multiplier(content, task_type, user_prompt=user_prompt)
241
221
  situational_score = base_score * multiplier
@@ -3130,10 +3130,9 @@ def store_experience(category: str, content: str, priority: int = None, subagent
3130
3130
  from pathlib import Path
3131
3131
 
3132
3132
  try:
3133
- # Use config.local_dir for project-local path resolution
3134
- from utim_cli.config import config as _cfg
3135
- utim_dir = _cfg.local_dir
3136
- utim_dir.mkdir(parents=True, exist_ok=True)
3133
+ # Ensure .utim directory exists
3134
+ utim_dir = Path('.utim')
3135
+ utim_dir.mkdir(exist_ok=True)
3137
3136
  exp_file = utim_dir / 'experiences.json'
3138
3137
 
3139
3138
  # Load existing experiences
@@ -3198,10 +3197,9 @@ def recall_experience(query: str, limit: int = 5, subagent: str = None) -> str:
3198
3197
  from pathlib import Path
3199
3198
  import json
3200
3199
  from utim_cli.state import STATE
3201
- from utim_cli.config import config as _cfg
3202
3200
 
3203
3201
  results_str = []
3204
- utim_dir = _cfg.local_dir
3202
+ utim_dir = Path('.utim')
3205
3203
  exp_file = utim_dir / 'experiences.json'
3206
3204
 
3207
3205
  # Search experiences from experiences.json