aline-ai 0.1.3__py3-none-any.whl → 0.1.5__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.3
3
+ Version: 0.1.5
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -1,23 +1,23 @@
1
- aline_ai-0.1.3.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
- realign/__init__.py,sha256=l5m3jmTKnHo3QKvqEtK55mM-yaFZtwAuMDofmHF1qFc,68
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
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=gqGlDWnKzH0hsqlf9LRobEPvfh4JoOz8kenvwZ5ICn8,14653
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
14
- realign/commands/commit.py,sha256=T5Cb6dfawNQXisztsCFminEaFg4jH5v6u88jkGMGOFI,9007
14
+ realign/commands/commit.py,sha256=yjhOrkRY_UvAa5EXufwwcYZaqE83L9Bzd0YPUc59fic,9196
15
15
  realign/commands/config.py,sha256=oarvn6UuGT8svd2h5_8M_ueV5QWOCUOn8SYoa4XYjs8,6500
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.3.dist-info/METADATA,sha256=uxqSPbYXK_-p2egCrKOYKpaOxefKKyMJAz2uw4D-k7w,1398
20
- aline_ai-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- aline_ai-0.1.3.dist-info/entry_points.txt,sha256=h-NocHDzSueXfsepHTIdRPNQzhNZQPAztJfldd-mQTE,202
22
- aline_ai-0.1.3.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
23
- aline_ai-0.1.3.dist-info/RECORD,,
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,,
realign/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Aline - AI Agent Chat Session Tracker."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.1.5"
@@ -13,10 +13,13 @@ from ..hooks import find_all_active_sessions
13
13
  console = Console()
14
14
 
15
15
 
16
- def has_file_changes() -> bool:
16
+ def has_file_changes(cwd: Optional[str] = None) -> bool:
17
17
  """
18
18
  Check if there are any staged or unstaged file changes (excluding .realign/sessions/).
19
19
 
20
+ Args:
21
+ cwd: Working directory for git commands (default: current directory)
22
+
20
23
  Returns:
21
24
  True if there are file changes, False otherwise
22
25
  """
@@ -27,6 +30,7 @@ def has_file_changes() -> bool:
27
30
  capture_output=True,
28
31
  text=True,
29
32
  check=True,
33
+ cwd=cwd,
30
34
  )
31
35
  unstaged = [
32
36
  line.strip()
@@ -40,6 +44,7 @@ def has_file_changes() -> bool:
40
44
  capture_output=True,
41
45
  text=True,
42
46
  check=True,
47
+ cwd=cwd,
43
48
  )
44
49
  staged = [
45
50
  line.strip()
@@ -53,6 +58,7 @@ def has_file_changes() -> bool:
53
58
  capture_output=True,
54
59
  text=True,
55
60
  check=True,
61
+ cwd=cwd,
56
62
  )
57
63
  untracked = [
58
64
  line.strip()
@@ -137,7 +143,7 @@ def smart_commit(
137
143
  subprocess.run(["git", "add", "-A"], cwd=repo_path, check=True)
138
144
 
139
145
  # Check for file changes and session changes
140
- has_files = has_file_changes()
146
+ has_files = has_file_changes(cwd=repo_path)
141
147
  has_sessions, session_files = has_session_changes(repo_root)
142
148
 
143
149
  # No changes detected
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)
@@ -59,13 +59,15 @@ class DialogueWatcher:
59
59
  print("[MCP Watcher] Stopped", file=sys.stderr)
60
60
 
61
61
  def _get_session_sizes(self) -> Dict[str, int]:
62
- """Get current sizes of all active session files."""
62
+ """Get current sizes of all active session files (from all Claude projects)."""
63
63
  sizes = {}
64
64
  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
65
+ # Watch ALL Claude session files, not just one project
66
+ claude_projects = Path.home() / ".claude" / "projects"
67
+ if claude_projects.exists():
68
+ for session_file in claude_projects.glob("*/*.jsonl"):
69
+ if session_file.exists():
70
+ sizes[str(session_file)] = session_file.stat().st_size
69
71
  except Exception as e:
70
72
  print(f"[MCP Watcher] Error getting session sizes: {e}", file=sys.stderr)
71
73
  return sizes
@@ -74,10 +76,12 @@ class DialogueWatcher:
74
76
  """Get current count of stop_reason entries in all active session files."""
75
77
  counts = {}
76
78
  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)
79
+ # Watch ALL Claude session files
80
+ claude_projects = Path.home() / ".claude" / "projects"
81
+ if claude_projects.exists():
82
+ for session_file in claude_projects.glob("*/*.jsonl"):
83
+ if session_file.exists():
84
+ counts[str(session_file)] = self._count_stop_reasons(session_file)
81
85
  except Exception as e:
82
86
  print(f"[MCP Watcher] Error getting stop_reason counts: {e}", file=sys.stderr)
83
87
  return counts
@@ -154,24 +158,22 @@ class DialogueWatcher:
154
158
  # Wait for debounce period
155
159
  await asyncio.sleep(self.debounce_delay)
156
160
 
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
161
  # Check if any of the changed files contains a complete dialogue turn
166
- has_complete_turn = False
167
162
  for session_file in changed_files:
168
163
  if await self._check_if_turn_complete(session_file):
169
- has_complete_turn = True
170
164
  print(f"[MCP Watcher] Complete turn detected in {session_file.name}", file=sys.stderr)
171
- break
172
165
 
173
- if has_complete_turn:
174
- await self._do_commit()
166
+ # Extract project path from session file's parent directory
167
+ project_path = self._get_project_path_from_session(session_file)
168
+ if project_path:
169
+ # Check cooldown for this specific project
170
+ current_time = time.time()
171
+ last_time = self.last_commit_time.get(str(project_path), 0)
172
+ if current_time - last_time < self.min_commit_interval:
173
+ print(f"[MCP Watcher] Skipping commit for {project_path} (cooldown)", file=sys.stderr)
174
+ continue
175
+
176
+ await self._do_commit(project_path)
175
177
 
176
178
  except asyncio.CancelledError:
177
179
  # Task was cancelled because a newer change was detected
@@ -210,31 +212,51 @@ class DialogueWatcher:
210
212
  print(f"[MCP Watcher] Error checking turn completion: {e}", file=sys.stderr)
211
213
  return False
212
214
 
213
- async def _do_commit(self):
214
- """Perform the actual commit."""
215
+ def _get_project_path_from_session(self, session_file: Path) -> Optional[Path]:
216
+ """
217
+ Extract the actual project path from a Claude session file's location.
218
+
219
+ Claude Code stores sessions in: ~/.claude/projects/-Users-username-path/session.jsonl
220
+ The directory name encodes the project path with dashes replacing slashes.
221
+ """
222
+ try:
223
+ project_dir_name = session_file.parent.name
224
+ if project_dir_name.startswith('-'):
225
+ # Convert back to path: -Users-foo-bar -> /Users/foo/bar
226
+ path_str = '/' + project_dir_name[1:].replace('-', '/')
227
+ candidate_path = Path(path_str)
228
+ if candidate_path.exists():
229
+ return candidate_path
230
+ except Exception as e:
231
+ print(f"[MCP Watcher] Error extracting project path: {e}", file=sys.stderr)
232
+ return None
233
+
234
+ async def _do_commit(self, project_path: Path):
235
+ """Perform the actual commit for a specific project."""
215
236
  try:
216
237
  # Generate commit message
217
238
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
218
239
  message = f"chore: Auto-commit MCP session ({timestamp})"
219
240
 
220
- print(f"[MCP Watcher] Attempting commit with message: {message}", file=sys.stderr)
241
+ print(f"[MCP Watcher] Attempting commit in {project_path} with message: {message}", file=sys.stderr)
221
242
 
222
- # Use realign commit command
243
+ # Use realign commit command with the specific project path
223
244
  result = await asyncio.get_event_loop().run_in_executor(
224
245
  None,
225
246
  self._run_realign_commit,
226
- message
247
+ message,
248
+ project_path
227
249
  )
228
250
 
229
251
  if result:
230
- print(f"[MCP Watcher] ✓ Committed: {message}", file=sys.stderr)
231
- self.last_commit_time = time.time()
252
+ print(f"[MCP Watcher] ✓ Committed in {project_path}: {message}", file=sys.stderr)
253
+ self.last_commit_time[str(project_path)] = time.time()
232
254
  # Baseline counts already updated in _check_if_turn_complete()
233
255
 
234
256
  except Exception as e:
235
257
  print(f"[MCP Watcher] Error during commit: {e}", file=sys.stderr)
236
258
 
237
- def _run_realign_commit(self, message: str) -> bool:
259
+ def _run_realign_commit(self, message: str, project_path: Path) -> bool:
238
260
  """
239
261
  Run aline commit command using Python functions directly.
240
262
 
@@ -249,14 +271,14 @@ class DialogueWatcher:
249
271
  from .commands.commit import smart_commit
250
272
 
251
273
  # Check if Aline is initialized
252
- realign_dir = self.repo_path / ".realign"
274
+ realign_dir = project_path / ".realign"
253
275
 
254
276
  if not realign_dir.exists():
255
- print("[MCP Watcher] Aline not initialized, initializing...", file=sys.stderr)
277
+ print(f"[MCP Watcher] Aline not initialized in {project_path}, initializing...", file=sys.stderr)
256
278
 
257
279
  # Auto-initialize Aline (which also inits git if needed)
258
280
  init_result = init_repository(
259
- repo_path=str(self.repo_path),
281
+ repo_path=str(project_path),
260
282
  auto_init_git=True,
261
283
  skip_commit=False,
262
284
  )
@@ -270,7 +292,7 @@ class DialogueWatcher:
270
292
  # Now run the commit with stage_all=True
271
293
  result = smart_commit(
272
294
  message=message,
273
- repo_path=str(self.repo_path),
295
+ repo_path=str(project_path),
274
296
  stage_all=True,
275
297
  amend=False,
276
298
  no_edit=False,