yaicli 0.2.0__py3-none-any.whl → 0.3.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.
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "yaicli"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "A simple CLI tool to interact with LLM"
5
5
  authors = [{ name = "belingud", email = "im.victor@qq.com" }]
6
6
  readme = "README.md"
yaicli/api.py CHANGED
@@ -8,33 +8,18 @@ from rich.console import Console
8
8
  from yaicli.const import (
9
9
  DEFAULT_BASE_URL,
10
10
  DEFAULT_COMPLETION_PATH,
11
- DEFAULT_MAX_TOKENS,
12
11
  DEFAULT_MODEL,
13
- DEFAULT_TEMPERATURE,
14
- DEFAULT_TIMEOUT,
15
- DEFAULT_TOP_P,
16
12
  EventTypeEnum,
17
13
  )
18
14
 
19
15
 
20
16
  def parse_stream_line(line: Union[bytes, str], console: Console, verbose: bool) -> Optional[dict]:
21
17
  """(Helper Function) Parse a single line from the SSE stream response."""
22
- line_str: str
23
- if isinstance(line, bytes):
24
- try:
25
- line_str = line.decode("utf-8")
26
- except UnicodeDecodeError:
27
- if verbose:
28
- console.print(f"Warning: Could not decode stream line bytes: {line!r}", style="yellow")
29
- return None
30
- elif isinstance(line, str):
31
- line_str = line
32
- else:
33
- # Handle unexpected line types
18
+ if not isinstance(line, (bytes, str)):
34
19
  if verbose:
35
- console.print(f"Warning: Received unexpected line type: {type(line)}", style="yellow")
20
+ console.print(f"Warning: Received non-string/bytes line: {line!r}", style="yellow")
36
21
  return None
37
-
22
+ line_str: str = line.decode("utf-8") if isinstance(line, bytes) else line
38
23
  line_str = line_str.strip()
39
24
  if not line_str or not line_str.startswith("data: "):
40
25
  return None
@@ -69,8 +54,8 @@ class ApiClient:
69
54
  self.completion_path = str(config.get("COMPLETION_PATH", DEFAULT_COMPLETION_PATH))
70
55
  self.api_key = str(config.get("API_KEY", ""))
71
56
  self.model = str(config.get("MODEL", DEFAULT_MODEL))
72
- self.timeout = self.config.get("TIMEOUT", DEFAULT_TIMEOUT)
73
- self.client = client or httpx.Client(timeout=self.config.get("TIMEOUT", DEFAULT_TIMEOUT))
57
+ self.timeout = self.config["TIMEOUT"]
58
+ self.client = client or httpx.Client(timeout=self.config["TIMEOUT"])
74
59
 
75
60
  def _prepare_request_body(self, messages: List[Dict[str, str]], stream: bool) -> Dict[str, Any]:
76
61
  """Prepare the common request body for API calls."""
@@ -78,9 +63,12 @@ class ApiClient:
78
63
  "messages": messages,
79
64
  "model": self.model,
80
65
  "stream": stream,
81
- "temperature": self.config.get("TEMPERATURE", DEFAULT_TEMPERATURE),
82
- "top_p": self.config.get("TOP_P", DEFAULT_TOP_P),
83
- "max_tokens": self.config.get("MAX_TOKENS", DEFAULT_MAX_TOKENS),
66
+ "temperature": self.config["TEMPERATURE"],
67
+ "top_p": self.config["TOP_P"],
68
+ "max_tokens": self.config[
69
+ "MAX_TOKENS"
70
+ ], # Openai: This value is now deprecated in favor of max_completion_tokens
71
+ "max_completion_tokens": self.config["MAX_TOKENS"],
84
72
  }
85
73
 
86
74
  def _handle_api_error(self, e: httpx.HTTPError) -> None:
@@ -190,7 +178,7 @@ class ApiClient:
190
178
  """Process a single chunk from the stream and yield events with updated reasoning state.
191
179
 
192
180
  Args:
193
- parsed_data: The parsed JSON data from a stream line
181
+ parsed_data: The parsed JSON data from a streamline
194
182
  in_reasoning: Whether we're currently in a reasoning state
195
183
 
196
184
  Yields:
@@ -273,7 +261,7 @@ class ApiClient:
273
261
  yield self._handle_http_error(e)
274
262
  return
275
263
 
276
- # Process the stream line by line
264
+ # Process the streamline by line
277
265
  for line in response.iter_lines():
278
266
  parsed_data = parse_stream_line(line, self.console, self.verbose)
279
267
  if parsed_data is None:
@@ -315,7 +303,7 @@ class ApiClient:
315
303
  # reasoning_content: deepseek/infi-ai
316
304
  # reasoning: openrouter
317
305
  # <think> block implementation not in here
318
- for key in ("reasoning_content", "reasoning", "metadata"):
306
+ for key in ("reasoning_content", "reasoning"):
319
307
  # Check if the key exists and its value is a non-empty string
320
308
  value = delta.get(key)
321
309
  if isinstance(value, str) and value:
yaicli/chat_manager.py CHANGED
@@ -3,17 +3,30 @@ import time
3
3
  from abc import ABC, abstractmethod
4
4
  from datetime import datetime
5
5
  from pathlib import Path
6
- from typing import Any, Dict, List, Optional, TypedDict
6
+ from typing import Any, Dict, List, Optional, TypedDict, Union
7
7
 
8
- from rich import get_console
9
8
  from rich.console import Console
10
9
 
11
- from yaicli.config import Config
10
+ from yaicli.config import Config, cfg
11
+ from yaicli.console import get_console
12
+ from yaicli.utils import option_callback
13
+
14
+
15
+ class ChatFileInfo(TypedDict):
16
+ """Chat info, parse chat filename and store metadata"""
17
+
18
+ index: int
19
+ path: str
20
+ title: str
21
+ date: str
22
+ timestamp: int
12
23
 
13
24
 
14
25
  class ChatsMap(TypedDict):
15
- title: Dict[str, dict]
16
- index: Dict[int, dict]
26
+ """Chat info cache for chat manager"""
27
+
28
+ title: Dict[str, ChatFileInfo]
29
+ index: Dict[int, ChatFileInfo]
17
30
 
18
31
 
19
32
  class ChatManager(ABC):
@@ -30,7 +43,7 @@ class ChatManager(ABC):
30
43
  pass
31
44
 
32
45
  @abstractmethod
33
- def list_chats(self) -> List[Dict[str, Any]]:
46
+ def list_chats(self) -> List[ChatFileInfo]:
34
47
  """List all saved chats and return the chat list"""
35
48
  pass
36
49
 
@@ -40,12 +53,12 @@ class ChatManager(ABC):
40
53
  pass
41
54
 
42
55
  @abstractmethod
43
- def load_chat_by_index(self, index: int) -> Dict[str, Any]:
56
+ def load_chat_by_index(self, index: int) -> Union[ChatFileInfo, Dict]:
44
57
  """Load a chat by index and return the chat data"""
45
58
  pass
46
59
 
47
60
  @abstractmethod
48
- def load_chat_by_title(self, title: str) -> Dict[str, Any]:
61
+ def load_chat_by_title(self, title: str) -> Union[ChatFileInfo, Dict]:
49
62
  """Load a chat by title and return the chat data"""
50
63
  pass
51
64
 
@@ -63,13 +76,14 @@ class ChatManager(ABC):
63
76
  class FileChatManager(ChatManager):
64
77
  """File system based chat manager implementation"""
65
78
 
66
- def __init__(self, config: Config, console: Optional[Console] = None):
67
- self.config = config
68
- self.chat_dir = Path(self.config["CHAT_HISTORY_DIR"])
69
- self.chat_dir.mkdir(parents=True, exist_ok=True)
70
- self.max_saved_chats = self.config["MAX_SAVED_CHATS"]
79
+ console: Console = get_console()
80
+ config: Config = cfg
81
+ chat_dir = Path(config["CHAT_HISTORY_DIR"])
82
+ max_saved_chats = config["MAX_SAVED_CHATS"]
83
+ chat_dir.mkdir(parents=True, exist_ok=True)
84
+
85
+ def __init__(self):
71
86
  self._chats_map: Optional[ChatsMap] = None # Cache for chat map
72
- self.console = console or get_console()
73
87
 
74
88
  @property
75
89
  def chats_map(self) -> ChatsMap:
@@ -78,6 +92,18 @@ class FileChatManager(ChatManager):
78
92
  self._load_chats()
79
93
  return self._chats_map or {"index": {}, "title": {}}
80
94
 
95
+ @classmethod
96
+ @option_callback
97
+ def print_list_option(cls, _: Any):
98
+ """Print the list of chats"""
99
+ cls.console.print("Finding Chats...")
100
+ c = -1
101
+ for c, file in enumerate(sorted(cls.chat_dir.glob("*.json"), key=lambda f: f.stat().st_mtime)):
102
+ info: ChatFileInfo = cls._parse_filename(file, c + 1)
103
+ cls.console.print(f"{c + 1}. {info['title']} ({info['date']})")
104
+ if c == -1:
105
+ cls.console.print("No chats found", style="dim")
106
+
81
107
  def make_chat_title(self, prompt: Optional[str] = None) -> str:
82
108
  """Make a chat title from a given full prompt"""
83
109
  if prompt:
@@ -93,7 +119,8 @@ class FileChatManager(ChatManager):
93
119
  """Force refresh the chat list from disk"""
94
120
  self._load_chats()
95
121
 
96
- def _parse_filename(self, chat_file: Path, index: int) -> Dict[str, Any]:
122
+ @staticmethod
123
+ def _parse_filename(chat_file: Path, index: int) -> ChatFileInfo:
97
124
  """Parse a chat filename and extract metadata"""
98
125
  # filename: "20250421-214005-title-meaning of life"
99
126
  filename = chat_file.stem
@@ -155,7 +182,7 @@ class FileChatManager(ChatManager):
155
182
 
156
183
  self._chats_map = chats_map
157
184
 
158
- def list_chats(self) -> List[Dict[str, Any]]:
185
+ def list_chats(self) -> List[ChatFileInfo]:
159
186
  """List all saved chats and return the chat list"""
160
187
  return list(self.chats_map["index"].values())
161
188
 
@@ -203,7 +230,7 @@ class FileChatManager(ChatManager):
203
230
  self.console.print(f"Error saving chat '{save_title}': {e}", style="dim")
204
231
  return ""
205
232
 
206
- def _load_chat_data(self, chat_info: Optional[Dict[str, Any]]) -> Dict[str, Any]:
233
+ def _load_chat_data(self, chat_info: Optional[ChatFileInfo]) -> Union[ChatFileInfo, Dict]:
207
234
  """Common method to load chat data from a chat info dict"""
208
235
  if not chat_info:
209
236
  return {}
@@ -228,14 +255,14 @@ class FileChatManager(ChatManager):
228
255
  self.console.print(f"Error loading chat from {chat_info['path']}: {e}", style="dim")
229
256
  return {}
230
257
 
231
- def load_chat_by_index(self, index: int) -> Dict[str, Any]:
258
+ def load_chat_by_index(self, index: int) -> Union[ChatFileInfo, Dict]:
232
259
  """Load a chat by index and return the chat data"""
233
260
  if not self.validate_chat_index(index):
234
261
  return {}
235
262
  chat_info = self.chats_map.get("index", {}).get(index)
236
263
  return self._load_chat_data(chat_info)
237
264
 
238
- def load_chat_by_title(self, title: str) -> Dict[str, Any]:
265
+ def load_chat_by_title(self, title: str) -> Union[ChatFileInfo, Dict]:
239
266
  """Load a chat by title and return the chat data"""
240
267
  chat_info = self.chats_map.get("title", {}).get(title)
241
268
  return self._load_chat_data(chat_info)
yaicli/cli.py CHANGED
@@ -4,22 +4,22 @@ import time
4
4
  import traceback
5
5
  from os.path import devnull
6
6
  from pathlib import Path
7
- from typing import List, Optional, Tuple
7
+ from typing import List, Literal, Optional, Tuple
8
8
 
9
9
  import typer
10
10
  from prompt_toolkit import PromptSession, prompt
11
11
  from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
12
12
  from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
13
13
  from prompt_toolkit.keys import Keys
14
- from rich import get_console
15
14
  from rich.markdown import Markdown
16
15
  from rich.padding import Padding
17
16
  from rich.panel import Panel
18
17
  from rich.prompt import Prompt
19
18
 
20
19
  from yaicli.api import ApiClient
21
- from yaicli.chat_manager import ChatManager, FileChatManager
22
- from yaicli.config import CONFIG_PATH, Config
20
+ from yaicli.chat_manager import ChatFileInfo, ChatManager, FileChatManager
21
+ from yaicli.config import CONFIG_PATH, Config, cfg
22
+ from yaicli.console import get_console
23
23
  from yaicli.const import (
24
24
  CHAT_MODE,
25
25
  CMD_CLEAR,
@@ -33,14 +33,14 @@ from yaicli.const import (
33
33
  DEFAULT_CODE_THEME,
34
34
  DEFAULT_INTERACTIVE_ROUND,
35
35
  DEFAULT_OS_NAME,
36
- DEFAULT_PROMPT,
37
36
  DEFAULT_SHELL_NAME,
38
37
  EXEC_MODE,
39
- SHELL_PROMPT,
40
38
  TEMP_MODE,
39
+ DefaultRoleNames,
41
40
  )
42
41
  from yaicli.history import LimitedFileHistory
43
42
  from yaicli.printer import Printer
43
+ from yaicli.roles import RoleManager
44
44
  from yaicli.utils import detect_os, detect_shell, filter_command
45
45
 
46
46
 
@@ -54,14 +54,26 @@ class CLI:
54
54
  api_client: Optional[ApiClient] = None,
55
55
  printer: Optional[Printer] = None,
56
56
  chat_manager: Optional[ChatManager] = None,
57
+ role: Optional[str] = None,
57
58
  ):
59
+ # General settings
58
60
  self.verbose = verbose
59
61
  self.stdin = stdin
60
62
  self.console = get_console()
61
63
  self.bindings = KeyBindings()
62
- self.config: Config = Config(self.console)
64
+ self.config: Config = cfg
63
65
  self.current_mode: str = TEMP_MODE
66
+ self.role: str = role or DefaultRoleNames.DEFAULT.value
64
67
 
68
+ # Initialize role manager
69
+ self.role_manager = RoleManager()
70
+
71
+ # Validate role
72
+ if not self.role_manager.role_exists(self.role):
73
+ self.console.print(f"Role '{self.role}' not found, using default role.", style="yellow")
74
+ self.role = DefaultRoleNames.DEFAULT.value
75
+
76
+ # Interactive chat mode settings
65
77
  self.history = []
66
78
  self.interactive_max_history = self.config.get("INTERACTIVE_MAX_HISTORY", DEFAULT_INTERACTIVE_ROUND)
67
79
  self.chat_title = None
@@ -72,8 +84,8 @@ class CLI:
72
84
  self.chat_history_dir = Path(self.config["CHAT_HISTORY_DIR"])
73
85
  self.chat_history_dir.mkdir(parents=True, exist_ok=True)
74
86
 
75
- # Initialize session manager
76
- self.chat_manager = chat_manager or FileChatManager(self.config)
87
+ # Initialize chat manager
88
+ self.chat_manager = chat_manager or FileChatManager()
77
89
 
78
90
  # Detect OS and Shell if set to auto
79
91
  if self.config.get("OS_NAME") == DEFAULT_OS_NAME:
@@ -87,10 +99,11 @@ class CLI:
87
99
  for key, value in self.config.items():
88
100
  display_value = "****" if key == "API_KEY" and value else value
89
101
  self.console.print(f" {key:<17}: {display_value}")
102
+ self.console.print(f"Current role: {self.role}")
90
103
  self.console.print(Markdown("---", code_theme=self.config.get("CODE_THEME", DEFAULT_CODE_THEME)))
91
104
 
92
105
  self.api_client = api_client or ApiClient(self.config, self.console, self.verbose)
93
- self.printer = printer or Printer(self.config, self.console, self.verbose)
106
+ self.printer = printer or Printer(self.config, self.console, self.verbose, markdown=True)
94
107
 
95
108
  _origin_stderr = None
96
109
  if not sys.stdin.isatty():
@@ -110,13 +123,25 @@ class CLI:
110
123
  return [("class:qmark", f" {mode_icon} "), ("class:prompt", "> ")]
111
124
 
112
125
  def _check_history_len(self) -> None:
113
- """Check history length and remove oldest messages if necessary"""
126
+ """Check history length and remove the oldest messages if necessary"""
114
127
  target_len = self.interactive_max_history * 2
115
128
  if len(self.history) > target_len:
116
129
  self.history = self.history[-target_len:]
117
130
  if self.verbose:
118
131
  self.console.print(f"History trimmed to {target_len} messages.", style="dim")
119
132
 
133
+ # ------------------- Role Command Methods -------------------
134
+ def set_role(self, role: str) -> None:
135
+ """Set the current role for the assistant"""
136
+ if not self.role_manager.role_exists(role):
137
+ self.console.print(f"Role '{role}' not found.", style="bold red")
138
+ return
139
+
140
+ self.role = role
141
+ if self.role == DefaultRoleNames.CODER:
142
+ self.printer = Printer(self.config, self.console, self.verbose, content_markdown=False)
143
+
144
+ # ------------------- Chat Command Methods -------------------
120
145
  def _save_chat(self, title: Optional[str] = None) -> None:
121
146
  """Save current chat history to a file using session manager."""
122
147
  saved_title = self.chat_manager.save_chat(self.history, title)
@@ -139,7 +164,7 @@ class CLI:
139
164
 
140
165
  def _list_chats(self) -> None:
141
166
  """List all saved chat sessions using session manager."""
142
- chats = self.chat_manager.list_chats()
167
+ chats: list[ChatFileInfo] = self.chat_manager.list_chats()
143
168
 
144
169
  if not chats:
145
170
  self.console.print("No saved chats found.", style="yellow")
@@ -199,6 +224,7 @@ class CLI:
199
224
  self.console.print(f"Failed to delete chat: {chat_data['title']}", style="bold red")
200
225
  return False
201
226
 
227
+ # ------------------- Special commands -------------------
202
228
  def _handle_special_commands(self, user_input: str) -> Optional[bool]:
203
229
  """Handle special command return: True-continue loop, False-exit loop, None-non-special command"""
204
230
  command = user_input.lower().strip()
@@ -266,8 +292,6 @@ class CLI:
266
292
  new_mode = parts[1]
267
293
  if self.current_mode != new_mode:
268
294
  self.current_mode = new_mode
269
- mode_name = "Chat" if self.current_mode == CHAT_MODE else "Exec"
270
- self.console.print(f"Switched to [bold yellow]{mode_name}[/bold yellow] mode")
271
295
  else:
272
296
  self.console.print(f"Already in {self.current_mode} mode.", style="yellow")
273
297
  else:
@@ -316,21 +340,24 @@ class CLI:
316
340
  elif _input != "e":
317
341
  self.console.print("Execution cancelled.", style="yellow")
318
342
 
343
+ # ------------------- LLM Methods -------------------
319
344
  def get_system_prompt(self) -> str:
320
- """Return system prompt for current mode"""
321
- prompt_template = SHELL_PROMPT if self.current_mode == EXEC_MODE else DEFAULT_PROMPT
322
- stdin = f"\n\nSTDIN\n{self.stdin}" if self.stdin else ""
323
- if self.verbose and stdin:
324
- self.console.print("Added STDIN to prompt", style="dim")
325
- return prompt_template.format(_os=self.config["OS_NAME"], _shell=self.config["SHELL_NAME"]) + stdin
345
+ """Get the system prompt based on current role and mode"""
346
+ # Use the role manager to get the system prompt
347
+ return self.role_manager.get_system_prompt(self.role)
326
348
 
327
349
  def _build_messages(self, user_input: str) -> List[dict]:
328
- """Build the list of messages for the API call."""
329
- return [
330
- {"role": "system", "content": self.get_system_prompt()},
331
- *self.history,
332
- {"role": "user", "content": user_input},
333
- ]
350
+ """Build message list for LLM API"""
351
+ # Create the message list
352
+ messages = [{"role": "system", "content": self.get_system_prompt()}]
353
+
354
+ # Add previous conversation if available
355
+ for msg in self.history:
356
+ messages.append(msg)
357
+
358
+ # Add user input
359
+ messages.append({"role": "user", "content": user_input})
360
+ return messages
334
361
 
335
362
  def _handle_llm_response(self, user_input: str) -> Optional[str]:
336
363
  """Get response from API (streaming or normal) and print it.
@@ -343,16 +370,16 @@ class CLI:
343
370
  Optional[str]: The assistant's response content or None if an error occurred.
344
371
  """
345
372
  messages = self._build_messages(user_input)
346
- content = None
347
- reasoning = None
348
-
373
+ if self.verbose:
374
+ self.console.print(messages)
375
+ is_code_mode = self.role == DefaultRoleNames.CODER
349
376
  try:
350
- if self.config.get("STREAM", True):
377
+ if self.config["STREAM"]:
351
378
  stream_iterator = self.api_client.stream_completion(messages)
352
- content, reasoning = self.printer.display_stream(stream_iterator)
379
+ content, reasoning = self.printer.display_stream(stream_iterator, not is_code_mode)
353
380
  else:
354
381
  content, reasoning = self.api_client.completion(messages)
355
- self.printer.display_normal(content, reasoning)
382
+ self.printer.display_normal(content, reasoning, not is_code_mode)
356
383
 
357
384
  if content is not None:
358
385
  # Add only the content (not reasoning) to history
@@ -382,6 +409,7 @@ class CLI:
382
409
  self._confirm_and_execute(content)
383
410
  return True
384
411
 
412
+ # ------------------- REPL Methods -------------------
385
413
  def _print_welcome_message(self) -> None:
386
414
  """Prints the initial welcome banner and instructions."""
387
415
  self.console.print(
@@ -446,21 +474,6 @@ class CLI:
446
474
 
447
475
  self.console.print("\nExiting YAICLI... Goodbye!", style="bold green")
448
476
 
449
- def _run_once(self, prompt_arg: str, is_shell_mode: bool) -> None:
450
- """Run a single command (non-interactive)."""
451
- self.current_mode = EXEC_MODE if is_shell_mode else TEMP_MODE
452
- if not self.config.get("API_KEY"):
453
- self.console.print("[bold red]Error:[/bold red] API key not found.")
454
- raise typer.Exit(code=1)
455
-
456
- content = self._handle_llm_response(prompt_arg)
457
-
458
- if content is None:
459
- raise typer.Exit(code=1)
460
-
461
- if is_shell_mode:
462
- self._confirm_and_execute(content)
463
-
464
477
  def prepare_chat_loop(self) -> None:
465
478
  """Setup key bindings and history for interactive modes."""
466
479
  self._setup_key_bindings()
@@ -477,6 +490,7 @@ class CLI:
477
490
  self.session = PromptSession(key_bindings=self.bindings)
478
491
  if self.chat_title:
479
492
  chat_info = self.chat_manager.load_chat_by_title(self.chat_title)
493
+ self.is_temp_session = False
480
494
  self.history = chat_info.get("history", [])
481
495
 
482
496
  def _setup_key_bindings(self) -> None:
@@ -486,29 +500,48 @@ class CLI:
486
500
  def _(event: KeyPressEvent) -> None:
487
501
  self.current_mode = EXEC_MODE if self.current_mode == CHAT_MODE else CHAT_MODE
488
502
 
489
- def run(self, chat: bool, shell: bool, prompt: Optional[str]) -> None:
490
- """Main entry point to run the CLI (REPL or single command)."""
491
- if chat:
492
- if not self.config.get("API_KEY"):
493
- self.console.print("[bold red]Error:[/bold red] API key not found. Cannot start chat mode.")
494
- return
495
- self.current_mode = CHAT_MODE
503
+ def _run_once(self, input: str, shell: bool) -> None:
504
+ """Run a single command (non-interactive)."""
505
+ self.current_mode = EXEC_MODE if shell else TEMP_MODE
506
+ if not self.config.get("API_KEY"):
507
+ self.console.print("[bold red]Error:[/bold red] API key not found.")
508
+ raise typer.Exit(code=1)
496
509
 
497
- # Only set chat start time and title if a prompt is provided
498
- if prompt:
499
- self.chat_start_time = int(time.time())
500
- self.chat_title = self.chat_manager.make_chat_title(prompt)
501
- self.console.print(f"Chat title: [bold]{self.chat_title}[/bold]")
502
- self.is_temp_session = False
503
- else:
504
- # Mark as temporary session if no prompt
505
- self.is_temp_session = True
506
- self.console.print(
507
- "Starting a temporary chat session (will not be saved automatically)", style="yellow"
508
- )
510
+ content = self._handle_llm_response(input)
509
511
 
512
+ if content is None:
513
+ raise typer.Exit(code=1)
514
+
515
+ if shell:
516
+ self._confirm_and_execute(content)
517
+
518
+ # ------------------- Main Entry Point -------------------
519
+ def run(
520
+ self,
521
+ chat: bool,
522
+ shell: bool,
523
+ input: Optional[str],
524
+ role: Optional[str | Literal[DefaultRoleNames.DEFAULT]] = None,
525
+ ) -> None:
526
+ """Run the CLI in the appropriate mode with the selected role."""
527
+ self.set_role(role or self.role)
528
+
529
+ # Now handle normal operation
530
+ if shell:
531
+ # Set mode to shell
532
+ self.role = DefaultRoleNames.SHELL
533
+ if input:
534
+ self._run_once(input, shell=True)
535
+ else:
536
+ self.console.print("No prompt provided for shell mode.", style="yellow")
537
+ elif chat:
538
+ # Start interactive chat mode
539
+ self.current_mode = CHAT_MODE
540
+ self.chat_title = input if input else None
541
+ self.prepare_chat_loop()
510
542
  self._run_repl()
511
- elif prompt:
512
- self._run_once(prompt, shell)
543
+ elif input:
544
+ # Run once with the given prompt
545
+ self._run_once(input, shell=False)
513
546
  else:
514
547
  self.console.print("No chat or prompt provided. Exiting.")
yaicli/config.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import configparser
2
+ from functools import lru_cache
2
3
  from os import getenv
3
4
  from typing import Optional
4
5
 
@@ -10,6 +11,7 @@ from yaicli.const import (
10
11
  DEFAULT_CHAT_HISTORY_DIR,
11
12
  DEFAULT_CONFIG_INI,
12
13
  DEFAULT_CONFIG_MAP,
14
+ DEFAULT_JUSTIFY,
13
15
  DEFAULT_MAX_SAVED_CHATS,
14
16
  )
15
17
  from yaicli.utils import str2bool
@@ -36,6 +38,7 @@ class Config(dict):
36
38
  def __init__(self, console: Optional[Console] = None):
37
39
  """Initializes and loads the configuration."""
38
40
  self.console = console or get_console()
41
+
39
42
  super().__init__()
40
43
  self.reload()
41
44
 
@@ -73,6 +76,8 @@ class Config(dict):
73
76
  f.write(f"\nCHAT_HISTORY_DIR={DEFAULT_CHAT_HISTORY_DIR}\n")
74
77
  if "MAX_SAVED_CHATS" not in config_content.strip(): # Check for empty lines
75
78
  f.write(f"\nMAX_SAVED_CHATS={DEFAULT_MAX_SAVED_CHATS}\n")
79
+ if "JUSTIFY" not in config_content.strip():
80
+ f.write(f"\nJUSTIFY={DEFAULT_JUSTIFY}\n")
76
81
 
77
82
  def _load_from_file(self) -> None:
78
83
  """Load configuration from the config file.
@@ -80,7 +85,7 @@ class Config(dict):
80
85
  Creates default config file if it doesn't exist.
81
86
  """
82
87
  if not CONFIG_PATH.exists():
83
- self.console.print("Creating default configuration file.", style="bold yellow")
88
+ self.console.print("Creating default configuration file.", style="bold yellow", justify=self.get("JUSTIFY"))
84
89
  CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
85
90
  with open(CONFIG_PATH, "w", encoding="utf-8") as f:
86
91
  f.write(DEFAULT_CONFIG_INI)
@@ -135,6 +140,7 @@ class Config(dict):
135
140
  f"[yellow]Warning:[/yellow] Invalid value '{raw_value}' for '{key}'. "
136
141
  f"Expected type '{target_type.__name__}'. Using default value '{default_values_str[key]}'. Error: {e}",
137
142
  style="dim",
143
+ justify=self.get("JUSTIFY"),
138
144
  )
139
145
  # Fallback to default string value if conversion fails
140
146
  try:
@@ -147,7 +153,17 @@ class Config(dict):
147
153
  self.console.print(
148
154
  f"[red]Error:[/red] Could not convert default value for '{key}'. Using raw value.",
149
155
  style="error",
156
+ justify=self.get("JUSTIFY"),
150
157
  )
151
158
  converted_value = raw_value # Or assign a hardcoded safe default
152
159
 
153
160
  self[key] = converted_value
161
+
162
+
163
+ @lru_cache(maxsize=1)
164
+ def get_config() -> Config:
165
+ """Get the configuration singleton"""
166
+ return Config()
167
+
168
+
169
+ cfg = get_config()