claude-mpm 3.4.27__py3-none-any.whl → 3.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/INSTRUCTIONS.md +182 -299
- claude_mpm/agents/agent_loader.py +283 -57
- claude_mpm/agents/agent_loader_integration.py +6 -9
- claude_mpm/agents/base_agent.json +2 -1
- claude_mpm/agents/base_agent_loader.py +1 -1
- claude_mpm/cli/__init__.py +5 -7
- claude_mpm/cli/commands/__init__.py +0 -2
- claude_mpm/cli/commands/agents.py +1 -1
- claude_mpm/cli/commands/memory.py +1 -1
- claude_mpm/cli/commands/run.py +12 -0
- claude_mpm/cli/parser.py +0 -13
- claude_mpm/cli/utils.py +1 -1
- claude_mpm/config/__init__.py +44 -2
- claude_mpm/config/agent_config.py +348 -0
- claude_mpm/config/paths.py +322 -0
- claude_mpm/constants.py +0 -1
- claude_mpm/core/__init__.py +2 -5
- claude_mpm/core/agent_registry.py +63 -17
- claude_mpm/core/claude_runner.py +354 -43
- claude_mpm/core/config.py +7 -1
- claude_mpm/core/config_aliases.py +4 -3
- claude_mpm/core/config_paths.py +151 -0
- claude_mpm/core/factories.py +4 -50
- claude_mpm/core/logger.py +11 -13
- claude_mpm/core/service_registry.py +2 -2
- claude_mpm/dashboard/static/js/components/agent-inference.js +101 -25
- claude_mpm/dashboard/static/js/components/event-processor.js +3 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +343 -83
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/init.py +37 -6
- claude_mpm/scripts/socketio_daemon.py +6 -2
- claude_mpm/services/__init__.py +71 -3
- claude_mpm/services/agents/__init__.py +85 -0
- claude_mpm/services/agents/deployment/__init__.py +21 -0
- claude_mpm/services/{agent_deployment.py → agents/deployment/agent_deployment.py} +192 -41
- claude_mpm/services/{agent_lifecycle_manager.py → agents/deployment/agent_lifecycle_manager.py} +11 -10
- claude_mpm/services/agents/loading/__init__.py +11 -0
- claude_mpm/services/{agent_profile_loader.py → agents/loading/agent_profile_loader.py} +9 -8
- claude_mpm/services/{base_agent_manager.py → agents/loading/base_agent_manager.py} +2 -2
- claude_mpm/services/{framework_agent_loader.py → agents/loading/framework_agent_loader.py} +116 -40
- claude_mpm/services/agents/management/__init__.py +9 -0
- claude_mpm/services/{agent_management_service.py → agents/management/agent_management_service.py} +6 -5
- claude_mpm/services/agents/memory/__init__.py +21 -0
- claude_mpm/services/{agent_memory_manager.py → agents/memory/agent_memory_manager.py} +3 -3
- claude_mpm/services/agents/registry/__init__.py +29 -0
- claude_mpm/services/{agent_registry.py → agents/registry/agent_registry.py} +101 -16
- claude_mpm/services/{deployed_agent_discovery.py → agents/registry/deployed_agent_discovery.py} +12 -2
- claude_mpm/services/{agent_modification_tracker.py → agents/registry/modification_tracker.py} +6 -5
- claude_mpm/services/async_session_logger.py +584 -0
- claude_mpm/services/claude_session_logger.py +299 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +17 -17
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +3 -3
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +1 -1
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +1 -1
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +19 -24
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +1 -1
- claude_mpm/services/framework_claude_md_generator.py +4 -2
- claude_mpm/services/memory/__init__.py +17 -0
- claude_mpm/services/{memory_builder.py → memory/builder.py} +3 -3
- claude_mpm/services/memory/cache/__init__.py +14 -0
- claude_mpm/services/{shared_prompt_cache.py → memory/cache/shared_prompt_cache.py} +1 -1
- claude_mpm/services/memory/cache/simple_cache.py +317 -0
- claude_mpm/services/{memory_optimizer.py → memory/optimizer.py} +1 -1
- claude_mpm/services/{memory_router.py → memory/router.py} +1 -1
- claude_mpm/services/optimized_hook_service.py +542 -0
- claude_mpm/services/project_registry.py +14 -8
- claude_mpm/services/response_tracker.py +237 -0
- claude_mpm/services/ticketing_service_original.py +4 -2
- claude_mpm/services/version_control/branch_strategy.py +3 -1
- claude_mpm/utils/paths.py +12 -10
- claude_mpm/utils/session_logging.py +114 -0
- claude_mpm/validation/agent_validator.py +2 -1
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/METADATA +26 -20
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/RECORD +83 -106
- claude_mpm/cli/commands/ui.py +0 -57
- claude_mpm/core/simple_runner.py +0 -1046
- claude_mpm/hooks/builtin/__init__.py +0 -1
- claude_mpm/hooks/builtin/logging_hook_example.py +0 -165
- claude_mpm/hooks/builtin/memory_hooks_example.py +0 -67
- claude_mpm/hooks/builtin/mpm_command_hook.py +0 -125
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +0 -124
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +0 -125
- claude_mpm/hooks/builtin/submit_hook_example.py +0 -100
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +0 -237
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +0 -240
- claude_mpm/hooks/builtin/workflow_start_hook.py +0 -181
- claude_mpm/orchestration/__init__.py +0 -6
- claude_mpm/orchestration/archive/direct_orchestrator.py +0 -195
- claude_mpm/orchestration/archive/factory.py +0 -215
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +0 -188
- claude_mpm/orchestration/archive/hook_integration_example.py +0 -178
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +0 -826
- claude_mpm/orchestration/archive/orchestrator.py +0 -501
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +0 -252
- claude_mpm/orchestration/archive/pty_orchestrator.py +0 -270
- claude_mpm/orchestration/archive/simple_orchestrator.py +0 -82
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +0 -801
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +0 -278
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +0 -187
- claude_mpm/schemas/workflow_validator.py +0 -411
- claude_mpm/services/parent_directory_manager/__init__.py +0 -577
- claude_mpm/services/parent_directory_manager/backup_manager.py +0 -258
- claude_mpm/services/parent_directory_manager/config_manager.py +0 -210
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +0 -279
- claude_mpm/services/parent_directory_manager/framework_protector.py +0 -143
- claude_mpm/services/parent_directory_manager/operations.py +0 -186
- claude_mpm/services/parent_directory_manager/state_manager.py +0 -624
- claude_mpm/services/parent_directory_manager/template_deployer.py +0 -579
- claude_mpm/services/parent_directory_manager/validation_manager.py +0 -378
- claude_mpm/services/parent_directory_manager/version_control_helper.py +0 -339
- claude_mpm/services/parent_directory_manager/version_manager.py +0 -222
- claude_mpm/ui/__init__.py +0 -1
- claude_mpm/ui/rich_terminal_ui.py +0 -295
- claude_mpm/ui/terminal_ui.py +0 -328
- /claude_mpm/services/{agent_versioning.py → agents/deployment/agent_versioning.py} +0 -0
- /claude_mpm/services/{agent_capabilities_generator.py → agents/management/agent_capabilities_generator.py} +0 -0
- /claude_mpm/services/{agent_persistence_service.py → agents/memory/agent_persistence_service.py} +0 -0
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/WHEEL +0 -0
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/top_level.txt +0 -0
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
"""Rich terminal UI for claude-mpm with live updates."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import subprocess
|
|
5
|
-
import threading
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Optional, List, Dict, Any
|
|
9
|
-
import json
|
|
10
|
-
|
|
11
|
-
from rich.console import Console
|
|
12
|
-
from rich.layout import Layout
|
|
13
|
-
from rich.live import Live
|
|
14
|
-
from rich.panel import Panel
|
|
15
|
-
from rich.table import Table
|
|
16
|
-
from rich.text import Text
|
|
17
|
-
from rich.syntax import Syntax
|
|
18
|
-
from rich.align import Align
|
|
19
|
-
from rich.box import ROUNDED
|
|
20
|
-
from rich import print as rprint
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
from ..services.ticket_manager import TicketManager
|
|
24
|
-
from ..core.logger import get_logger
|
|
25
|
-
except ImportError:
|
|
26
|
-
from claude_mpm.services.ticket_manager import TicketManager
|
|
27
|
-
from claude_mpm.core.logger import get_logger
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class RichTerminalUI:
|
|
31
|
-
"""Rich terminal UI with live updates."""
|
|
32
|
-
|
|
33
|
-
def __init__(self):
|
|
34
|
-
self.console = Console()
|
|
35
|
-
self.logger = get_logger("rich_ui")
|
|
36
|
-
self.ticket_manager = None
|
|
37
|
-
self.todos = []
|
|
38
|
-
self.tickets = []
|
|
39
|
-
self.claude_output = []
|
|
40
|
-
self.claude_process = None
|
|
41
|
-
self.running = True
|
|
42
|
-
|
|
43
|
-
# Try to initialize ticket manager
|
|
44
|
-
try:
|
|
45
|
-
self.ticket_manager = TicketManager()
|
|
46
|
-
except Exception as e:
|
|
47
|
-
self.logger.warning(f"Ticket manager not available: {e}")
|
|
48
|
-
|
|
49
|
-
# Create layout
|
|
50
|
-
self.layout = Layout()
|
|
51
|
-
self._setup_layout()
|
|
52
|
-
|
|
53
|
-
def _setup_layout(self):
|
|
54
|
-
"""Setup the layout structure."""
|
|
55
|
-
self.layout.split_column(
|
|
56
|
-
Layout(name="header", size=3),
|
|
57
|
-
Layout(name="body"),
|
|
58
|
-
Layout(name="footer", size=3)
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
# Split body into two columns
|
|
62
|
-
self.layout["body"].split_row(
|
|
63
|
-
Layout(name="main", ratio=2),
|
|
64
|
-
Layout(name="sidebar", ratio=1)
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
# Split sidebar into two sections
|
|
68
|
-
self.layout["sidebar"].split_column(
|
|
69
|
-
Layout(name="todos"),
|
|
70
|
-
Layout(name="tickets")
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
def _make_header(self) -> Panel:
|
|
74
|
-
"""Create header panel."""
|
|
75
|
-
grid = Table.grid(expand=True)
|
|
76
|
-
grid.add_column(justify="left")
|
|
77
|
-
grid.add_column(justify="center")
|
|
78
|
-
grid.add_column(justify="right")
|
|
79
|
-
|
|
80
|
-
grid.add_row(
|
|
81
|
-
"[bold blue]Claude MPM[/bold blue]",
|
|
82
|
-
"[yellow]Terminal UI[/yellow]",
|
|
83
|
-
datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
return Panel(grid, style="white on blue", box=ROUNDED)
|
|
87
|
-
|
|
88
|
-
def _make_footer(self) -> Panel:
|
|
89
|
-
"""Create footer panel."""
|
|
90
|
-
text = Text()
|
|
91
|
-
text.append("[F5]", style="bold yellow")
|
|
92
|
-
text.append(" Refresh ", style="white")
|
|
93
|
-
text.append("[Ctrl+N]", style="bold yellow")
|
|
94
|
-
text.append(" New Ticket ", style="white")
|
|
95
|
-
text.append("[Ctrl+C]", style="bold yellow")
|
|
96
|
-
text.append(" Quit", style="white")
|
|
97
|
-
|
|
98
|
-
return Panel(Align.center(text), style="white on blue", box=ROUNDED)
|
|
99
|
-
|
|
100
|
-
def _make_claude_panel(self) -> Panel:
|
|
101
|
-
"""Create Claude output panel."""
|
|
102
|
-
# Show last 30 lines
|
|
103
|
-
lines = self.claude_output[-30:] if self.claude_output else ["Waiting for Claude to start..."]
|
|
104
|
-
content = "\n".join(lines)
|
|
105
|
-
|
|
106
|
-
return Panel(
|
|
107
|
-
content,
|
|
108
|
-
title="[bold]Claude Output[/bold]",
|
|
109
|
-
border_style="green",
|
|
110
|
-
box=ROUNDED
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
def _make_todos_panel(self) -> Panel:
|
|
114
|
-
"""Create todos panel."""
|
|
115
|
-
table = Table(show_header=True, header_style="bold cyan", box=None)
|
|
116
|
-
table.add_column("", width=2)
|
|
117
|
-
table.add_column("Priority", width=8)
|
|
118
|
-
table.add_column("Task", overflow="fold")
|
|
119
|
-
|
|
120
|
-
for todo in self.todos[:10]: # Show top 10
|
|
121
|
-
status_icon = "✓" if todo.get('status') == 'completed' else "○"
|
|
122
|
-
priority = todo.get('priority', 'medium')
|
|
123
|
-
content = todo.get('content', '')
|
|
124
|
-
|
|
125
|
-
# Color based on priority
|
|
126
|
-
if priority == 'high':
|
|
127
|
-
style = "bold red"
|
|
128
|
-
elif priority == 'low':
|
|
129
|
-
style = "dim"
|
|
130
|
-
else:
|
|
131
|
-
style = "white"
|
|
132
|
-
|
|
133
|
-
if todo.get('status') == 'completed':
|
|
134
|
-
style = "green"
|
|
135
|
-
|
|
136
|
-
table.add_row(status_icon, priority.upper(), content, style=style)
|
|
137
|
-
|
|
138
|
-
return Panel(
|
|
139
|
-
table,
|
|
140
|
-
title=f"[bold]ToDo List ({len(self.todos)})[/bold]",
|
|
141
|
-
border_style="blue",
|
|
142
|
-
box=ROUNDED
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
def _make_tickets_panel(self) -> Panel:
|
|
146
|
-
"""Create tickets panel."""
|
|
147
|
-
table = Table(show_header=True, header_style="bold magenta", box=None)
|
|
148
|
-
table.add_column("ID", width=10)
|
|
149
|
-
table.add_column("Priority", width=8)
|
|
150
|
-
table.add_column("Title", overflow="fold")
|
|
151
|
-
|
|
152
|
-
for ticket in self.tickets[:10]: # Show top 10
|
|
153
|
-
ticket_id = ticket.get('id', 'N/A')
|
|
154
|
-
priority = ticket.get('priority', 'medium')
|
|
155
|
-
title = ticket.get('title', 'No title')
|
|
156
|
-
|
|
157
|
-
# Color based on priority
|
|
158
|
-
if priority == 'high':
|
|
159
|
-
style = "bold red"
|
|
160
|
-
elif priority == 'low':
|
|
161
|
-
style = "dim"
|
|
162
|
-
else:
|
|
163
|
-
style = "white"
|
|
164
|
-
|
|
165
|
-
table.add_row(ticket_id, priority.upper(), title, style=style)
|
|
166
|
-
|
|
167
|
-
return Panel(
|
|
168
|
-
table,
|
|
169
|
-
title=f"[bold]Tickets ({len(self.tickets)})[/bold]",
|
|
170
|
-
border_style="magenta",
|
|
171
|
-
box=ROUNDED
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
def _update_layout(self):
|
|
175
|
-
"""Update all layout panels."""
|
|
176
|
-
self.layout["header"].update(self._make_header())
|
|
177
|
-
self.layout["footer"].update(self._make_footer())
|
|
178
|
-
self.layout["main"].update(self._make_claude_panel())
|
|
179
|
-
self.layout["todos"].update(self._make_todos_panel())
|
|
180
|
-
self.layout["tickets"].update(self._make_tickets_panel())
|
|
181
|
-
|
|
182
|
-
def _start_claude(self):
|
|
183
|
-
"""Start Claude process in a thread."""
|
|
184
|
-
def run_claude():
|
|
185
|
-
try:
|
|
186
|
-
# Load system instructions
|
|
187
|
-
from ..core.simple_runner import SimpleClaudeRunner
|
|
188
|
-
runner = SimpleClaudeRunner(enable_tickets=False)
|
|
189
|
-
system_prompt = runner._create_system_prompt()
|
|
190
|
-
|
|
191
|
-
cmd = ["claude", "--model", "opus", "--dangerously-skip-permissions"]
|
|
192
|
-
if system_prompt:
|
|
193
|
-
cmd.extend(["--append-system-prompt", system_prompt])
|
|
194
|
-
|
|
195
|
-
self.claude_process = subprocess.Popen(
|
|
196
|
-
cmd,
|
|
197
|
-
stdout=subprocess.PIPE,
|
|
198
|
-
stderr=subprocess.STDOUT,
|
|
199
|
-
stdin=subprocess.PIPE,
|
|
200
|
-
text=True,
|
|
201
|
-
bufsize=1
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
# Read output
|
|
205
|
-
for line in iter(self.claude_process.stdout.readline, ''):
|
|
206
|
-
if line and self.running:
|
|
207
|
-
self.claude_output.append(line.rstrip())
|
|
208
|
-
# Keep last 1000 lines
|
|
209
|
-
if len(self.claude_output) > 1000:
|
|
210
|
-
self.claude_output = self.claude_output[-1000:]
|
|
211
|
-
|
|
212
|
-
except Exception as e:
|
|
213
|
-
self.claude_output.append(f"Error starting Claude: {e}")
|
|
214
|
-
|
|
215
|
-
thread = threading.Thread(target=run_claude, daemon=True)
|
|
216
|
-
thread.start()
|
|
217
|
-
|
|
218
|
-
def _load_todos(self):
|
|
219
|
-
"""Load ToDo items from Claude's todo file."""
|
|
220
|
-
try:
|
|
221
|
-
# Look for Claude's todo files
|
|
222
|
-
todo_dir = Path.home() / ".claude" / "todos"
|
|
223
|
-
if todo_dir.exists():
|
|
224
|
-
todos = []
|
|
225
|
-
for todo_file in todo_dir.glob("*.json"):
|
|
226
|
-
try:
|
|
227
|
-
with open(todo_file, 'r') as f:
|
|
228
|
-
data = json.load(f)
|
|
229
|
-
if isinstance(data, list):
|
|
230
|
-
todos.extend(data)
|
|
231
|
-
except:
|
|
232
|
-
pass
|
|
233
|
-
|
|
234
|
-
# Sort by priority and status
|
|
235
|
-
priority_order = {'high': 0, 'medium': 1, 'low': 2}
|
|
236
|
-
todos.sort(key=lambda x: (
|
|
237
|
-
x.get('status') == 'completed',
|
|
238
|
-
priority_order.get(x.get('priority', 'medium'), 1)
|
|
239
|
-
))
|
|
240
|
-
self.todos = todos
|
|
241
|
-
except Exception as e:
|
|
242
|
-
self.logger.error(f"Error loading todos: {e}")
|
|
243
|
-
|
|
244
|
-
def _load_tickets(self):
|
|
245
|
-
"""Load tickets from ticket manager."""
|
|
246
|
-
if self.ticket_manager:
|
|
247
|
-
try:
|
|
248
|
-
self.tickets = self.ticket_manager.list_recent_tickets(limit=20)
|
|
249
|
-
except Exception as e:
|
|
250
|
-
self.logger.error(f"Error loading tickets: {e}")
|
|
251
|
-
|
|
252
|
-
async def _refresh_data(self):
|
|
253
|
-
"""Refresh data periodically."""
|
|
254
|
-
while self.running:
|
|
255
|
-
self._load_todos()
|
|
256
|
-
self._load_tickets()
|
|
257
|
-
await asyncio.sleep(5) # Refresh every 5 seconds
|
|
258
|
-
|
|
259
|
-
async def run(self):
|
|
260
|
-
"""Run the rich terminal UI."""
|
|
261
|
-
# Start Claude
|
|
262
|
-
self._start_claude()
|
|
263
|
-
|
|
264
|
-
# Initial data load
|
|
265
|
-
self._load_todos()
|
|
266
|
-
self._load_tickets()
|
|
267
|
-
|
|
268
|
-
# Start refresh task
|
|
269
|
-
refresh_task = asyncio.create_task(self._refresh_data())
|
|
270
|
-
|
|
271
|
-
try:
|
|
272
|
-
with Live(self.layout, refresh_per_second=2, screen=True) as live:
|
|
273
|
-
while self.running:
|
|
274
|
-
self._update_layout()
|
|
275
|
-
await asyncio.sleep(0.5)
|
|
276
|
-
except KeyboardInterrupt:
|
|
277
|
-
self.running = False
|
|
278
|
-
finally:
|
|
279
|
-
refresh_task.cancel()
|
|
280
|
-
if self.claude_process:
|
|
281
|
-
self.claude_process.terminate()
|
|
282
|
-
|
|
283
|
-
def stop(self):
|
|
284
|
-
"""Stop the UI."""
|
|
285
|
-
self.running = False
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def main():
|
|
289
|
-
"""Run the rich terminal UI."""
|
|
290
|
-
ui = RichTerminalUI()
|
|
291
|
-
asyncio.run(ui.run())
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if __name__ == "__main__":
|
|
295
|
-
main()
|
claude_mpm/ui/terminal_ui.py
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
"""Terminal UI for claude-mpm with multiple panes."""
|
|
2
|
-
|
|
3
|
-
import curses
|
|
4
|
-
import subprocess
|
|
5
|
-
import threading
|
|
6
|
-
import queue
|
|
7
|
-
import time
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Optional, List, Dict, Any
|
|
10
|
-
import json
|
|
11
|
-
from datetime import datetime
|
|
12
|
-
|
|
13
|
-
try:
|
|
14
|
-
from ..services.ticket_manager import TicketManager
|
|
15
|
-
from ..core.logger import get_logger
|
|
16
|
-
except ImportError:
|
|
17
|
-
from claude_mpm.services.ticket_manager import TicketManager
|
|
18
|
-
from claude_mpm.core.logger import get_logger
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class TerminalUI:
|
|
22
|
-
"""Multi-pane terminal UI for claude-mpm."""
|
|
23
|
-
|
|
24
|
-
def __init__(self):
|
|
25
|
-
self.logger = get_logger("terminal_ui")
|
|
26
|
-
self.ticket_manager = None
|
|
27
|
-
self.todos = []
|
|
28
|
-
self.tickets = []
|
|
29
|
-
self.claude_output = []
|
|
30
|
-
self.active_pane = 0 # 0=claude, 1=todos, 2=tickets
|
|
31
|
-
self.claude_process = None
|
|
32
|
-
self.output_queue = queue.Queue()
|
|
33
|
-
|
|
34
|
-
# Try to initialize ticket manager
|
|
35
|
-
try:
|
|
36
|
-
self.ticket_manager = TicketManager()
|
|
37
|
-
except Exception as e:
|
|
38
|
-
self.logger.warning(f"Ticket manager not available: {e}")
|
|
39
|
-
|
|
40
|
-
def run(self):
|
|
41
|
-
"""Run the terminal UI."""
|
|
42
|
-
curses.wrapper(self._main)
|
|
43
|
-
|
|
44
|
-
def _main(self, stdscr):
|
|
45
|
-
"""Main curses loop."""
|
|
46
|
-
# Setup
|
|
47
|
-
curses.curs_set(0) # Hide cursor
|
|
48
|
-
stdscr.nodelay(1) # Non-blocking input
|
|
49
|
-
stdscr.timeout(100) # Refresh every 100ms
|
|
50
|
-
|
|
51
|
-
# Initialize color pairs
|
|
52
|
-
curses.start_color()
|
|
53
|
-
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) # Header
|
|
54
|
-
curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE) # Active pane
|
|
55
|
-
curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK) # Success
|
|
56
|
-
curses.init_pair(4, curses.COLOR_RED, curses.COLOR_BLACK) # Error
|
|
57
|
-
|
|
58
|
-
# Start Claude process
|
|
59
|
-
self._start_claude()
|
|
60
|
-
|
|
61
|
-
# Load initial data
|
|
62
|
-
self._load_todos()
|
|
63
|
-
self._load_tickets()
|
|
64
|
-
|
|
65
|
-
while True:
|
|
66
|
-
# Get terminal size
|
|
67
|
-
height, width = stdscr.getmaxyx()
|
|
68
|
-
|
|
69
|
-
# Clear screen
|
|
70
|
-
stdscr.clear()
|
|
71
|
-
|
|
72
|
-
# Draw UI
|
|
73
|
-
self._draw_header(stdscr, width)
|
|
74
|
-
self._draw_panes(stdscr, height, width)
|
|
75
|
-
self._draw_footer(stdscr, height, width)
|
|
76
|
-
|
|
77
|
-
# Handle input
|
|
78
|
-
key = stdscr.getch()
|
|
79
|
-
if key == ord('q') or key == ord('Q'):
|
|
80
|
-
if self._confirm_quit(stdscr):
|
|
81
|
-
break
|
|
82
|
-
elif key == ord('\t'): # Tab to switch panes
|
|
83
|
-
self.active_pane = (self.active_pane + 1) % 3
|
|
84
|
-
elif key == curses.KEY_F5: # Refresh
|
|
85
|
-
self._load_todos()
|
|
86
|
-
self._load_tickets()
|
|
87
|
-
elif key == ord('n') or key == ord('N'): # New ticket
|
|
88
|
-
if self.active_pane == 2:
|
|
89
|
-
self._create_ticket(stdscr)
|
|
90
|
-
|
|
91
|
-
# Update Claude output
|
|
92
|
-
self._update_claude_output()
|
|
93
|
-
|
|
94
|
-
# Refresh display
|
|
95
|
-
stdscr.refresh()
|
|
96
|
-
|
|
97
|
-
def _draw_header(self, stdscr, width):
|
|
98
|
-
"""Draw the header."""
|
|
99
|
-
header = " Claude MPM Terminal UI "
|
|
100
|
-
padding = (width - len(header)) // 2
|
|
101
|
-
stdscr.attron(curses.color_pair(1))
|
|
102
|
-
stdscr.addstr(0, 0, " " * width)
|
|
103
|
-
stdscr.addstr(0, padding, header)
|
|
104
|
-
stdscr.attroff(curses.color_pair(1))
|
|
105
|
-
|
|
106
|
-
def _draw_panes(self, stdscr, height, width):
|
|
107
|
-
"""Draw the three panes."""
|
|
108
|
-
# Calculate pane dimensions
|
|
109
|
-
pane_height = height - 3 # Minus header and footer
|
|
110
|
-
claude_width = width // 2
|
|
111
|
-
side_width = width - claude_width - 1
|
|
112
|
-
|
|
113
|
-
# Draw Claude pane (left half)
|
|
114
|
-
self._draw_claude_pane(stdscr, 1, 0, pane_height, claude_width)
|
|
115
|
-
|
|
116
|
-
# Draw vertical separator
|
|
117
|
-
for y in range(1, height - 1):
|
|
118
|
-
stdscr.addch(y, claude_width, '│')
|
|
119
|
-
|
|
120
|
-
# Draw ToDo pane (top right)
|
|
121
|
-
todo_height = pane_height // 2
|
|
122
|
-
self._draw_todo_pane(stdscr, 1, claude_width + 1, todo_height, side_width)
|
|
123
|
-
|
|
124
|
-
# Draw horizontal separator
|
|
125
|
-
stdscr.hline(todo_height + 1, claude_width + 1, '─', side_width)
|
|
126
|
-
|
|
127
|
-
# Draw Tickets pane (bottom right)
|
|
128
|
-
ticket_height = pane_height - todo_height - 1
|
|
129
|
-
self._draw_ticket_pane(stdscr, todo_height + 2, claude_width + 1, ticket_height, side_width)
|
|
130
|
-
|
|
131
|
-
def _draw_claude_pane(self, stdscr, y, x, height, width):
|
|
132
|
-
"""Draw Claude output pane."""
|
|
133
|
-
# Title
|
|
134
|
-
title = " Claude Output "
|
|
135
|
-
if self.active_pane == 0:
|
|
136
|
-
stdscr.attron(curses.color_pair(2))
|
|
137
|
-
stdscr.addstr(y, x, title + " " * (width - len(title)))
|
|
138
|
-
if self.active_pane == 0:
|
|
139
|
-
stdscr.attroff(curses.color_pair(2))
|
|
140
|
-
|
|
141
|
-
# Content
|
|
142
|
-
content_height = height - 2
|
|
143
|
-
start_line = max(0, len(self.claude_output) - content_height)
|
|
144
|
-
|
|
145
|
-
for i, line in enumerate(self.claude_output[start_line:start_line + content_height]):
|
|
146
|
-
if y + i + 2 < y + height:
|
|
147
|
-
truncated = line[:width-2] if len(line) > width-2 else line
|
|
148
|
-
try:
|
|
149
|
-
stdscr.addstr(y + i + 2, x + 1, truncated)
|
|
150
|
-
except curses.error:
|
|
151
|
-
pass # Ignore if we can't write (edge of screen)
|
|
152
|
-
|
|
153
|
-
def _draw_todo_pane(self, stdscr, y, x, height, width):
|
|
154
|
-
"""Draw ToDo list pane."""
|
|
155
|
-
# Title
|
|
156
|
-
title = f" ToDo List ({len(self.todos)}) "
|
|
157
|
-
if self.active_pane == 1:
|
|
158
|
-
stdscr.attron(curses.color_pair(2))
|
|
159
|
-
stdscr.addstr(y, x, title + " " * (width - len(title)))
|
|
160
|
-
if self.active_pane == 1:
|
|
161
|
-
stdscr.attroff(curses.color_pair(2))
|
|
162
|
-
|
|
163
|
-
# Content
|
|
164
|
-
content_height = height - 2
|
|
165
|
-
for i, todo in enumerate(self.todos[:content_height]):
|
|
166
|
-
if y + i + 2 < y + height:
|
|
167
|
-
status_icon = "✓" if todo.get('status') == 'completed' else "○"
|
|
168
|
-
priority = todo.get('priority', 'medium')[0].upper()
|
|
169
|
-
text = f"{status_icon} [{priority}] {todo.get('content', '')}"
|
|
170
|
-
truncated = text[:width-2] if len(text) > width-2 else text
|
|
171
|
-
|
|
172
|
-
# Color based on status
|
|
173
|
-
if todo.get('status') == 'completed':
|
|
174
|
-
stdscr.attron(curses.color_pair(3))
|
|
175
|
-
try:
|
|
176
|
-
stdscr.addstr(y + i + 2, x + 1, truncated)
|
|
177
|
-
except curses.error:
|
|
178
|
-
pass
|
|
179
|
-
if todo.get('status') == 'completed':
|
|
180
|
-
stdscr.attroff(curses.color_pair(3))
|
|
181
|
-
|
|
182
|
-
def _draw_ticket_pane(self, stdscr, y, x, height, width):
|
|
183
|
-
"""Draw Tickets pane."""
|
|
184
|
-
# Title
|
|
185
|
-
title = f" Tickets ({len(self.tickets)}) [N]ew "
|
|
186
|
-
if self.active_pane == 2:
|
|
187
|
-
stdscr.attron(curses.color_pair(2))
|
|
188
|
-
stdscr.addstr(y, x, title + " " * (width - len(title)))
|
|
189
|
-
if self.active_pane == 2:
|
|
190
|
-
stdscr.attroff(curses.color_pair(2))
|
|
191
|
-
|
|
192
|
-
# Content
|
|
193
|
-
content_height = height - 2
|
|
194
|
-
for i, ticket in enumerate(self.tickets[:content_height]):
|
|
195
|
-
if y + i + 2 < y + height:
|
|
196
|
-
ticket_id = ticket.get('id', 'N/A')
|
|
197
|
-
title = ticket.get('title', 'No title')
|
|
198
|
-
text = f"[{ticket_id}] {title}"
|
|
199
|
-
truncated = text[:width-2] if len(text) > width-2 else text
|
|
200
|
-
try:
|
|
201
|
-
stdscr.addstr(y + i + 2, x + 1, truncated)
|
|
202
|
-
except curses.error:
|
|
203
|
-
pass
|
|
204
|
-
|
|
205
|
-
def _draw_footer(self, stdscr, height, width):
|
|
206
|
-
"""Draw the footer."""
|
|
207
|
-
footer = " [Tab] Switch Pane | [F5] Refresh | [Q] Quit "
|
|
208
|
-
stdscr.attron(curses.color_pair(1))
|
|
209
|
-
stdscr.addstr(height - 1, 0, " " * width)
|
|
210
|
-
stdscr.addstr(height - 1, 0, footer)
|
|
211
|
-
stdscr.attroff(curses.color_pair(1))
|
|
212
|
-
|
|
213
|
-
def _start_claude(self):
|
|
214
|
-
"""Start Claude process in a thread."""
|
|
215
|
-
def run_claude():
|
|
216
|
-
try:
|
|
217
|
-
cmd = ["claude", "--model", "opus", "--dangerously-skip-permissions"]
|
|
218
|
-
self.claude_process = subprocess.Popen(
|
|
219
|
-
cmd,
|
|
220
|
-
stdout=subprocess.PIPE,
|
|
221
|
-
stderr=subprocess.STDOUT,
|
|
222
|
-
stdin=subprocess.PIPE,
|
|
223
|
-
text=True,
|
|
224
|
-
bufsize=1
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
# Read output
|
|
228
|
-
for line in iter(self.claude_process.stdout.readline, ''):
|
|
229
|
-
if line:
|
|
230
|
-
self.output_queue.put(line.rstrip())
|
|
231
|
-
|
|
232
|
-
except Exception as e:
|
|
233
|
-
self.output_queue.put(f"Error starting Claude: {e}")
|
|
234
|
-
|
|
235
|
-
thread = threading.Thread(target=run_claude, daemon=True)
|
|
236
|
-
thread.start()
|
|
237
|
-
|
|
238
|
-
def _update_claude_output(self):
|
|
239
|
-
"""Update Claude output from queue."""
|
|
240
|
-
try:
|
|
241
|
-
while True:
|
|
242
|
-
line = self.output_queue.get_nowait()
|
|
243
|
-
self.claude_output.append(line)
|
|
244
|
-
# Keep last 1000 lines
|
|
245
|
-
if len(self.claude_output) > 1000:
|
|
246
|
-
self.claude_output = self.claude_output[-1000:]
|
|
247
|
-
except queue.Empty:
|
|
248
|
-
pass
|
|
249
|
-
|
|
250
|
-
def _load_todos(self):
|
|
251
|
-
"""Load ToDo items from Claude's todo file."""
|
|
252
|
-
try:
|
|
253
|
-
# Look for Claude's todo files
|
|
254
|
-
todo_dir = Path.home() / ".claude" / "todos"
|
|
255
|
-
if todo_dir.exists():
|
|
256
|
-
todos = []
|
|
257
|
-
for todo_file in todo_dir.glob("*.json"):
|
|
258
|
-
try:
|
|
259
|
-
with open(todo_file, 'r') as f:
|
|
260
|
-
data = json.load(f)
|
|
261
|
-
if isinstance(data, list):
|
|
262
|
-
todos.extend(data)
|
|
263
|
-
except:
|
|
264
|
-
pass
|
|
265
|
-
|
|
266
|
-
# Sort by priority and status
|
|
267
|
-
priority_order = {'high': 0, 'medium': 1, 'low': 2}
|
|
268
|
-
todos.sort(key=lambda x: (
|
|
269
|
-
x.get('status') == 'completed',
|
|
270
|
-
priority_order.get(x.get('priority', 'medium'), 1)
|
|
271
|
-
))
|
|
272
|
-
self.todos = todos[:20] # Keep top 20
|
|
273
|
-
except Exception as e:
|
|
274
|
-
self.logger.error(f"Error loading todos: {e}")
|
|
275
|
-
|
|
276
|
-
def _load_tickets(self):
|
|
277
|
-
"""Load tickets from ticket manager."""
|
|
278
|
-
if self.ticket_manager:
|
|
279
|
-
try:
|
|
280
|
-
self.tickets = self.ticket_manager.list_recent_tickets(limit=20)
|
|
281
|
-
except Exception as e:
|
|
282
|
-
self.logger.error(f"Error loading tickets: {e}")
|
|
283
|
-
|
|
284
|
-
def _create_ticket(self, stdscr):
|
|
285
|
-
"""Create a new ticket."""
|
|
286
|
-
if not self.ticket_manager:
|
|
287
|
-
return
|
|
288
|
-
|
|
289
|
-
# Simple input dialog
|
|
290
|
-
curses.echo()
|
|
291
|
-
stdscr.addstr(10, 10, "Enter ticket title: ")
|
|
292
|
-
stdscr.refresh()
|
|
293
|
-
title = stdscr.getstr(10, 30, 60).decode('utf-8')
|
|
294
|
-
curses.noecho()
|
|
295
|
-
|
|
296
|
-
if title:
|
|
297
|
-
try:
|
|
298
|
-
ticket = self.ticket_manager.create_ticket(
|
|
299
|
-
title=title,
|
|
300
|
-
description="Created from Terminal UI",
|
|
301
|
-
priority="medium"
|
|
302
|
-
)
|
|
303
|
-
self._load_tickets()
|
|
304
|
-
except Exception as e:
|
|
305
|
-
self.logger.error(f"Error creating ticket: {e}")
|
|
306
|
-
|
|
307
|
-
def _confirm_quit(self, stdscr):
|
|
308
|
-
"""Confirm quit dialog."""
|
|
309
|
-
height, width = stdscr.getmaxyx()
|
|
310
|
-
msg = "Really quit? (y/n)"
|
|
311
|
-
y = height // 2
|
|
312
|
-
x = (width - len(msg)) // 2
|
|
313
|
-
|
|
314
|
-
stdscr.addstr(y, x, msg)
|
|
315
|
-
stdscr.refresh()
|
|
316
|
-
|
|
317
|
-
key = stdscr.getch()
|
|
318
|
-
return key == ord('y') or key == ord('Y')
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
def main():
|
|
322
|
-
"""Run the terminal UI."""
|
|
323
|
-
ui = TerminalUI()
|
|
324
|
-
ui.run()
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if __name__ == "__main__":
|
|
328
|
-
main()
|
|
File without changes
|
|
File without changes
|
/claude_mpm/services/{agent_persistence_service.py → agents/memory/agent_persistence_service.py}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|