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.
Files changed (42) hide show
  1. {yncli-1.0.7 → yncli-1.0.9}/PKG-INFO +1 -1
  2. {yncli-1.0.7 → yncli-1.0.9}/pyproject.toml +1 -1
  3. {yncli-1.0.7 → yncli-1.0.9}/setup.py +1 -1
  4. {yncli-1.0.7 → yncli-1.0.9}/yncli/client.py +4 -2
  5. {yncli-1.0.7 → yncli-1.0.9}/yncli/main.py +11 -0
  6. {yncli-1.0.7 → yncli-1.0.9}/yncli/tui.py +35 -17
  7. yncli-1.0.9/yncli/update_checker.py +120 -0
  8. yncli-1.0.9/yncli/version.py +1 -0
  9. {yncli-1.0.7 → yncli-1.0.9}/yncli.egg-info/PKG-INFO +1 -1
  10. yncli-1.0.7/yncli/update_checker.py +0 -86
  11. yncli-1.0.7/yncli/version.py +0 -1
  12. {yncli-1.0.7 → yncli-1.0.9}/README.md +0 -0
  13. {yncli-1.0.7 → yncli-1.0.9}/setup.cfg +0 -0
  14. {yncli-1.0.7 → yncli-1.0.9}/yncli/__init__.py +0 -0
  15. {yncli-1.0.7 → yncli-1.0.9}/yncli/agent.py +0 -0
  16. {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/architect.md +0 -0
  17. {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/code_review.md +0 -0
  18. {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/debug_oracle.md +0 -0
  19. {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/debugger.md +0 -0
  20. {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/polyglot.md +0 -0
  21. {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/refactor_master.md +0 -0
  22. {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/security_auditor.md +0 -0
  23. {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/system_architect.md +0 -0
  24. {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/test_runner.md +0 -0
  25. {yncli-1.0.7 → yncli-1.0.9}/yncli/built_in_skills/ultrabrain.md +0 -0
  26. {yncli-1.0.7 → yncli-1.0.9}/yncli/clean_text.py +0 -0
  27. {yncli-1.0.7 → yncli-1.0.9}/yncli/config.py +0 -0
  28. {yncli-1.0.7 → yncli-1.0.9}/yncli/language_detector.py +0 -0
  29. {yncli-1.0.7 → yncli-1.0.9}/yncli/skills_manager.py +0 -0
  30. {yncli-1.0.7 → yncli-1.0.9}/yncli/system_prompt.py +0 -0
  31. {yncli-1.0.7 → yncli-1.0.9}/yncli/tools/__init__.py +0 -0
  32. {yncli-1.0.7 → yncli-1.0.9}/yncli/tools/file_tools.py +0 -0
  33. {yncli-1.0.7 → yncli-1.0.9}/yncli/tools/polyglot_tools.py +0 -0
  34. {yncli-1.0.7 → yncli-1.0.9}/yncli/tools/search_tools.py +0 -0
  35. {yncli-1.0.7 → yncli-1.0.9}/yncli/tools/system_tools.py +0 -0
  36. {yncli-1.0.7 → yncli-1.0.9}/yncli/workspace_memory.py +0 -0
  37. {yncli-1.0.7 → yncli-1.0.9}/yncli/workspace_scanner.py +0 -0
  38. {yncli-1.0.7 → yncli-1.0.9}/yncli.egg-info/SOURCES.txt +0 -0
  39. {yncli-1.0.7 → yncli-1.0.9}/yncli.egg-info/dependency_links.txt +0 -0
  40. {yncli-1.0.7 → yncli-1.0.9}/yncli.egg-info/entry_points.txt +0 -0
  41. {yncli-1.0.7 → yncli-1.0.9}/yncli.egg-info/requires.txt +0 -0
  42. {yncli-1.0.7 → yncli-1.0.9}/yncli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yncli
3
- Version: 1.0.7
3
+ Version: 1.0.9
4
4
  Summary: Autonomous Polyglot AI Coding Agent & TUI
5
5
  Author: Yanzyuyu
6
6
  Requires-Python: >=3.8
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "yncli"
7
- version = "1.0.7"
7
+ version = "1.0.9"
8
8
  description = "Autonomous Polyglot AI Coding Agent & TUI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -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.7",
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=10)
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
- resp = requests.post(url, headers=self._get_headers(), json=payload, stream=True, timeout=60)
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
- self._start_spinner(f"[dim cyan]{status_text}[/dim cyan]")
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)) > 45:
486
- target = str(target)[:45] + "..."
497
+ if len(str(target)) > 40:
498
+ target = str(target)[:40] + "..."
487
499
 
488
- tool_display_map = {
489
- "list_directory": "ListDir",
490
- "read_file": "Read",
491
- "write_file": "Write",
492
- "edit_file_replace": "Edit",
493
- "save_plan_document": "SavePlan",
494
- "run_terminal_command": "Shell",
495
- "web_search": "WebSearch",
496
- "fetch_webpage": "Fetch",
497
- "change_directory": "Cd"
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
- display_name = tool_display_map.get(name, name)
513
+ stage_desc = tool_stage_map.get(name, f"Eksekusi {name} ({target})")
500
514
 
501
- # In-place dynamic single line
502
- self._start_spinner(f" [bold #3b82f6]●[/bold #3b82f6] [bold #f59e0b]{display_name}[/bold #f59e0b][dim]({target})[/dim] Working...")
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
- self._start_spinner(f"Working ({self.agent.model})...")
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yncli
3
- Version: 1.0.7
3
+ Version: 1.0.9
4
4
  Summary: Autonomous Polyglot AI Coding Agent & TUI
5
5
  Author: Yanzyuyu
6
6
  Requires-Python: >=3.8
@@ -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
@@ -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