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.
- {aline_ai-0.1.4.dist-info → aline_ai-0.1.6.dist-info}/METADATA +1 -1
- {aline_ai-0.1.4.dist-info → aline_ai-0.1.6.dist-info}/RECORD +9 -9
- realign/__init__.py +1 -1
- realign/mcp_server.py +11 -0
- realign/mcp_watcher.py +81 -36
- {aline_ai-0.1.4.dist-info → aline_ai-0.1.6.dist-info}/WHEEL +0 -0
- {aline_ai-0.1.4.dist-info → aline_ai-0.1.6.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.1.4.dist-info → aline_ai-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.1.4.dist-info → aline_ai-0.1.6.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
aline_ai-0.1.
|
|
2
|
-
realign/__init__.py,sha256=
|
|
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
|
|
10
|
-
realign/mcp_watcher.py,sha256=
|
|
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.
|
|
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.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
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)
|
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
214
|
-
"""
|
|
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 =
|
|
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(
|
|
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(
|
|
318
|
+
repo_path=str(project_path),
|
|
274
319
|
stage_all=True,
|
|
275
320
|
amend=False,
|
|
276
321
|
no_edit=False,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|