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/__init__.py +17 -0
- codemate/__main__.py +10 -0
- codemate/cli.py +815 -0
- codemate/client.py +311 -0
- codemate/commands/__init__.py +6 -0
- codemate/commands/chat.py +0 -0
- codemate/commands/config.py +103 -0
- codemate/commands/help.py +298 -0
- codemate/commands/kb_commands.py +749 -0
- codemate/config.py +233 -0
- codemate/ui/__init__.py +10 -0
- codemate/ui/markdown.py +212 -0
- codemate/ui/renderer.py +159 -0
- codemate/ui/streaming.py +436 -0
- codemate/utils/__init__.py +21 -0
- codemate/utils/auth.py +164 -0
- codemate/utils/error_handler.py +277 -0
- codemate/utils/errors.py +156 -0
- codemate/utils/kb_parser.py +111 -0
- codemate_cli-1.0.0.dist-info/METADATA +452 -0
- codemate_cli-1.0.0.dist-info/RECORD +25 -0
- codemate_cli-1.0.0.dist-info/WHEEL +5 -0
- codemate_cli-1.0.0.dist-info/entry_points.txt +3 -0
- codemate_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- codemate_cli-1.0.0.dist-info/top_level.txt +1 -0
codemate/client.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import json
|
|
3
|
+
from typing import AsyncGenerator, Dict, List, Optional, Any, Tuple
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from codemate.config import Config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CodeMateClient:
|
|
10
|
+
"""Client for interacting with the CodeMate API"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, config: Config):
|
|
13
|
+
self.config = config
|
|
14
|
+
self.base_url = config.get_endpoint()
|
|
15
|
+
self.api_key = config.get_api_key()
|
|
16
|
+
self.timeout = httpx.Timeout(120.0, connect=10.0)
|
|
17
|
+
self._kb_cache = None # Cache for knowledge bases
|
|
18
|
+
|
|
19
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
20
|
+
"""Get request headers with authorization"""
|
|
21
|
+
return {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def _prepare_messages(
|
|
27
|
+
self,
|
|
28
|
+
message: str,
|
|
29
|
+
conversation_history: Optional[List[Dict]] = None,
|
|
30
|
+
context: Optional[List[Dict]] = None
|
|
31
|
+
) -> List[Dict]:
|
|
32
|
+
"""Prepare messages array for API request"""
|
|
33
|
+
messages = conversation_history.copy() if conversation_history else []
|
|
34
|
+
|
|
35
|
+
# Add current message with context if provided
|
|
36
|
+
user_message = {
|
|
37
|
+
"role": "user",
|
|
38
|
+
"content": message
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# If context is provided, add it to the message along with web_search flag
|
|
42
|
+
if context:
|
|
43
|
+
user_message["context"] = context
|
|
44
|
+
user_message["web_search"] = False
|
|
45
|
+
else:
|
|
46
|
+
# Even without context, add empty context and web_search false
|
|
47
|
+
user_message["context"] = []
|
|
48
|
+
user_message["web_search"] = False
|
|
49
|
+
|
|
50
|
+
messages.append(user_message)
|
|
51
|
+
|
|
52
|
+
return messages
|
|
53
|
+
|
|
54
|
+
async def chat_stream(
|
|
55
|
+
self,
|
|
56
|
+
message: str,
|
|
57
|
+
model: str = "chat_c0_cli",
|
|
58
|
+
conversation_history: Optional[List[Dict]] = None,
|
|
59
|
+
conversation_id: Optional[str] = None,
|
|
60
|
+
call_for: str = "chat_c0_cli",
|
|
61
|
+
tools: Optional[List[Dict]] = None,
|
|
62
|
+
context: Optional[List[Dict]] = None,
|
|
63
|
+
) -> AsyncGenerator[Dict[str, Any], None]:
|
|
64
|
+
"""
|
|
65
|
+
Stream chat completions from the API
|
|
66
|
+
|
|
67
|
+
Yields dictionaries with:
|
|
68
|
+
- type: "message" | "reasoning" | "tool_call" | "tool_result" | "metadata"
|
|
69
|
+
- content: the actual content
|
|
70
|
+
- Additional fields depending on type
|
|
71
|
+
"""
|
|
72
|
+
messages = self._prepare_messages(message, conversation_history, context)
|
|
73
|
+
|
|
74
|
+
payload = {
|
|
75
|
+
"messages": messages,
|
|
76
|
+
"call_for": "chat_c0_cli",
|
|
77
|
+
"mode": "NORMAL",
|
|
78
|
+
"conversation_id": conversation_id,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
client = httpx.AsyncClient(timeout=self.timeout)
|
|
82
|
+
stream_context = None
|
|
83
|
+
reasoning_content = ""
|
|
84
|
+
message_content = ""
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
stream_context = client.stream(
|
|
88
|
+
"POST",
|
|
89
|
+
f"{self.base_url}/chat/stream",
|
|
90
|
+
headers=self._get_headers(),
|
|
91
|
+
json=payload,
|
|
92
|
+
)
|
|
93
|
+
response = await stream_context.__aenter__()
|
|
94
|
+
response.raise_for_status()
|
|
95
|
+
|
|
96
|
+
buffer = ""
|
|
97
|
+
should_stop = False
|
|
98
|
+
|
|
99
|
+
async for chunk in response.aiter_text():
|
|
100
|
+
# with open("extra.json", "a", encoding="utf-8") as f:
|
|
101
|
+
# f.write(chunk)
|
|
102
|
+
# f.write("\n")
|
|
103
|
+
|
|
104
|
+
if should_stop:
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
buffer += chunk
|
|
108
|
+
|
|
109
|
+
while "<__!!__END__!!__>" in buffer:
|
|
110
|
+
msg_str, buffer = buffer.split("<__!!__END__!!__>", 1)
|
|
111
|
+
msg_str = msg_str.strip()
|
|
112
|
+
|
|
113
|
+
if not msg_str:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
chunk_data = json.loads(msg_str)
|
|
118
|
+
msg_type = chunk_data.get("type")
|
|
119
|
+
|
|
120
|
+
# ADD ERROR HANDLING:
|
|
121
|
+
if msg_type == "error":
|
|
122
|
+
yield {
|
|
123
|
+
"type": "error",
|
|
124
|
+
"message": chunk_data.get("message", "Unknown error")
|
|
125
|
+
}
|
|
126
|
+
should_stop = True
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
elif msg_type == "end":
|
|
130
|
+
should_stop = True
|
|
131
|
+
break
|
|
132
|
+
elif msg_type == "message":
|
|
133
|
+
content = chunk_data.get("message")
|
|
134
|
+
reasoning = chunk_data.get("reasoning_content")
|
|
135
|
+
if content:
|
|
136
|
+
message_content += content
|
|
137
|
+
yield {"type": "message", "content": content}
|
|
138
|
+
elif reasoning:
|
|
139
|
+
reasoning_content += reasoning
|
|
140
|
+
yield {"type": "reasoning", "content": reasoning}
|
|
141
|
+
elif msg_type == "tool_calls":
|
|
142
|
+
# Yield tool calls information
|
|
143
|
+
yield {
|
|
144
|
+
"type": "tool_calls",
|
|
145
|
+
"tool_calls": chunk_data.get("tool_calls", [])
|
|
146
|
+
}
|
|
147
|
+
elif msg_type == "tool_call_start":
|
|
148
|
+
# Yield tool call start
|
|
149
|
+
yield {
|
|
150
|
+
"type": "tool_call_start",
|
|
151
|
+
"tool_call": chunk_data.get("tool_call", {})
|
|
152
|
+
}
|
|
153
|
+
elif msg_type == "tool_call_complete":
|
|
154
|
+
# Yield tool call completion with results
|
|
155
|
+
yield {
|
|
156
|
+
"type": "tool_call_complete",
|
|
157
|
+
"tool_call": chunk_data.get("tool_call", {})
|
|
158
|
+
}
|
|
159
|
+
elif msg_type == "usage":
|
|
160
|
+
# Yield usage information
|
|
161
|
+
yield {
|
|
162
|
+
"type": "usage",
|
|
163
|
+
"usage": chunk_data.get("usage", {})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
except json.JSONDecodeError as e:
|
|
167
|
+
print(f"JSON decode error: {e}, chunk: {msg_str[:100]}")
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
# Process remaining buffer
|
|
171
|
+
if buffer.strip() and not should_stop:
|
|
172
|
+
try:
|
|
173
|
+
chunk_data = json.loads(buffer.strip())
|
|
174
|
+
msg_type = chunk_data.get("type")
|
|
175
|
+
if msg_type == "message":
|
|
176
|
+
content = chunk_data.get("message")
|
|
177
|
+
reasoning = chunk_data.get("reasoning_content")
|
|
178
|
+
if content:
|
|
179
|
+
message_content += content
|
|
180
|
+
elif reasoning:
|
|
181
|
+
reasoning_content += reasoning
|
|
182
|
+
except json.JSONDecodeError:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
# Write to files after stream completes
|
|
186
|
+
# with open("reasoning_content.txt", "w", encoding="utf-8") as f:
|
|
187
|
+
# f.write(reasoning_content)
|
|
188
|
+
# with open("message_content.txt", "w", encoding="utf-8") as f:
|
|
189
|
+
# f.write(message_content)
|
|
190
|
+
|
|
191
|
+
finally:
|
|
192
|
+
# ✅ Ensure cleanup happens
|
|
193
|
+
if stream_context:
|
|
194
|
+
try:
|
|
195
|
+
await stream_context.__aexit__(None, None, None)
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
await client.aclose()
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def chat(
|
|
206
|
+
self,
|
|
207
|
+
message: str,
|
|
208
|
+
model: str = "chat_c0_cli",
|
|
209
|
+
conversation_history: Optional[List[Dict]] = None,
|
|
210
|
+
conversation_id: Optional[str] = None,
|
|
211
|
+
call_for: str = "chat_c0_cli",
|
|
212
|
+
tools: Optional[List[Dict]] = None,
|
|
213
|
+
context: Optional[List[Dict]] = None,
|
|
214
|
+
) -> Dict[str, Any]:
|
|
215
|
+
"""
|
|
216
|
+
Send a chat completion request (non-streaming)
|
|
217
|
+
|
|
218
|
+
Returns the complete response
|
|
219
|
+
"""
|
|
220
|
+
messages = self._prepare_messages(message, conversation_history, context)
|
|
221
|
+
|
|
222
|
+
payload = {
|
|
223
|
+
"messages": messages,
|
|
224
|
+
"call_for": "chat_c0_cli",
|
|
225
|
+
"mode": "NORMAL",
|
|
226
|
+
"conversation_id": conversation_id,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
230
|
+
response = client.post(
|
|
231
|
+
f"{self.base_url}/chat/stream",
|
|
232
|
+
headers=self._get_headers(),
|
|
233
|
+
json=payload,
|
|
234
|
+
)
|
|
235
|
+
response.raise_for_status()
|
|
236
|
+
return response.json()
|
|
237
|
+
|
|
238
|
+
async def get_models(self) -> List[Dict[str, Any]]:
|
|
239
|
+
"""Get list of available models"""
|
|
240
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
241
|
+
response = await client.get(
|
|
242
|
+
f"{self.base_url}/v1/models",
|
|
243
|
+
headers=self._get_headers(),
|
|
244
|
+
)
|
|
245
|
+
response.raise_for_status()
|
|
246
|
+
return response.json()
|
|
247
|
+
|
|
248
|
+
async def list_kbs(self) -> List[Dict[str, Any]]:
|
|
249
|
+
"""Get list of knowledge bases"""
|
|
250
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
251
|
+
response = await client.get(
|
|
252
|
+
f"{self.base_url}/list_kbs?include_cloud=false",
|
|
253
|
+
headers=self._get_headers(),
|
|
254
|
+
)
|
|
255
|
+
response.raise_for_status()
|
|
256
|
+
data = response.json()
|
|
257
|
+
# Cache the knowledge bases
|
|
258
|
+
self._kb_cache = data
|
|
259
|
+
return data
|
|
260
|
+
|
|
261
|
+
def get_cached_kbs(self) -> Optional[List[Dict[str, Any]]]:
|
|
262
|
+
"""Get cached knowledge bases"""
|
|
263
|
+
return self._kb_cache
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# Synchronous wrapper for CLI usage
|
|
267
|
+
class SyncCodeMateClient:
|
|
268
|
+
"""Synchronous wrapper for the async client"""
|
|
269
|
+
|
|
270
|
+
def __init__(self, config: Config):
|
|
271
|
+
self.async_client = CodeMateClient(config)
|
|
272
|
+
|
|
273
|
+
def chat_stream(self, *args, **kwargs):
|
|
274
|
+
"""Stream chat - returns async generator"""
|
|
275
|
+
return self.async_client.chat_stream(*args, **kwargs)
|
|
276
|
+
|
|
277
|
+
def chat(self, *args, **kwargs):
|
|
278
|
+
"""Non-streaming chat"""
|
|
279
|
+
return self.async_client.chat(*args, **kwargs)
|
|
280
|
+
|
|
281
|
+
def list_kbs(self):
|
|
282
|
+
"""List knowledge bases"""
|
|
283
|
+
import asyncio
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
# Check if there's already a running event loop
|
|
287
|
+
loop = asyncio.get_running_loop()
|
|
288
|
+
# If we're in an async context, we need to create a new task and wait for it
|
|
289
|
+
import concurrent.futures
|
|
290
|
+
import threading
|
|
291
|
+
|
|
292
|
+
def run_in_thread():
|
|
293
|
+
"""Run the async method in a separate thread with its own event loop"""
|
|
294
|
+
new_loop = asyncio.new_event_loop()
|
|
295
|
+
asyncio.set_event_loop(new_loop)
|
|
296
|
+
try:
|
|
297
|
+
return new_loop.run_until_complete(self.async_client.list_kbs())
|
|
298
|
+
finally:
|
|
299
|
+
new_loop.close()
|
|
300
|
+
|
|
301
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
302
|
+
future = executor.submit(run_in_thread)
|
|
303
|
+
return future.result()
|
|
304
|
+
|
|
305
|
+
except RuntimeError:
|
|
306
|
+
# No running event loop, safe to use asyncio.run()
|
|
307
|
+
return asyncio.run(self.async_client.list_kbs())
|
|
308
|
+
|
|
309
|
+
def get_cached_kbs(self):
|
|
310
|
+
"""Get cached knowledge bases"""
|
|
311
|
+
return self.async_client.get_cached_kbs()
|
|
File without changes
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration command implementation
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
from codemate.config import Config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConfigCommand:
|
|
13
|
+
"""Handler for configuration commands"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, config: Config, console: Console):
|
|
16
|
+
self.config = config
|
|
17
|
+
self.console = console
|
|
18
|
+
|
|
19
|
+
def set_api_key(self, api_key: str):
|
|
20
|
+
"""Set API key"""
|
|
21
|
+
if not api_key or not api_key.strip():
|
|
22
|
+
self.console.print("[red]Error: API key cannot be empty[/red]")
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
self.config.set_api_key(api_key)
|
|
26
|
+
self.console.print("[green]✓ API key saved successfully![/green]")
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
def get_api_key(self):
|
|
30
|
+
"""Display masked API key"""
|
|
31
|
+
key = self.config.get_api_key()
|
|
32
|
+
if key:
|
|
33
|
+
masked = key[:8] + "..." + key[-4:] if len(key) > 12 else "***"
|
|
34
|
+
self.console.print(f"API Key: [cyan]{masked}[/cyan]")
|
|
35
|
+
else:
|
|
36
|
+
self.console.print("[yellow]No API key configured.[/yellow]")
|
|
37
|
+
self.console.print(
|
|
38
|
+
"Set one with: [cyan]codemate config set-key YOUR_API_KEY[/cyan]"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def set_endpoint(self, endpoint: str):
|
|
42
|
+
"""Set API endpoint"""
|
|
43
|
+
if not endpoint or not endpoint.strip():
|
|
44
|
+
self.console.print("[red]Error: Endpoint cannot be empty[/red]")
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
# Basic URL validation
|
|
48
|
+
if not endpoint.startswith(("http://", "https://")):
|
|
49
|
+
self.console.print(
|
|
50
|
+
"[yellow]Warning: Endpoint should start with http:// or https://[/yellow]"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
self.config.set_endpoint(endpoint)
|
|
54
|
+
self.console.print(f"[green]✓ Endpoint set to: {endpoint}[/green]")
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
def get_endpoint(self):
|
|
58
|
+
"""Display current endpoint"""
|
|
59
|
+
endpoint = self.config.get_endpoint()
|
|
60
|
+
self.console.print(f"Endpoint: [cyan]{endpoint}[/cyan]")
|
|
61
|
+
|
|
62
|
+
def set_model(self, model: str):
|
|
63
|
+
"""Set default model"""
|
|
64
|
+
self.config.set_default_model(model)
|
|
65
|
+
self.console.print(f"[green]✓ Default model set to: {model}[/green]")
|
|
66
|
+
|
|
67
|
+
def show_all(self):
|
|
68
|
+
"""Display all configuration settings"""
|
|
69
|
+
settings = self.config.get_all()
|
|
70
|
+
|
|
71
|
+
table = Table(
|
|
72
|
+
title="Current Configuration",
|
|
73
|
+
show_header=True,
|
|
74
|
+
header_style="bold magenta"
|
|
75
|
+
)
|
|
76
|
+
table.add_column("Setting", style="cyan", width=20)
|
|
77
|
+
table.add_column("Value", style="white")
|
|
78
|
+
|
|
79
|
+
for key, value in settings.items():
|
|
80
|
+
# Mask API key
|
|
81
|
+
if key == "api_key" and value:
|
|
82
|
+
display_value = value[:8] + "..." + value[-4:] if len(value) > 12 else "***"
|
|
83
|
+
else:
|
|
84
|
+
display_value = str(value)
|
|
85
|
+
|
|
86
|
+
table.add_row(key, display_value)
|
|
87
|
+
|
|
88
|
+
self.console.print(table)
|
|
89
|
+
|
|
90
|
+
def reset(self):
|
|
91
|
+
"""Reset configuration to defaults"""
|
|
92
|
+
self.console.print("[yellow]Resetting configuration to defaults...[/yellow]")
|
|
93
|
+
self.config.reset()
|
|
94
|
+
self.console.print("[green]✓ Configuration reset successfully![/green]")
|
|
95
|
+
|
|
96
|
+
def show_location(self):
|
|
97
|
+
"""Show configuration file location"""
|
|
98
|
+
self.console.print(Panel(
|
|
99
|
+
f"Configuration file location:\n[cyan]{self.config.config_file}[/cyan]\n\n"
|
|
100
|
+
f"History directory:\n[cyan]{self.config.config_dir / 'history'}[/cyan]",
|
|
101
|
+
title="[bold]Config Location[/bold]",
|
|
102
|
+
border_style="cyan"
|
|
103
|
+
))
|