aline-ai 0.2.4__py3-none-any.whl → 0.2.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.2.4
3
+ Version: 0.2.5
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -1,27 +1,28 @@
1
- aline_ai-0.2.4.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
- realign/__init__.py,sha256=DWRnxKpLy7YmDatlK_Q7QaCcwo6p-FhjIz_gNyJ0o4o,68
1
+ aline_ai-0.2.5.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
+ realign/__init__.py,sha256=zx1q8jbbAtSFt2eDYbUqaqss8LohQnvmI4gRY_HWVWY,68
3
3
  realign/claude_detector.py,sha256=NLxI0zJWcqNxNha9jAy9AslTMwHKakCc9yPGdkrbiFE,3028
4
- realign/cli.py,sha256=OC5KWJ__7_MXbFRlacVxE-rkylBcDIYxdhVbfwsDbY0,1936
4
+ realign/cli.py,sha256=WSdT9zpHLswBeDPjb5M_U2cvhIm7yrgnXPDyb7vD-jg,2798
5
5
  realign/codex_detector.py,sha256=RI3JbZgebrhoqpRfTBMfclYCAISN7hZAHVW3bgftJpU,4428
6
6
  realign/config.py,sha256=jarinbr0mA6e5DmgY19b_VpMnxk6SOYTwyvB9luq0ww,7207
7
7
  realign/file_lock.py,sha256=-9c3tMdMj_ZxmasK5y6hV9Gfo6KDsSO3Q7PXiTBhsu4,3369
8
8
  realign/hooks.py,sha256=GzkEbW35-ifwAilYbVxDpNURZ_7XrF68DTqDSQ8v7fE,50670
9
9
  realign/logging_config.py,sha256=KvkKktF-bkUu031y9vgUoHpsbnOw7ud25jhpzliNZwA,4929
10
- realign/mcp_server.py,sha256=dntFatMpozI80K5hHrIiQ9sviC6ARKTP89goULhi1T4,16477
10
+ realign/mcp_server.py,sha256=Q82nuEm6LF17eKUZfHHt6exQjzOWbbBmGXLYwNIGnoo,21002
11
11
  realign/mcp_watcher.py,sha256=ffYOXDLuf9T6Kab3CdGNAOY3DBlAbjZrVrSjM5RdYGU,26828
12
12
  realign/redactor.py,sha256=FizaGSdW-QTBAQl4h-gtmMpx2mFrfd2a5DoPEPyLfRg,9989
13
- realign/commands/__init__.py,sha256=m4bAl1uQoPN6QQV7JUCD0zaLBMXFAXP0Kd4-Vpq9i8g,199
13
+ realign/commands/__init__.py,sha256=caHulsUeguKyy2ZIIa9hVwzGwNHfIbeHwZIC67C8gnI,213
14
14
  realign/commands/auto_commit.py,sha256=jgjAYZHqN34NmQkncZg3Vtwsl3MyAlsvucxEBwUj7ko,7450
15
15
  realign/commands/commit.py,sha256=mlwrv5nfTRY17WlcAdiJKKGh5uM7dGvT7sMxhdbsfkw,12605
16
16
  realign/commands/config.py,sha256=iiu7usqw00djKZja5bx0iDH8DB0vU2maUPMkXLdgXwI,6609
17
17
  realign/commands/hide.py,sha256=i_XLsmsB4duLKNIA-eRAUvniS4N6GdQUyfN92CGndEA,34138
18
18
  realign/commands/init.py,sha256=vOXSAveWgP8TWp0rgqZO8zNRKZMPiaxWsg4PeDZ9RVs,13586
19
+ realign/commands/push.py,sha256=77GXeKggzzc-100JN1PSgDFcDhSaB4kv2jN24h5YM6I,10249
19
20
  realign/commands/review.py,sha256=3TY6F87RGNEbutxvUr_konr24-gCyj5fmD9usd3n4-U,15570
20
21
  realign/commands/search.py,sha256=xTWuX0lpjQPX8cen0ewl-BNF0FeWgjMwN06bdeesED8,18770
21
22
  realign/commands/session_utils.py,sha256=L1DwZIGCOBirp6tkAswACJEeDa6i9aAAfsialAs4rRY,864
22
23
  realign/commands/show.py,sha256=A9LvhOBcY6_HoI76irPB2rBOSgdftBuX2uZiO8IwNoU,16338
23
- aline_ai-0.2.4.dist-info/METADATA,sha256=Hgg_pnm9t39cmHHrFRMllNsfASfBgHWbdEc8AtY8GZ0,1437
24
- aline_ai-0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- aline_ai-0.2.4.dist-info/entry_points.txt,sha256=h-NocHDzSueXfsepHTIdRPNQzhNZQPAztJfldd-mQTE,202
26
- aline_ai-0.2.4.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
27
- aline_ai-0.2.4.dist-info/RECORD,,
24
+ aline_ai-0.2.5.dist-info/METADATA,sha256=Deqqt3Tjs66ZW_76UA7itCXB_YLnhcO-rGa6vN_6vys,1437
25
+ aline_ai-0.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
+ aline_ai-0.2.5.dist-info/entry_points.txt,sha256=h-NocHDzSueXfsepHTIdRPNQzhNZQPAztJfldd-mQTE,202
27
+ aline_ai-0.2.5.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
28
+ aline_ai-0.2.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.2.4"
3
+ __version__ = "0.2.5"
realign/cli.py CHANGED
@@ -7,7 +7,7 @@ from typing import Optional
7
7
  from rich.console import Console
8
8
  from rich.syntax import Syntax
9
9
 
10
- from .commands import init, search, show, config, commit, auto_commit, review, hide
10
+ from .commands import init, search, show, config, commit, auto_commit, review, hide, push
11
11
 
12
12
  app = typer.Typer(
13
13
  name="realign",
@@ -48,6 +48,29 @@ def hide_cli(
48
48
  raise typer.Exit(code=exit_code)
49
49
 
50
50
 
51
+ @app.command(name="push")
52
+ def push_cli(
53
+ remote: str = typer.Option("origin", "--remote", "-r", help="Git remote name"),
54
+ branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Branch to push (default: current branch)"),
55
+ force: bool = typer.Option(False, "--force", "-f", help="Skip all confirmation prompts"),
56
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be pushed without actually pushing"),
57
+ ):
58
+ """Interactive push workflow with review and hide options.
59
+
60
+ This command provides an interactive workflow:
61
+ 1. Review unpushed commits
62
+ 2. Optionally hide sensitive commits
63
+ 3. Final confirmation before pushing
64
+ """
65
+ exit_code = push.push_command(
66
+ remote=remote,
67
+ branch=branch,
68
+ force=force,
69
+ dry_run=dry_run
70
+ )
71
+ raise typer.Exit(code=exit_code)
72
+
73
+
51
74
  @app.command()
52
75
  def version():
53
76
  """Show ReAlign version."""
@@ -1,5 +1,5 @@
1
1
  """ReAlign commands module."""
2
2
 
3
- from . import init, search, show, config, commit, auto_commit, review, hide
3
+ from . import init, search, show, config, commit, auto_commit, review, hide, push
4
4
 
5
- __all__ = ["init", "search", "show", "config", "commit", "auto_commit", "review", "hide"]
5
+ __all__ = ["init", "search", "show", "config", "commit", "auto_commit", "review", "hide", "push"]
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Push command - Interactive workflow for reviewing and pushing commits.
4
+
5
+ This command provides an interactive workflow:
6
+ 1. Run aline review to show unpushed commits
7
+ 2. Ask user if they want to hide any commits
8
+ 3. If yes, allow interactive hiding
9
+ 4. Ask for final confirmation before pushing
10
+ """
11
+
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ from .review import review_command, get_unpushed_commits
18
+ from .hide import hide_command, parse_commit_indices
19
+ from ..logging_config import setup_logger
20
+
21
+ logger = setup_logger('realign.commands.push', 'push.log')
22
+
23
+
24
+ def prompt_yes_no(question: str, default: bool = False) -> bool:
25
+ """
26
+ Prompt user for yes/no confirmation.
27
+
28
+ Args:
29
+ question: Question to ask
30
+ default: Default answer if user just presses Enter
31
+
32
+ Returns:
33
+ True for yes, False for no
34
+ """
35
+ default_str = "Y/n" if default else "y/N"
36
+ prompt = f"{question} [{default_str}] "
37
+
38
+ while True:
39
+ response = input(prompt).strip().lower()
40
+
41
+ if not response:
42
+ return default
43
+
44
+ if response in ('y', 'yes'):
45
+ return True
46
+ elif response in ('n', 'no'):
47
+ return False
48
+ else:
49
+ print("Please answer 'y' or 'n'")
50
+
51
+
52
+ def push_command(
53
+ repo_root: Optional[Path] = None,
54
+ remote: str = "origin",
55
+ branch: Optional[str] = None,
56
+ force: bool = False,
57
+ dry_run: bool = False
58
+ ) -> int:
59
+ """
60
+ Interactive push workflow with review and hide options.
61
+
62
+ Workflow:
63
+ 1. Review unpushed commits
64
+ 2. Ask if user wants to hide any commits
65
+ 3. Interactive hiding loop (if requested)
66
+ 4. Final confirmation
67
+ 5. Execute git push
68
+
69
+ Args:
70
+ repo_root: Path to repository root (auto-detected if None)
71
+ remote: Git remote name (default: origin)
72
+ branch: Branch to push (default: current branch)
73
+ force: Skip all confirmation prompts
74
+ dry_run: Don't actually push, just show what would happen
75
+
76
+ Returns:
77
+ 0 on success, 1 on error, 2 if cancelled by user
78
+ """
79
+ logger.info("======== Push command started ========")
80
+
81
+ # Auto-detect repo root if not provided
82
+ if repo_root is None:
83
+ try:
84
+ result = subprocess.run(
85
+ ["git", "rev-parse", "--show-toplevel"],
86
+ capture_output=True,
87
+ text=True,
88
+ check=True
89
+ )
90
+ repo_root = Path(result.stdout.strip())
91
+ logger.debug(f"Detected repo root: {repo_root}")
92
+ except subprocess.CalledProcessError:
93
+ print("Error: Not in a git repository", file=sys.stderr)
94
+ logger.error("Not in a git repository")
95
+ return 1
96
+
97
+ # Detect current branch if not provided
98
+ if branch is None:
99
+ try:
100
+ result = subprocess.run(
101
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
102
+ cwd=repo_root,
103
+ capture_output=True,
104
+ text=True,
105
+ check=True
106
+ )
107
+ branch = result.stdout.strip()
108
+ logger.debug(f"Detected current branch: {branch}")
109
+ except subprocess.CalledProcessError:
110
+ print("Error: Failed to detect current branch", file=sys.stderr)
111
+ logger.error("Failed to detect current branch")
112
+ return 1
113
+
114
+ # Step 1: Review unpushed commits
115
+ print("\n" + "=" * 60)
116
+ print("STEP 1: Review unpushed commits")
117
+ print("=" * 60 + "\n")
118
+
119
+ try:
120
+ commits = get_unpushed_commits(repo_root)
121
+ except Exception as e:
122
+ print(f"Error: Failed to get unpushed commits: {e}", file=sys.stderr)
123
+ logger.error(f"Failed to get unpushed commits: {e}", exc_info=True)
124
+ return 1
125
+
126
+ if not commits:
127
+ print("✓ No unpushed commits found. Nothing to push.\n")
128
+ logger.info("No unpushed commits found")
129
+ return 0
130
+
131
+ # Run review command to display commits
132
+ exit_code = review_command(repo_root=repo_root, verbose=False, detect_secrets=False)
133
+ if exit_code != 0:
134
+ logger.error("Review command failed")
135
+ return 1
136
+
137
+ # Step 2: Ask if user wants to hide any commits
138
+ if not force:
139
+ print("\n" + "=" * 60)
140
+ print("STEP 2: Hide sensitive commits (optional)")
141
+ print("=" * 60 + "\n")
142
+
143
+ want_to_hide = prompt_yes_no(
144
+ "Do you want to hide any commits before pushing?",
145
+ default=False
146
+ )
147
+
148
+ if want_to_hide:
149
+ # Interactive hiding loop
150
+ while True:
151
+ print("\nEnter commit indices to hide (e.g., '1', '1,3,5', '2-4', or 'done'):")
152
+ print(" Type 'done' when finished hiding commits")
153
+ print(" Type 'review' to see the current list of commits")
154
+ print(" Type 'cancel' to abort the push operation")
155
+
156
+ user_input = input("\n> ").strip()
157
+
158
+ if not user_input:
159
+ continue
160
+
161
+ if user_input.lower() == 'done':
162
+ break
163
+ elif user_input.lower() == 'cancel':
164
+ print("\n❌ Push cancelled by user.\n")
165
+ logger.info("Push cancelled by user during hide phase")
166
+ return 2
167
+ elif user_input.lower() == 'review':
168
+ print("\n" + "-" * 60)
169
+ review_command(repo_root=repo_root, verbose=False, detect_secrets=False)
170
+ print("-" * 60)
171
+ continue
172
+
173
+ # Try to parse and hide the commits
174
+ try:
175
+ # Validate indices first
176
+ indices_list = parse_commit_indices(user_input)
177
+
178
+ # Refresh commit list
179
+ commits = get_unpushed_commits(repo_root)
180
+ if not commits:
181
+ print("\n✓ No more unpushed commits to hide.\n")
182
+ break
183
+
184
+ max_index = len(commits)
185
+ invalid_indices = [i for i in indices_list if i < 1 or i > max_index]
186
+ if invalid_indices:
187
+ print(f"❌ Invalid indices (out of range 1-{max_index}): {invalid_indices}")
188
+ continue
189
+
190
+ # Execute hide command
191
+ print()
192
+ exit_code = hide_command(
193
+ indices=user_input,
194
+ repo_root=repo_root,
195
+ force=False # Always ask for confirmation
196
+ )
197
+
198
+ if exit_code == 0:
199
+ print("\n✅ Commits hidden successfully.\n")
200
+ logger.info(f"Successfully hidden commits: {user_input}")
201
+
202
+ # Refresh and show updated commit list
203
+ commits = get_unpushed_commits(repo_root)
204
+ if not commits:
205
+ print("✓ No more unpushed commits.\n")
206
+ logger.info("No commits left after hiding")
207
+ return 0
208
+ else:
209
+ print("\n📋 Updated commit list:\n")
210
+ review_command(repo_root=repo_root, verbose=False, detect_secrets=False)
211
+ else:
212
+ print(f"\n❌ Failed to hide commits.\n")
213
+ logger.error(f"Hide command failed for indices: {user_input}")
214
+
215
+ # Ask if user wants to continue
216
+ if not prompt_yes_no("Continue with push workflow?", default=True):
217
+ print("\n❌ Push cancelled.\n")
218
+ return 2
219
+
220
+ except ValueError as e:
221
+ print(f"❌ Invalid input: {e}")
222
+ print(" Examples: '1', '1,3,5', '2-4'")
223
+ continue
224
+ except Exception as e:
225
+ print(f"❌ Error during hide: {e}", file=sys.stderr)
226
+ logger.error(f"Error during hide: {e}", exc_info=True)
227
+
228
+ # Ask if user wants to continue
229
+ if not prompt_yes_no("Continue with push workflow?", default=True):
230
+ print("\n❌ Push cancelled.\n")
231
+ return 2
232
+
233
+ # Step 3: Final confirmation
234
+ if not force:
235
+ print("\n" + "=" * 60)
236
+ print("STEP 3: Final confirmation")
237
+ print("=" * 60 + "\n")
238
+
239
+ # Refresh commit list one more time
240
+ try:
241
+ commits = get_unpushed_commits(repo_root)
242
+ except Exception as e:
243
+ print(f"Error: Failed to refresh commits: {e}", file=sys.stderr)
244
+ return 1
245
+
246
+ if not commits:
247
+ print("✓ No unpushed commits. Nothing to push.\n")
248
+ return 0
249
+
250
+ print(f"Ready to push {len(commits)} commit(s) to {remote}/{branch}\n")
251
+
252
+ if not prompt_yes_no("Proceed with git push?", default=True):
253
+ print("\n❌ Push cancelled by user.\n")
254
+ logger.info("Push cancelled by user at final confirmation")
255
+ return 2
256
+
257
+ # Step 4: Execute git push
258
+ print("\n" + "=" * 60)
259
+ print("STEP 4: Pushing to remote")
260
+ print("=" * 60 + "\n")
261
+
262
+ if dry_run:
263
+ print(f"🔍 DRY RUN: Would execute: git push {remote} {branch}\n")
264
+ logger.info(f"Dry run: would push to {remote}/{branch}")
265
+ return 0
266
+
267
+ try:
268
+ print(f"🚀 Pushing to {remote}/{branch}...")
269
+
270
+ result = subprocess.run(
271
+ ["git", "push", remote, branch],
272
+ cwd=repo_root,
273
+ capture_output=True,
274
+ text=True
275
+ )
276
+
277
+ if result.returncode == 0:
278
+ print("\n✅ Successfully pushed to remote!\n")
279
+ print(result.stdout)
280
+ logger.info(f"Successfully pushed to {remote}/{branch}")
281
+ return 0
282
+ else:
283
+ print(f"\n❌ Push failed:\n{result.stderr}\n", file=sys.stderr)
284
+ logger.error(f"Git push failed: {result.stderr}")
285
+ return 1
286
+
287
+ except Exception as e:
288
+ print(f"\n❌ Error during push: {e}\n", file=sys.stderr)
289
+ logger.error(f"Error during push: {e}", exc_info=True)
290
+ return 1
realign/mcp_server.py CHANGED
@@ -170,6 +170,63 @@ async def list_tools() -> list[Tool]:
170
170
  "properties": {},
171
171
  },
172
172
  ),
173
+ Tool(
174
+ name="aline_review",
175
+ description=(
176
+ "Review unpushed commits before pushing. "
177
+ "Shows commit messages, LLM summaries, session files modified, and line counts. "
178
+ "Helps identify what content will be made public when you push."
179
+ ),
180
+ inputSchema={
181
+ "type": "object",
182
+ "properties": {
183
+ "repo_path": {
184
+ "type": "string",
185
+ "description": "Path to repository root (default: current directory)",
186
+ },
187
+ "verbose": {
188
+ "type": "boolean",
189
+ "description": "Show detailed information",
190
+ "default": False,
191
+ },
192
+ "detect_secrets": {
193
+ "type": "boolean",
194
+ "description": "Run sensitive content detection",
195
+ "default": False,
196
+ },
197
+ },
198
+ },
199
+ ),
200
+ Tool(
201
+ name="aline_hide",
202
+ description=(
203
+ "Hide (redact) specific unpushed commits by rewriting git history. "
204
+ "This redacts commit messages and session content. "
205
+ "WARNING: This rewrites git history. Use with caution."
206
+ ),
207
+ inputSchema={
208
+ "type": "object",
209
+ "properties": {
210
+ "indices": {
211
+ "type": "string",
212
+ "description": (
213
+ "Commit indices to hide (e.g., '1', '1,3,5', '2-4', or '--all'). "
214
+ "Use aline_review first to see the commit indices."
215
+ ),
216
+ },
217
+ "repo_path": {
218
+ "type": "string",
219
+ "description": "Path to repository root (default: current directory)",
220
+ },
221
+ "force": {
222
+ "type": "boolean",
223
+ "description": "Skip confirmation prompt",
224
+ "default": False,
225
+ },
226
+ },
227
+ "required": ["indices"],
228
+ },
229
+ ),
173
230
  ]
174
231
 
175
232
 
@@ -189,6 +246,10 @@ async def call_tool(name: str, arguments: Any) -> list[TextContent]:
189
246
  result = await handle_get_latest_session(arguments)
190
247
  elif name == "aline_version":
191
248
  result = await handle_version(arguments)
249
+ elif name == "aline_review":
250
+ result = await handle_review(arguments)
251
+ elif name == "aline_hide":
252
+ result = await handle_hide(arguments)
192
253
  else:
193
254
  return [TextContent(type="text", text=f"Unknown tool: {name}")]
194
255
 
@@ -395,6 +456,78 @@ async def handle_version(args: dict) -> list[TextContent]:
395
456
  )]
396
457
 
397
458
 
459
+ async def handle_review(args: dict) -> list[TextContent]:
460
+ """Handle aline_review tool."""
461
+ from .commands.review import review_command
462
+ from io import StringIO
463
+ import sys
464
+
465
+ repo_path = args.get("repo_path")
466
+ verbose = args.get("verbose", False)
467
+ detect_secrets = args.get("detect_secrets", False)
468
+
469
+ # Convert repo_path to Path if provided
470
+ repo_root = Path(repo_path) if repo_path else None
471
+
472
+ # Capture stdout
473
+ old_stdout = sys.stdout
474
+ sys.stdout = captured_output = StringIO()
475
+
476
+ try:
477
+ # Call the review command
478
+ exit_code = review_command(
479
+ repo_root=repo_root,
480
+ verbose=verbose,
481
+ detect_secrets=detect_secrets
482
+ )
483
+
484
+ # Get the output
485
+ output = captured_output.getvalue()
486
+
487
+ if exit_code == 0:
488
+ return [TextContent(type="text", text=output or "No unpushed commits found.")]
489
+ else:
490
+ return [TextContent(type="text", text=f"Error: Review failed\n{output}")]
491
+ finally:
492
+ sys.stdout = old_stdout
493
+
494
+
495
+ async def handle_hide(args: dict) -> list[TextContent]:
496
+ """Handle aline_hide tool."""
497
+ from .commands.hide import hide_command
498
+ from io import StringIO
499
+ import sys
500
+
501
+ indices = args["indices"]
502
+ repo_path = args.get("repo_path")
503
+ force = args.get("force", False)
504
+
505
+ # Convert repo_path to Path if provided
506
+ repo_root = Path(repo_path) if repo_path else None
507
+
508
+ # Capture stdout
509
+ old_stdout = sys.stdout
510
+ sys.stdout = captured_output = StringIO()
511
+
512
+ try:
513
+ # Call the hide command
514
+ exit_code = hide_command(
515
+ indices=indices,
516
+ repo_root=repo_root,
517
+ force=force
518
+ )
519
+
520
+ # Get the output
521
+ output = captured_output.getvalue()
522
+
523
+ if exit_code == 0:
524
+ return [TextContent(type="text", text=output or "Hide operation completed.")]
525
+ else:
526
+ return [TextContent(type="text", text=f"Error: Hide failed\n{output}")]
527
+ finally:
528
+ sys.stdout = old_stdout
529
+
530
+
398
531
  def _server_log(msg: str):
399
532
  """Log MCP server messages to both stderr and file."""
400
533
  from datetime import datetime