yncli 1.0.8__tar.gz → 1.1.0__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. {yncli-1.0.8 → yncli-1.1.0}/PKG-INFO +1 -1
  2. {yncli-1.0.8 → yncli-1.1.0}/pyproject.toml +1 -1
  3. {yncli-1.0.8 → yncli-1.1.0}/setup.py +1 -1
  4. {yncli-1.0.8 → yncli-1.1.0}/yncli/main.py +11 -0
  5. yncli-1.1.0/yncli/mention_resolver.py +88 -0
  6. {yncli-1.0.8 → yncli-1.1.0}/yncli/tui.py +42 -1
  7. yncli-1.1.0/yncli/update_checker.py +120 -0
  8. yncli-1.1.0/yncli/version.py +1 -0
  9. {yncli-1.0.8 → yncli-1.1.0}/yncli.egg-info/PKG-INFO +1 -1
  10. {yncli-1.0.8 → yncli-1.1.0}/yncli.egg-info/SOURCES.txt +1 -0
  11. yncli-1.0.8/yncli/update_checker.py +0 -86
  12. yncli-1.0.8/yncli/version.py +0 -1
  13. {yncli-1.0.8 → yncli-1.1.0}/README.md +0 -0
  14. {yncli-1.0.8 → yncli-1.1.0}/setup.cfg +0 -0
  15. {yncli-1.0.8 → yncli-1.1.0}/yncli/__init__.py +0 -0
  16. {yncli-1.0.8 → yncli-1.1.0}/yncli/agent.py +0 -0
  17. {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/architect.md +0 -0
  18. {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/code_review.md +0 -0
  19. {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/debug_oracle.md +0 -0
  20. {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/debugger.md +0 -0
  21. {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/polyglot.md +0 -0
  22. {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/refactor_master.md +0 -0
  23. {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/security_auditor.md +0 -0
  24. {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/system_architect.md +0 -0
  25. {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/test_runner.md +0 -0
  26. {yncli-1.0.8 → yncli-1.1.0}/yncli/built_in_skills/ultrabrain.md +0 -0
  27. {yncli-1.0.8 → yncli-1.1.0}/yncli/clean_text.py +0 -0
  28. {yncli-1.0.8 → yncli-1.1.0}/yncli/client.py +0 -0
  29. {yncli-1.0.8 → yncli-1.1.0}/yncli/config.py +0 -0
  30. {yncli-1.0.8 → yncli-1.1.0}/yncli/language_detector.py +0 -0
  31. {yncli-1.0.8 → yncli-1.1.0}/yncli/skills_manager.py +0 -0
  32. {yncli-1.0.8 → yncli-1.1.0}/yncli/system_prompt.py +0 -0
  33. {yncli-1.0.8 → yncli-1.1.0}/yncli/tools/__init__.py +0 -0
  34. {yncli-1.0.8 → yncli-1.1.0}/yncli/tools/file_tools.py +0 -0
  35. {yncli-1.0.8 → yncli-1.1.0}/yncli/tools/polyglot_tools.py +0 -0
  36. {yncli-1.0.8 → yncli-1.1.0}/yncli/tools/search_tools.py +0 -0
  37. {yncli-1.0.8 → yncli-1.1.0}/yncli/tools/system_tools.py +0 -0
  38. {yncli-1.0.8 → yncli-1.1.0}/yncli/workspace_memory.py +0 -0
  39. {yncli-1.0.8 → yncli-1.1.0}/yncli/workspace_scanner.py +0 -0
  40. {yncli-1.0.8 → yncli-1.1.0}/yncli.egg-info/dependency_links.txt +0 -0
  41. {yncli-1.0.8 → yncli-1.1.0}/yncli.egg-info/entry_points.txt +0 -0
  42. {yncli-1.0.8 → yncli-1.1.0}/yncli.egg-info/requires.txt +0 -0
  43. {yncli-1.0.8 → yncli-1.1.0}/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.8
3
+ Version: 1.1.0
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.8"
7
+ version = "1.1.0"
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.8",
9
+ version="1.1.0",
10
10
  description="Autonomous Polyglot AI Coding Agent & TUI",
11
11
  long_description=long_description,
12
12
  long_description_content_type="text/markdown",
@@ -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")
@@ -0,0 +1,88 @@
1
+ import re
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Tuple, List, Dict
5
+
6
+
7
+ def resolve_file_mentions(prompt: str, workspace_dir: str = ".") -> Tuple[str, List[Dict[str, str]]]:
8
+ """
9
+ Scans the prompt for @file or @folder mentions (e.g. @app/Http/Controllers/UserController.php or @index.html).
10
+ Loads the contents of referenced files and appends them as explicit contextual attachments.
11
+
12
+ Returns:
13
+ (cleaned_prompt, list_of_attached_files)
14
+ """
15
+ cwd = Path(workspace_dir).resolve()
16
+
17
+ # Matches patterns like @filename.ext or @"path with spaces/file.ext" or @path/to/file
18
+ mention_pattern = re.compile(r'@(?:"([^"]+)"|([a-zA-Z0-9_\-\.\/\\]+))')
19
+
20
+ matches = mention_pattern.findall(prompt)
21
+ if not matches:
22
+ return prompt, []
23
+
24
+ attached_files = []
25
+ seen_paths = set()
26
+
27
+ for m_quoted, m_unquoted in matches:
28
+ rel_path = (m_quoted or m_unquoted).strip()
29
+ if not rel_path:
30
+ continue
31
+
32
+ clean_path = rel_path.replace("\\", "/")
33
+ if clean_path in seen_paths:
34
+ continue
35
+ seen_paths.add(clean_path)
36
+
37
+ target_file = cwd / rel_path
38
+ if target_file.exists() and target_file.is_file():
39
+ try:
40
+ # Read content up to 100KB
41
+ size = target_file.stat().st_size
42
+ if size < 150000:
43
+ with open(target_file, "r", encoding="utf-8", errors="replace") as fp:
44
+ content = fp.read()
45
+ attached_files.append({
46
+ "path": clean_path,
47
+ "content": content,
48
+ "lines": len(content.splitlines()),
49
+ "size_bytes": size
50
+ })
51
+ except Exception:
52
+ pass
53
+ elif not target_file.exists():
54
+ # Try searching in subdirectories
55
+ for root, _, files in os.walk(cwd):
56
+ for f in files:
57
+ if f.lower() == Path(rel_path).name.lower():
58
+ found = Path(root) / f
59
+ try:
60
+ rel_found = found.relative_to(cwd).as_posix()
61
+ if rel_found not in seen_paths and found.stat().st_size < 150000:
62
+ seen_paths.add(rel_found)
63
+ with open(found, "r", encoding="utf-8", errors="replace") as fp:
64
+ content = fp.read()
65
+ attached_files.append({
66
+ "path": rel_found,
67
+ "content": content,
68
+ "lines": len(content.splitlines()),
69
+ "size_bytes": found.stat().st_size
70
+ })
71
+ except Exception:
72
+ pass
73
+ break
74
+
75
+ if not attached_files:
76
+ return prompt, []
77
+
78
+ # Build enriched context block
79
+ enrichment_blocks = ["\n\n### [USER ATTACHED @FILES CONTEXT]:"]
80
+ for item in attached_files:
81
+ enrichment_blocks.append(
82
+ f"\n--- ATTACHED FILE: @{item['path']} ({item['lines']} lines) ---\n"
83
+ f"{item['content']}\n"
84
+ f"--- END OF @{item['path']} ---"
85
+ )
86
+
87
+ enriched_prompt = prompt + "\n" + "\n".join(enrichment_blocks)
88
+ return enriched_prompt, attached_files
@@ -50,6 +50,7 @@ class TerminalUI:
50
50
  from prompt_toolkit.history import FileHistory
51
51
  from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
52
52
  from prompt_toolkit.styles import Style
53
+ from prompt_toolkit.completion import Completer, Completion
53
54
 
54
55
  CONFIG_DIR.mkdir(parents=True, exist_ok=True)
55
56
  history_file = CONFIG_DIR / "history.txt"
@@ -57,11 +58,37 @@ class TerminalUI:
57
58
  prompt_style = Style.from_dict({
58
59
  "prompt": "#38bdf8",
59
60
  "arrow": "#a855f7",
61
+ "completion-menu.completion": "bg:#1e1e2e #cdd6f4",
62
+ "completion-menu.completion.current": "bg:#313244 #89b4fa bold",
63
+ "completion-menu.meta": "bg:#181825 #a6adc8",
60
64
  })
61
65
 
66
+ class MentionCompleter(Completer):
67
+ def __init__(self, workspace_memory):
68
+ self.workspace_memory = workspace_memory
69
+
70
+ def get_completions(self, document, complete_event):
71
+ text = document.text_before_cursor
72
+ # Check if user just typed '@' or is typing a path after '@'
73
+ if "@" in text:
74
+ last_at = text.rfind("@")
75
+ word = text[last_at + 1:]
76
+ # Offer completion for files in workspace tree
77
+ for fpath in self.workspace_memory.file_tree:
78
+ if fpath.lower().startswith(word.lower()) or word.lower() in fpath.lower():
79
+ yield Completion(
80
+ fpath,
81
+ start_position=-len(word),
82
+ display=f"@{fpath}",
83
+ display_meta="file"
84
+ )
85
+
86
+ completer = MentionCompleter(self.agent.workspace_memory)
87
+
62
88
  self.session = PromptSession(
63
89
  history=FileHistory(str(history_file)),
64
90
  auto_suggest=AutoSuggestFromHistory(),
91
+ completer=completer,
65
92
  style=prompt_style
66
93
  )
67
94
  except Exception:
@@ -353,6 +380,11 @@ class TerminalUI:
353
380
  self.console.print(table)
354
381
  return True
355
382
 
383
+ elif cmd in ("/update", "/upgrade"):
384
+ from yncli.update_checker import perform_self_update
385
+ perform_self_update()
386
+ return True
387
+
356
388
  elif cmd == "/save":
357
389
  fname = args[0] if args else f"chat_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
358
390
  lines = [f"# yncli export - {datetime.datetime.now().isoformat()}\n\n"]
@@ -454,6 +486,15 @@ class TerminalUI:
454
486
  is_thinking = False
455
487
  step_history: List[str] = []
456
488
 
489
+ from yncli.mention_resolver import resolve_file_mentions
490
+ resolved_prompt, attached_files = resolve_file_mentions(user_prompt, workspace_dir=self.agent.workspace_dir)
491
+
492
+ if attached_files:
493
+ pills = []
494
+ for af in attached_files:
495
+ pills.append(f"[bold cyan]@{af['path']}[/bold cyan] [dim]({af['lines']} baris)[/dim]")
496
+ self.console.print(f"[dim]📎 Menyertakan file:[/dim] {', '.join(pills)}")
497
+
457
498
  def _update_live_display(current_status: str) -> None:
458
499
  # Maintain max 3 recent steps
459
500
  display_lines = list(step_history[-2:]) + [f"[bold cyan]●[/bold cyan] {current_status}"]
@@ -520,7 +561,7 @@ class TerminalUI:
520
561
  _update_live_display(f"[bold #38bdf8][ - ][/bold #38bdf8] Menganalisis instruksi ({self.agent.model})...")
521
562
  try:
522
563
  res = self.agent.run_turn(
523
- user_prompt=user_prompt,
564
+ user_prompt=resolved_prompt,
524
565
  on_thinking=on_thinking,
525
566
  on_content=on_content,
526
567
  on_tool_start=on_tool_start,
@@ -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.1.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yncli
3
- Version: 1.0.8
3
+ Version: 1.1.0
4
4
  Summary: Autonomous Polyglot AI Coding Agent & TUI
5
5
  Author: Yanzyuyu
6
6
  Requires-Python: >=3.8
@@ -8,6 +8,7 @@ yncli/client.py
8
8
  yncli/config.py
9
9
  yncli/language_detector.py
10
10
  yncli/main.py
11
+ yncli/mention_resolver.py
11
12
  yncli/skills_manager.py
12
13
  yncli/system_prompt.py
13
14
  yncli/tui.py
@@ -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.8"
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