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.
- utim_cli-1.44.1/CHANGELOG.md +29 -0
- {utim_cli-1.44.0/utim_cli.egg-info → utim_cli-1.44.1}/PKG-INFO +1 -1
- {utim_cli-1.44.0 → utim_cli-1.44.1}/pyproject.toml +1 -1
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/backup.py +8 -32
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/bootstrap.py +14 -38
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/config.py +2 -43
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/orchestrator.py +14 -95
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/situational_scoring.py +4 -24
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/tools.py +4 -6
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/utim.py +244 -315
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/vector_memory.py +7 -29
- {utim_cli-1.44.0 → utim_cli-1.44.1/utim_cli.egg-info}/PKG-INFO +1 -1
- utim_cli-1.44.0/CHANGELOG.md +0 -51
- {utim_cli-1.44.0 → utim_cli-1.44.1}/LICENSE +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/MANIFEST.in +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/README.md +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/setup.cfg +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/setup.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/__init__.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/agent.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/auth.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/billing.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/blender_agent.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/client_utils.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/context_pruner.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/doctor.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/logger.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/mcp_client.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/reflection.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/report.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/share.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/share_tui.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/state.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli/workspace.py +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli.egg-info/SOURCES.txt +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-1.44.0 → utim_cli-1.44.1}/utim_cli.egg-info/requires.txt +0 -0
- {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
|
+
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "utim-cli"
|
|
7
|
-
version = "1.44.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
77
|
-
dst = os.path.join(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
108
|
-
dst = os.path.join(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1947
|
-
|
|
1948
|
-
#
|
|
1949
|
-
|
|
1950
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
|
161
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
3134
|
-
|
|
3135
|
-
utim_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 =
|
|
3202
|
+
utim_dir = Path('.utim')
|
|
3205
3203
|
exp_file = utim_dir / 'experiences.json'
|
|
3206
3204
|
|
|
3207
3205
|
# Search experiences from experiences.json
|