yncli 1.0.0__py3-none-any.whl
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/__init__.py +5 -0
- yncli/agent.py +277 -0
- yncli/built_in_skills/architect.md +10 -0
- yncli/built_in_skills/code_review.md +8 -0
- yncli/built_in_skills/debug_oracle.md +10 -0
- yncli/built_in_skills/debugger.md +11 -0
- yncli/built_in_skills/polyglot.md +17 -0
- yncli/built_in_skills/refactor_master.md +16 -0
- yncli/built_in_skills/security_auditor.md +18 -0
- yncli/built_in_skills/system_architect.md +10 -0
- yncli/built_in_skills/test_runner.md +14 -0
- yncli/built_in_skills/ultrabrain.md +21 -0
- yncli/clean_text.py +70 -0
- yncli/client.py +146 -0
- yncli/config.py +44 -0
- yncli/language_detector.py +206 -0
- yncli/main.py +137 -0
- yncli/skills_manager.py +61 -0
- yncli/system_prompt.py +102 -0
- yncli/tools/__init__.py +266 -0
- yncli/tools/file_tools.py +206 -0
- yncli/tools/polyglot_tools.py +133 -0
- yncli/tools/search_tools.py +68 -0
- yncli/tools/system_tools.py +140 -0
- yncli/tui.py +536 -0
- yncli/workspace_memory.py +97 -0
- yncli/workspace_scanner.py +99 -0
- yncli-1.0.0.dist-info/METADATA +63 -0
- yncli-1.0.0.dist-info/RECORD +32 -0
- yncli-1.0.0.dist-info/WHEEL +5 -0
- yncli-1.0.0.dist-info/entry_points.txt +2 -0
- yncli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import subprocess
|
|
4
|
+
import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
|
|
8
|
+
from yncli.clean_text import clean_text_for_terminal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_current_datetime_str() -> str:
|
|
12
|
+
now = datetime.datetime.now()
|
|
13
|
+
return now.strftime("%A, %d %B %Y %H:%M:%S %Z")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_system_info(cwd: str = ".") -> Dict[str, Any]:
|
|
17
|
+
now = datetime.datetime.now()
|
|
18
|
+
os_name = platform.system()
|
|
19
|
+
os_release = platform.release()
|
|
20
|
+
shell = "powershell.exe" if os_name == "Windows" else os.getenv("SHELL", "/bin/bash")
|
|
21
|
+
|
|
22
|
+
info = {
|
|
23
|
+
"datetime": get_current_datetime_str(),
|
|
24
|
+
"date_iso": now.isoformat(),
|
|
25
|
+
"os": f"{os_name} {os_release} ({platform.machine()})",
|
|
26
|
+
"shell": shell,
|
|
27
|
+
"cwd": str(Path(cwd).resolve()),
|
|
28
|
+
"user": os.getenv("USERNAME", os.getenv("USER", "developer")),
|
|
29
|
+
}
|
|
30
|
+
return info
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def change_directory(path: str, current_cwd: str = ".") -> Dict[str, Any]:
|
|
34
|
+
"""
|
|
35
|
+
Changes the active working directory for the agent.
|
|
36
|
+
"""
|
|
37
|
+
target = Path(current_cwd) / Path(path) if not Path(path).is_absolute() else Path(path)
|
|
38
|
+
target = target.resolve()
|
|
39
|
+
|
|
40
|
+
if not target.exists():
|
|
41
|
+
return {
|
|
42
|
+
"success": False,
|
|
43
|
+
"message": f"[ERROR] Directory tidak ditemukan: {target}",
|
|
44
|
+
"new_cwd": str(Path(current_cwd).resolve())
|
|
45
|
+
}
|
|
46
|
+
if not target.is_dir():
|
|
47
|
+
return {
|
|
48
|
+
"success": False,
|
|
49
|
+
"message": f"[ERROR] Path bukan direktori: {target}",
|
|
50
|
+
"new_cwd": str(Path(current_cwd).resolve())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
os.chdir(str(target))
|
|
55
|
+
return {
|
|
56
|
+
"success": True,
|
|
57
|
+
"message": f"[SUCCESS] Berhasil pindah ke direktori: {target}",
|
|
58
|
+
"new_cwd": str(target)
|
|
59
|
+
}
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return {
|
|
62
|
+
"success": False,
|
|
63
|
+
"message": f"[ERROR] Gagal berpindah direktori: {str(e)}",
|
|
64
|
+
"new_cwd": str(Path(current_cwd).resolve())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def save_plan_document(content: str, current_cwd: str = ".") -> str:
|
|
69
|
+
"""
|
|
70
|
+
Saves the Technical Implementation Plan / PRD into plan.md in the current workspace.
|
|
71
|
+
Sanitizes content to ensure clean UTF-8 formatting.
|
|
72
|
+
"""
|
|
73
|
+
plan_path = Path(current_cwd).resolve() / "plan.md"
|
|
74
|
+
try:
|
|
75
|
+
cleaned_content = clean_text_for_terminal(content)
|
|
76
|
+
with open(plan_path, "w", encoding="utf-8") as f:
|
|
77
|
+
f.write(cleaned_content)
|
|
78
|
+
return f"[SUCCESS] Dokumen PRD dan rencana implementasi berhasil disimpan ke: {plan_path}"
|
|
79
|
+
except Exception as e:
|
|
80
|
+
return f"[ERROR] Gagal menyimpan plan.md: {str(e)}"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def run_terminal_command(command: str, timeout: int = 60, cwd: str = ".") -> str:
|
|
84
|
+
work_dir = Path(cwd).resolve()
|
|
85
|
+
if not work_dir.exists():
|
|
86
|
+
return f"[ERROR] Working directory does not exist: {work_dir}"
|
|
87
|
+
|
|
88
|
+
is_windows = platform.system() == "Windows"
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
if is_windows:
|
|
92
|
+
process = subprocess.run(
|
|
93
|
+
["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", command],
|
|
94
|
+
cwd=str(work_dir),
|
|
95
|
+
capture_output=True,
|
|
96
|
+
text=True,
|
|
97
|
+
timeout=timeout,
|
|
98
|
+
encoding="utf-8",
|
|
99
|
+
errors="replace"
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
process = subprocess.run(
|
|
103
|
+
command,
|
|
104
|
+
shell=True,
|
|
105
|
+
cwd=str(work_dir),
|
|
106
|
+
capture_output=True,
|
|
107
|
+
text=True,
|
|
108
|
+
timeout=timeout,
|
|
109
|
+
encoding="utf-8",
|
|
110
|
+
errors="replace"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
stdout = process.stdout.strip()
|
|
114
|
+
stderr = process.stderr.strip()
|
|
115
|
+
exit_code = process.returncode
|
|
116
|
+
|
|
117
|
+
output_parts = []
|
|
118
|
+
if stdout:
|
|
119
|
+
output_parts.append(f"STDOUT:\n{stdout}")
|
|
120
|
+
if stderr:
|
|
121
|
+
output_parts.append(f"STDERR:\n{stderr}")
|
|
122
|
+
if not stdout and not stderr:
|
|
123
|
+
output_parts.append("(Perintah selesai tanpa output)")
|
|
124
|
+
|
|
125
|
+
output_parts.append(f"\n[Process exited with code {exit_code}]")
|
|
126
|
+
return "\n".join(output_parts)
|
|
127
|
+
|
|
128
|
+
except subprocess.TimeoutExpired:
|
|
129
|
+
return f"[ERROR] Perintah melebihi batas waktu ({timeout} detik)."
|
|
130
|
+
except Exception as e:
|
|
131
|
+
return f"[ERROR] Gagal mengeksekusi perintah: {str(e)}"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def git_status(cwd: str = ".") -> str:
|
|
135
|
+
return run_terminal_command("git status --short", timeout=15, cwd=cwd)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def git_diff(cwd: str = ".", staged: bool = False) -> str:
|
|
139
|
+
cmd = "git diff --cached" if staged else "git diff"
|
|
140
|
+
return run_terminal_command(cmd, timeout=20, cwd=cwd)
|
yncli/tui.py
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Dict, Any, Optional
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.markdown import Markdown
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
from rich.syntax import Syntax
|
|
13
|
+
from rich.status import Status
|
|
14
|
+
from rich.columns import Columns
|
|
15
|
+
|
|
16
|
+
from yncli.agent import Agent
|
|
17
|
+
from yncli.config import save_config, CONFIG_DIR
|
|
18
|
+
from yncli.clean_text import clean_text_for_terminal
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TerminalUI:
|
|
22
|
+
def __init__(self, agent: Agent):
|
|
23
|
+
self.agent = agent
|
|
24
|
+
self.console = Console(force_terminal=True, soft_wrap=True)
|
|
25
|
+
self.session = None
|
|
26
|
+
self._active_spinner: Optional[Status] = None
|
|
27
|
+
|
|
28
|
+
def _stop_spinner(self) -> None:
|
|
29
|
+
if self._active_spinner:
|
|
30
|
+
try:
|
|
31
|
+
self._active_spinner.stop()
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
self._active_spinner = None
|
|
35
|
+
|
|
36
|
+
def _start_spinner(self, text: str) -> None:
|
|
37
|
+
self._stop_spinner()
|
|
38
|
+
try:
|
|
39
|
+
self._active_spinner = self.console.status(text, spinner="dots")
|
|
40
|
+
self._active_spinner.start()
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def _get_prompt_session(self):
|
|
45
|
+
if self.session is None:
|
|
46
|
+
try:
|
|
47
|
+
from prompt_toolkit import PromptSession
|
|
48
|
+
from prompt_toolkit.history import FileHistory
|
|
49
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
50
|
+
from prompt_toolkit.styles import Style
|
|
51
|
+
|
|
52
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
history_file = CONFIG_DIR / "history.txt"
|
|
54
|
+
|
|
55
|
+
prompt_style = Style.from_dict({
|
|
56
|
+
"prompt": "#38bdf8",
|
|
57
|
+
"arrow": "#a855f7",
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
self.session = PromptSession(
|
|
61
|
+
history=FileHistory(str(history_file)),
|
|
62
|
+
auto_suggest=AutoSuggestFromHistory(),
|
|
63
|
+
style=prompt_style
|
|
64
|
+
)
|
|
65
|
+
except Exception:
|
|
66
|
+
self.session = False
|
|
67
|
+
return self.session
|
|
68
|
+
|
|
69
|
+
def print_banner(self) -> None:
|
|
70
|
+
cwd = Path(self.agent.workspace_dir).resolve()
|
|
71
|
+
folder_name = cwd.name or str(cwd)
|
|
72
|
+
cwd_str = str(cwd).replace(str(Path.home()), "~")
|
|
73
|
+
mode_title = self.agent.mode.capitalize()
|
|
74
|
+
mode_color = "#38bdf8" if self.agent.mode == "plan" else ("#10b981" if self.agent.mode == "build" else "#f59e0b")
|
|
75
|
+
|
|
76
|
+
# Top pill badge
|
|
77
|
+
self.console.print()
|
|
78
|
+
pill = Text()
|
|
79
|
+
pill.append(f" {folder_name} ", style="bold black on #f59e0b")
|
|
80
|
+
pill.append(" yncli", style="dim white")
|
|
81
|
+
self.console.print(pill)
|
|
82
|
+
self.console.print()
|
|
83
|
+
|
|
84
|
+
# Rainbow pixel letter 'Y' logo
|
|
85
|
+
logo = Text()
|
|
86
|
+
logo.append("██ ██\n", style="bold #ef4444")
|
|
87
|
+
logo.append(" ██ ██ \n", style="bold #f97316")
|
|
88
|
+
logo.append(" ██ ██ \n", style="bold #eab308")
|
|
89
|
+
logo.append(" ██ ██ \n", style="bold #22c55e")
|
|
90
|
+
logo.append(" ███ \n", style="bold #06b6d4")
|
|
91
|
+
logo.append(" █ \n", style="bold #3b82f6")
|
|
92
|
+
logo.append(" █ ", style="bold #a855f7")
|
|
93
|
+
|
|
94
|
+
# Info metadata
|
|
95
|
+
info = Text()
|
|
96
|
+
info.append("yncli 1.0.0\n", style="bold #60a5fa")
|
|
97
|
+
info.append("Autonomous Polyglot AI Coding Agent\n", style="dim white")
|
|
98
|
+
info.append(f"{self.agent.model} ", style="white")
|
|
99
|
+
info.append(f"({mode_title} Mode)\n", style=mode_color)
|
|
100
|
+
info.append(f"{cwd_str}\n", style="dim")
|
|
101
|
+
|
|
102
|
+
cols = Columns([logo, info], padding=(0, 3))
|
|
103
|
+
self.console.print(cols)
|
|
104
|
+
self.console.print("─" * 60, style="dim #334155")
|
|
105
|
+
|
|
106
|
+
loaded_count = len(self.agent.workspace_memory.indexed_files)
|
|
107
|
+
total_files = len(self.agent.workspace_memory.file_tree)
|
|
108
|
+
self.console.print(f"[dim]Project: [cyan]{self.agent.workspace_memory.project_type}[/cyan] ({loaded_count}/{total_files} files loaded into active memory)[/dim]\n")
|
|
109
|
+
|
|
110
|
+
def print_help(self) -> None:
|
|
111
|
+
table = Table(title="Panduan Perintah", border_style="dim cyan", show_header=True)
|
|
112
|
+
table.add_column("Perintah", style="yellow")
|
|
113
|
+
table.add_column("Keterangan", style="white")
|
|
114
|
+
|
|
115
|
+
table.add_row("/model, /models", "Buka popup interaktif untuk memilih model AI (klik mouse / panah)")
|
|
116
|
+
table.add_row("/model <nama>", "Ganti model secara langsung")
|
|
117
|
+
table.add_row("/plan", "Masuk ke Plan Mode (Riset & susun PRD plan.md otomatis)")
|
|
118
|
+
table.add_row("/build", "Masuk ke Build Mode (Eksekusi autonomous pembuatan & editing kode)")
|
|
119
|
+
table.add_row("/ask", "Masuk ke Ask Mode (Konsultasi murni tanpa modifikasi file)")
|
|
120
|
+
table.add_row("/cd <folder>", "Pindah direktori kerja aktif")
|
|
121
|
+
table.add_row("/pwd", "Lihat path direktori saat ini")
|
|
122
|
+
table.add_row("/approval [on|off]", "Atur persetujuan tool (on = otomatis, off = tanya dulu)")
|
|
123
|
+
table.add_row("/thinking [on|off]", "Tampilkan atau sembunyikan live reasoning")
|
|
124
|
+
table.add_row("/skill <nama>", "Pilih skill (ultrabrain, system_architect, debug_oracle, dll)")
|
|
125
|
+
table.add_row("/clear", "Bersihkan memori percakapan sesi ini")
|
|
126
|
+
table.add_row("/save [file]", "Simpan percakapan ke file Markdown")
|
|
127
|
+
table.add_row("/exit, /quit", "Keluar dari yncli")
|
|
128
|
+
|
|
129
|
+
self.console.print(table)
|
|
130
|
+
|
|
131
|
+
def open_model_popup_selector(self) -> None:
|
|
132
|
+
self._start_spinner("[dim cyan]Mengambil daftar model dari server...[/dim cyan]")
|
|
133
|
+
try:
|
|
134
|
+
models = self.agent.client.list_models()
|
|
135
|
+
finally:
|
|
136
|
+
self._stop_spinner()
|
|
137
|
+
|
|
138
|
+
if not models:
|
|
139
|
+
self.console.print("[red]Gagal mengambil daftar model dari server.[/red]")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
values = []
|
|
143
|
+
for m in models:
|
|
144
|
+
mid = m.get("id", "")
|
|
145
|
+
caps = []
|
|
146
|
+
mcap = m.get("capabilities", {})
|
|
147
|
+
if mcap.get("reasoning"):
|
|
148
|
+
caps.append("Thinking")
|
|
149
|
+
if mcap.get("tools"):
|
|
150
|
+
caps.append("Tools")
|
|
151
|
+
cap_str = f" [{', '.join(caps)}]" if caps else ""
|
|
152
|
+
label = f"{mid}{cap_str}"
|
|
153
|
+
values.append((mid, label))
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
from prompt_toolkit.shortcuts import radiolist_dialog
|
|
157
|
+
from prompt_toolkit.styles import Style
|
|
158
|
+
|
|
159
|
+
dialog_style = Style.from_dict({
|
|
160
|
+
"dialog": "bg:#1e1e2e",
|
|
161
|
+
"dialog.body": "bg:#181825 #cdd6f4",
|
|
162
|
+
"dialog.title": "bg:#313244 #89b4fa bold",
|
|
163
|
+
"dialog.border": "#89b4fa",
|
|
164
|
+
"button.focused": "bg:#a6e3a1 #11111b bold",
|
|
165
|
+
"radio-selected": "bold #a6e3a1",
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
result = radiolist_dialog(
|
|
169
|
+
title="Pilih Model AI (Gunakan Panah / Klik Mouse)",
|
|
170
|
+
text=f"Model aktif: {self.agent.model}\nPilih model di bawah lalu klik OK atau tekan Enter:",
|
|
171
|
+
values=values,
|
|
172
|
+
default=self.agent.model,
|
|
173
|
+
style=dialog_style
|
|
174
|
+
).run()
|
|
175
|
+
|
|
176
|
+
if result:
|
|
177
|
+
self.agent.set_model(result)
|
|
178
|
+
save_config({"model": result})
|
|
179
|
+
self.console.print(f"[green]Model berhasil diubah ke: [cyan]{result}[/cyan][/green]")
|
|
180
|
+
self.console.print(f"[dim]Memory refreshed: {len(self.agent.workspace_memory.indexed_files)} files active in memory[/dim]")
|
|
181
|
+
else:
|
|
182
|
+
self.console.print("[dim]Pemilihan model dibatalkan.[/dim]")
|
|
183
|
+
|
|
184
|
+
except Exception:
|
|
185
|
+
self.console.print("[cyan]Daftar Model Tersedia:[/cyan]")
|
|
186
|
+
for idx, (mid, label) in enumerate(values, start=1):
|
|
187
|
+
marker = "(*)" if mid == self.agent.model else "( )"
|
|
188
|
+
self.console.print(f" {idx:2d}. {marker} [cyan]{mid}[/cyan]")
|
|
189
|
+
self.console.print("\nMasukkan nomor model atau ID model: ", end="")
|
|
190
|
+
try:
|
|
191
|
+
choice = input().strip()
|
|
192
|
+
if choice.isdigit() and 1 <= int(choice) <= len(values):
|
|
193
|
+
sel = values[int(choice) - 1][0]
|
|
194
|
+
self.agent.set_model(sel)
|
|
195
|
+
save_config({"model": sel})
|
|
196
|
+
self.console.print(f"[green]Model diubah ke: [cyan]{sel}[/cyan][/green]")
|
|
197
|
+
elif choice in [v[0] for v in values]:
|
|
198
|
+
self.agent.set_model(choice)
|
|
199
|
+
save_config({"model": choice})
|
|
200
|
+
self.console.print(f"[green]Model diubah ke: [cyan]{choice}[/cyan][/green]")
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
def handle_slash_command(self, cmd_line: str) -> bool:
|
|
205
|
+
parts = cmd_line.strip().split()
|
|
206
|
+
if not parts:
|
|
207
|
+
return True
|
|
208
|
+
cmd = parts[0].lower()
|
|
209
|
+
args = parts[1:]
|
|
210
|
+
|
|
211
|
+
if cmd in ("/exit", "/quit", "exit", "quit"):
|
|
212
|
+
self.console.print("\n[yellow]Sesi selesai. Sampai jumpa![/yellow]\n")
|
|
213
|
+
sys.exit(0)
|
|
214
|
+
|
|
215
|
+
elif cmd == "/help":
|
|
216
|
+
self.print_help()
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
elif cmd in ("/model", "/models"):
|
|
220
|
+
if not args:
|
|
221
|
+
self.open_model_popup_selector()
|
|
222
|
+
return True
|
|
223
|
+
new_model = args[0].strip()
|
|
224
|
+
self.agent.set_model(new_model)
|
|
225
|
+
save_config({"model": new_model})
|
|
226
|
+
self.console.print(f"[green]Model berhasil diganti ke: [cyan]{new_model}[/cyan][/green]")
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
elif cmd == "/plan":
|
|
230
|
+
self.agent.set_mode("plan")
|
|
231
|
+
save_config({"mode": "plan"})
|
|
232
|
+
self.console.print("[green]Mode aktif: [cyan]Plan Mode[/cyan] (Riset arsitektur & susun PRD plan.md).[/green]")
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
elif cmd == "/build":
|
|
236
|
+
self.agent.set_mode("build")
|
|
237
|
+
save_config({"mode": "build"})
|
|
238
|
+
self.console.print("[green]Mode aktif: [cyan]Build Mode[/cyan] (Eksekusi autonomous pembuatan & editing kode).[/green]")
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
elif cmd == "/ask":
|
|
242
|
+
self.agent.set_mode("ask")
|
|
243
|
+
save_config({"mode": "ask"})
|
|
244
|
+
self.console.print("[green]Mode aktif: [cyan]Ask Mode[/cyan] (Konsultasi murni tanpa modifikasi file).[/green]")
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
elif cmd in ("/mode", "/modes"):
|
|
248
|
+
if not args:
|
|
249
|
+
self.console.print(f"Mode saat ini: [cyan]{self.agent.mode.capitalize()} Mode[/cyan]")
|
|
250
|
+
self.console.print("Pilihan mode: [yellow]/plan[/yellow], [yellow]/build[/yellow], [yellow]/ask[/yellow]")
|
|
251
|
+
return True
|
|
252
|
+
m = args[0].lower()
|
|
253
|
+
if m in ("plan", "build", "ask"):
|
|
254
|
+
self.agent.set_mode(m)
|
|
255
|
+
save_config({"mode": m})
|
|
256
|
+
self.console.print(f"[green]Mode diubah ke: [cyan]{m.capitalize()} Mode[/cyan][/green]")
|
|
257
|
+
else:
|
|
258
|
+
self.console.print("[red]Mode tidak valid. Pilihan: plan, build, ask[/red]")
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
elif cmd == "/cd":
|
|
262
|
+
if not args:
|
|
263
|
+
self.console.print(f"Direktori saat ini: [cyan]{Path(self.agent.workspace_dir).resolve()}[/cyan]")
|
|
264
|
+
self.console.print("Gunakan: [yellow]/cd <path_direktori>[/yellow]")
|
|
265
|
+
return True
|
|
266
|
+
target_path = " ".join(args).strip()
|
|
267
|
+
from yncli.tools.system_tools import change_directory
|
|
268
|
+
res = change_directory(target_path, current_cwd=self.agent.workspace_dir)
|
|
269
|
+
if res.get("success"):
|
|
270
|
+
self.agent.set_workspace_dir(res["new_cwd"])
|
|
271
|
+
self.console.print(f"[green]Direktori dipindahkan ke: [cyan]{res['new_cwd']}[/cyan][/green]")
|
|
272
|
+
self.console.print(f"[dim]Workspace re-indexed: {len(self.agent.workspace_memory.indexed_files)} files loaded[/dim]")
|
|
273
|
+
else:
|
|
274
|
+
self.console.print(f"[red]Gagal pindah direktori: {res.get('message')}[/red]")
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
elif cmd == "/pwd":
|
|
278
|
+
self.console.print(f"Direktori kerja: [cyan]{Path(self.agent.workspace_dir).resolve()}[/cyan]")
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
elif cmd == "/thinking":
|
|
282
|
+
if not args:
|
|
283
|
+
cur = "Aktif" if self.agent.show_thinking else "Mati"
|
|
284
|
+
self.console.print(f"Status Thinking: [cyan]{cur}[/cyan] (Gunakan: /thinking on | /thinking off)")
|
|
285
|
+
return True
|
|
286
|
+
val = args[0].lower() in ("on", "true", "1", "yes", "enable")
|
|
287
|
+
self.agent.set_show_thinking(val)
|
|
288
|
+
save_config({"show_thinking": val})
|
|
289
|
+
status = "Diaktifkan" if val else "Dimatikan"
|
|
290
|
+
self.console.print(f"[green]Thinking {status}.[/green]")
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
elif cmd in ("/approval", "/auto"):
|
|
294
|
+
if not args:
|
|
295
|
+
cur = "Otomatis" if self.agent.auto_approve else "Tanya dulu"
|
|
296
|
+
self.console.print(f"Persetujuan tool: [cyan]{cur}[/cyan] (Gunakan: /approval on | /approval off)")
|
|
297
|
+
return True
|
|
298
|
+
val = args[0].lower() in ("on", "auto", "true", "1", "yes", "otomatis")
|
|
299
|
+
self.agent.set_auto_approve(val)
|
|
300
|
+
save_config({"auto_approve": val})
|
|
301
|
+
status = "Otomatis (Langsung eksekusi)" if val else "Tanya dulu (Konfirmasi manual)"
|
|
302
|
+
self.console.print(f"[green]Persetujuan tool diubah ke: [cyan]{status}[/cyan][/green]")
|
|
303
|
+
return True
|
|
304
|
+
|
|
305
|
+
elif cmd == "/clear":
|
|
306
|
+
self.agent.reset_history()
|
|
307
|
+
self.agent.workspace_memory.refresh()
|
|
308
|
+
self.console.print("[green]Riwayat percakapan telah dibersihkan & memori workspace diperbarui.[/green]")
|
|
309
|
+
return True
|
|
310
|
+
|
|
311
|
+
elif cmd == "/skills":
|
|
312
|
+
skills = self.agent.skills_mgr.list_skills()
|
|
313
|
+
current = self.agent.skills_mgr.active_skill
|
|
314
|
+
self.console.print(f"Skill aktif: [green]{current}[/green]\nDaftar skill yang tersedia:")
|
|
315
|
+
for s in skills:
|
|
316
|
+
marker = "* " if s == current else " "
|
|
317
|
+
self.console.print(f"{marker}[cyan]{s}[/cyan]")
|
|
318
|
+
return True
|
|
319
|
+
|
|
320
|
+
elif cmd == "/skill":
|
|
321
|
+
if not args:
|
|
322
|
+
self.console.print("Gunakan: [yellow]/skill <nama_skill>[/yellow] (contoh: /skill ultrabrain)")
|
|
323
|
+
return True
|
|
324
|
+
skill_name = args[0].lower().strip()
|
|
325
|
+
if self.agent.skills_mgr.set_active_skill(skill_name):
|
|
326
|
+
self.console.print(f"[green]Skill aktif: [cyan]{skill_name}[/cyan][/green]")
|
|
327
|
+
else:
|
|
328
|
+
self.console.print(f"[red]Skill '{skill_name}' tidak ditemukan.[/red]")
|
|
329
|
+
return True
|
|
330
|
+
|
|
331
|
+
elif cmd == "/tools":
|
|
332
|
+
from yncli.tools import AGENT_TOOLS
|
|
333
|
+
table = Table(title="Daftar Alat (Tools)", border_style="dim magenta")
|
|
334
|
+
table.add_column("Nama", style="yellow")
|
|
335
|
+
table.add_column("Fungsi", style="white")
|
|
336
|
+
for t in AGENT_TOOLS:
|
|
337
|
+
fn = t.get("function", {})
|
|
338
|
+
table.add_row(fn.get("name", ""), fn.get("description", ""))
|
|
339
|
+
self.console.print(table)
|
|
340
|
+
return True
|
|
341
|
+
|
|
342
|
+
elif cmd == "/save":
|
|
343
|
+
fname = args[0] if args else f"chat_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
|
|
344
|
+
lines = [f"# yncli export - {datetime.datetime.now().isoformat()}\n\n"]
|
|
345
|
+
for msg in self.agent.history:
|
|
346
|
+
role = msg.get("role", "").capitalize()
|
|
347
|
+
content = msg.get("content") or ""
|
|
348
|
+
lines.append(f"### {role}\n\n{content}\n\n---\n")
|
|
349
|
+
with open(fname, "w", encoding="utf-8") as f:
|
|
350
|
+
f.writelines(lines)
|
|
351
|
+
self.console.print(f"[green]Percakapan disimpan ke: [cyan]{fname}[/cyan][/green]")
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
else:
|
|
355
|
+
self.console.print(f"[red]Perintah tidak dikenal:[/red] '{cmd}'. Ketik [yellow]/help[/yellow] untuk bantuan.")
|
|
356
|
+
return True
|
|
357
|
+
|
|
358
|
+
def run_interactive_loop(self) -> None:
|
|
359
|
+
self.print_banner()
|
|
360
|
+
ps = self._get_prompt_session()
|
|
361
|
+
|
|
362
|
+
while True:
|
|
363
|
+
try:
|
|
364
|
+
self.console.print()
|
|
365
|
+
prompt_label = f"{self.agent.mode} > "
|
|
366
|
+
if ps:
|
|
367
|
+
user_input = ps.prompt([
|
|
368
|
+
("class:prompt", prompt_label),
|
|
369
|
+
]).strip()
|
|
370
|
+
else:
|
|
371
|
+
self.console.print(f"[cyan]{prompt_label}[/cyan]", end="")
|
|
372
|
+
user_input = input().strip()
|
|
373
|
+
|
|
374
|
+
if not user_input:
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
if user_input.startswith("/"):
|
|
378
|
+
self.handle_slash_command(user_input)
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
self.execute_turn(user_input)
|
|
382
|
+
|
|
383
|
+
except (KeyboardInterrupt, EOFError):
|
|
384
|
+
self.console.print("\n[dim](Sesi dihentikan. Ketik /exit untuk keluar)[/dim]")
|
|
385
|
+
except Exception as e:
|
|
386
|
+
self.console.print(f"\n[red]Error: {str(e)}[/red]")
|
|
387
|
+
|
|
388
|
+
def ask_tool_confirmation(self, name: str, args: Dict[str, Any]) -> bool:
|
|
389
|
+
self._stop_spinner()
|
|
390
|
+
self.console.print()
|
|
391
|
+
|
|
392
|
+
target_path = args.get("file_path", args.get("path", args.get("command", "")))
|
|
393
|
+
content = args.get("content", args.get("replacement_content", ""))
|
|
394
|
+
|
|
395
|
+
if name in ("write_file", "edit_file_replace", "save_plan_document") and content:
|
|
396
|
+
lines = content.strip().split("\n")
|
|
397
|
+
line_count = len(lines)
|
|
398
|
+
self.console.print(f"[bold white]{target_path}[/bold white] [dim #38bdf8]+{line_count}[/dim #38bdf8]")
|
|
399
|
+
|
|
400
|
+
preview_lines = lines[:12]
|
|
401
|
+
for idx, line in enumerate(preview_lines, start=1):
|
|
402
|
+
clean_l = clean_text_for_terminal(line[:80])
|
|
403
|
+
self.console.print(f" [dim]{idx:2d}[/dim] [green]+[/green] [dim cyan]{clean_l}[/dim cyan]")
|
|
404
|
+
|
|
405
|
+
if line_count > 12:
|
|
406
|
+
self.console.print(f" [dim]... {line_count - 12} baris lainnya[/dim]")
|
|
407
|
+
self.console.print("Reason: File modification in workspace\n", style="dim")
|
|
408
|
+
prompt_title = f"Allow execution of {name} on this file?"
|
|
409
|
+
else:
|
|
410
|
+
args_summary = ", ".join(f"{k}={repr(v)[:50]}" for k, v in args.items())
|
|
411
|
+
self.console.print(f"[bold white]{name}[/bold white]({args_summary})")
|
|
412
|
+
self.console.print("Reason: Terminal / Tool execution\n", style="dim")
|
|
413
|
+
prompt_title = f"Allow execution of {name}?"
|
|
414
|
+
|
|
415
|
+
self.console.print(f"[bold white]{prompt_title}[/bold white]")
|
|
416
|
+
self.console.print(" [bold #38bdf8]> 1. Yes, allow execution[/bold #38bdf8]")
|
|
417
|
+
self.console.print(" 2. Yes, and always allow (enable auto-approve)")
|
|
418
|
+
self.console.print(" 3. No, deny execution")
|
|
419
|
+
self.console.print("\nPilih (1/2/3 atau y/a/n): ", end="")
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
choice = input().strip().lower()
|
|
423
|
+
if choice in ("2", "a", "all", "always"):
|
|
424
|
+
self.agent.set_auto_approve(True)
|
|
425
|
+
save_config({"auto_approve": True})
|
|
426
|
+
self.console.print("[green]Auto-approve diaktifkan untuk aksi berikutnya.[/green]\n")
|
|
427
|
+
return True
|
|
428
|
+
elif choice in ("1", "y", "ya", "yes", ""):
|
|
429
|
+
self.console.print()
|
|
430
|
+
return True
|
|
431
|
+
else:
|
|
432
|
+
self.console.print("[dim red]Aksi dibatalkan.[/dim red]\n")
|
|
433
|
+
return False
|
|
434
|
+
except (KeyboardInterrupt, EOFError):
|
|
435
|
+
self.console.print("\n[dim red]Aksi dibatalkan.[/dim red]\n")
|
|
436
|
+
return False
|
|
437
|
+
|
|
438
|
+
def execute_turn(self, user_prompt: str) -> None:
|
|
439
|
+
content_chunks = []
|
|
440
|
+
is_thinking = False
|
|
441
|
+
|
|
442
|
+
def on_status_update(status_text: str) -> None:
|
|
443
|
+
if not is_thinking:
|
|
444
|
+
self._start_spinner(f"[dim cyan]{status_text}[/dim cyan]")
|
|
445
|
+
|
|
446
|
+
def on_thinking(chunk: str) -> None:
|
|
447
|
+
nonlocal is_thinking
|
|
448
|
+
self._stop_spinner()
|
|
449
|
+
clean_chunk = clean_text_for_terminal(chunk)
|
|
450
|
+
if not is_thinking:
|
|
451
|
+
self.console.print("[dim cyan]Thinking:[/dim cyan]\n", style="dim")
|
|
452
|
+
is_thinking = True
|
|
453
|
+
sys.stdout.write(clean_chunk)
|
|
454
|
+
sys.stdout.flush()
|
|
455
|
+
|
|
456
|
+
def on_content(chunk: str) -> None:
|
|
457
|
+
nonlocal is_thinking
|
|
458
|
+
clean_chunk = clean_text_for_terminal(chunk)
|
|
459
|
+
if is_thinking:
|
|
460
|
+
self.console.print("\n")
|
|
461
|
+
is_thinking = False
|
|
462
|
+
content_chunks.append(clean_chunk)
|
|
463
|
+
|
|
464
|
+
def on_tool_start(name: str, args: Dict[str, Any]) -> None:
|
|
465
|
+
nonlocal is_thinking
|
|
466
|
+
if is_thinking:
|
|
467
|
+
self.console.print("\n")
|
|
468
|
+
is_thinking = False
|
|
469
|
+
|
|
470
|
+
target = args.get("file_path", args.get("path", args.get("command", args.get("query", ""))))
|
|
471
|
+
if len(str(target)) > 45:
|
|
472
|
+
target = str(target)[:45] + "..."
|
|
473
|
+
|
|
474
|
+
tool_display_map = {
|
|
475
|
+
"list_directory": "ListDir",
|
|
476
|
+
"read_file": "Read",
|
|
477
|
+
"write_file": "Write",
|
|
478
|
+
"edit_file_replace": "Edit",
|
|
479
|
+
"save_plan_document": "SavePlan",
|
|
480
|
+
"run_terminal_command": "Shell",
|
|
481
|
+
"web_search": "WebSearch",
|
|
482
|
+
"fetch_webpage": "Fetch",
|
|
483
|
+
"change_directory": "Cd"
|
|
484
|
+
}
|
|
485
|
+
display_name = tool_display_map.get(name, name)
|
|
486
|
+
|
|
487
|
+
# In-place dynamic single line
|
|
488
|
+
self._start_spinner(f" [bold #3b82f6]●[/bold #3b82f6] [bold #f59e0b]{display_name}[/bold #f59e0b][dim]({target})[/dim] Working...")
|
|
489
|
+
|
|
490
|
+
def on_tool_end(name: str, output: str) -> None:
|
|
491
|
+
pass
|
|
492
|
+
|
|
493
|
+
self._start_spinner(f"Working ({self.agent.model})...")
|
|
494
|
+
try:
|
|
495
|
+
res = self.agent.run_turn(
|
|
496
|
+
user_prompt=user_prompt,
|
|
497
|
+
on_thinking=on_thinking,
|
|
498
|
+
on_content=on_content,
|
|
499
|
+
on_tool_start=on_tool_start,
|
|
500
|
+
on_tool_end=on_tool_end,
|
|
501
|
+
on_tool_confirm=self.ask_tool_confirmation,
|
|
502
|
+
on_status_update=on_status_update
|
|
503
|
+
)
|
|
504
|
+
finally:
|
|
505
|
+
self._stop_spinner()
|
|
506
|
+
|
|
507
|
+
# Render full Markdown with Rich (renders **bold**, headers, syntax highlighting)
|
|
508
|
+
final_text = clean_text_for_terminal("".join(content_chunks) or res.get("content", ""))
|
|
509
|
+
if final_text:
|
|
510
|
+
self.console.print()
|
|
511
|
+
try:
|
|
512
|
+
self.console.print(Markdown(final_text))
|
|
513
|
+
except Exception:
|
|
514
|
+
self.console.print(final_text)
|
|
515
|
+
|
|
516
|
+
# Plan Mode transition workflow: Auto prompt to Build
|
|
517
|
+
if self.agent.mode == "plan" and final_text:
|
|
518
|
+
plan_file = Path(self.agent.workspace_dir).resolve() / "plan.md"
|
|
519
|
+
if plan_file.exists():
|
|
520
|
+
self.console.print(f"\n[green]• Dokumen PRD rencana implementasi telah dibuat di: [bold cyan]{plan_file}[/bold cyan][/green]")
|
|
521
|
+
|
|
522
|
+
self.console.print("\n[bold white]Mau langsung dieksekusi / build sekarang?[/bold white]")
|
|
523
|
+
self.console.print(" [bold #10b981]> 1. Ya, beralih ke Build Mode dan mulai bangun kode[/bold #10b981]")
|
|
524
|
+
self.console.print(" 2. Tidak, tetap di Plan Mode")
|
|
525
|
+
self.console.print("\nPilih (1/2 atau y/n): ", end="")
|
|
526
|
+
try:
|
|
527
|
+
answer = input().strip().lower()
|
|
528
|
+
if answer in ("1", "y", "ya", "yes", ""):
|
|
529
|
+
self.agent.set_mode("build")
|
|
530
|
+
save_config({"mode": "build"})
|
|
531
|
+
self.console.print("\n[green]Beralih ke Build Mode. Memulai eksekusi implementasi rencana...[/green]\n")
|
|
532
|
+
self.execute_turn("Rencana di plan.md telah disetujui. Buka plan.md dan mulai eksekusi pembuatan/pembaruan kode secara tuntas.")
|
|
533
|
+
else:
|
|
534
|
+
self.console.print("[dim]Tetap berada dalam Plan Mode.[/dim]")
|
|
535
|
+
except (KeyboardInterrupt, EOFError):
|
|
536
|
+
pass
|