aline-ai 0.2.5__py3-none-any.whl → 0.2.6__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.2.5.dist-info → aline_ai-0.2.6.dist-info}/METADATA +1 -1
- {aline_ai-0.2.5.dist-info → aline_ai-0.2.6.dist-info}/RECORD +10 -10
- realign/__init__.py +1 -1
- realign/commands/auto_commit.py +17 -6
- realign/hooks.py +337 -57
- realign/mcp_watcher.py +4 -5
- {aline_ai-0.2.5.dist-info → aline_ai-0.2.6.dist-info}/WHEEL +0 -0
- {aline_ai-0.2.5.dist-info → aline_ai-0.2.6.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.2.5.dist-info → aline_ai-0.2.6.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.2.5.dist-info → aline_ai-0.2.6.dist-info}/top_level.txt +0 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
aline_ai-0.2.
|
|
2
|
-
realign/__init__.py,sha256=
|
|
1
|
+
aline_ai-0.2.6.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
|
|
2
|
+
realign/__init__.py,sha256=kYl1kB-KlgNv4t7nc-BFE8PSabtXYIzbte8yU4ZaTvY,68
|
|
3
3
|
realign/claude_detector.py,sha256=NLxI0zJWcqNxNha9jAy9AslTMwHKakCc9yPGdkrbiFE,3028
|
|
4
4
|
realign/cli.py,sha256=WSdT9zpHLswBeDPjb5M_U2cvhIm7yrgnXPDyb7vD-jg,2798
|
|
5
5
|
realign/codex_detector.py,sha256=RI3JbZgebrhoqpRfTBMfclYCAISN7hZAHVW3bgftJpU,4428
|
|
6
6
|
realign/config.py,sha256=jarinbr0mA6e5DmgY19b_VpMnxk6SOYTwyvB9luq0ww,7207
|
|
7
7
|
realign/file_lock.py,sha256=-9c3tMdMj_ZxmasK5y6hV9Gfo6KDsSO3Q7PXiTBhsu4,3369
|
|
8
|
-
realign/hooks.py,sha256=
|
|
8
|
+
realign/hooks.py,sha256=ZckwB_W7nx5WO_JfyqXP78peL-HDiefoZWE43yBhRtI,63854
|
|
9
9
|
realign/logging_config.py,sha256=KvkKktF-bkUu031y9vgUoHpsbnOw7ud25jhpzliNZwA,4929
|
|
10
10
|
realign/mcp_server.py,sha256=Q82nuEm6LF17eKUZfHHt6exQjzOWbbBmGXLYwNIGnoo,21002
|
|
11
|
-
realign/mcp_watcher.py,sha256=
|
|
11
|
+
realign/mcp_watcher.py,sha256=XmpaX5X8Dm91RCZKC5PoGPNaF1ssUcpRKOe4zWPJ-0A,26751
|
|
12
12
|
realign/redactor.py,sha256=FizaGSdW-QTBAQl4h-gtmMpx2mFrfd2a5DoPEPyLfRg,9989
|
|
13
13
|
realign/commands/__init__.py,sha256=caHulsUeguKyy2ZIIa9hVwzGwNHfIbeHwZIC67C8gnI,213
|
|
14
|
-
realign/commands/auto_commit.py,sha256=
|
|
14
|
+
realign/commands/auto_commit.py,sha256=3QMlUerXlIJgiwN1BKH6U--qwEqZupzoLPgyAivLr1A,7899
|
|
15
15
|
realign/commands/commit.py,sha256=mlwrv5nfTRY17WlcAdiJKKGh5uM7dGvT7sMxhdbsfkw,12605
|
|
16
16
|
realign/commands/config.py,sha256=iiu7usqw00djKZja5bx0iDH8DB0vU2maUPMkXLdgXwI,6609
|
|
17
17
|
realign/commands/hide.py,sha256=i_XLsmsB4duLKNIA-eRAUvniS4N6GdQUyfN92CGndEA,34138
|
|
@@ -21,8 +21,8 @@ realign/commands/review.py,sha256=3TY6F87RGNEbutxvUr_konr24-gCyj5fmD9usd3n4-U,15
|
|
|
21
21
|
realign/commands/search.py,sha256=xTWuX0lpjQPX8cen0ewl-BNF0FeWgjMwN06bdeesED8,18770
|
|
22
22
|
realign/commands/session_utils.py,sha256=L1DwZIGCOBirp6tkAswACJEeDa6i9aAAfsialAs4rRY,864
|
|
23
23
|
realign/commands/show.py,sha256=A9LvhOBcY6_HoI76irPB2rBOSgdftBuX2uZiO8IwNoU,16338
|
|
24
|
-
aline_ai-0.2.
|
|
25
|
-
aline_ai-0.2.
|
|
26
|
-
aline_ai-0.2.
|
|
27
|
-
aline_ai-0.2.
|
|
28
|
-
aline_ai-0.2.
|
|
24
|
+
aline_ai-0.2.6.dist-info/METADATA,sha256=UqZkpvO5NlU8KpL2KN7DezSmaWMTiS_Lrbcs-_xHmtg,1437
|
|
25
|
+
aline_ai-0.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
26
|
+
aline_ai-0.2.6.dist-info/entry_points.txt,sha256=h-NocHDzSueXfsepHTIdRPNQzhNZQPAztJfldd-mQTE,202
|
|
27
|
+
aline_ai-0.2.6.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
|
|
28
|
+
aline_ai-0.2.6.dist-info/RECORD,,
|
realign/__init__.py
CHANGED
realign/commands/auto_commit.py
CHANGED
|
@@ -85,10 +85,16 @@ def get_session_mtimes(repo_root: Path) -> dict:
|
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
def generate_commit_message() -> str:
|
|
88
|
-
"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
"""
|
|
89
|
+
Generate an automatic commit message.
|
|
90
|
+
|
|
91
|
+
DEPRECATED: This function is no longer used for auto-commits.
|
|
92
|
+
The commit message is now generated by the prepare-commit-msg hook
|
|
93
|
+
with LLM-based summaries and keywords.
|
|
94
|
+
|
|
95
|
+
Returns empty string to let the hook generate the message.
|
|
96
|
+
"""
|
|
97
|
+
return ""
|
|
92
98
|
|
|
93
99
|
|
|
94
100
|
def auto_commit_once(repo_root: Path, message: Optional[str] = None, silent: bool = False) -> bool:
|
|
@@ -97,12 +103,13 @@ def auto_commit_once(repo_root: Path, message: Optional[str] = None, silent: boo
|
|
|
97
103
|
|
|
98
104
|
Args:
|
|
99
105
|
repo_root: Path to the repository root
|
|
100
|
-
message: Custom commit message (
|
|
106
|
+
message: Custom commit message (if provided, will be used as-is and aline summary appended)
|
|
101
107
|
silent: If True, suppress console output (for watcher mode)
|
|
102
108
|
|
|
103
109
|
Returns:
|
|
104
110
|
True if commit was successful, False otherwise
|
|
105
111
|
"""
|
|
112
|
+
# If user provided a custom message, use it; otherwise let the hook generate it
|
|
106
113
|
commit_message = message or generate_commit_message()
|
|
107
114
|
|
|
108
115
|
try:
|
|
@@ -124,7 +131,11 @@ def auto_commit_once(repo_root: Path, message: Optional[str] = None, silent: boo
|
|
|
124
131
|
|
|
125
132
|
if result.returncode == 0:
|
|
126
133
|
if not silent:
|
|
127
|
-
|
|
134
|
+
# If message was provided by user, show it; otherwise indicate auto-commit
|
|
135
|
+
if message:
|
|
136
|
+
console.print(f"[green]✓[/green] Auto-committed: {message}")
|
|
137
|
+
else:
|
|
138
|
+
console.print("[green]✓[/green] Auto-committed successfully")
|
|
128
139
|
return True
|
|
129
140
|
else:
|
|
130
141
|
# Check if it's just "no changes" error
|
realign/hooks.py
CHANGED
|
@@ -356,6 +356,115 @@ def find_latest_session(history_path: Path, explicit_path: Optional[str] = None)
|
|
|
356
356
|
return max(session_files, key=lambda p: p.stat().st_mtime)
|
|
357
357
|
|
|
358
358
|
|
|
359
|
+
def filter_session_content(content: str) -> Tuple[str, str, str]:
|
|
360
|
+
"""
|
|
361
|
+
Filter session content to extract meaningful information for LLM summarization.
|
|
362
|
+
|
|
363
|
+
Filters out exploratory operations (Read, Grep, Glob) and technical details,
|
|
364
|
+
keeping only user requests, AI responses, and code changes.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
content: Raw text content of new session additions
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Tuple of (user_messages, assistant_replies, code_changes)
|
|
371
|
+
"""
|
|
372
|
+
if not content or not content.strip():
|
|
373
|
+
return "", "", ""
|
|
374
|
+
|
|
375
|
+
user_messages = []
|
|
376
|
+
assistant_replies = []
|
|
377
|
+
code_changes = []
|
|
378
|
+
|
|
379
|
+
lines = content.strip().split("\n")
|
|
380
|
+
|
|
381
|
+
for line in lines:
|
|
382
|
+
line = line.strip()
|
|
383
|
+
if not line:
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
obj = json.loads(line)
|
|
388
|
+
|
|
389
|
+
# Extract user messages and tool results
|
|
390
|
+
if obj.get("type") == "user":
|
|
391
|
+
msg = obj.get("message", {})
|
|
392
|
+
if isinstance(msg, dict):
|
|
393
|
+
content_data = msg.get("content", "")
|
|
394
|
+
if isinstance(content_data, str) and content_data.strip():
|
|
395
|
+
user_messages.append(content_data.strip())
|
|
396
|
+
elif isinstance(content_data, list):
|
|
397
|
+
# Extract text from content list
|
|
398
|
+
for item in content_data:
|
|
399
|
+
if isinstance(item, dict):
|
|
400
|
+
if item.get("type") == "text":
|
|
401
|
+
text = item.get("text", "").strip()
|
|
402
|
+
if text:
|
|
403
|
+
user_messages.append(text)
|
|
404
|
+
# Extract code changes from tool results
|
|
405
|
+
elif item.get("type") == "tool_result":
|
|
406
|
+
tool_use_result = obj.get("toolUseResult", {})
|
|
407
|
+
if "oldString" in tool_use_result and "newString" in tool_use_result:
|
|
408
|
+
# This is an Edit operation
|
|
409
|
+
new_string = tool_use_result.get("newString", "")
|
|
410
|
+
if new_string:
|
|
411
|
+
code_changes.append(f"Edit: {new_string[:300]}")
|
|
412
|
+
elif "content" in tool_use_result and "filePath" in tool_use_result:
|
|
413
|
+
# This is a Write operation
|
|
414
|
+
new_content = tool_use_result.get("content", "")
|
|
415
|
+
if new_content:
|
|
416
|
+
code_changes.append(f"Write: {new_content[:300]}")
|
|
417
|
+
|
|
418
|
+
# Extract assistant text replies (not tool use)
|
|
419
|
+
elif obj.get("type") == "assistant":
|
|
420
|
+
msg = obj.get("message", {})
|
|
421
|
+
if isinstance(msg, dict):
|
|
422
|
+
content_data = msg.get("content", [])
|
|
423
|
+
if isinstance(content_data, list):
|
|
424
|
+
for item in content_data:
|
|
425
|
+
if isinstance(item, dict):
|
|
426
|
+
# Only extract text blocks, skip tool_use blocks
|
|
427
|
+
if item.get("type") == "text":
|
|
428
|
+
text = item.get("text", "").strip()
|
|
429
|
+
if text:
|
|
430
|
+
assistant_replies.append(text)
|
|
431
|
+
# Extract code changes from Edit/Write tool uses
|
|
432
|
+
elif item.get("type") == "tool_use":
|
|
433
|
+
tool_name = item.get("name", "")
|
|
434
|
+
if tool_name in ("Edit", "Write"):
|
|
435
|
+
params = item.get("input", {})
|
|
436
|
+
if tool_name == "Edit":
|
|
437
|
+
new_string = params.get("new_string", "")
|
|
438
|
+
if new_string:
|
|
439
|
+
code_changes.append(f"Edit: {new_string[:200]}")
|
|
440
|
+
elif tool_name == "Write":
|
|
441
|
+
new_content = params.get("content", "")
|
|
442
|
+
if new_content:
|
|
443
|
+
code_changes.append(f"Write: {new_content[:200]}")
|
|
444
|
+
|
|
445
|
+
# Also handle simple role/content format (for compatibility)
|
|
446
|
+
elif obj.get("role") == "user":
|
|
447
|
+
content_text = obj.get("content", "")
|
|
448
|
+
if isinstance(content_text, str) and content_text.strip():
|
|
449
|
+
user_messages.append(content_text.strip())
|
|
450
|
+
|
|
451
|
+
elif obj.get("role") == "assistant":
|
|
452
|
+
content_text = obj.get("content", "")
|
|
453
|
+
if isinstance(content_text, str) and content_text.strip():
|
|
454
|
+
assistant_replies.append(content_text.strip())
|
|
455
|
+
|
|
456
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
457
|
+
# Not JSON or doesn't have expected structure, skip
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
# Join with newlines for better readability
|
|
461
|
+
user_str = "\n".join(user_messages) if user_messages else ""
|
|
462
|
+
assistant_str = "\n".join(assistant_replies) if assistant_replies else ""
|
|
463
|
+
code_str = "\n".join(code_changes) if code_changes else ""
|
|
464
|
+
|
|
465
|
+
return user_str, assistant_str, code_str
|
|
466
|
+
|
|
467
|
+
|
|
359
468
|
def simple_summarize(content: str, max_chars: int = 500) -> str:
|
|
360
469
|
"""
|
|
361
470
|
Generate a simple summary from new session content.
|
|
@@ -445,34 +554,71 @@ def generate_summary_with_llm(
|
|
|
445
554
|
content: str,
|
|
446
555
|
max_chars: int = 500,
|
|
447
556
|
provider: str = "auto"
|
|
448
|
-
) -> Tuple[Optional[str], Optional[str]]:
|
|
557
|
+
) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
449
558
|
"""
|
|
450
559
|
Generate summary using LLM (Anthropic Claude or OpenAI) for NEW content only.
|
|
451
|
-
Returns (
|
|
560
|
+
Returns (title, model_name, description) tuple, or (None, None, None) if LLM is unavailable.
|
|
452
561
|
|
|
453
562
|
Args:
|
|
454
563
|
content: Raw text content of new session additions
|
|
455
|
-
max_chars: Maximum characters in summary
|
|
564
|
+
max_chars: Maximum characters in summary (not used, kept for compatibility)
|
|
456
565
|
provider: LLM provider to use - "auto" (try Claude then OpenAI), "claude", or "openai"
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
Tuple of (title, model_name, description) where:
|
|
569
|
+
- title: One-line summary (max 150 chars)
|
|
570
|
+
- model_name: Name of the model used
|
|
571
|
+
- description: Detailed description of what happened (200-400 chars)
|
|
457
572
|
"""
|
|
458
|
-
logger.info(f"Attempting to generate LLM summary (provider: {provider}
|
|
573
|
+
logger.info(f"Attempting to generate LLM summary (provider: {provider})")
|
|
459
574
|
|
|
460
575
|
if not content or not content.strip():
|
|
461
576
|
logger.debug("No content provided for summarization")
|
|
462
|
-
return "No new content in this session", None
|
|
463
|
-
|
|
464
|
-
#
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
577
|
+
return "No new content in this session", None, ""
|
|
578
|
+
|
|
579
|
+
# Filter content to extract meaningful information
|
|
580
|
+
user_messages, assistant_replies, code_changes = filter_session_content(content)
|
|
581
|
+
|
|
582
|
+
# If no meaningful content after filtering, return early
|
|
583
|
+
if not user_messages and not assistant_replies and not code_changes:
|
|
584
|
+
logger.debug("No meaningful content after filtering")
|
|
585
|
+
return "Session update with no significant changes", None, "No significant changes detected in this session"
|
|
586
|
+
|
|
587
|
+
# System prompt for structured summarization
|
|
588
|
+
system_prompt = """You are a git commit message generator for AI chat sessions.
|
|
589
|
+
Analyze the conversation and code changes, then generate a summary in JSON format:
|
|
590
|
+
{
|
|
591
|
+
"title": "One-line summary (max 150 chars, imperative mood, like 'Add feature X' or 'Fix bug in Y'. NEVER truncate words - keep whole words only. Omit articles like 'the', 'a' when possible to save space)",
|
|
592
|
+
"description": "Detailed description of what happened in this session (200-400 chars). Focus on key actions, decisions, and outcomes. Don't worry about perfect grammar - clarity and completeness matter more. Include specific details like function names, features discussed, bugs fixed, etc."
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
IMPORTANT for description:
|
|
596
|
+
- Be specific and informative (200-400 characters)
|
|
597
|
+
- Focus on WHAT was accomplished and WHY, not HOW
|
|
598
|
+
- Include technical details: function names, module names, specific features
|
|
599
|
+
- Mention key decisions or discussions
|
|
600
|
+
- Don't worry about perfect grammar - focus on conveying the right meaning clearly
|
|
601
|
+
- Avoid mentioning tool names like 'Edit', 'Write', 'Read'
|
|
602
|
+
- For discussions without code: summarize the topics and conclusions
|
|
603
|
+
- For code changes: describe what was changed and the purpose
|
|
604
|
+
|
|
605
|
+
Return JSON only, no other text."""
|
|
606
|
+
|
|
607
|
+
# Build user prompt with filtered content
|
|
608
|
+
user_prompt_parts = ["Summarize this AI chat session:\n"]
|
|
609
|
+
|
|
610
|
+
if user_messages:
|
|
611
|
+
user_prompt_parts.append(f"User requests:\n{user_messages[:1500]}\n")
|
|
612
|
+
|
|
613
|
+
if assistant_replies:
|
|
614
|
+
user_prompt_parts.append(f"AI responses:\n{assistant_replies[:1500]}\n")
|
|
615
|
+
|
|
616
|
+
if code_changes:
|
|
617
|
+
user_prompt_parts.append(f"Code changes:\n{code_changes[:1500]}\n")
|
|
618
|
+
|
|
619
|
+
user_prompt_parts.append("\nReturn JSON only, no other text.")
|
|
620
|
+
|
|
621
|
+
user_prompt = "\n".join(user_prompt_parts)
|
|
476
622
|
|
|
477
623
|
# Determine which providers to try based on the provider parameter
|
|
478
624
|
try_claude = provider in ("auto", "claude")
|
|
@@ -493,7 +639,7 @@ def generate_summary_with_llm(
|
|
|
493
639
|
|
|
494
640
|
response = client.messages.create(
|
|
495
641
|
model="claude-3-5-haiku-20241022", # Fast and cost-effective
|
|
496
|
-
max_tokens=
|
|
642
|
+
max_tokens=300, # Increased for keywords
|
|
497
643
|
temperature=0.7,
|
|
498
644
|
system=system_prompt,
|
|
499
645
|
messages=[
|
|
@@ -505,17 +651,44 @@ def generate_summary_with_llm(
|
|
|
505
651
|
)
|
|
506
652
|
|
|
507
653
|
elapsed = time.time() - start_time
|
|
508
|
-
|
|
509
|
-
logger.info(f"Claude API success: {len(
|
|
510
|
-
logger.debug(f"Claude response: {
|
|
511
|
-
|
|
512
|
-
|
|
654
|
+
response_text = response.content[0].text.strip()
|
|
655
|
+
logger.info(f"Claude API success: {len(response_text)} chars in {elapsed:.2f}s")
|
|
656
|
+
logger.debug(f"Claude response: {response_text[:200]}...")
|
|
657
|
+
|
|
658
|
+
# Parse JSON response
|
|
659
|
+
try:
|
|
660
|
+
# Try to extract JSON if wrapped in markdown code blocks
|
|
661
|
+
if "```json" in response_text:
|
|
662
|
+
json_start = response_text.find("```json") + 7
|
|
663
|
+
json_end = response_text.find("```", json_start)
|
|
664
|
+
json_str = response_text[json_start:json_end].strip()
|
|
665
|
+
elif "```" in response_text:
|
|
666
|
+
json_start = response_text.find("```") + 3
|
|
667
|
+
json_end = response_text.find("```", json_start)
|
|
668
|
+
json_str = response_text[json_start:json_end].strip()
|
|
669
|
+
else:
|
|
670
|
+
json_str = response_text
|
|
671
|
+
|
|
672
|
+
summary_data = json.loads(json_str)
|
|
673
|
+
title = summary_data.get("title", "")[:150] # Enforce 150 char limit
|
|
674
|
+
description = summary_data.get("description", "")[:400] # Enforce 400 char limit
|
|
675
|
+
|
|
676
|
+
print(" ✅ Anthropic (Claude) summary successful", file=sys.stderr)
|
|
677
|
+
return title, "claude-3-5-haiku-20241022", description
|
|
678
|
+
|
|
679
|
+
except json.JSONDecodeError as e:
|
|
680
|
+
logger.warning(f"Failed to parse JSON from Claude response: {e}")
|
|
681
|
+
logger.debug(f"Raw response: {response_text}")
|
|
682
|
+
# Fallback: use first line as title, empty description
|
|
683
|
+
first_line = response_text.split("\n")[0][:150]
|
|
684
|
+
print(" ⚠️ Claude response was not valid JSON, using fallback", file=sys.stderr)
|
|
685
|
+
return first_line, "claude-3-5-haiku-20241022", ""
|
|
513
686
|
|
|
514
687
|
except ImportError:
|
|
515
688
|
logger.warning("Anthropic package not installed")
|
|
516
689
|
if provider == "claude":
|
|
517
690
|
print(" ❌ Anthropic package not installed", file=sys.stderr)
|
|
518
|
-
return None, None
|
|
691
|
+
return None, None, None
|
|
519
692
|
else:
|
|
520
693
|
print(" ❌ Anthropic package not installed, trying OpenAI...", file=sys.stderr)
|
|
521
694
|
except Exception as e:
|
|
@@ -531,7 +704,7 @@ def generate_summary_with_llm(
|
|
|
531
704
|
print(f" ❌ Anthropic quota/credit issue", file=sys.stderr)
|
|
532
705
|
else:
|
|
533
706
|
print(f" ❌ Anthropic API error: {e}", file=sys.stderr)
|
|
534
|
-
return None, None
|
|
707
|
+
return None, None, None
|
|
535
708
|
else:
|
|
536
709
|
# Auto mode: try falling back to OpenAI
|
|
537
710
|
if "authentication" in error_msg.lower() or "invalid" in error_msg.lower():
|
|
@@ -547,7 +720,7 @@ def generate_summary_with_llm(
|
|
|
547
720
|
logger.debug("ANTHROPIC_API_KEY not set")
|
|
548
721
|
if provider == "claude":
|
|
549
722
|
print(" ❌ ANTHROPIC_API_KEY not set", file=sys.stderr)
|
|
550
|
-
return None, None
|
|
723
|
+
return None, None, None
|
|
551
724
|
else:
|
|
552
725
|
print(" ⓘ ANTHROPIC_API_KEY not set, trying OpenAI...", file=sys.stderr)
|
|
553
726
|
|
|
@@ -576,21 +749,48 @@ def generate_summary_with_llm(
|
|
|
576
749
|
"content": user_prompt
|
|
577
750
|
}
|
|
578
751
|
],
|
|
579
|
-
max_tokens=
|
|
752
|
+
max_tokens=300, # Increased for keywords
|
|
580
753
|
temperature=0.7,
|
|
581
754
|
)
|
|
582
755
|
|
|
583
756
|
elapsed = time.time() - start_time
|
|
584
|
-
|
|
585
|
-
logger.info(f"OpenAI API success: {len(
|
|
586
|
-
logger.debug(f"OpenAI response: {
|
|
587
|
-
|
|
588
|
-
|
|
757
|
+
response_text = response.choices[0].message.content.strip()
|
|
758
|
+
logger.info(f"OpenAI API success: {len(response_text)} chars in {elapsed:.2f}s")
|
|
759
|
+
logger.debug(f"OpenAI response: {response_text[:200]}...")
|
|
760
|
+
|
|
761
|
+
# Parse JSON response
|
|
762
|
+
try:
|
|
763
|
+
# Try to extract JSON if wrapped in markdown code blocks
|
|
764
|
+
if "```json" in response_text:
|
|
765
|
+
json_start = response_text.find("```json") + 7
|
|
766
|
+
json_end = response_text.find("```", json_start)
|
|
767
|
+
json_str = response_text[json_start:json_end].strip()
|
|
768
|
+
elif "```" in response_text:
|
|
769
|
+
json_start = response_text.find("```") + 3
|
|
770
|
+
json_end = response_text.find("```", json_start)
|
|
771
|
+
json_str = response_text[json_start:json_end].strip()
|
|
772
|
+
else:
|
|
773
|
+
json_str = response_text
|
|
774
|
+
|
|
775
|
+
summary_data = json.loads(json_str)
|
|
776
|
+
title = summary_data.get("title", "")[:150] # Enforce 150 char limit
|
|
777
|
+
description = summary_data.get("description", "")[:400] # Enforce 400 char limit
|
|
778
|
+
|
|
779
|
+
print(" ✅ OpenAI (GPT) summary successful", file=sys.stderr)
|
|
780
|
+
return title, "gpt-3.5-turbo", description
|
|
781
|
+
|
|
782
|
+
except json.JSONDecodeError as e:
|
|
783
|
+
logger.warning(f"Failed to parse JSON from OpenAI response: {e}")
|
|
784
|
+
logger.debug(f"Raw response: {response_text}")
|
|
785
|
+
# Fallback: use first line as title, empty description
|
|
786
|
+
first_line = response_text.split("\n")[0][:150]
|
|
787
|
+
print(" ⚠️ OpenAI response was not valid JSON, using fallback", file=sys.stderr)
|
|
788
|
+
return first_line, "gpt-3.5-turbo", ""
|
|
589
789
|
|
|
590
790
|
except ImportError:
|
|
591
791
|
logger.warning("OpenAI package not installed")
|
|
592
792
|
print(" ❌ OpenAI package not installed", file=sys.stderr)
|
|
593
|
-
return None, None
|
|
793
|
+
return None, None, None
|
|
594
794
|
except Exception as e:
|
|
595
795
|
error_msg = str(e)
|
|
596
796
|
logger.error(f"OpenAI API error: {error_msg}", exc_info=True)
|
|
@@ -602,17 +802,17 @@ def generate_summary_with_llm(
|
|
|
602
802
|
print(f" ❌ OpenAI quota/billing issue", file=sys.stderr)
|
|
603
803
|
else:
|
|
604
804
|
print(f" ❌ OpenAI API error: {e}", file=sys.stderr)
|
|
605
|
-
return None, None
|
|
805
|
+
return None, None, None
|
|
606
806
|
elif try_openai:
|
|
607
807
|
logger.debug("OPENAI_API_KEY not set")
|
|
608
808
|
print(" ❌ OPENAI_API_KEY not set", file=sys.stderr)
|
|
609
|
-
return None, None
|
|
809
|
+
return None, None, None
|
|
610
810
|
|
|
611
811
|
# No API keys available or provider not configured
|
|
612
812
|
logger.warning(f"No LLM API keys available (provider: {provider})")
|
|
613
813
|
if provider == "auto":
|
|
614
814
|
print(" ❌ No LLM API keys configured", file=sys.stderr)
|
|
615
|
-
return None, None
|
|
815
|
+
return None, None, None
|
|
616
816
|
|
|
617
817
|
|
|
618
818
|
def generate_session_filename(user: str, agent: str = "claude") -> str:
|
|
@@ -694,6 +894,46 @@ def get_git_user() -> str:
|
|
|
694
894
|
return os.getenv("USER", "unknown")
|
|
695
895
|
|
|
696
896
|
|
|
897
|
+
def get_username(session_relpath: str = "") -> str:
|
|
898
|
+
"""
|
|
899
|
+
Get username for commit message.
|
|
900
|
+
|
|
901
|
+
Tries to get from git config first, then falls back to extracting
|
|
902
|
+
from session filename.
|
|
903
|
+
|
|
904
|
+
Args:
|
|
905
|
+
session_relpath: Relative path to session file (used for fallback)
|
|
906
|
+
|
|
907
|
+
Returns:
|
|
908
|
+
Username string
|
|
909
|
+
"""
|
|
910
|
+
# Try git config first
|
|
911
|
+
try:
|
|
912
|
+
result = subprocess.run(
|
|
913
|
+
["git", "config", "user.name"],
|
|
914
|
+
capture_output=True,
|
|
915
|
+
text=True,
|
|
916
|
+
check=True,
|
|
917
|
+
)
|
|
918
|
+
username = result.stdout.strip()
|
|
919
|
+
if username:
|
|
920
|
+
return username
|
|
921
|
+
except subprocess.CalledProcessError:
|
|
922
|
+
pass
|
|
923
|
+
|
|
924
|
+
# Fallback: extract from session filename
|
|
925
|
+
# Format: username_agent_hash.jsonl
|
|
926
|
+
if session_relpath:
|
|
927
|
+
filename = Path(session_relpath).name
|
|
928
|
+
parts = filename.split("_")
|
|
929
|
+
if len(parts) >= 3:
|
|
930
|
+
# First part is username
|
|
931
|
+
return parts[0]
|
|
932
|
+
|
|
933
|
+
# Final fallback
|
|
934
|
+
return os.getenv("USER", "unknown")
|
|
935
|
+
|
|
936
|
+
|
|
697
937
|
def copy_session_to_repo(
|
|
698
938
|
session_file: Path,
|
|
699
939
|
repo_root: Path,
|
|
@@ -1106,18 +1346,19 @@ def process_sessions(
|
|
|
1106
1346
|
continue
|
|
1107
1347
|
|
|
1108
1348
|
# Generate summary for NEW content only
|
|
1109
|
-
|
|
1349
|
+
summary_title: Optional[str] = None
|
|
1350
|
+
summary_description: str = ""
|
|
1110
1351
|
is_llm_summary = False
|
|
1111
1352
|
llm_model_name: Optional[str] = None
|
|
1112
1353
|
if config.use_LLM:
|
|
1113
1354
|
print(f"🤖 Attempting to generate LLM summary (provider: {config.llm_provider})...", file=sys.stderr)
|
|
1114
|
-
|
|
1355
|
+
summary_title, llm_model_name, summary_description = generate_summary_with_llm(
|
|
1115
1356
|
new_content,
|
|
1116
1357
|
config.summary_max_chars,
|
|
1117
1358
|
config.llm_provider
|
|
1118
1359
|
)
|
|
1119
1360
|
|
|
1120
|
-
if
|
|
1361
|
+
if summary_title:
|
|
1121
1362
|
print("✅ LLM summary generated successfully", file=sys.stderr)
|
|
1122
1363
|
is_llm_summary = True
|
|
1123
1364
|
if summary_model_label is None:
|
|
@@ -1126,23 +1367,26 @@ def process_sessions(
|
|
|
1126
1367
|
print("⚠️ LLM summary failed - falling back to local summarization", file=sys.stderr)
|
|
1127
1368
|
print(" Check your API keys: ANTHROPIC_API_KEY or OPENAI_API_KEY", file=sys.stderr)
|
|
1128
1369
|
|
|
1129
|
-
if not
|
|
1370
|
+
if not summary_title:
|
|
1130
1371
|
# Fallback to simple summarize
|
|
1131
1372
|
logger.info("Using local summarization (no LLM)")
|
|
1132
1373
|
print("📝 Using local summarization (no LLM)", file=sys.stderr)
|
|
1133
|
-
|
|
1374
|
+
summary_title = simple_summarize(new_content, config.summary_max_chars)
|
|
1375
|
+
summary_description = ""
|
|
1134
1376
|
|
|
1135
1377
|
# Identify agent type from filename
|
|
1136
1378
|
agent_name = detect_agent_from_session_path(session_relpath)
|
|
1137
1379
|
|
|
1138
|
-
|
|
1139
|
-
logger.debug(f"Summary for {session_relpath} ({agent_name}): {
|
|
1380
|
+
summary_title = summary_title.strip()
|
|
1381
|
+
logger.debug(f"Summary for {session_relpath} ({agent_name}): {summary_title[:100]}...")
|
|
1382
|
+
logger.debug(f"Description: {summary_description[:100]}...")
|
|
1140
1383
|
summary_entries.append({
|
|
1141
1384
|
"agent": agent_name,
|
|
1142
|
-
"
|
|
1385
|
+
"title": summary_title,
|
|
1386
|
+
"description": summary_description,
|
|
1143
1387
|
"source": "llm" if is_llm_summary else "local",
|
|
1144
1388
|
})
|
|
1145
|
-
legacy_summary_chunks.append(f"[{agent_name}] {
|
|
1389
|
+
legacy_summary_chunks.append(f"[{agent_name}] {summary_title}")
|
|
1146
1390
|
|
|
1147
1391
|
# Update metadata after successfully generating summary
|
|
1148
1392
|
save_session_metadata(repo_root, session_relpath, current_size)
|
|
@@ -1230,23 +1474,59 @@ def prepare_commit_msg_hook():
|
|
|
1230
1474
|
|
|
1231
1475
|
# Append summary to commit message
|
|
1232
1476
|
summary_entries = result.get("summary_entries") or []
|
|
1477
|
+
session_relpaths = result.get("session_relpaths") or []
|
|
1478
|
+
|
|
1233
1479
|
if summary_entries:
|
|
1234
1480
|
try:
|
|
1481
|
+
# Read existing commit message to check if user provided one
|
|
1482
|
+
with open(msg_file, "r", encoding="utf-8") as f:
|
|
1483
|
+
existing_msg = f.read().strip()
|
|
1484
|
+
|
|
1485
|
+
# Check if this is an auto-commit (empty message or only comments)
|
|
1486
|
+
is_auto_commit = not existing_msg or all(
|
|
1487
|
+
line.startswith("#") for line in existing_msg.split("\n") if line.strip()
|
|
1488
|
+
)
|
|
1489
|
+
|
|
1490
|
+
# Get username from first session path
|
|
1491
|
+
username = get_username(session_relpaths[0] if session_relpaths else "")
|
|
1492
|
+
|
|
1493
|
+
# Get timestamp
|
|
1494
|
+
from datetime import datetime
|
|
1495
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1496
|
+
|
|
1235
1497
|
with open(msg_file, "a", encoding="utf-8") as f:
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
f.write(f"
|
|
1245
|
-
|
|
1498
|
+
# Combine all summaries
|
|
1499
|
+
first_entry = summary_entries[0]
|
|
1500
|
+
title = first_entry.get("title", "Session update").strip()
|
|
1501
|
+
agent_label = first_entry.get("agent", "Unknown")
|
|
1502
|
+
description = first_entry.get("description", "").strip()
|
|
1503
|
+
|
|
1504
|
+
if is_auto_commit:
|
|
1505
|
+
# Auto-commit format: use aline summary as the main message
|
|
1506
|
+
f.write(f"aline: {title}\n\n")
|
|
1507
|
+
if description:
|
|
1508
|
+
f.write(f"{description}\n\n")
|
|
1509
|
+
f.write(f"Agent: {agent_label}\n")
|
|
1510
|
+
f.write(f"User: {username}\n")
|
|
1511
|
+
f.write(f"Timestamp: {timestamp}\n")
|
|
1512
|
+
else:
|
|
1513
|
+
# User-provided commit message format: append aline summary after separator
|
|
1514
|
+
f.write("\n\n---\n")
|
|
1515
|
+
f.write(f"aline: {title}\n\n")
|
|
1516
|
+
if description:
|
|
1517
|
+
f.write(f"{description}\n\n")
|
|
1518
|
+
f.write(f"Agent: {agent_label}\n")
|
|
1519
|
+
f.write(f"User: {username}\n")
|
|
1520
|
+
f.write(f"Timestamp: {timestamp}\n")
|
|
1521
|
+
|
|
1522
|
+
# Add redaction marker if applicable
|
|
1246
1523
|
if result.get("redacted"):
|
|
1247
|
-
f.write("
|
|
1524
|
+
f.write("Redacted: true\n")
|
|
1525
|
+
|
|
1248
1526
|
except Exception as e:
|
|
1249
1527
|
print(f"Warning: Could not append to commit message: {e}", file=sys.stderr)
|
|
1528
|
+
import traceback
|
|
1529
|
+
logger.error(f"Commit message formatting error: {traceback.format_exc()}")
|
|
1250
1530
|
|
|
1251
1531
|
sys.exit(0)
|
|
1252
1532
|
|
realign/mcp_watcher.py
CHANGED
|
@@ -442,9 +442,8 @@ class DialogueWatcher:
|
|
|
442
442
|
session_file: Session file that triggered the commit
|
|
443
443
|
"""
|
|
444
444
|
try:
|
|
445
|
-
#
|
|
446
|
-
|
|
447
|
-
message = f"chore: Auto-commit MCP session ({timestamp})"
|
|
445
|
+
# Use empty message to let prepare-commit-msg hook generate it with LLM summary
|
|
446
|
+
message = ""
|
|
448
447
|
|
|
449
448
|
# Use realign commit command
|
|
450
449
|
result = await asyncio.get_event_loop().run_in_executor(
|
|
@@ -455,8 +454,8 @@ class DialogueWatcher:
|
|
|
455
454
|
)
|
|
456
455
|
|
|
457
456
|
if result:
|
|
458
|
-
logger.info(f"✓ Committed to {project_path.name}
|
|
459
|
-
print(f"[MCP Watcher] ✓
|
|
457
|
+
logger.info(f"✓ Committed to {project_path.name}")
|
|
458
|
+
print(f"[MCP Watcher] ✓ Auto-committed to {project_path.name}", file=sys.stderr)
|
|
460
459
|
# Update last commit time for this project
|
|
461
460
|
self.last_commit_times[str(project_path)] = time.time()
|
|
462
461
|
# Baseline counts already updated in _check_if_turn_complete()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|