aline-ai 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aline-ai
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -1,14 +1,14 @@
1
- aline_ai-0.2.1.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
- realign/__init__.py,sha256=sNvIfQwzqITXLdabCJUP5PXKHVVX4aRdXM2HnStJpuY,68
1
+ aline_ai-0.2.2.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
+ realign/__init__.py,sha256=f1FalM6Y9iEIqHcRYlPxgxYsNHpJAHkmJtFz4WPRUeU,68
3
3
  realign/claude_detector.py,sha256=NLxI0zJWcqNxNha9jAy9AslTMwHKakCc9yPGdkrbiFE,3028
4
4
  realign/cli.py,sha256=bkwS329jMDEkrUEihXRN2DDyeTKE6HbAysoDxxskZ8g,941
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=kJXtmATg627dR_5ytER-lyGJwWCgfoANUjIg7gNgAJE,48724
8
+ realign/hooks.py,sha256=80x7kF7t_D2D0TWM4whCU2Id3ItgkPXeREKkz1jfL_c,48751
9
9
  realign/logging_config.py,sha256=KvkKktF-bkUu031y9vgUoHpsbnOw7ud25jhpzliNZwA,4929
10
10
  realign/mcp_server.py,sha256=dntFatMpozI80K5hHrIiQ9sviC6ARKTP89goULhi1T4,16477
11
- realign/mcp_watcher.py,sha256=D6qVM0yD2hQPxrD_HnHkDob78_dqK96klHIogh48cw4,23971
11
+ realign/mcp_watcher.py,sha256=ffYOXDLuf9T6Kab3CdGNAOY3DBlAbjZrVrSjM5RdYGU,26828
12
12
  realign/redactor.py,sha256=uZvLKKGrRGJm-qM8S4XJyJK6i0CSSby_wbKiay7VGJw,8148
13
13
  realign/commands/__init__.py,sha256=GG6IMw6fUBQAXGJDFJvOOQgv6pkiRSfMh8z3AYXTyRM,31
14
14
  realign/commands/auto_commit.py,sha256=jgjAYZHqN34NmQkncZg3Vtwsl3MyAlsvucxEBwUj7ko,7450
@@ -18,8 +18,8 @@ realign/commands/init.py,sha256=52WkcgdTdsvYXYOuTQ0zR1QF6QOWsjWOuu0f8vLhbL4,1345
18
18
  realign/commands/search.py,sha256=xTWuX0lpjQPX8cen0ewl-BNF0FeWgjMwN06bdeesED8,18770
19
19
  realign/commands/session_utils.py,sha256=L1DwZIGCOBirp6tkAswACJEeDa6i9aAAfsialAs4rRY,864
20
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,,
21
+ aline_ai-0.2.2.dist-info/METADATA,sha256=Eck_4ZFdzNnAcomzVJ5aoK4XgwPKpBmh8NiAzJfN2RE,1398
22
+ aline_ai-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ aline_ai-0.2.2.dist-info/entry_points.txt,sha256=h-NocHDzSueXfsepHTIdRPNQzhNZQPAztJfldd-mQTE,202
24
+ aline_ai-0.2.2.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
25
+ aline_ai-0.2.2.dist-info/RECORD,,
realign/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Aline - AI Agent Chat Session Tracker."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
realign/hooks.py CHANGED
@@ -311,6 +311,10 @@ def find_latest_session(history_path: Path, explicit_path: Optional[str] = None)
311
311
  """
312
312
  Find the most recent session file.
313
313
 
314
+ Filters out Claude Code agent sessions (agent-*.jsonl) since they are
315
+ sub-tasks of main sessions and their results are already incorporated
316
+ into the main session files.
317
+
314
318
  Args:
315
319
  history_path: Path to history directory or a specific session file (for Codex)
316
320
  explicit_path: Explicit path to a session file (overrides history_path)
@@ -337,7 +341,13 @@ def find_latest_session(history_path: Path, explicit_path: Optional[str] = None)
337
341
  # Otherwise, search directory for session files
338
342
  session_files = []
339
343
  for pattern in ["*.json", "*.jsonl"]:
340
- session_files.extend(history_path.glob(pattern))
344
+ for file in history_path.glob(pattern):
345
+ # Filter out Claude Code agent sessions (agent-*.jsonl)
346
+ # These are sub-tasks whose results are already in main sessions
347
+ if file.name.startswith("agent-"):
348
+ logger.debug(f"Skipping agent session: {file.name}")
349
+ continue
350
+ session_files.append(file)
341
351
 
342
352
  if not session_files:
343
353
  return None
@@ -1045,20 +1055,10 @@ def process_sessions(
1045
1055
  summary_model_label: Optional[str] = None
1046
1056
 
1047
1057
  for session_relpath in session_relpaths:
1048
- # Check if this session was already processed in pre-commit hook
1049
- previous_metadata = get_session_metadata(repo_root, session_relpath)
1058
+ # Extract NEW content using git diff (compares staged content with HEAD)
1059
+ # This correctly detects new content even if the file hasn't grown since pre-commit
1060
+ # (which happens in auto-commit scenarios where the AI has finished responding)
1050
1061
  current_size = session_metadata_map.get(session_relpath, 0)
1051
-
1052
- if previous_metadata:
1053
- previous_size = previous_metadata.get("content_size", 0)
1054
- if previous_size == current_size:
1055
- logger.info(f"Session {session_relpath} unchanged since pre-commit (size: {current_size}), skipping")
1056
- print(f"⏭️ Skipping {Path(session_relpath).name} (no new content since pre-commit)", file=sys.stderr)
1057
- continue
1058
- else:
1059
- logger.info(f"Session {session_relpath} size changed: {previous_size} -> {current_size}")
1060
-
1061
- # Extract NEW content using git diff
1062
1062
  new_content = get_new_content_from_git_diff(repo_root, session_relpath)
1063
1063
 
1064
1064
  if not new_content or not new_content.strip():
realign/mcp_watcher.py CHANGED
@@ -14,6 +14,10 @@ from datetime import datetime
14
14
 
15
15
  from .config import ReAlignConfig
16
16
  from .hooks import find_all_active_sessions
17
+ from .logging_config import setup_logger
18
+
19
+ # Initialize logger for watcher
20
+ logger = setup_logger('realign.mcp_watcher', 'mcp_watcher.log')
17
21
 
18
22
 
19
23
  # Session type detection
@@ -38,17 +42,24 @@ class DialogueWatcher:
38
42
  async def start(self):
39
43
  """Start watching session files."""
40
44
  if not self.config.mcp_auto_commit:
45
+ logger.info("Auto-commit disabled in config")
41
46
  print("[MCP Watcher] Auto-commit disabled in config", file=sys.stderr)
42
47
  return
43
48
 
44
49
  self.running = True
50
+ logger.info("Started watching for dialogue completion")
51
+ logger.info(f"Mode: Per-request (triggers at end of each AI response)")
52
+ logger.info(f"Supports: Claude Code & Codex (auto-detected)")
53
+ logger.info(f"Debounce: {self.debounce_delay}s, Cooldown: {self.min_commit_interval}s")
45
54
  print("[MCP Watcher] Started watching for dialogue completion", file=sys.stderr)
46
55
  print(f"[MCP Watcher] Mode: Per-request (triggers at end of each AI response)", file=sys.stderr)
47
56
  print(f"[MCP Watcher] Supports: Claude Code & Codex (auto-detected)", file=sys.stderr)
48
57
  print(f"[MCP Watcher] Debounce: {self.debounce_delay}s, Cooldown: {self.min_commit_interval}s", file=sys.stderr)
49
58
  if self.project_path:
59
+ logger.info(f"Monitoring project: {self.project_path}")
50
60
  print(f"[MCP Watcher] Monitoring project: {self.project_path}", file=sys.stderr)
51
61
  else:
62
+ logger.info("Project path unknown, falling back to multi-project scan")
52
63
  print("[MCP Watcher] Project path unknown, falling back to multi-project scan", file=sys.stderr)
53
64
 
54
65
  # Initialize baseline sizes and stop_reason counts
@@ -61,6 +72,7 @@ class DialogueWatcher:
61
72
  await self.check_for_changes()
62
73
  await asyncio.sleep(0.5) # Check every 0.5 seconds for responsiveness
63
74
  except Exception as e:
75
+ logger.error(f"Error in check loop: {e}", exc_info=True)
64
76
  print(f"[MCP Watcher] Error: {e}", file=sys.stderr)
65
77
  await asyncio.sleep(1.0)
66
78
 
@@ -69,6 +81,7 @@ class DialogueWatcher:
69
81
  self.running = False
70
82
  if self.pending_commit_task:
71
83
  self.pending_commit_task.cancel()
84
+ logger.info("Watcher stopped")
72
85
  print("[MCP Watcher] Stopped", file=sys.stderr)
73
86
 
74
87
  def _get_session_sizes(self) -> Dict[str, int]:
@@ -82,7 +95,9 @@ class DialogueWatcher:
82
95
  for session_file in session_files:
83
96
  if session_file.exists():
84
97
  sizes[str(session_file)] = session_file.stat().st_size
98
+ logger.debug(f"Tracked {len(sizes)} session file(s)")
85
99
  except Exception as e:
100
+ logger.error(f"Error getting session sizes: {e}", exc_info=True)
86
101
  print(f"[MCP Watcher] Error getting session sizes: {e}", file=sys.stderr)
87
102
  return sizes
88
103
 
@@ -239,8 +254,11 @@ class DialogueWatcher:
239
254
  """
240
255
  Count complete dialogue turns for Claude Code sessions.
241
256
 
242
- Uses stop_reason='end_turn' as the marker, with message ID deduplication
257
+ Uses stop_reason in ('end_turn', 'tool_use') as the marker, with message ID deduplication
243
258
  to handle Claude Code's incremental writes (thinking first, then full content).
259
+
260
+ Note: In Claude Code, most responses have stop_reason='tool_use' because the AI
261
+ frequently uses tools. We count both 'end_turn' and 'tool_use' as complete turns.
244
262
  """
245
263
  unique_message_ids = set()
246
264
  try:
@@ -252,8 +270,11 @@ class DialogueWatcher:
252
270
  try:
253
271
  data = json.loads(line)
254
272
  message = data.get("message", {})
255
- # Only count end_turn stop reasons (ignore tool_use, etc.)
256
- if message.get("stop_reason") == "end_turn":
273
+ stop_reason = message.get("stop_reason")
274
+ # Count both end_turn and tool_use as complete turns
275
+ # tool_use indicates the AI used tools (most common in Claude Code)
276
+ # end_turn indicates a simple response without tools
277
+ if stop_reason in ("end_turn", "tool_use"):
257
278
  # Deduplicate by message ID to handle incremental writes
258
279
  msg_id = message.get("id")
259
280
  if msg_id:
@@ -264,6 +285,7 @@ class DialogueWatcher:
264
285
  except json.JSONDecodeError:
265
286
  continue
266
287
  except Exception as e:
288
+ logger.error(f"Error counting Claude turns in {session_file}: {e}", exc_info=True)
267
289
  print(f"[MCP Watcher] Error counting Claude turns in {session_file}: {e}", file=sys.stderr)
268
290
  return len(unique_message_ids)
269
291
 
@@ -308,10 +330,13 @@ class DialogueWatcher:
308
330
  old_size = self.last_session_sizes.get(path, 0)
309
331
  if size > old_size:
310
332
  changed_files.append(Path(path))
333
+ logger.debug(f"Session file changed: {Path(path).name} ({old_size} -> {size} bytes)")
311
334
 
312
335
  if changed_files:
336
+ logger.info(f"Detected changes in {len(changed_files)} session file(s), scheduling commit check")
313
337
  # File changed - cancel any pending commit and schedule a new one
314
338
  if self.pending_commit_task:
339
+ logger.debug("Cancelling pending commit task")
315
340
  self.pending_commit_task.cancel()
316
341
 
317
342
  # Wait for debounce period to ensure the turn is complete
@@ -323,6 +348,7 @@ class DialogueWatcher:
323
348
  self.last_session_sizes = current_sizes
324
349
 
325
350
  except Exception as e:
351
+ logger.error(f"Error checking for changes: {e}", exc_info=True)
326
352
  print(f"[MCP Watcher] Error checking for changes: {e}", file=sys.stderr)
327
353
 
328
354
  async def _debounced_commit(self, changed_files: list):
@@ -337,6 +363,7 @@ class DialogueWatcher:
337
363
  for session_file in changed_files:
338
364
  if await self._check_if_turn_complete(session_file):
339
365
  session_to_commit = session_file
366
+ logger.info(f"Complete turn detected in {session_file.name}")
340
367
  print(f"[MCP Watcher] Complete turn detected in {session_file.name}", file=sys.stderr)
341
368
  break
342
369
 
@@ -344,6 +371,7 @@ class DialogueWatcher:
344
371
  # Extract project path from the session file
345
372
  project_path = self._extract_project_path(session_to_commit)
346
373
  if not project_path:
374
+ logger.warning(f"Could not determine project path for {session_to_commit.name}, skipping commit")
347
375
  print(f"[MCP Watcher] Could not determine project path for {session_to_commit.name}, skipping commit", file=sys.stderr)
348
376
  return
349
377
 
@@ -354,10 +382,12 @@ class DialogueWatcher:
354
382
  if last_commit_time:
355
383
  time_since_last = current_time - last_commit_time
356
384
  if time_since_last < self.min_commit_interval:
385
+ logger.info(f"Skipping commit for {project_path.name} (cooldown: {time_since_last:.1f}s < {self.min_commit_interval}s)")
357
386
  print(f"[MCP Watcher] Skipping commit for {project_path.name} (cooldown: {time_since_last:.1f}s < {self.min_commit_interval}s)", file=sys.stderr)
358
387
  return
359
388
 
360
389
  # Perform commit for this project
390
+ logger.info(f"Triggering commit for {project_path.name}")
361
391
  await self._do_commit(project_path, session_to_commit)
362
392
 
363
393
  except asyncio.CancelledError:
@@ -390,6 +420,7 @@ class DialogueWatcher:
390
420
 
391
421
  # Commit after each complete assistant response (1 new turn)
392
422
  if new_turns >= 1:
423
+ logger.info(f"Detected {new_turns} new turn(s) in {session_file.name} ({session_type})")
393
424
  print(f"[MCP Watcher] Detected {new_turns} new turn(s) in {session_file.name} ({session_type})", file=sys.stderr)
394
425
  # Update baseline immediately to avoid double-counting
395
426
  self.last_stop_reason_counts[session_path] = current_count
@@ -398,6 +429,7 @@ class DialogueWatcher:
398
429
  return False
399
430
 
400
431
  except Exception as e:
432
+ logger.error(f"Error checking turn completion: {e}", exc_info=True)
401
433
  print(f"[MCP Watcher] Error checking turn completion: {e}", file=sys.stderr)
402
434
  return False
403
435
 
@@ -423,12 +455,16 @@ class DialogueWatcher:
423
455
  )
424
456
 
425
457
  if result:
458
+ logger.info(f"✓ Committed to {project_path.name}: {message}")
426
459
  print(f"[MCP Watcher] ✓ Committed to {project_path.name}: {message}", file=sys.stderr)
427
460
  # Update last commit time for this project
428
461
  self.last_commit_times[str(project_path)] = time.time()
429
462
  # Baseline counts already updated in _check_if_turn_complete()
463
+ else:
464
+ logger.warning(f"Commit failed for {project_path.name}")
430
465
 
431
466
  except Exception as e:
467
+ logger.error(f"Error during commit for {project_path}: {e}", exc_info=True)
432
468
  print(f"[MCP Watcher] Error during commit for {project_path}: {e}", file=sys.stderr)
433
469
 
434
470
  def _run_realign_commit(self, project_path: Path, message: str) -> bool:
@@ -562,11 +598,15 @@ async def start_watcher():
562
598
  global _watcher
563
599
 
564
600
  if _watcher and _watcher.running:
601
+ logger.info("Watcher already running, skipping start")
565
602
  print("[MCP Watcher] Already running", file=sys.stderr)
566
603
  return
567
604
 
605
+ logger.info("Initializing global watcher instance")
568
606
  _watcher = DialogueWatcher()
607
+ logger.info("Starting watcher task")
569
608
  asyncio.create_task(_watcher.start())
609
+ logger.info("Watcher task created")
570
610
 
571
611
 
572
612
  async def stop_watcher():