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,501 +0,0 @@
|
|
|
1
|
-
"""Core orchestrator for Claude MPM."""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
import threading
|
|
5
|
-
import queue
|
|
6
|
-
import os
|
|
7
|
-
import sys
|
|
8
|
-
import logging
|
|
9
|
-
import select
|
|
10
|
-
import fcntl
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Optional, List, Dict, Any, IO
|
|
13
|
-
from datetime import datetime
|
|
14
|
-
|
|
15
|
-
try:
|
|
16
|
-
# Try relative imports first
|
|
17
|
-
from ..core.logger import get_logger, setup_logging
|
|
18
|
-
from ..utils.subprocess_runner import SubprocessRunner
|
|
19
|
-
# TicketExtractor removed from project
|
|
20
|
-
from ..core.framework_loader import FrameworkLoader
|
|
21
|
-
from .agent_delegator import AgentDelegator
|
|
22
|
-
except ImportError:
|
|
23
|
-
# Fall back to absolute imports
|
|
24
|
-
from core.logger import get_logger, setup_logging
|
|
25
|
-
from utils.subprocess_runner import SubprocessRunner
|
|
26
|
-
# TicketExtractor removed from project
|
|
27
|
-
from core.framework_loader import FrameworkLoader
|
|
28
|
-
from orchestration.agent_delegator import AgentDelegator
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class MPMOrchestrator:
|
|
32
|
-
"""
|
|
33
|
-
Orchestrates Claude as a subprocess with framework injection and ticket extraction.
|
|
34
|
-
|
|
35
|
-
This is the core component that:
|
|
36
|
-
1. Launches Claude as a child process
|
|
37
|
-
2. Injects framework instructions
|
|
38
|
-
3. Intercepts I/O for ticket extraction
|
|
39
|
-
4. Manages the session lifecycle
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
def __init__(
|
|
43
|
-
self,
|
|
44
|
-
framework_path: Optional[Path] = None,
|
|
45
|
-
agents_dir: Optional[Path] = None,
|
|
46
|
-
log_level: str = "OFF",
|
|
47
|
-
log_dir: Optional[Path] = None,
|
|
48
|
-
):
|
|
49
|
-
"""
|
|
50
|
-
Initialize the orchestrator.
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
framework_path: Path to framework directory
|
|
54
|
-
agents_dir: Custom agents directory
|
|
55
|
-
log_level: Logging level (OFF, INFO, DEBUG)
|
|
56
|
-
log_dir: Custom log directory
|
|
57
|
-
"""
|
|
58
|
-
self.log_level = log_level
|
|
59
|
-
self.log_dir = log_dir or (Path.home() / ".claude-mpm" / "logs")
|
|
60
|
-
|
|
61
|
-
# Set up logging
|
|
62
|
-
if log_level != "OFF":
|
|
63
|
-
self.logger = setup_logging(level=log_level, log_dir=log_dir)
|
|
64
|
-
self.logger.info(f"Initializing MPM Orchestrator (log_level={log_level})")
|
|
65
|
-
else:
|
|
66
|
-
# Minimal logger
|
|
67
|
-
self.logger = get_logger("orchestrator")
|
|
68
|
-
self.logger.setLevel(logging.WARNING)
|
|
69
|
-
|
|
70
|
-
# Components
|
|
71
|
-
self.framework_loader = FrameworkLoader(framework_path, agents_dir)
|
|
72
|
-
# TicketExtractor removed from project
|
|
73
|
-
self.agent_delegator = AgentDelegator(self.framework_loader.agent_registry)
|
|
74
|
-
|
|
75
|
-
# Process management
|
|
76
|
-
self.process: Optional[subprocess.Popen] = None
|
|
77
|
-
self.output_queue = queue.Queue()
|
|
78
|
-
self.input_queue = queue.Queue()
|
|
79
|
-
|
|
80
|
-
# State
|
|
81
|
-
self.first_interaction = True
|
|
82
|
-
self.session_start = datetime.now()
|
|
83
|
-
self.session_log = []
|
|
84
|
-
# Ticket creation removed from project
|
|
85
|
-
|
|
86
|
-
# Threading
|
|
87
|
-
self.output_thread: Optional[threading.Thread] = None
|
|
88
|
-
self.input_thread: Optional[threading.Thread] = None
|
|
89
|
-
self.running = False
|
|
90
|
-
|
|
91
|
-
def start(self) -> bool:
|
|
92
|
-
"""
|
|
93
|
-
Start the orchestrated Claude session.
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
True if successfully started, False otherwise
|
|
97
|
-
"""
|
|
98
|
-
try:
|
|
99
|
-
# Find Claude executable
|
|
100
|
-
claude_cmd = self._find_claude_executable()
|
|
101
|
-
if not claude_cmd:
|
|
102
|
-
self.logger.error("Claude executable not found")
|
|
103
|
-
return False
|
|
104
|
-
|
|
105
|
-
# Build command
|
|
106
|
-
cmd = [claude_cmd]
|
|
107
|
-
|
|
108
|
-
# Add model and permissions flags if needed
|
|
109
|
-
if "--model" not in claude_cmd:
|
|
110
|
-
cmd.extend(["--model", "opus"])
|
|
111
|
-
if "--dangerously-skip-permissions" not in claude_cmd:
|
|
112
|
-
cmd.append("--dangerously-skip-permissions")
|
|
113
|
-
|
|
114
|
-
# For non-interactive mode, we can pass the prompt directly
|
|
115
|
-
if hasattr(self, '_initial_prompt'):
|
|
116
|
-
cmd.extend(["--print", self._initial_prompt])
|
|
117
|
-
|
|
118
|
-
self.logger.info(f"Starting Claude subprocess: {' '.join(cmd)}")
|
|
119
|
-
|
|
120
|
-
# Start subprocess without PTY for now
|
|
121
|
-
self.process = subprocess.Popen(
|
|
122
|
-
cmd,
|
|
123
|
-
stdin=subprocess.PIPE,
|
|
124
|
-
stdout=subprocess.PIPE,
|
|
125
|
-
stderr=subprocess.PIPE,
|
|
126
|
-
text=True,
|
|
127
|
-
bufsize=0, # Unbuffered
|
|
128
|
-
universal_newlines=True,
|
|
129
|
-
)
|
|
130
|
-
self.use_pty = False
|
|
131
|
-
|
|
132
|
-
self.logger.info(f"Claude subprocess started with PID: {self.process.pid}")
|
|
133
|
-
|
|
134
|
-
# Start I/O threads
|
|
135
|
-
self.running = True
|
|
136
|
-
self.output_thread = threading.Thread(target=self._output_reader, daemon=True)
|
|
137
|
-
self.error_thread = threading.Thread(target=self._error_reader, daemon=True)
|
|
138
|
-
self.input_thread = threading.Thread(target=self._input_writer, daemon=True)
|
|
139
|
-
|
|
140
|
-
self.output_thread.start()
|
|
141
|
-
self.error_thread.start()
|
|
142
|
-
self.input_thread.start()
|
|
143
|
-
|
|
144
|
-
# Give Claude a moment to start up
|
|
145
|
-
import time
|
|
146
|
-
time.sleep(0.5)
|
|
147
|
-
|
|
148
|
-
return True
|
|
149
|
-
|
|
150
|
-
except Exception as e:
|
|
151
|
-
self.logger.error(f"Failed to start Claude: {e}")
|
|
152
|
-
return False
|
|
153
|
-
|
|
154
|
-
def _find_claude_executable(self) -> Optional[str]:
|
|
155
|
-
"""Find the Claude executable."""
|
|
156
|
-
# Check common locations
|
|
157
|
-
candidates = ["claude", "claude-cli", "/usr/local/bin/claude"]
|
|
158
|
-
|
|
159
|
-
for candidate in candidates:
|
|
160
|
-
if self._is_executable(candidate):
|
|
161
|
-
return candidate
|
|
162
|
-
|
|
163
|
-
# Check PATH
|
|
164
|
-
import shutil
|
|
165
|
-
claude_path = shutil.which("claude")
|
|
166
|
-
if claude_path:
|
|
167
|
-
return claude_path
|
|
168
|
-
|
|
169
|
-
return None
|
|
170
|
-
|
|
171
|
-
def _is_executable(self, path: str) -> bool:
|
|
172
|
-
"""Check if a path is an executable file."""
|
|
173
|
-
try:
|
|
174
|
-
return os.path.isfile(path) and os.access(path, os.X_OK)
|
|
175
|
-
except:
|
|
176
|
-
return False
|
|
177
|
-
|
|
178
|
-
def _output_reader(self):
|
|
179
|
-
"""Read output from Claude subprocess."""
|
|
180
|
-
response_buffer = []
|
|
181
|
-
try:
|
|
182
|
-
while self.running and self.process and self.process.poll() is None:
|
|
183
|
-
line = self.process.stdout.readline()
|
|
184
|
-
if line:
|
|
185
|
-
line = line.rstrip()
|
|
186
|
-
if self.log_level == "DEBUG":
|
|
187
|
-
self.logger.debug(f"Claude output: {line}")
|
|
188
|
-
response_buffer.append(line)
|
|
189
|
-
self._process_output_line(line)
|
|
190
|
-
|
|
191
|
-
except Exception as e:
|
|
192
|
-
if self.log_level != "OFF":
|
|
193
|
-
self.logger.error(f"Output reader error: {e}")
|
|
194
|
-
finally:
|
|
195
|
-
# Save complete response in DEBUG mode
|
|
196
|
-
if self.log_level == "DEBUG" and response_buffer:
|
|
197
|
-
session_path = Path.home() / ".claude-mpm" / "session"
|
|
198
|
-
session_path.mkdir(parents=True, exist_ok=True)
|
|
199
|
-
|
|
200
|
-
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')[:-3]
|
|
201
|
-
response_file = session_path / f"response_{timestamp}.txt"
|
|
202
|
-
response_file.write_text('\n'.join(response_buffer))
|
|
203
|
-
self.logger.debug(f"Response saved to: {response_file}")
|
|
204
|
-
|
|
205
|
-
if self.log_level == "DEBUG":
|
|
206
|
-
self.logger.debug("Output reader thread ending")
|
|
207
|
-
|
|
208
|
-
def _error_reader(self):
|
|
209
|
-
"""Read error output from Claude subprocess."""
|
|
210
|
-
if self.use_pty:
|
|
211
|
-
# In PTY mode, stderr goes to the same terminal
|
|
212
|
-
return
|
|
213
|
-
|
|
214
|
-
try:
|
|
215
|
-
while self.running and self.process and self.process.poll() is None:
|
|
216
|
-
line = self.process.stderr.readline()
|
|
217
|
-
if line:
|
|
218
|
-
line = line.rstrip()
|
|
219
|
-
if self.log_level != "OFF":
|
|
220
|
-
self.logger.error(f"Claude stderr: {line}")
|
|
221
|
-
# Also display errors to user
|
|
222
|
-
print(f"Error: {line}")
|
|
223
|
-
except Exception as e:
|
|
224
|
-
if self.log_level != "OFF":
|
|
225
|
-
self.logger.error(f"Error reader error: {e}")
|
|
226
|
-
|
|
227
|
-
def _input_writer(self):
|
|
228
|
-
"""Write input to Claude subprocess."""
|
|
229
|
-
try:
|
|
230
|
-
while self.running and self.process:
|
|
231
|
-
try:
|
|
232
|
-
user_input = self.input_queue.get(timeout=0.1)
|
|
233
|
-
|
|
234
|
-
# Inject framework on first interaction
|
|
235
|
-
if self.first_interaction:
|
|
236
|
-
if self.log_level != "OFF":
|
|
237
|
-
self.logger.info("Injecting framework instructions")
|
|
238
|
-
framework = self.framework_loader.get_framework_instructions()
|
|
239
|
-
|
|
240
|
-
# Log framework details for debugging
|
|
241
|
-
if self.log_level == "DEBUG":
|
|
242
|
-
self.logger.debug(f"Framework length: {len(framework)} characters")
|
|
243
|
-
self.logger.debug(f"Framework preview: {framework[:200]}...")
|
|
244
|
-
|
|
245
|
-
full_input = framework + "\n\nUser Input: " + user_input
|
|
246
|
-
|
|
247
|
-
# Save prompt to session directory when debugging
|
|
248
|
-
if self.log_level == "DEBUG":
|
|
249
|
-
session_path = Path.home() / ".claude-mpm" / "session"
|
|
250
|
-
session_path.mkdir(parents=True, exist_ok=True)
|
|
251
|
-
|
|
252
|
-
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')[:-3]
|
|
253
|
-
prompt_file = session_path / f"prompt_{timestamp}.txt"
|
|
254
|
-
prompt_file.write_text(full_input)
|
|
255
|
-
self.logger.debug(f"Full prompt saved to: {prompt_file}")
|
|
256
|
-
|
|
257
|
-
# Also save user input separately
|
|
258
|
-
user_input_file = session_path / f"user_input_{timestamp}.txt"
|
|
259
|
-
user_input_file.write_text(user_input)
|
|
260
|
-
|
|
261
|
-
# Keep backward compatibility with prompts directory
|
|
262
|
-
prompt_log_path = Path.home() / ".claude-mpm" / "prompts"
|
|
263
|
-
prompt_log_path.mkdir(parents=True, exist_ok=True)
|
|
264
|
-
prompt_file = prompt_log_path / f"prompt_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
|
|
265
|
-
prompt_file.write_text(full_input)
|
|
266
|
-
|
|
267
|
-
self.first_interaction = False
|
|
268
|
-
else:
|
|
269
|
-
full_input = user_input
|
|
270
|
-
|
|
271
|
-
# Send to Claude
|
|
272
|
-
if self.log_level == "DEBUG":
|
|
273
|
-
self.logger.debug(f"Writing to stdin: {len(full_input)} characters")
|
|
274
|
-
|
|
275
|
-
if self.use_pty:
|
|
276
|
-
# Write to PTY
|
|
277
|
-
os.write(self.master_fd, (full_input + "\n").encode('utf-8'))
|
|
278
|
-
else:
|
|
279
|
-
# Write to pipe
|
|
280
|
-
self.process.stdin.write(full_input + "\n")
|
|
281
|
-
self.process.stdin.flush()
|
|
282
|
-
|
|
283
|
-
# Log session
|
|
284
|
-
self._log_interaction("input", user_input)
|
|
285
|
-
|
|
286
|
-
except queue.Empty:
|
|
287
|
-
continue
|
|
288
|
-
|
|
289
|
-
except Exception as e:
|
|
290
|
-
self.logger.error(f"Input writer error: {e}")
|
|
291
|
-
finally:
|
|
292
|
-
self.logger.debug("Input writer thread ending")
|
|
293
|
-
|
|
294
|
-
def send_input(self, text: str):
|
|
295
|
-
"""Send input to Claude."""
|
|
296
|
-
self.input_queue.put(text)
|
|
297
|
-
|
|
298
|
-
def get_output(self, timeout: float = 0.1) -> Optional[str]:
|
|
299
|
-
"""Get output from Claude (non-blocking)."""
|
|
300
|
-
try:
|
|
301
|
-
return self.output_queue.get(timeout=timeout)
|
|
302
|
-
except queue.Empty:
|
|
303
|
-
return None
|
|
304
|
-
|
|
305
|
-
def _strip_ansi_codes(self, text: str) -> str:
|
|
306
|
-
"""Remove ANSI escape codes from text."""
|
|
307
|
-
import re
|
|
308
|
-
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
309
|
-
return ansi_escape.sub('', text)
|
|
310
|
-
|
|
311
|
-
def _process_output_line(self, line: str):
|
|
312
|
-
"""Process a line of output from Claude."""
|
|
313
|
-
# Ticket extraction removed from project
|
|
314
|
-
|
|
315
|
-
# Extract agent delegations
|
|
316
|
-
delegations = self.agent_delegator.extract_delegations(line)
|
|
317
|
-
for delegation in delegations:
|
|
318
|
-
if self.log_level != "OFF":
|
|
319
|
-
self.logger.info(f"Detected delegation to {delegation['agent']}: {delegation['task']}")
|
|
320
|
-
|
|
321
|
-
# Queue for display
|
|
322
|
-
self.output_queue.put(line)
|
|
323
|
-
|
|
324
|
-
# Log session
|
|
325
|
-
self._log_interaction("output", line)
|
|
326
|
-
|
|
327
|
-
def _log_interaction(self, interaction_type: str, content: str):
|
|
328
|
-
"""Log interaction for session history."""
|
|
329
|
-
self.session_log.append({
|
|
330
|
-
"type": interaction_type,
|
|
331
|
-
"content": content,
|
|
332
|
-
"timestamp": datetime.now().isoformat()
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
def stop(self):
|
|
336
|
-
"""Stop the orchestrated session."""
|
|
337
|
-
self.logger.info("Stopping orchestrator")
|
|
338
|
-
self.running = False
|
|
339
|
-
|
|
340
|
-
# Terminate subprocess
|
|
341
|
-
if self.process:
|
|
342
|
-
self.process.terminate()
|
|
343
|
-
try:
|
|
344
|
-
self.process.wait(timeout=5)
|
|
345
|
-
except subprocess.TimeoutExpired:
|
|
346
|
-
self.process.kill()
|
|
347
|
-
self.process.wait()
|
|
348
|
-
|
|
349
|
-
self.logger.info(f"Claude subprocess terminated (exit code: {self.process.returncode})")
|
|
350
|
-
|
|
351
|
-
# Clean up PTY
|
|
352
|
-
if hasattr(self, 'use_pty') and self.use_pty and hasattr(self, 'master_fd'):
|
|
353
|
-
try:
|
|
354
|
-
os.close(self.master_fd)
|
|
355
|
-
except:
|
|
356
|
-
pass
|
|
357
|
-
|
|
358
|
-
# Save session log
|
|
359
|
-
self._save_session_log()
|
|
360
|
-
|
|
361
|
-
# Ticket creation removed from project
|
|
362
|
-
|
|
363
|
-
def _save_session_log(self):
|
|
364
|
-
"""Save session log to file."""
|
|
365
|
-
try:
|
|
366
|
-
log_dir = Path.home() / ".claude-mpm" / "sessions"
|
|
367
|
-
log_dir.mkdir(parents=True, exist_ok=True)
|
|
368
|
-
|
|
369
|
-
timestamp = self.session_start.strftime("%Y%m%d_%H%M%S")
|
|
370
|
-
log_file = log_dir / f"session_{timestamp}.json"
|
|
371
|
-
|
|
372
|
-
import json
|
|
373
|
-
session_data = {
|
|
374
|
-
"session_start": self.session_start.isoformat(),
|
|
375
|
-
"session_end": datetime.now().isoformat(),
|
|
376
|
-
"interactions": self.session_log,
|
|
377
|
-
# Ticket extraction removed from project
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
with open(log_file, 'w') as f:
|
|
381
|
-
json.dump(session_data, f, indent=2)
|
|
382
|
-
|
|
383
|
-
self.logger.info(f"Session log saved to: {log_file}")
|
|
384
|
-
|
|
385
|
-
except Exception as e:
|
|
386
|
-
self.logger.error(f"Failed to save session log: {e}")
|
|
387
|
-
|
|
388
|
-
# _create_tickets method removed - TicketExtractor functionality removed from project
|
|
389
|
-
|
|
390
|
-
def run_interactive(self):
|
|
391
|
-
"""Run an interactive session."""
|
|
392
|
-
try:
|
|
393
|
-
from claude_mpm._version import __version__
|
|
394
|
-
print(f"Claude MPM v{__version__} - Interactive Session")
|
|
395
|
-
except ImportError:
|
|
396
|
-
print("Claude MPM Interactive Session")
|
|
397
|
-
print("Type 'exit' or 'quit' to end session")
|
|
398
|
-
print("-" * 50)
|
|
399
|
-
|
|
400
|
-
# Use print mode for each interaction
|
|
401
|
-
conversation_file = None
|
|
402
|
-
|
|
403
|
-
while True:
|
|
404
|
-
try:
|
|
405
|
-
user_input = input("\nYou: ").strip()
|
|
406
|
-
if user_input.lower() in ['exit', 'quit']:
|
|
407
|
-
break
|
|
408
|
-
|
|
409
|
-
if user_input:
|
|
410
|
-
# Prepare message with framework on first interaction
|
|
411
|
-
if self.first_interaction:
|
|
412
|
-
framework = self.framework_loader.get_framework_instructions()
|
|
413
|
-
full_message = framework + "\n\nUser: " + user_input
|
|
414
|
-
self.first_interaction = False
|
|
415
|
-
else:
|
|
416
|
-
full_message = user_input
|
|
417
|
-
|
|
418
|
-
# Build command
|
|
419
|
-
cmd = [
|
|
420
|
-
"claude",
|
|
421
|
-
"--model", "opus",
|
|
422
|
-
"--dangerously-skip-permissions",
|
|
423
|
-
"--print", # Print mode
|
|
424
|
-
full_message
|
|
425
|
-
]
|
|
426
|
-
|
|
427
|
-
# Continue conversation if we have a file
|
|
428
|
-
if conversation_file:
|
|
429
|
-
cmd.extend(["--continue", str(conversation_file)])
|
|
430
|
-
|
|
431
|
-
# Run Claude
|
|
432
|
-
print("\nClaude: ", end='', flush=True)
|
|
433
|
-
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
434
|
-
|
|
435
|
-
if result.returncode == 0:
|
|
436
|
-
print(result.stdout)
|
|
437
|
-
|
|
438
|
-
# Extract conversation file from stderr if mentioned
|
|
439
|
-
if "conversation saved to" in result.stderr.lower():
|
|
440
|
-
# Parse conversation file path
|
|
441
|
-
import re
|
|
442
|
-
match = re.search(r'conversation saved to[:\s]+(.+)', result.stderr, re.I)
|
|
443
|
-
if match:
|
|
444
|
-
conversation_file = Path(match.group(1).strip())
|
|
445
|
-
else:
|
|
446
|
-
print(f"Error: {result.stderr}")
|
|
447
|
-
|
|
448
|
-
except KeyboardInterrupt:
|
|
449
|
-
break
|
|
450
|
-
except Exception as e:
|
|
451
|
-
print(f"\nError: {e}")
|
|
452
|
-
break
|
|
453
|
-
|
|
454
|
-
print("\nSession ended")
|
|
455
|
-
|
|
456
|
-
def run_non_interactive(self, user_input: str):
|
|
457
|
-
"""
|
|
458
|
-
Run a non-interactive session with given input.
|
|
459
|
-
|
|
460
|
-
Args:
|
|
461
|
-
user_input: The input to send to Claude
|
|
462
|
-
"""
|
|
463
|
-
# Force non-PTY mode for non-interactive
|
|
464
|
-
saved_use_pty = getattr(self, 'use_pty', False)
|
|
465
|
-
self.use_pty = False
|
|
466
|
-
|
|
467
|
-
if not self.start():
|
|
468
|
-
return
|
|
469
|
-
|
|
470
|
-
self.logger.info("Running in non-interactive mode")
|
|
471
|
-
|
|
472
|
-
try:
|
|
473
|
-
# Send input
|
|
474
|
-
self.send_input(user_input)
|
|
475
|
-
|
|
476
|
-
# Wait for process to complete or timeout
|
|
477
|
-
import time
|
|
478
|
-
timeout = 300 # 5 minute timeout
|
|
479
|
-
start_time = time.time()
|
|
480
|
-
|
|
481
|
-
# Collect output
|
|
482
|
-
while self.running and self.process and self.process.poll() is None:
|
|
483
|
-
if time.time() - start_time > timeout:
|
|
484
|
-
self.logger.warning("Session timeout reached")
|
|
485
|
-
break
|
|
486
|
-
|
|
487
|
-
# Try to get output
|
|
488
|
-
output = self.get_output(timeout=0.1)
|
|
489
|
-
if output:
|
|
490
|
-
print(output)
|
|
491
|
-
|
|
492
|
-
finally:
|
|
493
|
-
self.use_pty = saved_use_pty
|
|
494
|
-
self.stop()
|
|
495
|
-
|
|
496
|
-
def _display_output(self):
|
|
497
|
-
"""Display output from Claude."""
|
|
498
|
-
while self.running:
|
|
499
|
-
output = self.get_output()
|
|
500
|
-
if output:
|
|
501
|
-
print(output)
|