claude-mpm 4.7.4__py3-none-any.whl → 4.18.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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +118 -0
- claude_mpm/agents/BASE_ENGINEER.md +286 -0
- claude_mpm/agents/BASE_PM.md +106 -1
- claude_mpm/agents/OUTPUT_STYLE.md +329 -11
- claude_mpm/agents/PM_INSTRUCTIONS.md +397 -459
- claude_mpm/agents/agent_loader.py +17 -5
- claude_mpm/agents/frontmatter_validator.py +284 -253
- claude_mpm/agents/templates/README.md +465 -0
- claude_mpm/agents/templates/agent-manager.json +4 -1
- claude_mpm/agents/templates/agentic-coder-optimizer.json +13 -3
- claude_mpm/agents/templates/api_qa.json +11 -2
- claude_mpm/agents/templates/circuit_breakers.md +638 -0
- claude_mpm/agents/templates/clerk-ops.json +12 -2
- claude_mpm/agents/templates/code_analyzer.json +8 -2
- claude_mpm/agents/templates/content-agent.json +358 -0
- claude_mpm/agents/templates/dart_engineer.json +15 -2
- claude_mpm/agents/templates/data_engineer.json +15 -2
- claude_mpm/agents/templates/documentation.json +10 -2
- claude_mpm/agents/templates/engineer.json +21 -1
- claude_mpm/agents/templates/gcp_ops_agent.json +12 -2
- claude_mpm/agents/templates/git_file_tracking.md +584 -0
- claude_mpm/agents/templates/golang_engineer.json +270 -0
- claude_mpm/agents/templates/imagemagick.json +4 -1
- claude_mpm/agents/templates/java_engineer.json +346 -0
- claude_mpm/agents/templates/local_ops_agent.json +1227 -6
- claude_mpm/agents/templates/memory_manager.json +4 -1
- claude_mpm/agents/templates/nextjs_engineer.json +141 -133
- claude_mpm/agents/templates/ops.json +12 -2
- claude_mpm/agents/templates/php-engineer.json +270 -174
- claude_mpm/agents/templates/pm_examples.md +474 -0
- claude_mpm/agents/templates/pm_red_flags.md +240 -0
- claude_mpm/agents/templates/product_owner.json +338 -0
- claude_mpm/agents/templates/project_organizer.json +14 -4
- claude_mpm/agents/templates/prompt-engineer.json +13 -2
- claude_mpm/agents/templates/python_engineer.json +174 -81
- claude_mpm/agents/templates/qa.json +11 -2
- claude_mpm/agents/templates/react_engineer.json +16 -3
- claude_mpm/agents/templates/refactoring_engineer.json +12 -2
- claude_mpm/agents/templates/research.json +34 -21
- claude_mpm/agents/templates/response_format.md +583 -0
- claude_mpm/agents/templates/ruby-engineer.json +129 -192
- claude_mpm/agents/templates/rust_engineer.json +270 -0
- claude_mpm/agents/templates/security.json +10 -2
- claude_mpm/agents/templates/svelte-engineer.json +225 -0
- claude_mpm/agents/templates/ticketing.json +10 -2
- claude_mpm/agents/templates/typescript_engineer.json +116 -125
- claude_mpm/agents/templates/validation_templates.md +312 -0
- claude_mpm/agents/templates/vercel_ops_agent.json +12 -2
- claude_mpm/agents/templates/version_control.json +12 -2
- claude_mpm/agents/templates/web_qa.json +11 -2
- claude_mpm/agents/templates/web_ui.json +15 -2
- claude_mpm/cli/__init__.py +34 -614
- claude_mpm/cli/commands/agent_manager.py +25 -12
- claude_mpm/cli/commands/agent_state_manager.py +186 -0
- claude_mpm/cli/commands/agents.py +235 -148
- claude_mpm/cli/commands/agents_detect.py +380 -0
- claude_mpm/cli/commands/agents_recommend.py +309 -0
- claude_mpm/cli/commands/aggregate.py +7 -3
- claude_mpm/cli/commands/analyze.py +9 -4
- claude_mpm/cli/commands/analyze_code.py +7 -2
- claude_mpm/cli/commands/auto_configure.py +570 -0
- claude_mpm/cli/commands/config.py +47 -13
- claude_mpm/cli/commands/configure.py +419 -1571
- claude_mpm/cli/commands/configure_agent_display.py +261 -0
- claude_mpm/cli/commands/configure_behavior_manager.py +204 -0
- claude_mpm/cli/commands/configure_hook_manager.py +225 -0
- claude_mpm/cli/commands/configure_models.py +18 -0
- claude_mpm/cli/commands/configure_navigation.py +167 -0
- claude_mpm/cli/commands/configure_paths.py +104 -0
- claude_mpm/cli/commands/configure_persistence.py +254 -0
- claude_mpm/cli/commands/configure_startup_manager.py +646 -0
- claude_mpm/cli/commands/configure_template_editor.py +497 -0
- claude_mpm/cli/commands/configure_validators.py +73 -0
- claude_mpm/cli/commands/local_deploy.py +537 -0
- claude_mpm/cli/commands/memory.py +54 -20
- claude_mpm/cli/commands/mpm_init.py +585 -196
- claude_mpm/cli/commands/mpm_init_handler.py +37 -3
- claude_mpm/cli/commands/search.py +170 -4
- claude_mpm/cli/commands/upgrade.py +152 -0
- claude_mpm/cli/executor.py +202 -0
- claude_mpm/cli/helpers.py +105 -0
- claude_mpm/cli/interactive/__init__.py +3 -0
- claude_mpm/cli/interactive/skills_wizard.py +491 -0
- claude_mpm/cli/parsers/__init__.py +7 -1
- claude_mpm/cli/parsers/agents_parser.py +9 -0
- claude_mpm/cli/parsers/auto_configure_parser.py +245 -0
- claude_mpm/cli/parsers/base_parser.py +110 -3
- claude_mpm/cli/parsers/local_deploy_parser.py +227 -0
- claude_mpm/cli/parsers/mpm_init_parser.py +65 -5
- claude_mpm/cli/shared/output_formatters.py +28 -19
- claude_mpm/cli/startup.py +481 -0
- claude_mpm/cli/utils.py +52 -1
- claude_mpm/commands/mpm-agents-detect.md +168 -0
- claude_mpm/commands/mpm-agents-recommend.md +214 -0
- claude_mpm/commands/mpm-agents.md +75 -1
- claude_mpm/commands/mpm-auto-configure.md +217 -0
- claude_mpm/commands/mpm-help.md +163 -0
- claude_mpm/commands/mpm-init.md +148 -3
- claude_mpm/commands/mpm-version.md +113 -0
- claude_mpm/commands/mpm.md +1 -0
- claude_mpm/config/agent_config.py +2 -2
- claude_mpm/config/model_config.py +428 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/base_service.py +13 -12
- claude_mpm/core/enums.py +452 -0
- claude_mpm/core/factories.py +1 -1
- claude_mpm/core/instruction_reinforcement_hook.py +2 -1
- claude_mpm/core/interactive_session.py +9 -3
- claude_mpm/core/log_manager.py +2 -0
- claude_mpm/core/logging_config.py +6 -2
- claude_mpm/core/oneshot_session.py +8 -4
- claude_mpm/core/optimized_agent_loader.py +3 -3
- claude_mpm/core/output_style_manager.py +12 -192
- claude_mpm/core/service_registry.py +5 -1
- claude_mpm/core/types.py +2 -9
- claude_mpm/core/typing_utils.py +7 -6
- claude_mpm/dashboard/static/js/dashboard.js +0 -14
- claude_mpm/dashboard/templates/index.html +3 -41
- claude_mpm/hooks/__init__.py +20 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +4 -2
- claude_mpm/hooks/claude_hooks/response_tracking.py +35 -1
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +23 -2
- claude_mpm/hooks/failure_learning/__init__.py +60 -0
- claude_mpm/hooks/failure_learning/failure_detection_hook.py +235 -0
- claude_mpm/hooks/failure_learning/fix_detection_hook.py +217 -0
- claude_mpm/hooks/failure_learning/learning_extraction_hook.py +286 -0
- claude_mpm/hooks/instruction_reinforcement.py +7 -2
- claude_mpm/hooks/kuzu_enrichment_hook.py +263 -0
- claude_mpm/hooks/kuzu_memory_hook.py +37 -12
- claude_mpm/hooks/kuzu_response_hook.py +183 -0
- claude_mpm/models/resume_log.py +340 -0
- claude_mpm/services/agents/__init__.py +18 -5
- claude_mpm/services/agents/auto_config_manager.py +796 -0
- claude_mpm/services/agents/deployment/agent_configuration_manager.py +1 -1
- claude_mpm/services/agents/deployment/agent_record_service.py +1 -1
- claude_mpm/services/agents/deployment/agent_validator.py +17 -1
- claude_mpm/services/agents/deployment/async_agent_deployment.py +1 -1
- claude_mpm/services/agents/deployment/interface_adapter.py +3 -2
- claude_mpm/services/agents/deployment/local_template_deployment.py +1 -1
- claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +7 -6
- claude_mpm/services/agents/deployment/pipeline/steps/base_step.py +7 -16
- claude_mpm/services/agents/deployment/pipeline/steps/configuration_step.py +4 -3
- claude_mpm/services/agents/deployment/pipeline/steps/target_directory_step.py +5 -3
- claude_mpm/services/agents/deployment/pipeline/steps/validation_step.py +6 -5
- claude_mpm/services/agents/deployment/refactored_agent_deployment_service.py +9 -6
- claude_mpm/services/agents/deployment/validation/__init__.py +3 -1
- claude_mpm/services/agents/deployment/validation/validation_result.py +1 -9
- claude_mpm/services/agents/local_template_manager.py +1 -1
- claude_mpm/services/agents/memory/agent_memory_manager.py +5 -2
- claude_mpm/services/agents/observers.py +547 -0
- claude_mpm/services/agents/recommender.py +568 -0
- claude_mpm/services/agents/registry/modification_tracker.py +5 -2
- claude_mpm/services/command_handler_service.py +11 -5
- claude_mpm/services/core/__init__.py +33 -1
- claude_mpm/services/core/interfaces/__init__.py +90 -3
- claude_mpm/services/core/interfaces/agent.py +184 -0
- claude_mpm/services/core/interfaces/health.py +172 -0
- claude_mpm/services/core/interfaces/model.py +281 -0
- claude_mpm/services/core/interfaces/process.py +372 -0
- claude_mpm/services/core/interfaces/project.py +121 -0
- claude_mpm/services/core/interfaces/restart.py +307 -0
- claude_mpm/services/core/interfaces/stability.py +260 -0
- claude_mpm/services/core/memory_manager.py +11 -24
- claude_mpm/services/core/models/__init__.py +79 -0
- claude_mpm/services/core/models/agent_config.py +381 -0
- claude_mpm/services/core/models/health.py +162 -0
- claude_mpm/services/core/models/process.py +235 -0
- claude_mpm/services/core/models/restart.py +302 -0
- claude_mpm/services/core/models/stability.py +264 -0
- claude_mpm/services/core/models/toolchain.py +306 -0
- claude_mpm/services/core/path_resolver.py +23 -7
- claude_mpm/services/diagnostics/__init__.py +2 -2
- claude_mpm/services/diagnostics/checks/agent_check.py +25 -24
- claude_mpm/services/diagnostics/checks/claude_code_check.py +24 -23
- claude_mpm/services/diagnostics/checks/common_issues_check.py +25 -24
- claude_mpm/services/diagnostics/checks/configuration_check.py +24 -23
- claude_mpm/services/diagnostics/checks/filesystem_check.py +18 -17
- claude_mpm/services/diagnostics/checks/installation_check.py +30 -29
- claude_mpm/services/diagnostics/checks/instructions_check.py +20 -19
- claude_mpm/services/diagnostics/checks/mcp_check.py +50 -36
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +38 -33
- claude_mpm/services/diagnostics/checks/monitor_check.py +23 -22
- claude_mpm/services/diagnostics/checks/startup_log_check.py +9 -8
- claude_mpm/services/diagnostics/diagnostic_runner.py +6 -5
- claude_mpm/services/diagnostics/doctor_reporter.py +28 -25
- claude_mpm/services/diagnostics/models.py +19 -24
- claude_mpm/services/infrastructure/monitoring/__init__.py +1 -1
- claude_mpm/services/infrastructure/monitoring/aggregator.py +12 -12
- claude_mpm/services/infrastructure/monitoring/base.py +5 -13
- claude_mpm/services/infrastructure/monitoring/network.py +7 -6
- claude_mpm/services/infrastructure/monitoring/process.py +13 -12
- claude_mpm/services/infrastructure/monitoring/resources.py +7 -6
- claude_mpm/services/infrastructure/monitoring/service.py +16 -15
- claude_mpm/services/infrastructure/resume_log_generator.py +439 -0
- claude_mpm/services/local_ops/__init__.py +163 -0
- claude_mpm/services/local_ops/crash_detector.py +257 -0
- claude_mpm/services/local_ops/health_checks/__init__.py +28 -0
- claude_mpm/services/local_ops/health_checks/http_check.py +224 -0
- claude_mpm/services/local_ops/health_checks/process_check.py +236 -0
- claude_mpm/services/local_ops/health_checks/resource_check.py +255 -0
- claude_mpm/services/local_ops/health_manager.py +430 -0
- claude_mpm/services/local_ops/log_monitor.py +396 -0
- claude_mpm/services/local_ops/memory_leak_detector.py +294 -0
- claude_mpm/services/local_ops/process_manager.py +595 -0
- claude_mpm/services/local_ops/resource_monitor.py +331 -0
- claude_mpm/services/local_ops/restart_manager.py +401 -0
- claude_mpm/services/local_ops/restart_policy.py +387 -0
- claude_mpm/services/local_ops/state_manager.py +372 -0
- claude_mpm/services/local_ops/unified_manager.py +600 -0
- claude_mpm/services/mcp_config_manager.py +9 -4
- claude_mpm/services/mcp_gateway/core/__init__.py +1 -2
- claude_mpm/services/mcp_gateway/core/base.py +18 -31
- claude_mpm/services/mcp_gateway/main.py +30 -0
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +206 -32
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +30 -28
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +25 -5
- claude_mpm/services/mcp_service_verifier.py +1 -1
- claude_mpm/services/memory/failure_tracker.py +563 -0
- claude_mpm/services/memory_hook_service.py +165 -4
- claude_mpm/services/model/__init__.py +147 -0
- claude_mpm/services/model/base_provider.py +365 -0
- claude_mpm/services/model/claude_provider.py +412 -0
- claude_mpm/services/model/model_router.py +453 -0
- claude_mpm/services/model/ollama_provider.py +415 -0
- claude_mpm/services/monitor/daemon_manager.py +3 -2
- claude_mpm/services/monitor/handlers/dashboard.py +2 -1
- claude_mpm/services/monitor/handlers/hooks.py +2 -1
- claude_mpm/services/monitor/management/lifecycle.py +3 -2
- claude_mpm/services/monitor/server.py +2 -1
- claude_mpm/services/project/__init__.py +23 -0
- claude_mpm/services/project/detection_strategies.py +719 -0
- claude_mpm/services/project/toolchain_analyzer.py +581 -0
- claude_mpm/services/self_upgrade_service.py +342 -0
- claude_mpm/services/session_management_service.py +3 -2
- claude_mpm/services/session_manager.py +205 -1
- claude_mpm/services/shared/async_service_base.py +16 -27
- claude_mpm/services/shared/lifecycle_service_base.py +1 -14
- claude_mpm/services/socketio/handlers/__init__.py +5 -2
- claude_mpm/services/socketio/handlers/hook.py +13 -2
- claude_mpm/services/socketio/handlers/registry.py +4 -2
- claude_mpm/services/socketio/server/main.py +10 -8
- claude_mpm/services/subprocess_launcher_service.py +14 -5
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +8 -7
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +6 -5
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +8 -7
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +7 -6
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +5 -4
- claude_mpm/services/unified/config_strategies/validation_strategy.py +13 -9
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +10 -3
- claude_mpm/services/unified/deployment_strategies/local.py +6 -5
- claude_mpm/services/unified/deployment_strategies/utils.py +6 -5
- claude_mpm/services/unified/deployment_strategies/vercel.py +7 -6
- claude_mpm/services/unified/interfaces.py +3 -1
- claude_mpm/services/unified/unified_analyzer.py +14 -10
- claude_mpm/services/unified/unified_config.py +2 -1
- claude_mpm/services/unified/unified_deployment.py +9 -4
- claude_mpm/services/version_service.py +104 -1
- claude_mpm/skills/__init__.py +21 -0
- claude_mpm/skills/bundled/__init__.py +6 -0
- claude_mpm/skills/bundled/api-documentation.md +393 -0
- claude_mpm/skills/bundled/async-testing.md +571 -0
- claude_mpm/skills/bundled/code-review.md +143 -0
- claude_mpm/skills/bundled/database-migration.md +199 -0
- claude_mpm/skills/bundled/docker-containerization.md +194 -0
- claude_mpm/skills/bundled/express-local-dev.md +1429 -0
- claude_mpm/skills/bundled/fastapi-local-dev.md +1199 -0
- claude_mpm/skills/bundled/git-workflow.md +414 -0
- claude_mpm/skills/bundled/imagemagick.md +204 -0
- claude_mpm/skills/bundled/json-data-handling.md +223 -0
- claude_mpm/skills/bundled/nextjs-local-dev.md +807 -0
- claude_mpm/skills/bundled/pdf.md +141 -0
- claude_mpm/skills/bundled/performance-profiling.md +567 -0
- claude_mpm/skills/bundled/refactoring-patterns.md +180 -0
- claude_mpm/skills/bundled/security-scanning.md +327 -0
- claude_mpm/skills/bundled/systematic-debugging.md +473 -0
- claude_mpm/skills/bundled/test-driven-development.md +378 -0
- claude_mpm/skills/bundled/vite-local-dev.md +1061 -0
- claude_mpm/skills/bundled/web-performance-optimization.md +2305 -0
- claude_mpm/skills/bundled/xlsx.md +157 -0
- claude_mpm/skills/registry.py +286 -0
- claude_mpm/skills/skill_manager.py +310 -0
- claude_mpm/storage/state_storage.py +15 -15
- claude_mpm/tools/code_tree_analyzer.py +177 -141
- claude_mpm/tools/code_tree_events.py +4 -2
- claude_mpm/utils/agent_dependency_loader.py +40 -20
- claude_mpm/utils/display_helper.py +260 -0
- claude_mpm/utils/git_analyzer.py +407 -0
- claude_mpm/utils/robust_installer.py +73 -19
- {claude_mpm-4.7.4.dist-info → claude_mpm-4.18.2.dist-info}/METADATA +129 -12
- {claude_mpm-4.7.4.dist-info → claude_mpm-4.18.2.dist-info}/RECORD +295 -193
- claude_mpm/dashboard/static/css/code-tree.css +0 -1639
- claude_mpm/dashboard/static/index-hub-backup.html +0 -713
- claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +0 -353
- claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +0 -235
- claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +0 -409
- claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +0 -435
- claude_mpm/dashboard/static/js/components/code-tree.js +0 -5869
- claude_mpm/dashboard/static/js/components/code-viewer.js +0 -1386
- claude_mpm/hooks/claude_hooks/hook_handler_eventbus.py +0 -425
- claude_mpm/hooks/claude_hooks/hook_handler_original.py +0 -1041
- claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +0 -347
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +0 -575
- claude_mpm/services/project/analyzer_refactored.py +0 -450
- {claude_mpm-4.7.4.dist-info → claude_mpm-4.18.2.dist-info}/WHEEL +0 -0
- {claude_mpm-4.7.4.dist-info → claude_mpm-4.18.2.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.7.4.dist-info → claude_mpm-4.18.2.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.7.4.dist-info → claude_mpm-4.18.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Display Helper for Rich Console Output.
|
|
3
|
+
|
|
4
|
+
WHY: Centralizes display formatting logic to reduce code duplication
|
|
5
|
+
across CLI commands. Provides reusable components for tables, panels,
|
|
6
|
+
reports, and structured output.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Tuple
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DisplayHelper:
|
|
17
|
+
"""Centralized display formatting for Rich console output."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, console: Console):
|
|
20
|
+
"""Initialize display helper with console instance."""
|
|
21
|
+
self.console = console
|
|
22
|
+
|
|
23
|
+
def display_separator(self, char: str = "=", width: int = 60) -> None:
|
|
24
|
+
"""Display a separator line."""
|
|
25
|
+
self.console.print(char * width)
|
|
26
|
+
|
|
27
|
+
def display_header(self, title: str, width: int = 60) -> None:
|
|
28
|
+
"""Display a formatted header with separators."""
|
|
29
|
+
self.display_separator(width=width)
|
|
30
|
+
self.console.print(f"[bold]{title}[/bold]")
|
|
31
|
+
self.display_separator(width=width)
|
|
32
|
+
self.console.print()
|
|
33
|
+
|
|
34
|
+
def display_section_title(self, title: str, emoji: str = "") -> None:
|
|
35
|
+
"""Display a section title with optional emoji."""
|
|
36
|
+
if emoji:
|
|
37
|
+
self.console.print(f"[bold cyan]{emoji} {title}[/bold cyan]")
|
|
38
|
+
else:
|
|
39
|
+
self.console.print(f"[bold cyan]{title}[/bold cyan]")
|
|
40
|
+
|
|
41
|
+
def display_key_value_table(
|
|
42
|
+
self,
|
|
43
|
+
title: str,
|
|
44
|
+
data: Dict[str, Any],
|
|
45
|
+
key_style: str = "cyan",
|
|
46
|
+
value_style: str = "white",
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Display a two-column key-value table."""
|
|
49
|
+
table = Table(title=title, show_header=True)
|
|
50
|
+
table.add_column("Property", style=key_style)
|
|
51
|
+
table.add_column("Value", style=value_style)
|
|
52
|
+
|
|
53
|
+
for key, value in data.items():
|
|
54
|
+
# Handle various value types
|
|
55
|
+
if isinstance(value, bool):
|
|
56
|
+
display_value = "✓" if value else "✗"
|
|
57
|
+
elif isinstance(value, int) and key.lower().find("size") >= 0:
|
|
58
|
+
display_value = f"{value:,} characters"
|
|
59
|
+
else:
|
|
60
|
+
display_value = str(value)
|
|
61
|
+
table.add_row(key, display_value)
|
|
62
|
+
|
|
63
|
+
self.console.print(table)
|
|
64
|
+
|
|
65
|
+
def display_list_section(
|
|
66
|
+
self, title: str, items: List[str], max_items: int = 10, color: str = "white"
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Display a titled list of items."""
|
|
69
|
+
self.console.print(f"\n[bold cyan]{title}[/bold cyan]")
|
|
70
|
+
for item in items[:max_items]:
|
|
71
|
+
self.console.print(f" [{color}]{item}[/{color}]")
|
|
72
|
+
|
|
73
|
+
def display_warning_list(self, title: str, items: List[str]) -> None:
|
|
74
|
+
"""Display a list of warning items."""
|
|
75
|
+
self.console.print(f"\n[yellow]{title}[/yellow]")
|
|
76
|
+
for item in items:
|
|
77
|
+
self.console.print(f" • {item}")
|
|
78
|
+
|
|
79
|
+
def display_info_list(self, title: str, items: List[str]) -> None:
|
|
80
|
+
"""Display a list of info items."""
|
|
81
|
+
self.console.print(f"\n[blue]{title}[/blue]")
|
|
82
|
+
for item in items[:5]:
|
|
83
|
+
self.console.print(f" • {item}")
|
|
84
|
+
|
|
85
|
+
def display_metric_row(
|
|
86
|
+
self, label: str, value: Any, indent: int = 2, warning: bool = False
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Display a single metric row with label and value."""
|
|
89
|
+
indent_str = " " * indent
|
|
90
|
+
if warning:
|
|
91
|
+
self.console.print(f"{indent_str}[yellow]{label}: {value}[/yellow]")
|
|
92
|
+
else:
|
|
93
|
+
self.console.print(f"{indent_str}{label}: {value}")
|
|
94
|
+
|
|
95
|
+
def display_metrics_section(
|
|
96
|
+
self, title: str, metrics: Dict[str, Any], emoji: str = ""
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Display a section with multiple metrics."""
|
|
99
|
+
self.display_section_title(title, emoji)
|
|
100
|
+
for label, value in metrics.items():
|
|
101
|
+
self.display_metric_row(label, value)
|
|
102
|
+
|
|
103
|
+
def display_report_section(
|
|
104
|
+
self,
|
|
105
|
+
title: str,
|
|
106
|
+
data: Dict[str, Any],
|
|
107
|
+
emoji: str = "",
|
|
108
|
+
show_warnings: bool = True,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Display a generic report section with data and optional warnings."""
|
|
111
|
+
self.display_section_title(title, emoji)
|
|
112
|
+
|
|
113
|
+
for key, value in data.items():
|
|
114
|
+
if isinstance(value, dict):
|
|
115
|
+
# Handle nested dictionaries
|
|
116
|
+
self.console.print(f" {key}:")
|
|
117
|
+
for sub_key, sub_value in value.items():
|
|
118
|
+
self.console.print(f" {sub_key}: {sub_value}")
|
|
119
|
+
elif isinstance(value, list):
|
|
120
|
+
# Handle lists
|
|
121
|
+
self.console.print(f" {key}:")
|
|
122
|
+
for item in value[:5]: # Limit to first 5 items
|
|
123
|
+
if isinstance(item, dict):
|
|
124
|
+
# Handle dict items in list
|
|
125
|
+
desc = item.get("description") or str(item)
|
|
126
|
+
prefix = "⚠️ " if show_warnings else "•"
|
|
127
|
+
self.console.print(f" {prefix} {desc}")
|
|
128
|
+
else:
|
|
129
|
+
self.console.print(f" • {item}")
|
|
130
|
+
else:
|
|
131
|
+
# Simple key-value
|
|
132
|
+
self.console.print(f" {key}: {value}")
|
|
133
|
+
|
|
134
|
+
def display_recommendations(self, recommendations: List[str]) -> None:
|
|
135
|
+
"""Display a recommendations section."""
|
|
136
|
+
if recommendations:
|
|
137
|
+
self.display_section_title("💡 Recommendations")
|
|
138
|
+
for rec in recommendations[:5]:
|
|
139
|
+
self.console.print(f" → {rec}")
|
|
140
|
+
|
|
141
|
+
def display_documentation_status(
|
|
142
|
+
self, analysis: Dict, title: str = "Current CLAUDE.md Status"
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Display documentation status table."""
|
|
145
|
+
data = {
|
|
146
|
+
"Size": analysis.get("size", 0),
|
|
147
|
+
"Lines": analysis.get("lines", 0),
|
|
148
|
+
"Sections": len(analysis.get("sections", [])),
|
|
149
|
+
"Has Priority Index": analysis.get("has_priority_index", False),
|
|
150
|
+
"Has Priority Markers": analysis.get("has_priority_markers", False),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if analysis.get("last_modified"):
|
|
154
|
+
data["Last Modified"] = analysis["last_modified"]
|
|
155
|
+
|
|
156
|
+
self.display_key_value_table(title, data)
|
|
157
|
+
|
|
158
|
+
# Display warnings if present
|
|
159
|
+
if analysis.get("outdated_patterns"):
|
|
160
|
+
self.display_warning_list(
|
|
161
|
+
"⚠️ Outdated patterns detected:", analysis["outdated_patterns"]
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Display custom sections if present
|
|
165
|
+
if analysis.get("custom_sections"):
|
|
166
|
+
self.display_info_list(
|
|
167
|
+
"[INFO]️ Custom sections found:", analysis["custom_sections"]
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def display_activity_summary(
|
|
171
|
+
self, summary: Dict, period: str = "Last 30 days"
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Display activity summary metrics."""
|
|
174
|
+
metrics = {
|
|
175
|
+
"Total commits": summary.get("total_commits", 0),
|
|
176
|
+
"Active contributors": summary.get("total_authors", 0),
|
|
177
|
+
"Files modified": summary.get("files_changed", 0),
|
|
178
|
+
"Current branch": summary.get("current_branch", "unknown"),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
self.display_metrics_section(
|
|
182
|
+
f"📊 Activity Overview ({period.lower()})", metrics
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if summary.get("has_uncommitted"):
|
|
186
|
+
self.display_metric_row(
|
|
187
|
+
"⚠️ Uncommitted changes",
|
|
188
|
+
f"{summary.get('uncommitted_count', 0)} files",
|
|
189
|
+
warning=True,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def display_commit_list(
|
|
193
|
+
self, commits: List[Dict], title: str = "📝 Recent Commits (last 10)"
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Display a list of commits."""
|
|
196
|
+
if commits:
|
|
197
|
+
self.display_section_title(title)
|
|
198
|
+
for commit in commits[:10]:
|
|
199
|
+
msg = commit.get("message", "")[:60]
|
|
200
|
+
hash_val = commit.get("hash", "")
|
|
201
|
+
author = commit.get("author", "")
|
|
202
|
+
self.console.print(f" [{hash_val}] {msg} - {author}")
|
|
203
|
+
|
|
204
|
+
def display_file_change_list(
|
|
205
|
+
self, files: List[Tuple[str, int]], title: str = "🔥 Most Changed Files"
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Display a list of changed files with change counts."""
|
|
208
|
+
if files:
|
|
209
|
+
self.display_section_title(title)
|
|
210
|
+
for file_path, changes in files[:10]:
|
|
211
|
+
self.console.print(f" {file_path}: {changes} changes")
|
|
212
|
+
|
|
213
|
+
def display_branch_list(
|
|
214
|
+
self,
|
|
215
|
+
branches: List[str],
|
|
216
|
+
current_branch: str,
|
|
217
|
+
title: str = "🌿 Active Branches",
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Display a list of branches with current branch marked."""
|
|
220
|
+
if branches:
|
|
221
|
+
self.display_section_title(title)
|
|
222
|
+
for branch in branches:
|
|
223
|
+
marker = "→" if branch == current_branch else " "
|
|
224
|
+
self.console.print(f" {marker} {branch}")
|
|
225
|
+
|
|
226
|
+
def display_success_panel(
|
|
227
|
+
self,
|
|
228
|
+
title: str,
|
|
229
|
+
content: str,
|
|
230
|
+
border_style: str = "green",
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Display a success panel with content."""
|
|
233
|
+
self.console.print(Panel(content, title=title, border_style=border_style))
|
|
234
|
+
|
|
235
|
+
def display_info_panel(
|
|
236
|
+
self,
|
|
237
|
+
title: str,
|
|
238
|
+
content: str,
|
|
239
|
+
border_style: str = "cyan",
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Display an info panel with content."""
|
|
242
|
+
self.console.print(Panel(content, title=title, border_style=border_style))
|
|
243
|
+
|
|
244
|
+
def display_files_list(
|
|
245
|
+
self, title: str, files: List[str], prefix: str = "•"
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Display a list of files."""
|
|
248
|
+
if files:
|
|
249
|
+
self.console.print(f"[bold]{title}[/bold]")
|
|
250
|
+
for file in files:
|
|
251
|
+
self.console.print(f" {prefix} {file}")
|
|
252
|
+
self.console.print()
|
|
253
|
+
|
|
254
|
+
def display_next_steps(self, steps: List[str]) -> None:
|
|
255
|
+
"""Display next steps list."""
|
|
256
|
+
if steps:
|
|
257
|
+
self.console.print("[bold]Next Steps:[/bold]")
|
|
258
|
+
for step in steps:
|
|
259
|
+
self.console.print(f" → {step}")
|
|
260
|
+
self.console.print()
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git history analysis utilities for intelligent context reconstruction.
|
|
3
|
+
|
|
4
|
+
This module provides utilities to analyze git repository activity for
|
|
5
|
+
context reconstruction and project intelligence. Extracted from the
|
|
6
|
+
session management system to support git-based context approaches.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from claude_mpm.core.logging_utils import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def analyze_recent_activity(
|
|
19
|
+
repo_path: str = ".", days: int = 7, max_commits: int = 50, min_commits: int = 25
|
|
20
|
+
) -> Dict[str, Any]:
|
|
21
|
+
"""
|
|
22
|
+
Analyze recent git activity for context reconstruction with adaptive time window.
|
|
23
|
+
|
|
24
|
+
Strategy:
|
|
25
|
+
1. Try to get commits from last {days} days
|
|
26
|
+
2. If fewer than {min_commits} found, expand window to get {min_commits}
|
|
27
|
+
3. Never exceed {max_commits} total
|
|
28
|
+
|
|
29
|
+
This ensures meaningful context for both high-velocity and low-velocity projects.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
repo_path: Path to the git repository (default: current directory)
|
|
33
|
+
days: Number of days to look back initially (default: 7)
|
|
34
|
+
max_commits: Maximum number of commits to analyze (default: 50)
|
|
35
|
+
min_commits: Minimum commits to retrieve, will expand window if needed (default: 25)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dict containing:
|
|
39
|
+
- time_range: str - Description of analysis period
|
|
40
|
+
- commits: List[Dict] - Recent commits with metadata
|
|
41
|
+
- branches: List[str] - Active branches in the repository
|
|
42
|
+
- contributors: Dict[str, Dict] - Contributor statistics
|
|
43
|
+
- file_changes: Dict[str, Dict] - File change statistics
|
|
44
|
+
- has_activity: bool - Whether any activity was found
|
|
45
|
+
- adaptive_mode: bool - Whether time window was expanded
|
|
46
|
+
- actual_time_span: Optional[str] - Actual time span if adaptive mode was used
|
|
47
|
+
- reason: Optional[str] - Explanation for adaptive mode
|
|
48
|
+
- error: Optional[str] - Error message if analysis failed
|
|
49
|
+
"""
|
|
50
|
+
repo_path_obj = Path(repo_path)
|
|
51
|
+
analysis = {
|
|
52
|
+
"time_range": f"last {days} days",
|
|
53
|
+
"commits": [],
|
|
54
|
+
"branches": [],
|
|
55
|
+
"contributors": {},
|
|
56
|
+
"file_changes": {},
|
|
57
|
+
"has_activity": False,
|
|
58
|
+
"adaptive_mode": False,
|
|
59
|
+
"min_commits_target": min_commits,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
# Get all branches
|
|
64
|
+
result = subprocess.run(
|
|
65
|
+
["git", "branch", "-a"],
|
|
66
|
+
cwd=str(repo_path_obj),
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
check=True,
|
|
70
|
+
)
|
|
71
|
+
branches = [
|
|
72
|
+
line.strip().replace("* ", "").replace("remotes/origin/", "")
|
|
73
|
+
for line in result.stdout.strip().split("\n")
|
|
74
|
+
if line.strip()
|
|
75
|
+
]
|
|
76
|
+
analysis["branches"] = list(set(branches))
|
|
77
|
+
|
|
78
|
+
# Step 1: Get commits from specified time window
|
|
79
|
+
result = subprocess.run(
|
|
80
|
+
[
|
|
81
|
+
"git",
|
|
82
|
+
"log",
|
|
83
|
+
"--all",
|
|
84
|
+
f"--since={days} days ago",
|
|
85
|
+
f"--max-count={max_commits}",
|
|
86
|
+
"--format=%h|%an|%ae|%ai|%s",
|
|
87
|
+
"--name-status",
|
|
88
|
+
],
|
|
89
|
+
cwd=str(repo_path_obj),
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
check=True,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if not result.stdout.strip():
|
|
96
|
+
return analysis
|
|
97
|
+
|
|
98
|
+
analysis["has_activity"] = True
|
|
99
|
+
|
|
100
|
+
# Step 2: Count commit lines (lines with pipe separator) to determine if we need adaptive mode
|
|
101
|
+
temp_commits = []
|
|
102
|
+
for line in result.stdout.strip().split("\n"):
|
|
103
|
+
if "|" in line:
|
|
104
|
+
temp_commits.append(line)
|
|
105
|
+
|
|
106
|
+
# Step 3: Check if we need adaptive mode
|
|
107
|
+
if len(temp_commits) < min_commits:
|
|
108
|
+
logger.info(
|
|
109
|
+
f"Only {len(temp_commits)} commits found in last {days} days, "
|
|
110
|
+
f"expanding to get at least {min_commits} commits"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Get last N commits regardless of date
|
|
114
|
+
expanded_result = subprocess.run(
|
|
115
|
+
[
|
|
116
|
+
"git",
|
|
117
|
+
"log",
|
|
118
|
+
"--all",
|
|
119
|
+
f"-{min_commits}",
|
|
120
|
+
"--format=%h|%an|%ae|%ai|%s",
|
|
121
|
+
"--name-status",
|
|
122
|
+
],
|
|
123
|
+
cwd=str(repo_path_obj),
|
|
124
|
+
capture_output=True,
|
|
125
|
+
text=True,
|
|
126
|
+
check=True,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if expanded_result.stdout.strip():
|
|
130
|
+
result = expanded_result
|
|
131
|
+
analysis["adaptive_mode"] = True
|
|
132
|
+
|
|
133
|
+
# Calculate actual time span
|
|
134
|
+
from datetime import datetime
|
|
135
|
+
|
|
136
|
+
commit_lines = [
|
|
137
|
+
line for line in result.stdout.strip().split("\n") if "|" in line
|
|
138
|
+
]
|
|
139
|
+
if commit_lines:
|
|
140
|
+
# Parse first and last commit dates
|
|
141
|
+
try:
|
|
142
|
+
first_parts = commit_lines[0].split("|", 4)
|
|
143
|
+
last_parts = commit_lines[-1].split("|", 4)
|
|
144
|
+
|
|
145
|
+
if len(first_parts) >= 4 and len(last_parts) >= 4:
|
|
146
|
+
# Parse ISO format dates (e.g., "2025-10-20 11:38:20 -0700")
|
|
147
|
+
# Extract just the date portion before timezone
|
|
148
|
+
first_date_str = first_parts[3].strip()
|
|
149
|
+
last_date_str = last_parts[3].strip()
|
|
150
|
+
|
|
151
|
+
# Remove timezone info for parsing
|
|
152
|
+
first_date_clean = first_date_str.split(" +")[0].split(
|
|
153
|
+
" -"
|
|
154
|
+
)[0]
|
|
155
|
+
last_date_clean = last_date_str.split(" +")[0].split(" -")[
|
|
156
|
+
0
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
# Parse as datetime
|
|
160
|
+
first_date = datetime.fromisoformat(
|
|
161
|
+
first_date_clean.replace(" ", "T")
|
|
162
|
+
)
|
|
163
|
+
last_date = datetime.fromisoformat(
|
|
164
|
+
last_date_clean.replace(" ", "T")
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
days_diff = (first_date - last_date).days
|
|
168
|
+
# Handle the case where days_diff might be 0 or 1
|
|
169
|
+
if days_diff <= 1:
|
|
170
|
+
days_diff = max(days_diff, 1)
|
|
171
|
+
|
|
172
|
+
analysis["actual_time_span"] = str(days_diff)
|
|
173
|
+
|
|
174
|
+
# Provide clear messaging based on the expansion
|
|
175
|
+
if days_diff > days:
|
|
176
|
+
analysis["reason"] = (
|
|
177
|
+
f"Expanded from {days} days to {days_diff} days "
|
|
178
|
+
f"to reach minimum {min_commits} commits for meaningful context"
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
# High-velocity project: reached min_commits without expanding time window
|
|
182
|
+
analysis["reason"] = (
|
|
183
|
+
f"Fetched last {min_commits} commits (spanning {days_diff} days) "
|
|
184
|
+
f"to ensure meaningful context"
|
|
185
|
+
)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.warning(f"Could not calculate actual time span: {e}")
|
|
188
|
+
analysis["actual_time_span"] = f">{days} days"
|
|
189
|
+
analysis["reason"] = (
|
|
190
|
+
f"Expanded beyond {days} days to get minimum {min_commits} commits"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Parse commit log
|
|
194
|
+
commits = []
|
|
195
|
+
current_commit = None
|
|
196
|
+
file_changes = {}
|
|
197
|
+
|
|
198
|
+
for line in result.stdout.strip().split("\n"):
|
|
199
|
+
if not line.strip():
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
if "|" in line:
|
|
203
|
+
# Commit line
|
|
204
|
+
if current_commit:
|
|
205
|
+
commits.append(current_commit)
|
|
206
|
+
|
|
207
|
+
parts = line.split("|", 4)
|
|
208
|
+
if len(parts) == 5:
|
|
209
|
+
sha, author, email, timestamp, message = parts
|
|
210
|
+
current_commit = {
|
|
211
|
+
"sha": sha,
|
|
212
|
+
"author": author,
|
|
213
|
+
"email": email,
|
|
214
|
+
"timestamp": timestamp,
|
|
215
|
+
"message": message,
|
|
216
|
+
"files": [],
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Track contributors
|
|
220
|
+
if author not in analysis["contributors"]:
|
|
221
|
+
analysis["contributors"][author] = {
|
|
222
|
+
"email": email,
|
|
223
|
+
"commits": 0,
|
|
224
|
+
}
|
|
225
|
+
analysis["contributors"][author]["commits"] += 1
|
|
226
|
+
# File change line
|
|
227
|
+
elif current_commit and "\t" in line:
|
|
228
|
+
parts = line.split("\t", 1)
|
|
229
|
+
if len(parts) == 2:
|
|
230
|
+
status, file_path = parts
|
|
231
|
+
current_commit["files"].append(
|
|
232
|
+
{"status": status, "path": file_path}
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Track file changes
|
|
236
|
+
if file_path not in file_changes:
|
|
237
|
+
file_changes[file_path] = {
|
|
238
|
+
"modifications": 0,
|
|
239
|
+
"contributors": set(),
|
|
240
|
+
}
|
|
241
|
+
file_changes[file_path]["modifications"] += 1
|
|
242
|
+
file_changes[file_path]["contributors"].add(
|
|
243
|
+
current_commit["author"]
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Add last commit
|
|
247
|
+
if current_commit:
|
|
248
|
+
commits.append(current_commit)
|
|
249
|
+
|
|
250
|
+
analysis["commits"] = commits
|
|
251
|
+
|
|
252
|
+
# Convert file changes to serializable format
|
|
253
|
+
analysis["file_changes"] = {
|
|
254
|
+
path: {
|
|
255
|
+
"modifications": info["modifications"],
|
|
256
|
+
"contributors": list(info["contributors"]),
|
|
257
|
+
}
|
|
258
|
+
for path, info in file_changes.items()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
except subprocess.CalledProcessError as e:
|
|
262
|
+
logger.warning(f"Git command failed: {e}")
|
|
263
|
+
analysis["error"] = f"Git command failed: {e}"
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.warning(f"Could not analyze recent activity: {e}")
|
|
266
|
+
analysis["error"] = str(e)
|
|
267
|
+
|
|
268
|
+
return analysis
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_current_branch(repo_path: str = ".") -> Optional[str]:
|
|
272
|
+
"""
|
|
273
|
+
Get the current git branch name.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
repo_path: Path to the git repository (default: current directory)
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Current branch name or None if not in a git repository
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
result = subprocess.run(
|
|
283
|
+
["git", "branch", "--show-current"],
|
|
284
|
+
cwd=str(Path(repo_path)),
|
|
285
|
+
capture_output=True,
|
|
286
|
+
text=True,
|
|
287
|
+
check=True,
|
|
288
|
+
)
|
|
289
|
+
return result.stdout.strip()
|
|
290
|
+
except Exception:
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def get_commits_since(since_sha: str, repo_path: str = ".") -> List[Dict[str, str]]:
|
|
295
|
+
"""
|
|
296
|
+
Get commits since a specific SHA.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
since_sha: The SHA to get commits after
|
|
300
|
+
repo_path: Path to the git repository (default: current directory)
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
List of commit dicts with sha, author, timestamp, and message
|
|
304
|
+
"""
|
|
305
|
+
try:
|
|
306
|
+
result = subprocess.run(
|
|
307
|
+
["git", "log", f"{since_sha}..HEAD", "--format=%h|%an|%ai|%s"],
|
|
308
|
+
cwd=str(Path(repo_path)),
|
|
309
|
+
capture_output=True,
|
|
310
|
+
text=True,
|
|
311
|
+
check=True,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
commits = []
|
|
315
|
+
for line in result.stdout.strip().split("\n"):
|
|
316
|
+
if not line:
|
|
317
|
+
continue
|
|
318
|
+
parts = line.split("|", 3)
|
|
319
|
+
if len(parts) == 4:
|
|
320
|
+
sha, author, timestamp, message = parts
|
|
321
|
+
commits.append(
|
|
322
|
+
{
|
|
323
|
+
"sha": sha,
|
|
324
|
+
"author": author,
|
|
325
|
+
"timestamp": timestamp,
|
|
326
|
+
"message": message,
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return commits
|
|
331
|
+
|
|
332
|
+
except Exception as e:
|
|
333
|
+
logger.warning(f"Could not get commits: {e}")
|
|
334
|
+
return []
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def get_current_status(repo_path: str = ".") -> Dict[str, Any]:
|
|
338
|
+
"""
|
|
339
|
+
Get current git status.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
repo_path: Path to the git repository (default: current directory)
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Dict with:
|
|
346
|
+
- clean: bool - Whether working directory is clean
|
|
347
|
+
- modified_files: List[str] - Modified files
|
|
348
|
+
- untracked_files: List[str] - Untracked files
|
|
349
|
+
"""
|
|
350
|
+
status = {"clean": True, "modified_files": [], "untracked_files": []}
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
result = subprocess.run(
|
|
354
|
+
["git", "status", "--porcelain"],
|
|
355
|
+
cwd=str(Path(repo_path)),
|
|
356
|
+
capture_output=True,
|
|
357
|
+
text=True,
|
|
358
|
+
check=True,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
modified_files = []
|
|
362
|
+
untracked_files = []
|
|
363
|
+
|
|
364
|
+
for line in result.stdout.strip().split("\n"):
|
|
365
|
+
if not line:
|
|
366
|
+
continue
|
|
367
|
+
status_code = line[:2]
|
|
368
|
+
file_path = line[3:]
|
|
369
|
+
|
|
370
|
+
if status_code.startswith("??"):
|
|
371
|
+
untracked_files.append(file_path)
|
|
372
|
+
else:
|
|
373
|
+
modified_files.append(file_path)
|
|
374
|
+
|
|
375
|
+
status = {
|
|
376
|
+
"clean": len(modified_files) == 0 and len(untracked_files) == 0,
|
|
377
|
+
"modified_files": modified_files,
|
|
378
|
+
"untracked_files": untracked_files,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
except Exception as e:
|
|
382
|
+
logger.warning(f"Could not get status: {e}")
|
|
383
|
+
|
|
384
|
+
return status
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def is_git_repository(repo_path: str = ".") -> bool:
|
|
388
|
+
"""
|
|
389
|
+
Check if the given path is a git repository.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
repo_path: Path to check (default: current directory)
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
True if the path is a git repository, False otherwise
|
|
396
|
+
"""
|
|
397
|
+
try:
|
|
398
|
+
result = subprocess.run(
|
|
399
|
+
["git", "rev-parse", "--git-dir"],
|
|
400
|
+
cwd=str(Path(repo_path)),
|
|
401
|
+
capture_output=True,
|
|
402
|
+
text=True,
|
|
403
|
+
check=False,
|
|
404
|
+
)
|
|
405
|
+
return result.returncode == 0
|
|
406
|
+
except Exception:
|
|
407
|
+
return False
|