Coverage for src / osiris_cli / commands.py: 0%
254 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-27 17:41 +0200
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-27 17:41 +0200
1from typing import Any
2import json
3from datetime import datetime
4from pathlib import Path
5import os
6import asyncio
7import shutil
9from .config import settings
10from .context import context
11from .session import session
12from .client import async_client, client
13from .tools import tools
14from .mcp import mcp_client
15from .config_wizard import ConfigWizard
16from .session_view import SessionManageScreen
17from .recipe_view import RecipeScreen
18from .file_picker import FilePickerScreen
19from .context_view import FileActionScreen
20from .prompt_view import PromptScreen
21from .search_view import SearchScreen
22from .persona_view import PersonaScreen
23from .lsp_view import LSPConfigScreen
24from .help_screen import HelpScreen
25from .screens import (
26 SettingsScreen, ToolsScreen, MCPSettingsScreen, ModelMatrix
27)
28from .ui import (
29 VoidSessions, VoidSettings, VoidPrompts,
30 VoidRecipes, VoidSearch, VoidFilePicker, VoidHelp,
31 VoidModels
32)
33from textual.widgets import Label
34from .header import OsirisHeader
36class CommandHandler:
37 def __init__(self, app: Any):
38 self.app = app
40 async def handle(self, cmd_str: str):
41 try:
42 parts = cmd_str.split()
43 if not parts: return
44 cmd = parts[0].lower()
45 args = parts[1:]
47 if cmd in ("/new", "/clear"):
48 self.app.query_one("#chat-scroll").remove_children()
49 context.clear()
50 session.clear()
51 await self.app.update_context_tree()
53 # Reset Header Stats
54 try:
55 self.app.query_one(OsirisHeader).update_tokens(0)
56 except Exception: pass
58 await self.app.log_agent("Session Reset.", "success")
60 await self.app.add_chat_message("Session Reset.", "assistant")
61 elif cmd == "/quit":
62 self.app.exit()
63 elif cmd == "/undo":
64 if session.undo_last_step():
65 # Refresh chat view
66 self.app.query_one("#chat-scroll").remove_children()
67 for msg in session.messages:
68 role = msg.get("role")
69 content = msg.get("content")
70 if role in ("user", "assistant") and content:
71 # Skip empty assistant messages (e.g. pure tool calls that were not fully undone?)
72 # undo_last_step removes tool outputs and tool calls, so we should be clean.
73 if role == "assistant" and not content.strip(): continue
74 await self.app.add_chat_message(content, role)
75 await self.app.log_agent("Undid last action.", "info")
76 else:
77 await self.app.add_chat_message("Nothing to undo.", "assistant")
78 elif cmd == "/resume":
79 if session.load_last_session():
80 self.app.query_one("#chat-scroll").remove_children()
81 await self.app.add_chat_message(f"Resumed session: {session.current_session_id}", "assistant")
83 # Re-populate context
84 context.clear()
85 for f in session.context_files:
86 context.add_file(f)
87 await self.app.update_context_tree()
89 # Re-populate chat
90 for msg in session.messages:
91 role = msg.get("role")
92 content = msg.get("content")
93 if role in ("user", "assistant") and content:
94 if role == "assistant" and not content.strip(): continue
95 await self.app.add_chat_message(content, role)
97 await self.app.log_agent(f"Resumed session {session.current_session_id}", "success")
98 else:
99 await self.app.add_chat_message("No previous session found.", "assistant")
100 elif cmd == "/config":
101 changed = await self.app.push_screen_wait(VoidSettings())
102 if changed:
103 async_client.__init__()
104 client.__init__()
105 await self.app.log_agent("System Reconfigured.", "success")
106 elif cmd == "/persona":
107 res = await self.app.push_screen_wait(PersonaScreen())
108 if res == "saved":
109 await self.app.log_agent("Persona updated.", "success")
110 await self.app.add_chat_message("System Prompt updated successfully.", "assistant")
111 elif cmd == "/lsp": await self.app.push_screen_wait(LSPConfigScreen())
112 elif cmd == "/settings": await self.app.push_screen_wait(VoidSettings())
113 elif cmd == "/status":
114 msg = f"**Status:**\n- Model: `{settings.default_model}`\n- Provider: `{settings.provider}`\n- Context: {len(context.loaded_files)} files"
115 await self.app.add_chat_message(msg, "assistant")
116 elif cmd == "/sessions":
117 while True:
118 res = await self.app.push_screen_wait(VoidSessions())
119 if not res: break
121 action = res.get("action")
122 if action == "load":
123 if session.load_session(res.get("id")):
124 await self.app.add_chat_message(f"Loaded session {res.get('id')}", "assistant")
125 # Restore context and chat
126 self.app.query_one("#chat-scroll").remove_children()
127 context.clear()
128 for f in session.context_files: context.add_file(f)
129 await self.app.update_context_tree()
130 for msg in session.messages:
131 role = msg.get("role")
132 content = msg.get("content")
133 if role in ("user", "assistant") and content:
134 if role == "assistant" and not content.strip(): continue
135 await self.app.add_chat_message(content, role)
136 break
137 elif action == "delete":
138 ids = res.get("ids", [])
139 count = 0
140 for sid in ids:
141 if session.delete_session(sid): count += 1
142 if count > 0:
143 await self.app.log_agent(f"Deleted {count} sessions.", "success")
144 else:
145 break
146 elif cmd == "/files": self.app.action_toggle_sidebar()
147 elif cmd == "/add":
148 if args:
149 if context.add_file(args[0]): await self.app.update_context_tree()
150 else:
151 res = await self.app.push_screen_wait(VoidFilePicker("."))
152 if res:
153 if context.add_file(str(res)): await self.app.update_context_tree()
154 elif cmd in ("/remove", "/drop", "/rm") and args:
155 context.remove_file(args[0])
156 await self.app.update_context_tree()
157 await self.app.add_chat_message(f"Removed: {args[0]}", "assistant")
158 elif cmd == "/tree":
159 try:
160 tree_view = tools.tools["get_file_tree"]["implementation"]()
161 await self.app.add_chat_message(f"**Project Tree**\n```\n{tree_view}\n```", "assistant")
162 except Exception as e:
163 await self.app.add_chat_message(f"Tree error: {e}", "assistant")
164 elif cmd == "/export_json":
165 if not args:
166 await self.app.add_chat_message("Usage: /export_json <filename.json>", "assistant")
167 return
169 filename = args[0]
170 try:
171 # Get the full session data to export
172 session_data = {
173 "id": session.current_session_id,
174 "timestamp": datetime.now().isoformat(),
175 "model": settings.default_model,
176 "context_files": list(context.loaded_files.keys()), # Only paths
177 "messages": session.messages,
178 "allowed_tools": session.allowed_tools,
179 "total_tokens": session.total_tokens,
180 }
182 Path(filename).write_text(json.dumps(session_data, indent=2), encoding="utf-8")
183 await self.app.add_chat_message(f"Exported session data to `{filename}`", "assistant")
184 except Exception as e:
185 await self.app.add_chat_message(f"Export failed: {e}", "assistant")
186 elif cmd == "/health":
187 await self.app.add_chat_message("🩺 Running System Diagnostics...", "assistant")
188 report = []
190 # 1. Config
191 report.append(f"✅ Provider: {settings.provider} ({settings.default_model})")
193 # 2. Tools
194 for tool in ["git", "rg", "python3"]:
195 path = shutil.which(tool)
196 if path:
197 report.append(f"✅ Tool '{tool}': Found")
198 else:
199 report.append(f"❌ Tool '{tool}': Missing")
201 # 3. Connectivity
202 try:
203 await async_client.list_models()
204 report.append("✅ API Connection: OK")
205 except Exception as e:
206 report.append(f"❌ API Connection: Failed ({e})")
208 await self.app.add_chat_message("\n".join(report), "assistant")
209 elif cmd == "/help": await self.app.push_screen_wait(VoidHelp())
210 elif cmd == "/mcp":
211 if args and args[0].lower() == "bundle":
212 if len(args) < 2:
213 await self.app.add_chat_message("Usage: /mcp bundle <bundle_name>", "assistant")
214 return
215 bundle_name = args[1]
216 if bundle_name not in settings.mcp_bundles:
217 await self.app.add_chat_message(f"Error: MCP bundle '{bundle_name}' not found.", "assistant")
218 return
220 bundle_servers_to_activate = settings.mcp_bundles[bundle_name].get("servers", [])
222 # Store original mcp_servers to restore later if needed, or if this is an override
223 original_mcp_servers = settings.mcp_servers.copy()
225 # Construct the new active mcp_servers dictionary based on the bundle
226 new_active_mcp_servers = {}
227 for server_name in bundle_servers_to_activate:
228 if server_name in original_mcp_servers: # Only add if the server is defined globally
229 new_active_mcp_servers[server_name] = original_mcp_servers[server_name]
230 else:
231 await self.app.add_chat_message(f"Warning: Server '{server_name}' in bundle '{bundle_name}' is not defined in global or project MCP servers.", "assistant")
233 # Temporarily update settings.mcp_servers to reflect the active bundle
234 settings.mcp_servers = new_active_mcp_servers
236 # Reload MCP client with the new server set
237 mcp_client.reload()
239 activated_server_names = ", ".join(new_active_mcp_servers.keys()) if new_active_mcp_servers else "None"
240 await self.app.log_agent(f"MCP Bundle '{bundle_name}' activated. Servers: {activated_server_names}", "success")
241 await self.app.add_chat_message(f"✅ MCP Bundle **'{bundle_name}'** activated. Active servers: `{activated_server_names}`", "assistant")
242 else:
243 await self.app.push_screen_wait(MCPSettingsScreen())
244 elif cmd == "/models":
245 sel = await self.app.push_screen_wait(VoidModels())
246 if sel:
247 settings.default_model = sel
248 settings.save()
249 await self.app.log_agent(f"Model switched to: {sel}", "info")
250 await self.app.add_chat_message(f"Active Model: **{sel}**", "system")
251 elif cmd == "/recipes":
252 rec = await self.app.push_screen_wait(VoidRecipes())
253 if rec:
254 await self.app.add_chat_message(f"🚀 Running Recipe: **{rec}**...", "assistant")
255 await self.app.log_agent(f"Recipe: {rec}", "tool")
256 from .recipes import RecipeRunner
257 runner = RecipeRunner()
258 try:
259 results = await asyncio.to_thread(runner.run, rec)
260 output = f"### Recipe Complete: {rec}\n"
261 for step in results:
262 icon = "✅" if step.success else "❌"
263 output += f"- {icon} **{step.name}**\n"
264 if step.output:
265 preview = step.output.strip()
266 if len(preview) > 300: preview = preview[:300] + "..."
267 output += f" ```\n{preview}\n ```\n"
268 await self.app.add_chat_message(output, "assistant")
269 except Exception as e:
270 await self.app.add_chat_message(f"Recipe Error: {e}", "assistant")
271 elif cmd == "/prompts":
272 from .prompts import prompt_manager # Import here to avoid circular dependencies potentially
274 if args and args[0].lower() == "export":
275 if len(args) < 2:
276 await self.app.add_chat_message("Usage: /prompts export <filename.json>", "assistant")
277 return
278 filename = Path(args[1])
279 try:
280 prompt_manager.export_prompts(filename)
281 await self.app.add_chat_message(f"✅ Prompts exported to `{filename}`.", "assistant")
282 await self.app.log_agent(f"Prompts exported to {filename}", "success")
283 except Exception as e:
284 await self.app.add_chat_message(f"❌ Error exporting prompts: {e}", "assistant")
285 await self.app.log_agent(f"Error exporting prompts: {e}", "error")
286 elif args and args[0].lower() == "import":
287 if len(args) < 2:
288 await self.app.add_chat_message("Usage: /prompts import <filename.json>", "assistant")
289 return
290 filename = Path(args[1])
291 try:
292 added_count = prompt_manager.import_prompts(filename)
293 await self.app.add_chat_message(f"✅ Imported {added_count} new prompts from `{filename}`.", "assistant")
294 await self.app.log_agent(f"Imported {added_count} prompts from {filename}", "success")
295 except FileNotFoundError:
296 await self.app.add_chat_message(f"❌ Error: File not found: `{filename}`.", "assistant")
297 await self.app.log_agent(f"Prompt import file not found: {filename}", "error")
298 except Exception as e:
299 await self.app.add_chat_message(f"❌ Error importing prompts: {e}", "assistant")
300 await self.app.log_agent(f"Error importing prompts: {e}", "error")
301 else:
302 await self.app.action_toggle_prompts()
303 elif cmd == "/tools": await self.app.push_screen_wait(ToolsScreen())
304 elif cmd == "/yolo": self.app.toggle_yolo()
305 elif cmd == "/search": await self.app.action_search_ui()
306 elif cmd == "/reasoning":
307 settings.reasoning_mode = not settings.reasoning_mode
308 settings.save()
309 status = "ENABLED" if settings.reasoning_mode else "DISABLED"
310 await self.app.log_agent(f"Reasoning Mode: {status}", "success")
311 await self.app.add_chat_message(f"🧠 Reasoning Mode: **{status}**", "assistant")
312 else: await self.app.add_chat_message(f"Unknown command: `{cmd}`. Type `/help` for a list of commands.", "assistant")
313 except Exception as e:
314 await self.app.add_chat_message(f"Command Error: {e}", "assistant")
315 await self.app.log_agent(f"Cmd Error: {e}", "error")