kubrick-cli 0.1.2__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.
kubrick_cli/main.py ADDED
@@ -0,0 +1,797 @@
1
+ #!/usr/bin/env python3
2
+ """Main CLI entry point for Kubrick."""
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import re
8
+ import sys
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+
12
+ from rich.console import Console
13
+ from rich.markdown import Markdown
14
+ from rich.panel import Panel
15
+ from rich.prompt import Prompt
16
+ from rich.table import Table
17
+
18
+ from .agent_loop import AgentLoop
19
+ from .classifier import TaskClassifier
20
+ from .config import KubrickConfig
21
+ from .display import DisplayManager
22
+ from .execution_strategy import ExecutionStrategy
23
+ from .planning import PlanningPhase
24
+ from .providers.factory import ProviderFactory
25
+ from .safety import SafetyConfig, SafetyManager
26
+ from .scheduler import ToolScheduler
27
+ from .tools import TOOL_DEFINITIONS, ToolExecutor, get_tools_prompt
28
+
29
+ console = Console()
30
+
31
+
32
+ class KubrickCLI:
33
+ """Main CLI application."""
34
+
35
+ def __init__(
36
+ self,
37
+ config: KubrickConfig,
38
+ working_dir: str = None,
39
+ conversation_id: str = None,
40
+ provider_override: str = None,
41
+ ):
42
+ """
43
+ Initialize Kubrick CLI.
44
+
45
+ Args:
46
+ config: KubrickConfig instance
47
+ working_dir: Working directory for file operations (overrides config)
48
+ conversation_id: Load existing conversation by ID
49
+ provider_override: Override configured provider (for testing)
50
+ """
51
+ self.config = config
52
+
53
+ # Override provider if specified
54
+ if provider_override:
55
+ config.set("provider", provider_override)
56
+
57
+ # Create provider using factory
58
+ try:
59
+ self.provider = ProviderFactory.create_provider(config.get_all())
60
+ console.print(
61
+ f"[dim]→ Using {self.provider.provider_name} provider "
62
+ f"with model {self.provider.model_name}[/dim]"
63
+ )
64
+ except ValueError as e:
65
+ console.print(f"[red]Error: {e}[/red]")
66
+ console.print(
67
+ "[yellow]Please run the setup wizard or check your configuration.[/yellow]"
68
+ )
69
+ raise
70
+
71
+ # Alias for backward compatibility
72
+ self.client = self.provider
73
+
74
+ # Initialize safety manager
75
+ self.safety_manager = SafetyManager(
76
+ SafetyConfig.from_config(config.get_all())
77
+ )
78
+
79
+ # Initialize tool executor with safety manager
80
+ self.tool_executor = ToolExecutor(
81
+ working_dir=working_dir, safety_manager=self.safety_manager
82
+ )
83
+
84
+ # Initialize display manager
85
+ self.display_manager = DisplayManager(config.get_all())
86
+
87
+ # Initialize tool scheduler
88
+ enable_parallel = config.get("enable_parallel_tools", True)
89
+ max_workers = config.get("max_parallel_workers", 3)
90
+ self.tool_scheduler = ToolScheduler(
91
+ tool_executor=self.tool_executor,
92
+ max_workers=max_workers,
93
+ enable_parallel=enable_parallel,
94
+ )
95
+
96
+ # Initialize agent loop with config values
97
+ max_iterations = config.get("max_iterations", 15)
98
+ max_tools_per_turn = config.get("max_tools_per_turn", 5)
99
+ timeout_seconds = config.get("total_timeout_seconds", 600)
100
+
101
+ self.agent_loop = AgentLoop(
102
+ llm_client=self.provider,
103
+ tool_executor=self.tool_executor,
104
+ max_iterations=max_iterations,
105
+ max_tools_per_turn=max_tools_per_turn,
106
+ timeout_seconds=timeout_seconds,
107
+ stream_options={},
108
+ display_manager=self.display_manager,
109
+ tool_scheduler=self.tool_scheduler,
110
+ )
111
+
112
+ # Initialize task classifier and planning phase
113
+ self.classifier = TaskClassifier(self.provider)
114
+ self.planning_phase = PlanningPhase(
115
+ llm_client=self.provider,
116
+ tool_executor=self.tool_executor,
117
+ agent_loop=self.agent_loop,
118
+ )
119
+
120
+ # Interrupt handling state
121
+ self.interrupt_count = 0
122
+
123
+ # Generate conversation ID
124
+ self.conversation_id = conversation_id or datetime.now().strftime(
125
+ "%Y%m%d_%H%M%S"
126
+ )
127
+
128
+ # Initialize or load conversation
129
+ if conversation_id:
130
+ loaded = self._load_conversation(conversation_id)
131
+ if loaded:
132
+ self.messages = loaded
133
+ else:
134
+ console.print(
135
+ f"[yellow]Conversation {conversation_id} not found, starting new conversation[/yellow]"
136
+ )
137
+ self.messages = self._get_initial_messages()
138
+ else:
139
+ self.messages = self._get_initial_messages()
140
+
141
+ def _get_initial_messages(self) -> list:
142
+ """Get initial system prompt messages."""
143
+ return [
144
+ {
145
+ "role": "system",
146
+ "content": f"""You are Kubrick, an AI coding assistant with agentic capabilities and file system access.
147
+
148
+ Current working directory: {self.tool_executor.working_dir}
149
+
150
+ # Agentic Behavior
151
+
152
+ You can ITERATE through multiple tool calls until a task is complete. You are not limited to a single response.
153
+
154
+ ## Process
155
+
156
+ 1. Call tools to gather information or make changes
157
+ 2. Analyze the results
158
+ 3. Continue calling tools as needed
159
+ 4. Signal completion when done
160
+
161
+ # Completion Signal
162
+
163
+ When you've completed the task, say "TASK_COMPLETE" followed by a summary of what you accomplished.
164
+
165
+ # Tool Call Format (EXACT SYNTAX REQUIRED)
166
+
167
+ ```tool_call
168
+ {{
169
+ "tool": "tool_name",
170
+ "parameters": {{
171
+ "param": "value"
172
+ }}
173
+ }}
174
+ ```
175
+
176
+ # Available Tools
177
+
178
+ {get_tools_prompt()}
179
+
180
+ # How to Explore Directories
181
+
182
+ To explore the full codebase structure:
183
+ - Use `list_files` with pattern `**/*.py` to list all Python files recursively
184
+ - Use `list_files` with pattern `**/*` to see ALL files and directories
185
+ - Use `run_bash` with `find . -type f` to list all files
186
+ - Use `run_bash` with `tree` or `ls -R` to see directory structure
187
+
188
+ Example - list all Python files:
189
+ ```tool_call
190
+ {{
191
+ "tool": "list_files",
192
+ "parameters": {{
193
+ "pattern": "**/*.py"
194
+ }}
195
+ }}
196
+ ```
197
+
198
+ # Important Rules
199
+
200
+ 1. **ITERATE**: Call tools immediately when needed, then analyze results and continue iterating
201
+ 2. **MULTIPLE TOOLS**: You can call multiple tools per response
202
+ 3. **READ BEFORE EDIT**: Always read a file before editing it
203
+ 4. **EXPLORE THOROUGHLY**: Use `**/*` patterns to see all files in subdirectories
204
+ 5. **SIGNAL COMPLETION**: Say "TASK_COMPLETE" when the task is done
205
+ 6. **USE TOOLS IMMEDIATELY**: Don't ask permission - just call the tool
206
+
207
+ # Examples
208
+
209
+ User: "Read the config.pbtxt file"
210
+ Assistant: I'll read that file for you.
211
+
212
+ ```tool_call
213
+ {{
214
+ "tool": "read_file",
215
+ "parameters": {{
216
+ "file_path": "config.pbtxt"
217
+ }}
218
+ }}
219
+ ```
220
+
221
+ User: "Create a new Python script called test.py with a hello world function"
222
+ Assistant: I'll create that file for you.
223
+
224
+ ```tool_call
225
+ {{
226
+ "tool": "write_file",
227
+ "parameters": {{
228
+ "file_path": "test.py",
229
+ "content": "def hello_world():\\n print('Hello, World!')\\n\\nif __name__ == '__main__':\\n hello_world()\\n"
230
+ }}
231
+ }}
232
+ ```
233
+
234
+ User: "Read main.py and add logging to all functions"
235
+ Assistant: I'll first read the file to understand its structure.
236
+
237
+ ```tool_call
238
+ {{
239
+ "tool": "read_file",
240
+ "parameters": {{
241
+ "file_path": "main.py"
242
+ }}
243
+ }}
244
+ ```
245
+
246
+ [After seeing the results, I'll add logging and continue iterating until done, then say TASK_COMPLETE]
247
+ """,
248
+ }
249
+ ]
250
+
251
+ def _load_conversation(self, conversation_id: str) -> list:
252
+ """Load a conversation by ID."""
253
+ data = self.config.load_conversation(conversation_id)
254
+ if data:
255
+ return data.get("messages", [])
256
+ return None
257
+
258
+ def _save_conversation(self):
259
+ """Save current conversation to disk."""
260
+ if self.config.get("auto_save_conversations", True):
261
+ metadata = {
262
+ "working_dir": str(self.tool_executor.working_dir),
263
+ "provider": self.provider.provider_name,
264
+ "model_name": self.provider.model_name,
265
+ "saved_at": datetime.now().isoformat(),
266
+ }
267
+ self.config.save_conversation(
268
+ self.conversation_id, self.messages, metadata
269
+ )
270
+
271
+ def parse_tool_calls(self, text: str) -> list:
272
+ """
273
+ Parse tool calls from LLM response.
274
+
275
+ Args:
276
+ text: Response text from LLM
277
+
278
+ Returns:
279
+ List of (tool_name, parameters) tuples
280
+ """
281
+ tool_calls = []
282
+
283
+ # Primary parser: Find all ```tool_call blocks (correct format)
284
+ pattern = r"```tool_call\s*\n(.*?)\n```"
285
+ matches = re.findall(pattern, text, re.DOTALL)
286
+
287
+ for match in matches:
288
+ try:
289
+ tool_data = json.loads(match.strip())
290
+ tool_name = tool_data.get("tool")
291
+ parameters = tool_data.get("parameters", {})
292
+ if tool_name:
293
+ tool_calls.append((tool_name, parameters))
294
+ except json.JSONDecodeError as e:
295
+ console.print(f"[red]Failed to parse tool call: {e}[/red]")
296
+ continue
297
+
298
+ # Fallback parser: Look for standalone JSON objects that look like tool calls
299
+ # This handles cases where the LLM forgot the markdown fence
300
+ if not tool_calls:
301
+ # Look for JSON objects with "tool" and "parameters" keys
302
+ json_pattern = r'\{\s*"tool"\s*:\s*"([^"]+)"\s*,\s*"parameters"\s*:\s*\{[^}]*\}\s*\}'
303
+ json_matches = re.findall(json_pattern, text, re.DOTALL)
304
+
305
+ if json_matches:
306
+ console.print(
307
+ "[yellow]⚠ Warning: Detected tool call without proper markdown fence. "
308
+ "Parsing anyway, but please use ```tool_call format.[/yellow]"
309
+ )
310
+
311
+ # Try to extract the full JSON objects
312
+ # More lenient pattern to capture the whole object
313
+ full_json_pattern = r'(\{\s*"tool"\s*:\s*"[^"]+"\s*,\s*"parameters"\s*:\s*\{.*?\}\s*\})'
314
+ for match in re.finditer(full_json_pattern, text, re.DOTALL):
315
+ try:
316
+ tool_data = json.loads(match.group(1))
317
+ tool_name = tool_data.get("tool")
318
+ parameters = tool_data.get("parameters", {})
319
+ if tool_name:
320
+ tool_calls.append((tool_name, parameters))
321
+ except json.JSONDecodeError:
322
+ continue
323
+
324
+ return tool_calls
325
+
326
+ def display_streaming_response(self, chunks: list) -> str:
327
+ """
328
+ Display streaming response and return full text.
329
+
330
+ Args:
331
+ chunks: List of text chunks
332
+
333
+ Returns:
334
+ Full response text
335
+ """
336
+ full_text = "".join(chunks)
337
+
338
+ # Split response around tool calls for better display
339
+ parts = re.split(r"(```tool_call.*?```)", full_text, flags=re.DOTALL)
340
+
341
+ for part in parts:
342
+ if part.startswith("```tool_call"):
343
+ # Display tool call in a panel
344
+ console.print(
345
+ Panel(part, title="Tool Call", border_style="cyan")
346
+ )
347
+ elif part.strip():
348
+ # Display regular text as markdown
349
+ console.print(Markdown(part))
350
+
351
+ return full_text
352
+
353
+ def run_conversation_turn(self, user_message: str) -> None:
354
+ """
355
+ Run one turn of the conversation with optimized execution strategy.
356
+
357
+ Args:
358
+ user_message: User's message
359
+ """
360
+ # Add user message to conversation
361
+ self.messages.append({"role": "user", "content": user_message})
362
+
363
+ try:
364
+ # Step 1: Classify task (if enabled)
365
+ classification = None
366
+ if self.config.get("enable_task_classification", True):
367
+ classification = self.classifier.classify(
368
+ user_message, self.messages
369
+ )
370
+
371
+ # Step 2: Get execution strategy based on classification
372
+ exec_config = ExecutionStrategy.get_execution_config(
373
+ classification,
374
+ self.provider.provider_name,
375
+ self.provider.model_name,
376
+ )
377
+
378
+ console.print(
379
+ f"[dim]→ Execution mode: {exec_config.mode} "
380
+ f"(model: {exec_config.model_tier}, "
381
+ f"max_iter: {exec_config.max_iterations})[/dim]"
382
+ )
383
+
384
+ # Step 3: Switch model if needed (and provider supports it)
385
+ current_model = self.provider.model_name
386
+ target_model = exec_config.hyperparameters.get("model")
387
+
388
+ if target_model and target_model != current_model:
389
+ try:
390
+ self.provider.set_model(target_model)
391
+ console.print(
392
+ f"[dim]→ Switched model: {current_model} → {target_model}[/dim]"
393
+ )
394
+ except Exception:
395
+ # Provider doesn't support model switching, continue with current
396
+ pass
397
+
398
+ else:
399
+ # Classification disabled, use default complex config
400
+ exec_config = ExecutionStrategy.get_execution_config(
401
+ None, # Will trigger fallback
402
+ self.provider.provider_name,
403
+ self.provider.model_name,
404
+ )
405
+
406
+ # Step 4: Execute based on mode
407
+ if exec_config.mode == "conversational":
408
+ # Conversational mode: single-turn response, no tools
409
+ self._run_conversational_turn(exec_config)
410
+
411
+ else:
412
+ # Agentic modes: use agent loop
413
+ self._run_agentic_turn(
414
+ classification, exec_config, user_message
415
+ )
416
+
417
+ except KeyboardInterrupt:
418
+ console.print("\n[yellow]Interrupted[/yellow]")
419
+ except Exception as e:
420
+ import traceback
421
+
422
+ console.print(f"\n[red]Error: {e}[/red]")
423
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
424
+
425
+ def _run_conversational_turn(self, exec_config):
426
+ """
427
+ Handle conversational turn (no tools, single response).
428
+
429
+ Args:
430
+ exec_config: Execution configuration
431
+ """
432
+ console.print("[bold cyan]Assistant:[/bold cyan]")
433
+ chunks = []
434
+
435
+ # Pass hyperparameters to provider
436
+ stream_options = exec_config.hyperparameters.copy()
437
+
438
+ for chunk in self.provider.generate_streaming(
439
+ self.messages, stream_options=stream_options
440
+ ):
441
+ console.print(chunk, end="")
442
+ chunks.append(chunk)
443
+
444
+ console.print("\n")
445
+
446
+ # Add response to conversation
447
+ response_text = "".join(chunks)
448
+ self.messages.append({"role": "assistant", "content": response_text})
449
+
450
+ def _run_agentic_turn(self, classification, exec_config, user_message):
451
+ """
452
+ Handle agentic turn (with tools and iterations).
453
+
454
+ Args:
455
+ classification: Task classification
456
+ exec_config: Execution configuration
457
+ user_message: Original user message
458
+ """
459
+ # Check if planning phase should be offered
460
+ if (
461
+ exec_config.use_planning
462
+ and classification
463
+ and classification.complexity == "COMPLEX"
464
+ and self.config.get("enable_planning_phase", True)
465
+ ):
466
+ response = Prompt.ask(
467
+ "[bold yellow]This looks complex. Create a plan first?[/bold yellow]",
468
+ choices=["yes", "no"],
469
+ default="yes",
470
+ )
471
+
472
+ if response == "yes":
473
+ # Execute planning phase
474
+ plan = self.planning_phase.execute_planning(
475
+ user_message, self.messages
476
+ )
477
+
478
+ # Get user approval
479
+ approval = self.planning_phase.get_user_approval(plan)
480
+
481
+ if not approval.get("approved", False):
482
+ console.print("[yellow]Task cancelled by user[/yellow]")
483
+ return
484
+
485
+ # If modifications requested, add them to conversation
486
+ if "modifications" in approval:
487
+ self.messages.append(
488
+ {
489
+ "role": "user",
490
+ "content": f"Plan modifications: {approval['modifications']}",
491
+ }
492
+ )
493
+
494
+ # Update agent loop max iterations for this turn
495
+ original_max_iterations = self.agent_loop.max_iterations
496
+ self.agent_loop.max_iterations = exec_config.max_iterations
497
+
498
+ # Run agent loop with hyperparameters
499
+ self.agent_loop.stream_options = exec_config.hyperparameters
500
+
501
+ result = self.agent_loop.run(
502
+ messages=self.messages,
503
+ tool_parser=self.parse_tool_calls,
504
+ display_callback=None,
505
+ )
506
+
507
+ # Restore original max iterations
508
+ self.agent_loop.max_iterations = original_max_iterations
509
+
510
+ # Display result summary
511
+ if result["success"]:
512
+ console.print(
513
+ f"\n[dim]Completed in {result['iterations']} iteration(s) "
514
+ f"with {result['tool_calls']} tool call(s)[/dim]"
515
+ )
516
+ else:
517
+ console.print(f"\n[red]Error: {result.get('error')}[/red]")
518
+
519
+ def run(self):
520
+ """Run the interactive CLI."""
521
+ console.print(
522
+ Panel.fit(
523
+ "[bold cyan]Kubrick CLI[/bold cyan]\n"
524
+ f"Working directory: {self.tool_executor.working_dir}\n"
525
+ f"Conversation ID: {self.conversation_id}\n"
526
+ "Type your questions or commands. Use 'exit' or 'quit' to exit.\n"
527
+ "Type '/help' to see available commands.",
528
+ border_style="cyan",
529
+ )
530
+ )
531
+
532
+ # Check provider health
533
+ if not self.provider.is_healthy():
534
+ console.print(
535
+ f"[red]Warning: Cannot connect to {self.provider.provider_name} provider[/red]"
536
+ )
537
+ console.print(
538
+ "[yellow]Please check your provider configuration and connectivity.[/yellow]"
539
+ )
540
+
541
+ while True:
542
+ try:
543
+ user_input = Prompt.ask("\n[bold green]You[/bold green]")
544
+
545
+ # Reset interrupt count when user enters a message
546
+ self.interrupt_count = 0
547
+
548
+ if not user_input.strip():
549
+ continue
550
+
551
+ if user_input.lower() in ["exit", "quit", "q"]:
552
+ # Save conversation before exiting
553
+ self._save_conversation()
554
+ console.print(
555
+ f"[cyan]Conversation saved as {self.conversation_id}[/cyan]"
556
+ )
557
+ console.print("[cyan]Goodbye![/cyan]")
558
+ break
559
+
560
+ # Handle special commands
561
+ if user_input.startswith("/"):
562
+ self._handle_command(user_input)
563
+ continue
564
+
565
+ self.run_conversation_turn(user_input)
566
+
567
+ # Auto-save after each turn
568
+ self._save_conversation()
569
+
570
+ except KeyboardInterrupt:
571
+ self.interrupt_count += 1
572
+
573
+ if self.interrupt_count == 1:
574
+ # First Ctrl+C: Clear prompt
575
+ console.print("\n[yellow]^C (Press again to start new conversation, once more to exit)[/yellow]")
576
+ continue
577
+
578
+ elif self.interrupt_count == 2:
579
+ # Second Ctrl+C: Start new conversation
580
+ console.print("\n[yellow]Starting new conversation...[/yellow]")
581
+ self._save_conversation()
582
+
583
+ # Reset conversation
584
+ self.conversation_id = datetime.now().strftime("%Y%m%d_%H%M%S")
585
+ self.messages = self._get_initial_messages()
586
+
587
+ console.print(f"[cyan]New conversation ID: {self.conversation_id}[/cyan]")
588
+ console.print("[dim]Press Ctrl+C once more to exit[/dim]")
589
+ continue
590
+
591
+ else:
592
+ # Third Ctrl+C: Exit
593
+ self._save_conversation()
594
+ console.print(
595
+ f"\n[cyan]Conversation saved as {self.conversation_id}[/cyan]"
596
+ )
597
+ console.print("[cyan]Goodbye![/cyan]")
598
+ break
599
+
600
+ except Exception as e:
601
+ console.print(f"[red]Error: {e}[/red]")
602
+
603
+ def _handle_command(self, command: str):
604
+ """Handle special CLI commands."""
605
+ parts = command.strip().split()
606
+ cmd = parts[0].lower()
607
+
608
+ if cmd == "/save":
609
+ self._save_conversation()
610
+ console.print(
611
+ f"[green]Conversation saved as {self.conversation_id}[/green]"
612
+ )
613
+
614
+ elif cmd == "/list":
615
+ limit = int(parts[1]) if len(parts) > 1 else 20
616
+ conversations = self.config.list_conversations(limit=limit)
617
+
618
+ if not conversations:
619
+ console.print("[yellow]No saved conversations found[/yellow]")
620
+ return
621
+
622
+ table = Table(title="Saved Conversations")
623
+ table.add_column("ID", style="cyan")
624
+ table.add_column("Messages", justify="right", style="green")
625
+ table.add_column("Working Dir", style="dim")
626
+ table.add_column("Modified", style="yellow")
627
+
628
+ for conv in conversations:
629
+ conv_id = conv["id"]
630
+ msg_count = str(conv["message_count"])
631
+ working_dir = conv["metadata"].get("working_dir", "N/A")
632
+ modified = datetime.fromtimestamp(conv["modified"]).strftime(
633
+ "%Y-%m-%d %H:%M"
634
+ )
635
+
636
+ table.add_row(conv_id, msg_count, working_dir, modified)
637
+
638
+ console.print(table)
639
+
640
+ elif cmd == "/config":
641
+ if len(parts) == 1:
642
+ # Show current config
643
+ config_data = self.config.get_all()
644
+ table = Table(title="Current Configuration")
645
+ table.add_column("Setting", style="cyan")
646
+ table.add_column("Value", style="green")
647
+
648
+ for key, value in config_data.items():
649
+ table.add_row(key, str(value))
650
+
651
+ console.print(table)
652
+
653
+ elif len(parts) == 3:
654
+ # Set config value
655
+ key = parts[1]
656
+ value = parts[2]
657
+
658
+ # Try to parse as JSON for booleans/numbers
659
+ try:
660
+ value = json.loads(value)
661
+ except json.JSONDecodeError:
662
+ pass # Keep as string
663
+
664
+ self.config.set(key, value)
665
+ console.print(f"[green]Set {key} = {value}[/green]")
666
+
667
+ else:
668
+ console.print("[yellow]Usage: /config [key value][/yellow]")
669
+
670
+ elif cmd == "/delete":
671
+ if len(parts) < 2:
672
+ console.print(
673
+ "[yellow]Usage: /delete <conversation_id>[/yellow]"
674
+ )
675
+ return
676
+
677
+ conv_id = parts[1]
678
+ if self.config.delete_conversation(conv_id):
679
+ console.print(f"[green]Deleted conversation {conv_id}[/green]")
680
+ else:
681
+ console.print(
682
+ f"[yellow]Conversation {conv_id} not found[/yellow]"
683
+ )
684
+
685
+ elif cmd == "/debug":
686
+ # Show debug information
687
+ table = Table(title="Debug Information")
688
+ table.add_column("Item", style="cyan")
689
+ table.add_column("Value", style="green")
690
+
691
+ table.add_row("Conversation ID", self.conversation_id)
692
+ table.add_row("Messages Count", str(len(self.messages)))
693
+ table.add_row("Working Dir", str(self.tool_executor.working_dir))
694
+ table.add_row("Provider", self.provider.provider_name)
695
+ table.add_row("Model Name", self.provider.model_name)
696
+
697
+ console.print(table)
698
+
699
+ # Show system prompt
700
+ if len(parts) > 1 and parts[1] == "prompt":
701
+ console.print("\n[bold cyan]System Prompt:[/bold cyan]")
702
+ system_msg = next(
703
+ (m for m in self.messages if m["role"] == "system"), None
704
+ )
705
+ if system_msg:
706
+ console.print(
707
+ Panel(system_msg["content"], border_style="dim")
708
+ )
709
+ else:
710
+ console.print("[yellow]No system message found[/yellow]")
711
+
712
+ elif cmd == "/help":
713
+ console.print("""
714
+ [bold cyan]Available Commands:[/bold cyan]
715
+
716
+ [green]/save[/green] - Save the current conversation
717
+ [green]/list [N][/green] - List saved conversations (default: 20)
718
+ [green]/config[/green] - Show current configuration
719
+ [green]/config KEY VALUE[/green] - Set a configuration value
720
+ [green]/delete ID[/green] - Delete a conversation
721
+ [green]/debug[/green] - Show debug information
722
+ [green]/debug prompt[/green] - Show the system prompt
723
+ [green]/help[/green] - Show this help message
724
+ """)
725
+
726
+ else:
727
+ console.print(f"[yellow]Unknown command: {cmd}[/yellow]")
728
+ console.print(
729
+ "[dim]Available commands: /save, /list, /config, /delete, /debug, /help[/dim]"
730
+ )
731
+
732
+
733
+ def main():
734
+ """Main entry point."""
735
+ # Initialize config (creates ~/.kubrick if needed, runs setup wizard on first run)
736
+ config = KubrickConfig()
737
+
738
+ parser = argparse.ArgumentParser(
739
+ description="Kubrick - AI-assisted coding CLI with agentic capabilities",
740
+ formatter_class=argparse.RawDescriptionHelpFormatter,
741
+ epilog="""
742
+ Special Commands:
743
+ /save Save the current conversation
744
+ /list [N] List saved conversations (default: 20)
745
+ /config Show current configuration
746
+ /config KEY VALUE Set a configuration value
747
+ /delete ID Delete a conversation
748
+
749
+ Examples:
750
+ kubrick # Start new conversation with configured provider
751
+ kubrick --load 20240118_143022 # Load a previous conversation
752
+ kubrick --working-dir /path/to/project # Set working directory
753
+ kubrick --provider openai # Override provider for this session
754
+ """,
755
+ )
756
+
757
+ parser.add_argument(
758
+ "--working-dir",
759
+ default=None,
760
+ help="Working directory for file operations (default: current directory)",
761
+ )
762
+
763
+ parser.add_argument(
764
+ "--load",
765
+ "--load-conversation",
766
+ dest="conversation_id",
767
+ default=None,
768
+ help="Load a previous conversation by ID",
769
+ )
770
+
771
+ parser.add_argument(
772
+ "--provider",
773
+ default=None,
774
+ choices=["triton", "openai", "anthropic"],
775
+ help="Override configured provider for this session",
776
+ )
777
+
778
+ args = parser.parse_args()
779
+
780
+ # Create and run CLI
781
+ try:
782
+ cli = KubrickCLI(
783
+ config=config,
784
+ working_dir=args.working_dir,
785
+ conversation_id=args.conversation_id,
786
+ provider_override=args.provider,
787
+ )
788
+
789
+ cli.run()
790
+ except Exception as e:
791
+ console.print(f"[red]Failed to start Kubrick: {e}[/red]")
792
+ import sys
793
+ sys.exit(1)
794
+
795
+
796
+ if __name__ == "__main__":
797
+ main()