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.
Files changed (123) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/INSTRUCTIONS.md +182 -299
  3. claude_mpm/agents/agent_loader.py +283 -57
  4. claude_mpm/agents/agent_loader_integration.py +6 -9
  5. claude_mpm/agents/base_agent.json +2 -1
  6. claude_mpm/agents/base_agent_loader.py +1 -1
  7. claude_mpm/cli/__init__.py +5 -7
  8. claude_mpm/cli/commands/__init__.py +0 -2
  9. claude_mpm/cli/commands/agents.py +1 -1
  10. claude_mpm/cli/commands/memory.py +1 -1
  11. claude_mpm/cli/commands/run.py +12 -0
  12. claude_mpm/cli/parser.py +0 -13
  13. claude_mpm/cli/utils.py +1 -1
  14. claude_mpm/config/__init__.py +44 -2
  15. claude_mpm/config/agent_config.py +348 -0
  16. claude_mpm/config/paths.py +322 -0
  17. claude_mpm/constants.py +0 -1
  18. claude_mpm/core/__init__.py +2 -5
  19. claude_mpm/core/agent_registry.py +63 -17
  20. claude_mpm/core/claude_runner.py +354 -43
  21. claude_mpm/core/config.py +7 -1
  22. claude_mpm/core/config_aliases.py +4 -3
  23. claude_mpm/core/config_paths.py +151 -0
  24. claude_mpm/core/factories.py +4 -50
  25. claude_mpm/core/logger.py +11 -13
  26. claude_mpm/core/service_registry.py +2 -2
  27. claude_mpm/dashboard/static/js/components/agent-inference.js +101 -25
  28. claude_mpm/dashboard/static/js/components/event-processor.js +3 -2
  29. claude_mpm/hooks/claude_hooks/hook_handler.py +343 -83
  30. claude_mpm/hooks/memory_integration_hook.py +1 -1
  31. claude_mpm/init.py +37 -6
  32. claude_mpm/scripts/socketio_daemon.py +6 -2
  33. claude_mpm/services/__init__.py +71 -3
  34. claude_mpm/services/agents/__init__.py +85 -0
  35. claude_mpm/services/agents/deployment/__init__.py +21 -0
  36. claude_mpm/services/{agent_deployment.py → agents/deployment/agent_deployment.py} +192 -41
  37. claude_mpm/services/{agent_lifecycle_manager.py → agents/deployment/agent_lifecycle_manager.py} +11 -10
  38. claude_mpm/services/agents/loading/__init__.py +11 -0
  39. claude_mpm/services/{agent_profile_loader.py → agents/loading/agent_profile_loader.py} +9 -8
  40. claude_mpm/services/{base_agent_manager.py → agents/loading/base_agent_manager.py} +2 -2
  41. claude_mpm/services/{framework_agent_loader.py → agents/loading/framework_agent_loader.py} +116 -40
  42. claude_mpm/services/agents/management/__init__.py +9 -0
  43. claude_mpm/services/{agent_management_service.py → agents/management/agent_management_service.py} +6 -5
  44. claude_mpm/services/agents/memory/__init__.py +21 -0
  45. claude_mpm/services/{agent_memory_manager.py → agents/memory/agent_memory_manager.py} +3 -3
  46. claude_mpm/services/agents/registry/__init__.py +29 -0
  47. claude_mpm/services/{agent_registry.py → agents/registry/agent_registry.py} +101 -16
  48. claude_mpm/services/{deployed_agent_discovery.py → agents/registry/deployed_agent_discovery.py} +12 -2
  49. claude_mpm/services/{agent_modification_tracker.py → agents/registry/modification_tracker.py} +6 -5
  50. claude_mpm/services/async_session_logger.py +584 -0
  51. claude_mpm/services/claude_session_logger.py +299 -0
  52. claude_mpm/services/framework_claude_md_generator/content_assembler.py +2 -2
  53. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +17 -17
  54. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +3 -3
  55. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +1 -1
  56. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +1 -1
  57. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +19 -24
  58. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +1 -1
  59. claude_mpm/services/framework_claude_md_generator.py +4 -2
  60. claude_mpm/services/memory/__init__.py +17 -0
  61. claude_mpm/services/{memory_builder.py → memory/builder.py} +3 -3
  62. claude_mpm/services/memory/cache/__init__.py +14 -0
  63. claude_mpm/services/{shared_prompt_cache.py → memory/cache/shared_prompt_cache.py} +1 -1
  64. claude_mpm/services/memory/cache/simple_cache.py +317 -0
  65. claude_mpm/services/{memory_optimizer.py → memory/optimizer.py} +1 -1
  66. claude_mpm/services/{memory_router.py → memory/router.py} +1 -1
  67. claude_mpm/services/optimized_hook_service.py +542 -0
  68. claude_mpm/services/project_registry.py +14 -8
  69. claude_mpm/services/response_tracker.py +237 -0
  70. claude_mpm/services/ticketing_service_original.py +4 -2
  71. claude_mpm/services/version_control/branch_strategy.py +3 -1
  72. claude_mpm/utils/paths.py +12 -10
  73. claude_mpm/utils/session_logging.py +114 -0
  74. claude_mpm/validation/agent_validator.py +2 -1
  75. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/METADATA +26 -20
  76. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/RECORD +83 -106
  77. claude_mpm/cli/commands/ui.py +0 -57
  78. claude_mpm/core/simple_runner.py +0 -1046
  79. claude_mpm/hooks/builtin/__init__.py +0 -1
  80. claude_mpm/hooks/builtin/logging_hook_example.py +0 -165
  81. claude_mpm/hooks/builtin/memory_hooks_example.py +0 -67
  82. claude_mpm/hooks/builtin/mpm_command_hook.py +0 -125
  83. claude_mpm/hooks/builtin/post_delegation_hook_example.py +0 -124
  84. claude_mpm/hooks/builtin/pre_delegation_hook_example.py +0 -125
  85. claude_mpm/hooks/builtin/submit_hook_example.py +0 -100
  86. claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +0 -237
  87. claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +0 -240
  88. claude_mpm/hooks/builtin/workflow_start_hook.py +0 -181
  89. claude_mpm/orchestration/__init__.py +0 -6
  90. claude_mpm/orchestration/archive/direct_orchestrator.py +0 -195
  91. claude_mpm/orchestration/archive/factory.py +0 -215
  92. claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +0 -188
  93. claude_mpm/orchestration/archive/hook_integration_example.py +0 -178
  94. claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +0 -826
  95. claude_mpm/orchestration/archive/orchestrator.py +0 -501
  96. claude_mpm/orchestration/archive/pexpect_orchestrator.py +0 -252
  97. claude_mpm/orchestration/archive/pty_orchestrator.py +0 -270
  98. claude_mpm/orchestration/archive/simple_orchestrator.py +0 -82
  99. claude_mpm/orchestration/archive/subprocess_orchestrator.py +0 -801
  100. claude_mpm/orchestration/archive/system_prompt_orchestrator.py +0 -278
  101. claude_mpm/orchestration/archive/wrapper_orchestrator.py +0 -187
  102. claude_mpm/schemas/workflow_validator.py +0 -411
  103. claude_mpm/services/parent_directory_manager/__init__.py +0 -577
  104. claude_mpm/services/parent_directory_manager/backup_manager.py +0 -258
  105. claude_mpm/services/parent_directory_manager/config_manager.py +0 -210
  106. claude_mpm/services/parent_directory_manager/deduplication_manager.py +0 -279
  107. claude_mpm/services/parent_directory_manager/framework_protector.py +0 -143
  108. claude_mpm/services/parent_directory_manager/operations.py +0 -186
  109. claude_mpm/services/parent_directory_manager/state_manager.py +0 -624
  110. claude_mpm/services/parent_directory_manager/template_deployer.py +0 -579
  111. claude_mpm/services/parent_directory_manager/validation_manager.py +0 -378
  112. claude_mpm/services/parent_directory_manager/version_control_helper.py +0 -339
  113. claude_mpm/services/parent_directory_manager/version_manager.py +0 -222
  114. claude_mpm/ui/__init__.py +0 -1
  115. claude_mpm/ui/rich_terminal_ui.py +0 -295
  116. claude_mpm/ui/terminal_ui.py +0 -328
  117. /claude_mpm/services/{agent_versioning.py → agents/deployment/agent_versioning.py} +0 -0
  118. /claude_mpm/services/{agent_capabilities_generator.py → agents/management/agent_capabilities_generator.py} +0 -0
  119. /claude_mpm/services/{agent_persistence_service.py → agents/memory/agent_persistence_service.py} +0 -0
  120. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/WHEEL +0 -0
  121. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/entry_points.txt +0 -0
  122. {claude_mpm-3.4.27.dist-info → claude_mpm-3.5.0.dist-info}/licenses/LICENSE +0 -0
  123. {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)