yncli 1.0.7__tar.gz → 1.0.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.
- {yncli-1.0.7 → yncli-1.0.9}/PKG-INFO +1 -1
- {yncli-1.0.7 → yncli-1.0.9}/pyproject.toml +1 -1
- {yncli-1.0.7 → yncli-1.0.9}/setup.py +1 -1
- {yncli-1.0.7 → yncli-1.0.9}/yncli/client.py +4 -2
- {yncli-1.0.7 → yncli-1.0.9}/yncli/main.py +11 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/tui.py +35 -17
- yncli-1.0.9/yncli/update_checker.py +120 -0
- yncli-1.0.9/yncli/version.py +1 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli.egg-info/PKG-INFO +1 -1
- yncli-1.0.7/yncli/update_checker.py +0 -86
- yncli-1.0.7/yncli/version.py +0 -1
- {yncli-1.0.7 → yncli-1.0.9}/README.md +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/setup.cfg +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/__init__.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/agent.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/architect.md +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/code_review.md +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/debug_oracle.md +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/debugger.md +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/polyglot.md +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/refactor_master.md +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/security_auditor.md +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/system_architect.md +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/test_runner.md +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/ultrabrain.md +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/clean_text.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/config.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/language_detector.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/skills_manager.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/system_prompt.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/tools/__init__.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/tools/file_tools.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/tools/polyglot_tools.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/tools/search_tools.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/tools/system_tools.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/workspace_memory.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli/workspace_scanner.py +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli.egg-info/SOURCES.txt +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli.egg-info/dependency_links.txt +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli.egg-info/entry_points.txt +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli.egg-info/requires.txt +0 -0
- {yncli-1.0.7 → yncli-1.0.9}/yncli.egg-info/top_level.txt +0 -0
|
@@ -6,7 +6,7 @@ long_description = (this_directory / "README.md").read_text(encoding="utf-8") if
|
|
|
6
6
|
|
|
7
7
|
setup(
|
|
8
8
|
name="yncli",
|
|
9
|
-
version="1.0.
|
|
9
|
+
version="1.0.9",
|
|
10
10
|
description="Autonomous Polyglot AI Coding Agent & TUI",
|
|
11
11
|
long_description=long_description,
|
|
12
12
|
long_description_content_type="text/markdown",
|
|
@@ -21,7 +21,7 @@ class LLMClient:
|
|
|
21
21
|
"""
|
|
22
22
|
url = f"{self.base_url}/models"
|
|
23
23
|
try:
|
|
24
|
-
resp = requests.get(url, headers={"Authorization": f"Bearer {self.api_key}"}, timeout=
|
|
24
|
+
resp = requests.get(url, headers={"Authorization": f"Bearer {self.api_key}"}, timeout=15)
|
|
25
25
|
resp.raise_for_status()
|
|
26
26
|
data = resp.json()
|
|
27
27
|
return data.get("data", [])
|
|
@@ -41,6 +41,7 @@ class LLMClient:
|
|
|
41
41
|
"""
|
|
42
42
|
Streams chat completions from the OpenAI-compatible endpoint.
|
|
43
43
|
Guarantees strict UTF-8 stream decoding to prevent ISO-8859-1 mojibake.
|
|
44
|
+
Uses resilient timeout (connect=15s, read=180s) to support heavy code generation.
|
|
44
45
|
"""
|
|
45
46
|
url = f"{self.base_url}/chat/completions"
|
|
46
47
|
payload = {
|
|
@@ -55,7 +56,8 @@ class LLMClient:
|
|
|
55
56
|
payload["tools"] = tools
|
|
56
57
|
payload["tool_choice"] = "auto"
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
# Resilient timeout: (connect_timeout=15s, read_timeout=180s)
|
|
60
|
+
resp = requests.post(url, headers=self._get_headers(), json=payload, stream=True, timeout=(15, 180))
|
|
59
61
|
resp.encoding = "utf-8"
|
|
60
62
|
|
|
61
63
|
if resp.status_code != 200:
|
|
@@ -74,8 +74,19 @@ def main():
|
|
|
74
74
|
action="store_true",
|
|
75
75
|
help="List available models and exit"
|
|
76
76
|
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--update", "-u",
|
|
79
|
+
action="store_true",
|
|
80
|
+
help="Check and automatically update yncli to the latest version"
|
|
81
|
+
)
|
|
77
82
|
|
|
78
83
|
args = parser.parse_args()
|
|
84
|
+
|
|
85
|
+
if args.update:
|
|
86
|
+
from yncli.update_checker import perform_self_update
|
|
87
|
+
perform_self_update()
|
|
88
|
+
sys.exit(0)
|
|
89
|
+
|
|
79
90
|
config = load_config()
|
|
80
91
|
|
|
81
92
|
base_url = config.get("base_url")
|
|
@@ -353,6 +353,11 @@ class TerminalUI:
|
|
|
353
353
|
self.console.print(table)
|
|
354
354
|
return True
|
|
355
355
|
|
|
356
|
+
elif cmd in ("/update", "/upgrade"):
|
|
357
|
+
from yncli.update_checker import perform_self_update
|
|
358
|
+
perform_self_update()
|
|
359
|
+
return True
|
|
360
|
+
|
|
356
361
|
elif cmd == "/save":
|
|
357
362
|
fname = args[0] if args else f"chat_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
|
|
358
363
|
lines = [f"# yncli export - {datetime.datetime.now().isoformat()}\n\n"]
|
|
@@ -452,10 +457,17 @@ class TerminalUI:
|
|
|
452
457
|
def execute_turn(self, user_prompt: str) -> None:
|
|
453
458
|
content_chunks = []
|
|
454
459
|
is_thinking = False
|
|
460
|
+
step_history: List[str] = []
|
|
461
|
+
|
|
462
|
+
def _update_live_display(current_status: str) -> None:
|
|
463
|
+
# Maintain max 3 recent steps
|
|
464
|
+
display_lines = list(step_history[-2:]) + [f"[bold cyan]●[/bold cyan] {current_status}"]
|
|
465
|
+
banner_text = "\n".join(display_lines)
|
|
466
|
+
self._start_spinner(banner_text)
|
|
455
467
|
|
|
456
468
|
def on_status_update(status_text: str) -> None:
|
|
457
469
|
if not is_thinking:
|
|
458
|
-
|
|
470
|
+
_update_live_display(f"[bold #38bdf8][ - ][/bold #38bdf8] {status_text}")
|
|
459
471
|
|
|
460
472
|
def on_thinking(chunk: str) -> None:
|
|
461
473
|
nonlocal is_thinking
|
|
@@ -482,29 +494,35 @@ class TerminalUI:
|
|
|
482
494
|
is_thinking = False
|
|
483
495
|
|
|
484
496
|
target = args.get("file_path", args.get("path", args.get("command", args.get("query", ""))))
|
|
485
|
-
if len(str(target)) >
|
|
486
|
-
target = str(target)[:
|
|
497
|
+
if len(str(target)) > 40:
|
|
498
|
+
target = str(target)[:40] + "..."
|
|
487
499
|
|
|
488
|
-
|
|
489
|
-
"list_directory": "
|
|
490
|
-
"
|
|
491
|
-
"
|
|
492
|
-
"
|
|
493
|
-
"
|
|
494
|
-
"
|
|
495
|
-
"
|
|
496
|
-
"
|
|
497
|
-
"
|
|
500
|
+
tool_stage_map = {
|
|
501
|
+
"list_directory": f"Memeriksa struktur folder ({target})",
|
|
502
|
+
"find_files": f"Mencari file ({target})",
|
|
503
|
+
"grep_search": f"Mencari isi kode ({target})",
|
|
504
|
+
"read_file": f"Membaca file ({target})",
|
|
505
|
+
"write_file": f"Building & menulis file ({target})",
|
|
506
|
+
"edit_file_replace": f"Memperbarui kode ({target})",
|
|
507
|
+
"save_plan_document": f"Menyimpan rencana ({target})",
|
|
508
|
+
"run_terminal_command": f"Menjalankan perintah terminal ({target})",
|
|
509
|
+
"web_search": f"Riset web ({target})",
|
|
510
|
+
"fetch_webpage": f"Mengambil referensi web ({target})",
|
|
511
|
+
"change_directory": f"Berpindah direktori ({target})"
|
|
498
512
|
}
|
|
499
|
-
|
|
513
|
+
stage_desc = tool_stage_map.get(name, f"Eksekusi {name} ({target})")
|
|
500
514
|
|
|
501
|
-
#
|
|
502
|
-
|
|
515
|
+
# Add previous action as completed step (max 3 sliding window)
|
|
516
|
+
step_history.append(f"[dim green]✔[/dim green] [dim]{stage_desc}[/dim]")
|
|
517
|
+
if len(step_history) > 3:
|
|
518
|
+
step_history.pop(0)
|
|
519
|
+
|
|
520
|
+
_update_live_display(f"[bold #f59e0b][ - ][/bold #f59e0b] [bold white]{stage_desc}[/bold white]")
|
|
503
521
|
|
|
504
522
|
def on_tool_end(name: str, output: str) -> None:
|
|
505
523
|
pass
|
|
506
524
|
|
|
507
|
-
|
|
525
|
+
_update_live_display(f"[bold #38bdf8][ - ][/bold #38bdf8] Menganalisis instruksi ({self.agent.model})...")
|
|
508
526
|
try:
|
|
509
527
|
res = self.agent.run_turn(
|
|
510
528
|
user_prompt=user_prompt,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import sys
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
|
+
|
|
8
|
+
from yncli.version import __version__
|
|
9
|
+
from yncli.config import CONFIG_DIR
|
|
10
|
+
|
|
11
|
+
CACHE_FILE = CONFIG_DIR / "update_cache.json"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_version_tuple(v: str):
|
|
15
|
+
"""Parse version string into tuple of ints for accurate semver comparison."""
|
|
16
|
+
try:
|
|
17
|
+
clean = v.strip().lstrip("v").split("-")[0]
|
|
18
|
+
return tuple(int(x) for x in clean.split(".") if x.isdigit())
|
|
19
|
+
except Exception:
|
|
20
|
+
return (0, 0, 0)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fetch_latest_version_live(timeout_sec: float = 1.2) -> Optional[str]:
|
|
24
|
+
"""Queries PyPI directly to get the absolute latest published version."""
|
|
25
|
+
try:
|
|
26
|
+
import requests
|
|
27
|
+
resp = requests.get("https://pypi.org/pypi/yncli/json", timeout=timeout_sec)
|
|
28
|
+
if resp.status_code == 200:
|
|
29
|
+
data = resp.json()
|
|
30
|
+
latest = data.get("info", {}).get("version")
|
|
31
|
+
if latest:
|
|
32
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
CACHE_FILE.write_text(json.dumps({
|
|
34
|
+
"last_checked": time.time(),
|
|
35
|
+
"latest_version": latest
|
|
36
|
+
}, indent=2), encoding="utf-8")
|
|
37
|
+
return latest
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_cached_update() -> Optional[Dict[str, Any]]:
|
|
44
|
+
"""Returns update info from cache if available."""
|
|
45
|
+
try:
|
|
46
|
+
if CACHE_FILE.exists():
|
|
47
|
+
data = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
|
48
|
+
latest_str = data.get("latest_version")
|
|
49
|
+
if latest_str:
|
|
50
|
+
curr_tuple = parse_version_tuple(__version__)
|
|
51
|
+
latest_tuple = parse_version_tuple(latest_str)
|
|
52
|
+
if latest_tuple > curr_tuple:
|
|
53
|
+
return {
|
|
54
|
+
"current": __version__,
|
|
55
|
+
"latest": latest_str
|
|
56
|
+
}
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def check_for_updates_fast(timeout_sec: float = 1.2) -> Optional[Dict[str, Any]]:
|
|
63
|
+
"""
|
|
64
|
+
Checks for updates immediately on every startup with a fast 1.2s timeout.
|
|
65
|
+
Falls back to cached update if offline or timeout.
|
|
66
|
+
"""
|
|
67
|
+
# 1. Try fast live fetch
|
|
68
|
+
latest_live = fetch_latest_version_live(timeout_sec=timeout_sec)
|
|
69
|
+
if latest_live:
|
|
70
|
+
curr_tuple = parse_version_tuple(__version__)
|
|
71
|
+
latest_tuple = parse_version_tuple(latest_live)
|
|
72
|
+
if latest_tuple > curr_tuple:
|
|
73
|
+
return {
|
|
74
|
+
"current": __version__,
|
|
75
|
+
"latest": latest_live
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# 2. Fallback to cache if network is slow/offline
|
|
79
|
+
return get_cached_update()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def perform_self_update() -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Direct self-updater: Runs pip install --upgrade --no-cache-dir yncli
|
|
85
|
+
and optionally npm install -g @yanzyuyu/yncli
|
|
86
|
+
"""
|
|
87
|
+
print(f"\n[YNCLI] Memeriksa versi terbaru dari PyPI...")
|
|
88
|
+
latest = fetch_latest_version_live(timeout_sec=5.0)
|
|
89
|
+
curr_tuple = parse_version_tuple(__version__)
|
|
90
|
+
latest_tuple = parse_version_tuple(latest) if latest else curr_tuple
|
|
91
|
+
|
|
92
|
+
print(f"[YNCLI] Versi saat ini: v{__version__}")
|
|
93
|
+
if latest:
|
|
94
|
+
print(f"[YNCLI] Versi terbaru: v{latest}")
|
|
95
|
+
|
|
96
|
+
if latest and latest_tuple <= curr_tuple:
|
|
97
|
+
print("\n[OK] YNCLI Anda sudah menggunakan versi paling baru!")
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
print("\n[YNCLI] Mengunduh dan menginstal pembaruan...")
|
|
101
|
+
|
|
102
|
+
# 1. Update Python module via pip
|
|
103
|
+
pip_cmd = [
|
|
104
|
+
sys.executable, "-m", "pip", "install",
|
|
105
|
+
"--upgrade", "--no-cache-dir",
|
|
106
|
+
"--disable-pip-version-check",
|
|
107
|
+
f"yncli=={latest}" if latest else "yncli"
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
res = subprocess.run(pip_cmd)
|
|
111
|
+
if res.returncode == 0:
|
|
112
|
+
print("\n[SUCCESS] YNCLI berhasil diperbarui ke versi terbaru!")
|
|
113
|
+
print("Silakan jalankan 'yncli' kembali untuk menikmati fitur baru.\n")
|
|
114
|
+
# Clear update cache
|
|
115
|
+
CACHE_FILE.unlink(missing_ok=True)
|
|
116
|
+
return True
|
|
117
|
+
else:
|
|
118
|
+
print("\n[ERROR] Gagal memperbarui via pip. Coba jalankan manual:")
|
|
119
|
+
print(f"pip install --upgrade --no-cache-dir yncli\n")
|
|
120
|
+
return False
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.9"
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import time
|
|
3
|
-
import threading
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Optional, Dict, Any
|
|
6
|
-
|
|
7
|
-
from yncli.version import __version__
|
|
8
|
-
from yncli.config import CONFIG_DIR
|
|
9
|
-
|
|
10
|
-
CACHE_FILE = CONFIG_DIR / "update_cache.json"
|
|
11
|
-
CHECK_INTERVAL_SECONDS = 2 * 3600 # 2 hours
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def parse_version_tuple(v: str):
|
|
15
|
-
"""Parse version string into tuple of ints for accurate semver comparison."""
|
|
16
|
-
try:
|
|
17
|
-
clean = v.strip().lstrip("v").split("-")[0]
|
|
18
|
-
return tuple(int(x) for x in clean.split(".") if x.isdigit())
|
|
19
|
-
except Exception:
|
|
20
|
-
return (0, 0, 0)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _fetch_latest_version_pypi(timeout_sec: float = 1.0) -> Optional[str]:
|
|
24
|
-
try:
|
|
25
|
-
import requests
|
|
26
|
-
resp = requests.get("https://pypi.org/pypi/yncli/json", timeout=timeout_sec)
|
|
27
|
-
if resp.status_code == 200:
|
|
28
|
-
data = resp.json()
|
|
29
|
-
return data.get("info", {}).get("version")
|
|
30
|
-
except Exception:
|
|
31
|
-
pass
|
|
32
|
-
return None
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def get_cached_update() -> Optional[Dict[str, Any]]:
|
|
36
|
-
"""Returns cached update info if available and latest > current."""
|
|
37
|
-
try:
|
|
38
|
-
if CACHE_FILE.exists():
|
|
39
|
-
data = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
|
40
|
-
latest_str = data.get("latest_version")
|
|
41
|
-
if latest_str:
|
|
42
|
-
curr_tuple = parse_version_tuple(__version__)
|
|
43
|
-
latest_tuple = parse_version_tuple(latest_str)
|
|
44
|
-
if latest_tuple > curr_tuple:
|
|
45
|
-
return {
|
|
46
|
-
"current": __version__,
|
|
47
|
-
"latest": latest_str
|
|
48
|
-
}
|
|
49
|
-
except Exception:
|
|
50
|
-
pass
|
|
51
|
-
return None
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def check_for_updates_fast(timeout_sec: float = 1.0) -> Optional[Dict[str, Any]]:
|
|
55
|
-
"""Checks cache first, and if not cached or expired, queries PyPI quickly."""
|
|
56
|
-
cached = get_cached_update()
|
|
57
|
-
if cached:
|
|
58
|
-
return cached
|
|
59
|
-
|
|
60
|
-
try:
|
|
61
|
-
now = time.time()
|
|
62
|
-
last_checked = 0
|
|
63
|
-
if CACHE_FILE.exists():
|
|
64
|
-
try:
|
|
65
|
-
data = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
|
|
66
|
-
last_checked = data.get("last_checked", 0)
|
|
67
|
-
except Exception:
|
|
68
|
-
pass
|
|
69
|
-
|
|
70
|
-
if (now - last_checked) > CHECK_INTERVAL_SECONDS:
|
|
71
|
-
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
72
|
-
latest = _fetch_latest_version_pypi(timeout_sec=timeout_sec)
|
|
73
|
-
if latest:
|
|
74
|
-
cache_data = {
|
|
75
|
-
"last_checked": now,
|
|
76
|
-
"latest_version": latest
|
|
77
|
-
}
|
|
78
|
-
CACHE_FILE.write_text(json.dumps(cache_data, indent=2), encoding="utf-8")
|
|
79
|
-
curr_tuple = parse_version_tuple(__version__)
|
|
80
|
-
latest_tuple = parse_version_tuple(latest)
|
|
81
|
-
if latest_tuple > curr_tuple:
|
|
82
|
-
return {"current": __version__, "latest": latest}
|
|
83
|
-
except Exception:
|
|
84
|
-
pass
|
|
85
|
-
|
|
86
|
-
return None
|
yncli-1.0.7/yncli/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.0.7"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|