aline-ai 0.1.5__py3-none-any.whl → 0.1.7__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.5
3
+ Version: 0.1.7
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -1,13 +1,13 @@
1
- aline_ai-0.1.5.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
- realign/__init__.py,sha256=CDA16K-Mc8tSfIhf-tvPjqevV8EFzXGEK_DxhKlCZhs,68
1
+ aline_ai-0.1.7.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
+ realign/__init__.py,sha256=GAkpHLIQWfGF0lu7nkgDnXCA2GC4tJrr6dGTk8GMQB4,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/hooks.py,sha256=qhAeuln_62OgTq0vboZcUAuP2apOrNn58vSZqKwNmWQ,36456
8
8
  realign/logging_config.py,sha256=KvkKktF-bkUu031y9vgUoHpsbnOw7ud25jhpzliNZwA,4929
9
- realign/mcp_server.py,sha256=-LAJIsxN8U1VSzr-8TYhV9s2jC_t4_XdblnGAcbaKNk,17572
10
- realign/mcp_watcher.py,sha256=gqGlDWnKzH0hsqlf9LRobEPvfh4JoOz8kenvwZ5ICn8,14653
9
+ realign/mcp_server.py,sha256=srR1leOYbPyi6L0KALkOs94_djELi-SRLFQgrubQuhc,17760
10
+ realign/mcp_watcher.py,sha256=xL35Nz35YF6Mc9yu-nCbDzKzTFHQaYmtPNfyBzKKOhc,16038
11
11
  realign/redactor.py,sha256=uZvLKKGrRGJm-qM8S4XJyJK6i0CSSby_wbKiay7VGJw,8148
12
12
  realign/commands/__init__.py,sha256=GG6IMw6fUBQAXGJDFJvOOQgv6pkiRSfMh8z3AYXTyRM,31
13
13
  realign/commands/auto_commit.py,sha256=_DOw7nt9q3tD_Y3qDL9IFKAUG1hM4qH_xZ-9nyBc2Bc,7451
@@ -16,8 +16,8 @@ realign/commands/config.py,sha256=oarvn6UuGT8svd2h5_8M_ueV5QWOCUOn8SYoa4XYjs8,65
16
16
  realign/commands/init.py,sha256=EpSzh2Dd2EmEQ_wo3vAsg6Uq7_YOlQWIpzIkZa_2y0A,11863
17
17
  realign/commands/search.py,sha256=0CZaXll99wtd01MRiZk5NAblxgogc4RUAzMyJunvckE,18044
18
18
  realign/commands/show.py,sha256=P1waa94-AKJr9XjagkE40OHMXzE6IwC74DpeDKqwsqw,16693
19
- aline_ai-0.1.5.dist-info/METADATA,sha256=wdN5dpuh8P-2ioSkUdi6Lgj0Q9E2Iyq12dtvHGsIqMc,1398
20
- aline_ai-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- aline_ai-0.1.5.dist-info/entry_points.txt,sha256=h-NocHDzSueXfsepHTIdRPNQzhNZQPAztJfldd-mQTE,202
22
- aline_ai-0.1.5.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
23
- aline_ai-0.1.5.dist-info/RECORD,,
19
+ aline_ai-0.1.7.dist-info/METADATA,sha256=lgNJRQ4b1lDkDVvchqzD9VbvXB7bTkxgy9LgmmNaq34,1398
20
+ aline_ai-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ aline_ai-0.1.7.dist-info/entry_points.txt,sha256=h-NocHDzSueXfsepHTIdRPNQzhNZQPAztJfldd-mQTE,202
22
+ aline_ai-0.1.7.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
23
+ aline_ai-0.1.7.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.5"
3
+ __version__ = "0.1.7"
realign/mcp_server.py CHANGED
@@ -395,10 +395,26 @@ async def handle_version(args: dict) -> list[TextContent]:
395
395
  )]
396
396
 
397
397
 
398
+ def _server_log(msg: str):
399
+ """Log MCP server messages to both stderr and file."""
400
+ from datetime import datetime
401
+ timestamp = datetime.now().strftime("%H:%M:%S")
402
+ print(f"[MCP Server] {msg}", file=sys.stderr)
403
+
404
+ # Also write to the watcher log file for consistency
405
+ log_path = Path.home() / ".aline_watcher.log"
406
+ try:
407
+ with open(log_path, "a") as f:
408
+ f.write(f"[{timestamp}] [MCP Server] {msg}\n")
409
+ except Exception:
410
+ pass
411
+
412
+
398
413
  async def async_main():
399
414
  """Run the MCP server (async)."""
400
- print("[MCP Server] Starting Aline MCP server...", file=sys.stderr)
401
- print(f"[MCP Server] Current working directory: {Path.cwd()}", file=sys.stderr)
415
+ _server_log("Starting Aline MCP server...")
416
+ _server_log(f"Current working directory: {Path.cwd()}")
417
+ _server_log(f"Home directory: {Path.home()}")
402
418
 
403
419
  # Detect workspace path and start the watcher
404
420
  # Try multiple methods since MCP server may run in different context
@@ -414,52 +430,52 @@ async def async_main():
414
430
  )
415
431
  if result.returncode == 0:
416
432
  repo_path = Path(result.stdout.strip())
417
- print(f"[MCP Server] Detected workspace from git: {repo_path}", file=sys.stderr)
433
+ _server_log(f"Detected workspace from git: {repo_path}")
418
434
  else:
419
- print(f"[MCP Server] Not in git repo (cwd: {Path.cwd()})", file=sys.stderr)
435
+ _server_log(f"Not in git repo (cwd: {Path.cwd()})")
420
436
 
421
437
  # Method 2: If not in git repo, try to find from Claude session files
422
438
  if not repo_path:
423
439
  claude_projects = Path.home() / ".claude" / "projects"
424
- print(f"[MCP Server] Checking Claude projects at: {claude_projects}", file=sys.stderr)
440
+ _server_log(f"Checking Claude projects at: {claude_projects}")
425
441
  if claude_projects.exists():
426
442
  # Find most recently modified session file
427
443
  session_files = list(claude_projects.glob("*/*.jsonl"))
428
- print(f"[MCP Server] Found {len(session_files)} Claude session files", file=sys.stderr)
444
+ _server_log(f"Found {len(session_files)} Claude session files")
429
445
  if session_files:
430
446
  # Sort by modification time, newest first
431
447
  session_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
432
- print(f"[MCP Server] Most recent session: {session_files[0]}", file=sys.stderr)
448
+ _server_log(f"Most recent session: {session_files[0]}")
433
449
  # Extract project path from directory name
434
450
  # Format: -Users-jundewu-Downloads-code-noclue -> /Users/jundewu/Downloads/code/noclue
435
451
  project_dir_name = session_files[0].parent.name
436
- print(f"[MCP Server] Project dir name: {project_dir_name}", file=sys.stderr)
452
+ _server_log(f"Project dir name: {project_dir_name}")
437
453
  if project_dir_name.startswith('-'):
438
454
  # Convert back to path: -Users-foo-bar -> /Users/foo/bar
439
455
  # Note: underscores were also replaced with dashes, but we can't distinguish
440
456
  # So we just replace dashes with slashes
441
457
  path_str = '/' + project_dir_name[1:].replace('-', '/')
442
458
  candidate_path = Path(path_str)
443
- print(f"[MCP Server] Candidate path: {candidate_path}, exists: {candidate_path.exists()}", file=sys.stderr)
459
+ _server_log(f"Candidate path: {candidate_path}, exists: {candidate_path.exists()}")
444
460
  if candidate_path.exists():
445
461
  repo_path = candidate_path
446
- print(f"[MCP Server] Detected workspace from Claude session: {repo_path}", file=sys.stderr)
462
+ _server_log(f"Detected workspace from Claude session: {repo_path}")
447
463
 
448
464
  # Method 3: Fallback to current directory
449
465
  if not repo_path:
450
466
  repo_path = Path.cwd()
451
- print(f"[MCP Server] Using current directory: {repo_path}", file=sys.stderr)
467
+ _server_log(f"Using current directory: {repo_path}")
452
468
 
453
469
  # Start the watcher
454
- print(f"[MCP Server] Starting watcher for: {repo_path}", file=sys.stderr)
470
+ _server_log(f"Starting watcher for: {repo_path}")
455
471
  await start_watcher(repo_path)
456
472
 
457
473
  except Exception as e:
458
- print(f"[MCP Server] Warning: Could not start watcher: {e}", file=sys.stderr)
474
+ _server_log(f"Warning: Could not start watcher: {e}")
459
475
  import traceback
460
476
  traceback.print_exc(file=sys.stderr)
461
477
 
462
- print("[MCP Server] MCP server ready", file=sys.stderr)
478
+ _server_log("MCP server ready")
463
479
 
464
480
  async with stdio_server() as (read_stream, write_stream):
465
481
  await app.run(
realign/mcp_watcher.py CHANGED
@@ -13,6 +13,35 @@ from .config import ReAlignConfig
13
13
  from .hooks import find_all_active_sessions
14
14
 
15
15
 
16
+ # File-based logger for debugging when stderr is not visible
17
+ _log_file = None
18
+
19
+ def _log(msg: str):
20
+ """Log to both stderr and a debug file."""
21
+ global _log_file
22
+ timestamp = datetime.now().strftime("%H:%M:%S")
23
+ full_msg = f"[{timestamp}] {msg}"
24
+ print(f"[MCP Watcher] {msg}", file=sys.stderr)
25
+
26
+ # Also write to file for debugging
27
+ if _log_file is None:
28
+ log_path = Path.home() / ".aline_watcher.log"
29
+ try:
30
+ _log_file = open(log_path, "a", buffering=1) # Line buffered
31
+ _log_file.write(f"\n{'='*60}\n")
32
+ _log_file.write(f"[{timestamp}] MCP Watcher Started\n")
33
+ _log_file.write(f"{'='*60}\n")
34
+ except Exception:
35
+ _log_file = False # Mark as failed so we don't retry
36
+
37
+ if _log_file and _log_file is not False:
38
+ try:
39
+ _log_file.write(full_msg + "\n")
40
+ _log_file.flush()
41
+ except Exception:
42
+ pass
43
+
44
+
16
45
  class DialogueWatcher:
17
46
  """Watch session files and auto-commit immediately after each user request completes."""
18
47
 
@@ -30,25 +59,38 @@ class DialogueWatcher:
30
59
  async def start(self):
31
60
  """Start watching session files."""
32
61
  if not self.config.mcp_auto_commit:
33
- print("[MCP Watcher] Auto-commit disabled in config", file=sys.stderr)
62
+ _log("Auto-commit disabled in config")
34
63
  return
35
64
 
36
65
  self.running = True
37
- print("[MCP Watcher] Started watching for dialogue completion", file=sys.stderr)
38
- print(f"[MCP Watcher] Mode: Per-request (triggers at end of each Claude response)", file=sys.stderr)
39
- print(f"[MCP Watcher] Debounce: {self.debounce_delay}s, Cooldown: {self.min_commit_interval}s", file=sys.stderr)
66
+ _log("Started watching for dialogue completion")
67
+ _log(f"Mode: Per-request (triggers at end of each Claude response)")
68
+ _log(f"Debounce: {self.debounce_delay}s, Cooldown: {self.min_commit_interval}s")
69
+ _log(f"Home directory: {Path.home()}")
70
+ _log(f"Current working directory: {Path.cwd()}")
40
71
 
41
72
  # Initialize baseline sizes and stop_reason counts
42
73
  self.last_session_sizes = self._get_session_sizes()
43
74
  self.last_stop_reason_counts = self._get_stop_reason_counts()
44
75
 
76
+ # Log initial session files being monitored
77
+ if self.last_session_sizes:
78
+ _log(f"Monitoring {len(self.last_session_sizes)} session file(s):")
79
+ for session_path in list(self.last_session_sizes.keys())[:5]: # Show first 5
80
+ _log(f" - {session_path}")
81
+ if len(self.last_session_sizes) > 5:
82
+ _log(f" ... and {len(self.last_session_sizes) - 5} more")
83
+ else:
84
+ claude_projects = Path.home() / ".claude" / "projects"
85
+ _log(f"WARNING: No session files found in {claude_projects}")
86
+
45
87
  # Poll for file changes more frequently
46
88
  while self.running:
47
89
  try:
48
90
  await self.check_for_changes()
49
91
  await asyncio.sleep(0.5) # Check every 0.5 seconds for responsiveness
50
92
  except Exception as e:
51
- print(f"[MCP Watcher] Error: {e}", file=sys.stderr)
93
+ _log(f"Error: {e}")
52
94
  await asyncio.sleep(1.0)
53
95
 
54
96
  async def stop(self):
@@ -56,7 +98,7 @@ class DialogueWatcher:
56
98
  self.running = False
57
99
  if self.pending_commit_task:
58
100
  self.pending_commit_task.cancel()
59
- print("[MCP Watcher] Stopped", file=sys.stderr)
101
+ _log("Stopped")
60
102
 
61
103
  def _get_session_sizes(self) -> Dict[str, int]:
62
104
  """Get current sizes of all active session files (from all Claude projects)."""
@@ -69,7 +111,7 @@ class DialogueWatcher:
69
111
  if session_file.exists():
70
112
  sizes[str(session_file)] = session_file.stat().st_size
71
113
  except Exception as e:
72
- print(f"[MCP Watcher] Error getting session sizes: {e}", file=sys.stderr)
114
+ _log(f"Error getting session sizes: {e}")
73
115
  return sizes
74
116
 
75
117
  def _get_stop_reason_counts(self) -> Dict[str, int]:
@@ -83,7 +125,7 @@ class DialogueWatcher:
83
125
  if session_file.exists():
84
126
  counts[str(session_file)] = self._count_stop_reasons(session_file)
85
127
  except Exception as e:
86
- print(f"[MCP Watcher] Error getting stop_reason counts: {e}", file=sys.stderr)
128
+ _log(f"Error getting stop_reason counts: {e}")
87
129
  return counts
88
130
 
89
131
  def _count_stop_reasons(self, session_file: Path) -> int:
@@ -115,7 +157,7 @@ class DialogueWatcher:
115
157
  except json.JSONDecodeError:
116
158
  continue
117
159
  except Exception as e:
118
- print(f"[MCP Watcher] Error counting stop_reasons in {session_file}: {e}", file=sys.stderr)
160
+ _log(f"Error counting stop_reasons in {session_file}: {e}")
119
161
  return len(unique_message_ids)
120
162
 
121
163
  async def check_for_changes(self):
@@ -134,7 +176,7 @@ class DialogueWatcher:
134
176
  old_size = self.last_session_sizes.get(path, 0)
135
177
  if size > old_size:
136
178
  changed_files.append(Path(path))
137
- print(f"[MCP Watcher] Session file changed: {Path(path).name} ({old_size} -> {size} bytes)", file=sys.stderr)
179
+ _log(f"Session file changed: {Path(path).name} ({old_size} -> {size} bytes)")
138
180
 
139
181
  if changed_files:
140
182
  # File changed - cancel any pending commit and schedule a new one
@@ -150,7 +192,7 @@ class DialogueWatcher:
150
192
  self.last_session_sizes = current_sizes
151
193
 
152
194
  except Exception as e:
153
- print(f"[MCP Watcher] Error checking for changes: {e}", file=sys.stderr)
195
+ _log(f"Error checking for changes: {e}")
154
196
 
155
197
  async def _debounced_commit(self, changed_files: list):
156
198
  """Wait for debounce period, then check if dialogue is complete and commit."""
@@ -161,7 +203,7 @@ class DialogueWatcher:
161
203
  # Check if any of the changed files contains a complete dialogue turn
162
204
  for session_file in changed_files:
163
205
  if await self._check_if_turn_complete(session_file):
164
- print(f"[MCP Watcher] Complete turn detected in {session_file.name}", file=sys.stderr)
206
+ _log(f"Complete turn detected in {session_file.name}")
165
207
 
166
208
  # Extract project path from session file's parent directory
167
209
  project_path = self._get_project_path_from_session(session_file)
@@ -170,16 +212,18 @@ class DialogueWatcher:
170
212
  current_time = time.time()
171
213
  last_time = self.last_commit_time.get(str(project_path), 0)
172
214
  if current_time - last_time < self.min_commit_interval:
173
- print(f"[MCP Watcher] Skipping commit for {project_path} (cooldown)", file=sys.stderr)
215
+ _log(f"Skipping commit for {project_path} (cooldown)")
174
216
  continue
175
217
 
176
218
  await self._do_commit(project_path)
219
+ else:
220
+ _log(f"WARNING: Could not extract project path from {session_file}, skipping commit")
177
221
 
178
222
  except asyncio.CancelledError:
179
223
  # Task was cancelled because a newer change was detected
180
224
  pass
181
225
  except Exception as e:
182
- print(f"[MCP Watcher] Error in debounced commit: {e}", file=sys.stderr)
226
+ _log(f"Error in debounced commit: {e}")
183
227
 
184
228
  async def _check_if_turn_complete(self, session_file: Path) -> bool:
185
229
  """
@@ -201,7 +245,7 @@ class DialogueWatcher:
201
245
 
202
246
  # Commit after each complete assistant response (1 new end_turn)
203
247
  if new_stop_reasons >= 1:
204
- print(f"[MCP Watcher] Detected {new_stop_reasons} new end_turn entry(ies) in {session_file.name}", file=sys.stderr)
248
+ _log(f"Detected {new_stop_reasons} new end_turn entry(ies) in {session_file.name}")
205
249
  # Update baseline immediately to avoid double-counting
206
250
  self.last_stop_reason_counts[session_path] = current_count
207
251
  return True
@@ -209,7 +253,7 @@ class DialogueWatcher:
209
253
  return False
210
254
 
211
255
  except Exception as e:
212
- print(f"[MCP Watcher] Error checking turn completion: {e}", file=sys.stderr)
256
+ _log(f"Error checking turn completion: {e}")
213
257
  return False
214
258
 
215
259
  def _get_project_path_from_session(self, session_file: Path) -> Optional[Path]:
@@ -221,14 +265,23 @@ class DialogueWatcher:
221
265
  """
222
266
  try:
223
267
  project_dir_name = session_file.parent.name
268
+ _log(f"Extracting project path from: {project_dir_name}")
269
+
224
270
  if project_dir_name.startswith('-'):
225
271
  # Convert back to path: -Users-foo-bar -> /Users/foo/bar
226
272
  path_str = '/' + project_dir_name[1:].replace('-', '/')
227
273
  candidate_path = Path(path_str)
274
+ _log(f"Candidate project path: {candidate_path}")
275
+
228
276
  if candidate_path.exists():
277
+ _log(f"Project path exists: {candidate_path}")
229
278
  return candidate_path
279
+ else:
280
+ _log(f"WARNING: Project path does not exist: {candidate_path}")
281
+ else:
282
+ _log(f"WARNING: Directory name doesn't start with '-': {project_dir_name}")
230
283
  except Exception as e:
231
- print(f"[MCP Watcher] Error extracting project path: {e}", file=sys.stderr)
284
+ _log(f"Error extracting project path: {e}")
232
285
  return None
233
286
 
234
287
  async def _do_commit(self, project_path: Path):
@@ -238,7 +291,7 @@ class DialogueWatcher:
238
291
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
239
292
  message = f"chore: Auto-commit MCP session ({timestamp})"
240
293
 
241
- print(f"[MCP Watcher] Attempting commit in {project_path} with message: {message}", file=sys.stderr)
294
+ _log(f"Attempting commit in {project_path} with message: {message}")
242
295
 
243
296
  # Use realign commit command with the specific project path
244
297
  result = await asyncio.get_event_loop().run_in_executor(
@@ -249,12 +302,12 @@ class DialogueWatcher:
249
302
  )
250
303
 
251
304
  if result:
252
- print(f"[MCP Watcher] ✓ Committed in {project_path}: {message}", file=sys.stderr)
305
+ _log(f"✓ Committed in {project_path}: {message}")
253
306
  self.last_commit_time[str(project_path)] = time.time()
254
307
  # Baseline counts already updated in _check_if_turn_complete()
255
308
 
256
309
  except Exception as e:
257
- print(f"[MCP Watcher] Error during commit: {e}", file=sys.stderr)
310
+ _log(f"Error during commit: {e}")
258
311
 
259
312
  def _run_realign_commit(self, message: str, project_path: Path) -> bool:
260
313
  """
@@ -274,7 +327,7 @@ class DialogueWatcher:
274
327
  realign_dir = project_path / ".realign"
275
328
 
276
329
  if not realign_dir.exists():
277
- print(f"[MCP Watcher] Aline not initialized in {project_path}, initializing...", file=sys.stderr)
330
+ _log(f"Aline not initialized in {project_path}, initializing...")
278
331
 
279
332
  # Auto-initialize Aline (which also inits git if needed)
280
333
  init_result = init_repository(
@@ -284,10 +337,10 @@ class DialogueWatcher:
284
337
  )
285
338
 
286
339
  if not init_result.get("success"):
287
- print(f"[MCP Watcher] Failed to initialize Aline: {init_result.get('message', 'Unknown error')}", file=sys.stderr)
340
+ _log(f"Failed to initialize Aline: {init_result.get('message', 'Unknown error')}")
288
341
  return False
289
342
 
290
- print("[MCP Watcher] ✓ Aline initialized successfully", file=sys.stderr)
343
+ _log("✓ Aline initialized successfully")
291
344
 
292
345
  # Now run the commit with stage_all=True
293
346
  result = smart_commit(
@@ -307,11 +360,11 @@ class DialogueWatcher:
307
360
  else:
308
361
  # Log the error for debugging
309
362
  error_msg = result.get("message", "Unknown error")
310
- print(f"[MCP Watcher] Commit failed: {error_msg}", file=sys.stderr)
363
+ _log(f"Commit failed: {error_msg}")
311
364
  return False
312
365
 
313
366
  except Exception as e:
314
- print(f"[MCP Watcher] Commit error: {e}", file=sys.stderr)
367
+ _log(f"Commit error: {e}")
315
368
  return False
316
369
 
317
370