aline-ai 0.1.10__py3-none-any.whl → 0.2.1__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.
- {aline_ai-0.1.10.dist-info → aline_ai-0.2.1.dist-info}/METADATA +1 -1
- aline_ai-0.2.1.dist-info/RECORD +25 -0
- realign/__init__.py +1 -1
- realign/commands/auto_commit.py +1 -1
- realign/commands/commit.py +100 -0
- realign/commands/config.py +13 -12
- realign/commands/init.py +48 -16
- realign/commands/search.py +57 -31
- realign/commands/session_utils.py +28 -0
- realign/commands/show.py +25 -38
- realign/file_lock.py +120 -0
- realign/hooks.py +362 -49
- realign/mcp_server.py +4 -54
- realign/mcp_watcher.py +356 -253
- aline_ai-0.1.10.dist-info/RECORD +0 -23
- {aline_ai-0.1.10.dist-info → aline_ai-0.2.1.dist-info}/WHEEL +0 -0
- {aline_ai-0.1.10.dist-info → aline_ai-0.2.1.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.1.10.dist-info → aline_ai-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.1.10.dist-info → aline_ai-0.2.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
aline_ai-0.2.1.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
|
|
2
|
+
realign/__init__.py,sha256=sNvIfQwzqITXLdabCJUP5PXKHVVX4aRdXM2HnStJpuY,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=kJXtmATg627dR_5ytER-lyGJwWCgfoANUjIg7gNgAJE,48724
|
|
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=52WkcgdTdsvYXYOuTQ0zR1QF6QOWsjWOuu0f8vLhbL4,13452
|
|
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.1.dist-info/METADATA,sha256=oG0lXeu_ljDF-lXinBIrfA_Hhk-fbxNG9KqKnw7V8vw,1398
|
|
22
|
+
aline_ai-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
aline_ai-0.2.1.dist-info/entry_points.txt,sha256=h-NocHDzSueXfsepHTIdRPNQzhNZQPAztJfldd-mQTE,202
|
|
24
|
+
aline_ai-0.2.1.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
|
|
25
|
+
aline_ai-0.2.1.dist-info/RECORD,,
|
realign/__init__.py
CHANGED
realign/commands/auto_commit.py
CHANGED
|
@@ -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
|
|
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:
|
realign/commands/commit.py
CHANGED
|
@@ -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)}"
|
realign/commands/config.py
CHANGED
|
@@ -30,12 +30,12 @@ def config_command(
|
|
|
30
30
|
|
|
31
31
|
Examples:
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 '
|
|
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 '
|
|
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:
|
|
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,24 +103,32 @@ 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
|
-
# Create .gitignore for sessions
|
|
122
|
+
# Create .gitignore for sessions and metadata
|
|
119
123
|
gitignore_path = realign_dir / ".gitignore"
|
|
120
124
|
if not gitignore_path.exists():
|
|
121
|
-
|
|
125
|
+
gitignore_content = (
|
|
126
|
+
"# Uncomment to ignore session files\n"
|
|
127
|
+
"# sessions/\n\n"
|
|
128
|
+
"# Ignore metadata files (used internally to prevent duplicate processing)\n"
|
|
129
|
+
".metadata/\n"
|
|
130
|
+
)
|
|
131
|
+
gitignore_path.write_text(gitignore_content, encoding="utf-8")
|
|
122
132
|
|
|
123
133
|
# Backup and set core.hooksPath
|
|
124
134
|
backup_file = realign_dir / "backup_hook_config.yaml"
|
|
@@ -193,13 +203,18 @@ def init_repository(
|
|
|
193
203
|
def init_command(
|
|
194
204
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts (deprecated, now default)"),
|
|
195
205
|
skip_commit: bool = typer.Option(False, "--skip-commit", help="Skip auto-commit of hooks"),
|
|
206
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force re-initialization and update hooks even if they exist"),
|
|
196
207
|
):
|
|
197
|
-
"""Initialize ReAlign in the current git repository.
|
|
208
|
+
"""Initialize ReAlign in the current git repository.
|
|
209
|
+
|
|
210
|
+
Use --force to re-initialize and update hooks to the latest version.
|
|
211
|
+
"""
|
|
198
212
|
# Call the core function
|
|
199
213
|
result = init_repository(
|
|
200
214
|
repo_path=".",
|
|
201
215
|
auto_init_git=True,
|
|
202
216
|
skip_commit=skip_commit,
|
|
217
|
+
force=force,
|
|
203
218
|
)
|
|
204
219
|
|
|
205
220
|
# Print detailed results
|
|
@@ -218,8 +233,9 @@ def init_command(
|
|
|
218
233
|
console.print(f" Config Path: [cyan]{result.get('config_path', 'N/A')}[/cyan]")
|
|
219
234
|
console.print(f" History Directory: [cyan]{result.get('history_dir', 'N/A')}[/cyan]")
|
|
220
235
|
console.print(f" Git Initialized: [cyan]{result.get('git_initialized', False)}[/cyan]")
|
|
236
|
+
console.print(f" Force Reinit: [cyan]{force}[/cyan]")
|
|
221
237
|
console.print(f" Skip Commit: [cyan]{skip_commit}[/cyan]")
|
|
222
|
-
console.print(f" Hooks
|
|
238
|
+
console.print(f" Hooks: [cyan]{', '.join(result.get('hooks_created', [])) or 'None'}[/cyan]")
|
|
223
239
|
console.print(f" Auto-committed: [cyan]{result.get('committed', False)}[/cyan]")
|
|
224
240
|
|
|
225
241
|
if result.get("errors"):
|
|
@@ -246,22 +262,30 @@ def get_pre_commit_hook() -> str:
|
|
|
246
262
|
# ReAlign pre-commit hook
|
|
247
263
|
# Finds and stages agent session files before commit
|
|
248
264
|
|
|
249
|
-
# 1. Try
|
|
265
|
+
# 1. Try direct Python execution first (development mode - highest priority for dev machines)
|
|
266
|
+
# Check if realign module is importable (e.g., installed in editable mode or in PYTHONPATH)
|
|
267
|
+
if python -c "import realign.hooks" 2>/dev/null; then
|
|
268
|
+
echo "Aline pre-commit hook (dev-mode)" >&2
|
|
269
|
+
exec python -m realign.hooks --pre-commit "$@"
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
# 2. Try to find aline-hook-pre-commit in PATH (pipx/pip installations)
|
|
250
273
|
if command -v aline-hook-pre-commit >/dev/null 2>&1; then
|
|
251
274
|
VERSION=$(aline version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
|
|
252
|
-
echo "Aline pre-commit hook (
|
|
275
|
+
echo "Aline pre-commit hook (cli-$VERSION)" >&2
|
|
253
276
|
exec aline-hook-pre-commit "$@"
|
|
254
277
|
fi
|
|
255
278
|
|
|
256
|
-
#
|
|
279
|
+
# 3. Try using uvx (for MCP installations where command is in uvx cache)
|
|
257
280
|
if command -v uvx >/dev/null 2>&1; then
|
|
258
281
|
VERSION=$(uvx --from aline-ai aline version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
|
|
259
|
-
echo "Aline pre-commit hook (
|
|
282
|
+
echo "Aline pre-commit hook (mcp-$VERSION)" >&2
|
|
260
283
|
exec uvx --from aline-ai aline-hook-pre-commit "$@"
|
|
261
284
|
fi
|
|
262
285
|
|
|
263
286
|
# If all else fails, print an error
|
|
264
287
|
echo "Error: Cannot find aline. Please ensure it's installed:" >&2
|
|
288
|
+
echo " - For dev: Ensure 'python -m realign.hooks' works (editable install or PYTHONPATH)" >&2
|
|
265
289
|
echo " - For CLI: pipx install aline-ai" >&2
|
|
266
290
|
echo " - For MCP: Ensure uvx is available" >&2
|
|
267
291
|
exit 1
|
|
@@ -282,22 +306,30 @@ if [ "$COMMIT_SOURCE" = "merge" ] || [ "$COMMIT_SOURCE" = "squash" ] || [ "$COMM
|
|
|
282
306
|
exit 0
|
|
283
307
|
fi
|
|
284
308
|
|
|
285
|
-
# 1. Try
|
|
309
|
+
# 1. Try direct Python execution first (development mode - highest priority for dev machines)
|
|
310
|
+
# Check if realign module is importable (e.g., installed in editable mode or in PYTHONPATH)
|
|
311
|
+
if python -c "import realign.hooks" 2>/dev/null; then
|
|
312
|
+
echo "Aline prepare-commit-msg hook (dev-mode)" >&2
|
|
313
|
+
exec python -m realign.hooks --prepare-commit-msg "$@"
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
# 2. Try to find aline-hook-prepare-commit-msg in PATH (pipx/pip installations)
|
|
286
317
|
if command -v aline-hook-prepare-commit-msg >/dev/null 2>&1; then
|
|
287
318
|
VERSION=$(aline version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
|
|
288
|
-
echo "Aline prepare-commit-msg hook (
|
|
319
|
+
echo "Aline prepare-commit-msg hook (cli-$VERSION)" >&2
|
|
289
320
|
exec aline-hook-prepare-commit-msg "$@"
|
|
290
321
|
fi
|
|
291
322
|
|
|
292
|
-
#
|
|
323
|
+
# 3. Try using uvx (for MCP installations where command is in uvx cache)
|
|
293
324
|
if command -v uvx >/dev/null 2>&1; then
|
|
294
325
|
VERSION=$(uvx --from aline-ai aline version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' || echo "unknown")
|
|
295
|
-
echo "Aline prepare-commit-msg hook (
|
|
326
|
+
echo "Aline prepare-commit-msg hook (mcp-$VERSION)" >&2
|
|
296
327
|
exec uvx --from aline-ai aline-hook-prepare-commit-msg "$@"
|
|
297
328
|
fi
|
|
298
329
|
|
|
299
330
|
# If all else fails, print an error
|
|
300
331
|
echo "Error: Cannot find aline. Please ensure it's installed:" >&2
|
|
332
|
+
echo " - For dev: Ensure 'python -m realign.hooks' works (editable install or PYTHONPATH)" >&2
|
|
301
333
|
echo " - For CLI: pipx install aline-ai" >&2
|
|
302
334
|
echo " - For MCP: Ensure uvx is available" >&2
|
|
303
335
|
exit 1
|
realign/commands/search.py
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
96
|
-
"
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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", "
|
|
121
|
+
["git", "show", f"{commit}:{session_path}"],
|
|
106
122
|
capture_output=True,
|
|
107
123
|
text=True,
|
|
108
|
-
check=
|
|
124
|
+
check=False,
|
|
109
125
|
cwd=repo_root,
|
|
110
126
|
)
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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:
|