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/router.py ADDED
@@ -0,0 +1,217 @@
1
+ """Intent router using small LLM for classification with few-shot examples"""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ import re
6
+
7
+ from .llm import LLM
8
+
9
+
10
+ class Intent(Enum):
11
+ SEARCH = "search"
12
+ CODE = "code"
13
+ FILE_READ = "file_read"
14
+ FILE_WRITE = "file_write"
15
+ FILE_LIST = "file_list"
16
+ EXPLORE = "explore"
17
+ PLAN = "plan"
18
+ CHAT = "chat"
19
+
20
+
21
+ @dataclass
22
+ class RoutedIntent:
23
+ intent: Intent
24
+ confidence: str # "high", "medium", "low"
25
+ extracted_params: dict
26
+
27
+
28
+ # Few-shot examples for the router
29
+ ROUTER_EXAMPLES = """
30
+ ## Examples
31
+
32
+ Input: "What's the weather like today?"
33
+ Output: search
34
+
35
+ Input: "Write a Python function to sort a list"
36
+ Output: code
37
+
38
+ Input: "Show me what's in config.py"
39
+ Output: file_read
40
+
41
+ Input: "Create a new file called utils.py with a helper function"
42
+ Output: file_write
43
+
44
+ Input: "What files are in the src directory?"
45
+ Output: file_list
46
+
47
+ Input: "Analyze this codebase for improvements"
48
+ Output: explore
49
+
50
+ Input: "How should I implement user authentication?"
51
+ Output: plan
52
+
53
+ Input: "What's the capital of France?"
54
+ Output: chat
55
+
56
+ Input: "Search for the latest Python 3.12 features"
57
+ Output: search
58
+
59
+ Input: "Fix the bug in main.py line 42"
60
+ Output: code
61
+
62
+ Input: "Read the README file"
63
+ Output: file_read
64
+
65
+ Input: "Save this code to server.py"
66
+ Output: file_write
67
+
68
+ Input: "Find all Python files in this project"
69
+ Output: file_list
70
+
71
+ Input: "Explore how the API endpoints work"
72
+ Output: explore
73
+
74
+ Input: "Plan out how to refactor the database layer"
75
+ Output: plan
76
+
77
+ Input: "Hello, how are you?"
78
+ Output: chat
79
+
80
+ Input: "Create a tool with a UI that pulls data from an API"
81
+ Output: code
82
+
83
+ Input: "Build me an app that tracks my tasks"
84
+ Output: code
85
+ """
86
+
87
+ ROUTER_SYSTEM_PROMPT = f"""You are a classifier. Classify the user's intent into exactly ONE category.
88
+
89
+ Categories:
90
+ - search: User wants current/real-time information from the web
91
+ - code: User wants to write, modify, fix, or generate code
92
+ - file_read: User wants to see contents of a specific file
93
+ - file_write: User wants to create or save a file
94
+ - file_list: User wants to list or find files
95
+ - explore: User wants to analyze or understand a codebase
96
+ - plan: User wants to plan a complex task before implementing
97
+ - chat: General conversation, questions, or explanations
98
+
99
+ {ROUTER_EXAMPLES}
100
+
101
+ Respond with ONLY the category name, nothing else.
102
+ """
103
+
104
+
105
+ class Router:
106
+ """Routes user input to the appropriate handler using few-shot classification."""
107
+
108
+ INTENT_CATEGORIES = [i.value for i in Intent]
109
+
110
+ def __init__(self, llm: LLM):
111
+ self.llm = llm
112
+
113
+ def route(self, user_input: str) -> RoutedIntent:
114
+ """
115
+ Classify user input and extract relevant parameters.
116
+ Uses few-shot examples for better accuracy with small models.
117
+ """
118
+ # First try rule-based classification for common patterns
119
+ rule_based = self._rule_based_classify(user_input)
120
+ if rule_based:
121
+ return rule_based
122
+
123
+ # Fall back to LLM classification
124
+ from .config import Config
125
+ config = Config()
126
+ system = config.router_prompt + "\n" + ROUTER_EXAMPLES + "\nRespond with ONLY the category name, nothing else."
127
+ response = self.llm.generate(
128
+ prompt=f"Input: {user_input}\nOutput:",
129
+ model=self.llm.select_model("router"),
130
+ system=system,
131
+ stream=False,
132
+ )
133
+
134
+ # Parse the response
135
+ intent_str = response.strip().lower()
136
+
137
+ # Find matching intent
138
+ intent = Intent.CHAT # Default
139
+ for cat in self.INTENT_CATEGORIES:
140
+ if cat in intent_str:
141
+ try:
142
+ intent = Intent(cat)
143
+ except ValueError:
144
+ pass
145
+ break
146
+
147
+ # Extract params based on intent
148
+ params = self._extract_params(user_input, intent)
149
+
150
+ return RoutedIntent(
151
+ intent=intent,
152
+ confidence="high" if intent_str in self.INTENT_CATEGORIES else "medium",
153
+ extracted_params=params,
154
+ )
155
+
156
+ def _rule_based_classify(self, user_input: str) -> RoutedIntent | None:
157
+ """Fast rule-based classification for common patterns."""
158
+ lower = user_input.lower().strip()
159
+
160
+ # Search patterns
161
+ search_keywords = ["weather", "latest", "current", "today's", "news", "price of"]
162
+ if any(kw in lower for kw in search_keywords):
163
+ return RoutedIntent(Intent.SEARCH, "high", {})
164
+
165
+ # File read patterns
166
+ if re.match(r"^(show|read|cat|display|what'?s in|open)\s+.+\.(py|js|ts|json|yaml|yml|md|txt)", lower):
167
+ # Extract filename
168
+ match = re.search(r'[\w./\\-]+\.\w+', user_input)
169
+ path = match.group(0) if match else ""
170
+ return RoutedIntent(Intent.FILE_READ, "high", {"path": path})
171
+
172
+ # File list patterns
173
+ if re.match(r"^(list|find|show|what)\s+(files?|all)", lower):
174
+ return RoutedIntent(Intent.FILE_LIST, "high", {})
175
+
176
+ # Explore patterns
177
+ if any(phrase in lower for phrase in ["explore", "analyze", "understand", "how does", "explain the code"]):
178
+ return RoutedIntent(Intent.EXPLORE, "high", {})
179
+
180
+ # Plan patterns
181
+ if any(phrase in lower for phrase in ["plan", "how should i", "how to implement", "design"]):
182
+ return RoutedIntent(Intent.PLAN, "high", {})
183
+
184
+ # Code patterns
185
+ code_keywords = ["write", "create", "fix", "modify", "add", "implement", "refactor", "update", "change", "build", "make"]
186
+ code_context = ["function", "class", "method", "code", "script", "bug", "error", "tool", "app", "application", "program", "ui", "gui", "api"]
187
+ if any(kw in lower for kw in code_keywords) and any(ctx in lower for ctx in code_context):
188
+ return RoutedIntent(Intent.CODE, "high", {})
189
+
190
+ # Greeting patterns
191
+ greetings = ["hello", "hi", "hey", "good morning", "good afternoon"]
192
+ if any(lower.startswith(g) for g in greetings):
193
+ return RoutedIntent(Intent.CHAT, "high", {})
194
+
195
+ return None # Fall back to LLM
196
+
197
+ def _extract_params(self, user_input: str, intent: Intent) -> dict:
198
+ """Extract relevant parameters from user input based on intent."""
199
+ params = {}
200
+
201
+ if intent in (Intent.FILE_READ, Intent.FILE_WRITE):
202
+ # Extract file path
203
+ path_match = re.search(r'[\w./\\-]+\.\w+', user_input)
204
+ if path_match:
205
+ params["path"] = path_match.group(0)
206
+
207
+ if intent == Intent.FILE_LIST:
208
+ # Extract glob pattern if present
209
+ pattern_match = re.search(r'\*+\.?\w*', user_input)
210
+ if pattern_match:
211
+ params["pattern"] = pattern_match.group(0)
212
+
213
+ if intent == Intent.SEARCH:
214
+ # The whole input is basically the search query
215
+ params["query"] = user_input
216
+
217
+ return params
kairo_code/sandbox.py ADDED
@@ -0,0 +1,248 @@
1
+ """Sandbox and safety controls for Kairo Code - prevents unauthorized file access"""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass
10
+ class SandboxConfig:
11
+ """Configuration for the sandbox."""
12
+ # Root directory for all file operations (usually project dir)
13
+ root_dir: Path
14
+ # Allow operations outside root (dangerous!)
15
+ allow_escape: bool = False
16
+ # Paths that are always blocked (even with allow_escape)
17
+ blocked_paths: tuple = (
18
+ "/etc", "/usr", "/bin", "/sbin", "/boot", "/root",
19
+ "/sys", "/proc", "/dev", "/var/log",
20
+ "~/.ssh", "~/.gnupg", "~/.aws", "~/.config",
21
+ )
22
+ # File extensions that require extra confirmation
23
+ sensitive_extensions: tuple = (
24
+ ".env", ".pem", ".key", ".crt", ".p12",
25
+ ".password", ".secret", ".credentials",
26
+ )
27
+ # Max file size to write (prevents filling disk)
28
+ max_file_size: int = 10 * 1024 * 1024 # 10MB
29
+
30
+
31
+ class Sandbox:
32
+ """
33
+ Sandbox for safe file operations.
34
+
35
+ Restricts file access to a root directory and blocks sensitive paths.
36
+ Similar to Claude Code's sandboxing.
37
+ """
38
+
39
+ def __init__(self, config: Optional[SandboxConfig] = None):
40
+ if config is None:
41
+ # Default: use current working directory as root
42
+ config = SandboxConfig(root_dir=Path.cwd())
43
+
44
+ self.config = config
45
+ self.root = config.root_dir.resolve()
46
+
47
+ # Expand blocked paths
48
+ self._blocked = set()
49
+ for p in config.blocked_paths:
50
+ expanded = Path(p).expanduser()
51
+ self._blocked.add(str(expanded.resolve()))
52
+
53
+ def validate_path(self, path: str, operation: str = "access") -> tuple[bool, str, Path]:
54
+ """
55
+ Validate a path for safety.
56
+
57
+ Args:
58
+ path: The path to validate
59
+ operation: The operation being performed (read, write, delete)
60
+
61
+ Returns:
62
+ (is_valid, error_message, resolved_path)
63
+ """
64
+ try:
65
+ # Resolve the path
66
+ target = Path(path).expanduser()
67
+
68
+ # Make absolute if relative
69
+ if not target.is_absolute():
70
+ target = self.root / target
71
+
72
+ resolved = target.resolve()
73
+
74
+ # Check if path escapes sandbox
75
+ if not self.config.allow_escape:
76
+ try:
77
+ resolved.relative_to(self.root)
78
+ except ValueError:
79
+ return (
80
+ False,
81
+ f"Path '{path}' is outside project directory. "
82
+ f"Only paths within '{self.root}' are allowed.",
83
+ resolved
84
+ )
85
+
86
+ # Check blocked paths
87
+ resolved_str = str(resolved)
88
+ for blocked in self._blocked:
89
+ if resolved_str.startswith(blocked):
90
+ return (
91
+ False,
92
+ f"Access to '{path}' is blocked for security reasons.",
93
+ resolved
94
+ )
95
+
96
+ # Check for path traversal attempts
97
+ if ".." in path:
98
+ # Re-verify after resolution
99
+ try:
100
+ resolved.relative_to(self.root)
101
+ except ValueError:
102
+ return (
103
+ False,
104
+ f"Path traversal detected in '{path}'.",
105
+ resolved
106
+ )
107
+
108
+ # Check sensitive extensions for write operations
109
+ if operation == "write" and resolved.suffix in self.config.sensitive_extensions:
110
+ return (
111
+ False,
112
+ f"Writing to sensitive file type '{resolved.suffix}' requires manual confirmation.",
113
+ resolved
114
+ )
115
+
116
+ return (True, "", resolved)
117
+
118
+ except Exception as e:
119
+ return (False, f"Invalid path: {e}", Path(path))
120
+
121
+ def validate_content(self, content: str, path: str) -> tuple[bool, str]:
122
+ """
123
+ Validate content being written.
124
+
125
+ Args:
126
+ content: The content to write
127
+ path: The target path
128
+
129
+ Returns:
130
+ (is_valid, error_message)
131
+ """
132
+ # Check size
133
+ size = len(content.encode('utf-8'))
134
+ if size > self.config.max_file_size:
135
+ return (
136
+ False,
137
+ f"Content too large ({size / 1024 / 1024:.1f}MB). "
138
+ f"Max allowed: {self.config.max_file_size / 1024 / 1024:.0f}MB"
139
+ )
140
+
141
+ # Check for suspicious content patterns
142
+ suspicious_patterns = [
143
+ ("#!/", "Script with shebang"),
144
+ ("rm -rf", "Destructive command"),
145
+ ("sudo ", "Sudo command"),
146
+ ("eval(", "Eval statement"),
147
+ ("exec(", "Exec statement"),
148
+ ("__import__", "Dynamic import"),
149
+ ]
150
+
151
+ warnings = []
152
+ for pattern, description in suspicious_patterns:
153
+ if pattern in content:
154
+ warnings.append(description)
155
+
156
+ if warnings:
157
+ # Don't block, just warn
158
+ return (True, f"Warning: Content contains: {', '.join(warnings)}")
159
+
160
+ return (True, "")
161
+
162
+ def safe_read(self, path: str) -> str:
163
+ """Safely read a file with validation."""
164
+ valid, error, resolved = self.validate_path(path, "read")
165
+ if not valid:
166
+ raise PermissionError(error)
167
+
168
+ if not resolved.exists():
169
+ raise FileNotFoundError(f"File not found: {path}")
170
+
171
+ if not resolved.is_file():
172
+ raise IsADirectoryError(f"Not a file: {path}")
173
+
174
+ return resolved.read_text()
175
+
176
+ def safe_write(self, path: str, content: str) -> str:
177
+ """Safely write a file with validation."""
178
+ valid, error, resolved = self.validate_path(path, "write")
179
+ if not valid:
180
+ raise PermissionError(error)
181
+
182
+ valid, warning = self.validate_content(content, path)
183
+ if not valid:
184
+ raise ValueError(warning)
185
+
186
+ # Create parent directories if needed
187
+ resolved.parent.mkdir(parents=True, exist_ok=True)
188
+
189
+ # Write the file
190
+ resolved.write_text(content)
191
+
192
+ line_count = content.count('\n') + 1
193
+ result = f"File written: {resolved.relative_to(self.root)} ({line_count} lines)"
194
+
195
+ if warning:
196
+ result += f"\n{warning}"
197
+
198
+ return result
199
+
200
+ def safe_delete(self, path: str) -> str:
201
+ """Safely delete a file with validation."""
202
+ valid, error, resolved = self.validate_path(path, "delete")
203
+ if not valid:
204
+ raise PermissionError(error)
205
+
206
+ if not resolved.exists():
207
+ raise FileNotFoundError(f"File not found: {path}")
208
+
209
+ if resolved.is_dir():
210
+ raise IsADirectoryError(f"Cannot delete directory: {path}. Use bash for that.")
211
+
212
+ resolved.unlink()
213
+ return f"Deleted: {resolved.relative_to(self.root)}"
214
+
215
+ def list_allowed_paths(self) -> list[str]:
216
+ """List paths that are allowed for access."""
217
+ return [str(self.root)]
218
+
219
+ def get_relative_path(self, path: str) -> str:
220
+ """Get path relative to sandbox root."""
221
+ try:
222
+ resolved = Path(path).resolve()
223
+ return str(resolved.relative_to(self.root))
224
+ except ValueError:
225
+ return path
226
+
227
+
228
+ # Global sandbox instance (initialized when Kairo Code starts)
229
+ _sandbox: Optional[Sandbox] = None
230
+
231
+
232
+ def init_sandbox(root: Optional[Path] = None, allow_escape: bool = False) -> Sandbox:
233
+ """Initialize the global sandbox."""
234
+ global _sandbox
235
+ config = SandboxConfig(
236
+ root_dir=root or Path.cwd(),
237
+ allow_escape=allow_escape
238
+ )
239
+ _sandbox = Sandbox(config)
240
+ return _sandbox
241
+
242
+
243
+ def get_sandbox() -> Sandbox:
244
+ """Get the global sandbox, initializing if needed."""
245
+ global _sandbox
246
+ if _sandbox is None:
247
+ _sandbox = Sandbox()
248
+ return _sandbox
kairo_code/settings.py ADDED
@@ -0,0 +1,183 @@
1
+ """User settings management with interactive prompts."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import yaml
8
+ from rich.console import Console
9
+ from rich.prompt import Confirm, Prompt
10
+
11
+ console = Console()
12
+
13
+ # User settings directory
14
+ SETTINGS_DIR = Path.home() / ".kairo_code"
15
+ SETTINGS_FILE = SETTINGS_DIR / "settings.yaml"
16
+
17
+ DEFAULT_SETTINGS = {
18
+ "auto_approve": {
19
+ "enabled": False,
20
+ "write_file": False,
21
+ "bash": False,
22
+ "prompted": False, # Whether we've asked the user yet
23
+ },
24
+ "verbose": False,
25
+ }
26
+
27
+
28
+ def load_user_settings() -> dict[str, Any]:
29
+ """Load user settings from ~/.kairo_code/settings.yaml"""
30
+ SETTINGS_DIR.mkdir(parents=True, exist_ok=True)
31
+
32
+ if not SETTINGS_FILE.exists():
33
+ return DEFAULT_SETTINGS.copy()
34
+
35
+ try:
36
+ with open(SETTINGS_FILE) as f:
37
+ settings = yaml.safe_load(f) or {}
38
+ # Merge with defaults for any missing keys
39
+ merged = DEFAULT_SETTINGS.copy()
40
+ _deep_merge(merged, settings)
41
+ return merged
42
+ except Exception:
43
+ return DEFAULT_SETTINGS.copy()
44
+
45
+
46
+ def save_user_settings(settings: dict[str, Any]) -> None:
47
+ """Save user settings to ~/.kairo_code/settings.yaml"""
48
+ SETTINGS_DIR.mkdir(parents=True, exist_ok=True)
49
+
50
+ with open(SETTINGS_FILE, "w") as f:
51
+ yaml.dump(settings, f, default_flow_style=False)
52
+
53
+
54
+ def _deep_merge(base: dict, override: dict) -> None:
55
+ """Recursively merge override into base."""
56
+ for key, value in override.items():
57
+ if key in base and isinstance(base[key], dict) and isinstance(value, dict):
58
+ _deep_merge(base[key], value)
59
+ else:
60
+ base[key] = value
61
+
62
+
63
+ def prompt_auto_approve_settings(force: bool = False) -> dict[str, Any]:
64
+ """
65
+ Interactively prompt user for auto-approve settings.
66
+ Only prompts on first run unless force=True.
67
+ Returns the updated settings.
68
+ """
69
+ settings = load_user_settings()
70
+
71
+ # Skip if already prompted (unless forced)
72
+ if settings["auto_approve"].get("prompted") and not force:
73
+ return settings
74
+
75
+ console.print("\n[bold cyan]═══ Auto-Approve Settings ═══[/]\n")
76
+ console.print("[dim]Kairo Code can auto-approve certain actions to speed up your workflow.[/]")
77
+ console.print("[dim]You can change these settings anytime with /settings[/]\n")
78
+
79
+ # Ask about file writes
80
+ settings["auto_approve"]["write_file"] = Confirm.ask(
81
+ "[yellow]?[/] Auto-approve [bold]file writes[/] (create/modify files without asking)?",
82
+ default=False
83
+ )
84
+
85
+ # Ask about bash commands
86
+ console.print("\n[dim]⚠ Bash commands can modify your system. Auto-approve with caution.[/]")
87
+ settings["auto_approve"]["bash"] = Confirm.ask(
88
+ "[yellow]?[/] Auto-approve [bold]bash commands[/] (run shell commands without asking)?",
89
+ default=False
90
+ )
91
+
92
+ # Set enabled if any are true
93
+ settings["auto_approve"]["enabled"] = (
94
+ settings["auto_approve"]["write_file"] or
95
+ settings["auto_approve"]["bash"]
96
+ )
97
+
98
+ # Mark as prompted
99
+ settings["auto_approve"]["prompted"] = True
100
+
101
+ # Save settings
102
+ save_user_settings(settings)
103
+
104
+ # Show summary
105
+ console.print("\n[bold]Settings saved![/]")
106
+ _print_current_settings(settings)
107
+ console.print()
108
+
109
+ return settings
110
+
111
+
112
+ def _print_current_settings(settings: dict[str, Any]) -> None:
113
+ """Print current auto-approve settings."""
114
+ auto = settings["auto_approve"]
115
+
116
+ write_status = "[green]✓ Yes[/]" if auto.get("write_file") else "[red]✗ No[/]"
117
+ bash_status = "[green]✓ Yes[/]" if auto.get("bash") else "[red]✗ No[/]"
118
+
119
+ console.print(f" • Auto-approve file writes: {write_status}")
120
+ console.print(f" • Auto-approve bash commands: {bash_status}")
121
+
122
+
123
+ def show_settings_menu() -> dict[str, Any]:
124
+ """Show settings menu and allow user to change settings."""
125
+ settings = load_user_settings()
126
+
127
+ console.print("\n[bold cyan]═══ Kairo Code Settings ═══[/]\n")
128
+ console.print("[bold]Current settings:[/]")
129
+ _print_current_settings(settings)
130
+
131
+ console.print("\n[bold]Options:[/]")
132
+ console.print(" [cyan]1[/] - Toggle auto-approve file writes")
133
+ console.print(" [cyan]2[/] - Toggle auto-approve bash commands")
134
+ console.print(" [cyan]3[/] - Enable all auto-approve")
135
+ console.print(" [cyan]4[/] - Disable all auto-approve")
136
+ console.print(" [cyan]q[/] - Back to chat")
137
+
138
+ choice = Prompt.ask("\n[yellow]?[/] Select option", choices=["1", "2", "3", "4", "q"], default="q")
139
+
140
+ if choice == "1":
141
+ settings["auto_approve"]["write_file"] = not settings["auto_approve"]["write_file"]
142
+ console.print(f"[green]File writes auto-approve: {'enabled' if settings['auto_approve']['write_file'] else 'disabled'}[/]")
143
+ elif choice == "2":
144
+ settings["auto_approve"]["bash"] = not settings["auto_approve"]["bash"]
145
+ console.print(f"[green]Bash auto-approve: {'enabled' if settings['auto_approve']['bash'] else 'disabled'}[/]")
146
+ elif choice == "3":
147
+ settings["auto_approve"]["write_file"] = True
148
+ settings["auto_approve"]["bash"] = True
149
+ settings["auto_approve"]["enabled"] = True
150
+ console.print("[green]All auto-approve enabled[/]")
151
+ elif choice == "4":
152
+ settings["auto_approve"]["write_file"] = False
153
+ settings["auto_approve"]["bash"] = False
154
+ settings["auto_approve"]["enabled"] = False
155
+ console.print("[green]All auto-approve disabled[/]")
156
+
157
+ # Update enabled flag
158
+ settings["auto_approve"]["enabled"] = (
159
+ settings["auto_approve"]["write_file"] or
160
+ settings["auto_approve"]["bash"]
161
+ )
162
+
163
+ save_user_settings(settings)
164
+ return settings
165
+
166
+
167
+ def should_auto_approve(tool_name: str, settings: dict[str, Any] | None = None) -> bool:
168
+ """Check if a tool should be auto-approved based on user settings."""
169
+ if settings is None:
170
+ settings = load_user_settings()
171
+
172
+ auto = settings.get("auto_approve", {})
173
+
174
+ if tool_name == "write_file":
175
+ return auto.get("write_file", False)
176
+ elif tool_name == "bash":
177
+ return auto.get("bash", False)
178
+
179
+ # Read-only tools are always auto-approved
180
+ if tool_name in ("read_file", "list_files", "tree", "search_files", "web_search"):
181
+ return True
182
+
183
+ return False
@@ -0,0 +1,51 @@
1
+ """Kairo Code Tools - File operations, search, and code generation"""
2
+
3
+ from .base import (
4
+ Tool,
5
+ ToolResult,
6
+ ToolRegistry,
7
+ FunctionTool,
8
+ parse_tool_calls,
9
+ format_tool_error,
10
+ ParsedToolCall,
11
+ ToolParseResult,
12
+ )
13
+ from .files import (
14
+ read_file,
15
+ write_file,
16
+ edit_file,
17
+ list_files,
18
+ search_files,
19
+ tree,
20
+ )
21
+ from .search import web_search, format_search_results
22
+ from .code import extract_code_blocks, build_code_prompt, build_explain_prompt
23
+ from .definitions import create_default_registry, create_readonly_registry
24
+
25
+ __all__ = [
26
+ # Base classes
27
+ "Tool",
28
+ "ToolResult",
29
+ "ToolRegistry",
30
+ "FunctionTool",
31
+ "parse_tool_calls",
32
+ "format_tool_error",
33
+ "ParsedToolCall",
34
+ "ToolParseResult",
35
+ "create_default_registry",
36
+ "create_readonly_registry",
37
+ # File tools
38
+ "read_file",
39
+ "write_file",
40
+ "edit_file",
41
+ "list_files",
42
+ "search_files",
43
+ "tree",
44
+ # Search
45
+ "web_search",
46
+ "format_search_results",
47
+ # Code
48
+ "extract_code_blocks",
49
+ "build_code_prompt",
50
+ "build_explain_prompt",
51
+ ]