devobin 1.0.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 (47) hide show
  1. devobin/__init__.py +4 -0
  2. devobin/app.py +42 -0
  3. devobin/cli/__init__.py +1 -0
  4. devobin/cli/chat.py +831 -0
  5. devobin/cli/commands.py +362 -0
  6. devobin/cli/slash_menu.py +74 -0
  7. devobin/cli/terminal.py +155 -0
  8. devobin/cli/ui.py +129 -0
  9. devobin/config/__init__.py +1 -0
  10. devobin/config/settings.py +124 -0
  11. devobin/core/__init__.py +1 -0
  12. devobin/core/analyzer.py +166 -0
  13. devobin/core/executor.py +244 -0
  14. devobin/core/memory.py +78 -0
  15. devobin/core/optimizer.py +106 -0
  16. devobin/core/prompt_builder.py +513 -0
  17. devobin/core/researcher.py +283 -0
  18. devobin/core/rtl.py +93 -0
  19. devobin/core/scanner.py +206 -0
  20. devobin/core/tools.py +283 -0
  21. devobin/core/validator.py +235 -0
  22. devobin/main.py +33 -0
  23. devobin/output/__init__.py +1 -0
  24. devobin/providers/__init__.py +85 -0
  25. devobin/providers/anthropic_provider.py +72 -0
  26. devobin/providers/google_provider.py +90 -0
  27. devobin/providers/ollama_provider.py +81 -0
  28. devobin/providers/openai_provider.py +102 -0
  29. devobin/storage/__init__.py +1 -0
  30. devobin/storage/database.py +207 -0
  31. devobin/ui/__init__.py +1 -0
  32. devobin/ui/ask_modal.py +96 -0
  33. devobin/ui/chat_screen.py +1029 -0
  34. devobin/ui/command_palette.py +131 -0
  35. devobin/ui/connect_modal.py +166 -0
  36. devobin/ui/export_modal.py +159 -0
  37. devobin/ui/history_modal.py +137 -0
  38. devobin/ui/memory_modal.py +88 -0
  39. devobin/ui/model_modal.py +143 -0
  40. devobin/ui/permission_modal.py +92 -0
  41. devobin/ui/project_modal.py +133 -0
  42. devobin/ui/settings_modal.py +89 -0
  43. devobin-1.0.0.dist-info/METADATA +364 -0
  44. devobin-1.0.0.dist-info/RECORD +47 -0
  45. devobin-1.0.0.dist-info/WHEEL +4 -0
  46. devobin-1.0.0.dist-info/entry_points.txt +2 -0
  47. devobin-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,362 @@
1
+ """Slash command handler for DevObin AI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from devobin.cli.ui import (
8
+ console,
9
+ print_error,
10
+ print_help,
11
+ print_provider_table,
12
+ print_model_table,
13
+ print_success,
14
+ print_status,
15
+ print_warning,
16
+ )
17
+ from devobin.config.settings import get_config, CONFIG_DIR
18
+
19
+
20
+ @dataclass
21
+ class CommandResult:
22
+ """Result of processing a command."""
23
+
24
+ handled: bool = False
25
+ should_exit: bool = False
26
+ message: str = ""
27
+
28
+
29
+ async def handle_command(command: str) -> CommandResult:
30
+ """Process a slash command and return the result."""
31
+ parts = command.strip().split(maxsplit=1)
32
+ cmd = parts[0].lower() if parts else ""
33
+ args = parts[1] if len(parts) > 1 else ""
34
+
35
+ handlers = {
36
+ "/help": _cmd_help,
37
+ "/connect": _cmd_connect,
38
+ "/models": _cmd_models,
39
+ "/model": _cmd_model,
40
+ "/memory": _cmd_memory,
41
+ "/project": _cmd_project,
42
+ "/export": _cmd_export,
43
+ "/settings": _cmd_settings,
44
+ "/clear": _cmd_clear,
45
+ "/quit": _cmd_quit,
46
+ "/exit": _cmd_quit,
47
+ }
48
+
49
+ handler = handlers.get(cmd)
50
+ if handler:
51
+ return await handler(args)
52
+
53
+ print_error(f"Unknown command: {cmd}. Type /help to see available commands.")
54
+ return CommandResult(handled=True)
55
+
56
+
57
+ async def _cmd_help(args: str) -> CommandResult:
58
+ print_help()
59
+ return CommandResult(handled=True)
60
+
61
+
62
+ async def _cmd_connect(args: str) -> CommandResult:
63
+ """Interactive provider connection wizard."""
64
+ from rich.prompt import Prompt, Confirm
65
+
66
+ config = get_config()
67
+
68
+ console.print()
69
+ console.print(" [bold cyan]AI Provider Setup[/]", style="bold")
70
+ console.print()
71
+
72
+ provider_types = ["openai", "anthropic", "google", "ollama", "openai-compatible"]
73
+ console.print(" Available provider types:")
74
+ for i, pt in enumerate(provider_types, 1):
75
+ console.print(f" {i}. {pt}")
76
+ console.print()
77
+
78
+ choice = Prompt.ask(" Select provider type", choices=[str(i) for i in range(1, 6)], default="1")
79
+ provider_type = provider_types[int(choice) - 1]
80
+
81
+ console.print()
82
+ name = Prompt.ask(" Provider name", default=provider_type)
83
+ api_key = Prompt.ask(" API key", password=True, default="")
84
+
85
+ base_url = ""
86
+ if provider_type == "openai-compatible":
87
+ base_url = Prompt.ask(" API base URL", default="https://api.openai.com/v1")
88
+ elif provider_type == "ollama":
89
+ base_url = Prompt.ask(" Ollama URL", default="http://localhost:11434")
90
+
91
+ # Get available models
92
+ from devobin.providers import AIProvider
93
+
94
+ temp_config = {
95
+ "api_key": api_key,
96
+ "base_url": base_url or None,
97
+ "model": "",
98
+ "type": provider_type,
99
+ }
100
+
101
+ # Try to get models from provider
102
+ model = ""
103
+ try:
104
+ if provider_type in ("openai", "openai-compatible"):
105
+ from devobin.providers.openai_provider import OpenAIProvider
106
+ temp_config["model"] = "gpt-4o"
107
+ p = OpenAIProvider(temp_config)
108
+ models = p.available_models()
109
+ elif provider_type == "anthropic":
110
+ from devobin.providers.anthropic_provider import AnthropicProvider
111
+ temp_config["model"] = "claude-sonnet-4-20250514"
112
+ p = AnthropicProvider(temp_config)
113
+ models = p.available_models()
114
+ elif provider_type == "google":
115
+ from devobin.providers.google_provider import GoogleProvider
116
+ temp_config["model"] = "gemini-2.0-flash"
117
+ p = GoogleProvider(temp_config)
118
+ models = p.available_models()
119
+ elif provider_type == "ollama":
120
+ from devobin.providers.ollama_provider import OllamaProvider
121
+ temp_config["model"] = "llama3"
122
+ p = OllamaProvider(temp_config)
123
+ models = p.available_models()
124
+ else:
125
+ models = []
126
+ except Exception:
127
+ models = []
128
+
129
+ if models:
130
+ console.print()
131
+ console.print(" Available models:")
132
+ for i, m in enumerate(models, 1):
133
+ console.print(f" {i}. {m}")
134
+ console.print()
135
+ model_choice = Prompt.ask(
136
+ " Select model (number or type name)",
137
+ default="1",
138
+ )
139
+ try:
140
+ idx = int(model_choice) - 1
141
+ model = models[idx] if 0 <= idx < len(models) else model_choice
142
+ except ValueError:
143
+ model = model_choice
144
+ else:
145
+ model = Prompt.ask(" Model name", default="gpt-4o")
146
+
147
+ # Save
148
+ provider_config = {
149
+ "type": provider_type,
150
+ "api_key": api_key,
151
+ "base_url": base_url or None,
152
+ "model": model,
153
+ }
154
+
155
+ config.add_provider(name, provider_config)
156
+ config.active_provider = name
157
+ config.active_model = model
158
+
159
+ console.print()
160
+ print_success(f"Connected to {name} ({provider_type}) — model: {model}")
161
+ console.print()
162
+ return CommandResult(handled=True)
163
+
164
+
165
+ async def _cmd_models(args: str) -> CommandResult:
166
+ """List available models from current provider."""
167
+ config = get_config()
168
+ providers = config.list_providers()
169
+
170
+ if not providers:
171
+ print_warning("No providers configured. Use /connect to set one up.")
172
+ return CommandResult(handled=True)
173
+
174
+ print_provider_table(providers)
175
+ return CommandResult(handled=True)
176
+
177
+
178
+ async def _cmd_model(args: str) -> CommandResult:
179
+ """Switch to a different model."""
180
+ config = get_config()
181
+
182
+ if not args:
183
+ current = config.active_model or "None"
184
+ print_status(f"Current model: [bold]{current}[/]")
185
+ return CommandResult(handled=True)
186
+
187
+ config.active_model = args.strip()
188
+ print_success(f"Switched to model: {args.strip()}")
189
+ return CommandResult(handled=True)
190
+
191
+
192
+ async def _cmd_memory(args: str) -> CommandResult:
193
+ """Show saved memories."""
194
+ from devobin.core.memory import MemoryManager
195
+
196
+ config = get_config()
197
+ manager = MemoryManager()
198
+
199
+ global_mems = manager.get_global_memories()
200
+ project_mems = manager.get_project_memories()
201
+
202
+ if not global_mems and not project_mems:
203
+ print_status("No memories saved yet. Chat with DevObin to build context.")
204
+ return CommandResult(handled=True)
205
+
206
+ if global_mems:
207
+ console.print()
208
+ console.print(" [bold cyan]Global Memories:[/]")
209
+ for m in global_mems[-15:]:
210
+ cat = m.get("category", "general")
211
+ console.print(f" [dim][{cat}][/dim] {m['content'][:100]}")
212
+
213
+ if project_mems:
214
+ console.print()
215
+ console.print(" [bold cyan]Project Memories:[/]")
216
+ for m in project_mems[-15:]:
217
+ cat = m.get("category", "general")
218
+ console.print(f" [dim][{cat}][/dim] {m['content'][:100]}")
219
+
220
+ console.print()
221
+ return CommandResult(handled=True)
222
+
223
+
224
+ async def _cmd_project(args: str) -> CommandResult:
225
+ """Manage projects."""
226
+ config = get_config()
227
+ parts = args.strip().split(maxsplit=1)
228
+ sub = parts[0].lower() if parts else ""
229
+ proj_name = parts[1].strip() if len(parts) > 1 else ""
230
+
231
+ if sub == "create":
232
+ if not proj_name:
233
+ print_error("Usage: /project create <name>")
234
+ return CommandResult(handled=True)
235
+
236
+ from devobin.storage.database import ProjectStorage
237
+ ProjectStorage(proj_name) # creates dirs
238
+ config.set("active_project", proj_name)
239
+ print_success(f"Created project: {proj_name}")
240
+
241
+ elif sub == "switch":
242
+ if not proj_name:
243
+ print_error("Usage: /project switch <name>")
244
+ return CommandResult(handled=True)
245
+
246
+ projects = [p.name for p in config.projects_dir().iterdir() if p.is_dir()]
247
+ if proj_name not in projects:
248
+ print_error(f"Project '{proj_name}' not found. Available: {', '.join(projects)}")
249
+ return CommandResult(handled=True)
250
+
251
+ config.set("active_project", proj_name)
252
+ print_success(f"Switched to project: {proj_name}")
253
+
254
+ elif sub == "list":
255
+ projects = [p.name for p in config.projects_dir().iterdir() if p.is_dir()]
256
+ if not projects:
257
+ print_status("No projects yet. Use /project create <name> to start.")
258
+ else:
259
+ console.print()
260
+ console.print(" [bold cyan]Projects:[/]")
261
+ active = config.get("active_project")
262
+ for p in sorted(projects):
263
+ marker = " [green]← active[/]" if p == active else ""
264
+ console.print(f" • {p}{marker}")
265
+ console.print()
266
+
267
+ else:
268
+ console.print(" [bold]Project Commands:[/]")
269
+ console.print(" /project create <name> — Create a new project")
270
+ console.print(" /project switch <name> — Switch to a project")
271
+ console.print(" /project list — List all projects")
272
+
273
+ return CommandResult(handled=True)
274
+
275
+
276
+ async def _cmd_export(args: str) -> CommandResult:
277
+ """Generate the project_prompt.md file."""
278
+ from devobin.cli.ui import render_markdown
279
+
280
+ config = get_config()
281
+ project = config.get("active_project")
282
+
283
+ if not project:
284
+ print_warning("No active project. Use /project create <name> first.")
285
+ return CommandResult(handled=True)
286
+
287
+ from devobin.storage.database import ProjectStorage
288
+
289
+ storage = ProjectStorage(project)
290
+ history = storage.get_history()
291
+
292
+ if not history:
293
+ print_warning("No conversation history to export.")
294
+ return CommandResult(handled=True)
295
+
296
+ # Re-analyze and build prompt from history
297
+ from devobin.core.analyzer import analyze_input
298
+ from devobin.core.researcher import research_analysis
299
+ from devobin.core.prompt_builder import build_prompt
300
+
301
+ # Combine all user messages for analysis
302
+ user_msgs = " ".join(m["content"] for m in history if m["role"] == "user")
303
+ analysis = analyze_input(user_msgs)
304
+ research = research_analysis(analysis)
305
+
306
+ from devobin.core.memory import MemoryManager
307
+ mm = MemoryManager(project)
308
+ memory_ctx = mm.get_memory_context()
309
+
310
+ from devobin.core.scanner import scan_workspace
311
+ pm = scan_workspace()
312
+
313
+ _, prompt_content = build_prompt(analysis, research, memory_context=memory_ctx, project_map=pm)
314
+
315
+ # Save to CWD with the canonical filename
316
+ import os
317
+ output_path = os.path.join(os.getcwd(), "devobin_prompt.md")
318
+ output_path.write_text(prompt_content, encoding="utf-8")
319
+
320
+ print_success(f"Generated: {output_path}")
321
+ console.print()
322
+ render_markdown(prompt_content[:2000])
323
+ if len(prompt_content) > 2000:
324
+ console.print("\n [dim]... (truncated preview)[/]")
325
+ console.print()
326
+
327
+ return CommandResult(handled=True)
328
+
329
+
330
+ async def _cmd_settings(args: str) -> CommandResult:
331
+ """Show current settings."""
332
+ config = get_config()
333
+
334
+ console.print()
335
+ console.print(" [bold cyan]Current Settings:[/]")
336
+ console.print(f" Active provider: [bold]{config.active_provider or 'None'}[/]")
337
+ console.print(f" Active model: [bold]{config.active_model or 'None'}[/]")
338
+ console.print(f" Active project: [bold]{config.get('active_project') or 'None'}[/]")
339
+ console.print(f" Config dir: [dim]{CONFIG_DIR}[/]")
340
+ console.print()
341
+ return CommandResult(handled=True)
342
+
343
+
344
+ async def _cmd_clear(args: str) -> CommandResult:
345
+ """Clear current conversation."""
346
+ from devobin.storage.database import ProjectStorage
347
+ config = get_config()
348
+ project = config.get("active_project")
349
+ if project:
350
+ storage = ProjectStorage(project)
351
+ storage.clear_history()
352
+ print_success("Conversation cleared.")
353
+ else:
354
+ print_warning("No active project to clear.")
355
+ return CommandResult(handled=True)
356
+
357
+
358
+ async def _cmd_quit(args: str) -> CommandResult:
359
+ console.print()
360
+ print_success("Goodbye! Happy coding!")
361
+ console.print()
362
+ return CommandResult(handled=True, should_exit=True)
@@ -0,0 +1,74 @@
1
+ """Slash command menu system for DevObin AI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Callable
7
+
8
+ from devobin.cli.terminal import console, render_slash_menu
9
+
10
+
11
+ @dataclass
12
+ class SlashCommand:
13
+ """A slash command definition."""
14
+
15
+ name: str
16
+ description: str
17
+ handler: Callable[..., None]
18
+ requires_args: bool = False
19
+
20
+
21
+ class SlashMenu:
22
+ """Manages slash commands and their display."""
23
+
24
+ def __init__(self) -> None:
25
+ self._commands: dict[str, SlashCommand] = {}
26
+
27
+ def register(
28
+ self,
29
+ name: str,
30
+ description: str,
31
+ handler: Callable[..., None],
32
+ requires_args: bool = False,
33
+ ) -> None:
34
+ """Register a slash command."""
35
+ self._commands[name] = SlashCommand(
36
+ name=name,
37
+ description=description,
38
+ handler=handler,
39
+ requires_args=requires_args,
40
+ )
41
+
42
+ def get_command(self, name: str) -> SlashCommand | None:
43
+ """Get a command by name."""
44
+ return self._commands.get(name)
45
+
46
+ def show_menu(self) -> None:
47
+ """Display the slash command menu."""
48
+ render_slash_menu()
49
+
50
+ def parse_command(self, input_text: str) -> tuple[str, str]:
51
+ """Parse slash command input into (command, args)."""
52
+ parts = input_text.strip().split(maxsplit=1)
53
+ cmd = parts[0].lower() if parts else ""
54
+ args = parts[1] if len(parts) > 1 else ""
55
+ return cmd, args
56
+
57
+ def execute(self, input_text: str) -> bool:
58
+ """Execute a slash command. Returns True if command was handled."""
59
+ cmd, args = self.parse_command(input_text)
60
+
61
+ if cmd == "/":
62
+ self.show_menu()
63
+ return True
64
+
65
+ command = self.get_command(cmd)
66
+ if command:
67
+ command.handler(args)
68
+ return True
69
+
70
+ return False
71
+
72
+ @property
73
+ def commands(self) -> dict[str, SlashCommand]:
74
+ return self._commands
@@ -0,0 +1,155 @@
1
+ """Terminal rendering for DevObin AI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+ from rich.text import Text
9
+ from rich.columns import Columns
10
+ from rich import box
11
+
12
+ from devobin.config.settings import get_config
13
+
14
+ console = Console()
15
+
16
+
17
+ def render_banner() -> None:
18
+ """Render the main DevObin banner."""
19
+ banner = Text()
20
+ banner.append(" ██████╗ ███████╗██╗ ██╗███████╗██╗ ██████╗ ██╗ ██╗\n", style="bold cyan")
21
+ banner.append(" ██╔══██╗██╔════╝██║ ██║██╔════╝██║ ██╔═══██╗██║ ██║\n", style="bold cyan")
22
+ banner.append(" ██║ ██║█████╗ ██║ ██║█████╗ ██║ ██║ ██║██║ █╗ ██║\n", style="bold cyan")
23
+ banner.append(" ██║ ██║██╔══╝ ╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║██║███╗██║\n", style="bold cyan")
24
+ banner.append(" ██████╔╝███████╗ ╚████╔╝ ██║ ███████╗╚██████╔╝╚███╔███╔╝\n", style="bold cyan")
25
+ banner.append(" ╚═════╝ ╚══════╝ ╚═══╝ ╚═╝ ╚══════╝ ╚═════╝ ╚══╝╚══╝\n", style="bold cyan")
26
+
27
+ config = get_config()
28
+ model = config.active_model or "Not connected"
29
+ project = config.get("active_project") or "default"
30
+
31
+ status = Text()
32
+ status.append(f"\n Model: ", style="dim")
33
+ status.append(f"{model}\n", style="bold green")
34
+ status.append(f" Project: ", style="dim")
35
+ status.append(f"{project}\n", style="bold yellow")
36
+ status.append(f"\n Developer: mobin hasanghasemi", style="dim white")
37
+ status.append(f"\n GitHub: ", style="dim white")
38
+ status.append(f"https://github.com/mobinhasanghasemi", style="bold dim cyan")
39
+
40
+ content = Text()
41
+ content.append_text(banner)
42
+ content.append_text(status)
43
+
44
+ console.print(Panel(
45
+ content,
46
+ border_style="cyan",
47
+ padding=(0, 1),
48
+ box=box.DOUBLE,
49
+ ))
50
+
51
+
52
+ def render_slash_menu() -> None:
53
+ """Render the beautiful slash command menu."""
54
+ table = Table(
55
+ show_header=False,
56
+ show_edge=False,
57
+ box=None,
58
+ padding=(0, 2),
59
+ )
60
+ table.add_column("Command", style="bold cyan", width=12)
61
+ table.add_column("Description", style="white")
62
+
63
+ commands = [
64
+ ("/connect", "Connect AI provider"),
65
+ ("/model", "Change active model"),
66
+ ("/models", "List models"),
67
+ ("/project", "Manage projects"),
68
+ ("/memory", "View memory"),
69
+ ("/export", "Generate prompt file"),
70
+ ("/settings", "Settings"),
71
+ ("/run", "Run shell command"),
72
+ ("/read", "Read file contents"),
73
+ ("/write", "Write / create file"),
74
+ ("/ls", "List directory"),
75
+ ("/tree", "Show file tree"),
76
+ ("/git", "Git commands"),
77
+ ("/pip", "Pip commands"),
78
+ ("/python", "Run Python"),
79
+ ("/clear", "Clear chat"),
80
+ ("/exit", "Exit DevObin"),
81
+ ]
82
+
83
+ for cmd, desc in commands:
84
+ table.add_row(cmd, desc)
85
+
86
+ console.print(Panel(
87
+ table,
88
+ title="[bold cyan]DevObin Commands[/]",
89
+ border_style="cyan",
90
+ box=box.ROUNDED,
91
+ padding=(1, 2),
92
+ ))
93
+
94
+
95
+ def render_action_menu(actions: list[tuple[str, str]]) -> None:
96
+ """Render an action menu (like after generating context)."""
97
+ table = Table(
98
+ show_header=False,
99
+ show_edge=False,
100
+ box=None,
101
+ padding=(0, 1),
102
+ )
103
+ table.add_column("Choice", style="bold yellow", width=4)
104
+ table.add_column("Action", style="white")
105
+
106
+ for i, (action, desc) in enumerate(actions, 1):
107
+ table.add_row(f"[{i}]", f"{action} — {desc}")
108
+
109
+ console.print(Panel(
110
+ table,
111
+ title="[bold yellow]Actions[/]",
112
+ border_style="yellow",
113
+ box=box.ROUNDED,
114
+ padding=(0, 1),
115
+ ))
116
+
117
+
118
+ def render_status_bar() -> None:
119
+ """Render the status bar with model and project info."""
120
+ config = get_config()
121
+ model = config.active_model or "No model"
122
+ project = config.get("active_project") or "No project"
123
+
124
+ status = Text()
125
+ status.append(" ╭────────────────────────────────────────────╮\n", style="dim")
126
+ status.append(f" │ Model: ", style="dim")
127
+ status.append(f"{model:<30}", style="bold green")
128
+ status.append("│\n", style="dim")
129
+ status.append(f" │ Project: ", style="dim")
130
+ status.append(f"{project:<28}", style="bold yellow")
131
+ status.append("│\n", style="dim")
132
+ status.append(" ╰────────────────────────────────────────────╯\n", style="dim")
133
+
134
+ console.print(status)
135
+
136
+
137
+ def render_divider() -> None:
138
+ """Render a visual divider."""
139
+ console.print(" " + "─" * 44, style="dim")
140
+
141
+
142
+ def render_input_prefix() -> Text:
143
+ """Create the input prefix."""
144
+ prefix = Text()
145
+ prefix.append(" You ", style="bold cyan")
146
+ prefix.append("› ", style="bold white")
147
+ return prefix
148
+
149
+
150
+ def render_ai_prefix() -> Text:
151
+ """Create the AI response prefix."""
152
+ prefix = Text()
153
+ prefix.append(" DevObin ", style="bold green")
154
+ prefix.append("› ", style="bold white")
155
+ return prefix