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.
- devobin/__init__.py +4 -0
- devobin/app.py +42 -0
- devobin/cli/__init__.py +1 -0
- devobin/cli/chat.py +831 -0
- devobin/cli/commands.py +362 -0
- devobin/cli/slash_menu.py +74 -0
- devobin/cli/terminal.py +155 -0
- devobin/cli/ui.py +129 -0
- devobin/config/__init__.py +1 -0
- devobin/config/settings.py +124 -0
- devobin/core/__init__.py +1 -0
- devobin/core/analyzer.py +166 -0
- devobin/core/executor.py +244 -0
- devobin/core/memory.py +78 -0
- devobin/core/optimizer.py +106 -0
- devobin/core/prompt_builder.py +513 -0
- devobin/core/researcher.py +283 -0
- devobin/core/rtl.py +93 -0
- devobin/core/scanner.py +206 -0
- devobin/core/tools.py +283 -0
- devobin/core/validator.py +235 -0
- devobin/main.py +33 -0
- devobin/output/__init__.py +1 -0
- devobin/providers/__init__.py +85 -0
- devobin/providers/anthropic_provider.py +72 -0
- devobin/providers/google_provider.py +90 -0
- devobin/providers/ollama_provider.py +81 -0
- devobin/providers/openai_provider.py +102 -0
- devobin/storage/__init__.py +1 -0
- devobin/storage/database.py +207 -0
- devobin/ui/__init__.py +1 -0
- devobin/ui/ask_modal.py +96 -0
- devobin/ui/chat_screen.py +1029 -0
- devobin/ui/command_palette.py +131 -0
- devobin/ui/connect_modal.py +166 -0
- devobin/ui/export_modal.py +159 -0
- devobin/ui/history_modal.py +137 -0
- devobin/ui/memory_modal.py +88 -0
- devobin/ui/model_modal.py +143 -0
- devobin/ui/permission_modal.py +92 -0
- devobin/ui/project_modal.py +133 -0
- devobin/ui/settings_modal.py +89 -0
- devobin-1.0.0.dist-info/METADATA +364 -0
- devobin-1.0.0.dist-info/RECORD +47 -0
- devobin-1.0.0.dist-info/WHEEL +4 -0
- devobin-1.0.0.dist-info/entry_points.txt +2 -0
- devobin-1.0.0.dist-info/licenses/LICENSE +21 -0
devobin/cli/chat.py
ADDED
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
"""Interactive chat system for DevObin AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from prompt_toolkit import PromptSession
|
|
9
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
10
|
+
from prompt_toolkit.history import InMemoryHistory
|
|
11
|
+
|
|
12
|
+
from devobin.cli.slash_menu import SlashMenu
|
|
13
|
+
from devobin.cli.terminal import (
|
|
14
|
+
console,
|
|
15
|
+
render_banner,
|
|
16
|
+
render_status_bar,
|
|
17
|
+
render_divider,
|
|
18
|
+
render_action_menu,
|
|
19
|
+
)
|
|
20
|
+
from devobin.cli.ui import (
|
|
21
|
+
print_detected,
|
|
22
|
+
print_error,
|
|
23
|
+
print_success,
|
|
24
|
+
print_warning,
|
|
25
|
+
print_status,
|
|
26
|
+
)
|
|
27
|
+
from devobin.config.settings import get_config
|
|
28
|
+
from devobin.core.analyzer import analyze_input
|
|
29
|
+
from devobin.core.memory import MemoryManager
|
|
30
|
+
from devobin.core.optimizer import compress_context
|
|
31
|
+
from devobin.core.executor import get_executor, get_file_manager
|
|
32
|
+
from devobin.core.rtl import smart_display
|
|
33
|
+
from devobin.storage.database import ProjectStorage
|
|
34
|
+
|
|
35
|
+
SYSTEM_PROMPT = """You are DevObin — an AI Prompt Engineering System.
|
|
36
|
+
|
|
37
|
+
Your ROLE has changed. You are a PROMPT ARCHITECT, not a CODER.
|
|
38
|
+
You create instructions FOR other AI agents to implement. You do NOT write the implementation yourself.
|
|
39
|
+
|
|
40
|
+
## STRICT RULES
|
|
41
|
+
|
|
42
|
+
1. NEVER generate complete source code
|
|
43
|
+
2. NEVER generate complete files
|
|
44
|
+
3. NEVER generate full applications
|
|
45
|
+
4. NEVER provide production-ready implementations
|
|
46
|
+
5. NEVER output code blocks (```html, ```css, ```js, ```python, etc.)
|
|
47
|
+
|
|
48
|
+
## ALLOWED
|
|
49
|
+
|
|
50
|
+
- Architecture explanations
|
|
51
|
+
- Technical requirements
|
|
52
|
+
- Engineering instructions
|
|
53
|
+
- Design patterns
|
|
54
|
+
- Short educational snippets (max 3 lines)
|
|
55
|
+
- Pseudocode
|
|
56
|
+
- Feature descriptions
|
|
57
|
+
- Responsive design specs
|
|
58
|
+
- UI/UX requirements
|
|
59
|
+
- Performance rules
|
|
60
|
+
- Security rules
|
|
61
|
+
|
|
62
|
+
## FORBIDDEN
|
|
63
|
+
|
|
64
|
+
- Full HTML files
|
|
65
|
+
- Full CSS files
|
|
66
|
+
- Full JavaScript files
|
|
67
|
+
- Full Python files
|
|
68
|
+
- Complete React/Vue/Angular components
|
|
69
|
+
- Complete projects
|
|
70
|
+
- Multi-file implementations
|
|
71
|
+
- Any code that could be directly executed
|
|
72
|
+
|
|
73
|
+
## IF USER REQUESTS CODE
|
|
74
|
+
|
|
75
|
+
Do NOT comply.
|
|
76
|
+
|
|
77
|
+
Instead respond:
|
|
78
|
+
"I am DevObin — a prompt engineering tool. I create engineering instructions for AI coding agents. I do NOT write code.
|
|
79
|
+
|
|
80
|
+
Use /export to generate your engineering document, then give it to Codex/Claude/Cursor to implement."
|
|
81
|
+
|
|
82
|
+
## OUTPUT FORMAT
|
|
83
|
+
|
|
84
|
+
Your output must be structured text with these sections:
|
|
85
|
+
- Role (who the coding agent should be)
|
|
86
|
+
- Project Goal
|
|
87
|
+
- Technical Constraints
|
|
88
|
+
- Required Sections
|
|
89
|
+
- Implementation Details
|
|
90
|
+
- Responsive Design
|
|
91
|
+
- UI/UX Quality
|
|
92
|
+
- Performance Rules
|
|
93
|
+
|
|
94
|
+
Your output is optimized as a PROMPT for coding agents, NOT as code itself.
|
|
95
|
+
|
|
96
|
+
## AVAILABLE COMMANDS
|
|
97
|
+
|
|
98
|
+
User commands: /run, /read, /write, /ls, /export, /connect, /model, /project, /memory"""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class DevObinCompleter(Completer):
|
|
102
|
+
"""Autocomplete for slash commands."""
|
|
103
|
+
|
|
104
|
+
COMMANDS = [
|
|
105
|
+
"/connect", "/model", "/models", "/project", "/memory",
|
|
106
|
+
"/history", "/export", "/research", "/settings", "/clear",
|
|
107
|
+
"/help", "/exit", "/quit",
|
|
108
|
+
"/run", "/exec", "/read", "/cat", "/write", "/create",
|
|
109
|
+
"/ls", "/dir", "/tree", "/cwd", "/cd", "/rm", "/delete",
|
|
110
|
+
"/touch", "/mkdir", "/env", "/pip", "/npm", "/git", "/python",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
def get_completions(self, document, complete_event):
|
|
114
|
+
text = document.text_before_cursor
|
|
115
|
+
if text.startswith("/"):
|
|
116
|
+
word = text.lower()
|
|
117
|
+
for cmd in self.COMMANDS:
|
|
118
|
+
if cmd.startswith(word):
|
|
119
|
+
yield Completion(cmd, start_position=-len(text))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ChatSession:
|
|
123
|
+
"""Manages an interactive chat session with the AI."""
|
|
124
|
+
|
|
125
|
+
def __init__(self) -> None:
|
|
126
|
+
self.config = get_config()
|
|
127
|
+
self.project = self.config.get("active_project")
|
|
128
|
+
self.memory = MemoryManager(self.project)
|
|
129
|
+
self.storage = ProjectStorage(self.project) if self.project else None
|
|
130
|
+
self.running = False
|
|
131
|
+
self.slash_menu = SlashMenu()
|
|
132
|
+
self._last_ai_response: str = ""
|
|
133
|
+
self._register_commands()
|
|
134
|
+
self._session = PromptSession(
|
|
135
|
+
completer=DevObinCompleter(),
|
|
136
|
+
history=InMemoryHistory(),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _register_commands(self) -> None:
|
|
140
|
+
"""Register all slash commands."""
|
|
141
|
+
self.slash_menu.register("/help", "Show commands", self._cmd_help)
|
|
142
|
+
self.slash_menu.register("/connect", "Connect AI provider", self._cmd_connect)
|
|
143
|
+
self.slash_menu.register("/models", "List models", self._cmd_models)
|
|
144
|
+
self.slash_menu.register("/model", "Change model", self._cmd_model, requires_args=True)
|
|
145
|
+
self.slash_menu.register("/project", "Manage projects", self._cmd_project)
|
|
146
|
+
self.slash_menu.register("/memory", "View memory", self._cmd_memory)
|
|
147
|
+
self.slash_menu.register("/export", "Generate prompt file", self._cmd_export)
|
|
148
|
+
self.slash_menu.register("/settings", "Settings", self._cmd_settings)
|
|
149
|
+
self.slash_menu.register("/clear", "Clear chat", self._cmd_clear)
|
|
150
|
+
self.slash_menu.register("/exit", "Exit DevObin", self._cmd_exit)
|
|
151
|
+
self.slash_menu.register("/quit", "Exit DevObin", self._cmd_exit)
|
|
152
|
+
# Shell & file commands
|
|
153
|
+
self.slash_menu.register("/run", "Run shell command", self._cmd_run)
|
|
154
|
+
self.slash_menu.register("/exec", "Run shell command", self._cmd_run)
|
|
155
|
+
self.slash_menu.register("/read", "Read file", self._cmd_read)
|
|
156
|
+
self.slash_menu.register("/cat", "Read file", self._cmd_read)
|
|
157
|
+
self.slash_menu.register("/write", "Write/create file", self._cmd_write)
|
|
158
|
+
self.slash_menu.register("/create", "Write/create file", self._cmd_write)
|
|
159
|
+
self.slash_menu.register("/ls", "List directory", self._cmd_ls)
|
|
160
|
+
self.slash_menu.register("/dir", "List directory", self._cmd_ls)
|
|
161
|
+
self.slash_menu.register("/tree", "Show file tree", self._cmd_tree)
|
|
162
|
+
self.slash_menu.register("/cwd", "Show current directory", self._cmd_cwd)
|
|
163
|
+
self.slash_menu.register("/cd", "Change directory", self._cmd_cd)
|
|
164
|
+
self.slash_menu.register("/rm", "Delete file", self._cmd_rm)
|
|
165
|
+
self.slash_menu.register("/delete", "Delete file", self._cmd_rm)
|
|
166
|
+
self.slash_menu.register("/touch", "Create empty file", self._cmd_touch)
|
|
167
|
+
self.slash_menu.register("/mkdir", "Create directory", self._cmd_mkdir)
|
|
168
|
+
self.slash_menu.register("/env", "Show environment", self._cmd_env)
|
|
169
|
+
self.slash_menu.register("/pip", "Pip commands", self._cmd_pip)
|
|
170
|
+
self.slash_menu.register("/npm", "Npm commands", self._cmd_npm)
|
|
171
|
+
self.slash_menu.register("/git", "Git commands", self._cmd_git)
|
|
172
|
+
self.slash_menu.register("/python", "Run Python", self._cmd_python)
|
|
173
|
+
|
|
174
|
+
def start(self) -> None:
|
|
175
|
+
"""Start the interactive chat loop."""
|
|
176
|
+
render_banner()
|
|
177
|
+
render_status_bar()
|
|
178
|
+
render_divider()
|
|
179
|
+
console.print()
|
|
180
|
+
|
|
181
|
+
if not self.config.active_provider:
|
|
182
|
+
console.print(" [bold cyan]Welcome to DevObin AI![/]")
|
|
183
|
+
console.print(" [dim]Type /connect to set up your AI model.[/]")
|
|
184
|
+
console.print(" [dim]Type / to see all commands.[/]")
|
|
185
|
+
console.print()
|
|
186
|
+
else:
|
|
187
|
+
console.print(" [bold cyan]Ready![/]")
|
|
188
|
+
console.print(" [dim]Type your project idea or / for commands.[/]")
|
|
189
|
+
console.print()
|
|
190
|
+
|
|
191
|
+
self.running = True
|
|
192
|
+
|
|
193
|
+
while self.running:
|
|
194
|
+
try:
|
|
195
|
+
user_input = self._get_input()
|
|
196
|
+
if user_input is None:
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
user_input = user_input.strip()
|
|
200
|
+
if not user_input:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
if user_input.startswith("/"):
|
|
204
|
+
self._execute_command(user_input)
|
|
205
|
+
self._refresh_state()
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
self._process_message(user_input)
|
|
209
|
+
|
|
210
|
+
except KeyboardInterrupt:
|
|
211
|
+
console.print()
|
|
212
|
+
print_status("Use [bold]/exit[/] to quit.")
|
|
213
|
+
except EOFError:
|
|
214
|
+
self.running = False
|
|
215
|
+
|
|
216
|
+
def _get_input(self) -> str | None:
|
|
217
|
+
"""Get user input using prompt-toolkit."""
|
|
218
|
+
try:
|
|
219
|
+
return self._session.prompt("\n You › ")
|
|
220
|
+
except (KeyboardInterrupt, EOFError):
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
def _execute_command(self, input_text: str) -> None:
|
|
224
|
+
"""Execute a slash command synchronously."""
|
|
225
|
+
parts = input_text.strip().split(maxsplit=1)
|
|
226
|
+
cmd = parts[0].lower() if parts else ""
|
|
227
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
228
|
+
|
|
229
|
+
command = self.slash_menu.get_command(cmd)
|
|
230
|
+
if command:
|
|
231
|
+
if asyncio.iscoroutinefunction(command.handler):
|
|
232
|
+
asyncio.run(command.handler(args))
|
|
233
|
+
else:
|
|
234
|
+
command.handler(args)
|
|
235
|
+
elif cmd == "/":
|
|
236
|
+
self.slash_menu.show_menu()
|
|
237
|
+
else:
|
|
238
|
+
print_error(f"Unknown command: {cmd}")
|
|
239
|
+
|
|
240
|
+
def _refresh_state(self) -> None:
|
|
241
|
+
"""Refresh project and memory references after commands."""
|
|
242
|
+
self.project = self.config.get("active_project")
|
|
243
|
+
self.memory = MemoryManager(self.project)
|
|
244
|
+
self.storage = ProjectStorage(self.project) if self.project else None
|
|
245
|
+
|
|
246
|
+
def _process_message(self, user_input: str) -> None:
|
|
247
|
+
"""Process a user message through the AI pipeline."""
|
|
248
|
+
if self.storage:
|
|
249
|
+
self.storage.add_message("user", user_input)
|
|
250
|
+
|
|
251
|
+
analysis = analyze_input(user_input)
|
|
252
|
+
|
|
253
|
+
if analysis.detected_techs:
|
|
254
|
+
tech_names = list(set(t.name for t in analysis.detected_techs))
|
|
255
|
+
print_detected(tech_names, "Detected")
|
|
256
|
+
console.print()
|
|
257
|
+
|
|
258
|
+
if self.config.active_provider:
|
|
259
|
+
self._chat_with_ai(user_input, analysis)
|
|
260
|
+
else:
|
|
261
|
+
self._local_analysis(user_input, analysis)
|
|
262
|
+
|
|
263
|
+
def _chat_with_ai(self, user_input: str, analysis) -> None:
|
|
264
|
+
"""Chat with the configured AI provider."""
|
|
265
|
+
from devobin.providers import get_provider
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
provider = get_provider()
|
|
269
|
+
except ValueError as e:
|
|
270
|
+
print_error(str(e))
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
messages = []
|
|
274
|
+
if self.storage:
|
|
275
|
+
messages = self.storage.get_context_messages(max_messages=20)
|
|
276
|
+
|
|
277
|
+
memory_ctx = self.memory.get_memory_context()
|
|
278
|
+
system = SYSTEM_PROMPT
|
|
279
|
+
if memory_ctx:
|
|
280
|
+
system += f"\n\n## User Preferences & Context\n{memory_ctx}"
|
|
281
|
+
|
|
282
|
+
messages = compress_context(messages, max_tokens=6000)
|
|
283
|
+
|
|
284
|
+
console.print(" [bold green]DevObin[/] ", end="")
|
|
285
|
+
|
|
286
|
+
async def _stream():
|
|
287
|
+
full_response = ""
|
|
288
|
+
async for chunk in provider.chat_stream(
|
|
289
|
+
messages=messages,
|
|
290
|
+
system=system,
|
|
291
|
+
temperature=0.7,
|
|
292
|
+
max_tokens=8192,
|
|
293
|
+
):
|
|
294
|
+
full_response += chunk
|
|
295
|
+
# Show raw during streaming
|
|
296
|
+
print(chunk, end="", flush=True)
|
|
297
|
+
# After streaming, reshape RTL text
|
|
298
|
+
reshaped = smart_display(full_response)
|
|
299
|
+
if reshaped != full_response:
|
|
300
|
+
# Clear and reprint reshaped version
|
|
301
|
+
print()
|
|
302
|
+
print(f" {reshaped}")
|
|
303
|
+
return full_response
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
full_response = asyncio.run(_stream())
|
|
307
|
+
console.print()
|
|
308
|
+
console.print()
|
|
309
|
+
|
|
310
|
+
self._last_ai_response = full_response
|
|
311
|
+
|
|
312
|
+
if self.storage:
|
|
313
|
+
self.storage.add_message("assistant", full_response)
|
|
314
|
+
|
|
315
|
+
self._extract_memories(user_input, full_response)
|
|
316
|
+
|
|
317
|
+
render_action_menu([
|
|
318
|
+
("Export Prompt", "Generate project_prompt.md"),
|
|
319
|
+
("Continue", "Keep refining context"),
|
|
320
|
+
("Add Rules", "Add architecture/security rules"),
|
|
321
|
+
])
|
|
322
|
+
console.print()
|
|
323
|
+
|
|
324
|
+
except Exception as e:
|
|
325
|
+
console.print()
|
|
326
|
+
print_error(f"AI request failed: {e}")
|
|
327
|
+
|
|
328
|
+
def _local_analysis(self, user_input: str, analysis) -> None:
|
|
329
|
+
"""Perform local analysis without an AI provider."""
|
|
330
|
+
console.print()
|
|
331
|
+
|
|
332
|
+
if analysis.detected_techs:
|
|
333
|
+
from devobin.core.researcher import research_analysis
|
|
334
|
+
research = research_analysis(analysis)
|
|
335
|
+
|
|
336
|
+
for name, topic in research.items():
|
|
337
|
+
if topic.best_practices:
|
|
338
|
+
console.print(f" [bold cyan]{name}[/] best practices:")
|
|
339
|
+
for bp in topic.best_practices[:3]:
|
|
340
|
+
console.print(f" • {bp}")
|
|
341
|
+
console.print()
|
|
342
|
+
|
|
343
|
+
if analysis.requirements:
|
|
344
|
+
console.print(" [bold]Key Requirements:[/]")
|
|
345
|
+
for req in analysis.requirements:
|
|
346
|
+
console.print(f" → {req}")
|
|
347
|
+
console.print()
|
|
348
|
+
|
|
349
|
+
print_status("Connect an AI model with [bold]/connect[/] for full interactive experience.")
|
|
350
|
+
print_status("Use [bold]/export[/] to generate the project_prompt.md file.")
|
|
351
|
+
else:
|
|
352
|
+
print_status("I can help you define your project. Try describing:")
|
|
353
|
+
console.print(" • What you want to build")
|
|
354
|
+
console.print(" • Technologies you want to use")
|
|
355
|
+
console.print(" • Scale requirements")
|
|
356
|
+
console.print()
|
|
357
|
+
print_status("Example: [bold]I want to create a Django ecommerce platform with React[/]")
|
|
358
|
+
|
|
359
|
+
console.print()
|
|
360
|
+
|
|
361
|
+
def _extract_memories(self, user_input: str, response: str) -> None:
|
|
362
|
+
"""Extract and save memories from conversation."""
|
|
363
|
+
preference_patterns = [
|
|
364
|
+
"i prefer", "i always use", "i like to use", "my preferred",
|
|
365
|
+
"i want to use", "let's use", "we should use", "i use",
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
lower_input = user_input.lower()
|
|
369
|
+
for pattern in preference_patterns:
|
|
370
|
+
if pattern in lower_input:
|
|
371
|
+
self.memory.remember(user_input, scope="global", category="preference")
|
|
372
|
+
break
|
|
373
|
+
|
|
374
|
+
analysis = analyze_input(user_input)
|
|
375
|
+
if analysis.detected_techs:
|
|
376
|
+
for tech in analysis.detected_techs:
|
|
377
|
+
if any(kw in lower_input for kw in ["use", "with", "using", "built with", "stack"]):
|
|
378
|
+
self.memory.remember(
|
|
379
|
+
f"Technology choice: {tech.name} ({tech.category})",
|
|
380
|
+
scope="project",
|
|
381
|
+
category="technology",
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# ── Slash Command Handlers ────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
def _cmd_help(self, args: str) -> None:
|
|
387
|
+
from devobin.cli.ui import print_help
|
|
388
|
+
print_help()
|
|
389
|
+
|
|
390
|
+
def _cmd_connect(self, args: str) -> None:
|
|
391
|
+
"""Interactive provider connection wizard."""
|
|
392
|
+
from rich.prompt import Prompt
|
|
393
|
+
|
|
394
|
+
config = get_config()
|
|
395
|
+
|
|
396
|
+
console.print()
|
|
397
|
+
console.print(" [bold cyan]Connect AI Provider[/]")
|
|
398
|
+
console.print()
|
|
399
|
+
|
|
400
|
+
provider_types = ["openai", "anthropic", "google", "ollama", "openai-compatible"]
|
|
401
|
+
console.print(" Provider types:")
|
|
402
|
+
for i, pt in enumerate(provider_types, 1):
|
|
403
|
+
console.print(f" {i}. {pt}")
|
|
404
|
+
console.print()
|
|
405
|
+
|
|
406
|
+
choice = Prompt.ask(" Select", choices=[str(i) for i in range(1, 6)], default="1")
|
|
407
|
+
provider_type = provider_types[int(choice) - 1]
|
|
408
|
+
|
|
409
|
+
console.print()
|
|
410
|
+
name = Prompt.ask(" Name", default=provider_type)
|
|
411
|
+
api_key = Prompt.ask(" API key", password=True, default="")
|
|
412
|
+
|
|
413
|
+
base_url = ""
|
|
414
|
+
if provider_type == "openai-compatible":
|
|
415
|
+
base_url = Prompt.ask(" Base URL", default="https://api.openai.com/v1")
|
|
416
|
+
elif provider_type == "ollama":
|
|
417
|
+
base_url = Prompt.ask(" Ollama URL", default="http://localhost:11434")
|
|
418
|
+
|
|
419
|
+
temp_config = {
|
|
420
|
+
"api_key": api_key,
|
|
421
|
+
"base_url": base_url or None,
|
|
422
|
+
"model": "",
|
|
423
|
+
"type": provider_type,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
model = ""
|
|
427
|
+
try:
|
|
428
|
+
if provider_type in ("openai", "openai-compatible"):
|
|
429
|
+
from devobin.providers.openai_provider import OpenAIProvider
|
|
430
|
+
temp_config["model"] = "gpt-4o"
|
|
431
|
+
p = OpenAIProvider(temp_config)
|
|
432
|
+
models = p.available_models()
|
|
433
|
+
elif provider_type == "anthropic":
|
|
434
|
+
from devobin.providers.anthropic_provider import AnthropicProvider
|
|
435
|
+
temp_config["model"] = "claude-sonnet-4-20250514"
|
|
436
|
+
p = AnthropicProvider(temp_config)
|
|
437
|
+
models = p.available_models()
|
|
438
|
+
elif provider_type == "google":
|
|
439
|
+
from devobin.providers.google_provider import GoogleProvider
|
|
440
|
+
temp_config["model"] = "gemini-2.0-flash"
|
|
441
|
+
p = GoogleProvider(temp_config)
|
|
442
|
+
models = p.available_models()
|
|
443
|
+
elif provider_type == "ollama":
|
|
444
|
+
from devobin.providers.ollama_provider import OllamaProvider
|
|
445
|
+
temp_config["model"] = "llama3"
|
|
446
|
+
p = OllamaProvider(temp_config)
|
|
447
|
+
models = p.available_models()
|
|
448
|
+
else:
|
|
449
|
+
models = []
|
|
450
|
+
except Exception:
|
|
451
|
+
models = []
|
|
452
|
+
|
|
453
|
+
if models:
|
|
454
|
+
console.print()
|
|
455
|
+
console.print(" Available models:")
|
|
456
|
+
for i, m in enumerate(models, 1):
|
|
457
|
+
console.print(f" {i}. {m}")
|
|
458
|
+
console.print()
|
|
459
|
+
model_choice = Prompt.ask(" Select model (number or name)", default="1")
|
|
460
|
+
try:
|
|
461
|
+
idx = int(model_choice) - 1
|
|
462
|
+
model = models[idx] if 0 <= idx < len(models) else model_choice
|
|
463
|
+
except ValueError:
|
|
464
|
+
model = model_choice
|
|
465
|
+
else:
|
|
466
|
+
model = Prompt.ask(" Model name", default="gpt-4o")
|
|
467
|
+
|
|
468
|
+
provider_config = {
|
|
469
|
+
"type": provider_type,
|
|
470
|
+
"api_key": api_key,
|
|
471
|
+
"base_url": base_url or None,
|
|
472
|
+
"model": model,
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
config.add_provider(name, provider_config)
|
|
476
|
+
config.active_provider = name
|
|
477
|
+
config.active_model = model
|
|
478
|
+
|
|
479
|
+
console.print()
|
|
480
|
+
print_success(f"Connected to {name} ({provider_type}) — model: {model}")
|
|
481
|
+
console.print()
|
|
482
|
+
|
|
483
|
+
def _cmd_models(self, args: str) -> None:
|
|
484
|
+
from devobin.cli.ui import print_provider_table
|
|
485
|
+
config = get_config()
|
|
486
|
+
providers = config.list_providers()
|
|
487
|
+
if not providers:
|
|
488
|
+
print_warning("No providers configured. Use /connect to set one up.")
|
|
489
|
+
return
|
|
490
|
+
print_provider_table(providers)
|
|
491
|
+
|
|
492
|
+
def _cmd_model(self, args: str) -> None:
|
|
493
|
+
config = get_config()
|
|
494
|
+
if not args:
|
|
495
|
+
current = config.active_model or "None"
|
|
496
|
+
print_status(f"Current model: [bold]{current}[/]")
|
|
497
|
+
return
|
|
498
|
+
config.active_model = args.strip()
|
|
499
|
+
print_success(f"Switched to model: {args.strip()}")
|
|
500
|
+
|
|
501
|
+
def _cmd_project(self, args: str) -> None:
|
|
502
|
+
config = get_config()
|
|
503
|
+
parts = args.strip().split(maxsplit=1)
|
|
504
|
+
sub = parts[0].lower() if parts else ""
|
|
505
|
+
proj_name = parts[1].strip() if len(parts) > 1 else ""
|
|
506
|
+
|
|
507
|
+
if sub == "create":
|
|
508
|
+
if not proj_name:
|
|
509
|
+
print_error("Usage: /project create <name>")
|
|
510
|
+
return
|
|
511
|
+
ProjectStorage(proj_name)
|
|
512
|
+
config.set("active_project", proj_name)
|
|
513
|
+
print_success(f"Created project: {proj_name}")
|
|
514
|
+
|
|
515
|
+
elif sub == "switch":
|
|
516
|
+
if not proj_name:
|
|
517
|
+
print_error("Usage: /project switch <name>")
|
|
518
|
+
return
|
|
519
|
+
projects = [p.name for p in config.projects_dir().iterdir() if p.is_dir()]
|
|
520
|
+
if proj_name not in projects:
|
|
521
|
+
print_error(f"Project '{proj_name}' not found.")
|
|
522
|
+
return
|
|
523
|
+
config.set("active_project", proj_name)
|
|
524
|
+
print_success(f"Switched to project: {proj_name}")
|
|
525
|
+
|
|
526
|
+
elif sub == "list":
|
|
527
|
+
projects = [p.name for p in config.projects_dir().iterdir() if p.is_dir()]
|
|
528
|
+
if not projects:
|
|
529
|
+
print_status("No projects yet. Use /project create <name> to start.")
|
|
530
|
+
else:
|
|
531
|
+
console.print()
|
|
532
|
+
console.print(" [bold cyan]Projects:[/]")
|
|
533
|
+
active = config.get("active_project")
|
|
534
|
+
for p in sorted(projects):
|
|
535
|
+
marker = " [green]← active[/]" if p == active else ""
|
|
536
|
+
console.print(f" • {p}{marker}")
|
|
537
|
+
console.print()
|
|
538
|
+
else:
|
|
539
|
+
console.print(" [bold]Project Commands:[/]")
|
|
540
|
+
console.print(" /project create <name>")
|
|
541
|
+
console.print(" /project switch <name>")
|
|
542
|
+
console.print(" /project list")
|
|
543
|
+
|
|
544
|
+
def _cmd_memory(self, args: str) -> None:
|
|
545
|
+
global_mems = self.memory.get_global_memories()
|
|
546
|
+
project_mems = self.memory.get_project_memories()
|
|
547
|
+
|
|
548
|
+
if not global_mems and not project_mems:
|
|
549
|
+
print_status("No memories saved yet.")
|
|
550
|
+
return
|
|
551
|
+
|
|
552
|
+
if global_mems:
|
|
553
|
+
console.print()
|
|
554
|
+
console.print(" [bold cyan]Global Memories:[/]")
|
|
555
|
+
for m in global_mems[-15:]:
|
|
556
|
+
cat = m.get("category", "general")
|
|
557
|
+
console.print(f" [dim][{cat}][/dim] {m['content'][:100]}")
|
|
558
|
+
|
|
559
|
+
if project_mems:
|
|
560
|
+
console.print()
|
|
561
|
+
console.print(" [bold cyan]Project Memories:[/]")
|
|
562
|
+
for m in project_mems[-15:]:
|
|
563
|
+
cat = m.get("category", "general")
|
|
564
|
+
console.print(f" [dim][{cat}][/dim] {m['content'][:100]}")
|
|
565
|
+
|
|
566
|
+
console.print()
|
|
567
|
+
|
|
568
|
+
def _cmd_export(self, args: str) -> None:
|
|
569
|
+
"""Generate the project prompt file. Supports /export [filename]."""
|
|
570
|
+
from devobin.cli.ui import render_markdown
|
|
571
|
+
|
|
572
|
+
config = get_config()
|
|
573
|
+
project = config.get("active_project")
|
|
574
|
+
custom_name = args.strip() if args.strip() else None
|
|
575
|
+
|
|
576
|
+
# Use the actual AI response if available
|
|
577
|
+
if self._last_ai_response and self._last_ai_response.strip():
|
|
578
|
+
prompt_content = self._last_ai_response.strip()
|
|
579
|
+
name = custom_name or project or "project"
|
|
580
|
+
slug = name.lower().strip()
|
|
581
|
+
slug = slug.replace(" ", "_").replace("-", "_")
|
|
582
|
+
slug = "".join(c for c in slug if c.isalnum() or c == "_")
|
|
583
|
+
filename = f"{slug}_prompt.md"
|
|
584
|
+
else:
|
|
585
|
+
# Fallback to prompt_builder template
|
|
586
|
+
if not project:
|
|
587
|
+
print_warning("No active project. Use /project create <name> first.")
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
storage = ProjectStorage(project)
|
|
591
|
+
history = storage.get_history()
|
|
592
|
+
|
|
593
|
+
if not history:
|
|
594
|
+
print_warning("No conversation history to export.")
|
|
595
|
+
return
|
|
596
|
+
|
|
597
|
+
from devobin.core.researcher import research_analysis
|
|
598
|
+
from devobin.core.prompt_builder import build_prompt
|
|
599
|
+
|
|
600
|
+
user_msgs = " ".join(m["content"] for m in history if m["role"] == "user")
|
|
601
|
+
analysis = analyze_input(user_msgs)
|
|
602
|
+
research = research_analysis(analysis)
|
|
603
|
+
|
|
604
|
+
memory_ctx = self.memory.get_memory_context()
|
|
605
|
+
filename, prompt_content = build_prompt(
|
|
606
|
+
analysis, research,
|
|
607
|
+
memory_context=memory_ctx,
|
|
608
|
+
project_name=custom_name or project,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
# Save to current working directory (where CLI was opened)
|
|
612
|
+
cwd = os.getcwd()
|
|
613
|
+
output_path = os.path.join(cwd, filename)
|
|
614
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
615
|
+
f.write(prompt_content)
|
|
616
|
+
|
|
617
|
+
print_success(f"Created: {output_path}")
|
|
618
|
+
console.print()
|
|
619
|
+
render_markdown(prompt_content[:2000])
|
|
620
|
+
if len(prompt_content) > 2000:
|
|
621
|
+
console.print("\n [dim]... (truncated preview)[/]")
|
|
622
|
+
console.print()
|
|
623
|
+
|
|
624
|
+
def _cmd_settings(self, args: str) -> None:
|
|
625
|
+
from devobin.config.settings import CONFIG_DIR
|
|
626
|
+
config = get_config()
|
|
627
|
+
|
|
628
|
+
console.print()
|
|
629
|
+
console.print(" [bold cyan]Settings:[/]")
|
|
630
|
+
console.print(f" Provider: [bold]{config.active_provider or 'None'}[/]")
|
|
631
|
+
console.print(f" Model: [bold]{config.active_model or 'None'}[/]")
|
|
632
|
+
console.print(f" Project: [bold]{config.get('active_project') or 'None'}[/]")
|
|
633
|
+
console.print(f" Config: [dim]{CONFIG_DIR}[/]")
|
|
634
|
+
console.print()
|
|
635
|
+
|
|
636
|
+
def _cmd_clear(self, args: str) -> None:
|
|
637
|
+
if self.storage:
|
|
638
|
+
self.storage.clear_history()
|
|
639
|
+
print_success("Conversation cleared.")
|
|
640
|
+
else:
|
|
641
|
+
print_warning("No active project to clear.")
|
|
642
|
+
|
|
643
|
+
def _cmd_exit(self, args: str) -> None:
|
|
644
|
+
console.print()
|
|
645
|
+
print_success("Goodbye!")
|
|
646
|
+
console.print()
|
|
647
|
+
self.running = False
|
|
648
|
+
|
|
649
|
+
# ── Shell & File Commands ──────────────────────────────────────
|
|
650
|
+
|
|
651
|
+
def _cmd_run(self, args: str) -> None:
|
|
652
|
+
"""Execute a shell command."""
|
|
653
|
+
if not args.strip():
|
|
654
|
+
print_error("Usage: /run <command>")
|
|
655
|
+
return
|
|
656
|
+
executor = get_executor()
|
|
657
|
+
console.print(f" [dim]$ {escape(args)}[/]")
|
|
658
|
+
result = executor.run(args)
|
|
659
|
+
if result.stdout.strip():
|
|
660
|
+
console.print(f" {result.stdout.strip()}")
|
|
661
|
+
if result.stderr.strip():
|
|
662
|
+
console.print(f" [red]{result.stderr.strip()}[/]")
|
|
663
|
+
if result.timed_out:
|
|
664
|
+
console.print(" [red]Command timed out after 30s[/]")
|
|
665
|
+
if self.storage:
|
|
666
|
+
self.storage.add_message("user", f"/run {args}")
|
|
667
|
+
self.storage.add_message("assistant", f"[command output]\n{result.output}")
|
|
668
|
+
console.print()
|
|
669
|
+
|
|
670
|
+
def _cmd_read(self, args: str) -> None:
|
|
671
|
+
"""Read a file."""
|
|
672
|
+
if not args.strip():
|
|
673
|
+
print_error("Usage: /read <file_path>")
|
|
674
|
+
return
|
|
675
|
+
files = get_file_manager()
|
|
676
|
+
result = files.read(args.strip())
|
|
677
|
+
if result.success:
|
|
678
|
+
console.print(f" [dim]{result.path}[/] ({result.size} bytes)")
|
|
679
|
+
console.print()
|
|
680
|
+
for line in result.content.split("\n")[:50]:
|
|
681
|
+
console.print(f" {line}")
|
|
682
|
+
if result.content.count("\n") > 50:
|
|
683
|
+
console.print(f"\n [dim]... ({result.content.count(chr(10)) - 50} more lines)[/]")
|
|
684
|
+
else:
|
|
685
|
+
print_error(result.error)
|
|
686
|
+
if self.storage and result.success:
|
|
687
|
+
self.storage.add_message("user", f"/read {args}")
|
|
688
|
+
console.print()
|
|
689
|
+
|
|
690
|
+
def _cmd_write(self, args: str) -> None:
|
|
691
|
+
"""Write content to a file."""
|
|
692
|
+
if not args.strip():
|
|
693
|
+
print_error("Usage: /write <file_path> <content>")
|
|
694
|
+
return
|
|
695
|
+
parts = args.strip().split("\n", 1)
|
|
696
|
+
filepath = parts[0].strip()
|
|
697
|
+
content = parts[1] if len(parts) > 1 else ""
|
|
698
|
+
if not content:
|
|
699
|
+
print_error(f"Provide content after the filename.\nUsage: /write {filepath}\n<your content here>")
|
|
700
|
+
return
|
|
701
|
+
files = get_file_manager()
|
|
702
|
+
result = files.write(filepath, content)
|
|
703
|
+
if result.success:
|
|
704
|
+
print_success(f"Written {result.size} bytes to {result.path}")
|
|
705
|
+
else:
|
|
706
|
+
print_error(result.error)
|
|
707
|
+
console.print()
|
|
708
|
+
|
|
709
|
+
def _cmd_ls(self, args: str) -> None:
|
|
710
|
+
"""List directory contents."""
|
|
711
|
+
files = get_file_manager()
|
|
712
|
+
result = files.list_dir(args.strip() or ".")
|
|
713
|
+
if result.success:
|
|
714
|
+
console.print(f" [bold]{result.path}[/] ({len(result.files)} items)")
|
|
715
|
+
for f in result.files:
|
|
716
|
+
icon = "[blue]📁[/]" if f.endswith("/") else "📄"
|
|
717
|
+
console.print(f" {icon} {f}")
|
|
718
|
+
else:
|
|
719
|
+
print_error(result.error)
|
|
720
|
+
console.print()
|
|
721
|
+
|
|
722
|
+
def _cmd_tree(self, args: str) -> None:
|
|
723
|
+
"""Show project file tree."""
|
|
724
|
+
executor = get_executor()
|
|
725
|
+
path = args.strip() or "."
|
|
726
|
+
result = executor.run(
|
|
727
|
+
f'find "{path}" -not -path "*/.git/*" -not -path "*/__pycache__/*" '
|
|
728
|
+
f'-not -path "*/node_modules/*" -not -path "*/.venv/*" | head -50'
|
|
729
|
+
)
|
|
730
|
+
if result.stdout.strip():
|
|
731
|
+
console.print(f" [bold]{path}[/]")
|
|
732
|
+
for line in result.stdout.strip().split("\n"):
|
|
733
|
+
console.print(f" {line}")
|
|
734
|
+
else:
|
|
735
|
+
self._cmd_ls(args)
|
|
736
|
+
console.print()
|
|
737
|
+
|
|
738
|
+
def _cmd_cwd(self, _args: str = "") -> None:
|
|
739
|
+
"""Show current working directory."""
|
|
740
|
+
import os
|
|
741
|
+
console.print(f" [bold]cwd:[/] {os.getcwd()}")
|
|
742
|
+
console.print()
|
|
743
|
+
|
|
744
|
+
def _cmd_cd(self, args: str) -> None:
|
|
745
|
+
"""Change working directory."""
|
|
746
|
+
import os
|
|
747
|
+
if not args.strip():
|
|
748
|
+
print_error("Usage: /cd <path>")
|
|
749
|
+
return
|
|
750
|
+
try:
|
|
751
|
+
os.chdir(args.strip())
|
|
752
|
+
print_success(f"cwd: {os.getcwd()}")
|
|
753
|
+
except Exception as e:
|
|
754
|
+
print_error(f"cd failed: {e}")
|
|
755
|
+
console.print()
|
|
756
|
+
|
|
757
|
+
def _cmd_rm(self, args: str) -> None:
|
|
758
|
+
"""Delete a file."""
|
|
759
|
+
if not args.strip():
|
|
760
|
+
print_error("Usage: /rm <file_or_dir>")
|
|
761
|
+
return
|
|
762
|
+
files = get_file_manager()
|
|
763
|
+
result = files.delete(args.strip())
|
|
764
|
+
if result.success:
|
|
765
|
+
print_success(f"Deleted: {result.path}")
|
|
766
|
+
else:
|
|
767
|
+
print_error(result.error)
|
|
768
|
+
console.print()
|
|
769
|
+
|
|
770
|
+
def _cmd_touch(self, args: str) -> None:
|
|
771
|
+
"""Create an empty file."""
|
|
772
|
+
if not args.strip():
|
|
773
|
+
print_error("Usage: /touch <file_path>")
|
|
774
|
+
return
|
|
775
|
+
files = get_file_manager()
|
|
776
|
+
result = files.write(args.strip(), "")
|
|
777
|
+
if result.success:
|
|
778
|
+
print_success(f"Created: {args.strip()}")
|
|
779
|
+
else:
|
|
780
|
+
print_error(result.error)
|
|
781
|
+
console.print()
|
|
782
|
+
|
|
783
|
+
def _cmd_mkdir(self, args: str) -> None:
|
|
784
|
+
"""Create a directory."""
|
|
785
|
+
import os
|
|
786
|
+
if not args.strip():
|
|
787
|
+
print_error("Usage: /mkdir <directory>")
|
|
788
|
+
return
|
|
789
|
+
try:
|
|
790
|
+
os.makedirs(args.strip(), exist_ok=True)
|
|
791
|
+
print_success(f"Created directory: {args.strip()}")
|
|
792
|
+
except Exception as e:
|
|
793
|
+
print_error(f"mkdir failed: {e}")
|
|
794
|
+
console.print()
|
|
795
|
+
|
|
796
|
+
def _cmd_env(self, args: str) -> None:
|
|
797
|
+
"""Show environment variables."""
|
|
798
|
+
import os
|
|
799
|
+
if args.strip():
|
|
800
|
+
val = os.environ.get(args.strip().upper(), "not set")
|
|
801
|
+
console.print(f" {args.strip().upper()}={val}")
|
|
802
|
+
else:
|
|
803
|
+
safe_keys = ["PATH", "HOME", "USER", "SHELL", "PYTHON", "NODE", "PWD",
|
|
804
|
+
"VIRTUAL_ENV", "CONDA_DEFAULT_ENV", "TERM", "LANG", "EDITOR"]
|
|
805
|
+
env_vars = {k: v for k, v in os.environ.items()
|
|
806
|
+
if any(sk in k.upper() for sk in safe_keys)}
|
|
807
|
+
for k, v in sorted(env_vars.items()):
|
|
808
|
+
console.print(f" [bold]{k}[/]={v}")
|
|
809
|
+
console.print()
|
|
810
|
+
|
|
811
|
+
def _cmd_pip(self, args: str) -> None:
|
|
812
|
+
"""Run pip command."""
|
|
813
|
+
self._cmd_run(f"pip {args}" if args.strip() else "pip --version")
|
|
814
|
+
|
|
815
|
+
def _cmd_npm(self, args: str) -> None:
|
|
816
|
+
"""Run npm command."""
|
|
817
|
+
self._cmd_run(f"npm {args}" if args.strip() else "npm --version")
|
|
818
|
+
|
|
819
|
+
def _cmd_git(self, args: str) -> None:
|
|
820
|
+
"""Run git command."""
|
|
821
|
+
self._cmd_run(f"git {args}" if args.strip() else "git --version")
|
|
822
|
+
|
|
823
|
+
def _cmd_python(self, args: str) -> None:
|
|
824
|
+
"""Run Python code."""
|
|
825
|
+
if not args.strip():
|
|
826
|
+
self._cmd_run("python --version")
|
|
827
|
+
elif args.strip().endswith(".py"):
|
|
828
|
+
self._cmd_run(f"python {args}")
|
|
829
|
+
else:
|
|
830
|
+
import shlex
|
|
831
|
+
self._cmd_run(f'python -c {shlex.quote(args)}')
|