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.
- {aline_ai-0.1.3.dist-info → aline_ai-0.1.5.dist-info}/METADATA +1 -1
- {aline_ai-0.1.3.dist-info → aline_ai-0.1.5.dist-info}/RECORD +10 -10
- realign/__init__.py +1 -1
- realign/commands/commit.py +8 -2
- realign/mcp_server.py +11 -0
- realign/mcp_watcher.py +58 -36
- {aline_ai-0.1.3.dist-info → aline_ai-0.1.5.dist-info}/WHEEL +0 -0
- {aline_ai-0.1.3.dist-info → aline_ai-0.1.5.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.1.3.dist-info → aline_ai-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.1.3.dist-info → aline_ai-0.1.5.dist-info}/top_level.txt +0 -0
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
aline_ai-0.1.
|
|
2
|
-
realign/__init__.py,sha256=
|
|
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
|
|
10
|
-
realign/mcp_watcher.py,sha256=
|
|
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=
|
|
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.
|
|
20
|
-
aline_ai-0.1.
|
|
21
|
-
aline_ai-0.1.
|
|
22
|
-
aline_ai-0.1.
|
|
23
|
-
aline_ai-0.1.
|
|
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
realign/commands/commit.py
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
214
|
-
"""
|
|
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 =
|
|
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(
|
|
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(
|
|
295
|
+
repo_path=str(project_path),
|
|
274
296
|
stage_all=True,
|
|
275
297
|
amend=False,
|
|
276
298
|
no_edit=False,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|