aline-ai 0.1.4__py3-none-any.whl → 0.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aline-ai
3
- Version: 0.1.4
3
+ Version: 0.1.6
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.4.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
- realign/__init__.py,sha256=PFVJVPatfhf1vGJIGnMKgKkPLWTkGXI6NKq_PQMW7Bc,68
1
+ aline_ai-0.1.6.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
+ realign/__init__.py,sha256=7fKH0C4lNvrYFiQ00kUEAH-H3RUUMLXG-69ehNncimI,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=oe2RQZ7_en_6vDMqXgVaSHCSqc_aQejc8wM2vkF0rj4,16702
10
- realign/mcp_watcher.py,sha256=jxr4em27hw79N9d-lYCbu4dGclzG7OyPuPFtPeeadMM,13122
9
+ realign/mcp_server.py,sha256=-LAJIsxN8U1VSzr-8TYhV9s2jC_t4_XdblnGAcbaKNk,17572
10
+ realign/mcp_watcher.py,sha256=a_9R0r4DM8a9BiRIimJlY8_EhoHTQcTo55yCf4vVYOU,16163
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.4.dist-info/METADATA,sha256=fGfNH8tf8zKsOhpi7vfAkDkRDF0zFzpWHNaFtCtrnIM,1398
20
- aline_ai-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- aline_ai-0.1.4.dist-info/entry_points.txt,sha256=h-NocHDzSueXfsepHTIdRPNQzhNZQPAztJfldd-mQTE,202
22
- aline_ai-0.1.4.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
23
- aline_ai-0.1.4.dist-info/RECORD,,
19
+ aline_ai-0.1.6.dist-info/METADATA,sha256=GK1pSW1EFmnNV2ojpzEBhoBDlQrYKr9OAf4c_VELcww,1398
20
+ aline_ai-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ aline_ai-0.1.6.dist-info/entry_points.txt,sha256=h-NocHDzSueXfsepHTIdRPNQzhNZQPAztJfldd-mQTE,202
22
+ aline_ai-0.1.6.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
23
+ aline_ai-0.1.6.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.4"
3
+ __version__ = "0.1.6"
realign/mcp_server.py CHANGED
@@ -398,6 +398,7 @@ async def handle_version(args: dict) -> list[TextContent]:
398
398
  async def async_main():
399
399
  """Run the MCP server (async)."""
400
400
  print("[MCP Server] Starting Aline MCP server...", file=sys.stderr)
401
+ print(f"[MCP Server] Current working directory: {Path.cwd()}", file=sys.stderr)
401
402
 
402
403
  # Detect workspace path and start the watcher
403
404
  # Try multiple methods since MCP server may run in different context
@@ -414,25 +415,32 @@ async def async_main():
414
415
  if result.returncode == 0:
415
416
  repo_path = Path(result.stdout.strip())
416
417
  print(f"[MCP Server] Detected workspace from git: {repo_path}", file=sys.stderr)
418
+ else:
419
+ print(f"[MCP Server] Not in git repo (cwd: {Path.cwd()})", file=sys.stderr)
417
420
 
418
421
  # Method 2: If not in git repo, try to find from Claude session files
419
422
  if not repo_path:
420
423
  claude_projects = Path.home() / ".claude" / "projects"
424
+ print(f"[MCP Server] Checking Claude projects at: {claude_projects}", file=sys.stderr)
421
425
  if claude_projects.exists():
422
426
  # Find most recently modified session file
423
427
  session_files = list(claude_projects.glob("*/*.jsonl"))
428
+ print(f"[MCP Server] Found {len(session_files)} Claude session files", file=sys.stderr)
424
429
  if session_files:
425
430
  # Sort by modification time, newest first
426
431
  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)
427
433
  # Extract project path from directory name
428
434
  # Format: -Users-jundewu-Downloads-code-noclue -> /Users/jundewu/Downloads/code/noclue
429
435
  project_dir_name = session_files[0].parent.name
436
+ print(f"[MCP Server] Project dir name: {project_dir_name}", file=sys.stderr)
430
437
  if project_dir_name.startswith('-'):
431
438
  # Convert back to path: -Users-foo-bar -> /Users/foo/bar
432
439
  # Note: underscores were also replaced with dashes, but we can't distinguish
433
440
  # So we just replace dashes with slashes
434
441
  path_str = '/' + project_dir_name[1:].replace('-', '/')
435
442
  candidate_path = Path(path_str)
443
+ print(f"[MCP Server] Candidate path: {candidate_path}, exists: {candidate_path.exists()}", file=sys.stderr)
436
444
  if candidate_path.exists():
437
445
  repo_path = candidate_path
438
446
  print(f"[MCP Server] Detected workspace from Claude session: {repo_path}", file=sys.stderr)
@@ -443,10 +451,13 @@ async def async_main():
443
451
  print(f"[MCP Server] Using current directory: {repo_path}", file=sys.stderr)
444
452
 
445
453
  # Start the watcher
454
+ print(f"[MCP Server] Starting watcher for: {repo_path}", file=sys.stderr)
446
455
  await start_watcher(repo_path)
447
456
 
448
457
  except Exception as e:
449
458
  print(f"[MCP Server] Warning: Could not start watcher: {e}", file=sys.stderr)
459
+ import traceback
460
+ traceback.print_exc(file=sys.stderr)
450
461
 
451
462
  print("[MCP Server] MCP server ready", file=sys.stderr)
452
463
 
realign/mcp_watcher.py CHANGED
@@ -17,9 +17,9 @@ class DialogueWatcher:
17
17
  """Watch session files and auto-commit immediately after each user request completes."""
18
18
 
19
19
  def __init__(self, repo_path: Path):
20
- self.repo_path = repo_path
20
+ self.repo_path = repo_path # Default repo path (may be overridden per-session)
21
21
  self.config = ReAlignConfig.load()
22
- self.last_commit_time: Optional[float] = None
22
+ self.last_commit_time: Dict[str, float] = {} # Track commit time per project
23
23
  self.last_session_sizes: Dict[str, int] = {} # Track file sizes
24
24
  self.last_stop_reason_counts: Dict[str, int] = {} # Track stop_reason counts per session
25
25
  self.min_commit_interval = 5.0 # Minimum 5 seconds between commits (cooldown)
@@ -37,11 +37,23 @@ class DialogueWatcher:
37
37
  print("[MCP Watcher] Started watching for dialogue completion", file=sys.stderr)
38
38
  print(f"[MCP Watcher] Mode: Per-request (triggers at end of each Claude response)", file=sys.stderr)
39
39
  print(f"[MCP Watcher] Debounce: {self.debounce_delay}s, Cooldown: {self.min_commit_interval}s", file=sys.stderr)
40
+ print(f"[MCP Watcher] Home directory: {Path.home()}", file=sys.stderr)
40
41
 
41
42
  # Initialize baseline sizes and stop_reason counts
42
43
  self.last_session_sizes = self._get_session_sizes()
43
44
  self.last_stop_reason_counts = self._get_stop_reason_counts()
44
45
 
46
+ # Log initial session files being monitored
47
+ if self.last_session_sizes:
48
+ print(f"[MCP Watcher] Monitoring {len(self.last_session_sizes)} session file(s):", file=sys.stderr)
49
+ for session_path in list(self.last_session_sizes.keys())[:5]: # Show first 5
50
+ print(f" - {session_path}", file=sys.stderr)
51
+ if len(self.last_session_sizes) > 5:
52
+ print(f" ... and {len(self.last_session_sizes) - 5} more", file=sys.stderr)
53
+ else:
54
+ claude_projects = Path.home() / ".claude" / "projects"
55
+ print(f"[MCP Watcher] WARNING: No session files found in {claude_projects}", file=sys.stderr)
56
+
45
57
  # Poll for file changes more frequently
46
58
  while self.running:
47
59
  try:
@@ -59,13 +71,15 @@ class DialogueWatcher:
59
71
  print("[MCP Watcher] Stopped", file=sys.stderr)
60
72
 
61
73
  def _get_session_sizes(self) -> Dict[str, int]:
62
- """Get current sizes of all active session files."""
74
+ """Get current sizes of all active session files (from all Claude projects)."""
63
75
  sizes = {}
64
76
  try:
65
- session_files = find_all_active_sessions(self.config, self.repo_path)
66
- for session_file in session_files:
67
- if session_file.exists():
68
- sizes[str(session_file)] = session_file.stat().st_size
77
+ # Watch ALL Claude session files, not just one project
78
+ claude_projects = Path.home() / ".claude" / "projects"
79
+ if claude_projects.exists():
80
+ for session_file in claude_projects.glob("*/*.jsonl"):
81
+ if session_file.exists():
82
+ sizes[str(session_file)] = session_file.stat().st_size
69
83
  except Exception as e:
70
84
  print(f"[MCP Watcher] Error getting session sizes: {e}", file=sys.stderr)
71
85
  return sizes
@@ -74,10 +88,12 @@ class DialogueWatcher:
74
88
  """Get current count of stop_reason entries in all active session files."""
75
89
  counts = {}
76
90
  try:
77
- session_files = find_all_active_sessions(self.config, self.repo_path)
78
- for session_file in session_files:
79
- if session_file.exists():
80
- counts[str(session_file)] = self._count_stop_reasons(session_file)
91
+ # Watch ALL Claude session files
92
+ claude_projects = Path.home() / ".claude" / "projects"
93
+ if claude_projects.exists():
94
+ for session_file in claude_projects.glob("*/*.jsonl"):
95
+ if session_file.exists():
96
+ counts[str(session_file)] = self._count_stop_reasons(session_file)
81
97
  except Exception as e:
82
98
  print(f"[MCP Watcher] Error getting stop_reason counts: {e}", file=sys.stderr)
83
99
  return counts
@@ -154,24 +170,24 @@ class DialogueWatcher:
154
170
  # Wait for debounce period
155
171
  await asyncio.sleep(self.debounce_delay)
156
172
 
157
- # Check cooldown period
158
- current_time = time.time()
159
- if self.last_commit_time:
160
- time_since_last = current_time - self.last_commit_time
161
- if time_since_last < self.min_commit_interval:
162
- print(f"[MCP Watcher] Skipping commit (cooldown: {time_since_last:.1f}s < {self.min_commit_interval}s)", file=sys.stderr)
163
- return
164
-
165
173
  # Check if any of the changed files contains a complete dialogue turn
166
- has_complete_turn = False
167
174
  for session_file in changed_files:
168
175
  if await self._check_if_turn_complete(session_file):
169
- has_complete_turn = True
170
176
  print(f"[MCP Watcher] Complete turn detected in {session_file.name}", file=sys.stderr)
171
- break
172
177
 
173
- if has_complete_turn:
174
- await self._do_commit()
178
+ # Extract project path from session file's parent directory
179
+ project_path = self._get_project_path_from_session(session_file)
180
+ if project_path:
181
+ # Check cooldown for this specific project
182
+ current_time = time.time()
183
+ last_time = self.last_commit_time.get(str(project_path), 0)
184
+ if current_time - last_time < self.min_commit_interval:
185
+ print(f"[MCP Watcher] Skipping commit for {project_path} (cooldown)", file=sys.stderr)
186
+ continue
187
+
188
+ await self._do_commit(project_path)
189
+ else:
190
+ print(f"[MCP Watcher] WARNING: Could not extract project path from {session_file}, skipping commit", file=sys.stderr)
175
191
 
176
192
  except asyncio.CancelledError:
177
193
  # Task was cancelled because a newer change was detected
@@ -210,31 +226,60 @@ class DialogueWatcher:
210
226
  print(f"[MCP Watcher] Error checking turn completion: {e}", file=sys.stderr)
211
227
  return False
212
228
 
213
- async def _do_commit(self):
214
- """Perform the actual commit."""
229
+ def _get_project_path_from_session(self, session_file: Path) -> Optional[Path]:
230
+ """
231
+ Extract the actual project path from a Claude session file's location.
232
+
233
+ Claude Code stores sessions in: ~/.claude/projects/-Users-username-path/session.jsonl
234
+ The directory name encodes the project path with dashes replacing slashes.
235
+ """
236
+ try:
237
+ project_dir_name = session_file.parent.name
238
+ print(f"[MCP Watcher] Extracting project path from: {project_dir_name}", file=sys.stderr)
239
+
240
+ if project_dir_name.startswith('-'):
241
+ # Convert back to path: -Users-foo-bar -> /Users/foo/bar
242
+ path_str = '/' + project_dir_name[1:].replace('-', '/')
243
+ candidate_path = Path(path_str)
244
+ print(f"[MCP Watcher] Candidate project path: {candidate_path}", file=sys.stderr)
245
+
246
+ if candidate_path.exists():
247
+ print(f"[MCP Watcher] Project path exists: {candidate_path}", file=sys.stderr)
248
+ return candidate_path
249
+ else:
250
+ print(f"[MCP Watcher] WARNING: Project path does not exist: {candidate_path}", file=sys.stderr)
251
+ else:
252
+ print(f"[MCP Watcher] WARNING: Directory name doesn't start with '-': {project_dir_name}", file=sys.stderr)
253
+ except Exception as e:
254
+ print(f"[MCP Watcher] Error extracting project path: {e}", file=sys.stderr)
255
+ return None
256
+
257
+ async def _do_commit(self, project_path: Path):
258
+ """Perform the actual commit for a specific project."""
215
259
  try:
216
260
  # Generate commit message
217
261
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
218
262
  message = f"chore: Auto-commit MCP session ({timestamp})"
219
263
 
220
- print(f"[MCP Watcher] Attempting commit with message: {message}", file=sys.stderr)
264
+ print(f"[MCP Watcher] Attempting commit in {project_path} with message: {message}", file=sys.stderr)
221
265
 
222
- # Use realign commit command
266
+ # Use realign commit command with the specific project path
223
267
  result = await asyncio.get_event_loop().run_in_executor(
224
268
  None,
225
269
  self._run_realign_commit,
226
- message
270
+ message,
271
+ project_path
227
272
  )
228
273
 
229
274
  if result:
230
- print(f"[MCP Watcher] ✓ Committed: {message}", file=sys.stderr)
231
- self.last_commit_time = time.time()
275
+ print(f"[MCP Watcher] ✓ Committed in {project_path}: {message}", file=sys.stderr)
276
+ self.last_commit_time[str(project_path)] = time.time()
232
277
  # Baseline counts already updated in _check_if_turn_complete()
233
278
 
234
279
  except Exception as e:
235
280
  print(f"[MCP Watcher] Error during commit: {e}", file=sys.stderr)
236
281
 
237
- def _run_realign_commit(self, message: str) -> bool:
282
+ def _run_realign_commit(self, message: str, project_path: Path) -> bool:
238
283
  """
239
284
  Run aline commit command using Python functions directly.
240
285
 
@@ -249,14 +294,14 @@ class DialogueWatcher:
249
294
  from .commands.commit import smart_commit
250
295
 
251
296
  # Check if Aline is initialized
252
- realign_dir = self.repo_path / ".realign"
297
+ realign_dir = project_path / ".realign"
253
298
 
254
299
  if not realign_dir.exists():
255
- print("[MCP Watcher] Aline not initialized, initializing...", file=sys.stderr)
300
+ print(f"[MCP Watcher] Aline not initialized in {project_path}, initializing...", file=sys.stderr)
256
301
 
257
302
  # Auto-initialize Aline (which also inits git if needed)
258
303
  init_result = init_repository(
259
- repo_path=str(self.repo_path),
304
+ repo_path=str(project_path),
260
305
  auto_init_git=True,
261
306
  skip_commit=False,
262
307
  )
@@ -270,7 +315,7 @@ class DialogueWatcher:
270
315
  # Now run the commit with stage_all=True
271
316
  result = smart_commit(
272
317
  message=message,
273
- repo_path=str(self.repo_path),
318
+ repo_path=str(project_path),
274
319
  stage_all=True,
275
320
  amend=False,
276
321
  no_edit=False,