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
@@ -0,0 +1,185 @@
1
+ """Web search and fetch tools"""
2
+
3
+ import ipaddress
4
+ import socket
5
+ import requests
6
+ from html.parser import HTMLParser
7
+ from urllib.parse import urlparse
8
+
9
+ try:
10
+ from ddgs import DDGS # New package name
11
+ except ImportError:
12
+ from duckduckgo_search import DDGS # Fallback to old name
13
+
14
+
15
+ # ── SSRF protection ────────────────────────────────────────────
16
+
17
+ _BLOCKED_SCHEMES = {"file", "ftp", "gopher", "data", "javascript"}
18
+
19
+ def _is_private_ip(ip_str: str) -> bool:
20
+ """Check if an IP address is private/reserved (SSRF protection)."""
21
+ try:
22
+ addr = ipaddress.ip_address(ip_str)
23
+ return (
24
+ addr.is_private
25
+ or addr.is_loopback
26
+ or addr.is_reserved
27
+ or addr.is_link_local
28
+ or addr.is_multicast
29
+ )
30
+ except ValueError:
31
+ return False
32
+
33
+
34
+ def _validate_url(url: str) -> None:
35
+ """Validate a URL is safe to fetch (blocks private IPs, bad schemes)."""
36
+ parsed = urlparse(url)
37
+
38
+ # Block dangerous schemes
39
+ if parsed.scheme.lower() in _BLOCKED_SCHEMES:
40
+ raise ValueError(f"Blocked URL scheme: {parsed.scheme}")
41
+
42
+ # Only allow http/https
43
+ if parsed.scheme.lower() not in ("http", "https"):
44
+ raise ValueError(f"Unsupported URL scheme: {parsed.scheme}")
45
+
46
+ hostname = parsed.hostname
47
+ if not hostname:
48
+ raise ValueError("URL has no hostname")
49
+
50
+ # Resolve hostname and check all IPs
51
+ try:
52
+ addrs = socket.getaddrinfo(hostname, parsed.port or 443)
53
+ for family, _type, _proto, _canonname, sockaddr in addrs:
54
+ ip = sockaddr[0]
55
+ if _is_private_ip(ip):
56
+ raise ValueError(
57
+ f"Blocked: {hostname} resolves to private/reserved IP {ip}"
58
+ )
59
+ except socket.gaierror:
60
+ raise ValueError(f"Cannot resolve hostname: {hostname}")
61
+
62
+
63
+ class HTMLTextExtractor(HTMLParser):
64
+ """Extract text content from HTML, removing scripts and styles."""
65
+
66
+ def __init__(self):
67
+ super().__init__()
68
+ self.text_parts = []
69
+ self.skip_data = False
70
+ self.skip_tags = {'script', 'style', 'nav', 'footer', 'header'}
71
+
72
+ def handle_starttag(self, tag, attrs):
73
+ if tag in self.skip_tags:
74
+ self.skip_data = True
75
+
76
+ def handle_endtag(self, tag):
77
+ if tag in self.skip_tags:
78
+ self.skip_data = False
79
+
80
+ def handle_data(self, data):
81
+ if not self.skip_data:
82
+ text = data.strip()
83
+ if text:
84
+ self.text_parts.append(text)
85
+
86
+ def get_text(self) -> str:
87
+ return ' '.join(self.text_parts)
88
+
89
+
90
+ def web_search(query: str, max_results: int = 5) -> list[dict]:
91
+ """
92
+ Search the web using DuckDuckGo.
93
+
94
+ Args:
95
+ query: Search query
96
+ max_results: Maximum number of results to return
97
+
98
+ Returns:
99
+ List of search results with title, url, and body
100
+ """
101
+ with DDGS() as ddgs:
102
+ results = list(ddgs.text(query, max_results=max_results))
103
+
104
+ return [
105
+ {
106
+ "title": r.get("title", ""),
107
+ "url": r.get("href", ""),
108
+ "snippet": r.get("body", ""),
109
+ }
110
+ for r in results
111
+ ]
112
+
113
+
114
+ def format_search_results(results: list[dict]) -> str:
115
+ """Format search results for LLM context."""
116
+ if not results:
117
+ return "No search results found."
118
+
119
+ formatted = []
120
+ for i, r in enumerate(results, 1):
121
+ formatted.append(
122
+ f"{i}. **{r['title']}**\n"
123
+ f" URL: {r['url']}\n"
124
+ f" {r['snippet']}\n"
125
+ )
126
+
127
+ return "\n".join(formatted)
128
+
129
+
130
+ def web_fetch(url: str, max_chars: int = 8000) -> str:
131
+ """
132
+ Fetch a URL and extract text content.
133
+
134
+ Args:
135
+ url: The URL to fetch
136
+ max_chars: Maximum characters to return (default 8000)
137
+
138
+ Returns:
139
+ Extracted text content from the page
140
+ """
141
+ # SSRF protection: validate URL before fetching
142
+ _validate_url(url)
143
+
144
+ try:
145
+ headers = {
146
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
147
+ }
148
+ # Follow redirects manually so we can validate each hop
149
+ max_redirects = 5
150
+ current_url = url
151
+ response = None
152
+ for _ in range(max_redirects):
153
+ response = requests.get(current_url, headers=headers, timeout=30, allow_redirects=False)
154
+ if response.is_redirect and "Location" in response.headers:
155
+ current_url = response.headers["Location"]
156
+ _validate_url(current_url)
157
+ continue
158
+ break
159
+ response.raise_for_status()
160
+
161
+ content_type = response.headers.get('content-type', '')
162
+
163
+ # If it's JSON, return formatted JSON
164
+ if 'application/json' in content_type:
165
+ import json
166
+ try:
167
+ data = response.json()
168
+ return json.dumps(data, indent=2)[:max_chars]
169
+ except Exception:
170
+ return response.text[:max_chars]
171
+
172
+ # If it's HTML, extract text
173
+ if 'text/html' in content_type:
174
+ extractor = HTMLTextExtractor()
175
+ extractor.feed(response.text)
176
+ text = extractor.get_text()
177
+ return text[:max_chars]
178
+
179
+ # Otherwise return raw text
180
+ return response.text[:max_chars]
181
+
182
+ except requests.exceptions.Timeout:
183
+ raise TimeoutError("Request timed out fetching URL")
184
+ except requests.exceptions.RequestException as e:
185
+ raise RuntimeError(f"Error fetching URL: {e}") from e
kairo_code/ui.py ADDED
@@ -0,0 +1,418 @@
1
+ """Claude Code-inspired UI formatting for Kairo Code"""
2
+
3
+ import re
4
+ import time
5
+ from dataclasses import dataclass, field
6
+ from typing import Generator, Optional
7
+ from rich.console import Console, Group
8
+ from rich.panel import Panel
9
+ from rich.syntax import Syntax
10
+ from rich.text import Text
11
+ from rich.live import Live
12
+ from rich.spinner import Spinner
13
+ from rich.table import Table
14
+ from rich.box import ROUNDED, SIMPLE
15
+ from rich.rule import Rule
16
+ from rich.style import Style
17
+ from rich.padding import Padding
18
+
19
+
20
+ console = Console()
21
+
22
+
23
+ @dataclass
24
+ class ToolExecution:
25
+ """Tracks a tool execution for display."""
26
+ tool_name: str
27
+ params: dict
28
+ start_time: float = field(default_factory=time.time)
29
+ result: Optional[str] = None
30
+ success: bool = True
31
+ end_time: Optional[float] = None
32
+
33
+ @property
34
+ def duration(self) -> float:
35
+ end = self.end_time or time.time()
36
+ return end - self.start_time
37
+
38
+ @property
39
+ def duration_str(self) -> str:
40
+ d = self.duration
41
+ if d < 1:
42
+ return f"{d*1000:.0f}ms"
43
+ elif d < 60:
44
+ return f"{d:.1f}s"
45
+ else:
46
+ mins = int(d // 60)
47
+ secs = int(d % 60)
48
+ return f"{mins}m {secs}s"
49
+
50
+
51
+ class OutputFormatter:
52
+ """
53
+ Formats agent output in Claude Code style.
54
+
55
+ Features:
56
+ - Boxed tool calls with bullet points
57
+ - Collapsible long output
58
+ - Diff view for file edits
59
+ - Timing information
60
+ - Spinners during execution
61
+ """
62
+
63
+ TOOL_PATTERN = re.compile(r'<tool>(\w+)</tool>')
64
+ PARAMS_PATTERN = re.compile(r'<params>(.*?)</params>', re.DOTALL)
65
+ THINKING_PATTERN = re.compile(r'<thinking>(.*?)</thinking>', re.DOTALL)
66
+
67
+ # Tool display names (Claude Code style)
68
+ TOOL_NAMES = {
69
+ "read_file": "Read",
70
+ "write_file": "Write",
71
+ "edit_file": "Edit",
72
+ "list_files": "Glob",
73
+ "search_files": "Grep",
74
+ "tree": "Tree",
75
+ "bash": "Bash",
76
+ "web_search": "WebSearch",
77
+ "web_fetch": "WebFetch",
78
+ "lint": "Lint",
79
+ "typecheck": "TypeCheck",
80
+ "run_tests": "Test",
81
+ "security_scan": "Security",
82
+ "code_review": "Review",
83
+ }
84
+
85
+ def __init__(self):
86
+ self.in_tool_call = False
87
+ self.current_tool: Optional[ToolExecution] = None
88
+ self.buffer = ""
89
+ self.session_start = time.time()
90
+ self.collapsed_lines = 6 # Show this many lines before collapsing
91
+
92
+ def format_tool_header(self, tool_name: str, params: dict) -> Text:
93
+ """Format tool call header like Claude Code: ● Bash(command)"""
94
+ display_name = self.TOOL_NAMES.get(tool_name, tool_name.title())
95
+
96
+ # Build parameter preview
97
+ param_preview = self._get_param_preview(tool_name, params)
98
+
99
+ text = Text()
100
+ text.append("● ", style="cyan bold")
101
+ text.append(display_name, style="cyan bold")
102
+ text.append(f"({param_preview})", style="dim")
103
+
104
+ return text
105
+
106
+ def _get_param_preview(self, tool_name: str, params: dict) -> str:
107
+ """Get a concise parameter preview for tool header."""
108
+ if tool_name in ("read_file", "write_file", "edit_file"):
109
+ path = params.get("path", params.get("file", ""))
110
+ return self._truncate(path, 50)
111
+ elif tool_name == "bash":
112
+ cmd = params.get("command", "")
113
+ return self._truncate(cmd, 60)
114
+ elif tool_name == "list_files":
115
+ return params.get("pattern", "*")
116
+ elif tool_name == "search_files":
117
+ query = params.get("query", params.get("pattern", ""))
118
+ return f'"{self._truncate(query, 40)}"'
119
+ elif tool_name == "web_search":
120
+ return f'"{self._truncate(params.get("query", ""), 40)}"'
121
+ elif tool_name == "web_fetch":
122
+ return self._truncate(params.get("url", ""), 50)
123
+ else:
124
+ # Generic: show first param value
125
+ for v in params.values():
126
+ if isinstance(v, str):
127
+ return self._truncate(v, 40)
128
+ return "..."
129
+
130
+ def _truncate(self, text: str, max_len: int) -> str:
131
+ """Truncate text with ellipsis."""
132
+ text = text.replace("\n", " ").strip()
133
+ if len(text) > max_len:
134
+ return text[:max_len-1] + "…"
135
+ return text
136
+
137
+ def format_tool_output(self, output: str, success: bool = True) -> Text:
138
+ """Format tool output with collapsing for long content."""
139
+ if not output:
140
+ return Text(" ⎿ (No content)", style="dim")
141
+
142
+ lines = output.split("\n")
143
+ text = Text()
144
+
145
+ if not success:
146
+ text.append(" ⎿ ", style="dim")
147
+ text.append("Error: ", style="red bold")
148
+ text.append(self._truncate(output, 100), style="red")
149
+ return text
150
+
151
+ # Show output with collapse indicator
152
+ if len(lines) <= self.collapsed_lines:
153
+ # Short output - show all
154
+ for i, line in enumerate(lines):
155
+ text.append(" ⎿ " if i == 0 else " ", style="dim")
156
+ text.append(self._truncate(line, 100) + "\n", style="dim")
157
+ else:
158
+ # Long output - show preview with collapse hint
159
+ for i, line in enumerate(lines[:3]):
160
+ text.append(" ⎿ " if i == 0 else " ", style="dim")
161
+ text.append(self._truncate(line, 100) + "\n", style="dim")
162
+ text.append(f" … +{len(lines) - 3} lines (ctrl+o to expand)\n", style="dim italic")
163
+
164
+ return text
165
+
166
+ def format_file_diff(self, old_content: str, new_content: str, path: str) -> Text:
167
+ """Format a file edit as a diff view."""
168
+ text = Text()
169
+ text.append(" ⎿ ", style="dim")
170
+
171
+ old_lines = old_content.split("\n") if old_content else []
172
+ new_lines = new_content.split("\n") if new_content else []
173
+
174
+ added = len(new_lines) - len(old_lines)
175
+ if added > 0:
176
+ text.append(f"Added {added} line{'s' if added != 1 else ''}", style="green")
177
+ elif added < 0:
178
+ text.append(f"Removed {-added} line{'s' if -added != 1 else ''}", style="red")
179
+ else:
180
+ text.append("Modified", style="yellow")
181
+
182
+ return text
183
+
184
+ def format_edit_preview(self, old_str: str, new_str: str) -> Text:
185
+ """Format an edit operation showing old vs new."""
186
+ text = Text()
187
+
188
+ old_lines = old_str.strip().split("\n")
189
+ new_lines = new_str.strip().split("\n")
190
+
191
+ # Show a few lines of context
192
+ max_preview = 4
193
+
194
+ for i, line in enumerate(old_lines[:max_preview]):
195
+ text.append(" ", style="dim")
196
+ text.append(f"{i+1:4} ", style="dim")
197
+ text.append("-", style="red")
198
+ text.append(self._truncate(line, 70) + "\n", style="red dim")
199
+
200
+ for i, line in enumerate(new_lines[:max_preview]):
201
+ text.append(" ", style="dim")
202
+ text.append(f"{i+1:4} ", style="dim")
203
+ text.append("+", style="green")
204
+ text.append(self._truncate(line, 70) + "\n", style="green")
205
+
206
+ if len(old_lines) > max_preview or len(new_lines) > max_preview:
207
+ text.append(f" … more changes\n", style="dim italic")
208
+
209
+ return text
210
+
211
+ def format_session_time(self) -> str:
212
+ """Format total session time like 'Churned for 1m 28s'."""
213
+ elapsed = time.time() - self.session_start
214
+
215
+ verbs = ["Crunched", "Churned", "Processed", "Computed"]
216
+ verb = verbs[int(elapsed) % len(verbs)]
217
+
218
+ if elapsed < 60:
219
+ return f"✻ {verb} for {elapsed:.0f}s"
220
+ else:
221
+ mins = int(elapsed // 60)
222
+ secs = int(elapsed % 60)
223
+ return f"✻ {verb} for {mins}m {secs}s"
224
+
225
+ def stream_formatted(self, raw_stream: Generator[str, None, str]) -> Generator[str, None, None]:
226
+ """Transform raw agent stream into Claude Code-style output."""
227
+ buffer = ""
228
+ in_thinking = False
229
+
230
+ for chunk in raw_stream:
231
+ buffer += chunk
232
+
233
+ while "\n" in buffer:
234
+ idx = buffer.find("\n")
235
+ line = buffer[:idx]
236
+ buffer = buffer[idx + 1:]
237
+
238
+ result = self._process_line(line, in_thinking)
239
+
240
+ if "<thinking>" in line:
241
+ in_thinking = True
242
+ if "</thinking>" in line:
243
+ in_thinking = False
244
+
245
+ if result:
246
+ yield result
247
+
248
+ if buffer.strip():
249
+ result = self._process_line(buffer, in_thinking)
250
+ if result:
251
+ yield result
252
+
253
+ # Add session timing at end
254
+ yield f"\n[dim]{self.format_session_time()}[/]\n"
255
+
256
+ def _process_line(self, line: str, in_thinking: bool) -> str:
257
+ """Process a single line and return formatted output."""
258
+ import json
259
+
260
+ # Skip thinking content
261
+ if in_thinking or "<thinking>" in line:
262
+ return ""
263
+
264
+ # Handle tool calls
265
+ if "<tool>" in line and "</tool>" in line:
266
+ match = self.TOOL_PATTERN.search(line)
267
+ if match:
268
+ tool_name = match.group(1)
269
+ params_match = self.PARAMS_PATTERN.search(line)
270
+ params = {}
271
+ if params_match:
272
+ try:
273
+ params = json.loads(params_match.group(1))
274
+ except:
275
+ pass
276
+
277
+ header = self.format_tool_header(tool_name, params)
278
+ self.current_tool = ToolExecution(tool_name, params)
279
+ return f"\n{header}\n"
280
+
281
+ # Handle params on separate line
282
+ if "<params>" in line and "</params>" in line:
283
+ return ""
284
+
285
+ # Handle tool markers from agent
286
+ if line.startswith("[Tool:"):
287
+ return ""
288
+
289
+ if line.startswith("[Result:"):
290
+ result = line[8:].rstrip("]").strip()
291
+ if self.current_tool:
292
+ self.current_tool.end_time = time.time()
293
+ output = self.format_tool_output(result, success=True)
294
+ return str(output)
295
+ return ""
296
+
297
+ if line.startswith("[Error:"):
298
+ error = line[7:].rstrip("]").strip()
299
+ if self.current_tool:
300
+ self.current_tool.end_time = time.time()
301
+ self.current_tool.success = False
302
+ # Show more of the error (200 chars instead of 80) for debugging
303
+ return f" ⎿ [red]Error: {self._truncate(error, 200)}[/]\n"
304
+
305
+ if line.startswith("[Output:"):
306
+ output = line[8:].rstrip("]").strip()
307
+ if self.current_tool:
308
+ self.current_tool.success = False
309
+ # Show command output on failure (helps with debugging)
310
+ return f" ⎿ [yellow]{self._truncate(output, 300)}[/]\n"
311
+
312
+ # Skip other internal markers
313
+ if line.startswith("[") and line.endswith("]"):
314
+ if any(x in line for x in ["iterations", "Parse error", "retry", "Context"]):
315
+ return f"[dim]{line}[/]\n"
316
+ return ""
317
+
318
+ # Regular text
319
+ cleaned = self._clean_text(line)
320
+ if cleaned:
321
+ return cleaned + "\n"
322
+
323
+ return ""
324
+
325
+ def _clean_text(self, text: str) -> str:
326
+ """Clean text by removing markers."""
327
+ text = re.sub(r'</?tool>', '', text)
328
+ text = re.sub(r'</?params>', '', text)
329
+ text = re.sub(r'</?thinking>', '', text)
330
+ text = re.sub(r'\[Tool:.*?\]', '', text)
331
+ text = re.sub(r'\[Result:.*?\]', '', text)
332
+ text = re.sub(r'\n{3,}', '\n\n', text)
333
+ return text.strip()
334
+
335
+
336
+ def print_tool_call(tool_name: str, params: dict, timeout: Optional[str] = None) -> None:
337
+ """Print a tool call in Claude Code style."""
338
+ formatter = OutputFormatter()
339
+ header = formatter.format_tool_header(tool_name, params)
340
+
341
+ timeout_str = f" timeout: {timeout}" if timeout else ""
342
+ console.print(f"\n{header}[dim]{timeout_str}[/]")
343
+
344
+
345
+ def print_tool_result(result: str, success: bool = True, show_full: bool = False) -> None:
346
+ """Print a tool result with optional expansion."""
347
+ formatter = OutputFormatter()
348
+ output = formatter.format_tool_output(result, success)
349
+ console.print(output)
350
+
351
+
352
+ def print_update(path: str, added: int, removed: int) -> None:
353
+ """Print file update summary like Claude Code."""
354
+ text = Text()
355
+ text.append(" ⎿ ", style="dim")
356
+
357
+ if added > 0 and removed > 0:
358
+ text.append(f"Added {added} line{'s' if added != 1 else ''}, ", style="green")
359
+ text.append(f"removed {removed} line{'s' if removed != 1 else ''}", style="red")
360
+ elif added > 0:
361
+ text.append(f"Added {added} line{'s' if added != 1 else ''}", style="green")
362
+ elif removed > 0:
363
+ text.append(f"Removed {removed} line{'s' if removed != 1 else ''}", style="red")
364
+
365
+ console.print(text)
366
+
367
+
368
+ def print_status(message: str, style: str = "dim") -> None:
369
+ """Print a status message."""
370
+ console.print(f"[{style}]{message}[/]")
371
+
372
+
373
+ def print_file_written(path: str, lines: int) -> None:
374
+ """Print file written confirmation."""
375
+ console.print(f" ⎿ [green]Wrote {path} ({lines} lines)[/]")
376
+
377
+
378
+ def print_file_read(path: str, lines: int) -> None:
379
+ """Print file read confirmation."""
380
+ console.print(f" ⎿ [dim]Read {path} ({lines} lines)[/]")
381
+
382
+
383
+ def create_progress_table(items: list[tuple[str, str, str]]) -> Table:
384
+ """Create a progress table for multi-step operations."""
385
+ table = Table(show_header=False, box=None, padding=(0, 1))
386
+ table.add_column("Status", width=3)
387
+ table.add_column("Step")
388
+ table.add_column("Details", style="dim")
389
+
390
+ for status, step, details in items:
391
+ if status == "done":
392
+ table.add_row("[green]✓[/]", step, details)
393
+ elif status == "current":
394
+ table.add_row("[cyan]●[/]", f"[bold]{step}[/]", details)
395
+ elif status == "error":
396
+ table.add_row("[red]✗[/]", step, details)
397
+ else:
398
+ table.add_row("[dim]○[/]", f"[dim]{step}[/]", details)
399
+
400
+ return table
401
+
402
+
403
+ def create_spinner(message: str = "Working") -> Spinner:
404
+ """Create a spinner for long operations."""
405
+ return Spinner("dots", text=Text(f" {message}...", style="cyan"))
406
+
407
+
408
+ def print_welcome_banner(coder_model: str, router_model: str, sandbox_path: str) -> None:
409
+ """Print welcome banner in Claude Code style."""
410
+ console.print()
411
+ from . import __version__
412
+ console.print(f"[bold cyan]Kairo Code[/] v{__version__} - AI Coding Assistant by Kairon Labs")
413
+ console.print(f"[dim]Model: {coder_model}[/]")
414
+ console.print(f"[dim]Sandbox: {sandbox_path}[/]")
415
+ console.print()
416
+ console.print("[dim]Just describe what you want - Kairo Code will plan and implement automatically.[/]")
417
+ console.print("[dim]Agents: /explore, /code, /plan | Utils: /search, /settings, /help, /exit[/]")
418
+ console.print()
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: kairo-code
3
+ Version: 0.1.0
4
+ Summary: Kairo Code - AI Coding Assistant by Kairon Labs
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: rich>=13.0.0
8
+ Requires-Dist: openai>=1.0.0
9
+ Requires-Dist: duckduckgo-search>=4.0.0
10
+ Requires-Dist: pyyaml>=6.0
11
+ Requires-Dist: prompt-toolkit>=3.0.0
12
+ Requires-Dist: requests>=2.31.0
13
+ Requires-Dist: httpx>=0.27.0