devorch 0.1.2__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.
- cli/commands/ask.py +0 -0
- cli/commands/edit.py +0 -0
- cli/commands/run.py +0 -0
- cli/main.py +1527 -0
- config/permissions.py +285 -0
- config/profiles.py +0 -0
- config/settings.py +185 -0
- core/agent.py +433 -0
- core/context.py +0 -0
- core/executor.py +213 -0
- core/modes.py +141 -0
- core/planner.py +16 -0
- core/sessions.py +271 -0
- core/tasks.py +142 -0
- devorch-0.1.2.dist-info/METADATA +528 -0
- devorch-0.1.2.dist-info/RECORD +50 -0
- devorch-0.1.2.dist-info/WHEEL +4 -0
- devorch-0.1.2.dist-info/entry_points.txt +2 -0
- providers/__init__.py +130 -0
- providers/anthropic.py +127 -0
- providers/base.py +51 -0
- providers/custom.py +186 -0
- providers/deepseek.py +170 -0
- providers/gemini.py +192 -0
- providers/github_copilot.py +196 -0
- providers/groq.py +158 -0
- providers/kimi.py +181 -0
- providers/lmstudio.py +147 -0
- providers/local.py +214 -0
- providers/mistral.py +161 -0
- providers/openai.py +114 -0
- providers/openrouter.py +195 -0
- providers/together.py +159 -0
- schemas/message.py +32 -0
- schemas/task.py +115 -0
- schemas/tool.py +0 -0
- tools/__init__.py +30 -0
- tools/base.py +40 -0
- tools/edit.py +236 -0
- tools/filesystem.py +224 -0
- tools/git.py +0 -0
- tools/grep.py +280 -0
- tools/search.py +150 -0
- tools/shell.py +55 -0
- tools/task.py +91 -0
- tools/terminal.py +123 -0
- tools/terminal_session.py +298 -0
- tools/tests.py +0 -0
- tools/websearch.py +166 -0
- utils/logger.py +52 -0
cli/main.py
ADDED
|
@@ -0,0 +1,1527 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import questionary
|
|
4
|
+
import typer
|
|
5
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
6
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
7
|
+
from prompt_toolkit.formatted_text import HTML
|
|
8
|
+
from prompt_toolkit.styles import Style
|
|
9
|
+
from questionary import Style as QStyle
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from config.permissions import (
|
|
14
|
+
PERMISSIONS_FILE,
|
|
15
|
+
PermissionLevel,
|
|
16
|
+
get_permissions,
|
|
17
|
+
reset_permissions,
|
|
18
|
+
)
|
|
19
|
+
from config.settings import (
|
|
20
|
+
CONFIG_FILE,
|
|
21
|
+
ProviderConfig,
|
|
22
|
+
Settings,
|
|
23
|
+
keyring_available,
|
|
24
|
+
save_config,
|
|
25
|
+
set_api_key,
|
|
26
|
+
)
|
|
27
|
+
from core.agent import Agent
|
|
28
|
+
from core.executor import ToolExecutor
|
|
29
|
+
from core.modes import AgentMode, ModeManager
|
|
30
|
+
from core.planner import Planner
|
|
31
|
+
from core.sessions import DEFAULT_MESSAGE_LIMIT, SessionManager
|
|
32
|
+
from core.tasks import get_task_manager, reset_task_manager
|
|
33
|
+
from providers import PROVIDER_ENV_VARS, PROVIDER_INFO, PROVIDERS, get_provider
|
|
34
|
+
from schemas.message import Message
|
|
35
|
+
from tools.edit import EditTool
|
|
36
|
+
from tools.filesystem import FilesystemTool
|
|
37
|
+
from tools.grep import GrepTool
|
|
38
|
+
from tools.search import SearchTool
|
|
39
|
+
from tools.shell import ShellTool
|
|
40
|
+
from tools.task import TaskTool
|
|
41
|
+
from tools.terminal import OpenTerminalTool
|
|
42
|
+
from tools.terminal_session import TerminalSessionTool
|
|
43
|
+
from tools.websearch import WebFetchTool, WebSearchTool
|
|
44
|
+
from utils.logger import (
|
|
45
|
+
get_console,
|
|
46
|
+
print_error,
|
|
47
|
+
print_info,
|
|
48
|
+
print_panel,
|
|
49
|
+
print_success,
|
|
50
|
+
print_warning,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Custom style for questionary prompts
|
|
54
|
+
QUESTIONARY_STYLE = QStyle(
|
|
55
|
+
[
|
|
56
|
+
("qmark", "fg:yellow bold"),
|
|
57
|
+
("question", "fg:white bold"),
|
|
58
|
+
("answer", "fg:green bold"),
|
|
59
|
+
("pointer", "fg:cyan bold"),
|
|
60
|
+
("highlighted", "fg:white"), # Normal white text, no background - arrow shows selection
|
|
61
|
+
("selected", "fg:white"),
|
|
62
|
+
("instruction", "fg:gray"),
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# ASCII Art Banner
|
|
67
|
+
BANNER = r"""
|
|
68
|
+
[bold blue]
|
|
69
|
+
____ ____ _ _ _
|
|
70
|
+
| _ \ _____ _| _ \(_) | ___ | |_
|
|
71
|
+
| | | |/ _ \ \ / / |_) | | |/ _ \| __|
|
|
72
|
+
| |_| | __/\ V /| __/| | | (_) | |_
|
|
73
|
+
|____/ \___| \_/ |_| |_|_|\___/ \__|
|
|
74
|
+
[/bold blue]
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
BANNER_SMALL = "[bold blue]DevOrch[/bold blue] - AI Coding Assistant"
|
|
78
|
+
|
|
79
|
+
VERSION = "0.1.0"
|
|
80
|
+
|
|
81
|
+
# Slash commands with descriptions
|
|
82
|
+
SLASH_COMMANDS = {
|
|
83
|
+
"/help": "Show available commands",
|
|
84
|
+
"/mode": "Show or change mode (plan/auto/ask)",
|
|
85
|
+
"/plan": "Switch to plan mode",
|
|
86
|
+
"/auto": "Switch to auto mode",
|
|
87
|
+
"/ask": "Switch to ask mode (default)",
|
|
88
|
+
"/clear": "Clear conversation history",
|
|
89
|
+
"/session": "Show current session info",
|
|
90
|
+
"/config": "Show configuration settings",
|
|
91
|
+
"/permissions": "Show permission settings",
|
|
92
|
+
"/compact": "Summarize and compact history",
|
|
93
|
+
"/models": "List available models for current provider",
|
|
94
|
+
"/model": "Switch to a different model",
|
|
95
|
+
"/providers": "List all available providers",
|
|
96
|
+
"/provider": "Switch to a different provider",
|
|
97
|
+
"/history": "Show conversation history",
|
|
98
|
+
"/undo": "Undo last message",
|
|
99
|
+
"/save": "Save conversation to file",
|
|
100
|
+
"/status": "Show current provider, model, and mode",
|
|
101
|
+
"/tasks": "Show current task list",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Style for prompt_toolkit (including completion menu)
|
|
105
|
+
PROMPT_STYLE = Style.from_dict(
|
|
106
|
+
{
|
|
107
|
+
"prompt": "#00aa00 bold",
|
|
108
|
+
"command": "#00aaff bold",
|
|
109
|
+
"description": "#888888",
|
|
110
|
+
# Completion menu styling
|
|
111
|
+
"completion-menu": "bg:#1a1a2e",
|
|
112
|
+
"completion-menu.completion": "bg:#1a1a2e #e0e0e0",
|
|
113
|
+
"completion-menu.completion.current": "bg:#0066cc #ffffff bold",
|
|
114
|
+
"completion-menu.meta": "bg:#1a1a2e #666666 italic",
|
|
115
|
+
"completion-menu.meta.current": "bg:#0066cc #cccccc italic",
|
|
116
|
+
# Scrollbar
|
|
117
|
+
"scrollbar.background": "bg:#333344",
|
|
118
|
+
"scrollbar.button": "bg:#0066cc",
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class SlashCommandCompleter(Completer):
|
|
124
|
+
"""Autocomplete for slash commands."""
|
|
125
|
+
|
|
126
|
+
def get_completions(self, document, complete_event):
|
|
127
|
+
text = document.text_before_cursor
|
|
128
|
+
|
|
129
|
+
# Only complete if starts with /
|
|
130
|
+
if not text.startswith("/"):
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Get the partial command
|
|
134
|
+
partial = text.lower()
|
|
135
|
+
|
|
136
|
+
for cmd, desc in SLASH_COMMANDS.items():
|
|
137
|
+
if cmd.startswith(partial):
|
|
138
|
+
# Calculate how much to complete
|
|
139
|
+
yield Completion(
|
|
140
|
+
cmd,
|
|
141
|
+
start_position=-len(text),
|
|
142
|
+
display=HTML(f"<command>{cmd}</command> <description>- {desc}</description>"),
|
|
143
|
+
display_meta=desc,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def print_banner(small: bool = False):
|
|
148
|
+
"""Print the DevOrch banner."""
|
|
149
|
+
if small:
|
|
150
|
+
console.print(BANNER_SMALL)
|
|
151
|
+
else:
|
|
152
|
+
console.print(BANNER)
|
|
153
|
+
console.print(f" [dim]v{VERSION} - Your AI Coding Assistant[/dim]\n")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
SYSTEM_PROMPT = """You are DevOrch, an AI coding assistant with access to tools for interacting with the user's computer.
|
|
157
|
+
|
|
158
|
+
IMPORTANT: You have the following tools available and MUST use them to help the user:
|
|
159
|
+
|
|
160
|
+
1. **shell** - Execute shell commands (bash/powershell). Use this to:
|
|
161
|
+
- Run commands like `npm install`, `git clone`, `git status`, etc.
|
|
162
|
+
- Navigate directories, create files, run scripts
|
|
163
|
+
- Any short-lived terminal command that returns output
|
|
164
|
+
|
|
165
|
+
2. **open_terminal** - Open a NEW terminal window and run a command inside it. Use this for:
|
|
166
|
+
- Starting dev servers or daemons: `npm run dev`, `vite`, `uvicorn`, `flask run`, `next dev`, `ng serve`
|
|
167
|
+
- Interactive scaffold tools that prompt the user: `npm create vite@latest`, `npx create-next-app`, `ng new`, `django-admin startproject`
|
|
168
|
+
- Any long-running process that should NOT block the current session
|
|
169
|
+
- ALWAYS prefer this over `shell` for servers and scaffolds
|
|
170
|
+
|
|
171
|
+
3. **terminal_session** — Manage a long-running background process across turns:
|
|
172
|
+
- `start` — launch command in a named background session
|
|
173
|
+
- `read` — read recent stdout/stderr output
|
|
174
|
+
- `send` — send input to the process stdin
|
|
175
|
+
- `stop` — terminate the session
|
|
176
|
+
- `list` — show all active sessions
|
|
177
|
+
Use this when you need to check server logs, send commands to a running process, or manage multiple background processes.
|
|
178
|
+
|
|
179
|
+
4. **filesystem** - Read/write/list files. Use this to:
|
|
180
|
+
- Read file contents to understand code
|
|
181
|
+
- Write or create new files
|
|
182
|
+
- List directory contents
|
|
183
|
+
|
|
184
|
+
5. **search** - Find files by name patterns (like glob)
|
|
185
|
+
|
|
186
|
+
6. **grep** - Search for text patterns within files
|
|
187
|
+
|
|
188
|
+
7. **edit** - Make targeted edits to existing files
|
|
189
|
+
|
|
190
|
+
8. **task** - Track progress on multi-step work. Use this to:
|
|
191
|
+
- Create a task list when working on complex requests (3+ steps)
|
|
192
|
+
- Show the user what you're currently working on
|
|
193
|
+
- Mark tasks complete as you finish them
|
|
194
|
+
|
|
195
|
+
Task guidelines:
|
|
196
|
+
- Use when working on multiple steps or user gives multiple items
|
|
197
|
+
- Only ONE task should be 'in_progress' at a time
|
|
198
|
+
- Mark tasks 'completed' immediately after finishing each one
|
|
199
|
+
- content: imperative form (e.g., "Fix bug", "Run tests")
|
|
200
|
+
- activeForm: present continuous (e.g., "Fixing bug", "Running tests")
|
|
201
|
+
|
|
202
|
+
9. **websearch** - Search the web for current information. Use when:
|
|
203
|
+
- You need up-to-date information (news, docs, releases)
|
|
204
|
+
- Looking up programming solutions or best practices
|
|
205
|
+
- Finding package/library documentation
|
|
206
|
+
- User asks about something you're unsure about
|
|
207
|
+
|
|
208
|
+
10. **webfetch** - Fetch content from a specific URL. Use when:
|
|
209
|
+
- You need to read a documentation page
|
|
210
|
+
- User provides a URL to check
|
|
211
|
+
- You found a relevant URL from search results
|
|
212
|
+
|
|
213
|
+
RULES:
|
|
214
|
+
- When the user asks you to CREATE something (app, file, project), USE THE TOOLS to actually do it
|
|
215
|
+
- Do NOT just give instructions - execute the commands yourself using the shell tool
|
|
216
|
+
- Do NOT ask the user to run commands manually - run them for the user
|
|
217
|
+
- Always prefer action over explanation
|
|
218
|
+
- For multi-step tasks, use the task tool to track and show progress
|
|
219
|
+
- IMPORTANT: Use `open_terminal` (not `shell`) for dev servers and interactive scaffold commands
|
|
220
|
+
|
|
221
|
+
When executing shell commands, use the shell tool with the command to run."""
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class SimplePlanner(Planner):
|
|
225
|
+
def plan(self, history: list[Message]) -> list[Message]:
|
|
226
|
+
system_prompt = Message(role="system", content=SYSTEM_PROMPT)
|
|
227
|
+
return [system_prompt] + history
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# Main app with invoke_without_command=True so we can handle bare `devorch`
|
|
231
|
+
app = typer.Typer(
|
|
232
|
+
help="DevOrch - Your AI Coding Assistant", invoke_without_command=True, no_args_is_help=False
|
|
233
|
+
)
|
|
234
|
+
sessions_app = typer.Typer(help="Manage chat sessions")
|
|
235
|
+
app.add_typer(sessions_app, name="sessions")
|
|
236
|
+
|
|
237
|
+
permissions_app = typer.Typer(help="Manage tool permissions")
|
|
238
|
+
app.add_typer(permissions_app, name="permissions")
|
|
239
|
+
|
|
240
|
+
console = get_console()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def has_any_provider_configured(settings: Settings) -> bool:
|
|
244
|
+
"""Check if any provider is configured (has API key or is local/lmstudio with model).
|
|
245
|
+
|
|
246
|
+
Also checks if config file exists - if not, we need onboarding even if keys exist in keyring.
|
|
247
|
+
"""
|
|
248
|
+
# If config file doesn't exist, run onboarding (even if keys exist in keyring)
|
|
249
|
+
if not CONFIG_FILE.exists():
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
# Check if any API-based provider has a key configured (keyring or env var)
|
|
253
|
+
for name in PROVIDERS.keys():
|
|
254
|
+
if name not in ("local", "lmstudio"):
|
|
255
|
+
if settings.get_api_key(name):
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
# Check if local/lmstudio is configured (has a saved model in config)
|
|
259
|
+
for name in ("local", "lmstudio"):
|
|
260
|
+
config = settings.providers.get(name)
|
|
261
|
+
if config and config.default_model:
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def run_onboarding() -> str | None:
|
|
268
|
+
"""Run first-time setup with interactive prompts. Returns the configured provider name or None."""
|
|
269
|
+
print_banner()
|
|
270
|
+
|
|
271
|
+
# Welcome panel
|
|
272
|
+
console.print(
|
|
273
|
+
Panel(
|
|
274
|
+
"[bold]Welcome to DevOrch![/bold]\n\nLet's set up your AI provider to get started.",
|
|
275
|
+
border_style="blue",
|
|
276
|
+
padding=(1, 2),
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
console.print()
|
|
280
|
+
|
|
281
|
+
# Provider selection with questionary
|
|
282
|
+
provider_choices = [
|
|
283
|
+
questionary.Choice("OpenAI (GPT-4o, GPT-4, etc.)", value="openai"),
|
|
284
|
+
questionary.Choice("Anthropic (Claude Sonnet, Opus, etc.)", value="anthropic"),
|
|
285
|
+
questionary.Choice("Google Gemini (Gemini Pro, Flash, etc.)", value="gemini"),
|
|
286
|
+
questionary.Choice("Groq (Ultra-fast Llama, Mixtral)", value="groq"),
|
|
287
|
+
questionary.Choice("OpenRouter (Access 100+ models)", value="openrouter"),
|
|
288
|
+
questionary.Choice("Mistral (Mistral Large, Codestral)", value="mistral"),
|
|
289
|
+
questionary.Choice("Together AI (Open source models)", value="together"),
|
|
290
|
+
questionary.Separator(),
|
|
291
|
+
questionary.Choice("Ollama - Local (No API key needed)", value="local"),
|
|
292
|
+
questionary.Choice("LM Studio - Local (No API key needed)", value="lmstudio"),
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
provider = questionary.select(
|
|
297
|
+
"Select your AI provider:",
|
|
298
|
+
choices=provider_choices,
|
|
299
|
+
style=QUESTIONARY_STYLE,
|
|
300
|
+
instruction="(Use arrow keys to navigate, Enter to select)",
|
|
301
|
+
).ask()
|
|
302
|
+
|
|
303
|
+
if not provider:
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
except (KeyboardInterrupt, EOFError):
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
if provider in ("local", "lmstudio"):
|
|
310
|
+
print_success(f"{provider.title()} provider selected - no API key needed!")
|
|
311
|
+
if provider == "local":
|
|
312
|
+
print_info("Make sure Ollama is running at http://localhost:11434")
|
|
313
|
+
else:
|
|
314
|
+
print_info("Make sure LM Studio is running at http://localhost:1234")
|
|
315
|
+
|
|
316
|
+
# Try to list available models and let user select
|
|
317
|
+
settings = Settings.load()
|
|
318
|
+
try:
|
|
319
|
+
with console.status("[bold cyan]Fetching available models...", spinner="dots"):
|
|
320
|
+
temp_provider = get_provider(provider)
|
|
321
|
+
models = temp_provider.list_models()
|
|
322
|
+
|
|
323
|
+
if models:
|
|
324
|
+
model_choices = [questionary.Choice(m.id, value=m.id) for m in models[:15]]
|
|
325
|
+
|
|
326
|
+
selected_model = questionary.select(
|
|
327
|
+
"Select a model:",
|
|
328
|
+
choices=model_choices,
|
|
329
|
+
style=QUESTIONARY_STYLE,
|
|
330
|
+
instruction="(Use arrow keys)",
|
|
331
|
+
).ask()
|
|
332
|
+
|
|
333
|
+
if selected_model:
|
|
334
|
+
if provider not in settings.providers:
|
|
335
|
+
settings.providers[provider] = ProviderConfig()
|
|
336
|
+
settings.providers[provider].default_model = selected_model
|
|
337
|
+
settings.default_provider = provider
|
|
338
|
+
save_config(settings)
|
|
339
|
+
print_success(f"Saved: provider={provider}, model={selected_model}")
|
|
340
|
+
|
|
341
|
+
except Exception as e:
|
|
342
|
+
print_warning(f"Could not list models: {e}")
|
|
343
|
+
settings.default_provider = provider
|
|
344
|
+
try:
|
|
345
|
+
save_config(settings)
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
return provider
|
|
350
|
+
|
|
351
|
+
# Get API key for cloud providers
|
|
352
|
+
env_var = PROVIDER_ENV_VARS.get(provider, f"{provider.upper()}_API_KEY")
|
|
353
|
+
|
|
354
|
+
console.print()
|
|
355
|
+
console.print(
|
|
356
|
+
Panel(
|
|
357
|
+
f"[bold]Setting up {provider.title()}[/bold]\n\n"
|
|
358
|
+
f"You'll need an API key from {provider.title()}.\n"
|
|
359
|
+
f"Alternatively, set the [cyan]{env_var}[/cyan] environment variable.",
|
|
360
|
+
border_style="yellow",
|
|
361
|
+
padding=(0, 1),
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
console.print()
|
|
365
|
+
|
|
366
|
+
api_key = questionary.password(f"Enter your {provider} API key:", style=QUESTIONARY_STYLE).ask()
|
|
367
|
+
|
|
368
|
+
if not api_key or not api_key.strip():
|
|
369
|
+
print_error("API key cannot be empty.")
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
api_key = api_key.strip()
|
|
373
|
+
|
|
374
|
+
# Try to store in keyring
|
|
375
|
+
if keyring_available():
|
|
376
|
+
if set_api_key(provider, api_key):
|
|
377
|
+
print_success("API key stored securely in system keychain!")
|
|
378
|
+
else:
|
|
379
|
+
print_warning("Could not store in keychain. Key will only be available this session.")
|
|
380
|
+
else:
|
|
381
|
+
print_warning("Keychain not available. Set the environment variable for persistence.")
|
|
382
|
+
|
|
383
|
+
# Save as default provider
|
|
384
|
+
settings = Settings.load()
|
|
385
|
+
settings.default_provider = provider
|
|
386
|
+
if provider not in settings.providers:
|
|
387
|
+
settings.providers[provider] = ProviderConfig()
|
|
388
|
+
settings.providers[provider].api_key = api_key
|
|
389
|
+
|
|
390
|
+
# Let user select a model
|
|
391
|
+
selected_model = None
|
|
392
|
+
try:
|
|
393
|
+
with console.status("[bold cyan]Fetching available models...", spinner="dots"):
|
|
394
|
+
temp_provider = get_provider(provider, api_key=api_key)
|
|
395
|
+
models = temp_provider.list_models()
|
|
396
|
+
|
|
397
|
+
if models:
|
|
398
|
+
model_choices = []
|
|
399
|
+
for m in models[:15]:
|
|
400
|
+
desc = (
|
|
401
|
+
f" - {m.description[:40]}..."
|
|
402
|
+
if m.description and len(m.description) > 40
|
|
403
|
+
else ""
|
|
404
|
+
)
|
|
405
|
+
model_choices.append(questionary.Choice(f"{m.id}{desc}", value=m.id))
|
|
406
|
+
|
|
407
|
+
selected_model = questionary.select(
|
|
408
|
+
"Select a model:",
|
|
409
|
+
choices=model_choices,
|
|
410
|
+
style=QUESTIONARY_STYLE,
|
|
411
|
+
instruction="(Use arrow keys)",
|
|
412
|
+
).ask()
|
|
413
|
+
|
|
414
|
+
if selected_model:
|
|
415
|
+
settings.providers[provider].default_model = selected_model
|
|
416
|
+
|
|
417
|
+
except Exception as e:
|
|
418
|
+
print_warning(f"Could not fetch models: {e}")
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
save_config(settings)
|
|
422
|
+
if selected_model:
|
|
423
|
+
print_success(f"Saved: provider={provider}, model={selected_model}")
|
|
424
|
+
else:
|
|
425
|
+
print_success(f"Default provider set to: {provider}")
|
|
426
|
+
except Exception:
|
|
427
|
+
pass # Config save failed, but key is in memory
|
|
428
|
+
|
|
429
|
+
console.print()
|
|
430
|
+
console.print(
|
|
431
|
+
Panel(
|
|
432
|
+
"[bold green]Setup complete![/bold green]\n\n"
|
|
433
|
+
"You're ready to start using DevOrch.\n"
|
|
434
|
+
"Type your questions or commands, or use /help for available commands.",
|
|
435
|
+
border_style="green",
|
|
436
|
+
padding=(0, 1),
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
return provider
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def create_provider_safe(provider_name: str, model: str, settings: Settings):
|
|
444
|
+
"""Create provider, returning None if API key missing (for onboarding check)."""
|
|
445
|
+
provider_name = provider_name.lower()
|
|
446
|
+
|
|
447
|
+
if provider_name not in PROVIDERS:
|
|
448
|
+
return None
|
|
449
|
+
|
|
450
|
+
api_key = settings.get_api_key(provider_name)
|
|
451
|
+
|
|
452
|
+
if provider_name != "local" and not api_key:
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
if not model:
|
|
456
|
+
model = settings.get_default_model(provider_name)
|
|
457
|
+
|
|
458
|
+
kwargs = {}
|
|
459
|
+
if provider_name == "local":
|
|
460
|
+
base_url = settings.get_base_url(provider_name)
|
|
461
|
+
if base_url:
|
|
462
|
+
kwargs["base_url"] = base_url
|
|
463
|
+
|
|
464
|
+
return get_provider(provider_name, model=model, api_key=api_key, **kwargs)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def create_provider(provider_name: str, model: str, settings: Settings):
|
|
468
|
+
"""Create and validate a provider instance."""
|
|
469
|
+
provider_name = provider_name.lower()
|
|
470
|
+
|
|
471
|
+
if provider_name not in PROVIDERS:
|
|
472
|
+
print_error(f"Unknown provider '{provider_name}'. Available: {', '.join(PROVIDERS.keys())}")
|
|
473
|
+
raise typer.Exit(1)
|
|
474
|
+
|
|
475
|
+
api_key = settings.get_api_key(provider_name)
|
|
476
|
+
|
|
477
|
+
if provider_name != "local" and not api_key:
|
|
478
|
+
env_var_name = {
|
|
479
|
+
"openai": "OPENAI_API_KEY",
|
|
480
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
481
|
+
"gemini": "GOOGLE_API_KEY",
|
|
482
|
+
}.get(provider_name, f"{provider_name.upper()}_API_KEY")
|
|
483
|
+
|
|
484
|
+
print_error(f"No API key found for {provider_name}.")
|
|
485
|
+
print_error(
|
|
486
|
+
f"Use 'devorch set-key {provider_name}' or set {env_var_name} environment variable"
|
|
487
|
+
)
|
|
488
|
+
raise typer.Exit(1)
|
|
489
|
+
|
|
490
|
+
if not model:
|
|
491
|
+
model = settings.get_default_model(provider_name)
|
|
492
|
+
|
|
493
|
+
kwargs = {}
|
|
494
|
+
if provider_name == "local":
|
|
495
|
+
base_url = settings.get_base_url(provider_name)
|
|
496
|
+
if base_url:
|
|
497
|
+
kwargs["base_url"] = base_url
|
|
498
|
+
|
|
499
|
+
return get_provider(provider_name, model=model, api_key=api_key, **kwargs)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def start_repl(
|
|
503
|
+
provider: str | None = None,
|
|
504
|
+
model: str | None = None,
|
|
505
|
+
resume: str | None = None,
|
|
506
|
+
message_limit: int = DEFAULT_MESSAGE_LIMIT,
|
|
507
|
+
show_banner: bool = True,
|
|
508
|
+
):
|
|
509
|
+
"""Start the interactive REPL session."""
|
|
510
|
+
if show_banner:
|
|
511
|
+
print_banner()
|
|
512
|
+
|
|
513
|
+
settings = Settings.load()
|
|
514
|
+
session_manager = SessionManager(message_limit=message_limit)
|
|
515
|
+
|
|
516
|
+
# Reset task manager for new session
|
|
517
|
+
reset_task_manager()
|
|
518
|
+
|
|
519
|
+
context_summary = None
|
|
520
|
+
|
|
521
|
+
# Handle session resumption
|
|
522
|
+
if resume:
|
|
523
|
+
try:
|
|
524
|
+
session_info, messages = session_manager.load_session(resume)
|
|
525
|
+
provider = session_info["provider"]
|
|
526
|
+
model = session_info["model"]
|
|
527
|
+
context_summary = session_info.get("summary")
|
|
528
|
+
|
|
529
|
+
print_success(f"Resumed session: {resume}")
|
|
530
|
+
print_info(f"Provider: {provider} | Model: {model} | Messages: {len(messages)}")
|
|
531
|
+
|
|
532
|
+
if context_summary:
|
|
533
|
+
print_info("Session has context from previous conversation")
|
|
534
|
+
|
|
535
|
+
except ValueError as e:
|
|
536
|
+
print_error(str(e))
|
|
537
|
+
raise typer.Exit(1) from e
|
|
538
|
+
else:
|
|
539
|
+
messages = []
|
|
540
|
+
|
|
541
|
+
if not provider:
|
|
542
|
+
provider = settings.default_provider
|
|
543
|
+
|
|
544
|
+
llm = create_provider(provider, model, settings)
|
|
545
|
+
|
|
546
|
+
# Create new session if not resuming
|
|
547
|
+
if not resume:
|
|
548
|
+
session_manager.create_session(llm.name, llm.model)
|
|
549
|
+
|
|
550
|
+
tools = [
|
|
551
|
+
ShellTool(),
|
|
552
|
+
OpenTerminalTool(),
|
|
553
|
+
TerminalSessionTool(),
|
|
554
|
+
FilesystemTool(),
|
|
555
|
+
SearchTool(),
|
|
556
|
+
GrepTool(),
|
|
557
|
+
EditTool(),
|
|
558
|
+
TaskTool(),
|
|
559
|
+
WebSearchTool(),
|
|
560
|
+
WebFetchTool(),
|
|
561
|
+
]
|
|
562
|
+
|
|
563
|
+
# Create mode manager (shared between agent and executor)
|
|
564
|
+
mode_manager = ModeManager(default_mode=AgentMode.ASK)
|
|
565
|
+
|
|
566
|
+
executor = ToolExecutor(tools=tools, require_confirmation=True, mode_manager=mode_manager)
|
|
567
|
+
planner = SimplePlanner()
|
|
568
|
+
|
|
569
|
+
def on_session_continue(new_session_id: str):
|
|
570
|
+
print_info(f"Session continued: {new_session_id}")
|
|
571
|
+
|
|
572
|
+
agent = Agent(
|
|
573
|
+
provider=llm,
|
|
574
|
+
planner=planner,
|
|
575
|
+
executor=executor,
|
|
576
|
+
tools=tools,
|
|
577
|
+
session_manager=session_manager,
|
|
578
|
+
on_session_continue=on_session_continue,
|
|
579
|
+
mode_manager=mode_manager,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
if messages:
|
|
583
|
+
agent.set_history(messages)
|
|
584
|
+
|
|
585
|
+
if context_summary:
|
|
586
|
+
agent.set_context_summary(context_summary)
|
|
587
|
+
|
|
588
|
+
# Get current working directory for display
|
|
589
|
+
cwd = os.getcwd()
|
|
590
|
+
cwd_short = os.path.basename(cwd) or cwd
|
|
591
|
+
|
|
592
|
+
# Show session info
|
|
593
|
+
console.print(
|
|
594
|
+
f" [dim]Provider:[/dim] [cyan]{llm.name}[/cyan] [dim]Model:[/dim] [cyan]{llm.model}[/cyan]"
|
|
595
|
+
)
|
|
596
|
+
console.print(
|
|
597
|
+
f" [dim]Session:[/dim] {session_manager.current_session_id} [dim]cwd:[/dim] {cwd_short}"
|
|
598
|
+
)
|
|
599
|
+
console.print(
|
|
600
|
+
f" [dim]Mode:[/dim] {mode_manager.get_mode_display()} [dim]- Type[/dim] / [dim]to see commands[/dim]\n"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# Create completer for slash commands
|
|
604
|
+
completer = SlashCommandCompleter()
|
|
605
|
+
|
|
606
|
+
# Track current provider/model for switching
|
|
607
|
+
current_llm = llm
|
|
608
|
+
current_settings = settings
|
|
609
|
+
|
|
610
|
+
def get_prompt():
|
|
611
|
+
"""Generate prompt with mode indicator."""
|
|
612
|
+
mode_indicator = {
|
|
613
|
+
AgentMode.PLAN: "[yellow]P[/yellow]",
|
|
614
|
+
AgentMode.AUTO: "[green]A[/green]",
|
|
615
|
+
AgentMode.ASK: "[blue]?[/blue]",
|
|
616
|
+
}.get(mode_manager.mode, "")
|
|
617
|
+
return f"[{mode_indicator}] {cwd_short}> "
|
|
618
|
+
|
|
619
|
+
while True:
|
|
620
|
+
try:
|
|
621
|
+
# Build prompt with mode indicator
|
|
622
|
+
mode_char = {"plan": "P", "auto": "A", "ask": "?"}.get(mode_manager.mode.value, "?")
|
|
623
|
+
prompt_str = f"[{mode_char}] {cwd_short}> "
|
|
624
|
+
|
|
625
|
+
# Use prompt_toolkit with autocomplete
|
|
626
|
+
user_input = pt_prompt(
|
|
627
|
+
prompt_str,
|
|
628
|
+
completer=completer,
|
|
629
|
+
complete_while_typing=True,
|
|
630
|
+
style=PROMPT_STYLE,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
if user_input.lower() in ("exit", "quit", "q"):
|
|
634
|
+
print_info(f"Session saved: {session_manager.current_session_id}")
|
|
635
|
+
break
|
|
636
|
+
|
|
637
|
+
if user_input.strip() == "":
|
|
638
|
+
continue
|
|
639
|
+
|
|
640
|
+
# Handle slash commands
|
|
641
|
+
if user_input.startswith("/"):
|
|
642
|
+
cmd_parts = user_input[1:].strip().split(maxsplit=1)
|
|
643
|
+
cmd = cmd_parts[0].lower() if cmd_parts else ""
|
|
644
|
+
cmd_arg = cmd_parts[1] if len(cmd_parts) > 1 else None
|
|
645
|
+
|
|
646
|
+
if cmd == "help":
|
|
647
|
+
console.print("\n[bold]Available Commands:[/bold]")
|
|
648
|
+
for slash_cmd, desc in SLASH_COMMANDS.items():
|
|
649
|
+
console.print(f" [cyan]{slash_cmd:<14}[/cyan] - {desc}")
|
|
650
|
+
console.print(f" [cyan]{'exit':<14}[/cyan] - Exit DevOrch")
|
|
651
|
+
console.print("\n[bold]Modes:[/bold]")
|
|
652
|
+
console.print(
|
|
653
|
+
" [yellow]PLAN[/yellow] - Shows plan before executing, asks for approval"
|
|
654
|
+
)
|
|
655
|
+
console.print(
|
|
656
|
+
" [green]AUTO[/green] - Executes tools automatically (trusted mode)"
|
|
657
|
+
)
|
|
658
|
+
console.print(" [blue]ASK[/blue] - Asks before each tool execution (default)")
|
|
659
|
+
console.print("\n[dim]Tip: Type / and use Tab for autocomplete[/dim]\n")
|
|
660
|
+
continue
|
|
661
|
+
|
|
662
|
+
elif cmd == "mode":
|
|
663
|
+
if cmd_arg:
|
|
664
|
+
mode_name = cmd_arg.lower()
|
|
665
|
+
else:
|
|
666
|
+
# Interactive mode selection
|
|
667
|
+
mode_choices = [
|
|
668
|
+
questionary.Choice(
|
|
669
|
+
f"{'> ' if mode_manager.mode == AgentMode.PLAN else ' '}PLAN - Shows plan before executing, asks for approval",
|
|
670
|
+
value="plan",
|
|
671
|
+
),
|
|
672
|
+
questionary.Choice(
|
|
673
|
+
f"{'> ' if mode_manager.mode == AgentMode.AUTO else ' '}AUTO - Executes tools automatically (trusted mode)",
|
|
674
|
+
value="auto",
|
|
675
|
+
),
|
|
676
|
+
questionary.Choice(
|
|
677
|
+
f"{'> ' if mode_manager.mode == AgentMode.ASK else ' '}ASK - Asks before each tool execution (default)",
|
|
678
|
+
value="ask",
|
|
679
|
+
),
|
|
680
|
+
]
|
|
681
|
+
try:
|
|
682
|
+
mode_name = questionary.select(
|
|
683
|
+
"Select mode:",
|
|
684
|
+
choices=mode_choices,
|
|
685
|
+
style=QUESTIONARY_STYLE,
|
|
686
|
+
instruction="(Use arrow keys)",
|
|
687
|
+
).ask()
|
|
688
|
+
if not mode_name:
|
|
689
|
+
continue
|
|
690
|
+
except (KeyboardInterrupt, EOFError):
|
|
691
|
+
continue
|
|
692
|
+
|
|
693
|
+
if mode_name in ("plan", "auto", "ask"):
|
|
694
|
+
mode_manager.mode = AgentMode(mode_name)
|
|
695
|
+
print_success(f"Switched to {mode_name.upper()} mode")
|
|
696
|
+
console.print(f" [dim]{mode_manager.get_mode_description()}[/dim]")
|
|
697
|
+
else:
|
|
698
|
+
print_error(f"Unknown mode: {mode_name}")
|
|
699
|
+
continue
|
|
700
|
+
|
|
701
|
+
elif cmd == "plan":
|
|
702
|
+
mode_manager.mode = AgentMode.PLAN
|
|
703
|
+
print_success("Switched to PLAN mode")
|
|
704
|
+
console.print(" [dim]I'll show you the plan before executing anything[/dim]")
|
|
705
|
+
continue
|
|
706
|
+
|
|
707
|
+
elif cmd == "auto":
|
|
708
|
+
mode_manager.mode = AgentMode.AUTO
|
|
709
|
+
print_success("Switched to AUTO mode")
|
|
710
|
+
console.print(
|
|
711
|
+
" [dim]I'll execute tools automatically (dangerous commands still blocked)[/dim]"
|
|
712
|
+
)
|
|
713
|
+
continue
|
|
714
|
+
|
|
715
|
+
elif cmd == "ask":
|
|
716
|
+
mode_manager.mode = AgentMode.ASK
|
|
717
|
+
print_success("Switched to ASK mode")
|
|
718
|
+
console.print(" [dim]I'll ask before each tool execution[/dim]")
|
|
719
|
+
continue
|
|
720
|
+
|
|
721
|
+
elif cmd == "clear":
|
|
722
|
+
agent.history = []
|
|
723
|
+
print_success("Conversation cleared.")
|
|
724
|
+
continue
|
|
725
|
+
|
|
726
|
+
elif cmd == "status":
|
|
727
|
+
console.print("\n[bold]Status[/bold]")
|
|
728
|
+
console.print(f" [dim]Provider:[/dim] [cyan]{current_llm.name}[/cyan]")
|
|
729
|
+
console.print(f" [dim]Model:[/dim] [cyan]{current_llm.model}[/cyan]")
|
|
730
|
+
console.print(f" [dim]Mode:[/dim] {mode_manager.get_mode_display()}")
|
|
731
|
+
console.print()
|
|
732
|
+
continue
|
|
733
|
+
|
|
734
|
+
elif cmd == "session":
|
|
735
|
+
console.print("\n[bold]Current Session[/bold]")
|
|
736
|
+
print_info(f"Session ID: {session_manager.current_session_id}")
|
|
737
|
+
print_info(f"Messages: {len(agent.history)}")
|
|
738
|
+
print_info(f"Provider: {current_llm.name}")
|
|
739
|
+
print_info(f"Model: {current_llm.model}")
|
|
740
|
+
console.print(f" [dim]Mode:[/dim] {mode_manager.get_mode_display()}")
|
|
741
|
+
console.print()
|
|
742
|
+
continue
|
|
743
|
+
|
|
744
|
+
elif cmd == "config":
|
|
745
|
+
console.print("\n[bold]Configuration[/bold]")
|
|
746
|
+
print_info(f"Provider: {current_llm.name}")
|
|
747
|
+
print_info(f"Model: {current_llm.model}")
|
|
748
|
+
print_info(f"Session limit: {session_manager.message_limit} messages")
|
|
749
|
+
print_info(
|
|
750
|
+
f"Keyring: {'available' if keyring_available() else 'not available'}"
|
|
751
|
+
)
|
|
752
|
+
console.print()
|
|
753
|
+
continue
|
|
754
|
+
|
|
755
|
+
elif cmd == "permissions":
|
|
756
|
+
perms = get_permissions()
|
|
757
|
+
console.print("\n[bold]Tool Permissions:[/bold]")
|
|
758
|
+
for tool_name, perm in perms.tools.items():
|
|
759
|
+
level_color = {"allow": "green", "deny": "red", "ask": "yellow"}.get(
|
|
760
|
+
perm.level.value, "white"
|
|
761
|
+
)
|
|
762
|
+
console.print(
|
|
763
|
+
f" {tool_name}: [{level_color}]{perm.level.value}[/{level_color}]"
|
|
764
|
+
)
|
|
765
|
+
if perms.session_allowed:
|
|
766
|
+
console.print(
|
|
767
|
+
f"\n[green]Session allowed:[/green] {len(perms.session_allowed)} patterns"
|
|
768
|
+
)
|
|
769
|
+
console.print()
|
|
770
|
+
continue
|
|
771
|
+
|
|
772
|
+
elif cmd == "compact":
|
|
773
|
+
print_info("Compacting conversation history...")
|
|
774
|
+
summary = agent._generate_summary()
|
|
775
|
+
agent.history = []
|
|
776
|
+
agent.set_context_summary(summary)
|
|
777
|
+
print_success("History compacted. Summary preserved.")
|
|
778
|
+
continue
|
|
779
|
+
|
|
780
|
+
elif cmd == "models":
|
|
781
|
+
console.print(f"\n[bold]Available models for {current_llm.name}:[/bold]")
|
|
782
|
+
try:
|
|
783
|
+
with console.status("[dim]Fetching models...", spinner="dots"):
|
|
784
|
+
models = current_llm.list_models()
|
|
785
|
+
for m in models[:30]: # Limit display
|
|
786
|
+
marker = "[green]>[/green]" if m.id == current_llm.model else " "
|
|
787
|
+
ctx = f" ({m.context_length} ctx)" if m.context_length else ""
|
|
788
|
+
# Show tool capability warning for local models
|
|
789
|
+
desc = ""
|
|
790
|
+
if m.description:
|
|
791
|
+
if "no tool" in m.description.lower():
|
|
792
|
+
desc = f" [yellow]{m.description}[/yellow]"
|
|
793
|
+
else:
|
|
794
|
+
desc = f" [dim]{m.description}[/dim]"
|
|
795
|
+
console.print(f" {marker} {m.id}{ctx}{desc}")
|
|
796
|
+
if len(models) > 30:
|
|
797
|
+
console.print(f" [dim]... and {len(models) - 30} more[/dim]")
|
|
798
|
+
if current_llm.name == "local":
|
|
799
|
+
console.print(
|
|
800
|
+
"\n [dim]For tool/function calling, use 7B+ models[/dim]"
|
|
801
|
+
)
|
|
802
|
+
except Exception as e:
|
|
803
|
+
print_error(f"Failed to fetch models: {e}")
|
|
804
|
+
console.print("\n[dim]Use /model <name> to switch[/dim]\n")
|
|
805
|
+
continue
|
|
806
|
+
|
|
807
|
+
elif cmd == "model":
|
|
808
|
+
selected_model = cmd_arg
|
|
809
|
+
|
|
810
|
+
if not selected_model:
|
|
811
|
+
# Interactive model selection
|
|
812
|
+
try:
|
|
813
|
+
with console.status("[cyan]Fetching models...", spinner="dots"):
|
|
814
|
+
models = current_llm.list_models()
|
|
815
|
+
|
|
816
|
+
if models:
|
|
817
|
+
model_choices = []
|
|
818
|
+
for m in models[:20]:
|
|
819
|
+
is_current = m.id == current_llm.model
|
|
820
|
+
prefix = "> " if is_current else " "
|
|
821
|
+
ctx = f" ({m.context_length} ctx)" if m.context_length else ""
|
|
822
|
+
model_choices.append(
|
|
823
|
+
questionary.Choice(f"{prefix}{m.id}{ctx}", value=m.id)
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
selected_model = questionary.select(
|
|
827
|
+
f"Select model for {current_llm.name}:",
|
|
828
|
+
choices=model_choices,
|
|
829
|
+
style=QUESTIONARY_STYLE,
|
|
830
|
+
instruction="(Use arrow keys)",
|
|
831
|
+
).ask()
|
|
832
|
+
|
|
833
|
+
if not selected_model:
|
|
834
|
+
continue
|
|
835
|
+
else:
|
|
836
|
+
print_warning("No models available")
|
|
837
|
+
continue
|
|
838
|
+
except Exception as e:
|
|
839
|
+
print_error(f"Failed to fetch models: {e}")
|
|
840
|
+
continue
|
|
841
|
+
|
|
842
|
+
try:
|
|
843
|
+
# Build kwargs for provider (include base_url for local)
|
|
844
|
+
provider_kwargs = {}
|
|
845
|
+
if current_llm.name == "local":
|
|
846
|
+
base_url = current_settings.get_base_url(current_llm.name)
|
|
847
|
+
if base_url:
|
|
848
|
+
provider_kwargs["base_url"] = base_url
|
|
849
|
+
|
|
850
|
+
# Get API key - prefer current provider's key, then settings
|
|
851
|
+
api_key = getattr(
|
|
852
|
+
current_llm, "api_key", None
|
|
853
|
+
) or current_settings.get_api_key(current_llm.name)
|
|
854
|
+
|
|
855
|
+
new_llm = get_provider(
|
|
856
|
+
current_llm.name,
|
|
857
|
+
model=selected_model,
|
|
858
|
+
api_key=api_key,
|
|
859
|
+
**provider_kwargs,
|
|
860
|
+
)
|
|
861
|
+
current_llm = new_llm
|
|
862
|
+
agent.provider = new_llm
|
|
863
|
+
|
|
864
|
+
# Save model selection to settings
|
|
865
|
+
try:
|
|
866
|
+
if current_llm.name not in current_settings.providers:
|
|
867
|
+
current_settings.providers[current_llm.name] = ProviderConfig()
|
|
868
|
+
current_settings.providers[
|
|
869
|
+
current_llm.name
|
|
870
|
+
].default_model = selected_model
|
|
871
|
+
save_config(current_settings)
|
|
872
|
+
print_success(f"Switched to model: {selected_model}")
|
|
873
|
+
except Exception:
|
|
874
|
+
print_success(f"Switched to model: {selected_model}")
|
|
875
|
+
except Exception as e:
|
|
876
|
+
print_error(f"Failed to switch model: {e}")
|
|
877
|
+
continue
|
|
878
|
+
|
|
879
|
+
elif cmd == "providers":
|
|
880
|
+
console.print("\n[bold]Available Providers:[/bold]")
|
|
881
|
+
for name, desc in PROVIDER_INFO.items():
|
|
882
|
+
marker = "[green]>[/green]" if name == current_llm.name else " "
|
|
883
|
+
env_var = PROVIDER_ENV_VARS.get(name)
|
|
884
|
+
key_status = ""
|
|
885
|
+
if env_var:
|
|
886
|
+
has_key = bool(current_settings.get_api_key(name))
|
|
887
|
+
key_status = (
|
|
888
|
+
" [green](configured)[/green]"
|
|
889
|
+
if has_key
|
|
890
|
+
else " [yellow](needs key)[/yellow]"
|
|
891
|
+
)
|
|
892
|
+
elif name in ("local", "lmstudio"):
|
|
893
|
+
key_status = " [dim](no key needed)[/dim]"
|
|
894
|
+
console.print(f" {marker} [cyan]{name:<12}[/cyan] - {desc}{key_status}")
|
|
895
|
+
console.print("\n[dim]Use /provider <name> to switch[/dim]\n")
|
|
896
|
+
continue
|
|
897
|
+
|
|
898
|
+
elif cmd == "provider":
|
|
899
|
+
if cmd_arg:
|
|
900
|
+
new_provider = cmd_arg.lower()
|
|
901
|
+
else:
|
|
902
|
+
# Interactive provider selection
|
|
903
|
+
provider_choices = []
|
|
904
|
+
for name, desc in PROVIDER_INFO.items():
|
|
905
|
+
has_key = bool(current_settings.get_api_key(name))
|
|
906
|
+
status = ""
|
|
907
|
+
if name in ("local", "lmstudio"):
|
|
908
|
+
status = " (local)"
|
|
909
|
+
elif has_key:
|
|
910
|
+
status = " (configured)"
|
|
911
|
+
else:
|
|
912
|
+
status = " (needs key)"
|
|
913
|
+
|
|
914
|
+
is_current = name == current_llm.name
|
|
915
|
+
display = f"{'> ' if is_current else ' '}{name} - {desc}{status}"
|
|
916
|
+
provider_choices.append(questionary.Choice(display, value=name))
|
|
917
|
+
|
|
918
|
+
try:
|
|
919
|
+
new_provider = questionary.select(
|
|
920
|
+
"Select provider:",
|
|
921
|
+
choices=provider_choices,
|
|
922
|
+
style=QUESTIONARY_STYLE,
|
|
923
|
+
instruction="(Use arrow keys)",
|
|
924
|
+
).ask()
|
|
925
|
+
if not new_provider:
|
|
926
|
+
continue
|
|
927
|
+
except (KeyboardInterrupt, EOFError):
|
|
928
|
+
continue
|
|
929
|
+
|
|
930
|
+
if new_provider not in PROVIDERS:
|
|
931
|
+
print_error(f"Unknown provider: {new_provider}")
|
|
932
|
+
continue
|
|
933
|
+
|
|
934
|
+
# Check if provider needs API key and doesn't have one
|
|
935
|
+
needs_key = new_provider not in ("local", "lmstudio")
|
|
936
|
+
has_key = bool(current_settings.get_api_key(new_provider))
|
|
937
|
+
|
|
938
|
+
entered_key = None
|
|
939
|
+
selected_model = None
|
|
940
|
+
|
|
941
|
+
if needs_key and not has_key:
|
|
942
|
+
# Prompt for API key with questionary
|
|
943
|
+
env_var = PROVIDER_ENV_VARS.get(
|
|
944
|
+
new_provider, f"{new_provider.upper()}_API_KEY"
|
|
945
|
+
)
|
|
946
|
+
console.print(
|
|
947
|
+
Panel(
|
|
948
|
+
f"[bold]Setting up {new_provider}[/bold]\n"
|
|
949
|
+
f"[dim]You can also set {env_var} environment variable[/dim]",
|
|
950
|
+
border_style="yellow",
|
|
951
|
+
)
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
try:
|
|
955
|
+
api_key = questionary.password(
|
|
956
|
+
f"Enter your {new_provider} API key:", style=QUESTIONARY_STYLE
|
|
957
|
+
).ask()
|
|
958
|
+
|
|
959
|
+
if api_key and api_key.strip():
|
|
960
|
+
entered_key = api_key.strip()
|
|
961
|
+
|
|
962
|
+
# Store in keyring
|
|
963
|
+
if keyring_available():
|
|
964
|
+
set_api_key(new_provider, entered_key)
|
|
965
|
+
print_success("API key stored in keychain!")
|
|
966
|
+
|
|
967
|
+
# Update settings
|
|
968
|
+
if new_provider not in current_settings.providers:
|
|
969
|
+
current_settings.providers[new_provider] = ProviderConfig()
|
|
970
|
+
current_settings.providers[new_provider].api_key = entered_key
|
|
971
|
+
|
|
972
|
+
# Offer model selection with questionary
|
|
973
|
+
try:
|
|
974
|
+
with console.status("[cyan]Fetching models...", spinner="dots"):
|
|
975
|
+
temp_llm = get_provider(new_provider, api_key=entered_key)
|
|
976
|
+
models = temp_llm.list_models()
|
|
977
|
+
if models:
|
|
978
|
+
model_choices = [
|
|
979
|
+
questionary.Choice(m.id, value=m.id)
|
|
980
|
+
for m in models[:12]
|
|
981
|
+
]
|
|
982
|
+
selected_model = questionary.select(
|
|
983
|
+
"Select a model:",
|
|
984
|
+
choices=model_choices,
|
|
985
|
+
style=QUESTIONARY_STYLE,
|
|
986
|
+
).ask()
|
|
987
|
+
if selected_model:
|
|
988
|
+
current_settings.providers[
|
|
989
|
+
new_provider
|
|
990
|
+
].default_model = selected_model
|
|
991
|
+
except Exception:
|
|
992
|
+
pass # Model selection is optional
|
|
993
|
+
else:
|
|
994
|
+
print_error("API key cannot be empty.")
|
|
995
|
+
continue
|
|
996
|
+
except (KeyboardInterrupt, EOFError):
|
|
997
|
+
console.print("\nCancelled.")
|
|
998
|
+
continue
|
|
999
|
+
|
|
1000
|
+
try:
|
|
1001
|
+
# Use entered key directly if we just got it, otherwise use settings
|
|
1002
|
+
if entered_key:
|
|
1003
|
+
new_llm = get_provider(
|
|
1004
|
+
new_provider, model=selected_model, api_key=entered_key
|
|
1005
|
+
)
|
|
1006
|
+
else:
|
|
1007
|
+
new_llm = create_provider(new_provider, None, current_settings)
|
|
1008
|
+
current_llm = new_llm
|
|
1009
|
+
agent.provider = new_llm
|
|
1010
|
+
|
|
1011
|
+
# Save provider selection to settings
|
|
1012
|
+
try:
|
|
1013
|
+
current_settings.default_provider = new_provider
|
|
1014
|
+
save_config(current_settings)
|
|
1015
|
+
print_success(f"Switched to: {new_provider} ({new_llm.model})")
|
|
1016
|
+
except Exception:
|
|
1017
|
+
print_success(f"Switched to: {new_provider} ({new_llm.model})")
|
|
1018
|
+
except Exception as e:
|
|
1019
|
+
print_error(f"Failed to switch provider: {e}")
|
|
1020
|
+
continue
|
|
1021
|
+
|
|
1022
|
+
elif cmd == "history":
|
|
1023
|
+
console.print("\n[bold]Conversation History[/bold]")
|
|
1024
|
+
if not agent.history:
|
|
1025
|
+
print_info("No messages yet.")
|
|
1026
|
+
else:
|
|
1027
|
+
for _i, msg in enumerate(agent.history[-10:], 1):
|
|
1028
|
+
role_color = {
|
|
1029
|
+
"user": "green",
|
|
1030
|
+
"assistant": "blue",
|
|
1031
|
+
"tool": "yellow",
|
|
1032
|
+
}.get(msg.role, "white")
|
|
1033
|
+
content_preview = (
|
|
1034
|
+
(msg.content[:80] + "...") if len(msg.content) > 80 else msg.content
|
|
1035
|
+
)
|
|
1036
|
+
console.print(
|
|
1037
|
+
f" [{role_color}]{msg.role}[/{role_color}]: {content_preview}"
|
|
1038
|
+
)
|
|
1039
|
+
if len(agent.history) > 10:
|
|
1040
|
+
console.print(
|
|
1041
|
+
f" [dim]... and {len(agent.history) - 10} more messages[/dim]"
|
|
1042
|
+
)
|
|
1043
|
+
console.print()
|
|
1044
|
+
continue
|
|
1045
|
+
|
|
1046
|
+
elif cmd == "undo":
|
|
1047
|
+
if agent.history:
|
|
1048
|
+
# Remove last user message and any following assistant/tool messages
|
|
1049
|
+
removed = 0
|
|
1050
|
+
while agent.history and agent.history[-1].role != "user":
|
|
1051
|
+
agent.history.pop()
|
|
1052
|
+
removed += 1
|
|
1053
|
+
if agent.history and agent.history[-1].role == "user":
|
|
1054
|
+
agent.history.pop()
|
|
1055
|
+
removed += 1
|
|
1056
|
+
print_success(f"Removed {removed} message(s)")
|
|
1057
|
+
else:
|
|
1058
|
+
print_warning("No messages to undo")
|
|
1059
|
+
continue
|
|
1060
|
+
|
|
1061
|
+
elif cmd == "save":
|
|
1062
|
+
filename = (
|
|
1063
|
+
cmd_arg or f"devorch_session_{session_manager.current_session_id}.txt"
|
|
1064
|
+
)
|
|
1065
|
+
try:
|
|
1066
|
+
with open(filename, "w") as f:
|
|
1067
|
+
for msg in agent.history:
|
|
1068
|
+
f.write(f"[{msg.role}]\n{msg.content}\n\n")
|
|
1069
|
+
print_success(f"Saved to: {filename}")
|
|
1070
|
+
except Exception as e:
|
|
1071
|
+
print_error(f"Failed to save: {e}")
|
|
1072
|
+
continue
|
|
1073
|
+
|
|
1074
|
+
elif cmd == "tasks":
|
|
1075
|
+
task_manager = get_task_manager()
|
|
1076
|
+
if task_manager.task_list.total_count == 0:
|
|
1077
|
+
print_info("No tasks in progress.")
|
|
1078
|
+
else:
|
|
1079
|
+
panel = task_manager._create_panel()
|
|
1080
|
+
console.print(panel)
|
|
1081
|
+
continue
|
|
1082
|
+
|
|
1083
|
+
else:
|
|
1084
|
+
print_warning(f"Unknown command: /{cmd}")
|
|
1085
|
+
print_info("Type /help to see available commands")
|
|
1086
|
+
continue
|
|
1087
|
+
|
|
1088
|
+
result = agent.run(user_input, max_iterations=15)
|
|
1089
|
+
print_panel(result, title="DevOrch", border_style="green")
|
|
1090
|
+
|
|
1091
|
+
except (typer.Abort, EOFError):
|
|
1092
|
+
print_info(f"\nSession saved: {session_manager.current_session_id}")
|
|
1093
|
+
break
|
|
1094
|
+
except KeyboardInterrupt:
|
|
1095
|
+
console.print() # New line after ^C
|
|
1096
|
+
continue # Don't exit on Ctrl+C, just cancel current input
|
|
1097
|
+
except Exception as e:
|
|
1098
|
+
error_str = str(e).lower()
|
|
1099
|
+
print_error(str(e))
|
|
1100
|
+
|
|
1101
|
+
# Provide helpful hints for common errors
|
|
1102
|
+
if "401" in error_str or "unauthorized" in error_str or "authentication" in error_str:
|
|
1103
|
+
console.print("[dim] Tip: Your API key may be invalid. Try:[/dim]")
|
|
1104
|
+
console.print("[dim] - /provider <name> to switch providers[/dim]")
|
|
1105
|
+
console.print(
|
|
1106
|
+
f"[dim] - devorch set-key {current_llm.name} to update the key[/dim]"
|
|
1107
|
+
)
|
|
1108
|
+
elif (
|
|
1109
|
+
"402" in error_str
|
|
1110
|
+
or "payment" in error_str
|
|
1111
|
+
or "quota" in error_str
|
|
1112
|
+
or "rate" in error_str
|
|
1113
|
+
):
|
|
1114
|
+
console.print("[dim] Tip: You may have exceeded your quota or rate limit.[/dim]")
|
|
1115
|
+
console.print("[dim] - /provider <name> to switch to another provider[/dim]")
|
|
1116
|
+
elif "connection" in error_str or "timeout" in error_str or "network" in error_str:
|
|
1117
|
+
console.print("[dim] Tip: Network error. Check your connection.[/dim]")
|
|
1118
|
+
if current_llm.name == "local":
|
|
1119
|
+
console.print("[dim] - Make sure Ollama is running: ollama serve[/dim]")
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
@app.callback()
|
|
1123
|
+
def main_callback(
|
|
1124
|
+
ctx: typer.Context,
|
|
1125
|
+
provider: str = typer.Option(None, "--provider", "-p", help="LLM Provider"),
|
|
1126
|
+
model: str = typer.Option(None, "--model", "-m", help="Model name"),
|
|
1127
|
+
resume: str = typer.Option(None, "--resume", "-r", help="Resume session by ID"),
|
|
1128
|
+
):
|
|
1129
|
+
"""
|
|
1130
|
+
DevOrch - Your AI Coding Assistant
|
|
1131
|
+
|
|
1132
|
+
Just run 'devorch' to start chatting!
|
|
1133
|
+
"""
|
|
1134
|
+
# If a subcommand is being invoked, don't run the default behavior
|
|
1135
|
+
if ctx.invoked_subcommand is not None:
|
|
1136
|
+
return
|
|
1137
|
+
|
|
1138
|
+
# No subcommand - run the default REPL behavior
|
|
1139
|
+
settings = Settings.load()
|
|
1140
|
+
|
|
1141
|
+
# Check if we need onboarding
|
|
1142
|
+
if not has_any_provider_configured(settings):
|
|
1143
|
+
configured_provider = run_onboarding()
|
|
1144
|
+
if not configured_provider:
|
|
1145
|
+
raise typer.Exit(1)
|
|
1146
|
+
# Reload settings after onboarding
|
|
1147
|
+
settings = Settings.load()
|
|
1148
|
+
provider = configured_provider
|
|
1149
|
+
|
|
1150
|
+
# Start REPL
|
|
1151
|
+
start_repl(provider=provider, model=model, resume=resume)
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
@app.command()
|
|
1155
|
+
def chat(
|
|
1156
|
+
provider: str = typer.Option(None, "--provider", "-p", help="LLM Provider"),
|
|
1157
|
+
model: str = typer.Option(None, "--model", "-m", help="Model name"),
|
|
1158
|
+
resume: str = typer.Option(None, "--resume", "-r", help="Resume session by ID"),
|
|
1159
|
+
message_limit: int = typer.Option(
|
|
1160
|
+
DEFAULT_MESSAGE_LIMIT, "--limit", "-l", help="Messages before auto-summarization"
|
|
1161
|
+
),
|
|
1162
|
+
):
|
|
1163
|
+
"""
|
|
1164
|
+
Start an interactive chat session (alias for running devorch directly).
|
|
1165
|
+
"""
|
|
1166
|
+
settings = Settings.load()
|
|
1167
|
+
|
|
1168
|
+
if not has_any_provider_configured(settings):
|
|
1169
|
+
configured_provider = run_onboarding()
|
|
1170
|
+
if not configured_provider:
|
|
1171
|
+
raise typer.Exit(1)
|
|
1172
|
+
settings = Settings.load()
|
|
1173
|
+
provider = configured_provider
|
|
1174
|
+
|
|
1175
|
+
start_repl(provider=provider, model=model, resume=resume, message_limit=message_limit)
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
@app.command()
|
|
1179
|
+
def ask(
|
|
1180
|
+
prompt: str = typer.Argument(..., help="The prompt or question for DevOrch"),
|
|
1181
|
+
provider: str = typer.Option(None, "--provider", "-p", help="LLM Provider"),
|
|
1182
|
+
model: str = typer.Option(None, "--model", "-m", help="Model name"),
|
|
1183
|
+
):
|
|
1184
|
+
"""
|
|
1185
|
+
Ask DevOrch a single question (non-interactive).
|
|
1186
|
+
"""
|
|
1187
|
+
settings = Settings.load()
|
|
1188
|
+
|
|
1189
|
+
if not provider:
|
|
1190
|
+
provider = settings.default_provider
|
|
1191
|
+
|
|
1192
|
+
llm = create_provider(provider, model, settings)
|
|
1193
|
+
|
|
1194
|
+
tools = [
|
|
1195
|
+
ShellTool(),
|
|
1196
|
+
OpenTerminalTool(),
|
|
1197
|
+
TerminalSessionTool(),
|
|
1198
|
+
FilesystemTool(),
|
|
1199
|
+
SearchTool(),
|
|
1200
|
+
GrepTool(),
|
|
1201
|
+
EditTool(),
|
|
1202
|
+
TaskTool(),
|
|
1203
|
+
WebSearchTool(),
|
|
1204
|
+
WebFetchTool(),
|
|
1205
|
+
]
|
|
1206
|
+
executor = ToolExecutor(tools=tools)
|
|
1207
|
+
planner = SimplePlanner()
|
|
1208
|
+
|
|
1209
|
+
agent = Agent(provider=llm, planner=planner, executor=executor, tools=tools)
|
|
1210
|
+
|
|
1211
|
+
console.print(f"[dim]Using {llm.name}/{llm.model}[/dim]")
|
|
1212
|
+
try:
|
|
1213
|
+
result = agent.run(prompt, max_iterations=15)
|
|
1214
|
+
print_panel(result, title="DevOrch", border_style="green")
|
|
1215
|
+
except Exception as e:
|
|
1216
|
+
print_error(str(e))
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
@app.command()
|
|
1220
|
+
def config():
|
|
1221
|
+
"""
|
|
1222
|
+
Show current configuration.
|
|
1223
|
+
"""
|
|
1224
|
+
settings = Settings.load()
|
|
1225
|
+
|
|
1226
|
+
console.print("\n[bold]DevOrch Configuration[/bold]\n")
|
|
1227
|
+
console.print(f"Default Provider: [cyan]{settings.default_provider}[/cyan]")
|
|
1228
|
+
console.print(
|
|
1229
|
+
f"Keyring Available: {'[green]yes[/green]' if keyring_available() else '[yellow]no[/yellow]'}"
|
|
1230
|
+
)
|
|
1231
|
+
console.print("\n[bold]Providers:[/bold]")
|
|
1232
|
+
|
|
1233
|
+
for name in PROVIDERS.keys():
|
|
1234
|
+
provider_config = settings.providers.get(name)
|
|
1235
|
+
if provider_config:
|
|
1236
|
+
if name == "local":
|
|
1237
|
+
key_status = "[dim]not required[/dim]"
|
|
1238
|
+
elif provider_config.api_key:
|
|
1239
|
+
if provider_config.key_encrypted:
|
|
1240
|
+
key_status = "[green]configured (encrypted)[/green]"
|
|
1241
|
+
else:
|
|
1242
|
+
key_status = "[green]configured[/green]"
|
|
1243
|
+
else:
|
|
1244
|
+
key_status = "[yellow]not set[/yellow]"
|
|
1245
|
+
model = provider_config.default_model or "default"
|
|
1246
|
+
console.print(f" [bold]{name}[/bold]: {model} (API key: {key_status})")
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
@app.command("set-key")
|
|
1250
|
+
def set_key(
|
|
1251
|
+
provider: str = typer.Argument(..., help="Provider name (openai, anthropic, gemini)"),
|
|
1252
|
+
set_default: bool = typer.Option(
|
|
1253
|
+
True, "--default/--no-default", help="Set as default provider"
|
|
1254
|
+
),
|
|
1255
|
+
):
|
|
1256
|
+
"""
|
|
1257
|
+
Securely store an API key for a provider.
|
|
1258
|
+
"""
|
|
1259
|
+
if provider.lower() not in PROVIDERS:
|
|
1260
|
+
print_error(f"Unknown provider '{provider}'. Available: {', '.join(PROVIDERS.keys())}")
|
|
1261
|
+
raise typer.Exit(1)
|
|
1262
|
+
|
|
1263
|
+
if provider.lower() == "local":
|
|
1264
|
+
print_warning("Local provider doesn't require an API key.")
|
|
1265
|
+
raise typer.Exit(0)
|
|
1266
|
+
|
|
1267
|
+
if not keyring_available():
|
|
1268
|
+
print_error("Keyring is not available on this system.")
|
|
1269
|
+
print_error("Please set API keys via environment variables instead.")
|
|
1270
|
+
raise typer.Exit(1)
|
|
1271
|
+
|
|
1272
|
+
api_key = typer.prompt(f"Enter API key for {provider}", hide_input=True)
|
|
1273
|
+
|
|
1274
|
+
if not api_key.strip():
|
|
1275
|
+
print_error("API key cannot be empty.")
|
|
1276
|
+
raise typer.Exit(1)
|
|
1277
|
+
|
|
1278
|
+
if set_api_key(provider.lower(), api_key.strip()):
|
|
1279
|
+
print_success(f"API key for {provider} stored securely.")
|
|
1280
|
+
|
|
1281
|
+
# Also set as default provider
|
|
1282
|
+
if set_default:
|
|
1283
|
+
settings = Settings.load()
|
|
1284
|
+
settings.default_provider = provider.lower()
|
|
1285
|
+
try:
|
|
1286
|
+
save_config(settings)
|
|
1287
|
+
print_success(f"Set {provider} as default provider.")
|
|
1288
|
+
except Exception:
|
|
1289
|
+
print_warning(
|
|
1290
|
+
f"Key stored but couldn't save as default. Use: devorch -p {provider}"
|
|
1291
|
+
)
|
|
1292
|
+
else:
|
|
1293
|
+
print_error("Failed to store API key.")
|
|
1294
|
+
raise typer.Exit(1)
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
@app.command()
|
|
1298
|
+
def providers():
|
|
1299
|
+
"""
|
|
1300
|
+
List available providers.
|
|
1301
|
+
"""
|
|
1302
|
+
console.print("\n[bold]Available Providers:[/bold]\n")
|
|
1303
|
+
for name in PROVIDERS.keys():
|
|
1304
|
+
console.print(f" - {name}")
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
# Session commands
|
|
1308
|
+
@sessions_app.command("list")
|
|
1309
|
+
def sessions_list(
|
|
1310
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Number of sessions to show"),
|
|
1311
|
+
):
|
|
1312
|
+
"""
|
|
1313
|
+
List recent chat sessions.
|
|
1314
|
+
"""
|
|
1315
|
+
session_manager = SessionManager()
|
|
1316
|
+
sessions = session_manager.list_sessions(limit=limit)
|
|
1317
|
+
|
|
1318
|
+
if not sessions:
|
|
1319
|
+
print_info("No sessions found.")
|
|
1320
|
+
return
|
|
1321
|
+
|
|
1322
|
+
table = Table(title="Chat Sessions")
|
|
1323
|
+
table.add_column("ID", style="cyan")
|
|
1324
|
+
table.add_column("Name", style="white")
|
|
1325
|
+
table.add_column("Provider", style="green")
|
|
1326
|
+
table.add_column("Model", style="blue")
|
|
1327
|
+
table.add_column("Msgs", justify="right")
|
|
1328
|
+
table.add_column("Parent", style="dim")
|
|
1329
|
+
table.add_column("Updated", style="dim")
|
|
1330
|
+
|
|
1331
|
+
for session in sessions:
|
|
1332
|
+
parent = session.get("parent_session_id") or "-"
|
|
1333
|
+
table.add_row(
|
|
1334
|
+
session["id"],
|
|
1335
|
+
(session["name"] or "-")[:20],
|
|
1336
|
+
session["provider"],
|
|
1337
|
+
session["model"][:15],
|
|
1338
|
+
str(session["message_count"]),
|
|
1339
|
+
parent[:8] if parent != "-" else "-",
|
|
1340
|
+
session["updated_at"][:16],
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
console.print(table)
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
@sessions_app.command("show")
|
|
1347
|
+
def sessions_show(session_id: str = typer.Argument(..., help="Session ID to show details")):
|
|
1348
|
+
"""
|
|
1349
|
+
Show details of a specific session.
|
|
1350
|
+
"""
|
|
1351
|
+
session_manager = SessionManager()
|
|
1352
|
+
|
|
1353
|
+
try:
|
|
1354
|
+
session_info, messages = session_manager.load_session(session_id)
|
|
1355
|
+
except ValueError as e:
|
|
1356
|
+
print_error(str(e))
|
|
1357
|
+
raise typer.Exit(1) from e
|
|
1358
|
+
|
|
1359
|
+
console.print(f"\n[bold]Session: {session_id}[/bold]")
|
|
1360
|
+
console.print(f"Name: {session_info.get('name', '-')}")
|
|
1361
|
+
console.print(f"Provider: {session_info['provider']}")
|
|
1362
|
+
console.print(f"Model: {session_info['model']}")
|
|
1363
|
+
console.print(f"Messages: {len(messages)}")
|
|
1364
|
+
|
|
1365
|
+
if session_info.get("parent_session_id"):
|
|
1366
|
+
console.print(f"Parent: {session_info['parent_session_id']}")
|
|
1367
|
+
|
|
1368
|
+
if session_info.get("summary"):
|
|
1369
|
+
console.print("\n[bold]Context Summary:[/bold]")
|
|
1370
|
+
summary = session_info["summary"]
|
|
1371
|
+
print_panel(summary[:500] + "..." if len(summary) > 500 else summary, border_style="dim")
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
@sessions_app.command("delete")
|
|
1375
|
+
def sessions_delete(session_id: str = typer.Argument(..., help="Session ID to delete")):
|
|
1376
|
+
"""
|
|
1377
|
+
Delete a chat session.
|
|
1378
|
+
"""
|
|
1379
|
+
session_manager = SessionManager()
|
|
1380
|
+
|
|
1381
|
+
if not session_manager.session_exists(session_id):
|
|
1382
|
+
print_error(f"Session '{session_id}' not found.")
|
|
1383
|
+
raise typer.Exit(1)
|
|
1384
|
+
|
|
1385
|
+
if session_manager.delete_session(session_id):
|
|
1386
|
+
print_success(f"Session '{session_id}' deleted.")
|
|
1387
|
+
else:
|
|
1388
|
+
print_error("Failed to delete session.")
|
|
1389
|
+
raise typer.Exit(1)
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
@sessions_app.command("clear")
|
|
1393
|
+
def sessions_clear(force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation")):
|
|
1394
|
+
"""
|
|
1395
|
+
Delete all chat sessions.
|
|
1396
|
+
"""
|
|
1397
|
+
session_manager = SessionManager()
|
|
1398
|
+
sessions = session_manager.list_sessions(limit=1000)
|
|
1399
|
+
|
|
1400
|
+
if not sessions:
|
|
1401
|
+
print_info("No sessions to delete.")
|
|
1402
|
+
return
|
|
1403
|
+
|
|
1404
|
+
if not force:
|
|
1405
|
+
confirm = typer.confirm(f"Delete {len(sessions)} sessions?")
|
|
1406
|
+
if not confirm:
|
|
1407
|
+
print_warning("Cancelled.")
|
|
1408
|
+
return
|
|
1409
|
+
|
|
1410
|
+
deleted = 0
|
|
1411
|
+
for session in sessions:
|
|
1412
|
+
if session_manager.delete_session(session["id"]):
|
|
1413
|
+
deleted += 1
|
|
1414
|
+
|
|
1415
|
+
print_success(f"Deleted {deleted} sessions.")
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
# Permission commands
|
|
1419
|
+
@permissions_app.command("list")
|
|
1420
|
+
def permissions_list():
|
|
1421
|
+
"""
|
|
1422
|
+
Show current permission settings.
|
|
1423
|
+
"""
|
|
1424
|
+
permissions = get_permissions()
|
|
1425
|
+
|
|
1426
|
+
console.print("\n[bold]Tool Permissions[/bold]\n")
|
|
1427
|
+
|
|
1428
|
+
for tool_name, perm in permissions.tools.items():
|
|
1429
|
+
level_color = {
|
|
1430
|
+
PermissionLevel.ALLOW: "green",
|
|
1431
|
+
PermissionLevel.DENY: "red",
|
|
1432
|
+
PermissionLevel.ASK: "yellow",
|
|
1433
|
+
}.get(perm.level, "white")
|
|
1434
|
+
|
|
1435
|
+
console.print(
|
|
1436
|
+
f"[bold]{tool_name}[/bold]: [{level_color}]{perm.level.value}[/{level_color}]"
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
if perm.allowed_patterns:
|
|
1440
|
+
console.print(f" [green]Allowed patterns:[/green] {len(perm.allowed_patterns)}")
|
|
1441
|
+
for p in perm.allowed_patterns[:5]:
|
|
1442
|
+
console.print(f" - {p}")
|
|
1443
|
+
if len(perm.allowed_patterns) > 5:
|
|
1444
|
+
console.print(f" [dim]... and {len(perm.allowed_patterns) - 5} more[/dim]")
|
|
1445
|
+
|
|
1446
|
+
if perm.denied_patterns:
|
|
1447
|
+
console.print(f" [red]Denied patterns:[/red] {len(perm.denied_patterns)}")
|
|
1448
|
+
for p in perm.denied_patterns[:3]:
|
|
1449
|
+
console.print(f" - {p}")
|
|
1450
|
+
|
|
1451
|
+
# Session permissions
|
|
1452
|
+
if permissions.session_allowed or permissions.session_denied:
|
|
1453
|
+
console.print("\n[bold]Session Permissions (temporary)[/bold]")
|
|
1454
|
+
if permissions.session_allowed:
|
|
1455
|
+
console.print(f" [green]Allowed:[/green] {', '.join(permissions.session_allowed)}")
|
|
1456
|
+
if permissions.session_denied:
|
|
1457
|
+
console.print(f" [red]Denied:[/red] {', '.join(permissions.session_denied)}")
|
|
1458
|
+
|
|
1459
|
+
console.print(f"\n[dim]Config file: {PERMISSIONS_FILE}[/dim]")
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
@permissions_app.command("allow")
|
|
1463
|
+
def permissions_allow(
|
|
1464
|
+
tool: str = typer.Argument(..., help="Tool name (shell, filesystem)"),
|
|
1465
|
+
pattern: str = typer.Argument(..., help="Command pattern to allow (e.g., 'git *')"),
|
|
1466
|
+
):
|
|
1467
|
+
"""
|
|
1468
|
+
Add a pattern to the allowed list for a tool.
|
|
1469
|
+
"""
|
|
1470
|
+
permissions = get_permissions()
|
|
1471
|
+
permissions.add_allowed_pattern(tool, pattern, session_only=False)
|
|
1472
|
+
print_success(f"Added to allowed patterns for {tool}: {pattern}")
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
@permissions_app.command("deny")
|
|
1476
|
+
def permissions_deny(
|
|
1477
|
+
tool: str = typer.Argument(..., help="Tool name (shell, filesystem)"),
|
|
1478
|
+
pattern: str = typer.Argument(..., help="Command pattern to deny"),
|
|
1479
|
+
):
|
|
1480
|
+
"""
|
|
1481
|
+
Add a pattern to the denied list for a tool.
|
|
1482
|
+
"""
|
|
1483
|
+
permissions = get_permissions()
|
|
1484
|
+
permissions.add_denied_pattern(tool, pattern, session_only=False)
|
|
1485
|
+
print_success(f"Added to denied patterns for {tool}: {pattern}")
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
@permissions_app.command("set")
|
|
1489
|
+
def permissions_set(
|
|
1490
|
+
tool: str = typer.Argument(..., help="Tool name (shell, filesystem, search)"),
|
|
1491
|
+
level: str = typer.Argument(..., help="Permission level (allow, deny, ask)"),
|
|
1492
|
+
):
|
|
1493
|
+
"""
|
|
1494
|
+
Set the default permission level for a tool.
|
|
1495
|
+
"""
|
|
1496
|
+
try:
|
|
1497
|
+
perm_level = PermissionLevel(level.lower())
|
|
1498
|
+
except ValueError:
|
|
1499
|
+
print_error(f"Invalid level: {level}. Use: allow, deny, or ask")
|
|
1500
|
+
raise typer.Exit(1) from None
|
|
1501
|
+
|
|
1502
|
+
permissions = get_permissions()
|
|
1503
|
+
permissions.set_tool_permission(tool, perm_level)
|
|
1504
|
+
print_success(f"Set {tool} permission to: {perm_level.value}")
|
|
1505
|
+
|
|
1506
|
+
|
|
1507
|
+
@permissions_app.command("reset")
|
|
1508
|
+
def permissions_reset(force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation")):
|
|
1509
|
+
"""
|
|
1510
|
+
Reset all permissions to defaults.
|
|
1511
|
+
"""
|
|
1512
|
+
if not force:
|
|
1513
|
+
confirm = typer.confirm("Reset all permissions to defaults?")
|
|
1514
|
+
if not confirm:
|
|
1515
|
+
print_warning("Cancelled.")
|
|
1516
|
+
return
|
|
1517
|
+
|
|
1518
|
+
reset_permissions()
|
|
1519
|
+
print_success("Permissions reset to defaults.")
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
def main():
|
|
1523
|
+
app()
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
if __name__ == "__main__":
|
|
1527
|
+
main()
|