aline-ai 0.1.9__py3-none-any.whl → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aline-ai
3
- Version: 0.1.9
3
+ Version: 0.2.0
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -0,0 +1,25 @@
1
+ aline_ai-0.2.0.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
+ realign/__init__.py,sha256=BQbPK4WDn_XTLzhYq5k0CThranUR0W-UN21_yU9w-D4,68
3
+ realign/claude_detector.py,sha256=NLxI0zJWcqNxNha9jAy9AslTMwHKakCc9yPGdkrbiFE,3028
4
+ realign/cli.py,sha256=bkwS329jMDEkrUEihXRN2DDyeTKE6HbAysoDxxskZ8g,941
5
+ realign/codex_detector.py,sha256=RI3JbZgebrhoqpRfTBMfclYCAISN7hZAHVW3bgftJpU,4428
6
+ realign/config.py,sha256=jarinbr0mA6e5DmgY19b_VpMnxk6SOYTwyvB9luq0ww,7207
7
+ realign/file_lock.py,sha256=-9c3tMdMj_ZxmasK5y6hV9Gfo6KDsSO3Q7PXiTBhsu4,3369
8
+ realign/hooks.py,sha256=l4RclCoUaj4w84r3kLSPTh3c9IVqYE5_pv_JKXMp1f8,45076
9
+ realign/logging_config.py,sha256=KvkKktF-bkUu031y9vgUoHpsbnOw7ud25jhpzliNZwA,4929
10
+ realign/mcp_server.py,sha256=dntFatMpozI80K5hHrIiQ9sviC6ARKTP89goULhi1T4,16477
11
+ realign/mcp_watcher.py,sha256=D6qVM0yD2hQPxrD_HnHkDob78_dqK96klHIogh48cw4,23971
12
+ realign/redactor.py,sha256=uZvLKKGrRGJm-qM8S4XJyJK6i0CSSby_wbKiay7VGJw,8148
13
+ realign/commands/__init__.py,sha256=GG6IMw6fUBQAXGJDFJvOOQgv6pkiRSfMh8z3AYXTyRM,31
14
+ realign/commands/auto_commit.py,sha256=jgjAYZHqN34NmQkncZg3Vtwsl3MyAlsvucxEBwUj7ko,7450
15
+ realign/commands/commit.py,sha256=mlwrv5nfTRY17WlcAdiJKKGh5uM7dGvT7sMxhdbsfkw,12605
16
+ realign/commands/config.py,sha256=iiu7usqw00djKZja5bx0iDH8DB0vU2maUPMkXLdgXwI,6609
17
+ realign/commands/init.py,sha256=_w5_ZzNpdiHO5EJ6KHKwJf3_ne54EXhwuq4sT-XSZ0U,13246
18
+ realign/commands/search.py,sha256=xTWuX0lpjQPX8cen0ewl-BNF0FeWgjMwN06bdeesED8,18770
19
+ realign/commands/session_utils.py,sha256=L1DwZIGCOBirp6tkAswACJEeDa6i9aAAfsialAs4rRY,864
20
+ realign/commands/show.py,sha256=A9LvhOBcY6_HoI76irPB2rBOSgdftBuX2uZiO8IwNoU,16338
21
+ aline_ai-0.2.0.dist-info/METADATA,sha256=vBklfx2KSYMZbpmnjY9h1A3AhwlCQDdHMr4xEVX2iuM,1398
22
+ aline_ai-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ aline_ai-0.2.0.dist-info/entry_points.txt,sha256=h-NocHDzSueXfsepHTIdRPNQzhNZQPAztJfldd-mQTE,202
24
+ aline_ai-0.2.0.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
25
+ aline_ai-0.2.0.dist-info/RECORD,,
realign/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Aline - AI Agent Chat Session Tracker."""
2
2
 
3
- __version__ = "0.1.9"
3
+ __version__ = "0.2.0"
@@ -88,7 +88,7 @@ def generate_commit_message() -> str:
88
88
  """Generate an automatic commit message."""
89
89
  from datetime import datetime
90
90
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
91
- return f"chore: Auto-commit chat session ({timestamp})"
91
+ return f"chore: Auto-commit MCP session ({timestamp})"
92
92
 
93
93
 
94
94
  def auto_commit_once(repo_root: Path, message: Optional[str] = None, silent: bool = False) -> bool:
@@ -277,3 +277,103 @@ def commit_command(
277
277
  except subprocess.CalledProcessError as e:
278
278
  console.print(f"[red]Error creating commit:[/red] {e}")
279
279
  raise typer.Exit(1)
280
+
281
+
282
+ def commit_internal(
283
+ repo_root: Path,
284
+ message: str,
285
+ all_files: bool = True,
286
+ amend: bool = False,
287
+ no_edit: bool = False,
288
+ ) -> Tuple[bool, Optional[str], str]:
289
+ """
290
+ Internal commit API for programmatic use (e.g., by MCP watcher).
291
+ Does not use typer or rich console - pure Python function.
292
+
293
+ Args:
294
+ repo_root: Path to git repository root
295
+ message: Commit message
296
+ all_files: Stage all changes before commit (default: True)
297
+ amend: Amend the previous commit
298
+ no_edit: Use previous commit message (with --amend)
299
+
300
+ Returns:
301
+ Tuple of (success, commit_hash, message)
302
+ - success: True if commit was created, False otherwise
303
+ - commit_hash: Short commit hash if successful, None otherwise
304
+ - message: Status/error message
305
+ """
306
+ try:
307
+ # Change to repo directory
308
+ original_cwd = Path.cwd()
309
+ os.chdir(repo_root)
310
+
311
+ try:
312
+ # Stage all changes if requested
313
+ if all_files:
314
+ try:
315
+ subprocess.run(["git", "add", "-A"], check=True, capture_output=True)
316
+ except subprocess.CalledProcessError as e:
317
+ return False, None, f"Failed to stage changes: {e}"
318
+
319
+ # Check for file changes and session changes
320
+ has_files = has_file_changes()
321
+ has_sessions, session_files = has_session_changes(repo_root)
322
+
323
+ # Determine commit type
324
+ if not has_files and not has_sessions:
325
+ return False, None, "No changes detected"
326
+
327
+ # Build git commit command
328
+ commit_cmd = ["git", "commit"]
329
+
330
+ # Add message
331
+ commit_cmd.extend(["-m", message])
332
+
333
+ # Add amend flag if set
334
+ if amend:
335
+ commit_cmd.append("--amend")
336
+ if no_edit:
337
+ commit_cmd.append("--no-edit")
338
+
339
+ # If only session changes, use --allow-empty
340
+ if has_sessions and not has_files:
341
+ commit_cmd.append("--allow-empty")
342
+
343
+ # Execute git commit
344
+ try:
345
+ subprocess.run(
346
+ commit_cmd,
347
+ check=True,
348
+ capture_output=True,
349
+ text=True,
350
+ )
351
+
352
+ # Get commit hash
353
+ result = subprocess.run(
354
+ ["git", "rev-parse", "--short", "HEAD"],
355
+ capture_output=True,
356
+ text=True,
357
+ check=True,
358
+ )
359
+ commit_hash = result.stdout.strip()
360
+
361
+ # Build status message
362
+ if has_sessions and not has_files:
363
+ status_msg = f"Discussion commit created (sessions only): {commit_hash}"
364
+ elif has_files and has_sessions:
365
+ status_msg = f"Commit created (files + sessions): {commit_hash}"
366
+ else:
367
+ status_msg = f"Commit created: {commit_hash}"
368
+
369
+ return True, commit_hash, status_msg
370
+
371
+ except subprocess.CalledProcessError as e:
372
+ return False, None, f"Git commit failed: {e.stderr if e.stderr else str(e)}"
373
+
374
+ finally:
375
+ # Restore original directory
376
+ os.chdir(original_cwd)
377
+
378
+ except Exception as e:
379
+ return False, None, f"Unexpected error: {str(e)}"
@@ -30,12 +30,12 @@ def config_command(
30
30
 
31
31
  Examples:
32
32
 
33
- realign config init # Create default config file
34
- realign config get # Show all config values
35
- realign config get use_LLM # Get specific config value
36
- realign config set llm_provider claude # Set LLM provider to Claude
37
- realign config set llm_provider openai # Set LLM provider to OpenAI
38
- realign config set llm_provider auto # Set LLM provider to auto
33
+ aline config init # Create default config file
34
+ aline config get # Show all config values
35
+ aline config get use_LLM # Get specific config value
36
+ aline config set llm_provider claude # Set LLM provider to Claude
37
+ aline config set llm_provider openai # Set LLM provider to OpenAI
38
+ aline config set llm_provider auto # Set LLM provider to auto
39
39
  """
40
40
  config_path = Path.home() / ".config" / "realign" / "config.yaml"
41
41
 
@@ -43,7 +43,7 @@ def config_command(
43
43
  # Initialize config file
44
44
  if config_path.exists():
45
45
  console.print(f"[yellow]Config file already exists at {config_path}[/yellow]")
46
- console.print("Use 'realign config get' to view current settings")
46
+ console.print("Use 'aline config get' to view current settings")
47
47
  return
48
48
 
49
49
  config_path.parent.mkdir(parents=True, exist_ok=True)
@@ -61,7 +61,7 @@ def config_command(
61
61
  # Get config value(s)
62
62
  if not config_path.exists():
63
63
  console.print(f"[red]✗[/red] Config file not found at {config_path}")
64
- console.print("Run 'realign config init' to create it")
64
+ console.print("Run 'aline config init' to create it")
65
65
  raise typer.Exit(1)
66
66
 
67
67
  config = ReAlignConfig.load()
@@ -80,6 +80,7 @@ def config_command(
80
80
  table.add_row("llm_provider", config.llm_provider)
81
81
  table.add_row("auto_detect_claude", str(config.auto_detect_claude))
82
82
  table.add_row("auto_detect_codex", str(config.auto_detect_codex))
83
+ table.add_row("mcp_auto_commit", str(config.mcp_auto_commit))
83
84
 
84
85
  console.print(table)
85
86
  console.print(f"\nConfig file location: {config_path}")
@@ -87,7 +88,7 @@ def config_command(
87
88
  # Show specific config value
88
89
  if not hasattr(config, key):
89
90
  console.print(f"[red]✗[/red] Unknown config key: {key}")
90
- console.print("Available keys: local_history_path, summary_max_chars, redact_on_match, hooks_installation, use_LLM, llm_provider, auto_detect_claude, auto_detect_codex")
91
+ console.print("Available keys: local_history_path, summary_max_chars, redact_on_match, hooks_installation, use_LLM, llm_provider, auto_detect_claude, auto_detect_codex, mcp_auto_commit")
91
92
  raise typer.Exit(1)
92
93
 
93
94
  value = getattr(config, key)
@@ -98,7 +99,7 @@ def config_command(
98
99
  # Set config value
99
100
  if key is None or value is None:
100
101
  console.print("[red]✗[/red] Both key and value are required for 'set' action")
101
- console.print("Usage: realign config set <key> <value>")
102
+ console.print("Usage: aline config set <key> <value>")
102
103
  raise typer.Exit(1)
103
104
 
104
105
  if not config_path.exists():
@@ -112,14 +113,14 @@ def config_command(
112
113
  # Validate and set the value
113
114
  if not hasattr(config, key):
114
115
  console.print(f"[red]✗[/red] Unknown config key: {key}")
115
- console.print("Available keys: local_history_path, summary_max_chars, redact_on_match, hooks_installation, use_LLM, llm_provider, auto_detect_claude, auto_detect_codex")
116
+ console.print("Available keys: local_history_path, summary_max_chars, redact_on_match, hooks_installation, use_LLM, llm_provider, auto_detect_claude, auto_detect_codex, mcp_auto_commit")
116
117
  raise typer.Exit(1)
117
118
 
118
119
  # Type conversion and validation
119
120
  try:
120
121
  if key == "summary_max_chars":
121
122
  value = int(value)
122
- elif key in ["redact_on_match", "use_LLM", "auto_detect_claude", "auto_detect_codex"]:
123
+ elif key in ["redact_on_match", "use_LLM", "auto_detect_claude", "auto_detect_codex", "mcp_auto_commit"]:
123
124
  value = value.lower() in ("true", "1", "yes")
124
125
  elif key == "llm_provider":
125
126
  if value not in ("auto", "claude", "openai"):
realign/commands/init.py CHANGED
@@ -18,6 +18,7 @@ def init_repository(
18
18
  repo_path: str = ".",
19
19
  auto_init_git: bool = True,
20
20
  skip_commit: bool = False,
21
+ force: bool = False,
21
22
  ) -> Dict[str, Any]:
22
23
  """
23
24
  Core initialization logic (non-interactive).
@@ -26,6 +27,7 @@ def init_repository(
26
27
  repo_path: Path to the repository to initialize
27
28
  auto_init_git: Automatically initialize git repo if not exists
28
29
  skip_commit: Skip auto-commit of hooks
30
+ force: Force re-initialization and update all hooks even if they exist
29
31
 
30
32
  Returns:
31
33
  Dictionary with initialization results and metadata
@@ -101,19 +103,21 @@ def init_repository(
101
103
 
102
104
  # Install pre-commit hook
103
105
  pre_commit_path = hooks_dir / "pre-commit"
104
- if not pre_commit_path.exists():
106
+ if force or not pre_commit_path.exists():
107
+ action = "updated" if pre_commit_path.exists() else "created"
105
108
  pre_commit_content = get_pre_commit_hook()
106
109
  pre_commit_path.write_text(pre_commit_content, encoding="utf-8")
107
110
  pre_commit_path.chmod(0o755)
108
- result["hooks_created"].append("pre-commit")
111
+ result["hooks_created"].append(f"pre-commit ({action})")
109
112
 
110
113
  # Install prepare-commit-msg hook
111
114
  hook_path = hooks_dir / "prepare-commit-msg"
112
- if not hook_path.exists():
115
+ if force or not hook_path.exists():
116
+ action = "updated" if hook_path.exists() else "created"
113
117
  hook_content = get_prepare_commit_msg_hook()
114
118
  hook_path.write_text(hook_content, encoding="utf-8")
115
119
  hook_path.chmod(0o755)
116
- result["hooks_created"].append("prepare-commit-msg")
120
+ result["hooks_created"].append(f"prepare-commit-msg ({action})")
117
121
 
118
122
  # Create .gitignore for sessions (optional - commented out for MVP)
119
123
  gitignore_path = realign_dir / ".gitignore"
@@ -193,13 +197,18 @@ def init_repository(
193
197
  def init_command(
194
198
  yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts (deprecated, now default)"),
195
199
  skip_commit: bool = typer.Option(False, "--skip-commit", help="Skip auto-commit of hooks"),
200
+ force: bool = typer.Option(False, "--force", "-f", help="Force re-initialization and update hooks even if they exist"),
196
201
  ):
197
- """Initialize ReAlign in the current git repository."""
202
+ """Initialize ReAlign in the current git repository.
203
+
204
+ Use --force to re-initialize and update hooks to the latest version.
205
+ """
198
206
  # Call the core function
199
207
  result = init_repository(
200
208
  repo_path=".",
201
209
  auto_init_git=True,
202
210
  skip_commit=skip_commit,
211
+ force=force,
203
212
  )
204
213
 
205
214
  # Print detailed results
@@ -218,8 +227,9 @@ def init_command(
218
227
  console.print(f" Config Path: [cyan]{result.get('config_path', 'N/A')}[/cyan]")
219
228
  console.print(f" History Directory: [cyan]{result.get('history_dir', 'N/A')}[/cyan]")
220
229
  console.print(f" Git Initialized: [cyan]{result.get('git_initialized', False)}[/cyan]")
230
+ console.print(f" Force Reinit: [cyan]{force}[/cyan]")
221
231
  console.print(f" Skip Commit: [cyan]{skip_commit}[/cyan]")
222
- console.print(f" Hooks Created: [cyan]{', '.join(result.get('hooks_created', [])) or 'None'}[/cyan]")
232
+ console.print(f" Hooks: [cyan]{', '.join(result.get('hooks_created', [])) or 'None'}[/cyan]")
223
233
  console.print(f" Auto-committed: [cyan]{result.get('committed', False)}[/cyan]")
224
234
 
225
235
  if result.get("errors"):
@@ -246,23 +256,31 @@ def get_pre_commit_hook() -> str:
246
256
  # ReAlign pre-commit hook
247
257
  # Finds and stages agent session files before commit
248
258
 
249
- # 1. Try to find realign-hook-pre-commit in PATH first (pipx/pip installations)
250
- if command -v realign-hook-pre-commit >/dev/null 2>&1; then
251
- VERSION=$(realign version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
252
- echo "ReAlign pre-commit hook (dev-$VERSION)" >&2
253
- exec realign-hook-pre-commit "$@"
259
+ # 1. Try direct Python execution first (development mode - highest priority for dev machines)
260
+ # Check if realign module is importable (e.g., installed in editable mode or in PYTHONPATH)
261
+ if python -c "import realign.hooks" 2>/dev/null; then
262
+ echo "Aline pre-commit hook (dev-mode)" >&2
263
+ exec python -m realign.hooks --pre-commit "$@"
264
+ fi
265
+
266
+ # 2. Try to find aline-hook-pre-commit in PATH (pipx/pip installations)
267
+ if command -v aline-hook-pre-commit >/dev/null 2>&1; then
268
+ VERSION=$(aline version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
269
+ echo "Aline pre-commit hook (cli-$VERSION)" >&2
270
+ exec aline-hook-pre-commit "$@"
254
271
  fi
255
272
 
256
- # 2. Try using uvx (for MCP installations where command is in uvx cache)
273
+ # 3. Try using uvx (for MCP installations where command is in uvx cache)
257
274
  if command -v uvx >/dev/null 2>&1; then
258
- VERSION=$(uvx --from realign-git realign version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
259
- echo "ReAlign pre-commit hook (release-$VERSION)" >&2
260
- exec uvx --from realign-git realign-hook-pre-commit "$@"
275
+ VERSION=$(uvx --from aline-ai aline version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
276
+ echo "Aline pre-commit hook (mcp-$VERSION)" >&2
277
+ exec uvx --from aline-ai aline-hook-pre-commit "$@"
261
278
  fi
262
279
 
263
280
  # If all else fails, print an error
264
- echo "Error: Cannot find realign. Please ensure it's installed:" >&2
265
- echo " - For CLI: pipx install realign-git" >&2
281
+ echo "Error: Cannot find aline. Please ensure it's installed:" >&2
282
+ echo " - For dev: Ensure 'python -m realign.hooks' works (editable install or PYTHONPATH)" >&2
283
+ echo " - For CLI: pipx install aline-ai" >&2
266
284
  echo " - For MCP: Ensure uvx is available" >&2
267
285
  exit 1
268
286
  '''
@@ -282,23 +300,31 @@ if [ "$COMMIT_SOURCE" = "merge" ] || [ "$COMMIT_SOURCE" = "squash" ] || [ "$COMM
282
300
  exit 0
283
301
  fi
284
302
 
285
- # 1. Try to find realign-hook-prepare-commit-msg in PATH first (pipx/pip installations)
286
- if command -v realign-hook-prepare-commit-msg >/dev/null 2>&1; then
287
- VERSION=$(realign version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
288
- echo "ReAlign prepare-commit-msg hook (dev-$VERSION)" >&2
289
- exec realign-hook-prepare-commit-msg "$@"
303
+ # 1. Try direct Python execution first (development mode - highest priority for dev machines)
304
+ # Check if realign module is importable (e.g., installed in editable mode or in PYTHONPATH)
305
+ if python -c "import realign.hooks" 2>/dev/null; then
306
+ echo "Aline prepare-commit-msg hook (dev-mode)" >&2
307
+ exec python -m realign.hooks --prepare-commit-msg "$@"
308
+ fi
309
+
310
+ # 2. Try to find aline-hook-prepare-commit-msg in PATH (pipx/pip installations)
311
+ if command -v aline-hook-prepare-commit-msg >/dev/null 2>&1; then
312
+ VERSION=$(aline version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
313
+ echo "Aline prepare-commit-msg hook (cli-$VERSION)" >&2
314
+ exec aline-hook-prepare-commit-msg "$@"
290
315
  fi
291
316
 
292
- # 2. Try using uvx (for MCP installations where command is in uvx cache)
317
+ # 3. Try using uvx (for MCP installations where command is in uvx cache)
293
318
  if command -v uvx >/dev/null 2>&1; then
294
- VERSION=$(uvx --from realign-git realign version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
295
- echo "ReAlign prepare-commit-msg hook (release-$VERSION)" >&2
296
- exec uvx --from realign-git realign-hook-prepare-commit-msg "$@"
319
+ VERSION=$(uvx --from aline-ai aline version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
320
+ echo "Aline prepare-commit-msg hook (mcp-$VERSION)" >&2
321
+ exec uvx --from aline-ai aline-hook-prepare-commit-msg "$@"
297
322
  fi
298
323
 
299
324
  # If all else fails, print an error
300
- echo "Error: Cannot find realign. Please ensure it's installed:" >&2
301
- echo " - For CLI: pipx install realign-git" >&2
325
+ echo "Error: Cannot find aline. Please ensure it's installed:" >&2
326
+ echo " - For dev: Ensure 'python -m realign.hooks' works (editable install or PYTHONPATH)" >&2
327
+ echo " - For CLI: pipx install aline-ai" >&2
302
328
  echo " - For MCP: Ensure uvx is available" >&2
303
329
  exit 1
304
330
  '''
@@ -4,12 +4,17 @@ import subprocess
4
4
  import re
5
5
  import json
6
6
  from pathlib import Path
7
- from typing import Optional, List, Dict, Any
7
+ from typing import Optional, List, Dict, Any, Tuple
8
8
  import typer
9
9
  from rich.console import Console
10
10
  from rich.table import Table
11
+ from .session_utils import find_session_paths_for_commit
11
12
 
12
13
  console = Console()
14
+ SUMMARY_BLOCK_PATTERN = re.compile(
15
+ r"\n*--- LLM-Summary \((?P<model>.+?)\) ---\n(?P<bullets>(?:\*.*\n?)+)",
16
+ re.MULTILINE,
17
+ )
13
18
 
14
19
 
15
20
  def search_command(
@@ -47,7 +52,7 @@ def search_command(
47
52
  search_commits = not session_only
48
53
  search_sessions = not commits_only
49
54
 
50
- # Search in commit messages (including Agent-Summary footers)
55
+ # Search in commit messages (including summary blocks)
51
56
  if search_commits:
52
57
  console.print("\n[bold]Commits with matching summaries:[/bold]")
53
58
 
@@ -79,21 +84,16 @@ def search_command(
79
84
  author = parts[1]
80
85
  subject = parts[2]
81
86
  body = parts[3] if len(parts) > 3 else ""
82
-
83
- # Extract Agent-Summary and Agent-Session-Paths from body
84
- summary_match = re.search(r"Agent-Summary:\s*(.+?)(?=\nAgent-|\n\n|\Z)", body, re.DOTALL)
85
- session_match = re.search(r"Agent-Session-Paths?:\s*(.+?)(?:\n|$)", body)
86
-
87
- summary = summary_match.group(1).strip() if summary_match else ""
88
- session_path = session_match.group(1) if session_match else ""
87
+ summary_model, summary_bullets, cleaned_body = extract_summary_from_body(body)
89
88
 
90
89
  commits.append({
91
90
  "hash": commit_hash,
92
91
  "author": author,
93
92
  "subject": subject,
94
93
  "body": body,
95
- "summary": summary,
96
- "session_path": session_path,
94
+ "display_body": cleaned_body.strip(),
95
+ "summary_model": summary_model,
96
+ "summary_bullets": summary_bullets,
97
97
  })
98
98
 
99
99
  if commits:
@@ -110,29 +110,23 @@ def search_command(
110
110
  console.print(f" [bold]{highlight_text(commit['subject'], keyword)}[/bold]")
111
111
 
112
112
  # Show body if it exists (excluding agent metadata)
113
- if commit["body"]:
114
- # Remove Agent-* metadata from body for display
115
- display_body = re.sub(r'\n*Agent-Summary:.*', '', commit["body"], flags=re.DOTALL)
116
- display_body = re.sub(r'\n*Agent-Session-Paths?:.*', '', display_body, flags=re.DOTALL)
117
- display_body = display_body.strip()
118
- if display_body:
119
- # Indent body lines
120
- for line in display_body.split('\n'):
121
- console.print(f" {highlight_text(line, keyword)}")
122
-
123
- # Show Agent-Summary if it exists
124
- if commit["summary"]:
125
- console.print(f" [yellow]Agent-Summary: {highlight_text(commit['summary'], keyword)}[/yellow]")
126
-
127
- if commit["session_path"]:
128
- console.print(f" [dim]Session: {commit['session_path']}[/dim]")
113
+ display_body = commit.get("display_body", "")
114
+ if display_body:
115
+ for line in display_body.split('\n'):
116
+ console.print(f" {highlight_text(line, keyword)}")
117
+
118
+ bullets = commit.get("summary_bullets") or []
119
+ if bullets:
120
+ header = f"LLM-Summary ({commit.get('summary_model') or 'Local summarizer'})"
121
+ console.print(f" [yellow]{highlight_text(header, keyword)}[/yellow]")
122
+ for bullet in bullets:
123
+ console.print(f" [yellow]{highlight_text(bullet, keyword)}[/yellow]")
129
124
 
130
125
  # Show session content if requested
131
126
  if show_session and commits:
132
127
  console.print("\n[bold]Session content:[/bold]")
133
128
  for commit in commits[:5]: # Limit to first 5 for session display
134
- if commit["session_path"]:
135
- show_session_content(repo_root, commit["hash"], commit["session_path"])
129
+ show_session_content(repo_root, commit["hash"])
136
130
  else:
137
131
  console.print("[yellow]No commits found matching the keyword.[/yellow]")
138
132
  else:
@@ -170,15 +164,47 @@ def search_command(
170
164
  console.print("[yellow]Could not search session files.[/yellow]")
171
165
 
172
166
 
173
- def show_session_content(repo_root: Path, commit_hash: str, session_path: str):
167
+ def show_session_content(repo_root: Path, commit_hash: str):
174
168
  """Show content of a session file from a commit."""
175
169
  console.print(f"\n[bold cyan]Session for commit {commit_hash}:[/bold cyan]")
176
170
 
171
+ session_paths = find_session_paths_for_commit(repo_root, commit_hash)
172
+ if not session_paths:
173
+ console.print(" [yellow]No session files recorded in this commit.[/yellow]")
174
+ return
175
+
176
+ session_path = session_paths[0]
177
177
  session_file = repo_root / session_path
178
178
  if session_file.exists():
179
+ console.print(f" [dim]Path: {session_path}[/dim]")
179
180
  show_file_content(session_file)
180
181
  else:
181
- console.print(f"[yellow]Session file not found:[/yellow] {session_path}")
182
+ result = subprocess.run(
183
+ ["git", "show", f"{commit_hash}:{session_path}"],
184
+ capture_output=True,
185
+ text=True,
186
+ check=False,
187
+ cwd=repo_root,
188
+ )
189
+ if result.returncode == 0:
190
+ console.print(f" [dim]Path: {session_path} (from commit tree)[/dim]")
191
+ console.print(result.stdout)
192
+ else:
193
+ console.print(f"[yellow]Session file not found:[/yellow] {session_path}")
194
+
195
+
196
+ def extract_summary_from_body(body: str) -> Tuple[str, List[str], str]:
197
+ """Extract summary metadata from commit body."""
198
+ match = SUMMARY_BLOCK_PATTERN.search(body)
199
+ if not match:
200
+ return "", [], body
201
+
202
+ summary_model = match.group("model").strip()
203
+ bullets_text = match.group("bullets").strip()
204
+ bullet_lines = [line.strip() for line in bullets_text.splitlines() if line.strip()]
205
+ start, end = match.span()
206
+ cleaned_body = (body[:start] + body[end:]).strip()
207
+ return summary_model, bullet_lines, cleaned_body
182
208
 
183
209
 
184
210
  def show_file_content(file_path: Path, highlight: Optional[str] = None, max_lines: int = 20):
@@ -0,0 +1,28 @@
1
+ """Shared helpers for working with session files recorded in commits."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import List
8
+
9
+
10
+ def find_session_paths_for_commit(repo_root: Path, commit_hash: str) -> List[str]:
11
+ """Return relative session file paths tracked in a given commit."""
12
+ try:
13
+ result = subprocess.run(
14
+ ["git", "show", "--pretty=format:", "--name-only", commit_hash],
15
+ capture_output=True,
16
+ text=True,
17
+ check=True,
18
+ cwd=repo_root,
19
+ )
20
+ except subprocess.CalledProcessError:
21
+ return []
22
+
23
+ paths = []
24
+ for line in result.stdout.splitlines():
25
+ candidate = line.strip()
26
+ if candidate.startswith(".realign/sessions/") and candidate.endswith(".jsonl"):
27
+ paths.append(candidate)
28
+ return paths
realign/commands/show.py CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  import subprocess
4
4
  import json
5
- import re
6
5
  from pathlib import Path
7
6
  from typing import Optional
8
7
  import typer
@@ -12,6 +11,7 @@ from rich.panel import Panel
12
11
  from rich.markdown import Markdown
13
12
 
14
13
  console = Console()
14
+ from .session_utils import find_session_paths_for_commit
15
15
 
16
16
 
17
17
  def calculate_line_range(
@@ -99,49 +99,36 @@ def show_command(
99
99
  if commit:
100
100
  console.print(f"[blue]Fetching session for commit:[/blue] {commit}")
101
101
 
102
- try:
103
- # Get commit message to extract session path
102
+ session_paths = find_session_paths_for_commit(repo_root, commit)
103
+ if not session_paths:
104
+ console.print("[yellow]No agent session files tracked in this commit.[/yellow]")
105
+ raise typer.Exit(0)
106
+
107
+ session_path = session_paths[0]
108
+ if len(session_paths) > 1:
109
+ other_paths = ", ".join(session_paths[1:])
110
+ console.print(f"[yellow]Multiple session files found; showing first. Others: {other_paths}[/yellow]")
111
+ console.print(f"[green]Found session:[/green] {session_path}")
112
+
113
+ # Try to read from working tree first
114
+ full_session_path = repo_root / session_path
115
+ if full_session_path.exists():
116
+ with open(full_session_path, "r", encoding="utf-8") as f:
117
+ session_content = f.read()
118
+ else:
119
+ # Try to get from git
104
120
  result = subprocess.run(
105
- ["git", "show", "--format=%b", "-s", commit],
121
+ ["git", "show", f"{commit}:{session_path}"],
106
122
  capture_output=True,
107
123
  text=True,
108
- check=True,
124
+ check=False,
109
125
  cwd=repo_root,
110
126
  )
111
-
112
- body = result.stdout
113
- session_match = re.search(r"Agent-Session-Path:\s*(.+?)(?:\n|$)", body)
114
-
115
- if not session_match:
116
- console.print("[yellow]No agent session found in this commit.[/yellow]")
117
- raise typer.Exit(0)
118
-
119
- session_path = session_match.group(1).strip()
120
- console.print(f"[green]Found session:[/green] {session_path}")
121
-
122
- # Try to read from working tree first
123
- full_session_path = repo_root / session_path
124
- if full_session_path.exists():
125
- with open(full_session_path, "r", encoding="utf-8") as f:
126
- session_content = f.read()
127
+ if result.returncode == 0:
128
+ session_content = result.stdout
127
129
  else:
128
- # Try to get from git
129
- result = subprocess.run(
130
- ["git", "show", f"{commit}:{session_path}"],
131
- capture_output=True,
132
- text=True,
133
- check=False,
134
- cwd=repo_root,
135
- )
136
- if result.returncode == 0:
137
- session_content = result.stdout
138
- else:
139
- console.print(f"[red]Could not find session file:[/red] {session_path}")
140
- raise typer.Exit(1)
141
-
142
- except subprocess.CalledProcessError as e:
143
- console.print(f"[red]Error fetching commit:[/red] {e}")
144
- raise typer.Exit(1)
130
+ console.print(f"[red]Could not find session file:[/red] {session_path}")
131
+ raise typer.Exit(1)
145
132
 
146
133
  # Get session from direct path
147
134
  elif session: