kairo-code 0.1.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.
Files changed (144) hide show
  1. image-service/main.py +178 -0
  2. infra/chat/app/main.py +84 -0
  3. kairo/backend/__init__.py +0 -0
  4. kairo/backend/api/__init__.py +0 -0
  5. kairo/backend/api/admin/__init__.py +23 -0
  6. kairo/backend/api/admin/audit.py +54 -0
  7. kairo/backend/api/admin/content.py +142 -0
  8. kairo/backend/api/admin/incidents.py +148 -0
  9. kairo/backend/api/admin/stats.py +125 -0
  10. kairo/backend/api/admin/system.py +87 -0
  11. kairo/backend/api/admin/users.py +279 -0
  12. kairo/backend/api/agents.py +94 -0
  13. kairo/backend/api/api_keys.py +85 -0
  14. kairo/backend/api/auth.py +116 -0
  15. kairo/backend/api/billing.py +41 -0
  16. kairo/backend/api/chat.py +72 -0
  17. kairo/backend/api/conversations.py +125 -0
  18. kairo/backend/api/device_auth.py +100 -0
  19. kairo/backend/api/files.py +83 -0
  20. kairo/backend/api/health.py +36 -0
  21. kairo/backend/api/images.py +80 -0
  22. kairo/backend/api/openai_compat.py +225 -0
  23. kairo/backend/api/projects.py +102 -0
  24. kairo/backend/api/usage.py +32 -0
  25. kairo/backend/api/webhooks.py +79 -0
  26. kairo/backend/app.py +297 -0
  27. kairo/backend/config.py +179 -0
  28. kairo/backend/core/__init__.py +0 -0
  29. kairo/backend/core/admin_auth.py +24 -0
  30. kairo/backend/core/api_key_auth.py +55 -0
  31. kairo/backend/core/database.py +28 -0
  32. kairo/backend/core/dependencies.py +70 -0
  33. kairo/backend/core/logging.py +23 -0
  34. kairo/backend/core/rate_limit.py +73 -0
  35. kairo/backend/core/security.py +29 -0
  36. kairo/backend/models/__init__.py +19 -0
  37. kairo/backend/models/agent.py +30 -0
  38. kairo/backend/models/api_key.py +25 -0
  39. kairo/backend/models/api_usage.py +29 -0
  40. kairo/backend/models/audit_log.py +26 -0
  41. kairo/backend/models/conversation.py +48 -0
  42. kairo/backend/models/device_code.py +30 -0
  43. kairo/backend/models/feature_flag.py +21 -0
  44. kairo/backend/models/image_generation.py +24 -0
  45. kairo/backend/models/incident.py +28 -0
  46. kairo/backend/models/project.py +28 -0
  47. kairo/backend/models/uptime_record.py +24 -0
  48. kairo/backend/models/usage.py +24 -0
  49. kairo/backend/models/user.py +49 -0
  50. kairo/backend/schemas/__init__.py +0 -0
  51. kairo/backend/schemas/admin/__init__.py +0 -0
  52. kairo/backend/schemas/admin/audit.py +28 -0
  53. kairo/backend/schemas/admin/content.py +53 -0
  54. kairo/backend/schemas/admin/stats.py +77 -0
  55. kairo/backend/schemas/admin/system.py +44 -0
  56. kairo/backend/schemas/admin/users.py +48 -0
  57. kairo/backend/schemas/agent.py +42 -0
  58. kairo/backend/schemas/api_key.py +30 -0
  59. kairo/backend/schemas/auth.py +57 -0
  60. kairo/backend/schemas/chat.py +26 -0
  61. kairo/backend/schemas/conversation.py +39 -0
  62. kairo/backend/schemas/device_auth.py +40 -0
  63. kairo/backend/schemas/image.py +15 -0
  64. kairo/backend/schemas/openai_compat.py +76 -0
  65. kairo/backend/schemas/project.py +21 -0
  66. kairo/backend/schemas/status.py +81 -0
  67. kairo/backend/schemas/usage.py +15 -0
  68. kairo/backend/services/__init__.py +0 -0
  69. kairo/backend/services/admin/__init__.py +0 -0
  70. kairo/backend/services/admin/audit_service.py +78 -0
  71. kairo/backend/services/admin/content_service.py +119 -0
  72. kairo/backend/services/admin/incident_service.py +94 -0
  73. kairo/backend/services/admin/stats_service.py +281 -0
  74. kairo/backend/services/admin/system_service.py +126 -0
  75. kairo/backend/services/admin/user_service.py +157 -0
  76. kairo/backend/services/agent_service.py +107 -0
  77. kairo/backend/services/api_key_service.py +66 -0
  78. kairo/backend/services/api_usage_service.py +126 -0
  79. kairo/backend/services/auth_service.py +101 -0
  80. kairo/backend/services/chat_service.py +501 -0
  81. kairo/backend/services/conversation_service.py +264 -0
  82. kairo/backend/services/device_auth_service.py +193 -0
  83. kairo/backend/services/email_service.py +55 -0
  84. kairo/backend/services/image_service.py +181 -0
  85. kairo/backend/services/llm_service.py +186 -0
  86. kairo/backend/services/project_service.py +109 -0
  87. kairo/backend/services/status_service.py +167 -0
  88. kairo/backend/services/stripe_service.py +78 -0
  89. kairo/backend/services/usage_service.py +150 -0
  90. kairo/backend/services/web_search_service.py +96 -0
  91. kairo/migrations/env.py +60 -0
  92. kairo/migrations/versions/001_initial.py +55 -0
  93. kairo/migrations/versions/002_usage_tracking_and_indexes.py +66 -0
  94. kairo/migrations/versions/003_username_to_email.py +21 -0
  95. kairo/migrations/versions/004_add_plans_and_verification.py +67 -0
  96. kairo/migrations/versions/005_add_projects.py +52 -0
  97. kairo/migrations/versions/006_add_image_generation.py +63 -0
  98. kairo/migrations/versions/007_add_admin_portal.py +107 -0
  99. kairo/migrations/versions/008_add_device_code_auth.py +76 -0
  100. kairo/migrations/versions/009_add_status_page.py +65 -0
  101. kairo/tools/extract_claude_data.py +465 -0
  102. kairo/tools/filter_claude_data.py +303 -0
  103. kairo/tools/generate_curated_data.py +157 -0
  104. kairo/tools/mix_training_data.py +295 -0
  105. kairo_code/__init__.py +3 -0
  106. kairo_code/agents/__init__.py +25 -0
  107. kairo_code/agents/architect.py +98 -0
  108. kairo_code/agents/audit.py +100 -0
  109. kairo_code/agents/base.py +463 -0
  110. kairo_code/agents/coder.py +155 -0
  111. kairo_code/agents/database.py +77 -0
  112. kairo_code/agents/docs.py +88 -0
  113. kairo_code/agents/explorer.py +62 -0
  114. kairo_code/agents/guardian.py +80 -0
  115. kairo_code/agents/planner.py +66 -0
  116. kairo_code/agents/reviewer.py +91 -0
  117. kairo_code/agents/security.py +94 -0
  118. kairo_code/agents/terraform.py +88 -0
  119. kairo_code/agents/testing.py +97 -0
  120. kairo_code/agents/uiux.py +88 -0
  121. kairo_code/auth.py +232 -0
  122. kairo_code/config.py +172 -0
  123. kairo_code/conversation.py +173 -0
  124. kairo_code/heartbeat.py +63 -0
  125. kairo_code/llm.py +291 -0
  126. kairo_code/logging_config.py +156 -0
  127. kairo_code/main.py +818 -0
  128. kairo_code/router.py +217 -0
  129. kairo_code/sandbox.py +248 -0
  130. kairo_code/settings.py +183 -0
  131. kairo_code/tools/__init__.py +51 -0
  132. kairo_code/tools/analysis.py +509 -0
  133. kairo_code/tools/base.py +417 -0
  134. kairo_code/tools/code.py +58 -0
  135. kairo_code/tools/definitions.py +617 -0
  136. kairo_code/tools/files.py +315 -0
  137. kairo_code/tools/review.py +390 -0
  138. kairo_code/tools/search.py +185 -0
  139. kairo_code/ui.py +418 -0
  140. kairo_code-0.1.0.dist-info/METADATA +13 -0
  141. kairo_code-0.1.0.dist-info/RECORD +144 -0
  142. kairo_code-0.1.0.dist-info/WHEEL +5 -0
  143. kairo_code-0.1.0.dist-info/entry_points.txt +2 -0
  144. kairo_code-0.1.0.dist-info/top_level.txt +4 -0
kairo_code/main.py ADDED
@@ -0,0 +1,818 @@
1
+ """Kairo Code CLI - Main entry point"""
2
+
3
+ import sys
4
+ import time
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ from rich.console import Console
10
+ from rich.markdown import Markdown
11
+ from rich.panel import Panel
12
+ from rich.live import Live
13
+ from rich.prompt import Confirm
14
+ from rich.status import Status
15
+ from prompt_toolkit import PromptSession
16
+ from prompt_toolkit.history import FileHistory
17
+
18
+ from .config import Config
19
+ from .llm import LLM
20
+ from .conversation import Conversation
21
+ from .router import Router, Intent
22
+ from .tools import create_default_registry, create_readonly_registry
23
+ from .agents import (
24
+ ExplorerAgent, CoderAgent, PlannerAgent, AgentEvent,
25
+ SecurityAgent, ReviewerAgent, AuditAgent, TestingAgent,
26
+ UiUxAgent, GuardianAgent, DocsAgent, ArchitectAgent,
27
+ DatabaseAgent, TerraformAgent,
28
+ )
29
+ from .sandbox import init_sandbox, get_sandbox
30
+ from .ui import OutputFormatter, print_tool_call, print_tool_result, print_status, print_welcome_banner
31
+ from .logging_config import setup_logging, get_logger
32
+ from .settings import (
33
+ load_user_settings, prompt_auto_approve_settings,
34
+ show_settings_menu, should_auto_approve
35
+ )
36
+ from .heartbeat import Heartbeat
37
+
38
+
39
+ console = Console()
40
+ logger = None # Initialized in main()
41
+
42
+
43
+ def check_python() -> dict:
44
+ """
45
+ Check Python availability and return status info.
46
+ Returns dict with 'available', 'command', 'version', and 'install_hint'.
47
+ """
48
+ result = {
49
+ "available": False,
50
+ "command": None,
51
+ "version": None,
52
+ "install_hint": None,
53
+ }
54
+
55
+ # Try python3 first (preferred on Linux/macOS), then python
56
+ for cmd in ["python3", "python"]:
57
+ if shutil.which(cmd):
58
+ try:
59
+ proc = subprocess.run(
60
+ [cmd, "--version"],
61
+ capture_output=True,
62
+ text=True,
63
+ timeout=5,
64
+ )
65
+ if proc.returncode == 0:
66
+ result["available"] = True
67
+ result["command"] = cmd
68
+ result["version"] = proc.stdout.strip() or proc.stderr.strip()
69
+ return result
70
+ except Exception:
71
+ continue
72
+
73
+ # Python not found - provide install hints based on system
74
+ import platform
75
+ system = platform.system().lower()
76
+
77
+ if "linux" in system:
78
+ # Check for common distros
79
+ if shutil.which("apt-get"):
80
+ result["install_hint"] = "sudo apt-get install python3"
81
+ elif shutil.which("dnf"):
82
+ result["install_hint"] = "sudo dnf install python3"
83
+ elif shutil.which("pacman"):
84
+ result["install_hint"] = "sudo pacman -S python"
85
+ elif shutil.which("apk"):
86
+ result["install_hint"] = "apk add python3"
87
+ else:
88
+ result["install_hint"] = "Install Python 3 using your package manager"
89
+ elif "darwin" in system:
90
+ result["install_hint"] = "brew install python3"
91
+ elif "windows" in system:
92
+ result["install_hint"] = "Download from https://python.org or: winget install Python.Python.3"
93
+ else:
94
+ result["install_hint"] = "Download from https://python.org"
95
+
96
+ return result
97
+
98
+
99
+ def make_confirm_callback(user_settings: dict):
100
+ """Create a confirmation callback that respects user settings."""
101
+ def confirm_action(message: str) -> bool:
102
+ """Ask user to confirm an action (respects auto-approve settings)."""
103
+ # Extract tool name from message like "Execute write_file(...)?"
104
+ tool_name = None
105
+ if "write_file" in message:
106
+ tool_name = "write_file"
107
+ elif "bash" in message:
108
+ tool_name = "bash"
109
+
110
+ # Check if auto-approved
111
+ if tool_name and should_auto_approve(tool_name, user_settings):
112
+ console.print(f"[dim][auto-approved][/]")
113
+ return True
114
+
115
+ return Confirm.ask(f"[yellow]{message}[/]", default=True)
116
+
117
+ return confirm_action
118
+
119
+
120
+ class KairoCode:
121
+ """Main Kairo Code application."""
122
+
123
+ def __init__(self):
124
+ self.config = Config()
125
+ self.llm = LLM()
126
+
127
+ # Check Python availability BEFORE creating tools (BashTool reads KAIRO_PYTHON_CMD)
128
+ self.python_status = check_python()
129
+ if self.python_status["available"]:
130
+ import os
131
+ os.environ["KAIRO_PYTHON_CMD"] = self.python_status["command"]
132
+
133
+ self.tools = create_default_registry()
134
+ self.conversation = Conversation()
135
+ self.router = Router(self.llm)
136
+
137
+ # Load user settings
138
+ self.user_settings = load_user_settings()
139
+
140
+ # Initialize sandbox for safe file operations
141
+ self.sandbox = init_sandbox()
142
+
143
+ # Create confirmation callback that respects user settings
144
+ self.confirm_callback = make_confirm_callback(self.user_settings)
145
+
146
+ # Create read-only registry for non-execution agents
147
+ self.readonly_tools = create_readonly_registry()
148
+
149
+ # Initialize agents with appropriate tool registries
150
+ self.agents = {
151
+ # Core agents
152
+ "explore": ExplorerAgent(self.llm, self.readonly_tools),
153
+ "code": CoderAgent(self.llm, self.tools, on_confirm=self.confirm_callback),
154
+ "plan": PlannerAgent(self.llm, self.readonly_tools),
155
+ # Specialist agents (read-only analysis)
156
+ "security": SecurityAgent(self.llm, self.readonly_tools),
157
+ "review": ReviewerAgent(self.llm, self.readonly_tools),
158
+ "audit": AuditAgent(self.llm, self.readonly_tools),
159
+ "uiux": UiUxAgent(self.llm, self.readonly_tools),
160
+ "guardian": GuardianAgent(self.llm, self.readonly_tools),
161
+ # Specialist agents (can write files)
162
+ "test": TestingAgent(self.llm, self.tools, on_confirm=self.confirm_callback),
163
+ "docs": DocsAgent(self.llm, self.tools, on_confirm=self.confirm_callback),
164
+ "architect": ArchitectAgent(self.llm, self.tools, on_confirm=self.confirm_callback),
165
+ "database": DatabaseAgent(self.llm, self.tools, on_confirm=self.confirm_callback),
166
+ "terraform": TerraformAgent(self.llm, self.tools, on_confirm=self.confirm_callback),
167
+ }
168
+
169
+ # Output formatter for clean display
170
+ self.formatter = OutputFormatter()
171
+
172
+ def print_welcome(self) -> None:
173
+ """Print welcome banner in Claude Code style."""
174
+ sandbox_path = str(self.sandbox.root)
175
+
176
+ # Get resolved model names (handles "auto" selection)
177
+ try:
178
+ coder_model = self.llm.select_model("coder")
179
+ router_model = self.llm.select_model("router")
180
+ except RuntimeError as e:
181
+ console.print(f"[red]Error: {e}[/]")
182
+ coder_model = "none"
183
+ router_model = "none"
184
+
185
+ print_welcome_banner(coder_model, router_model, sandbox_path)
186
+
187
+ # Check backend connectivity
188
+ if not self.llm.check_health():
189
+ console.print("[yellow]Warning: Kairo backend unreachable. Check your connection or API key.[/]")
190
+ console.print(f"[dim]Endpoint: {self.config.cloud_endpoint}[/]\n")
191
+
192
+ def handle_command(self, user_input: str) -> bool:
193
+ """
194
+ Handle special commands.
195
+ Returns True if command was handled.
196
+ """
197
+ parts = user_input.strip().split(maxsplit=1)
198
+ cmd = parts[0].lower()
199
+ arg = parts[1] if len(parts) > 1 else ""
200
+
201
+ if cmd in ("/exit", "/quit", "/q"):
202
+ console.print("[dim]Goodbye![/]")
203
+ sys.exit(0)
204
+
205
+ if cmd == "/clear":
206
+ self.conversation.clear()
207
+ console.print("[dim]Conversation cleared.[/]")
208
+ return True
209
+
210
+ if cmd == "/help":
211
+ self._print_help()
212
+ return True
213
+
214
+ if cmd == "/login":
215
+ from kairo_code.auth import authenticate, clear_credentials
216
+ clear_credentials()
217
+ key = authenticate(self.config.cloud_endpoint)
218
+ if key:
219
+ Config._instance = None
220
+ self.config = Config()
221
+ console.print("[green]Re-authenticated successfully.[/]")
222
+ return True
223
+
224
+ if cmd == "/logout":
225
+ from kairo_code.auth import clear_credentials
226
+ clear_credentials()
227
+ console.print("[dim]Logged out. Restart to re-authenticate.[/]")
228
+ return True
229
+
230
+ if cmd == "/whoami":
231
+ from kairo_code.auth import load_credentials
232
+ creds = load_credentials()
233
+ if creds:
234
+ console.print(f"[cyan]{creds.get('email', 'Unknown')}[/] ({creds.get('plan', 'unknown')} plan)")
235
+ else:
236
+ console.print("[dim]Not authenticated.[/]")
237
+ return True
238
+
239
+ if cmd == "/explore":
240
+ task = arg or "Explore this codebase and provide a summary"
241
+ self._run_agent("explore", task)
242
+ return True
243
+
244
+ if cmd == "/code":
245
+ if not arg:
246
+ console.print("[red]Usage: /code <task>[/]")
247
+ return True
248
+ self._run_agent("code", arg)
249
+ return True
250
+
251
+ if cmd == "/plan":
252
+ if not arg:
253
+ console.print("[red]Usage: /plan <task>[/]")
254
+ return True
255
+ self._run_agent("plan", arg)
256
+ return True
257
+
258
+ # Specialist agent commands
259
+ specialist_cmds = {
260
+ "/security": ("security", "Run a security audit"),
261
+ "/review": ("review", "Review code for quality issues"),
262
+ "/audit": ("audit", "Comprehensive codebase health assessment"),
263
+ "/test": ("test", "Generate or review tests"),
264
+ "/uiux": ("uiux", "Evaluate UI/UX and accessibility"),
265
+ "/guardian": ("guardian", "Verify architectural alignment"),
266
+ "/docs": ("docs", "Create or review documentation"),
267
+ "/architect": ("architect", "Design modular architecture"),
268
+ "/database": ("database", "Database design and optimization"),
269
+ "/terraform": ("terraform", "Generate Terraform IaC"),
270
+ }
271
+
272
+ if cmd in specialist_cmds:
273
+ agent_key, desc = specialist_cmds[cmd]
274
+ task = arg or f"{desc} for this project"
275
+ self._run_agent(agent_key, task)
276
+ return True
277
+
278
+ if cmd == "/search":
279
+ if not arg:
280
+ console.print("[red]Usage: /search <query>[/]")
281
+ return True
282
+ self._do_search(arg)
283
+ return True
284
+
285
+ if cmd == "/read":
286
+ if not arg:
287
+ console.print("[red]Usage: /read <file>[/]")
288
+ return True
289
+ self._read_file(arg)
290
+ return True
291
+
292
+ if cmd == "/tree":
293
+ self._show_tree(arg or ".")
294
+ return True
295
+
296
+ if cmd == "/models":
297
+ self._show_models()
298
+ return True
299
+
300
+ if cmd == "/logs":
301
+ self._show_logs(arg)
302
+ return True
303
+
304
+ if cmd == "/settings":
305
+ self._show_settings()
306
+ return True
307
+
308
+ if cmd == "/history":
309
+ self._show_history()
310
+ return True
311
+
312
+ if cmd == "/resume":
313
+ if not arg:
314
+ console.print("[red]Usage: /resume <conversation-id>[/]")
315
+ return True
316
+ self._resume_conversation(arg)
317
+ return True
318
+
319
+ return False
320
+
321
+ def _print_help(self) -> None:
322
+ """Print help information."""
323
+ help_text = """
324
+ ## How to Use
325
+
326
+ **Just describe what you want!** Kairo Code will automatically:
327
+ - Plan first if it's a complex task
328
+ - Then implement the solution
329
+
330
+ ## Core Agents
331
+
332
+ | Agent | Description |
333
+ |-------|-------------|
334
+ | `/explore [task]` | Explore and analyze codebase |
335
+ | `/code <task>` | Write or modify code (skips auto-planning) |
336
+ | `/plan <task>` | Create a plan only (no implementation) |
337
+
338
+ ## Specialist Agents
339
+
340
+ | Agent | Description |
341
+ |-------|-------------|
342
+ | `/security [task]` | Security audit — vulnerabilities, OWASP, secrets |
343
+ | `/review [task]` | Code review — bugs, quality, architecture |
344
+ | `/audit [task]` | Codebase health assessment (KEEP/REFACTOR/REWRITE) |
345
+ | `/test [task]` | Generate or review tests |
346
+ | `/uiux [task]` | UI/UX and accessibility evaluation |
347
+ | `/guardian [task]` | Verify architectural alignment |
348
+ | `/docs [task]` | Create or review documentation |
349
+ | `/architect [task]` | Design modular architecture |
350
+ | `/database [task]` | Schema design, query optimization |
351
+ | `/terraform [task]` | Generate Terraform infrastructure code |
352
+
353
+ ## Utilities
354
+
355
+ | Command | Description |
356
+ |---------|-------------|
357
+ | `/search <query>` | Search the web |
358
+ | `/read <file>` | Read a file |
359
+ | `/tree [path]` | Show directory tree |
360
+ | `/models` | Show available models |
361
+ | `/history` | List saved conversations |
362
+ | `/resume <id>` | Resume a past conversation |
363
+ | `/settings` | Configure auto-approve |
364
+ | `/login` | Re-authenticate with Kairo |
365
+ | `/logout` | Clear saved credentials |
366
+ | `/whoami` | Show current account |
367
+ | `/clear` | Clear conversation |
368
+ | `/exit` | Exit Kairo Code |
369
+
370
+ ## Examples
371
+
372
+ ```
373
+ I want to create an app that tracks my tasks with a UI
374
+ /security audit the authentication module
375
+ /review check the code I wrote today
376
+ /test generate tests for the API endpoints
377
+ /architect refactor utils.py into modules
378
+ /terraform create an S3 bucket with versioning
379
+ ```
380
+
381
+ ## Tips
382
+
383
+ - Complex tasks (apps, APIs, UIs) → auto-plans first, then implements
384
+ - Simple tasks (fix bug, add function) → implements directly
385
+ - Use specialist agents for focused analysis (security, review, testing, etc.)
386
+ """
387
+ console.print(Markdown(help_text))
388
+
389
+ def _run_agent(self, agent_name: str, task: str) -> None:
390
+ """Run a specialized agent with clean event-based output."""
391
+ agent = self.agents.get(agent_name)
392
+ if not agent:
393
+ console.print(f"[red]Unknown agent: {agent_name}[/]")
394
+ return
395
+
396
+ # Claude Code style header
397
+ max_iter = self.config.max_iterations or 20
398
+ console.print(f"\n[bold cyan]▶ {agent_name.title()}[/]\n")
399
+
400
+ # Tool display names
401
+ tool_names = {
402
+ "read_file": "Read", "write_file": "Write", "edit_file": "Edit",
403
+ "list_files": "Glob", "search_files": "Grep", "tree": "Tree",
404
+ "bash": "Bash", "web_search": "WebSearch", "web_fetch": "WebFetch",
405
+ "git_status": "Git_Status", "git_diff": "Git_Diff",
406
+ }
407
+
408
+ full_response = ""
409
+ start_time = time.time()
410
+ tool_count = 0
411
+ spinner = None # Active Status spinner, if any
412
+
413
+ try:
414
+ for event in agent.run_events(task):
415
+ if event.type == "thinking":
416
+ # Show a spinner while waiting for LLM
417
+ if spinner:
418
+ spinner.stop()
419
+ spinner = Status(f"[dim]{event.content}[/]", console=console, spinner="dots")
420
+ spinner.start()
421
+
422
+ elif event.type == "tool_start":
423
+ if spinner:
424
+ spinner.stop()
425
+ spinner = None
426
+ tool_count += 1
427
+ # Show tool call with parameter preview and progress
428
+ display_name = tool_names.get(event.tool_name, event.tool_name)
429
+ param_preview = self._get_param_preview(event.tool_name, event.tool_params or {})
430
+ console.print(f"\n[dim][{tool_count}][/] [cyan]● {display_name}[/][dim]({param_preview})[/]")
431
+
432
+ elif event.type == "tool_result":
433
+ # Show result summary
434
+ if event.success:
435
+ # Truncate long results
436
+ content = event.content
437
+ if len(content) > 200:
438
+ lines = content.split('\n')
439
+ if len(lines) > 3:
440
+ content = '\n'.join(lines[:3]) + f"\n ... +{len(lines)-3} more lines"
441
+ else:
442
+ content = content[:200] + "..."
443
+ console.print(f" [dim]⎿ {content}[/]")
444
+ else:
445
+ console.print(f" [red]⎿ Error: {event.content[:150]}[/]")
446
+
447
+ elif event.type == "text":
448
+ if spinner:
449
+ spinner.stop()
450
+ spinner = None
451
+ # Final response - print as a clean block
452
+ full_response = event.content
453
+ if event.content.strip():
454
+ console.print() # Blank line before text
455
+ console.print(Markdown(event.content))
456
+
457
+ elif event.type == "error":
458
+ if spinner:
459
+ spinner.stop()
460
+ spinner = None
461
+ console.print(f"\n[red]⚠ {event.content}[/]")
462
+
463
+ elif event.type == "done":
464
+ if spinner:
465
+ spinner.stop()
466
+ spinner = None
467
+ elapsed = time.time() - start_time
468
+ if elapsed < 60:
469
+ console.print(f"\n[dim]✻ Completed in {elapsed:.1f}s[/]")
470
+ else:
471
+ mins = int(elapsed // 60)
472
+ secs = int(elapsed % 60)
473
+ console.print(f"\n[dim]✻ Completed in {mins}m {secs}s[/]")
474
+
475
+ except KeyboardInterrupt:
476
+ if spinner:
477
+ spinner.stop()
478
+ console.print("\n[yellow]⚠ Interrupted[/]")
479
+
480
+ # Add to conversation history
481
+ self.conversation.add_user(f"[{agent_name}] {task}")
482
+ self.conversation.add_assistant(full_response)
483
+
484
+ def _get_param_preview(self, tool_name: str, params: dict) -> str:
485
+ """Get a concise parameter preview for tool display."""
486
+ return self.formatter._get_param_preview(tool_name, params)
487
+
488
+ def _do_search(self, query: str) -> None:
489
+ """Perform a web search and display results."""
490
+ from .tools import web_search, format_search_results
491
+
492
+ console.print(f"[cyan]Searching: {query}[/]\n")
493
+
494
+ try:
495
+ results = web_search(query)
496
+ formatted = format_search_results(results)
497
+ console.print(Markdown(formatted))
498
+ except Exception as e:
499
+ console.print(f"[red]Search error: {e}[/]")
500
+
501
+ def _read_file(self, path: str) -> None:
502
+ """Read and display a file."""
503
+ from .tools import read_file
504
+
505
+ try:
506
+ content = read_file(path)
507
+ console.print(Markdown(f"```\n{content}\n```"))
508
+ except Exception as e:
509
+ console.print(f"[red]Error: {e}[/]")
510
+
511
+ def _show_tree(self, path: str) -> None:
512
+ """Show directory tree."""
513
+ from .tools import tree
514
+
515
+ try:
516
+ output = tree(path)
517
+ console.print(output)
518
+ except Exception as e:
519
+ console.print(f"[red]Error: {e}[/]")
520
+
521
+ def _show_models(self) -> None:
522
+ """Show available Ollama models."""
523
+ try:
524
+ models = self.llm.list_models()
525
+ coder = self.llm.select_model("coder")
526
+ router = self.llm.select_model("router")
527
+ console.print("[bold]Available Models:[/]")
528
+ for model in models:
529
+ marker = ""
530
+ if model == coder or coder in model:
531
+ marker = " [cyan](coder)[/]"
532
+ elif model == router or router in model:
533
+ marker = " [yellow](router)[/]"
534
+ console.print(f" - {model}{marker}")
535
+ except Exception as e:
536
+ console.print(f"[red]Error listing models: {e}[/]")
537
+
538
+ def _show_logs(self, lines_arg: str = "") -> None:
539
+ """Show recent log entries."""
540
+ from .logging_config import LOG_DIR
541
+ import subprocess
542
+
543
+ try:
544
+ # Find most recent log file
545
+ log_files = sorted(LOG_DIR.glob("kairo_*.log"), reverse=True)
546
+ if not log_files:
547
+ console.print("[yellow]No log files found.[/]")
548
+ return
549
+
550
+ current_log = log_files[0]
551
+ lines = int(lines_arg) if lines_arg.isdigit() else 50
552
+
553
+ console.print(f"[bold]Recent logs ({current_log.name}):[/]")
554
+ result = subprocess.run(
555
+ ["tail", "-n", str(lines), str(current_log)],
556
+ capture_output=True,
557
+ text=True
558
+ )
559
+ console.print(result.stdout)
560
+ console.print(f"\n[dim]Full log: {current_log}[/]")
561
+ except Exception as e:
562
+ console.print(f"[red]Error reading logs: {e}[/]")
563
+
564
+ def _show_settings(self) -> None:
565
+ """Show and modify settings."""
566
+ self.user_settings = show_settings_menu()
567
+ # Update the confirm callback with new settings
568
+ self.confirm_callback = make_confirm_callback(self.user_settings)
569
+ # Update all agents that have confirmation callbacks
570
+ for agent in self.agents.values():
571
+ if hasattr(agent, 'on_confirm'):
572
+ agent.on_confirm = self.confirm_callback
573
+
574
+ def _show_history(self) -> None:
575
+ """Show saved conversations."""
576
+ conversations = Conversation.list_saved()
577
+ if not conversations:
578
+ console.print("[dim]No saved conversations.[/]")
579
+ return
580
+
581
+ console.print("[bold]Saved Conversations:[/]\n")
582
+ for conv in conversations[:20]: # Show last 20
583
+ preview = conv["preview"] or "(empty)"
584
+ console.print(
585
+ f" [cyan]{conv['id']}[/] "
586
+ f"[dim]{conv['created_at'][:10]}[/] "
587
+ f"{conv['message_count']} msgs "
588
+ f"[dim]{preview}[/]"
589
+ )
590
+ console.print(f"\n[dim]Use /resume <id> to continue a conversation.[/]")
591
+
592
+ def _resume_conversation(self, conversation_id: str) -> None:
593
+ """Resume a saved conversation."""
594
+ try:
595
+ self.conversation = Conversation.load(conversation_id)
596
+ count = len(self.conversation.messages)
597
+ console.print(f"[green]Resumed conversation {conversation_id} ({count} messages)[/]")
598
+ except FileNotFoundError:
599
+ console.print(f"[red]Conversation {conversation_id} not found.[/]")
600
+ except Exception as e:
601
+ console.print(f"[red]Error loading conversation: {e}[/]")
602
+
603
+ def _prompt_first_run_settings(self) -> None:
604
+ """Prompt for settings on first run."""
605
+ if not self.user_settings["auto_approve"].get("prompted"):
606
+ self.user_settings = prompt_auto_approve_settings()
607
+ # Update the confirm callback with new settings
608
+ self.confirm_callback = make_confirm_callback(self.user_settings)
609
+ # Update all agents that have confirmation callbacks
610
+ for agent in self.agents.values():
611
+ if hasattr(agent, 'on_confirm'):
612
+ agent.on_confirm = self.confirm_callback
613
+
614
+ def _is_complex_task(self, task: str) -> bool:
615
+ """Check if a task is complex enough to need planning first."""
616
+ task_lower = task.lower()
617
+ # Complex indicators
618
+ complex_keywords = [
619
+ "app", "application", "create", "build", "implement",
620
+ "multiple", "features", "ui", "interface", "api",
621
+ "database", "authentication", "with a", "that can",
622
+ "schedule", "daily", "weekly", "notifications"
623
+ ]
624
+ # Count how many complex indicators are present
625
+ matches = sum(1 for kw in complex_keywords if kw in task_lower)
626
+ return matches >= 2 # 2 or more indicators = complex
627
+
628
+ def _run_autonomous(self, task: str) -> None:
629
+ """Run autonomous workflow: plan if complex, then implement."""
630
+ if self._is_complex_task(task):
631
+ console.print("[dim]Complex task detected - planning first...[/]\n")
632
+
633
+ # Step 1: Plan
634
+ self._run_agent("plan", task)
635
+
636
+ # Step 2: Implement (pass the original task, coder will see the plan context)
637
+ console.print("\n[dim]Implementing plan...[/]\n")
638
+ self._run_agent("code", f"Implement the following task based on the plan above: {task}")
639
+ else:
640
+ # Simple task - just code directly
641
+ self._run_agent("code", task)
642
+
643
+ def chat(self, user_input: str) -> None:
644
+ """Handle a regular chat message with smart routing."""
645
+ # Route the intent
646
+ routed = self.router.route(user_input)
647
+ console.print(f"[dim][{routed.intent.value}][/]")
648
+
649
+ # Handle based on intent
650
+ if routed.intent == Intent.SEARCH:
651
+ self._do_search(user_input)
652
+ return
653
+
654
+ if routed.intent == Intent.EXPLORE:
655
+ self._run_agent("explore", user_input)
656
+ return
657
+
658
+ if routed.intent == Intent.PLAN:
659
+ # Explicit /plan - just plan, don't implement
660
+ self._run_agent("plan", user_input)
661
+ return
662
+
663
+ if routed.intent == Intent.CODE:
664
+ # Autonomous: plan if complex, then code
665
+ self._run_autonomous(user_input)
666
+ return
667
+
668
+ if routed.intent == Intent.FILE_READ:
669
+ path = routed.extracted_params.get("path", "")
670
+ if path:
671
+ self._read_file(path)
672
+ else:
673
+ # Ask LLM to help find the file
674
+ self._run_agent("code", user_input)
675
+ return
676
+
677
+ if routed.intent == Intent.FILE_LIST:
678
+ pattern = routed.extracted_params.get("pattern", "**/*")
679
+ self._show_tree(".")
680
+ return
681
+
682
+ if routed.intent == Intent.FILE_WRITE:
683
+ self._run_agent("code", user_input)
684
+ return
685
+
686
+ # Default: regular chat
687
+ self.conversation.add_user(user_input)
688
+ messages = self.conversation.to_messages()
689
+ self._stream_response(messages)
690
+
691
+ def _stream_response(self, messages: list[dict], model: str | None = None) -> None:
692
+ """Stream and display LLM response with a thinking spinner."""
693
+ full_response = ""
694
+ first_chunk = True
695
+ spinner = Status("[dim]Thinking...[/]", console=console, spinner="dots")
696
+
697
+ try:
698
+ spinner.start()
699
+
700
+ with Live(console=console, refresh_per_second=10, auto_refresh=False) as live:
701
+ buffer = ""
702
+ for chunk in self.llm.chat(messages, model=model, stream=True):
703
+ if first_chunk:
704
+ spinner.stop()
705
+ first_chunk = False
706
+ buffer += chunk
707
+ full_response += chunk
708
+ live.update(Markdown(buffer))
709
+ live.refresh()
710
+
711
+ if first_chunk:
712
+ spinner.stop()
713
+
714
+ self.conversation.add_assistant(full_response)
715
+ console.print()
716
+
717
+ except Exception as e:
718
+ if first_chunk:
719
+ spinner.stop()
720
+ console.print(f"[red]Error: {e}[/]")
721
+
722
+ def run(self) -> None:
723
+ """Main run loop."""
724
+ self.print_welcome()
725
+
726
+ # Prompt for auto-approve settings on first run
727
+ self._prompt_first_run_settings()
728
+
729
+ # Authenticate with Kairo cloud
730
+ from kairo_code.auth import ensure_authenticated
731
+ api_key = self.config.cloud_api_key
732
+ if not api_key or api_key == "not-needed":
733
+ api_key = ensure_authenticated(self.config.cloud_endpoint)
734
+ if not api_key:
735
+ console.print("[red]Authentication required to use Kairo Code.[/]")
736
+ console.print("[dim]Visit https://app.kaironlabs.io/pricing to upgrade to Max plan.[/]")
737
+ sys.exit(1)
738
+ # Reset config singleton so the new key is picked up
739
+ Config._instance = None
740
+ self.config = Config()
741
+
742
+ # Check if models were resolved (auto-selection already handles this, but warn if no models)
743
+ try:
744
+ coder_model = self.llm.select_model("coder")
745
+ if not self.llm.check_model(coder_model):
746
+ console.print(f"[yellow]Warning: Model {coder_model} not found.[/]")
747
+ console.print(f"[yellow]Run: ollama pull {coder_model}[/]\n")
748
+ except RuntimeError as e:
749
+ console.print(f"[red]{e}[/]")
750
+ console.print("[yellow]Run: ollama pull llama3 to download a model[/]\n")
751
+
752
+ # Check if Python is available
753
+ if not self.python_status["available"]:
754
+ console.print("[yellow]Warning: Python not found on this system.[/]")
755
+ console.print(f"[yellow]Install: {self.python_status['install_hint']}[/]")
756
+ console.print("[dim]Python is required to run generated scripts.[/]\n")
757
+
758
+ # Start heartbeat if API key is configured
759
+ self._heartbeat = None
760
+ api_key = self.config.cloud_api_key
761
+ if api_key and api_key != "not-needed":
762
+ agent_id = self.config.get("cloud.agent_id", "")
763
+ if agent_id:
764
+ self._heartbeat = Heartbeat(api_key, agent_id)
765
+ self._heartbeat.start()
766
+
767
+ # Setup prompt with history
768
+ history_file = Path.home() / ".kairo_code_history"
769
+ session = PromptSession(history=FileHistory(str(history_file)))
770
+
771
+ while True:
772
+ try:
773
+ user_input = session.prompt("\n> ").strip()
774
+
775
+ if not user_input:
776
+ continue
777
+
778
+ # Check for commands
779
+ if user_input.startswith("/"):
780
+ if self.handle_command(user_input):
781
+ continue
782
+
783
+ # Regular chat with smart routing
784
+ self.chat(user_input)
785
+
786
+ except KeyboardInterrupt:
787
+ console.print("\n[dim]Use /exit to quit[/]")
788
+ except EOFError:
789
+ break
790
+ except Exception as e:
791
+ console.print(f"[red]Error: {e}[/]")
792
+
793
+
794
+ def main():
795
+ """Entry point."""
796
+ global logger
797
+
798
+ # Initialize logging first
799
+ logger = setup_logging()
800
+ logger.info("Kairo Code starting up...")
801
+
802
+ try:
803
+ logger.info("Initializing Kairo Code application...")
804
+ app = KairoCode()
805
+ logger.info("Starting main run loop...")
806
+ app.run()
807
+ except KeyboardInterrupt:
808
+ logger.info("User interrupted with Ctrl+C")
809
+ console.print("\n[dim]Goodbye![/]")
810
+ except Exception as e:
811
+ logger.exception(f"Fatal error: {e}")
812
+ console.print(f"[red]Fatal error: {e}[/]")
813
+ console.print(f"[dim]Check logs at: {logger.log_file}[/]")
814
+ sys.exit(1)
815
+
816
+
817
+ if __name__ == "__main__":
818
+ main()