claude-mpm 5.6.13__py3-none-any.whl → 5.6.14__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/commander.py +173 -3
- claude_mpm/cli/parsers/commander_parser.py +41 -8
- claude_mpm/cli/startup.py +10 -1
- claude_mpm/cli/startup_display.py +2 -1
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +19 -21
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +4 -0
- claude_mpm/commander/daemon.py +139 -9
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/work/executor.py +22 -12
- claude_mpm/core/output_style_manager.py +34 -7
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +0 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +0 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +0 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +0 -0
- claude_mpm/hooks/claude_hooks/response_tracking.py +0 -0
- claude_mpm/hooks/templates/pre_tool_use_template.py +0 -0
- claude_mpm/scripts/start_activity_logging.py +0 -0
- {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/METADATA +2 -2
- {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/RECORD +41 -27
- {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/WHEEL +0 -0
- {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.13.dist-info → claude_mpm-5.6.14.dist-info}/top_level.txt +0 -0
claude_mpm/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.6.
|
|
1
|
+
5.6.14
|
|
@@ -2,25 +2,129 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
import shutil
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
5
9
|
|
|
6
10
|
logger = logging.getLogger(__name__)
|
|
7
11
|
|
|
12
|
+
# ANSI colors
|
|
13
|
+
CYAN = "\033[36m"
|
|
14
|
+
DIM = "\033[2m"
|
|
15
|
+
BOLD = "\033[1m"
|
|
16
|
+
YELLOW = "\033[33m"
|
|
17
|
+
GREEN = "\033[32m"
|
|
18
|
+
RED = "\033[31m"
|
|
19
|
+
RESET = "\033[0m"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_terminal_width() -> int:
|
|
23
|
+
"""Get terminal width with reasonable bounds."""
|
|
24
|
+
try:
|
|
25
|
+
width = shutil.get_terminal_size().columns
|
|
26
|
+
return max(80, min(width, 120))
|
|
27
|
+
except Exception:
|
|
28
|
+
return 100
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_version() -> str:
|
|
32
|
+
"""Get Commander version."""
|
|
33
|
+
version_file = Path(__file__).parent.parent.parent / "VERSION"
|
|
34
|
+
if version_file.exists():
|
|
35
|
+
return version_file.read_text().strip()
|
|
36
|
+
return "unknown"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def display_commander_banner():
|
|
40
|
+
"""Display Commander-specific startup banner."""
|
|
41
|
+
width = _get_terminal_width()
|
|
42
|
+
version = _get_version()
|
|
43
|
+
|
|
44
|
+
# Commander ASCII art banner
|
|
45
|
+
banner = f"""
|
|
46
|
+
{CYAN}╭{'─' * (width - 2)}╮{RESET}
|
|
47
|
+
{CYAN}│{RESET}{BOLD} ⚡ MPM Commander {RESET}{DIM}v{version}{RESET}{' ' * (width - 24 - len(version))}│
|
|
48
|
+
{CYAN}│{RESET}{DIM} Multi-Project AI Orchestration{RESET}{' ' * (width - 36)}│
|
|
49
|
+
{CYAN}├{'─' * (width - 2)}┤{RESET}
|
|
50
|
+
{CYAN}│{RESET} {YELLOW}ALPHA{RESET} - APIs may change {' ' * (width - 55)}│
|
|
51
|
+
{CYAN}╰{'─' * (width - 2)}╯{RESET}
|
|
52
|
+
"""
|
|
53
|
+
print(banner)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _count_cached_agents() -> int:
|
|
57
|
+
"""Count cached agents from ~/.claude-mpm/cache/agents/."""
|
|
58
|
+
try:
|
|
59
|
+
cache_agents_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
|
|
60
|
+
if not cache_agents_dir.exists():
|
|
61
|
+
return 0
|
|
62
|
+
# Recursively find all .md files excluding base/README files
|
|
63
|
+
agent_files = [
|
|
64
|
+
f
|
|
65
|
+
for f in cache_agents_dir.rglob("*.md")
|
|
66
|
+
if f.is_file()
|
|
67
|
+
and not f.name.startswith(".")
|
|
68
|
+
and f.name not in ("README.md", "BASE-AGENT.md", "INSTRUCTIONS.md")
|
|
69
|
+
]
|
|
70
|
+
return len(agent_files)
|
|
71
|
+
except Exception:
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _count_cached_skills() -> int:
|
|
76
|
+
"""Count cached skills from ~/.claude-mpm/cache/skills/."""
|
|
77
|
+
try:
|
|
78
|
+
cache_skills_dir = Path.home() / ".claude-mpm" / "cache" / "skills"
|
|
79
|
+
if not cache_skills_dir.exists():
|
|
80
|
+
return 0
|
|
81
|
+
# Recursively find all directories containing SKILL.md
|
|
82
|
+
skill_files = list(cache_skills_dir.rglob("SKILL.md"))
|
|
83
|
+
return len(skill_files)
|
|
84
|
+
except Exception:
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_agents_and_skills():
|
|
89
|
+
"""Load agents and skills for Commander sessions."""
|
|
90
|
+
try:
|
|
91
|
+
print(f"{DIM}Loading agents...{RESET}", end=" ", flush=True)
|
|
92
|
+
agent_count = _count_cached_agents()
|
|
93
|
+
print(f"{GREEN}✓{RESET} {agent_count} agents")
|
|
94
|
+
|
|
95
|
+
print(f"{DIM}Loading skills...{RESET}", end=" ", flush=True)
|
|
96
|
+
skill_count = _count_cached_skills()
|
|
97
|
+
print(f"{GREEN}✓{RESET} {skill_count} skills")
|
|
98
|
+
|
|
99
|
+
return agent_count, skill_count
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.warning(f"Could not load agents/skills: {e}")
|
|
102
|
+
print(f"{YELLOW}⚠{RESET} Could not load agents/skills")
|
|
103
|
+
return 0, 0
|
|
104
|
+
|
|
8
105
|
|
|
9
106
|
def handle_commander_command(args) -> int:
|
|
10
|
-
"""Handle the commander command.
|
|
107
|
+
"""Handle the commander command with auto-starting daemon.
|
|
11
108
|
|
|
12
109
|
Args:
|
|
13
110
|
args: Parsed command line arguments with:
|
|
14
|
-
- port: Port for
|
|
111
|
+
- port: Port for daemon (default: 8765)
|
|
112
|
+
- host: Host for daemon (default: 127.0.0.1)
|
|
15
113
|
- state_dir: Optional state directory path
|
|
16
114
|
- debug: Enable debug logging
|
|
115
|
+
- no_chat: Start daemon only without interactive chat
|
|
116
|
+
- daemon_only: Alias for no_chat
|
|
17
117
|
|
|
18
118
|
Returns:
|
|
19
119
|
Exit code (0 for success, 1 for error)
|
|
20
120
|
"""
|
|
21
121
|
try:
|
|
22
122
|
# Import here to avoid circular dependencies
|
|
123
|
+
import requests
|
|
124
|
+
|
|
23
125
|
from claude_mpm.commander.chat.cli import run_commander
|
|
126
|
+
from claude_mpm.commander.config import DaemonConfig
|
|
127
|
+
from claude_mpm.commander.daemon import main as daemon_main
|
|
24
128
|
|
|
25
129
|
# Setup debug logging if requested
|
|
26
130
|
if getattr(args, "debug", False):
|
|
@@ -29,11 +133,76 @@ def handle_commander_command(args) -> int:
|
|
|
29
133
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
30
134
|
)
|
|
31
135
|
|
|
136
|
+
# Display Commander banner
|
|
137
|
+
display_commander_banner()
|
|
138
|
+
|
|
139
|
+
# Load agents and skills
|
|
140
|
+
load_agents_and_skills()
|
|
141
|
+
|
|
142
|
+
print() # Blank line after loading
|
|
143
|
+
|
|
32
144
|
# Get arguments
|
|
33
145
|
port = getattr(args, "port", 8765)
|
|
146
|
+
host = getattr(args, "host", "127.0.0.1")
|
|
34
147
|
state_dir = getattr(args, "state_dir", None)
|
|
148
|
+
no_chat = getattr(args, "no_chat", False) or getattr(args, "daemon_only", False)
|
|
149
|
+
|
|
150
|
+
# Check if daemon already running
|
|
151
|
+
daemon_running = False
|
|
152
|
+
try:
|
|
153
|
+
resp = requests.get(f"http://{host}:{port}/api/health", timeout=1)
|
|
154
|
+
if resp.status_code == 200:
|
|
155
|
+
print(f"{GREEN}✓{RESET} Daemon already running on {host}:{port}")
|
|
156
|
+
daemon_running = True
|
|
157
|
+
except (requests.RequestException, requests.ConnectionError):
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
# Start daemon if not running
|
|
161
|
+
if not daemon_running:
|
|
162
|
+
print(
|
|
163
|
+
f"{DIM}Starting daemon on {host}:{port}...{RESET}", end=" ", flush=True
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Create daemon config
|
|
167
|
+
config_kwargs = {"host": host, "port": port}
|
|
168
|
+
if state_dir:
|
|
169
|
+
config_kwargs["state_dir"] = state_dir
|
|
170
|
+
config = DaemonConfig(**config_kwargs)
|
|
171
|
+
|
|
172
|
+
# Start daemon in background thread
|
|
173
|
+
daemon_thread = threading.Thread(
|
|
174
|
+
target=lambda: asyncio.run(daemon_main(config)), daemon=True
|
|
175
|
+
)
|
|
176
|
+
daemon_thread.start()
|
|
177
|
+
|
|
178
|
+
# Wait for daemon to be ready (max 3 seconds)
|
|
179
|
+
for _ in range(30):
|
|
180
|
+
time.sleep(0.1)
|
|
181
|
+
try:
|
|
182
|
+
resp = requests.get(f"http://{host}:{port}/api/health", timeout=1)
|
|
183
|
+
if resp.status_code == 200:
|
|
184
|
+
print(f"{GREEN}✓{RESET}")
|
|
185
|
+
daemon_running = True
|
|
186
|
+
break
|
|
187
|
+
except (requests.RequestException, requests.ConnectionError):
|
|
188
|
+
pass
|
|
189
|
+
else:
|
|
190
|
+
print(f"{RED}✗{RESET} Failed (timeout)")
|
|
191
|
+
return 1
|
|
192
|
+
|
|
193
|
+
# If daemon-only mode, keep running until interrupted
|
|
194
|
+
if no_chat:
|
|
195
|
+
print(f"\n{CYAN}Daemon running.{RESET} API at http://{host}:{port}")
|
|
196
|
+
print(f"{DIM}Press Ctrl+C to stop{RESET}\n")
|
|
197
|
+
try:
|
|
198
|
+
while True:
|
|
199
|
+
time.sleep(1)
|
|
200
|
+
except KeyboardInterrupt:
|
|
201
|
+
print(f"\n{DIM}Shutting down...{RESET}")
|
|
202
|
+
return 0
|
|
35
203
|
|
|
36
|
-
#
|
|
204
|
+
# Launch interactive chat
|
|
205
|
+
print(f"\n{CYAN}Entering Commander chat...{RESET}\n")
|
|
37
206
|
asyncio.run(run_commander(port=port, state_dir=state_dir))
|
|
38
207
|
|
|
39
208
|
return 0
|
|
@@ -43,4 +212,5 @@ def handle_commander_command(args) -> int:
|
|
|
43
212
|
return 0
|
|
44
213
|
except Exception as e:
|
|
45
214
|
logger.error(f"Commander error: {e}", exc_info=True)
|
|
215
|
+
print(f"{RED}Error:{RESET} {e}")
|
|
46
216
|
return 1
|
|
@@ -23,17 +23,21 @@ def add_commander_subparser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
23
23
|
"""
|
|
24
24
|
commander_parser = subparsers.add_parser(
|
|
25
25
|
"commander",
|
|
26
|
-
help="
|
|
26
|
+
help="Launch Commander multi-project orchestration (ALPHA)",
|
|
27
27
|
description="""
|
|
28
|
-
Commander Mode -
|
|
28
|
+
Commander Mode - Multi-Project Orchestration (ALPHA)
|
|
29
29
|
|
|
30
|
-
Commander
|
|
31
|
-
|
|
32
|
-
- Connecting to instances and sending natural language commands
|
|
33
|
-
- Managing multiple concurrent projects
|
|
34
|
-
- Viewing instance status and output
|
|
30
|
+
The commander subcommand auto-starts the Commander daemon (if not already running)
|
|
31
|
+
and launches an interactive REPL for managing multiple Claude Code instances.
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
Commander provides:
|
|
34
|
+
- Auto-starting daemon that manages project lifecycles
|
|
35
|
+
- Interactive REPL for controlling instances
|
|
36
|
+
- Tmux-based session management
|
|
37
|
+
- Real-time output monitoring
|
|
38
|
+
- REST API for external control (http://127.0.0.1:8765)
|
|
39
|
+
|
|
40
|
+
REPL Commands:
|
|
37
41
|
list, ls, instances List active instances
|
|
38
42
|
start <path> Start new instance at path
|
|
39
43
|
--framework <cc|mpm> Specify framework (default: cc)
|
|
@@ -50,7 +54,16 @@ Natural Language:
|
|
|
50
54
|
command will be sent to the connected instance as a message.
|
|
51
55
|
|
|
52
56
|
Examples:
|
|
57
|
+
# Start daemon and launch interactive chat
|
|
53
58
|
claude-mpm commander
|
|
59
|
+
|
|
60
|
+
# Start daemon only (no chat interface)
|
|
61
|
+
claude-mpm commander --daemon-only
|
|
62
|
+
|
|
63
|
+
# Use custom port
|
|
64
|
+
claude-mpm commander --port 9000
|
|
65
|
+
|
|
66
|
+
# In REPL:
|
|
54
67
|
> start ~/myproject --framework cc --name myapp
|
|
55
68
|
> connect myapp
|
|
56
69
|
> Fix the authentication bug in login.py
|
|
@@ -81,3 +94,23 @@ Examples:
|
|
|
81
94
|
action="store_true",
|
|
82
95
|
help="Enable debug logging",
|
|
83
96
|
)
|
|
97
|
+
|
|
98
|
+
# Daemon auto-start options
|
|
99
|
+
commander_parser.add_argument(
|
|
100
|
+
"--host",
|
|
101
|
+
type=str,
|
|
102
|
+
default="127.0.0.1",
|
|
103
|
+
help="Daemon host (default: 127.0.0.1)",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
commander_parser.add_argument(
|
|
107
|
+
"--no-chat",
|
|
108
|
+
action="store_true",
|
|
109
|
+
help="Start daemon only without interactive chat",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
commander_parser.add_argument(
|
|
113
|
+
"--daemon-only",
|
|
114
|
+
action="store_true",
|
|
115
|
+
help="Alias for --no-chat (start daemon only)",
|
|
116
|
+
)
|
claude_mpm/cli/startup.py
CHANGED
|
@@ -210,7 +210,16 @@ def should_skip_background_services(args, processed_argv):
|
|
|
210
210
|
return any(cmd in (processed_argv or sys.argv[1:]) for cmd in skip_commands) or (
|
|
211
211
|
hasattr(args, "command")
|
|
212
212
|
and args.command
|
|
213
|
-
in [
|
|
213
|
+
in [
|
|
214
|
+
"info",
|
|
215
|
+
"doctor",
|
|
216
|
+
"config",
|
|
217
|
+
"mcp",
|
|
218
|
+
"configure",
|
|
219
|
+
"hook-errors",
|
|
220
|
+
"autotodos",
|
|
221
|
+
"commander",
|
|
222
|
+
]
|
|
214
223
|
)
|
|
215
224
|
|
|
216
225
|
|
|
@@ -540,7 +540,8 @@ def should_show_banner(args) -> bool:
|
|
|
540
540
|
return False
|
|
541
541
|
|
|
542
542
|
# Check for commands that should skip banner
|
|
543
|
-
|
|
543
|
+
# Commander has its own banner, so skip the main MPM banner
|
|
544
|
+
skip_commands = {"info", "doctor", "config", "configure", "commander"}
|
|
544
545
|
if hasattr(args, "command") and args.command in skip_commands:
|
|
545
546
|
return False
|
|
546
547
|
|
claude_mpm/commander/__init__.py
CHANGED
|
@@ -12,12 +12,18 @@ Key Components:
|
|
|
12
12
|
- ProjectSession: Per-project lifecycle management
|
|
13
13
|
- InstanceManager: Framework selection and instance lifecycle
|
|
14
14
|
- Frameworks: Claude Code, MPM framework abstractions
|
|
15
|
+
- Memory: Conversation storage, semantic search, context compression
|
|
15
16
|
|
|
16
17
|
Example:
|
|
17
18
|
>>> from claude_mpm.commander import ProjectRegistry
|
|
18
19
|
>>> registry = ProjectRegistry()
|
|
19
20
|
>>> project = registry.register("/path/to/project")
|
|
20
21
|
>>> registry.update_state(project.id, ProjectState.WORKING)
|
|
22
|
+
|
|
23
|
+
>>> # Memory integration
|
|
24
|
+
>>> from claude_mpm.commander.memory import MemoryIntegration
|
|
25
|
+
>>> memory = MemoryIntegration.create()
|
|
26
|
+
>>> await memory.capture_project_conversation(project)
|
|
21
27
|
"""
|
|
22
28
|
|
|
23
29
|
from claude_mpm.commander.config import DaemonConfig
|
|
@@ -6,26 +6,55 @@ the TmuxOrchestrator to interface with various runtimes in a uniform way.
|
|
|
6
6
|
Two types of adapters:
|
|
7
7
|
- RuntimeAdapter: Synchronous parsing and state detection
|
|
8
8
|
- CommunicationAdapter: Async I/O and state management
|
|
9
|
+
|
|
10
|
+
Available Runtime Adapters:
|
|
11
|
+
- ClaudeCodeAdapter: Vanilla Claude Code CLI
|
|
12
|
+
- AuggieAdapter: Auggie with MCP support
|
|
13
|
+
- CodexAdapter: Codex (limited features)
|
|
14
|
+
- MPMAdapter: Full MPM with agents, hooks, skills, monitoring
|
|
15
|
+
|
|
16
|
+
Registry:
|
|
17
|
+
- AdapterRegistry: Centralized adapter management with auto-detection
|
|
9
18
|
"""
|
|
10
19
|
|
|
11
|
-
from .
|
|
20
|
+
from .auggie import AuggieAdapter
|
|
21
|
+
from .base import (
|
|
22
|
+
Capability,
|
|
23
|
+
ParsedResponse,
|
|
24
|
+
RuntimeAdapter,
|
|
25
|
+
RuntimeCapability,
|
|
26
|
+
RuntimeInfo,
|
|
27
|
+
)
|
|
12
28
|
from .claude_code import ClaudeCodeAdapter
|
|
29
|
+
from .codex import CodexAdapter
|
|
13
30
|
from .communication import (
|
|
14
31
|
AdapterResponse,
|
|
15
32
|
AdapterState,
|
|
16
33
|
BaseCommunicationAdapter,
|
|
17
34
|
ClaudeCodeCommunicationAdapter,
|
|
18
35
|
)
|
|
36
|
+
from .mpm import MPMAdapter
|
|
37
|
+
from .registry import AdapterRegistry
|
|
38
|
+
|
|
39
|
+
# Auto-register all adapters
|
|
40
|
+
AdapterRegistry.register("claude-code", ClaudeCodeAdapter)
|
|
41
|
+
AdapterRegistry.register("auggie", AuggieAdapter)
|
|
42
|
+
AdapterRegistry.register("codex", CodexAdapter)
|
|
43
|
+
AdapterRegistry.register("mpm", MPMAdapter)
|
|
19
44
|
|
|
20
45
|
__all__ = [
|
|
21
|
-
|
|
46
|
+
"AdapterRegistry",
|
|
22
47
|
"AdapterResponse",
|
|
23
48
|
"AdapterState",
|
|
49
|
+
"AuggieAdapter",
|
|
24
50
|
"BaseCommunicationAdapter",
|
|
25
|
-
# Runtime adapters (parsing)
|
|
26
51
|
"Capability",
|
|
27
52
|
"ClaudeCodeAdapter",
|
|
28
53
|
"ClaudeCodeCommunicationAdapter",
|
|
54
|
+
"CodexAdapter",
|
|
55
|
+
"MPMAdapter",
|
|
29
56
|
"ParsedResponse",
|
|
30
57
|
"RuntimeAdapter",
|
|
58
|
+
"RuntimeCapability",
|
|
59
|
+
"RuntimeInfo",
|
|
31
60
|
]
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Auggie CLI runtime adapter.
|
|
2
|
+
|
|
3
|
+
This module implements the RuntimeAdapter interface for Auggie,
|
|
4
|
+
an AI coding assistant with MCP (Model Context Protocol) support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
import shlex
|
|
10
|
+
from typing import List, Optional, Set
|
|
11
|
+
|
|
12
|
+
from .base import (
|
|
13
|
+
Capability,
|
|
14
|
+
ParsedResponse,
|
|
15
|
+
RuntimeAdapter,
|
|
16
|
+
RuntimeCapability,
|
|
17
|
+
RuntimeInfo,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuggieAdapter(RuntimeAdapter):
|
|
24
|
+
"""Adapter for Auggie CLI.
|
|
25
|
+
|
|
26
|
+
Auggie is an AI coding assistant with support for MCP servers,
|
|
27
|
+
custom instructions, and various tool capabilities.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> adapter = AuggieAdapter()
|
|
31
|
+
>>> cmd = adapter.build_launch_command("/home/user/project")
|
|
32
|
+
>>> print(cmd)
|
|
33
|
+
cd '/home/user/project' && auggie
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# Idle detection patterns
|
|
37
|
+
IDLE_PATTERNS = [
|
|
38
|
+
r"^>\s*$", # Simple prompt
|
|
39
|
+
r"auggie>\s*$", # Named prompt
|
|
40
|
+
r"Ready for input",
|
|
41
|
+
r"What would you like",
|
|
42
|
+
r"How can I assist",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# Error patterns
|
|
46
|
+
ERROR_PATTERNS = [
|
|
47
|
+
r"Error:",
|
|
48
|
+
r"Failed:",
|
|
49
|
+
r"Exception:",
|
|
50
|
+
r"Permission denied",
|
|
51
|
+
r"not found",
|
|
52
|
+
r"Traceback \(most recent call last\)",
|
|
53
|
+
r"FATAL:",
|
|
54
|
+
r"command not found",
|
|
55
|
+
r"cannot access",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# Question patterns
|
|
59
|
+
QUESTION_PATTERNS = [
|
|
60
|
+
r"Which option",
|
|
61
|
+
r"Should I proceed",
|
|
62
|
+
r"Please choose",
|
|
63
|
+
r"\(y/n\)\?",
|
|
64
|
+
r"Are you sure",
|
|
65
|
+
r"Do you want",
|
|
66
|
+
r"\[Y/n\]",
|
|
67
|
+
r"\[yes/no\]",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# ANSI escape code pattern
|
|
71
|
+
ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def name(self) -> str:
|
|
75
|
+
"""Return the runtime identifier."""
|
|
76
|
+
return "auggie"
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def capabilities(self) -> Set[Capability]:
|
|
80
|
+
"""Return the set of capabilities supported by Auggie."""
|
|
81
|
+
return {
|
|
82
|
+
Capability.TOOL_USE,
|
|
83
|
+
Capability.FILE_EDIT,
|
|
84
|
+
Capability.FILE_CREATE,
|
|
85
|
+
Capability.GIT_OPERATIONS,
|
|
86
|
+
Capability.SHELL_COMMANDS,
|
|
87
|
+
Capability.COMPLEX_REASONING,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def runtime_info(self) -> RuntimeInfo:
|
|
92
|
+
"""Return detailed runtime information."""
|
|
93
|
+
return RuntimeInfo(
|
|
94
|
+
name="auggie",
|
|
95
|
+
version=None, # Version detection could be added
|
|
96
|
+
capabilities={
|
|
97
|
+
RuntimeCapability.FILE_READ,
|
|
98
|
+
RuntimeCapability.FILE_EDIT,
|
|
99
|
+
RuntimeCapability.FILE_CREATE,
|
|
100
|
+
RuntimeCapability.BASH_EXECUTION,
|
|
101
|
+
RuntimeCapability.GIT_OPERATIONS,
|
|
102
|
+
RuntimeCapability.TOOL_USE,
|
|
103
|
+
RuntimeCapability.MCP_TOOLS, # Auggie supports MCP
|
|
104
|
+
RuntimeCapability.INSTRUCTIONS,
|
|
105
|
+
RuntimeCapability.COMPLEX_REASONING,
|
|
106
|
+
RuntimeCapability.AGENT_DELEGATION, # Auggie now supports agents
|
|
107
|
+
},
|
|
108
|
+
command="auggie",
|
|
109
|
+
supports_agents=True, # Auggie now supports agent delegation
|
|
110
|
+
instruction_file=".augment/instructions.md",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def build_launch_command(
|
|
114
|
+
self, project_path: str, agent_prompt: Optional[str] = None
|
|
115
|
+
) -> str:
|
|
116
|
+
"""Generate shell command to start Auggie.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
project_path: Absolute path to the project directory
|
|
120
|
+
agent_prompt: Optional system prompt to configure Auggie
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Shell command string ready to execute
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
>>> adapter = AuggieAdapter()
|
|
127
|
+
>>> adapter.build_launch_command("/home/user/project")
|
|
128
|
+
"cd '/home/user/project' && auggie"
|
|
129
|
+
"""
|
|
130
|
+
quoted_path = shlex.quote(project_path)
|
|
131
|
+
cmd = f"cd {quoted_path} && auggie"
|
|
132
|
+
|
|
133
|
+
if agent_prompt:
|
|
134
|
+
# Auggie may support --prompt or similar flag
|
|
135
|
+
# Adjust based on actual Auggie CLI options
|
|
136
|
+
quoted_prompt = shlex.quote(agent_prompt)
|
|
137
|
+
cmd += f" --prompt {quoted_prompt}"
|
|
138
|
+
|
|
139
|
+
logger.debug(f"Built Auggie launch command: {cmd}")
|
|
140
|
+
return cmd
|
|
141
|
+
|
|
142
|
+
def format_input(self, message: str) -> str:
|
|
143
|
+
"""Prepare message for Auggie's input format."""
|
|
144
|
+
formatted = message.strip()
|
|
145
|
+
logger.debug(f"Formatted input: {formatted[:100]}...")
|
|
146
|
+
return formatted
|
|
147
|
+
|
|
148
|
+
def strip_ansi(self, text: str) -> str:
|
|
149
|
+
"""Remove ANSI escape codes from text."""
|
|
150
|
+
return self.ANSI_ESCAPE.sub("", text)
|
|
151
|
+
|
|
152
|
+
def detect_idle(self, output: str) -> bool:
|
|
153
|
+
"""Recognize when Auggie is waiting for input."""
|
|
154
|
+
if not output:
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
clean = self.strip_ansi(output)
|
|
158
|
+
lines = clean.strip().split("\n")
|
|
159
|
+
|
|
160
|
+
if not lines:
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
last_line = lines[-1].strip()
|
|
164
|
+
|
|
165
|
+
for pattern in self.IDLE_PATTERNS:
|
|
166
|
+
if re.search(pattern, last_line):
|
|
167
|
+
logger.debug(f"Detected idle state with pattern: {pattern}")
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
def detect_error(self, output: str) -> Optional[str]:
|
|
173
|
+
"""Recognize error states and extract error message."""
|
|
174
|
+
clean = self.strip_ansi(output)
|
|
175
|
+
|
|
176
|
+
for pattern in self.ERROR_PATTERNS:
|
|
177
|
+
match = re.search(pattern, clean, re.IGNORECASE)
|
|
178
|
+
if match:
|
|
179
|
+
for line in clean.split("\n"):
|
|
180
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
181
|
+
error_msg = line.strip()
|
|
182
|
+
logger.warning(f"Detected error: {error_msg}")
|
|
183
|
+
return error_msg
|
|
184
|
+
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
def detect_question(
|
|
188
|
+
self, output: str
|
|
189
|
+
) -> tuple[bool, Optional[str], Optional[List[str]]]:
|
|
190
|
+
"""Detect if Auggie is asking a question."""
|
|
191
|
+
clean = self.strip_ansi(output)
|
|
192
|
+
|
|
193
|
+
for pattern in self.QUESTION_PATTERNS:
|
|
194
|
+
if re.search(pattern, clean, re.IGNORECASE):
|
|
195
|
+
lines = clean.strip().split("\n")
|
|
196
|
+
question = None
|
|
197
|
+
options = []
|
|
198
|
+
|
|
199
|
+
for line in lines:
|
|
200
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
201
|
+
question = line.strip()
|
|
202
|
+
|
|
203
|
+
# Look for numbered options
|
|
204
|
+
opt_match = re.match(r"^\s*(\d+)[.):]\s*(.+)$", line)
|
|
205
|
+
if opt_match:
|
|
206
|
+
options.append(opt_match.group(2).strip())
|
|
207
|
+
|
|
208
|
+
logger.debug(
|
|
209
|
+
f"Detected question: {question}, options: {options if options else 'none'}"
|
|
210
|
+
)
|
|
211
|
+
return True, question, options if options else None
|
|
212
|
+
|
|
213
|
+
return False, None, None
|
|
214
|
+
|
|
215
|
+
def parse_response(self, output: str) -> ParsedResponse:
|
|
216
|
+
"""Extract meaningful content from Auggie output."""
|
|
217
|
+
if not output:
|
|
218
|
+
return ParsedResponse(
|
|
219
|
+
content="",
|
|
220
|
+
is_complete=False,
|
|
221
|
+
is_error=False,
|
|
222
|
+
is_question=False,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
clean = self.strip_ansi(output)
|
|
226
|
+
error_msg = self.detect_error(output)
|
|
227
|
+
is_question, question_text, options = self.detect_question(output)
|
|
228
|
+
is_complete = self.detect_idle(output)
|
|
229
|
+
|
|
230
|
+
response = ParsedResponse(
|
|
231
|
+
content=clean,
|
|
232
|
+
is_complete=is_complete,
|
|
233
|
+
is_error=error_msg is not None,
|
|
234
|
+
error_message=error_msg,
|
|
235
|
+
is_question=is_question,
|
|
236
|
+
question_text=question_text,
|
|
237
|
+
options=options,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
logger.debug(
|
|
241
|
+
f"Parsed response: complete={is_complete}, error={error_msg is not None}, "
|
|
242
|
+
f"question={is_question}"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return response
|
|
246
|
+
|
|
247
|
+
def inject_instructions(self, instructions: str) -> Optional[str]:
|
|
248
|
+
"""Return command to inject custom instructions.
|
|
249
|
+
|
|
250
|
+
Auggie supports .augment/instructions.md file for custom instructions.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
instructions: Instructions text to inject
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Command to write instructions file
|
|
257
|
+
"""
|
|
258
|
+
# Write to .augment/instructions.md
|
|
259
|
+
escaped = instructions.replace("'", "'\\''")
|
|
260
|
+
return f"mkdir -p .augment && echo '{escaped}' > .augment/instructions.md"
|