devpilot-agentic-cli 1.0.2__py3-none-any.whl → 1.0.4__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.
- agent/cli.py +112 -15
- agent/config.py +21 -6
- agent/context.py +6 -8
- agent/loop.py +4 -2
- agent/mcp_client.py +67 -4
- agent/setup_wizard.py +93 -35
- agent/tools/registry.py +10 -12
- agent/tui/app.py +15 -10
- {devpilot_agentic_cli-1.0.2.dist-info → devpilot_agentic_cli-1.0.4.dist-info}/METADATA +18 -18
- {devpilot_agentic_cli-1.0.2.dist-info → devpilot_agentic_cli-1.0.4.dist-info}/RECORD +13 -13
- {devpilot_agentic_cli-1.0.2.dist-info → devpilot_agentic_cli-1.0.4.dist-info}/WHEEL +0 -0
- {devpilot_agentic_cli-1.0.2.dist-info → devpilot_agentic_cli-1.0.4.dist-info}/entry_points.txt +0 -0
- {devpilot_agentic_cli-1.0.2.dist-info → devpilot_agentic_cli-1.0.4.dist-info}/top_level.txt +0 -0
agent/cli.py
CHANGED
|
@@ -12,18 +12,99 @@ import argparse
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import os
|
|
14
14
|
import sys
|
|
15
|
+
import time
|
|
15
16
|
from datetime import datetime
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
|
|
20
|
+
def _fix_windows_console() -> None:
|
|
21
|
+
"""
|
|
22
|
+
Enable ANSI/VT100 support and UTF-8 encoding in Windows terminals.
|
|
23
|
+
|
|
24
|
+
Root cause of the blank black CMD screen:
|
|
25
|
+
- CMD.exe does NOT enable Virtual Terminal Processing by default.
|
|
26
|
+
- Textual sends ANSI escape sequences to probe terminal capabilities.
|
|
27
|
+
- Without VT support, CMD either hangs or renders nothing.
|
|
28
|
+
- PowerShell has VT enabled by default → works immediately.
|
|
29
|
+
|
|
30
|
+
Fixes applied:
|
|
31
|
+
1. Enable ENABLE_VIRTUAL_TERMINAL_PROCESSING via Windows API (ctypes).
|
|
32
|
+
2. Switch stdout/stderr to UTF-8 so → ✓ etc don't crash in cp1252.
|
|
33
|
+
3. Set TERM / COLORTERM so Textual skips slow capability negotiation.
|
|
34
|
+
"""
|
|
35
|
+
if sys.platform != "win32":
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# ── 1. Enable Virtual Terminal Processing ─────────────────────────────────
|
|
39
|
+
try:
|
|
40
|
+
import ctypes
|
|
41
|
+
import ctypes.wintypes
|
|
42
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
43
|
+
|
|
44
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
45
|
+
ENABLE_PROCESSED_OUTPUT = 0x0001
|
|
46
|
+
|
|
47
|
+
for handle_id in (-10, -11): # STD_INPUT=-10, STD_OUTPUT=-11
|
|
48
|
+
handle = kernel32.GetStdHandle(handle_id)
|
|
49
|
+
if handle and handle != -1:
|
|
50
|
+
mode = ctypes.wintypes.DWORD(0)
|
|
51
|
+
if kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
52
|
+
new_mode = (mode.value
|
|
53
|
+
| ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
54
|
+
| ENABLE_PROCESSED_OUTPUT)
|
|
55
|
+
kernel32.SetConsoleMode(handle, new_mode)
|
|
56
|
+
except Exception:
|
|
57
|
+
pass # non-fatal — worst case CMD is slow, not broken
|
|
58
|
+
|
|
59
|
+
# ── 2. Switch stdout/stderr to UTF-8 ──────────────────────────────────────
|
|
60
|
+
# Prevents UnicodeEncodeError on → ✓ ✗ characters in cp1252 CMD sessions
|
|
61
|
+
try:
|
|
62
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
63
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
64
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
65
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
66
|
+
# Also tell Windows console host to use UTF-8 codepage
|
|
67
|
+
import ctypes
|
|
68
|
+
ctypes.windll.kernel32.SetConsoleOutputCP(65001) # type: ignore[attr-defined]
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# ── 3. Hint terminal type so Textual doesn't hang on capability detection ─
|
|
73
|
+
# Textual checks these env vars first; without them it sends probe sequences
|
|
74
|
+
# that CMD ignores, causing a multi-second hang.
|
|
75
|
+
os.environ.setdefault("TERM", "xterm-256color")
|
|
76
|
+
os.environ.setdefault("COLORTERM", "truecolor")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Run immediately — must happen before any rich/textual import
|
|
80
|
+
_fix_windows_console()
|
|
81
|
+
|
|
82
|
+
# Python version guard — must run before any other import
|
|
83
|
+
if sys.version_info < (3, 11):
|
|
84
|
+
print(
|
|
85
|
+
f"\n[DevPilot] Python 3.11 or newer is required.\n"
|
|
86
|
+
f" You are running Python {sys.version_info.major}.{sys.version_info.minor}.\n"
|
|
87
|
+
f" Download the latest Python from: https://python.org/downloads/\n"
|
|
88
|
+
)
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
# Module-level imports: only lightweight config/ui — everything else is lazy inside main_async()
|
|
19
92
|
from agent.config import Config, ConfigError
|
|
20
|
-
from agent.history import HistoryManager
|
|
21
|
-
from agent.loop import run_agent_loop
|
|
22
|
-
from agent.mcp_client import MCPManager
|
|
23
93
|
from agent.providers.factory import create_provider
|
|
24
|
-
from agent.tools import ToolRegistry
|
|
25
94
|
from agent.ui import UI
|
|
26
95
|
|
|
96
|
+
_T0 = time.perf_counter()
|
|
97
|
+
|
|
98
|
+
def _log_startup(msg: str) -> None:
|
|
99
|
+
"""Print a startup progress line. Helps users see DevPilot is loading."""
|
|
100
|
+
elapsed = time.perf_counter() - _T0
|
|
101
|
+
# Use plain ASCII fallback so CMD doesn't choke on escape codes if VT failed
|
|
102
|
+
print(f"\r[{elapsed:.1f}s] {msg}...", end="", flush=True)
|
|
103
|
+
|
|
104
|
+
def _clear_startup() -> None:
|
|
105
|
+
"""Clear the startup progress line once TUI takes over."""
|
|
106
|
+
print("\r" + " " * 60 + "\r", end="", flush=True)
|
|
107
|
+
|
|
27
108
|
|
|
28
109
|
def parse_args() -> argparse.Namespace:
|
|
29
110
|
parser = argparse.ArgumentParser(
|
|
@@ -88,7 +169,8 @@ def _ensure_api_key(config: Config, args: argparse.Namespace) -> Config:
|
|
|
88
169
|
# Force re-run wizard if --setup flag passed
|
|
89
170
|
if args.setup or not config.active_api_key:
|
|
90
171
|
from agent.setup_wizard import run_setup_wizard
|
|
91
|
-
|
|
172
|
+
_user_env = Path.home() / ".devpilot" / ".env"
|
|
173
|
+
success = run_setup_wizard(env_path=_user_env)
|
|
92
174
|
if not success:
|
|
93
175
|
# Wizard skipped (CI/no TTY) or failed — print helpful error
|
|
94
176
|
UI.print_error(
|
|
@@ -115,6 +197,7 @@ async def main_async() -> None:
|
|
|
115
197
|
_apply_cli_overrides(args)
|
|
116
198
|
|
|
117
199
|
# Load config
|
|
200
|
+
_log_startup("Loading configuration")
|
|
118
201
|
try:
|
|
119
202
|
config = Config.load()
|
|
120
203
|
except ConfigError as e:
|
|
@@ -123,8 +206,11 @@ async def main_async() -> None:
|
|
|
123
206
|
|
|
124
207
|
# Handle --setup flag (force wizard even if key exists)
|
|
125
208
|
if args.setup:
|
|
209
|
+
_clear_startup()
|
|
126
210
|
from agent.setup_wizard import run_setup_wizard
|
|
127
|
-
|
|
211
|
+
_user_env = Path.home() / ".devpilot" / ".env"
|
|
212
|
+
run_setup_wizard(env_path=_user_env)
|
|
213
|
+
|
|
128
214
|
try:
|
|
129
215
|
config = Config.load()
|
|
130
216
|
except ConfigError as e:
|
|
@@ -141,28 +227,36 @@ async def main_async() -> None:
|
|
|
141
227
|
UI.print_error(str(e))
|
|
142
228
|
sys.exit(1)
|
|
143
229
|
|
|
144
|
-
# Wire up provider, context, registry
|
|
230
|
+
# Wire up provider, context, registry — lazy imports keep startup lean
|
|
231
|
+
_log_startup("Initializing AI provider")
|
|
145
232
|
provider = create_provider(config)
|
|
146
233
|
|
|
234
|
+
_log_startup("Scanning project context")
|
|
147
235
|
from agent.context import RepoContext
|
|
236
|
+
from agent.history import HistoryManager
|
|
237
|
+
from agent.tools import ToolRegistry
|
|
148
238
|
repo_context = RepoContext(config.workdir)
|
|
149
239
|
registry = ToolRegistry(config, _context=repo_context)
|
|
150
240
|
|
|
151
|
-
# MCP
|
|
241
|
+
# MCP — lazy import to avoid loading the mcp library at startup
|
|
242
|
+
_log_startup("Connecting MCP servers")
|
|
243
|
+
from agent.mcp_client import MCPManager
|
|
152
244
|
mcp_config_path = Path("mcp_servers.json")
|
|
153
245
|
if not mcp_config_path.exists():
|
|
154
246
|
mcp_config_path = Path(__file__).parent.parent / "mcp_servers.json"
|
|
155
247
|
mcp_manager = MCPManager(mcp_config_path)
|
|
156
248
|
await mcp_manager.connect_all(registry)
|
|
157
249
|
|
|
158
|
-
# A2A server
|
|
159
|
-
|
|
160
|
-
a2a_app.state.registry = registry
|
|
161
|
-
|
|
250
|
+
# A2A server — only import fastapi/uvicorn when A2A is actually enabled
|
|
251
|
+
_log_startup("Starting A2A server")
|
|
162
252
|
if config.a2a_enabled:
|
|
253
|
+
from agent.a2a_server import app as a2a_app
|
|
163
254
|
import uvicorn
|
|
164
255
|
import socket
|
|
165
|
-
|
|
256
|
+
|
|
257
|
+
a2a_app.state.config = config
|
|
258
|
+
a2a_app.state.registry = registry
|
|
259
|
+
|
|
166
260
|
# Check if port is available
|
|
167
261
|
port_in_use = False
|
|
168
262
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
@@ -170,7 +264,7 @@ async def main_async() -> None:
|
|
|
170
264
|
s.bind(("0.0.0.0", config.a2a_port))
|
|
171
265
|
except OSError:
|
|
172
266
|
port_in_use = True
|
|
173
|
-
|
|
267
|
+
|
|
174
268
|
if port_in_use:
|
|
175
269
|
UI.print_error(f"Port {config.a2a_port} is already in use. A2A server disabled. Use --a2a-port <port> to change.")
|
|
176
270
|
a2a_server = None
|
|
@@ -207,6 +301,7 @@ async def main_async() -> None:
|
|
|
207
301
|
|
|
208
302
|
# ── CI / single-task mode ─────────────────────────────────────────────────
|
|
209
303
|
if args.task:
|
|
304
|
+
from agent.loop import run_agent_loop
|
|
210
305
|
history.append(provider.make_user_message(args.task))
|
|
211
306
|
await run_agent_loop(
|
|
212
307
|
provider=provider,
|
|
@@ -224,7 +319,9 @@ async def main_async() -> None:
|
|
|
224
319
|
sys.exit(0)
|
|
225
320
|
|
|
226
321
|
# ── Interactive TUI mode ──────────────────────────────────────────────────
|
|
322
|
+
_log_startup("Starting DevPilot TUI")
|
|
227
323
|
from agent.tui.app import DevPilotApp
|
|
324
|
+
_clear_startup() # erase progress line before TUI takes over the screen
|
|
228
325
|
app = DevPilotApp(
|
|
229
326
|
provider=provider,
|
|
230
327
|
registry=registry,
|
agent/config.py
CHANGED
|
@@ -17,8 +17,19 @@ from typing import Literal
|
|
|
17
17
|
|
|
18
18
|
from dotenv import load_dotenv
|
|
19
19
|
|
|
20
|
-
# ──
|
|
21
|
-
|
|
20
|
+
# ── Persistent user config: ~/.devpilot/.env ─────────────────────────────────
|
|
21
|
+
# This is where the setup wizard saves configuration so it persists
|
|
22
|
+
# regardless of which directory `devpilot` is run from.
|
|
23
|
+
_USER_CONFIG_DIR = Path.home() / ".devpilot"
|
|
24
|
+
_USER_ENV_FILE = _USER_CONFIG_DIR / ".env"
|
|
25
|
+
|
|
26
|
+
# Load global user config first — this is where the setup wizard saves settings
|
|
27
|
+
load_dotenv(dotenv_path=_USER_ENV_FILE, override=False)
|
|
28
|
+
|
|
29
|
+
# Load project-local .env second — can ADD vars but won't override global config
|
|
30
|
+
# (override=False means global ~/.devpilot/.env always wins)
|
|
31
|
+
load_dotenv(dotenv_path=Path(".") / ".env", override=False)
|
|
32
|
+
|
|
22
33
|
|
|
23
34
|
|
|
24
35
|
# ── Custom exception ─────────────────────────────────────────────────────────
|
|
@@ -32,8 +43,8 @@ class ConfigError(Exception):
|
|
|
32
43
|
Provider = Literal["anthropic", "openai"]
|
|
33
44
|
|
|
34
45
|
PROVIDER_DEFAULTS: dict[str, str] = {
|
|
35
|
-
"anthropic": "claude-opus-4-5",
|
|
36
|
-
"openai":
|
|
46
|
+
"anthropic": "claude-opus-4-5-20251101",
|
|
47
|
+
"openai": "gpt-5.4",
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
# Keys required per provider (checked lazily so we can build/test without keys)
|
|
@@ -43,11 +54,15 @@ REQUIRED_ENV_KEYS: dict[str, str] = {
|
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
# Models that support extended thinking (Anthropic only)
|
|
57
|
+
# Verified via Anthropic API — May 2026
|
|
46
58
|
THINKING_CAPABLE_MODELS: set[str] = {
|
|
59
|
+
"claude-opus-4-7",
|
|
60
|
+
"claude-opus-4-6",
|
|
61
|
+
"claude-opus-4-5-20251101",
|
|
62
|
+
"claude-opus-4-1-20250805",
|
|
63
|
+
"claude-sonnet-4-5-20250929",
|
|
47
64
|
"claude-3-7-sonnet-20250219",
|
|
48
65
|
"claude-3-5-sonnet-20241022",
|
|
49
|
-
"claude-opus-4-5",
|
|
50
|
-
"claude-opus-4-20250514",
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
|
agent/context.py
CHANGED
|
@@ -145,33 +145,31 @@ class RepoContext:
|
|
|
145
145
|
return sigs
|
|
146
146
|
|
|
147
147
|
def _build_project_tree(self, max_entries: int = 200) -> list[str]:
|
|
148
|
+
"""
|
|
149
|
+
Build a fast file-listing project tree.
|
|
150
|
+
AST signature extraction is intentionally skipped here to avoid
|
|
151
|
+
blocking startup — signatures are extracted lazily on file read.
|
|
152
|
+
"""
|
|
148
153
|
entries: list[str] = []
|
|
149
154
|
ignores = {
|
|
150
|
-
".git", "node_modules", ".venv", "__pycache__", "dist",
|
|
155
|
+
".git", "node_modules", ".venv", "__pycache__", "dist",
|
|
151
156
|
"build", ".next", ".tox", "coverage_html_report", ".devpilot_sessions"
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
def _traverse(directory: Path, prefix: str = "") -> None:
|
|
155
160
|
if len(entries) >= max_entries:
|
|
156
161
|
return
|
|
157
|
-
|
|
158
162
|
try:
|
|
159
163
|
for item in sorted(directory.iterdir()):
|
|
160
164
|
if item.name.startswith(".") and item.name not in (".env", ".gitignore", ".github"):
|
|
161
165
|
continue
|
|
162
166
|
if item.name in ignores or item.name.endswith(".egg-info"):
|
|
163
167
|
continue
|
|
164
|
-
|
|
165
168
|
if item.is_dir():
|
|
166
169
|
entries.append(f"{prefix}📁 {item.name}/")
|
|
167
170
|
_traverse(item, prefix + " ")
|
|
168
171
|
else:
|
|
169
172
|
entries.append(f"{prefix}📄 {item.name}")
|
|
170
|
-
if item.suffix in (".py", ".js", ".ts"):
|
|
171
|
-
sigs = self._extract_signatures(item)
|
|
172
|
-
for sig in sigs:
|
|
173
|
-
entries.append(f"{prefix}{sig}")
|
|
174
|
-
|
|
175
173
|
if len(entries) >= max_entries:
|
|
176
174
|
entries.append("… (truncated to ~200 items for context size)")
|
|
177
175
|
break
|
agent/loop.py
CHANGED
|
@@ -56,8 +56,10 @@ async def run_agent_loop(
|
|
|
56
56
|
if response.thinking:
|
|
57
57
|
UI.print_thinking_block(response.thinking)
|
|
58
58
|
|
|
59
|
-
# UI:
|
|
60
|
-
|
|
59
|
+
# UI: always fire AssistantMessageEvent so the TUI can finalize the response.
|
|
60
|
+
# For streamed responses the TUI uses the already-buffered text from StreamTokenEvents
|
|
61
|
+
# and ignores event.text to avoid duplication. For non-streamed it uses event.text directly.
|
|
62
|
+
if response.text:
|
|
61
63
|
UI.print_assistant_message(response.text)
|
|
62
64
|
|
|
63
65
|
if not response.has_tool_uses:
|
agent/mcp_client.py
CHANGED
|
@@ -8,6 +8,7 @@ and registers them into the ToolRegistry.
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
import asyncio
|
|
11
|
+
import shutil
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
13
14
|
from mcp.client.session import ClientSession
|
|
@@ -18,6 +19,43 @@ from agent.tools import ToolRegistry, ToolResult
|
|
|
18
19
|
from agent.ui import UI
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
# ── Install hints for common MCP server commands ─────────────────────────────
|
|
23
|
+
|
|
24
|
+
_INSTALL_HINTS: dict[str, str] = {
|
|
25
|
+
"npx": "Node.js is not installed or not on PATH.\n"
|
|
26
|
+
" → Install it from https://nodejs.org/ (LTS version recommended)\n"
|
|
27
|
+
" → After installing, restart your terminal and try again.",
|
|
28
|
+
"node": "Node.js is not installed or not on PATH.\n"
|
|
29
|
+
" → Install it from https://nodejs.org/",
|
|
30
|
+
"uvx": "'uvx' (uv tool runner) is not installed.\n"
|
|
31
|
+
" → Install uv: https://docs.astral.sh/uv/getting-started/installation/",
|
|
32
|
+
"docker": "Docker is not installed or not running.\n"
|
|
33
|
+
" → Install Docker Desktop from https://www.docker.com/products/docker-desktop/",
|
|
34
|
+
"python": "Python is not on PATH (unexpected — DevPilot itself runs on Python).",
|
|
35
|
+
"python3":"Python 3 is not on PATH.",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _check_command(command: str, server_name: str) -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Return True if `command` is available on PATH.
|
|
42
|
+
If not, print a clear, actionable error and return False.
|
|
43
|
+
"""
|
|
44
|
+
if shutil.which(command) is not None:
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
hint = _INSTALL_HINTS.get(command.lower(),
|
|
48
|
+
f"'{command}' was not found on your PATH.\n"
|
|
49
|
+
f" → Make sure it is installed and your terminal can see it."
|
|
50
|
+
)
|
|
51
|
+
UI.print_error(
|
|
52
|
+
f"MCP server '{server_name}' requires '{command}' but it was not found.\n"
|
|
53
|
+
f" {hint}\n"
|
|
54
|
+
f" This MCP server will be skipped. Disable it in mcp_servers.json to suppress this message."
|
|
55
|
+
)
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
21
59
|
class MCPManager:
|
|
22
60
|
"""Manages connections to multiple MCP servers."""
|
|
23
61
|
|
|
@@ -79,7 +117,14 @@ class MCPManager:
|
|
|
79
117
|
ready_event.set()
|
|
80
118
|
|
|
81
119
|
async def connect_all(self, registry: ToolRegistry) -> None:
|
|
82
|
-
"""Connect to all servers in mcp_servers.json and register tools.
|
|
120
|
+
"""Connect to all servers in mcp_servers.json and register tools.
|
|
121
|
+
|
|
122
|
+
Uses a non-blocking approach: MCP servers connect in background tasks.
|
|
123
|
+
Startup waits at most MCP_CONNECT_TIMEOUT seconds total so a slow
|
|
124
|
+
npx download never freezes DevPilot at launch.
|
|
125
|
+
"""
|
|
126
|
+
MCP_CONNECT_TIMEOUT = 2.0 # seconds — enough for cached/local servers; slow npx downloads skip
|
|
127
|
+
|
|
83
128
|
if not self.config_path.exists():
|
|
84
129
|
return
|
|
85
130
|
|
|
@@ -92,7 +137,7 @@ class MCPManager:
|
|
|
92
137
|
return
|
|
93
138
|
|
|
94
139
|
if isinstance(servers, dict):
|
|
95
|
-
server_items = servers.items()
|
|
140
|
+
server_items = list(servers.items())
|
|
96
141
|
else:
|
|
97
142
|
server_items = [(s.get("name", f"server_{i}"), s) for i, s in enumerate(servers)]
|
|
98
143
|
|
|
@@ -100,7 +145,7 @@ class MCPManager:
|
|
|
100
145
|
for name, server_config in server_items:
|
|
101
146
|
if server_config.get("enabled", True) is False:
|
|
102
147
|
continue
|
|
103
|
-
|
|
148
|
+
|
|
104
149
|
command = server_config.get("command")
|
|
105
150
|
args = server_config.get("args", [])
|
|
106
151
|
|
|
@@ -108,6 +153,10 @@ class MCPManager:
|
|
|
108
153
|
UI.print_error(f"MCP server '{name}' missing 'command'. Skipping.")
|
|
109
154
|
continue
|
|
110
155
|
|
|
156
|
+
# Pre-flight: make sure the binary is actually installed
|
|
157
|
+
if not _check_command(command, name):
|
|
158
|
+
continue
|
|
159
|
+
|
|
111
160
|
ready_event = asyncio.Event()
|
|
112
161
|
events.append(ready_event)
|
|
113
162
|
task = asyncio.create_task(
|
|
@@ -116,7 +165,21 @@ class MCPManager:
|
|
|
116
165
|
self._tasks.append(task)
|
|
117
166
|
|
|
118
167
|
if events:
|
|
119
|
-
|
|
168
|
+
# Wait for all servers to be ready, but cap total wait time.
|
|
169
|
+
# If any server is slow (e.g. npx downloading a package), DevPilot
|
|
170
|
+
# still starts promptly and the server connects in the background.
|
|
171
|
+
try:
|
|
172
|
+
await asyncio.wait_for(
|
|
173
|
+
asyncio.gather(*(e.wait() for e in events)),
|
|
174
|
+
timeout=MCP_CONNECT_TIMEOUT,
|
|
175
|
+
)
|
|
176
|
+
except asyncio.TimeoutError:
|
|
177
|
+
pending = sum(1 for e in events if not e.is_set())
|
|
178
|
+
UI.print_info(
|
|
179
|
+
f"{pending} MCP server(s) still connecting in background "
|
|
180
|
+
f"(took >{MCP_CONNECT_TIMEOUT:.0f}s). DevPilot is ready now."
|
|
181
|
+
)
|
|
182
|
+
|
|
120
183
|
|
|
121
184
|
async def close(self) -> None:
|
|
122
185
|
"""Close all connections."""
|
agent/setup_wizard.py
CHANGED
|
@@ -25,24 +25,69 @@ from rich.table import Table
|
|
|
25
25
|
|
|
26
26
|
console = Console()
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
|
|
29
|
+
def _prompt_secret(prompt_text: str) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Prompt for a secret (API key) showing '*' for each character typed.
|
|
32
|
+
Works on Windows (CMD / PowerShell) where rich's password=True renders
|
|
33
|
+
completely blank and provides no visual feedback to the user.
|
|
34
|
+
Falls back to rich Prompt (password=True) on non-TTY / CI environments.
|
|
35
|
+
"""
|
|
36
|
+
import sys
|
|
37
|
+
if not sys.stdin.isatty():
|
|
38
|
+
return Prompt.ask(prompt_text, password=True).strip()
|
|
39
|
+
|
|
40
|
+
import platform
|
|
41
|
+
if platform.system() == "Windows":
|
|
42
|
+
import msvcrt
|
|
43
|
+
console.print(f"{prompt_text}: ", end="")
|
|
44
|
+
chars: list[str] = []
|
|
45
|
+
while True:
|
|
46
|
+
ch = msvcrt.getwch() # read one char without echo
|
|
47
|
+
if ch in ("\r", "\n"): # Enter key
|
|
48
|
+
console.print() # move to next line
|
|
49
|
+
break
|
|
50
|
+
elif ch == "\x03": # Ctrl+C
|
|
51
|
+
raise KeyboardInterrupt
|
|
52
|
+
elif ch in ("\x08", "\x7f"): # Backspace
|
|
53
|
+
if chars:
|
|
54
|
+
chars.pop()
|
|
55
|
+
# erase last asterisk: backspace + space + backspace
|
|
56
|
+
console.print("\b \b", end="")
|
|
57
|
+
elif ch == "\x00" or ch == "\xe0": # special key prefix — skip next byte
|
|
58
|
+
msvcrt.getwch()
|
|
59
|
+
else:
|
|
60
|
+
chars.append(ch)
|
|
61
|
+
console.print("*", end="")
|
|
62
|
+
return "".join(chars).strip()
|
|
63
|
+
else:
|
|
64
|
+
# Unix: getpass already hides input cleanly
|
|
65
|
+
import getpass
|
|
66
|
+
return getpass.getpass(f"{prompt_text}: ").strip()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── Model lists (verified against live APIs) ──────────────────────────────────
|
|
70
|
+
# Last verified: May 2026
|
|
29
71
|
|
|
30
72
|
_ANTHROPIC_MODELS = [
|
|
31
|
-
|
|
32
|
-
("claude-
|
|
33
|
-
("claude-
|
|
34
|
-
("claude-
|
|
73
|
+
# Verified via GET https://api.anthropic.com/v1/models
|
|
74
|
+
("claude-opus-4-7", "Newest — most capable Claude (latest)"),
|
|
75
|
+
("claude-opus-4-5-20251101", "Claude Opus 4.5 — powerful & reliable"),
|
|
76
|
+
("claude-sonnet-4-5-20250929", "Claude Sonnet 4.5 — balanced speed & quality"),
|
|
77
|
+
("claude-haiku-4-5-20251001", "Claude Haiku 4.5 — fastest, most affordable"),
|
|
35
78
|
]
|
|
36
79
|
|
|
37
80
|
_OPENAI_MODELS = [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
("
|
|
41
|
-
("
|
|
81
|
+
# Verified via platform.openai.com/docs/models (May 2026)
|
|
82
|
+
# Note: gpt-4o, o3, o4-mini are retired as of early 2026
|
|
83
|
+
("gpt-5.5", "Flagship — best reasoning & coding"),
|
|
84
|
+
("gpt-5.4", "Primary model — coding & professional work"),
|
|
85
|
+
("gpt-5.4-mini", "Fast & cost-efficient"),
|
|
86
|
+
("gpt-5.4-nano", "Fastest — high-volume low-latency tasks"),
|
|
42
87
|
]
|
|
43
88
|
|
|
44
89
|
# OpenAI-compatible third-party providers
|
|
45
|
-
# (
|
|
90
|
+
# (display_name, key_url, key_env_var, base_url, models)
|
|
46
91
|
_COMPATIBLE_PROVIDERS = {
|
|
47
92
|
"1": (
|
|
48
93
|
"Groq",
|
|
@@ -50,10 +95,12 @@ _COMPATIBLE_PROVIDERS = {
|
|
|
50
95
|
"GROQ_API_KEY",
|
|
51
96
|
"https://api.groq.com/openai/v1",
|
|
52
97
|
[
|
|
53
|
-
|
|
54
|
-
("llama-3.
|
|
55
|
-
("
|
|
56
|
-
("
|
|
98
|
+
# Verified via GET https://api.groq.com/openai/v1/models
|
|
99
|
+
("llama-3.3-70b-versatile", "Llama 3.3 70B — best quality (131k ctx)"),
|
|
100
|
+
("meta-llama/llama-4-scout-17b-16e-instruct","Llama 4 Scout 17B — latest & fast"),
|
|
101
|
+
("qwen/qwen3-32b", "Qwen3 32B — strong reasoning"),
|
|
102
|
+
("llama-3.1-8b-instant", "Llama 3.1 8B — ultra-fast"),
|
|
103
|
+
("groq/compound", "Groq Compound — agentic tasks"),
|
|
57
104
|
],
|
|
58
105
|
),
|
|
59
106
|
"2": (
|
|
@@ -62,9 +109,12 @@ _COMPATIBLE_PROVIDERS = {
|
|
|
62
109
|
"TOGETHER_API_KEY",
|
|
63
110
|
"https://api.together.xyz/v1",
|
|
64
111
|
[
|
|
65
|
-
|
|
66
|
-
("meta-llama/Llama-
|
|
67
|
-
("
|
|
112
|
+
# Verified via Together AI docs (May 2026)
|
|
113
|
+
("meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", "Llama 4 Maverick — best quality"),
|
|
114
|
+
("meta-llama/Llama-4-Scout-17B-16E-Instruct-FP8", "Llama 4 Scout — fast MoE"),
|
|
115
|
+
("meta-llama/Llama-3.3-70B-Instruct-Turbo", "Llama 3.3 70B Turbo — reliable"),
|
|
116
|
+
("meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", "Llama 3.1 405B — most capable"),
|
|
117
|
+
("meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", "Llama 3.1 8B — very fast"),
|
|
68
118
|
],
|
|
69
119
|
),
|
|
70
120
|
"3": (
|
|
@@ -73,9 +123,10 @@ _COMPATIBLE_PROVIDERS = {
|
|
|
73
123
|
"MISTRAL_API_KEY",
|
|
74
124
|
"https://api.mistral.ai/v1",
|
|
75
125
|
[
|
|
76
|
-
|
|
77
|
-
("mistral-
|
|
78
|
-
("
|
|
126
|
+
# Verified via Mistral docs — latest aliases always point to newest stable
|
|
127
|
+
("mistral-large-latest", "Most capable Mistral model"),
|
|
128
|
+
("mistral-small-latest", "Fast and affordable"),
|
|
129
|
+
("codestral-latest", "Optimised for code generation"),
|
|
79
130
|
],
|
|
80
131
|
),
|
|
81
132
|
"4": (
|
|
@@ -84,10 +135,11 @@ _COMPATIBLE_PROVIDERS = {
|
|
|
84
135
|
"OLLAMA_API_KEY", # Ollama doesn't need a real key
|
|
85
136
|
"http://localhost:11434/v1",
|
|
86
137
|
[
|
|
87
|
-
|
|
88
|
-
("
|
|
89
|
-
("
|
|
90
|
-
("
|
|
138
|
+
# Run: ollama pull <model> before using
|
|
139
|
+
("qwen2.5-coder:7b", "Qwen2.5-Coder 7B — best local coding (8GB VRAM)"),
|
|
140
|
+
("qwen2.5-coder:32b", "Qwen2.5-Coder 32B — benchmark king (24GB VRAM)"),
|
|
141
|
+
("deepseek-coder-v2:16b","DeepSeek-Coder-V2 16B — strong coding (16GB VRAM)"),
|
|
142
|
+
("llama3.3:70b", "Llama 3.3 70B — general purpose"),
|
|
91
143
|
],
|
|
92
144
|
),
|
|
93
145
|
"5": (
|
|
@@ -100,6 +152,7 @@ _COMPATIBLE_PROVIDERS = {
|
|
|
100
152
|
}
|
|
101
153
|
|
|
102
154
|
|
|
155
|
+
|
|
103
156
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
104
157
|
|
|
105
158
|
def _is_interactive() -> bool:
|
|
@@ -162,12 +215,17 @@ def _pick_model(models: list[tuple[str, str]], allow_custom: bool = False) -> st
|
|
|
162
215
|
def run_setup_wizard(env_path: Path | None = None) -> bool:
|
|
163
216
|
"""
|
|
164
217
|
Run the interactive first-run setup wizard.
|
|
218
|
+
Saves configuration to ~/.devpilot/.env so settings persist
|
|
219
|
+
regardless of which directory `devpilot` is run from.
|
|
165
220
|
Returns True if setup completed, False if skipped/failed.
|
|
166
221
|
"""
|
|
167
222
|
if not _is_interactive():
|
|
168
223
|
return False
|
|
169
224
|
|
|
170
|
-
|
|
225
|
+
# Always save to the persistent user config directory
|
|
226
|
+
user_config_dir = Path.home() / ".devpilot"
|
|
227
|
+
user_config_dir.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
env_path = env_path or (user_config_dir / ".env")
|
|
171
229
|
|
|
172
230
|
console.print(Panel(
|
|
173
231
|
"[bold cyan]Welcome to DevPilot! 🚀[/bold cyan]\n\n"
|
|
@@ -179,12 +237,12 @@ def run_setup_wizard(env_path: Path | None = None) -> bool:
|
|
|
179
237
|
|
|
180
238
|
# ── Step 1: Choose provider ───────────────────────────────────────────────
|
|
181
239
|
console.print("\n[bold]Step 1 of 3 — Choose your AI provider[/bold]\n")
|
|
182
|
-
console.print(" [cyan]1[/cyan] Anthropic (Claude
|
|
183
|
-
console.print(" [cyan]2[/cyan] OpenAI (GPT-
|
|
184
|
-
console.print(" [cyan]3[/cyan] Groq (Llama
|
|
185
|
-
console.print(" [cyan]4[/cyan] Together AI (Llama
|
|
186
|
-
console.print(" [cyan]5[/cyan] Mistral AI (Mistral, Codestral)")
|
|
187
|
-
console.print(" [cyan]6[/cyan] Ollama (local models
|
|
240
|
+
console.print(" [cyan]1[/cyan] Anthropic (Claude Opus 4.7, Sonnet 4.5, Haiku 4.5)")
|
|
241
|
+
console.print(" [cyan]2[/cyan] OpenAI (GPT-5.5, GPT-5.4, GPT-5.4-mini)")
|
|
242
|
+
console.print(" [cyan]3[/cyan] Groq (Llama 4 Scout, Llama 3.3 70B — very fast, free tier)")
|
|
243
|
+
console.print(" [cyan]4[/cyan] Together AI (Llama 4 Maverick/Scout, Llama 3.3 70B)")
|
|
244
|
+
console.print(" [cyan]5[/cyan] Mistral AI (Mistral Large, Codestral)")
|
|
245
|
+
console.print(" [cyan]6[/cyan] Ollama (local models — Qwen2.5-Coder, DeepSeek, Llama)")
|
|
188
246
|
console.print(" [cyan]7[/cyan] Other (any OpenAI-compatible endpoint)")
|
|
189
247
|
|
|
190
248
|
choice = Prompt.ask("\nProvider", choices=["1","2","3","4","5","6","7"], default="1")
|
|
@@ -195,7 +253,7 @@ def run_setup_wizard(env_path: Path | None = None) -> bool:
|
|
|
195
253
|
if choice == "1":
|
|
196
254
|
console.print("\n[bold]Step 2 of 3 — Enter your Anthropic API key[/bold]")
|
|
197
255
|
console.print(" Get one at: [link=https://console.anthropic.com/]https://console.anthropic.com/[/link]")
|
|
198
|
-
api_key =
|
|
256
|
+
api_key = _prompt_secret("ANTHROPIC_API_KEY")
|
|
199
257
|
if not api_key:
|
|
200
258
|
console.print("[red]API key cannot be empty.[/red]")
|
|
201
259
|
return False
|
|
@@ -215,7 +273,7 @@ def run_setup_wizard(env_path: Path | None = None) -> bool:
|
|
|
215
273
|
elif choice == "2":
|
|
216
274
|
console.print("\n[bold]Step 2 of 3 — Enter your OpenAI API key[/bold]")
|
|
217
275
|
console.print(" Get one at: [link=https://platform.openai.com/api-keys]https://platform.openai.com/api-keys[/link]")
|
|
218
|
-
api_key =
|
|
276
|
+
api_key = _prompt_secret("OPENAI_API_KEY")
|
|
219
277
|
if not api_key:
|
|
220
278
|
console.print("[red]API key cannot be empty.[/red]")
|
|
221
279
|
return False
|
|
@@ -244,7 +302,7 @@ def run_setup_wizard(env_path: Path | None = None) -> bool:
|
|
|
244
302
|
api_key = "ollama"
|
|
245
303
|
else:
|
|
246
304
|
console.print(f" Get one at: [link={key_url}]{key_url}[/link]")
|
|
247
|
-
api_key =
|
|
305
|
+
api_key = _prompt_secret(key_env)
|
|
248
306
|
if not api_key:
|
|
249
307
|
console.print("[red]API key cannot be empty.[/red]")
|
|
250
308
|
return False
|
|
@@ -264,7 +322,7 @@ def run_setup_wizard(env_path: Path | None = None) -> bool:
|
|
|
264
322
|
elif choice == "7":
|
|
265
323
|
console.print("\n[bold]Step 2 of 3 — Custom OpenAI-compatible endpoint[/bold]")
|
|
266
324
|
base_url = Prompt.ask("Base URL (e.g. https://api.example.com/v1)").strip()
|
|
267
|
-
api_key =
|
|
325
|
+
api_key = _prompt_secret("API key")
|
|
268
326
|
model = Prompt.ask("Model name (e.g. llama-3-70b)").strip()
|
|
269
327
|
|
|
270
328
|
if not base_url or not model:
|
agent/tools/registry.py
CHANGED
|
@@ -15,15 +15,6 @@ from dataclasses import dataclass, field
|
|
|
15
15
|
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
|
16
16
|
|
|
17
17
|
from agent.tools.base import BaseTool, ToolResult
|
|
18
|
-
from agent.tools.search_code import SearchCodeTool
|
|
19
|
-
|
|
20
|
-
from agent.tools.shell import RunBashTool
|
|
21
|
-
from agent.tools.a2a import A2ATool
|
|
22
|
-
from agent.tools.web_search import WebSearchTool
|
|
23
|
-
|
|
24
|
-
from agent.tools.git_ops import GitStatusTool, GitCommitTool
|
|
25
|
-
from agent.tools.doc_gen import DocGenTool
|
|
26
|
-
from agent.tools.diagram import DiagramTool
|
|
27
18
|
from agent.ui import UI
|
|
28
19
|
|
|
29
20
|
if TYPE_CHECKING:
|
|
@@ -108,8 +99,15 @@ class ToolRegistry:
|
|
|
108
99
|
|
|
109
100
|
def _register_builtins(self) -> None:
|
|
110
101
|
"""Register all default native tools."""
|
|
111
|
-
#
|
|
102
|
+
# All heavy tool imports are lazy here — keeps startup fast.
|
|
103
|
+
# GitPython, tavily, shell, and other deps only load when the
|
|
104
|
+
# registry is instantiated inside main_async(), not at module import time.
|
|
112
105
|
from agent.tools.fs import ListFilesTool, ReadFileTool, WriteFileTool, EditFileTool
|
|
106
|
+
from agent.tools.shell import RunBashTool
|
|
107
|
+
from agent.tools.search_code import SearchCodeTool
|
|
108
|
+
from agent.tools.git_ops import GitStatusTool, GitCommitTool
|
|
109
|
+
from agent.tools.doc_gen import DocGenTool
|
|
110
|
+
from agent.tools.diagram import DiagramTool
|
|
113
111
|
|
|
114
112
|
tools: list[BaseTool] = [
|
|
115
113
|
ReadFileTool(self._config, self._context),
|
|
@@ -118,7 +116,6 @@ class ToolRegistry:
|
|
|
118
116
|
ListFilesTool(self._config, self._context),
|
|
119
117
|
RunBashTool(self._config),
|
|
120
118
|
SearchCodeTool(self._config),
|
|
121
|
-
|
|
122
119
|
GitStatusTool(self._config),
|
|
123
120
|
GitCommitTool(self._config),
|
|
124
121
|
DocGenTool(self._config),
|
|
@@ -126,10 +123,11 @@ class ToolRegistry:
|
|
|
126
123
|
]
|
|
127
124
|
|
|
128
125
|
if self._config.web_search_enabled:
|
|
126
|
+
from agent.tools.web_search import WebSearchTool
|
|
129
127
|
tools.append(WebSearchTool(self._config))
|
|
130
128
|
|
|
131
|
-
|
|
132
129
|
if self._config.a2a_enabled:
|
|
130
|
+
from agent.tools.a2a import A2ATool
|
|
133
131
|
tools.append(A2ATool(self._config))
|
|
134
132
|
|
|
135
133
|
for t in tools:
|
agent/tui/app.py
CHANGED
|
@@ -429,22 +429,25 @@ class DevPilotApp(App):
|
|
|
429
429
|
"""Re-enable input when the agent loop finishes."""
|
|
430
430
|
if event.worker.name == "run_agent_task" and event.state.name in ("SUCCESS", "ERROR", "CANCELLED"):
|
|
431
431
|
self.spinner.display = False
|
|
432
|
-
|
|
432
|
+
|
|
433
433
|
try:
|
|
434
434
|
inp = self.query_one("#chat-input", Input)
|
|
435
435
|
inp.disabled = False
|
|
436
436
|
inp.focus()
|
|
437
437
|
except Exception:
|
|
438
438
|
pass
|
|
439
|
-
|
|
440
|
-
#
|
|
439
|
+
|
|
440
|
+
# Safety fallback: if AssistantMessageEvent never arrived (edge case),
|
|
441
|
+
# finalize the stream now so the response is never lost.
|
|
441
442
|
if self._active_stream:
|
|
442
443
|
final_text = self._active_stream._buffer
|
|
443
444
|
await self._active_stream.remove()
|
|
444
445
|
self._active_stream = None
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
446
|
+
if final_text:
|
|
447
|
+
await self.chat_log.mount(ChatMessage("DevPilot", final_text))
|
|
448
|
+
self.chat_log.scroll_end(animate=False)
|
|
449
|
+
self._last_assistant_message = final_text
|
|
450
|
+
|
|
448
451
|
# Refresh tree to show newly read/written files
|
|
449
452
|
self._refresh_project_map()
|
|
450
453
|
|
|
@@ -479,14 +482,16 @@ class DevPilotApp(App):
|
|
|
479
482
|
@on(ThinkingEvent)
|
|
480
483
|
async def handle_ui_events(self, event: UIEvent) -> None:
|
|
481
484
|
if isinstance(event, AssistantMessageEvent):
|
|
482
|
-
final_text = ""
|
|
483
485
|
if self._active_stream:
|
|
486
|
+
# Streaming was used — use the already-buffered text.
|
|
487
|
+
# Ignore event.text to avoid duplication (it's the same content).
|
|
484
488
|
final_text = self._active_stream._buffer
|
|
485
489
|
await self._active_stream.remove()
|
|
486
490
|
self._active_stream = None
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
491
|
+
else:
|
|
492
|
+
# Non-streaming path — event.text is the only source.
|
|
493
|
+
final_text = event.text.strip()
|
|
494
|
+
|
|
490
495
|
if final_text:
|
|
491
496
|
await self.chat_log.mount(ChatMessage("DevPilot", final_text))
|
|
492
497
|
self.chat_log.scroll_end(animate=False)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devpilot-agentic-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Autonomous AI coding agent for your terminal — Claude, GPT-4o, Groq, Ollama, and more
|
|
5
5
|
Author: Thijesh Praveen V
|
|
6
6
|
License: MIT
|
|
@@ -13,6 +13,8 @@ Classifier: Environment :: Console
|
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
18
|
Classifier: License :: OSI Approved :: MIT License
|
|
17
19
|
Classifier: Topic :: Software Development
|
|
18
20
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
@@ -46,7 +48,7 @@ Requires-Dist: pytest-asyncio>=0.23.7; extra == "dev"
|
|
|
46
48
|
[](https://pypi.org/project/devpilot-agentic-cli/)
|
|
47
49
|
[](https://opensource.org/licenses/MIT)
|
|
48
50
|
|
|
49
|
-
**An autonomous AI coding agent for your terminal.** DevPilot gives Claude, GPT
|
|
51
|
+
**An autonomous AI coding agent for your terminal.** DevPilot gives Claude, GPT, Groq, Mistral, or any local model a full suite of tools — file read/write, bash execution, code search, git, MCP, and more — orchestrated into a self-healing agentic loop inside a rich Textual TUI. Works in PowerShell, CMD, and any modern terminal.
|
|
50
52
|
|
|
51
53
|
```
|
|
52
54
|
┌─ DevPilot ──────────────────────────────────────────────────────┐
|
|
@@ -76,7 +78,6 @@ Requires-Dist: pytest-asyncio>=0.23.7; extra == "dev"
|
|
|
76
78
|
| **Git** | `git_status`, `git_commit` with surgical file staging |
|
|
77
79
|
| **Documentation** | `doc_gen` (markdown), `diagram` (Mermaid) |
|
|
78
80
|
| **Web search** | `web_search` via Tavily (optional) |
|
|
79
|
-
|
|
80
81
|
| **MCP** | Connect any MCP server via `mcp_servers.json` |
|
|
81
82
|
| **A2A** | Agent-to-agent task delegation over HTTP |
|
|
82
83
|
| **Providers** | Anthropic, OpenAI, Groq, Together AI, Mistral, Ollama, any OpenAI-compatible endpoint |
|
|
@@ -92,7 +93,7 @@ pip install devpilot-agentic-cli
|
|
|
92
93
|
devpilot
|
|
93
94
|
```
|
|
94
95
|
|
|
95
|
-
That's it. On first run DevPilot launches a setup wizard — pick your provider, enter your API key, choose a model. It saves everything to
|
|
96
|
+
That's it. On first run DevPilot launches a setup wizard — pick your provider, enter your API key, choose a model. It saves everything to `~/.devpilot/.env` and opens the TUI. Every subsequent run starts immediately.
|
|
96
97
|
|
|
97
98
|
### Optional Features
|
|
98
99
|
|
|
@@ -111,19 +112,19 @@ DevPilot works with any of these out of the box — the setup wizard walks you t
|
|
|
111
112
|
|
|
112
113
|
| Provider | Models | Get API key |
|
|
113
114
|
|---|---|---|
|
|
114
|
-
| **Anthropic** | claude-opus-4-5, claude-sonnet-4-5, claude-haiku-4-5 | [console.anthropic.com](https://console.anthropic.com/) |
|
|
115
|
-
| **OpenAI** | gpt-
|
|
116
|
-
| **Groq** | llama-3.3-70b,
|
|
117
|
-
| **Together AI** | Llama
|
|
118
|
-
| **Mistral AI** | mistral-large, codestral | [console.mistral.ai](https://console.mistral.ai/api-keys/) |
|
|
119
|
-
| **Ollama** |
|
|
115
|
+
| **Anthropic** | claude-opus-4-7, claude-opus-4-5-20251101, claude-sonnet-4-5-20250929, claude-haiku-4-5-20251001 | [console.anthropic.com](https://console.anthropic.com/) |
|
|
116
|
+
| **OpenAI** | gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.4-nano | [platform.openai.com](https://platform.openai.com/api-keys) |
|
|
117
|
+
| **Groq** | llama-3.3-70b-versatile, llama-4-scout-17b, qwen3-32b | [console.groq.com](https://console.groq.com/keys) |
|
|
118
|
+
| **Together AI** | Llama 4 Maverick/Scout, Llama 3.3 70B Turbo | [api.together.xyz](https://api.together.xyz/settings/api-keys) |
|
|
119
|
+
| **Mistral AI** | mistral-large-latest, mistral-small-latest, codestral-latest | [console.mistral.ai](https://console.mistral.ai/api-keys/) |
|
|
120
|
+
| **Ollama** | qwen2.5-coder:7b, deepseek-coder-v2:16b, llama3.3:70b — no API key needed | [ollama.com](https://ollama.com/library) |
|
|
120
121
|
| **Other** | Any OpenAI-compatible endpoint | — |
|
|
121
122
|
|
|
122
123
|
---
|
|
123
124
|
|
|
124
125
|
## Configuration
|
|
125
126
|
|
|
126
|
-
All settings live in
|
|
127
|
+
All settings live in `~/.devpilot/.env` — a persistent file in your home directory that works regardless of which folder you run `devpilot` from. The setup wizard creates this on first run. You can edit it manually anytime — or re-run the wizard with `devpilot --setup`.
|
|
127
128
|
|
|
128
129
|
### Full settings reference
|
|
129
130
|
|
|
@@ -131,9 +132,10 @@ All settings live in a `.env` file in your project directory. The setup wizard c
|
|
|
131
132
|
|---|---|---|
|
|
132
133
|
| `ANTHROPIC_API_KEY` | — | API key for Anthropic provider |
|
|
133
134
|
| `OPENAI_API_KEY` | — | API key for OpenAI / Groq / Together / Mistral / Ollama |
|
|
135
|
+
| `GROQ_API_KEY` | — | API key for Groq (optional — can reuse as OPENAI_API_KEY) |
|
|
134
136
|
| `DEVPILOT_PROVIDER` | `anthropic` | `anthropic` or `openai` |
|
|
135
|
-
| `DEVPILOT_MODEL` | `claude-opus-4-5` | Model name |
|
|
136
|
-
| `DEVPILOT_BASE_URL` | — | Custom endpoint, e.g. `
|
|
137
|
+
| `DEVPILOT_MODEL` | `claude-opus-4-5-20251101` | Model name |
|
|
138
|
+
| `DEVPILOT_BASE_URL` | — | Custom endpoint, e.g. `https://api.groq.com/openai/v1` |
|
|
137
139
|
| `DEVPILOT_NO_CONFIRM` | `false` | Skip confirmation prompts (useful for CI) |
|
|
138
140
|
| `DEVPILOT_MAX_ITERATIONS` | `50` | Max tool-use iterations before loop aborts |
|
|
139
141
|
| `DEVPILOT_WORKDIR` | `cwd` | Root directory for file operations |
|
|
@@ -144,12 +146,12 @@ All settings live in a `.env` file in your project directory. The setup wizard c
|
|
|
144
146
|
### Override priority
|
|
145
147
|
|
|
146
148
|
```
|
|
147
|
-
CLI flags → env vars →
|
|
149
|
+
CLI flags → env vars → ~/.devpilot/.env → defaults
|
|
148
150
|
```
|
|
149
151
|
|
|
150
152
|
Per-run override example:
|
|
151
153
|
```bash
|
|
152
|
-
DEVPILOT_MODEL=
|
|
154
|
+
DEVPILOT_MODEL=llama-3.3-70b-versatile devpilot --task "fix typos"
|
|
153
155
|
```
|
|
154
156
|
|
|
155
157
|
---
|
|
@@ -172,7 +174,6 @@ Options:
|
|
|
172
174
|
--a2a-port PORT A2A server port (default: 8000)
|
|
173
175
|
--no-a2a Disable A2A server
|
|
174
176
|
--no-web-search Disable Tavily web search
|
|
175
|
-
|
|
176
177
|
--setup Re-run the setup wizard
|
|
177
178
|
```
|
|
178
179
|
|
|
@@ -232,7 +233,7 @@ agent/
|
|
|
232
233
|
├── setup_wizard.py First-run interactive configuration wizard
|
|
233
234
|
├── loop.py Core agentic loop (plan → act → verify → heal)
|
|
234
235
|
├── config.py Config dataclass — all settings from env vars
|
|
235
|
-
├── context.py RepoContext — file awareness, AST map
|
|
236
|
+
├── context.py RepoContext — file awareness, AST project map
|
|
236
237
|
├── history.py Conversation history + smart context pruning
|
|
237
238
|
├── providers/
|
|
238
239
|
│ ├── anthropic_provider.py
|
|
@@ -247,7 +248,6 @@ agent/
|
|
|
247
248
|
│ ├── doc_gen.py doc_gen
|
|
248
249
|
│ ├── diagram.py diagram (Mermaid)
|
|
249
250
|
│ ├── web_search.py web_search (Tavily)
|
|
250
|
-
|
|
251
251
|
│ ├── a2a.py A2A delegation
|
|
252
252
|
│ └── registry.py ToolRegistry + PermissionGuard
|
|
253
253
|
└── tui/
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
agent/__init__.py,sha256=lfJPQT9NbRRWfa9YABD9u8apcLjs3tzDVZvrOb69GBQ,25
|
|
2
2
|
agent/a2a_client.py,sha256=vY4qZahkWP-DVU2smIeTz1xLj61kkDjE9Tmt_-9PKUU,3416
|
|
3
3
|
agent/a2a_server.py,sha256=3rkDy8dm49_5qCa6fKYkLdmHTnHs5ocHBuZ8j32-p48,5230
|
|
4
|
-
agent/cli.py,sha256=
|
|
5
|
-
agent/config.py,sha256=
|
|
6
|
-
agent/context.py,sha256=
|
|
4
|
+
agent/cli.py,sha256=ApAnD7pQUvHPsnHt5g8JoK0CaGAIPS78qiGQOt7B4X4,13305
|
|
5
|
+
agent/config.py,sha256=b53O7kkdAUYTNCm7YLxBYiySj6ZXn2qzoWqgPN1NIUw,11244
|
|
6
|
+
agent/context.py,sha256=atGJgIiMAmYfcNu6k9gwUzsz5cykBAx9A8PCdgK64R0,7297
|
|
7
7
|
agent/history.py,sha256=IMY3IO5TN9Sh7tJPAWE9VZYC0pkAwgPj2a_F1yA4OmE,6730
|
|
8
|
-
agent/loop.py,sha256=
|
|
9
|
-
agent/mcp_client.py,sha256=
|
|
10
|
-
agent/setup_wizard.py,sha256=
|
|
8
|
+
agent/loop.py,sha256=3QxOb2c09ewAcIbAnd3RVCsXzKaGTJFBLZ-5OdhnOi0,3693
|
|
9
|
+
agent/mcp_client.py,sha256=A8WgS2DhfBbpZZGrJYwOZwMAF0gA-YG4gylBgMzkriE,7785
|
|
10
|
+
agent/setup_wizard.py,sha256=KNDxy-0jC6erB9IGpZoPCcrgpzU523D8p1zEx8aMbC0,15655
|
|
11
11
|
agent/ui.py,sha256=LK8L2fTq_JIcYiVVjpVWaoYMFWXwqUcqYL4gspdAqvs,8283
|
|
12
12
|
agent/providers/__init__.py,sha256=6xBg26vCBV2e3hybMT2JCHEZoRNpImkVOK-FytBW4xY,166
|
|
13
13
|
agent/providers/anthropic_provider.py,sha256=i257T8T7fYuUEVvsy-3_eudbjSlxZqbn3zi7DFvukRE,6067
|
|
@@ -22,14 +22,14 @@ agent/tools/diagram.py,sha256=FpQRWud6GMi34ysjbEkqnvy5aaHOJwIvpmDELsyIqPE,4884
|
|
|
22
22
|
agent/tools/doc_gen.py,sha256=TaLw5JJ1hSgfViGyfYRMfUafjRTfxa0JYtZw0ycluR8,6064
|
|
23
23
|
agent/tools/fs.py,sha256=nBrGzq-yIzFcRzhPguBxJ9H23lhsUjGPzQXhwfrcQY0,16121
|
|
24
24
|
agent/tools/git_ops.py,sha256=CbulyzzSuFUQdAqu-6eMkg7CpB45h7nFP_5fxTecHeE,5505
|
|
25
|
-
agent/tools/registry.py,sha256=
|
|
25
|
+
agent/tools/registry.py,sha256=0I8Pgt7obgNaVB8BTeHHRmdouTj3P3Kk7GCCra7D2fg,8620
|
|
26
26
|
agent/tools/search_code.py,sha256=DwxT6D10OpWvIX7KR3-cRW1Z1MkLW2_WcJ47H_jfuHM,4490
|
|
27
27
|
agent/tools/shell.py,sha256=i9nar3gyS28p6wjfAljNREUll-7RiD_LhQQ_CDkz-CM,3923
|
|
28
28
|
agent/tools/web_search.py,sha256=7a533lYcRG0rwYle00xsIncfPRL1zCiS-wsL_qVxlyI,3736
|
|
29
29
|
agent/tui/__init__.py,sha256=FB4u1BY9RSmYVfCM2yMeCyMmzjnb9T0bhmPp9hHH8Ys,37
|
|
30
|
-
agent/tui/app.py,sha256=
|
|
31
|
-
devpilot_agentic_cli-1.0.
|
|
32
|
-
devpilot_agentic_cli-1.0.
|
|
33
|
-
devpilot_agentic_cli-1.0.
|
|
34
|
-
devpilot_agentic_cli-1.0.
|
|
35
|
-
devpilot_agentic_cli-1.0.
|
|
30
|
+
agent/tui/app.py,sha256=dBa3l5J3dkS7j56b-NgTQXR1At8aRXpayORk4CbJGBo,18831
|
|
31
|
+
devpilot_agentic_cli-1.0.4.dist-info/METADATA,sha256=VHAXdhDQOcW-VLetIITT-AZmXmUbsdViyjK7dho1IZE,11608
|
|
32
|
+
devpilot_agentic_cli-1.0.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
33
|
+
devpilot_agentic_cli-1.0.4.dist-info/entry_points.txt,sha256=Zi0zH4sqSQptehekvn5CBhU452GQ23Vyja4l8C58IuY,44
|
|
34
|
+
devpilot_agentic_cli-1.0.4.dist-info/top_level.txt,sha256=0gvCG7PHc22NA63j3bTGi2Zc37ym9t8Pf90ZLzf1kGA,6
|
|
35
|
+
devpilot_agentic_cli-1.0.4.dist-info/RECORD,,
|
|
File without changes
|
{devpilot_agentic_cli-1.0.2.dist-info → devpilot_agentic_cli-1.0.4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|