claude-mpm 3.1.0__py3-none-any.whl → 3.1.2__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.
@@ -0,0 +1,171 @@
1
+ """
2
+ Utility functions for the CLI.
3
+
4
+ WHY: This module contains shared utility functions used across different CLI commands.
5
+ Centralizing these functions reduces code duplication and provides a single place
6
+ for common CLI operations.
7
+ """
8
+
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from ..core.logger import get_logger
14
+
15
+
16
+ def get_user_input(input_arg: Optional[str], logger) -> str:
17
+ """
18
+ Get user input based on command line arguments.
19
+
20
+ WHY: This function handles the three ways users can provide input:
21
+ 1. Direct text via -i/--input
22
+ 2. File path via -i/--input
23
+ 3. stdin (for piping)
24
+
25
+ DESIGN DECISION: We check if the input is a file path first, then fall back
26
+ to treating it as direct text. This allows maximum flexibility.
27
+
28
+ Args:
29
+ input_arg: The value of the -i/--input argument
30
+ logger: Logger instance for output
31
+
32
+ Returns:
33
+ The user input as a string
34
+ """
35
+ if input_arg:
36
+ # Check if it's a file path
37
+ input_path = Path(input_arg)
38
+ if input_path.exists():
39
+ logger.info(f"Reading input from file: {input_path}")
40
+ return input_path.read_text()
41
+ else:
42
+ logger.info("Using command line input")
43
+ return input_arg
44
+ else:
45
+ # Read from stdin
46
+ logger.info("Reading input from stdin")
47
+ return sys.stdin.read()
48
+
49
+
50
+ def get_agent_versions_display() -> Optional[str]:
51
+ """
52
+ Get formatted agent versions display as a string.
53
+
54
+ WHY: This function provides a single source of truth for agent version
55
+ information that can be displayed both at startup and on-demand via the
56
+ /mpm agents command. This ensures consistency in how agent versions are
57
+ presented to users.
58
+
59
+ Returns:
60
+ Formatted string containing agent version information, or None if failed
61
+ """
62
+ try:
63
+ from ..services.agent_deployment import AgentDeploymentService
64
+ deployment_service = AgentDeploymentService()
65
+
66
+ # Get deployed agents
67
+ verification = deployment_service.verify_deployment()
68
+ if not verification.get("agents_found"):
69
+ return None
70
+
71
+ output_lines = []
72
+ output_lines.append("\nDeployed Agent Versions:")
73
+ output_lines.append("-" * 40)
74
+
75
+ # Sort agents by name for consistent display
76
+ agents = sorted(verification["agents_found"], key=lambda x: x.get('name', x.get('file', '')))
77
+
78
+ for agent in agents:
79
+ name = agent.get('name', 'unknown')
80
+ version = agent.get('version', 'unknown')
81
+ # Format: name (version)
82
+ output_lines.append(f" {name:<20} {version}")
83
+
84
+ # Add base agent version info
85
+ try:
86
+ import json
87
+ base_agent_path = deployment_service.base_agent_path
88
+ if base_agent_path.exists():
89
+ base_data = json.loads(base_agent_path.read_text())
90
+ # Parse version the same way as AgentDeploymentService
91
+ raw_version = base_data.get('base_version') or base_data.get('version', 0)
92
+ base_version_tuple = deployment_service._parse_version(raw_version)
93
+ base_version_str = deployment_service._format_version_display(base_version_tuple)
94
+ output_lines.append(f"\n Base Agent Version: {base_version_str}")
95
+ except:
96
+ pass
97
+
98
+ # Check for agents needing migration
99
+ if verification.get("agents_needing_migration"):
100
+ output_lines.append(f"\n ⚠️ {len(verification['agents_needing_migration'])} agent(s) need migration to semantic versioning")
101
+ output_lines.append(f" Run 'claude-mpm agents deploy' to update")
102
+
103
+ output_lines.append("-" * 40)
104
+ return "\n".join(output_lines)
105
+ except Exception as e:
106
+ # Log error but don't fail
107
+ logger = get_logger("cli")
108
+ logger.debug(f"Failed to get agent versions: {e}")
109
+ return None
110
+
111
+
112
+ def list_agent_versions_at_startup() -> None:
113
+ """
114
+ List deployed agent versions at startup.
115
+
116
+ WHY: Users want to see what agents are available when they start a session.
117
+ This provides immediate feedback about the deployed agent environment.
118
+ """
119
+ agent_versions = get_agent_versions_display()
120
+ if agent_versions:
121
+ print(agent_versions)
122
+ print() # Extra newline after the display
123
+
124
+
125
+ def setup_logging(args) -> object:
126
+ """
127
+ Set up logging based on parsed arguments.
128
+
129
+ WHY: This centralizes logging setup logic, handling the deprecated --debug flag
130
+ and the new --logging argument consistently across all commands.
131
+
132
+ Args:
133
+ args: Parsed command line arguments
134
+
135
+ Returns:
136
+ Logger instance
137
+ """
138
+ from ..core.logger import setup_logging as core_setup_logging, get_logger
139
+ from ..constants import LogLevel
140
+
141
+ # Handle deprecated --debug flag
142
+ if hasattr(args, 'debug') and args.debug and args.logging == LogLevel.INFO.value:
143
+ args.logging = LogLevel.DEBUG.value
144
+
145
+ # Only setup logging if not OFF
146
+ if args.logging != LogLevel.OFF.value:
147
+ logger = core_setup_logging(level=args.logging, log_dir=args.log_dir)
148
+ else:
149
+ # Minimal logger for CLI feedback
150
+ import logging
151
+ logger = logging.getLogger("cli")
152
+ logger.setLevel(logging.WARNING)
153
+
154
+ return logger
155
+
156
+
157
+ def ensure_directories() -> None:
158
+ """
159
+ Ensure required directories exist on first run.
160
+
161
+ WHY: Claude-mpm needs certain directories to function properly. Rather than
162
+ failing when they don't exist, we create them automatically for a better
163
+ user experience.
164
+ """
165
+ try:
166
+ from ..init import ensure_directories as init_ensure_directories
167
+ init_ensure_directories()
168
+ except Exception:
169
+ # Continue even if initialization fails
170
+ # The individual commands will handle missing directories as needed
171
+ pass
@@ -1,6 +1,25 @@
1
1
  """
2
2
  Enhanced CLI operations for claude-mpm.
3
3
 
4
+ WHY THIS FILE EXISTS:
5
+ This module provides an alternative CLI implementation with enhanced error handling
6
+ and validation features. It was created to explore advanced CLI patterns including:
7
+ - Comprehensive prerequisite validation
8
+ - User-friendly error messages with suggestions
9
+ - Dry-run mode for testing
10
+ - Profile validation and generation
11
+ - Rich terminal output with status indicators
12
+
13
+ CURRENT STATUS: This is an experimental/alternative CLI implementation that uses
14
+ Click instead of argparse. It's kept separate from the main CLI to:
15
+ 1. Preserve the existing CLI behavior
16
+ 2. Allow testing of new features without breaking the main interface
17
+ 3. Provide a reference implementation for future CLI enhancements
18
+
19
+ NOTE: This CLI is not currently used in production. The main CLI is in cli/__init__.py.
20
+ To use this enhanced CLI, you would need to create a separate entry point or
21
+ integrate selected features into the main CLI.
22
+
4
23
  Implements error handling and user guidance patterns from awesome-claude-code.
5
24
  """
6
25
 
@@ -194,6 +194,18 @@ class SimpleClaudeRunner:
194
194
  for var in claude_vars_to_remove:
195
195
  clean_env.pop(var, None)
196
196
 
197
+ # Set the correct working directory for Claude Code
198
+ # If CLAUDE_MPM_USER_PWD is set, use that as the working directory
199
+ if 'CLAUDE_MPM_USER_PWD' in clean_env:
200
+ user_pwd = clean_env['CLAUDE_MPM_USER_PWD']
201
+ clean_env['CLAUDE_WORKSPACE'] = user_pwd
202
+ # Also change to that directory before launching Claude
203
+ try:
204
+ os.chdir(user_pwd)
205
+ self.logger.info(f"Changed working directory to: {user_pwd}")
206
+ except Exception as e:
207
+ self.logger.warning(f"Could not change to user directory {user_pwd}: {e}")
208
+
197
209
  print("Launching Claude...")
198
210
 
199
211
  if self.project_logger:
@@ -225,7 +237,8 @@ class SimpleClaudeRunner:
225
237
  })
226
238
  # Fallback to subprocess
227
239
  try:
228
- subprocess.run(cmd, stdin=None, stdout=None, stderr=None)
240
+ # Use the same clean_env we prepared earlier
241
+ subprocess.run(cmd, stdin=None, stdout=None, stderr=None, env=clean_env)
229
242
  if self.project_logger:
230
243
  self.project_logger.log_system(
231
244
  "Interactive session completed (subprocess fallback)",
@@ -296,6 +309,24 @@ class SimpleClaudeRunner:
296
309
  cmd.insert(-2, system_prompt)
297
310
 
298
311
  try:
312
+ # Set up environment with correct working directory
313
+ env = os.environ.copy()
314
+
315
+ # Set the correct working directory for Claude Code
316
+ if 'CLAUDE_MPM_USER_PWD' in env:
317
+ user_pwd = env['CLAUDE_MPM_USER_PWD']
318
+ env['CLAUDE_WORKSPACE'] = user_pwd
319
+ # Change to that directory before running Claude
320
+ try:
321
+ original_cwd = os.getcwd()
322
+ os.chdir(user_pwd)
323
+ self.logger.info(f"Changed working directory to: {user_pwd}")
324
+ except Exception as e:
325
+ self.logger.warning(f"Could not change to user directory {user_pwd}: {e}")
326
+ original_cwd = None
327
+ else:
328
+ original_cwd = None
329
+
299
330
  # Run Claude
300
331
  if self.project_logger:
301
332
  self.project_logger.log_system(
@@ -304,7 +335,14 @@ class SimpleClaudeRunner:
304
335
  component="session"
305
336
  )
306
337
 
307
- result = subprocess.run(cmd, capture_output=True, text=True)
338
+ result = subprocess.run(cmd, capture_output=True, text=True, env=env)
339
+
340
+ # Restore original directory if we changed it
341
+ if original_cwd:
342
+ try:
343
+ os.chdir(original_cwd)
344
+ except Exception:
345
+ pass
308
346
  execution_time = time.time() - start_time
309
347
 
310
348
  if result.returncode == 0:
@@ -0,0 +1,24 @@
1
+ """
2
+ Agent models package.
3
+
4
+ WHY: This package centralizes all data models used for agent management,
5
+ providing a single source of truth for data structures across the system.
6
+ """
7
+
8
+ from .agent_definition import (
9
+ AgentDefinition,
10
+ AgentMetadata,
11
+ AgentType,
12
+ AgentSection,
13
+ AgentWorkflow,
14
+ AgentPermissions
15
+ )
16
+
17
+ __all__ = [
18
+ 'AgentDefinition',
19
+ 'AgentMetadata',
20
+ 'AgentType',
21
+ 'AgentSection',
22
+ 'AgentWorkflow',
23
+ 'AgentPermissions'
24
+ ]
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Agent Definition Models
4
+ =======================
5
+
6
+ Data models for agent definitions used by AgentManager.
7
+
8
+ WHY: These models provide a structured representation of agent data to ensure
9
+ consistency across the system. They separate the data structure from the
10
+ business logic, following the separation of concerns principle.
11
+
12
+ DESIGN DECISION: Using dataclasses for models because:
13
+ - They provide automatic __init__, __repr__, and other methods
14
+ - Type hints ensure better IDE support and runtime validation
15
+ - Easy to serialize/deserialize for persistence
16
+ - Less boilerplate than traditional classes
17
+ """
18
+
19
+ from dataclasses import dataclass, field
20
+ from datetime import datetime
21
+ from enum import Enum
22
+ from typing import Dict, List, Optional, Any
23
+
24
+
25
+ class AgentType(str, Enum):
26
+ """Agent type classification.
27
+
28
+ WHY: Enum ensures only valid agent types are used throughout the system,
29
+ preventing typos and making the code more maintainable.
30
+ """
31
+ CORE = "core"
32
+ PROJECT = "project"
33
+ CUSTOM = "custom"
34
+ SYSTEM = "system"
35
+ SPECIALIZED = "specialized"
36
+
37
+
38
+ class AgentSection(str, Enum):
39
+ """Agent markdown section identifiers.
40
+
41
+ WHY: Standardizes section names across the codebase, making it easier
42
+ to parse and update specific sections programmatically.
43
+ """
44
+ PRIMARY_ROLE = "Primary Role"
45
+ WHEN_TO_USE = "When to Use This Agent"
46
+ CAPABILITIES = "Core Capabilities"
47
+ AUTHORITY = "Authority & Permissions"
48
+ WORKFLOWS = "Agent-Specific Workflows"
49
+ ESCALATION = "Unique Escalation Triggers"
50
+ KPI = "Key Performance Indicators"
51
+ DEPENDENCIES = "Critical Dependencies"
52
+ TOOLS = "Specialized Tools/Commands"
53
+
54
+
55
+ @dataclass
56
+ class AgentPermissions:
57
+ """Agent authority and permissions.
58
+
59
+ WHY: Separating permissions into a dedicated class allows for:
60
+ - Clear permission boundaries
61
+ - Easy permission checking and validation
62
+ - Future extension without modifying the main agent definition
63
+ """
64
+ exclusive_write_access: List[str] = field(default_factory=list)
65
+ forbidden_operations: List[str] = field(default_factory=list)
66
+ read_access: List[str] = field(default_factory=list)
67
+
68
+
69
+ @dataclass
70
+ class AgentWorkflow:
71
+ """Agent workflow definition.
72
+
73
+ WHY: Workflows are complex structures that benefit from their own model:
74
+ - Ensures consistent workflow structure
75
+ - Makes workflow validation easier
76
+ - Allows workflow-specific operations
77
+ """
78
+ name: str
79
+ trigger: str
80
+ process: List[str]
81
+ output: str
82
+ raw_yaml: Optional[str] = None
83
+
84
+
85
+ @dataclass
86
+ class AgentMetadata:
87
+ """Agent metadata information.
88
+
89
+ WHY: Metadata is separated from the main definition because:
90
+ - It changes independently of agent behavior
91
+ - It's used for discovery and management, not execution
92
+ - Different services may need different metadata views
93
+ """
94
+ type: AgentType
95
+ model_preference: str = "claude-3-sonnet"
96
+ version: str = "1.0.0"
97
+ last_updated: Optional[datetime] = None
98
+ author: Optional[str] = None
99
+ tags: List[str] = field(default_factory=list)
100
+ specializations: List[str] = field(default_factory=list)
101
+
102
+ def increment_serial_version(self) -> None:
103
+ """Increment the patch version number.
104
+
105
+ WHY: Automatic version incrementing ensures every change is tracked
106
+ and follows semantic versioning principles.
107
+ """
108
+ parts = self.version.split('.')
109
+ if len(parts) == 3:
110
+ parts[2] = str(int(parts[2]) + 1)
111
+ self.version = '.'.join(parts)
112
+ else:
113
+ # If version format is unexpected, append .1
114
+ self.version = f"{self.version}.1"
115
+
116
+
117
+ @dataclass
118
+ class AgentDefinition:
119
+ """Complete agent definition.
120
+
121
+ WHY: This is the main model that represents an agent's complete configuration:
122
+ - Combines all aspects of an agent in one place
123
+ - Provides a clear contract for what constitutes an agent
124
+ - Makes serialization/deserialization straightforward
125
+
126
+ DESIGN DECISION: Using composition over inheritance:
127
+ - AgentMetadata, AgentPermissions, and AgentWorkflow are separate classes
128
+ - This allows each component to evolve independently
129
+ - Services can work with just the parts they need
130
+ """
131
+ # Core identifiers
132
+ name: str
133
+ title: str
134
+ file_path: str
135
+
136
+ # Metadata
137
+ metadata: AgentMetadata
138
+
139
+ # Agent behavior definition
140
+ primary_role: str
141
+ when_to_use: Dict[str, List[str]] # {"select": [...], "do_not_select": [...]}
142
+ capabilities: List[str]
143
+ authority: AgentPermissions
144
+ workflows: List[AgentWorkflow]
145
+ escalation_triggers: List[str]
146
+ kpis: List[str]
147
+ dependencies: List[str]
148
+ tools_commands: str
149
+
150
+ # Raw content for preservation
151
+ raw_content: str = ""
152
+ raw_sections: Dict[str, str] = field(default_factory=dict)
153
+
154
+ def to_dict(self) -> Dict[str, Any]:
155
+ """Convert to dictionary for API responses.
156
+
157
+ WHY: Many parts of the system need agent data as dictionaries:
158
+ - JSON serialization for APIs
159
+ - Configuration storage
160
+ - Integration with other services
161
+ """
162
+ return {
163
+ "name": self.name,
164
+ "title": self.title,
165
+ "file_path": self.file_path,
166
+ "metadata": {
167
+ "type": self.metadata.type.value,
168
+ "model_preference": self.metadata.model_preference,
169
+ "version": self.metadata.version,
170
+ "last_updated": self.metadata.last_updated.isoformat() if self.metadata.last_updated else None,
171
+ "author": self.metadata.author,
172
+ "tags": self.metadata.tags,
173
+ "specializations": self.metadata.specializations
174
+ },
175
+ "primary_role": self.primary_role,
176
+ "when_to_use": self.when_to_use,
177
+ "capabilities": self.capabilities,
178
+ "authority": {
179
+ "exclusive_write_access": self.authority.exclusive_write_access,
180
+ "forbidden_operations": self.authority.forbidden_operations,
181
+ "read_access": self.authority.read_access
182
+ },
183
+ "workflows": [
184
+ {
185
+ "name": w.name,
186
+ "trigger": w.trigger,
187
+ "process": w.process,
188
+ "output": w.output
189
+ }
190
+ for w in self.workflows
191
+ ],
192
+ "escalation_triggers": self.escalation_triggers,
193
+ "kpis": self.kpis,
194
+ "dependencies": self.dependencies,
195
+ "tools_commands": self.tools_commands
196
+ }