claude-mpm 4.9.0__py3-none-any.whl → 4.11.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/templates/research.json +30 -23
- claude_mpm/cli/commands/mpm_init.py +250 -1
- claude_mpm/cli/commands/mpm_init_handler.py +41 -1
- claude_mpm/cli/commands/search.py +170 -4
- claude_mpm/cli/commands/session_pause_manager.py +389 -0
- claude_mpm/cli/commands/session_resume_manager.py +523 -0
- claude_mpm/cli/parsers/mpm_init_parser.py +59 -0
- claude_mpm/commands/mpm-init.md +78 -1
- claude_mpm/services/mcp_gateway/main.py +21 -0
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +141 -14
- claude_mpm/storage/state_storage.py +15 -15
- {claude_mpm-4.9.0.dist-info → claude_mpm-4.11.0.dist-info}/METADATA +10 -3
- {claude_mpm-4.9.0.dist-info → claude_mpm-4.11.0.dist-info}/RECORD +18 -16
- {claude_mpm-4.9.0.dist-info → claude_mpm-4.11.0.dist-info}/WHEEL +0 -0
- {claude_mpm-4.9.0.dist-info → claude_mpm-4.11.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.9.0.dist-info → claude_mpm-4.11.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.9.0.dist-info → claude_mpm-4.11.0.dist-info}/top_level.txt +0 -0
@@ -16,8 +16,7 @@ from rich.panel import Panel
|
|
16
16
|
from rich.syntax import Syntax
|
17
17
|
from rich.table import Table
|
18
18
|
|
19
|
-
from claude_mpm.
|
20
|
-
from claude_mpm.services.service_container import get_service_container
|
19
|
+
from claude_mpm.services.core.service_container import get_global_container
|
21
20
|
|
22
21
|
console = Console()
|
23
22
|
|
@@ -27,8 +26,9 @@ class MCPSearchInterface:
|
|
27
26
|
|
28
27
|
def __init__(self):
|
29
28
|
"""Initialize the search interface."""
|
30
|
-
self.container =
|
29
|
+
self.container = get_global_container()
|
31
30
|
self.mcp_gateway = None
|
31
|
+
self.vector_search_available = False
|
32
32
|
|
33
33
|
async def initialize(self):
|
34
34
|
"""Initialize the MCP gateway connection."""
|
@@ -39,10 +39,127 @@ class MCPSearchInterface:
|
|
39
39
|
if not self.mcp_gateway:
|
40
40
|
self.mcp_gateway = MCPGatewayService()
|
41
41
|
await self.mcp_gateway.initialize()
|
42
|
+
|
43
|
+
# Check if vector search is available
|
44
|
+
self.vector_search_available = await self._check_vector_search_available()
|
45
|
+
|
42
46
|
except Exception as e:
|
43
47
|
console.print(f"[red]Failed to initialize MCP gateway: {e}[/red]")
|
44
48
|
raise
|
45
49
|
|
50
|
+
async def _check_vector_search_available(self) -> bool:
|
51
|
+
"""Check if mcp-vector-search is available and offer installation if not."""
|
52
|
+
import importlib.util
|
53
|
+
|
54
|
+
# Check if package is installed
|
55
|
+
spec = importlib.util.find_spec("mcp_vector_search")
|
56
|
+
if spec is not None:
|
57
|
+
return True
|
58
|
+
|
59
|
+
# Package not found - offer installation
|
60
|
+
console.print("\n[yellow]⚠️ mcp-vector-search not found[/yellow]")
|
61
|
+
console.print("This package enables semantic code search (optional feature).")
|
62
|
+
console.print("\nInstallation options:")
|
63
|
+
console.print(" 1. Install via pip (recommended for this project)")
|
64
|
+
console.print(" 2. Install via pipx (isolated, system-wide)")
|
65
|
+
console.print(" 3. Skip (use traditional grep/glob instead)")
|
66
|
+
|
67
|
+
try:
|
68
|
+
choice = input("\nChoose option (1/2/3) [3]: ").strip() or "3"
|
69
|
+
|
70
|
+
if choice == "1":
|
71
|
+
return await self._install_via_pip()
|
72
|
+
if choice == "2":
|
73
|
+
return await self._install_via_pipx()
|
74
|
+
console.print(
|
75
|
+
"[dim]Continuing with fallback search methods (grep/glob)[/dim]"
|
76
|
+
)
|
77
|
+
return False
|
78
|
+
|
79
|
+
except (EOFError, KeyboardInterrupt):
|
80
|
+
console.print("\n[dim]Installation cancelled, using fallback methods[/dim]")
|
81
|
+
return False
|
82
|
+
|
83
|
+
async def _install_via_pip(self) -> bool:
|
84
|
+
"""Install mcp-vector-search via pip."""
|
85
|
+
import subprocess
|
86
|
+
|
87
|
+
try:
|
88
|
+
console.print("\n[cyan]📦 Installing mcp-vector-search via pip...[/cyan]")
|
89
|
+
result = subprocess.run(
|
90
|
+
[sys.executable, "-m", "pip", "install", "mcp-vector-search"],
|
91
|
+
capture_output=True,
|
92
|
+
text=True,
|
93
|
+
timeout=120,
|
94
|
+
check=False,
|
95
|
+
)
|
96
|
+
|
97
|
+
if result.returncode == 0:
|
98
|
+
console.print(
|
99
|
+
"[green]✓ Successfully installed mcp-vector-search[/green]"
|
100
|
+
)
|
101
|
+
return True
|
102
|
+
|
103
|
+
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
|
104
|
+
console.print(f"[red]✗ Installation failed: {error_msg}[/red]")
|
105
|
+
return False
|
106
|
+
|
107
|
+
except subprocess.TimeoutExpired:
|
108
|
+
console.print("[red]✗ Installation timed out[/red]")
|
109
|
+
return False
|
110
|
+
except Exception as e:
|
111
|
+
console.print(f"[red]✗ Installation error: {e}[/red]")
|
112
|
+
return False
|
113
|
+
|
114
|
+
async def _install_via_pipx(self) -> bool:
|
115
|
+
"""Install mcp-vector-search via pipx."""
|
116
|
+
import subprocess
|
117
|
+
|
118
|
+
try:
|
119
|
+
# Check if pipx is available
|
120
|
+
pipx_check = subprocess.run(
|
121
|
+
["pipx", "--version"],
|
122
|
+
capture_output=True,
|
123
|
+
text=True,
|
124
|
+
timeout=5,
|
125
|
+
check=False,
|
126
|
+
)
|
127
|
+
|
128
|
+
if pipx_check.returncode != 0:
|
129
|
+
console.print("[red]✗ pipx is not installed[/red]")
|
130
|
+
console.print("Install pipx first: python -m pip install pipx")
|
131
|
+
return False
|
132
|
+
|
133
|
+
console.print("\n[cyan]📦 Installing mcp-vector-search via pipx...[/cyan]")
|
134
|
+
result = subprocess.run(
|
135
|
+
["pipx", "install", "mcp-vector-search"],
|
136
|
+
capture_output=True,
|
137
|
+
text=True,
|
138
|
+
timeout=120,
|
139
|
+
check=False,
|
140
|
+
)
|
141
|
+
|
142
|
+
if result.returncode == 0:
|
143
|
+
console.print(
|
144
|
+
"[green]✓ Successfully installed mcp-vector-search[/green]"
|
145
|
+
)
|
146
|
+
return True
|
147
|
+
|
148
|
+
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
|
149
|
+
console.print(f"[red]✗ Installation failed: {error_msg}[/red]")
|
150
|
+
return False
|
151
|
+
|
152
|
+
except FileNotFoundError:
|
153
|
+
console.print("[red]✗ pipx command not found[/red]")
|
154
|
+
console.print("Install pipx first: python -m pip install pipx")
|
155
|
+
return False
|
156
|
+
except subprocess.TimeoutExpired:
|
157
|
+
console.print("[red]✗ Installation timed out[/red]")
|
158
|
+
return False
|
159
|
+
except Exception as e:
|
160
|
+
console.print(f"[red]✗ Installation error: {e}[/red]")
|
161
|
+
return False
|
162
|
+
|
46
163
|
async def search_code(
|
47
164
|
self,
|
48
165
|
query: str,
|
@@ -125,6 +242,12 @@ class MCPSearchInterface:
|
|
125
242
|
if not self.mcp_gateway:
|
126
243
|
await self.initialize()
|
127
244
|
|
245
|
+
# Check if vector search is available
|
246
|
+
if not self.vector_search_available:
|
247
|
+
return {
|
248
|
+
"error": "mcp-vector-search is not available. Use traditional grep/glob tools instead, or run command again to install."
|
249
|
+
}
|
250
|
+
|
128
251
|
try:
|
129
252
|
return await self.mcp_gateway.call_tool(tool_name, params)
|
130
253
|
except Exception as e:
|
@@ -196,7 +319,6 @@ def display_search_results(results: Dict[str, Any], output_format: str = "rich")
|
|
196
319
|
@click.option("--focus", multiple=True, help="Focus areas (with --context)")
|
197
320
|
@click.option("--force", is_flag=True, help="Force reindexing (with --index)")
|
198
321
|
@click.option("--json", "output_json", is_flag=True, help="Output results as JSON")
|
199
|
-
@handle_async_errors
|
200
322
|
async def search_command(
|
201
323
|
query: Optional[str],
|
202
324
|
similar: Optional[str],
|
@@ -228,8 +350,24 @@ async def search_command(
|
|
228
350
|
output_format = "json" if output_json else "rich"
|
229
351
|
|
230
352
|
try:
|
353
|
+
# Show first-time usage tips if vector search is available
|
354
|
+
if search.vector_search_available and not (index or status):
|
355
|
+
console.print(
|
356
|
+
"\n[dim]💡 Tip: Vector search provides semantic code understanding.[/dim]"
|
357
|
+
)
|
358
|
+
console.print(
|
359
|
+
"[dim] Run with --index first to index your project.[/dim]\n"
|
360
|
+
)
|
361
|
+
|
231
362
|
# Handle different operation modes
|
232
363
|
if index:
|
364
|
+
if not search.vector_search_available:
|
365
|
+
console.print("[red]✗ mcp-vector-search is required for indexing[/red]")
|
366
|
+
console.print(
|
367
|
+
"[dim]Install it or use traditional grep/glob for search[/dim]"
|
368
|
+
)
|
369
|
+
sys.exit(1)
|
370
|
+
|
233
371
|
console.print("[cyan]Indexing project...[/cyan]")
|
234
372
|
result = await search.index_project(
|
235
373
|
force=force, file_extensions=list(extensions) if extensions else None
|
@@ -239,10 +377,23 @@ async def search_command(
|
|
239
377
|
display_search_results(result, output_format)
|
240
378
|
|
241
379
|
elif status:
|
380
|
+
if not search.vector_search_available:
|
381
|
+
console.print(
|
382
|
+
"[red]✗ mcp-vector-search is required for status check[/red]"
|
383
|
+
)
|
384
|
+
console.print("[dim]Install it to use vector search features[/dim]")
|
385
|
+
sys.exit(1)
|
386
|
+
|
242
387
|
result = await search.get_status()
|
243
388
|
display_search_results(result, output_format)
|
244
389
|
|
245
390
|
elif similar:
|
391
|
+
if not search.vector_search_available:
|
392
|
+
console.print("[yellow]⚠️ Vector search not available[/yellow]")
|
393
|
+
console.print("[dim]Similarity search requires mcp-vector-search[/dim]")
|
394
|
+
console.print("[dim]Falling back to basic file search...[/dim]")
|
395
|
+
sys.exit(1)
|
396
|
+
|
246
397
|
result = await search.search_similar(
|
247
398
|
file_path=similar,
|
248
399
|
function_name=function,
|
@@ -252,6 +403,12 @@ async def search_command(
|
|
252
403
|
display_search_results(result, output_format)
|
253
404
|
|
254
405
|
elif context:
|
406
|
+
if not search.vector_search_available:
|
407
|
+
console.print("[yellow]⚠️ Vector search not available[/yellow]")
|
408
|
+
console.print("[dim]Context search requires mcp-vector-search[/dim]")
|
409
|
+
console.print("[dim]Try using grep for text-based search instead[/dim]")
|
410
|
+
sys.exit(1)
|
411
|
+
|
255
412
|
result = await search.search_context(
|
256
413
|
description=context,
|
257
414
|
focus_areas=list(focus) if focus else None,
|
@@ -260,6 +417,15 @@ async def search_command(
|
|
260
417
|
display_search_results(result, output_format)
|
261
418
|
|
262
419
|
elif query:
|
420
|
+
if not search.vector_search_available:
|
421
|
+
console.print("[yellow]⚠️ Vector search not available[/yellow]")
|
422
|
+
console.print("[dim]Code search requires mcp-vector-search[/dim]")
|
423
|
+
console.print(
|
424
|
+
"\n[cyan]Alternative: Use grep for pattern matching:[/cyan]"
|
425
|
+
)
|
426
|
+
console.print(f" grep -r '{query}' .")
|
427
|
+
sys.exit(1)
|
428
|
+
|
263
429
|
result = await search.search_code(
|
264
430
|
query=query,
|
265
431
|
limit=limit,
|
@@ -0,0 +1,389 @@
|
|
1
|
+
"""
|
2
|
+
Session Pause Manager - Captures and saves session state for later resumption.
|
3
|
+
|
4
|
+
This module provides functionality to pause a Claude MPM session by capturing:
|
5
|
+
- Conversation context and progress
|
6
|
+
- Git repository state
|
7
|
+
- Todo list status
|
8
|
+
- Working directory changes
|
9
|
+
|
10
|
+
The saved state enables seamless session resumption with full context.
|
11
|
+
"""
|
12
|
+
|
13
|
+
import subprocess
|
14
|
+
from datetime import datetime, timezone
|
15
|
+
from pathlib import Path
|
16
|
+
from typing import Any, Dict, List, Optional
|
17
|
+
|
18
|
+
from rich.console import Console
|
19
|
+
from rich.panel import Panel
|
20
|
+
|
21
|
+
from claude_mpm.core.logging_utils import get_logger
|
22
|
+
from claude_mpm.storage.state_storage import StateStorage
|
23
|
+
|
24
|
+
logger = get_logger(__name__)
|
25
|
+
console = Console()
|
26
|
+
|
27
|
+
|
28
|
+
class SessionPauseManager:
|
29
|
+
"""Manages pausing and saving session state."""
|
30
|
+
|
31
|
+
def __init__(self, project_path: Path):
|
32
|
+
"""Initialize Session Pause Manager.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
project_path: Path to the project directory
|
36
|
+
"""
|
37
|
+
self.project_path = project_path
|
38
|
+
self.session_dir = project_path / ".claude-mpm" / "sessions" / "pause"
|
39
|
+
self.storage = StateStorage()
|
40
|
+
|
41
|
+
def pause_session(
|
42
|
+
self,
|
43
|
+
conversation_summary: Optional[str] = None,
|
44
|
+
accomplishments: Optional[List[str]] = None,
|
45
|
+
next_steps: Optional[List[str]] = None,
|
46
|
+
todos_active: Optional[List[Dict[str, str]]] = None,
|
47
|
+
todos_completed: Optional[List[Dict[str, str]]] = None,
|
48
|
+
) -> Dict[str, Any]:
|
49
|
+
"""Pause the current session and save state.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
conversation_summary: Summary of what was being worked on
|
53
|
+
accomplishments: List of things accomplished in this session
|
54
|
+
next_steps: List of next steps to continue work
|
55
|
+
todos_active: Active todo items
|
56
|
+
todos_completed: Completed todo items
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
Dict containing pause result with session_file path
|
60
|
+
"""
|
61
|
+
try:
|
62
|
+
# Ensure session directory exists
|
63
|
+
self.session_dir.mkdir(parents=True, exist_ok=True)
|
64
|
+
|
65
|
+
# Generate session ID
|
66
|
+
timestamp = datetime.now(timezone.utc)
|
67
|
+
session_id = f"session-{timestamp.strftime('%Y%m%d-%H%M%S')}"
|
68
|
+
|
69
|
+
# Capture git context
|
70
|
+
git_context = self._capture_git_context()
|
71
|
+
|
72
|
+
# Build session state
|
73
|
+
session_state = {
|
74
|
+
"session_id": session_id,
|
75
|
+
"paused_at": timestamp.isoformat(),
|
76
|
+
"conversation": {
|
77
|
+
"summary": conversation_summary
|
78
|
+
or "Session paused - context not provided",
|
79
|
+
"accomplishments": accomplishments or [],
|
80
|
+
"next_steps": next_steps or [],
|
81
|
+
},
|
82
|
+
"git_context": git_context,
|
83
|
+
"todos": {
|
84
|
+
"active": todos_active or [],
|
85
|
+
"completed": todos_completed or [],
|
86
|
+
},
|
87
|
+
"version": self._get_version(),
|
88
|
+
"build": self._get_build_number(),
|
89
|
+
"project_path": str(self.project_path),
|
90
|
+
}
|
91
|
+
|
92
|
+
# Save session state to file
|
93
|
+
session_file = self.session_dir / f"{session_id}.json"
|
94
|
+
success = self.storage.write_json(
|
95
|
+
session_state, session_file, atomic=True, compress=False
|
96
|
+
)
|
97
|
+
|
98
|
+
if not success:
|
99
|
+
return {
|
100
|
+
"status": "error",
|
101
|
+
"message": "Failed to write session state file",
|
102
|
+
}
|
103
|
+
|
104
|
+
# Create git commit with session state
|
105
|
+
commit_success = self._create_pause_commit(session_id, conversation_summary)
|
106
|
+
|
107
|
+
# Display success message
|
108
|
+
self._display_pause_success(session_id, session_file, commit_success)
|
109
|
+
|
110
|
+
return {
|
111
|
+
"status": "success",
|
112
|
+
"session_id": session_id,
|
113
|
+
"session_file": str(session_file),
|
114
|
+
"git_commit_created": commit_success,
|
115
|
+
"message": f"Session paused: {session_id}",
|
116
|
+
}
|
117
|
+
|
118
|
+
except Exception as e:
|
119
|
+
logger.error(f"Failed to pause session: {e}")
|
120
|
+
return {"status": "error", "message": str(e)}
|
121
|
+
|
122
|
+
def _capture_git_context(self) -> Dict[str, Any]:
|
123
|
+
"""Capture current git repository state.
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
Dict containing git context information
|
127
|
+
"""
|
128
|
+
git_context: Dict[str, Any] = {
|
129
|
+
"is_git_repo": False,
|
130
|
+
"branch": None,
|
131
|
+
"recent_commits": [],
|
132
|
+
"status": {"clean": True, "modified_files": [], "untracked_files": []},
|
133
|
+
}
|
134
|
+
|
135
|
+
try:
|
136
|
+
# Check if git repo
|
137
|
+
result = subprocess.run(
|
138
|
+
["git", "rev-parse", "--git-dir"],
|
139
|
+
cwd=str(self.project_path),
|
140
|
+
capture_output=True,
|
141
|
+
text=True,
|
142
|
+
check=False,
|
143
|
+
)
|
144
|
+
|
145
|
+
if result.returncode != 0:
|
146
|
+
return git_context
|
147
|
+
|
148
|
+
git_context["is_git_repo"] = True
|
149
|
+
|
150
|
+
# Get current branch
|
151
|
+
result = subprocess.run(
|
152
|
+
["git", "branch", "--show-current"],
|
153
|
+
cwd=str(self.project_path),
|
154
|
+
capture_output=True,
|
155
|
+
text=True,
|
156
|
+
check=True,
|
157
|
+
)
|
158
|
+
git_context["branch"] = result.stdout.strip()
|
159
|
+
|
160
|
+
# Get recent commits (last 10)
|
161
|
+
result = subprocess.run(
|
162
|
+
["git", "log", "--format=%h|%an|%ai|%s", "-10"],
|
163
|
+
cwd=str(self.project_path),
|
164
|
+
capture_output=True,
|
165
|
+
text=True,
|
166
|
+
check=True,
|
167
|
+
)
|
168
|
+
|
169
|
+
commits = []
|
170
|
+
for line in result.stdout.strip().split("\n"):
|
171
|
+
if not line:
|
172
|
+
continue
|
173
|
+
parts = line.split("|", 3)
|
174
|
+
if len(parts) == 4:
|
175
|
+
sha, author, timestamp_str, message = parts
|
176
|
+
commits.append(
|
177
|
+
{
|
178
|
+
"sha": sha,
|
179
|
+
"author": author,
|
180
|
+
"timestamp": timestamp_str,
|
181
|
+
"message": message,
|
182
|
+
}
|
183
|
+
)
|
184
|
+
git_context["recent_commits"] = commits
|
185
|
+
|
186
|
+
# Get status
|
187
|
+
result = subprocess.run(
|
188
|
+
["git", "status", "--porcelain"],
|
189
|
+
cwd=str(self.project_path),
|
190
|
+
capture_output=True,
|
191
|
+
text=True,
|
192
|
+
check=True,
|
193
|
+
)
|
194
|
+
|
195
|
+
modified_files = []
|
196
|
+
untracked_files = []
|
197
|
+
|
198
|
+
for line in result.stdout.strip().split("\n"):
|
199
|
+
if not line:
|
200
|
+
continue
|
201
|
+
status_code = line[:2]
|
202
|
+
file_path = line[3:]
|
203
|
+
|
204
|
+
if status_code.startswith("??"):
|
205
|
+
untracked_files.append(file_path)
|
206
|
+
else:
|
207
|
+
modified_files.append(file_path)
|
208
|
+
|
209
|
+
git_context["status"] = {
|
210
|
+
"clean": len(modified_files) == 0 and len(untracked_files) == 0,
|
211
|
+
"modified_files": modified_files,
|
212
|
+
"untracked_files": untracked_files,
|
213
|
+
}
|
214
|
+
|
215
|
+
except Exception as e:
|
216
|
+
logger.warning(f"Could not capture git context: {e}")
|
217
|
+
|
218
|
+
return git_context
|
219
|
+
|
220
|
+
def _get_version(self) -> str:
|
221
|
+
"""Get Claude MPM version.
|
222
|
+
|
223
|
+
Returns:
|
224
|
+
Version string or "unknown"
|
225
|
+
"""
|
226
|
+
try:
|
227
|
+
version_file = Path(__file__).parent.parent.parent.parent.parent / "VERSION"
|
228
|
+
if version_file.exists():
|
229
|
+
return version_file.read_text().strip()
|
230
|
+
except Exception:
|
231
|
+
pass
|
232
|
+
return "unknown"
|
233
|
+
|
234
|
+
def _get_build_number(self) -> str:
|
235
|
+
"""Get Claude MPM build number.
|
236
|
+
|
237
|
+
Returns:
|
238
|
+
Build number string or "unknown"
|
239
|
+
"""
|
240
|
+
try:
|
241
|
+
build_file = (
|
242
|
+
Path(__file__).parent.parent.parent.parent.parent / "BUILD_NUMBER"
|
243
|
+
)
|
244
|
+
if build_file.exists():
|
245
|
+
return build_file.read_text().strip()
|
246
|
+
except Exception:
|
247
|
+
pass
|
248
|
+
return "unknown"
|
249
|
+
|
250
|
+
def _create_pause_commit(self, session_id: str, summary: Optional[str]) -> bool:
|
251
|
+
"""Create git commit with session pause information.
|
252
|
+
|
253
|
+
Args:
|
254
|
+
session_id: The session identifier
|
255
|
+
summary: Optional summary of what was being worked on
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
True if commit was created successfully
|
259
|
+
"""
|
260
|
+
try:
|
261
|
+
# Check if we're in a git repo
|
262
|
+
result = subprocess.run(
|
263
|
+
["git", "rev-parse", "--git-dir"],
|
264
|
+
cwd=str(self.project_path),
|
265
|
+
capture_output=True,
|
266
|
+
text=True,
|
267
|
+
check=False,
|
268
|
+
)
|
269
|
+
|
270
|
+
if result.returncode != 0:
|
271
|
+
logger.debug("Not a git repository, skipping commit")
|
272
|
+
return False
|
273
|
+
|
274
|
+
# Add session files to git
|
275
|
+
subprocess.run(
|
276
|
+
["git", "add", ".claude-mpm/sessions/pause/"],
|
277
|
+
cwd=str(self.project_path),
|
278
|
+
capture_output=True,
|
279
|
+
check=False,
|
280
|
+
)
|
281
|
+
|
282
|
+
# Check if there are changes to commit
|
283
|
+
result = subprocess.run(
|
284
|
+
["git", "diff", "--cached", "--quiet"],
|
285
|
+
cwd=str(self.project_path),
|
286
|
+
check=False,
|
287
|
+
)
|
288
|
+
|
289
|
+
if result.returncode == 0:
|
290
|
+
logger.debug("No changes to commit")
|
291
|
+
return False
|
292
|
+
|
293
|
+
# Build commit message
|
294
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
295
|
+
context = summary or "Session paused"
|
296
|
+
|
297
|
+
commit_message = f"""session: pause at {timestamp}
|
298
|
+
|
299
|
+
Session ID: {session_id}
|
300
|
+
Context: {context}
|
301
|
+
|
302
|
+
🤖👥 Generated with [Claude MPM](https://github.com/bobmatnyc/claude-mpm)
|
303
|
+
|
304
|
+
Co-Authored-By: Claude <noreply@anthropic.com>"""
|
305
|
+
|
306
|
+
# Create commit
|
307
|
+
result = subprocess.run(
|
308
|
+
["git", "commit", "-m", commit_message],
|
309
|
+
cwd=str(self.project_path),
|
310
|
+
capture_output=True,
|
311
|
+
text=True,
|
312
|
+
check=False,
|
313
|
+
)
|
314
|
+
|
315
|
+
if result.returncode == 0:
|
316
|
+
logger.info(f"Created pause commit for session {session_id}")
|
317
|
+
return True
|
318
|
+
logger.warning(f"Could not create commit: {result.stderr}")
|
319
|
+
return False
|
320
|
+
|
321
|
+
except Exception as e:
|
322
|
+
logger.warning(f"Could not create pause commit: {e}")
|
323
|
+
return False
|
324
|
+
|
325
|
+
def _display_pause_success(
|
326
|
+
self, session_id: str, session_file: Path, commit_created: bool
|
327
|
+
) -> None:
|
328
|
+
"""Display success message for paused session.
|
329
|
+
|
330
|
+
Args:
|
331
|
+
session_id: The session identifier
|
332
|
+
session_file: Path to the session state file
|
333
|
+
commit_created: Whether git commit was created
|
334
|
+
"""
|
335
|
+
console.print()
|
336
|
+
console.print(
|
337
|
+
Panel(
|
338
|
+
f"[bold green]Session Paused Successfully[/bold green]\n\n"
|
339
|
+
f"[cyan]Session ID:[/cyan] {session_id}\n"
|
340
|
+
f"[cyan]State saved:[/cyan] {session_file}\n"
|
341
|
+
f"[cyan]Git commit:[/cyan] {'✓ Created' if commit_created else '✗ Not created (no git repo or no changes)'}\n\n"
|
342
|
+
f"[dim]To resume this session later, run:[/dim]\n"
|
343
|
+
f"[yellow] claude-mpm mpm-init pause resume[/yellow]",
|
344
|
+
title="🔴 Session Pause",
|
345
|
+
border_style="green",
|
346
|
+
)
|
347
|
+
)
|
348
|
+
console.print()
|
349
|
+
|
350
|
+
def list_paused_sessions(self) -> List[Dict[str, Any]]:
|
351
|
+
"""List all paused sessions.
|
352
|
+
|
353
|
+
Returns:
|
354
|
+
List of session information dicts
|
355
|
+
"""
|
356
|
+
if not self.session_dir.exists():
|
357
|
+
return []
|
358
|
+
|
359
|
+
sessions = []
|
360
|
+
for session_file in sorted(self.session_dir.glob("session-*.json")):
|
361
|
+
try:
|
362
|
+
state = self.storage.read_json(session_file)
|
363
|
+
if state:
|
364
|
+
sessions.append(
|
365
|
+
{
|
366
|
+
"session_id": state.get("session_id"),
|
367
|
+
"paused_at": state.get("paused_at"),
|
368
|
+
"summary": state.get("conversation", {}).get("summary"),
|
369
|
+
"file_path": str(session_file),
|
370
|
+
}
|
371
|
+
)
|
372
|
+
except Exception as e:
|
373
|
+
logger.warning(f"Could not read session file {session_file}: {e}")
|
374
|
+
|
375
|
+
return sessions
|
376
|
+
|
377
|
+
def get_latest_session(self) -> Optional[Dict[str, Any]]:
|
378
|
+
"""Get the most recent paused session.
|
379
|
+
|
380
|
+
Returns:
|
381
|
+
Session state dict or None if no sessions found
|
382
|
+
"""
|
383
|
+
sessions = self.list_paused_sessions()
|
384
|
+
if not sessions:
|
385
|
+
return None
|
386
|
+
|
387
|
+
# Return the most recent (last in sorted list)
|
388
|
+
latest_file = Path(sessions[-1]["file_path"])
|
389
|
+
return self.storage.read_json(latest_file)
|