codemate-cli 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.
codemate/config.py ADDED
@@ -0,0 +1,233 @@
1
+ import os
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Optional, Dict, Any
5
+ from dotenv import load_dotenv
6
+
7
+ # Load environment variables
8
+ load_dotenv()
9
+
10
+
11
+ class Config:
12
+ """Configuration manager for CodeMate CLI"""
13
+
14
+ def __init__(self):
15
+ self.config_dir = self._get_config_dir()
16
+ self.config_file = self.config_dir / "cli_config.json"
17
+ self.session_file = self.config_dir / "session.txt"
18
+ self._ensure_config_exists()
19
+ self._load_config()
20
+
21
+ def _get_config_dir(self) -> Path:
22
+ """Get or create config directory in user home"""
23
+ # Use user home directory + .codemate/user
24
+ home = Path.home()
25
+ config_dir = home / ".codemate" / "user"
26
+ config_dir.mkdir(parents=True, exist_ok=True)
27
+ return config_dir
28
+
29
+ def _ensure_config_exists(self):
30
+ """Create config file if it doesn't exist"""
31
+ if not self.config_file.exists():
32
+ default_config = {
33
+ "endpoint": "http://localhost:45223",
34
+ "default_model": "chat_c0_cli",
35
+ "stream": True,
36
+ "theme": "monokai",
37
+ "save_history": True,
38
+ }
39
+ self._save_config(default_config)
40
+
41
+ def _load_config(self):
42
+ """Load configuration from file"""
43
+ try:
44
+ with open(self.config_file, 'r') as f:
45
+ self.config = json.load(f)
46
+ except Exception as e:
47
+ print(f"Warning: Could not load config: {e}")
48
+ self.config = {}
49
+
50
+ def _save_config(self, config: Dict[str, Any]):
51
+ """Save configuration to file"""
52
+ try:
53
+ with open(self.config_file, 'w') as f:
54
+ json.dump(config, f, indent=2)
55
+ self.config = config
56
+ except Exception as e:
57
+ print(f"Error saving config: {e}")
58
+
59
+ def is_logged_in(self) -> bool:
60
+ """Check if user is logged in by checking session.txt existence"""
61
+ return self.session_file.exists()
62
+
63
+ def get_session(self) -> Optional[str]:
64
+ """Get session from session.txt file"""
65
+ if not self.session_file.exists():
66
+ return None
67
+
68
+ try:
69
+ with open(self.session_file, 'r') as f:
70
+ session = f.read().strip()
71
+ return session if session else None
72
+ except Exception:
73
+ return None
74
+
75
+ def save_session(self, session: str):
76
+ """Save session to session.txt file"""
77
+ try:
78
+ with open(self.session_file, 'w') as f:
79
+ f.write(session)
80
+ except Exception as e:
81
+ print(f"Error saving session: {e}")
82
+
83
+ def clear_session(self):
84
+ """Clear session (logout)"""
85
+ if self.session_file.exists():
86
+ self.session_file.unlink()
87
+
88
+ def get_api_key(self) -> Optional[str]:
89
+ """Get API key from session.txt (for backward compatibility)"""
90
+ return self.get_session()
91
+
92
+ def set_api_key(self, api_key: str):
93
+ """Set API key in session.txt (for backward compatibility)"""
94
+ self.save_session(api_key)
95
+
96
+ def get_endpoint(self) -> str:
97
+ """Get API endpoint"""
98
+ return os.environ.get('CODEMATE_ENDPOINT') or self.config.get(
99
+ 'endpoint',
100
+ 'http://localhost:45223'
101
+ )
102
+
103
+ def set_endpoint(self, endpoint: str):
104
+ """Set API endpoint"""
105
+ self.config['endpoint'] = endpoint
106
+ self._save_config(self.config)
107
+
108
+ def get_default_model(self) -> str:
109
+ """Get default model"""
110
+ return self.config.get('default_model', 'chat_c0_cli')
111
+
112
+ def set_default_model(self, model: str):
113
+ """Set default model"""
114
+ self.config['default_model'] = model
115
+ self._save_config(self.config)
116
+
117
+ def get_stream_enabled(self) -> bool:
118
+ """Check if streaming is enabled"""
119
+ return self.config.get('stream', True)
120
+
121
+ def set_stream_enabled(self, enabled: bool):
122
+ """Enable/disable streaming"""
123
+ self.config['stream'] = enabled
124
+ self._save_config(self.config)
125
+
126
+ def get_theme(self) -> str:
127
+ """Get code theme"""
128
+ return self.config.get('theme', 'monokai')
129
+
130
+ def set_theme(self, theme: str):
131
+ """Set code theme"""
132
+ self.config['theme'] = theme
133
+ self._save_config(self.config)
134
+
135
+ def get_save_history(self) -> bool:
136
+ """Check if history saving is enabled"""
137
+ return self.config.get('save_history', True)
138
+
139
+ def set_save_history(self, enabled: bool):
140
+ """Enable/disable history saving"""
141
+ self.config['save_history'] = enabled
142
+ self._save_config(self.config)
143
+
144
+ def get_all(self) -> Dict[str, Any]:
145
+ """Get all configuration values"""
146
+ session = self.get_session()
147
+ return {
148
+ "logged_in": self.is_logged_in(),
149
+ "session": session[:10] + "..." + session[-4:] if session and len(session) > 14 else "Not logged in",
150
+ "endpoint": self.get_endpoint(),
151
+ "default_model": self.get_default_model(),
152
+ "stream": self.get_stream_enabled(),
153
+ "theme": self.get_theme(),
154
+ "save_history": self.get_save_history(),
155
+ "config_location": str(self.config_file),
156
+ "session_location": str(self.session_file),
157
+ }
158
+
159
+ def reset(self):
160
+ """Reset configuration to defaults"""
161
+ self.config_file.unlink(missing_ok=True)
162
+ self._ensure_config_exists()
163
+ self._load_config()
164
+
165
+
166
+ class HistoryManager:
167
+ """Manage conversation history"""
168
+
169
+ def __init__(self, config: Config):
170
+ self.config = config
171
+ self.history_dir = config.config_dir / "history"
172
+ self.history_dir.mkdir(exist_ok=True)
173
+ self.current_session_file = self.history_dir / "current_session.json"
174
+
175
+ def save_message(self, role: str, content: str, metadata: Optional[Dict] = None):
176
+ """Save a message to current session"""
177
+ if not self.config.get_save_history():
178
+ return
179
+
180
+ try:
181
+ # Load existing history
182
+ history = []
183
+ if self.current_session_file.exists():
184
+ with open(self.current_session_file, 'r') as f:
185
+ history = json.load(f)
186
+
187
+ # Add new message
188
+ message = {
189
+ "role": role,
190
+ "content": content,
191
+ "timestamp": self._get_timestamp(),
192
+ }
193
+ if metadata:
194
+ message["metadata"] = metadata
195
+
196
+ history.append(message)
197
+
198
+ # Save updated history
199
+ with open(self.current_session_file, 'w') as f:
200
+ json.dump(history, f, indent=2)
201
+
202
+ except Exception as e:
203
+ print(f"Warning: Could not save history: {e}")
204
+
205
+ def load_current_session(self) -> list:
206
+ """Load current session history"""
207
+ try:
208
+ if self.current_session_file.exists():
209
+ with open(self.current_session_file, 'r') as f:
210
+ return json.load(f)
211
+ except Exception:
212
+ pass
213
+ return []
214
+
215
+ def clear_current_session(self):
216
+ """Clear current session history"""
217
+ self.current_session_file.unlink(missing_ok=True)
218
+
219
+ def archive_session(self, name: Optional[str] = None):
220
+ """Archive current session with optional name"""
221
+ if not self.current_session_file.exists():
222
+ return
223
+
224
+ timestamp = self._get_timestamp()
225
+ archive_name = name or f"session_{timestamp}.json"
226
+ archive_path = self.history_dir / archive_name
227
+
228
+ self.current_session_file.rename(archive_path)
229
+
230
+ def _get_timestamp(self) -> str:
231
+ """Get current timestamp"""
232
+ from datetime import datetime
233
+ return datetime.now().isoformat()
@@ -0,0 +1,10 @@
1
+ """
2
+ UI components for CodeMate CLI
3
+ """
4
+
5
+ from codemate.ui.streaming import StreamingHandler, MarkdownRenderer
6
+
7
+ __all__ = [
8
+ "StreamingHandler",
9
+ "MarkdownRenderer",
10
+ ]
@@ -0,0 +1,212 @@
1
+ """
2
+ Enhanced Markdown rendering utilities
3
+ """
4
+
5
+ from rich.console import Console
6
+ from rich.markdown import Markdown
7
+ from rich.panel import Panel
8
+ from rich.syntax import Syntax
9
+ import re
10
+ from typing import Optional
11
+
12
+
13
+ class MarkdownProcessor:
14
+ """Process and enhance markdown content"""
15
+
16
+ def __init__(self):
17
+ self.code_block_pattern = re.compile(r'```(\w+)?\n(.*?)```', re.DOTALL)
18
+
19
+ def extract_code_blocks(self, text: str):
20
+ """Extract code blocks from markdown"""
21
+ blocks = []
22
+ for match in self.code_block_pattern.finditer(text):
23
+ language = match.group(1) or "text"
24
+ code = match.group(2)
25
+ blocks.append({
26
+ "language": language,
27
+ "code": code,
28
+ "start": match.start(),
29
+ "end": match.end()
30
+ })
31
+ return blocks
32
+
33
+ def replace_code_blocks(self, text: str, placeholder: str = "[CODE_BLOCK]"):
34
+ """Replace code blocks with placeholder"""
35
+ return self.code_block_pattern.sub(placeholder, text)
36
+
37
+ def split_by_code_blocks(self, text: str):
38
+ """Split text into markdown and code sections"""
39
+ sections = []
40
+ last_end = 0
41
+
42
+ for match in self.code_block_pattern.finditer(text):
43
+ # Add markdown before code block
44
+ if match.start() > last_end:
45
+ sections.append({
46
+ "type": "markdown",
47
+ "content": text[last_end:match.start()]
48
+ })
49
+
50
+ # Add code block
51
+ sections.append({
52
+ "type": "code",
53
+ "language": match.group(1) or "text",
54
+ "content": match.group(2)
55
+ })
56
+
57
+ last_end = match.end()
58
+
59
+ # Add remaining markdown
60
+ if last_end < len(text):
61
+ sections.append({
62
+ "type": "markdown",
63
+ "content": text[last_end:]
64
+ })
65
+
66
+ return sections
67
+
68
+
69
+ class EnhancedMarkdownRenderer:
70
+ """Enhanced markdown renderer with custom styling"""
71
+
72
+ def __init__(self, console: Console, theme: str = "monokai"):
73
+ self.console = console
74
+ self.theme = theme
75
+ self.processor = MarkdownProcessor()
76
+
77
+ def render(
78
+ self,
79
+ markdown_text: str,
80
+ title: Optional[str] = None,
81
+ show_panel: bool = True
82
+ ):
83
+ """
84
+ Render markdown with enhanced styling
85
+
86
+ Args:
87
+ markdown_text: Markdown text to render
88
+ title: Optional title for panel
89
+ show_panel: Whether to wrap in a panel
90
+ """
91
+ # Split content into sections
92
+ sections = self.processor.split_by_code_blocks(markdown_text)
93
+
94
+ if show_panel:
95
+ self.console.print() # Add spacing
96
+
97
+ for section in sections:
98
+ if section["type"] == "markdown":
99
+ # Render markdown
100
+ if section["content"].strip():
101
+ md = Markdown(section["content"], code_theme=self.theme)
102
+ if show_panel:
103
+ self.console.print(md)
104
+ else:
105
+ self.console.print(md)
106
+
107
+ elif section["type"] == "code":
108
+ # Render code with syntax highlighting
109
+ self.render_code_block(
110
+ section["content"],
111
+ section["language"]
112
+ )
113
+
114
+ if show_panel:
115
+ self.console.print() # Add spacing
116
+
117
+ def render_code_block(self, code: str, language: str = "python"):
118
+ """Render a single code block with syntax highlighting"""
119
+ syntax = Syntax(
120
+ code.strip(),
121
+ language,
122
+ theme=self.theme,
123
+ line_numbers=True,
124
+ word_wrap=False,
125
+ background_color="default"
126
+ )
127
+
128
+ self.console.print(Panel(
129
+ syntax,
130
+ border_style="#48AEF3",
131
+ padding=(0, 1),
132
+ title=f"[bold color(#48AEF3)]{language.upper()}[/bold color(#48AEF3)]",
133
+ title_align="left"
134
+ ))
135
+
136
+ def render_inline(self, markdown_text: str):
137
+ """Render markdown inline without panels"""
138
+ md = Markdown(markdown_text, code_theme=self.theme)
139
+ self.console.print(md)
140
+
141
+ def render_with_title(self, markdown_text: str, title: str):
142
+ """Render markdown with a title panel"""
143
+ md = Markdown(markdown_text, code_theme=self.theme)
144
+ self.console.print(Panel(
145
+ md,
146
+ title=f"[bold color(#2FCACE)]{title}[/bold color(#2FCACE)]",
147
+ border_style="#2FCACE",
148
+ padding=(1, 2)
149
+ ))
150
+
151
+
152
+ class MarkdownFormatter:
153
+ """Format text into markdown"""
154
+
155
+ @staticmethod
156
+ def heading(text: str, level: int = 1) -> str:
157
+ """Create markdown heading"""
158
+ return f"{'#' * level} {text}\n"
159
+
160
+ @staticmethod
161
+ def bold(text: str) -> str:
162
+ """Make text bold"""
163
+ return f"**{text}**"
164
+
165
+ @staticmethod
166
+ def italic(text: str) -> str:
167
+ """Make text italic"""
168
+ return f"*{text}*"
169
+
170
+ @staticmethod
171
+ def code(text: str) -> str:
172
+ """Make inline code"""
173
+ return f"`{text}`"
174
+
175
+ @staticmethod
176
+ def code_block(code: str, language: str = "python") -> str:
177
+ """Create code block"""
178
+ return f"```{language}\n{code}\n```\n"
179
+
180
+ @staticmethod
181
+ def link(text: str, url: str) -> str:
182
+ """Create link"""
183
+ return f"[{text}]({url})"
184
+
185
+ @staticmethod
186
+ def list_item(text: str, ordered: bool = False, number: int = 1) -> str:
187
+ """Create list item"""
188
+ if ordered:
189
+ return f"{number}. {text}\n"
190
+ return f"- {text}\n"
191
+
192
+ @staticmethod
193
+ def table(headers: list, rows: list) -> str:
194
+ """Create markdown table"""
195
+ # Header row
196
+ table = "| " + " | ".join(headers) + " |\n"
197
+ # Separator
198
+ table += "| " + " | ".join(["---"] * len(headers)) + " |\n"
199
+ # Data rows
200
+ for row in rows:
201
+ table += "| " + " | ".join(str(cell) for cell in row) + " |\n"
202
+ return table
203
+
204
+ @staticmethod
205
+ def quote(text: str) -> str:
206
+ """Create blockquote"""
207
+ return f"> {text}\n"
208
+
209
+ @staticmethod
210
+ def horizontal_rule() -> str:
211
+ """Create horizontal rule"""
212
+ return "---\n"
@@ -0,0 +1,159 @@
1
+ """
2
+ Rich rendering utilities for beautiful terminal output
3
+ """
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+ from rich.syntax import Syntax
9
+ from rich.markdown import Markdown
10
+ from rich.text import Text
11
+ from typing import List, Dict, Any, Optional
12
+
13
+
14
+ class UIRenderer:
15
+ """Utility class for rendering rich UI components"""
16
+
17
+ def __init__(self, console: Console):
18
+ self.console = console
19
+
20
+ def render_panel(
21
+ self,
22
+ content: str,
23
+ title: Optional[str] = None,
24
+ border_style: str = "#2FCACE",
25
+ padding: tuple = (1, 2)
26
+ ):
27
+ """Render content in a panel"""
28
+ panel = Panel(
29
+ content,
30
+ title=title,
31
+ border_style=border_style,
32
+ padding=padding
33
+ )
34
+ self.console.print(panel)
35
+
36
+ def render_table(
37
+ self,
38
+ data: List[Dict[str, Any]],
39
+ title: Optional[str] = None,
40
+ columns: Optional[List[str]] = None
41
+ ):
42
+ """Render data as a table"""
43
+ if not data:
44
+ self.console.print("[yellow]No data to display[/yellow]")
45
+ return
46
+
47
+ # Get columns from first row if not provided
48
+ if columns is None:
49
+ columns = list(data[0].keys())
50
+
51
+ table = Table(title=title, show_header=True, header_style="bold magenta")
52
+
53
+ # Add columns
54
+ for col in columns:
55
+ table.add_column(col, style="#2FCACE")
56
+
57
+ # Add rows
58
+ for row in data:
59
+ table.add_row(*[str(row.get(col, "")) for col in columns])
60
+
61
+ self.console.print(table)
62
+
63
+ def render_code(
64
+ self,
65
+ code: str,
66
+ language: str = "python",
67
+ theme: str = "monokai",
68
+ line_numbers: bool = True
69
+ ):
70
+ """Render syntax-highlighted code"""
71
+ syntax = Syntax(
72
+ code,
73
+ language,
74
+ theme=theme,
75
+ line_numbers=line_numbers,
76
+ word_wrap=False
77
+ )
78
+ self.console.print(syntax)
79
+
80
+ def render_markdown(self, markdown_text: str, code_theme: str = "monokai"):
81
+ """Render markdown content"""
82
+ markdown = Markdown(markdown_text, code_theme=code_theme)
83
+ self.console.print(markdown)
84
+
85
+ def render_success(self, message: str):
86
+ """Render success message"""
87
+ self.console.print(f"[green]✓ {message}[/green]")
88
+
89
+ def render_error(self, message: str):
90
+ """Render error message"""
91
+ self.console.print(f"[red]✗ {message}[/red]")
92
+
93
+ def render_warning(self, message: str):
94
+ """Render warning message"""
95
+ self.console.print(f"[yellow]⚠ {message}[/yellow]")
96
+
97
+ def render_info(self, message: str):
98
+ """Render info message"""
99
+ self.console.print(f"[color(#48AEF3)]ℹ {message}[/color(#48AEF3)]")
100
+
101
+ def render_list(
102
+ self,
103
+ items: List[str],
104
+ title: Optional[str] = None,
105
+ style: str = "#2FCACE"
106
+ ):
107
+ """Render a list of items"""
108
+ if title:
109
+ self.console.print(f"\n[bold {style}]{title}[/bold {style}]")
110
+
111
+ for item in items:
112
+ self.console.print(f" • {item}")
113
+
114
+ def render_key_value_pairs(
115
+ self,
116
+ data: Dict[str, Any],
117
+ title: Optional[str] = None
118
+ ):
119
+ """Render key-value pairs"""
120
+ table = Table(title=title, show_header=False, box=None)
121
+ table.add_column("Key", style="#2FCACE", justify="right")
122
+ table.add_column("Value", style="white")
123
+
124
+ for key, value in data.items():
125
+ table.add_row(f"{key}:", str(value))
126
+
127
+ self.console.print(table)
128
+
129
+ def clear_screen(self):
130
+ """Clear the terminal screen"""
131
+ self.console.clear()
132
+
133
+ def render_separator(self, char: str = "─", style: str = "dim"):
134
+ """Render a horizontal separator"""
135
+ width = self.console.width
136
+ self.console.print(char * width, style=style)
137
+
138
+
139
+ class ProgressRenderer:
140
+ """Renderer for progress indicators"""
141
+
142
+ def __init__(self, console: Console):
143
+ self.console = console
144
+
145
+ def show_spinner(self, message: str = "Processing...", spinner: str = "dots"):
146
+ """Show a spinner with message"""
147
+ return self.console.status(f"[color(#2FCACE)]{message}[/color(#2FCACE)]", spinner=spinner)
148
+
149
+ def show_progress_bar(self, total: int, description: str = "Progress"):
150
+ """Show a progress bar"""
151
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
152
+
153
+ return Progress(
154
+ SpinnerColumn(),
155
+ TextColumn("[progress.description]{task.description}"),
156
+ BarColumn(),
157
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
158
+ console=self.console
159
+ )