aline-ai 0.5.13__py3-none-any.whl → 0.6.1__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.5.13
3
+ Version: 0.6.1
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -1,8 +1,8 @@
1
- aline_ai-0.5.13.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
- realign/__init__.py,sha256=7hBPixwKhL2BzvTWOw-rerx1yBjnC8f5kVd0IDyl-Bo,1624
3
- realign/auth.py,sha256=63fdy-KsNoLZ9A6X0Mz_v-0tQOXN_1XncXBGBlEoXqE,16030
1
+ aline_ai-0.6.1.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
+ realign/__init__.py,sha256=ohwpTX-XJAcf8rW1lcIJqXudQQ72HJvA9d0RsAJhURU,1623
3
+ realign/auth.py,sha256=d_1yvCwluN5iIrdgjtuSKpOYAksDzrzNgntKacLVJrw,16583
4
4
  realign/claude_detector.py,sha256=ZLSJacMo6zzQclXByABKA70UNpstxqIv3fPGqdpA934,2792
5
- realign/cli.py,sha256=RuFw_FWnKcRm0-3J-pGNR-S9e0D1rZ-ptAPlO2FFOlM,31807
5
+ realign/cli.py,sha256=HIKpLIx7lwHV-Gd3cbdZVaOst784or6asQdAI-DFE80,43543
6
6
  realign/codex_detector.py,sha256=N9ulgMgvTzDfXE4s4vLd6OoS0hT7R6h2bDFFXWa-2hE,4183
7
7
  realign/config.py,sha256=d8HQ6v1xju6cjNGbR7LlfHaMyvFPcMDofhGbepxpQq8,11634
8
8
  realign/context.py,sha256=8hzgNOg-7_eMW22wt7OM5H9IsmMveKXCv0epG7E0G7w,13917
@@ -14,9 +14,9 @@ realign/mcp_server.py,sha256=LWiQ2qukYoNLsoV2ID2f0vF9jkJlBvB587HpM5jymgE,10193
14
14
  realign/mcp_watcher.py,sha256=aK4jWStv7CoCroS4tXFHgZ_y_-q4QDjrpWgm4DxcEj4,1260
15
15
  realign/redactor.py,sha256=Zsoi5HfYak2yPmck20JArhm-1cPSB78IdkBJiNVXfrc,17096
16
16
  realign/watcher_core.py,sha256=NNn_xlm50Ybb60--DrF9dyvzGgJ4ENQeAUbZsH7w4to,106555
17
- realign/watcher_daemon.py,sha256=AVOMXrlVVy7Rlx3Yfib4e-KLszIR7CLdSHpdoxDRp8c,3090
17
+ realign/watcher_daemon.py,sha256=OHUQ9P1LlagKJHfrf6uRnzO-zDtBRXIxt8ydMFHf5S8,3475
18
18
  realign/worker_core.py,sha256=-GOItHE0vzExB8LZK6KeHx4tt_mIqtCoUljOtEg2x8A,10105
19
- realign/worker_daemon.py,sha256=LpJbQDY0Z4AMtq0LmpxvFeQM4puuoGDRBayKRafvKhc,3574
19
+ realign/worker_daemon.py,sha256=X7Xyjw_u6m6KG4E84nx0HpDFw4cWMv8ja1G8btc9PiM,3957
20
20
  realign/adapters/__init__.py,sha256=bpDm5aBxMdq4OA_beYahoUb4zfNaq3KOG6KghQJruRc,827
21
21
  realign/adapters/antigravity.py,sha256=geaYxAEswpgsVtERqsQ1OwvPFsy5tRkyjx2yQ-Uq9nM,5461
22
22
  realign/adapters/base.py,sha256=2IdAZKGjg5gPB3YLf_8r3V4XAdbK7fHpj06GjjsYEFY,7409
@@ -34,20 +34,25 @@ realign/claude_hooks/user_prompt_submit_hook.py,sha256=WD-UavhBTueN2TPfnZrnPC7DF
34
34
  realign/claude_hooks/user_prompt_submit_hook_installer.py,sha256=2xLF8yZcE7Iwib9gU-xCkA1NWxNH9Nc5CFKPYK7rtXw,5371
35
35
  realign/commands/__init__.py,sha256=sx_ck55oxaoiF4N3LugG0ZXwonUDxeEZ5uHbBKCC7K8,89
36
36
  realign/commands/add.py,sha256=njZgg3paUmOw-sb-sWkXr_eUaf5bD-hBEiRectaphPs,24332
37
- realign/commands/auth.py,sha256=AnUXpnRBkdBflFIweUz5WQKFVmtyxWRjwvw-96w0rRw,7625
37
+ realign/commands/auth.py,sha256=QrPukpP-ogYEDSwztV0NOYI-HDgn5fPxlCQ1-e2n7gU,11082
38
38
  realign/commands/config.py,sha256=nYnu_h2pk7GODcrzrV04K51D-s7v06FlRXHJ0HJ-gvU,6732
39
39
  realign/commands/context.py,sha256=pM2KfZHVkB-ou4nBhFvKSwnYliLBzwN3zerLyBAbhfE,7095
40
40
  realign/commands/export_shares.py,sha256=g2SxlPEb7FPMarjTZcoZntviZo5JdqEe-M28vggc2-s,136601
41
- realign/commands/import_shares.py,sha256=4_Bzf9IgSpK2oOLoYWEnF1BmpXZaB5ijvpUQPeCtIfg,25386
41
+ realign/commands/import_shares.py,sha256=HiswLlYHqR0dR3wgB7Rs54_WownqahIs5IdyJOHuot8,25572
42
42
  realign/commands/init.py,sha256=nhP1Qjl6Xo5R1ry_iTGVu3RwMxP-pYT5Z50NdzEMKrY,32756
43
43
  realign/commands/restore.py,sha256=s2BxQZHxQw9r12NzRVsK20KlGafy5AIoSjWMo5PcnHY,11173
44
44
  realign/commands/search.py,sha256=QJrC0hln9sCDFxXbpo0nPGMHXrud18qA5QfRyD0z6fQ,25926
45
45
  realign/commands/upgrade.py,sha256=L3PLOUIN5qAQTbkfoVtSsIbbzEezA_xjjk9F1GMVfjw,12781
46
- realign/commands/watcher.py,sha256=VunN3deqE1_DLW9UcFgLj_MFX8jNkVr2Ra7aNmWG9xA,136006
47
- realign/commands/worker.py,sha256=K1DG1uZ--ebKwklHCyIFdN_axoLjL9Onx8Naq-DOZBs,23078
46
+ realign/commands/watcher.py,sha256=rB3x2diC7scqghl7wLAeBNgMj4NLXUKbr0ou1_VVUIU,136292
47
+ realign/commands/worker.py,sha256=jTu7Pj60nTnn7SsH3oNCNnO6zl4TIFCJVNSC1OoQ_0o,23363
48
48
  realign/dashboard/__init__.py,sha256=QZkHTsGityH8UkF8rmvA3xW7dMXNe0swEWr443qfgCM,128
49
- realign/dashboard/app.py,sha256=jyW6mqmItTy253CPSqInxctkWzkrGEikdy-ikuShQ14,13299
49
+ realign/dashboard/app.py,sha256=l4qbjQfnUJCzX2M0A5vPJ-fTX-Tz2Ykw4_cY0eMC1LE,16029
50
+ realign/dashboard/layout.py,sha256=sZxmFj6QTbkois9MHTvBEMMcnaRVehCDqugdbiFx10k,9072
51
+ realign/dashboard/terminal_backend.py,sha256=MlDfwtqhftyQK6jDNizQGFjAWIo5Bx2TDpSnP3MCZVM,3375
50
52
  realign/dashboard/tmux_manager.py,sha256=Vt_30WNtDg7c_9SEh8xdDtBLJ8kNq6bGSPh5r3VXpg0,26276
53
+ realign/dashboard/backends/__init__.py,sha256=POROX7YKtukYZcLB1pi_kO0sSEpuO3y-hwmF3WIN1Kk,163
54
+ realign/dashboard/backends/iterm2.py,sha256=XYYJT5lrrp4pW_MyEqPZYkRI0qyKUwJlezwMidgnsHc,21390
55
+ realign/dashboard/backends/kitty.py,sha256=5jdkR1f2PwB8a4SnS3EG6uOQ2XU-PB7-cpKBfIJq3hU,12066
51
56
  realign/dashboard/screens/__init__.py,sha256=US6sAmQs5VVkH2tFkH_z0WDT4H8cVhLL-JckfSR1yQY,446
52
57
  realign/dashboard/screens/create_agent.py,sha256=lpcT1zLq_p02codtHTE8KdbEzCEaNLnk1lqU3QLcXCg,10057
53
58
  realign/dashboard/screens/create_event.py,sha256=oiQY1zKpUYnQU-5fQLeuZH9BV5NClE5B5XZIVBYG5A8,5506
@@ -58,12 +63,12 @@ realign/dashboard/screens/share_import.py,sha256=hl2x0yGVycsoUI76AmdZTAV-br3Q619
58
63
  realign/dashboard/styles/dashboard.tcss,sha256=ewonevBGLN-dfSsgxUk4VBCPchtxY4rx_vj1u6Ox2Fw,3454
59
64
  realign/dashboard/widgets/__init__.py,sha256=3Pf2_K9obrertgv_psfxradgkI9RXlmjoXYQH7oBKm0,583
60
65
  realign/dashboard/widgets/config_panel.py,sha256=JRv9Hgm5V-vqVptS7gQqnjg5MbKpt_FmdZV13D4E9A4,17894
61
- realign/dashboard/widgets/events_table.py,sha256=dXN_aD94YJ1fDSV2B_5m7YMvMU3bNUhGFCMIRWvvLMg,31141
66
+ realign/dashboard/widgets/events_table.py,sha256=Ru4y5Xq04Y-3CN9Dwm-fHrHR37Aup9NFW_Yh-J1x-Jo,33247
62
67
  realign/dashboard/widgets/header.py,sha256=0HHCFXX7F3C6HII-WDwOJwWkJrajmKPWmdoMWyOkn9E,1587
63
68
  realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
64
69
  realign/dashboard/widgets/search_panel.py,sha256=ZNJDfwDSxUFnCeltYQYsQsPJ6t4HDeNWpENoTOoBdVM,8951
65
- realign/dashboard/widgets/sessions_table.py,sha256=PohOkg-ESLBa-Sq0PdLPhV-YzVXOGpUo5ETs0MYO4u8,33415
66
- realign/dashboard/widgets/terminal_panel.py,sha256=uoi3LjgYWyFE6Yr208KC5iKg0QxLcXpN6hCERlI6pBg,29069
70
+ realign/dashboard/widgets/sessions_table.py,sha256=GyaWzvt-elKx1iE2YR94CBNPyjqM8B7g0j7zsukiXi0,36517
71
+ realign/dashboard/widgets/terminal_panel.py,sha256=7VrLN_Wsk98O0rojckrqJ_k5TGM47hP4UqXFvuRTvRs,45097
67
72
  realign/dashboard/widgets/watcher_panel.py,sha256=O_mdDacgc87xA-5KEfta53Ik_Xsk_B2OfwenMOTtGw8,19722
68
73
  realign/dashboard/widgets/worker_panel.py,sha256=F_jKWABuCNmjQgeeuCr4KnFRKdY4CLTNcEXMYwsNaSk,18691
69
74
  realign/db/__init__.py,sha256=65LsNdsq_rkwNC1eg1OAr3HC0ORXtelOh0I8MhNGr-g,3288
@@ -91,8 +96,8 @@ realign/triggers/next_turn_trigger.py,sha256=BpP0PWn4mU1MZd6mv89jWcjs8Jtv0zEWapW
91
96
  realign/triggers/registry.py,sha256=cb-AVLbYB2pqwfWL3q1DQxLv4kOw7g7m-GshTdfFESc,3827
92
97
  realign/triggers/turn_status.py,sha256=wAZEhXDAmDoX5F-ohWfSnZZ0eA6DAJ9svSPiSv_f6sg,6041
93
98
  realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
94
- aline_ai-0.5.13.dist-info/METADATA,sha256=yh0oIdfIbiWQHUhP8UPOdsLFxqCjsdvF3xYfAWquT9c,1598
95
- aline_ai-0.5.13.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
- aline_ai-0.5.13.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
97
- aline_ai-0.5.13.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
98
- aline_ai-0.5.13.dist-info/RECORD,,
99
+ aline_ai-0.6.1.dist-info/METADATA,sha256=ftCsc3aI1fjTZhCJd8rE_7-teyrN8ARYIyEmDGMtJdg,1597
100
+ aline_ai-0.6.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
101
+ aline_ai-0.6.1.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
102
+ aline_ai-0.6.1.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
103
+ aline_ai-0.6.1.dist-info/RECORD,,
realign/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  import hashlib
4
4
  from pathlib import Path
5
5
 
6
- __version__ = "0.5.13"
6
+ __version__ = "0.6.1"
7
7
 
8
8
 
9
9
  def get_realign_dir(project_root: Path) -> Path:
realign/auth.py CHANGED
@@ -44,6 +44,7 @@ logger = setup_logger("realign.auth", "auth.log")
44
44
  # Hardcoded Supabase/backend configuration
45
45
  DEFAULT_AUTH_URL = "https://realign-server.vercel.app"
46
46
  CLI_LOGIN_PATH = "/cli-login"
47
+ CLI_LOGOUT_PATH = "/cli-logout"
47
48
  CLI_VALIDATE_PATH = "/api/auth/cli/validate"
48
49
  CLI_REFRESH_PATH = "/api/auth/cli/refresh"
49
50
 
@@ -394,6 +395,26 @@ def open_login_page(callback_port: Optional[int] = None) -> str:
394
395
  return login_url
395
396
 
396
397
 
398
+ def open_logout_page() -> str:
399
+ """
400
+ Open the web logout page in browser to sign out from web session.
401
+
402
+ Returns:
403
+ The URL that was opened
404
+ """
405
+ config = ReAlignConfig.load()
406
+ backend_url = config.share_backend_url or DEFAULT_AUTH_URL
407
+ logout_url = f"{backend_url}{CLI_LOGOUT_PATH}"
408
+
409
+ try:
410
+ webbrowser.open(logout_url)
411
+ logger.info(f"Opened logout page: {logout_url}")
412
+ except Exception as e:
413
+ logger.warning(f"Failed to open browser: {e}")
414
+
415
+ return logout_url
416
+
417
+
397
418
  # Local callback server for automatic token receipt
398
419
  _received_token: Optional[str] = None
399
420
  _server_error: Optional[str] = None
realign/cli.py CHANGED
@@ -33,6 +33,26 @@ def main(
33
33
  ctx.obj["dev"] = dev
34
34
 
35
35
  if ctx.invoked_subcommand is None:
36
+ # Check login status before launching dashboard
37
+ from .auth import is_logged_in, get_current_user
38
+
39
+ if not is_logged_in():
40
+ console.print("[yellow]You need to login before using Aline.[/yellow]")
41
+ console.print("Starting login flow...\n")
42
+
43
+ # Run login command
44
+ exit_code = auth.login_command()
45
+ if exit_code != 0:
46
+ console.print("\n[red]Login failed. Please try again with 'aline login'.[/red]")
47
+ raise typer.Exit(code=1)
48
+
49
+ # Verify login succeeded
50
+ if not is_logged_in():
51
+ console.print("\n[red]Login verification failed. Please try again.[/red]")
52
+ raise typer.Exit(code=1)
53
+
54
+ console.print() # Add spacing before dashboard launch
55
+
36
56
  # Check for updates before launching dashboard
37
57
  from .commands.upgrade import check_and_prompt_update
38
58
 
@@ -41,13 +61,33 @@ def main(
41
61
  raise typer.Exit(0)
42
62
 
43
63
  # Launch the dashboard when no subcommand is provided
44
- from .dashboard.tmux_manager import bootstrap_dashboard_into_tmux
64
+ import os
65
+
66
+ terminal_mode = os.environ.get("ALINE_TERMINAL_MODE", "").strip().lower()
67
+ use_native_terminal = terminal_mode in {"native", "iterm2", "iterm", "kitty"}
45
68
 
46
- bootstrap_dashboard_into_tmux()
69
+ right_pane_session_id = None
70
+
71
+ if not use_native_terminal:
72
+ # Only bootstrap tmux for tmux mode (default)
73
+ from .dashboard.tmux_manager import bootstrap_dashboard_into_tmux
74
+
75
+ bootstrap_dashboard_into_tmux()
76
+ elif terminal_mode in {"iterm2", "iterm"}:
77
+ # Set up split pane layout for iTerm2
78
+ try:
79
+ from .dashboard.backends.iterm2 import setup_split_pane_layout_sync
80
+
81
+ right_pane_session_id = setup_split_pane_layout_sync()
82
+ if right_pane_session_id:
83
+ # Store in environment for dashboard to pick up
84
+ os.environ["ALINE_ITERM2_RIGHT_PANE"] = right_pane_session_id
85
+ except Exception as e:
86
+ console.print(f"[yellow]Warning: Could not set up split pane: {e}[/yellow]")
47
87
 
48
88
  from .dashboard.app import AlineDashboard
49
89
 
50
- dashboard = AlineDashboard(dev_mode=dev)
90
+ dashboard = AlineDashboard(dev_mode=dev, use_native_terminal=use_native_terminal)
51
91
  dashboard.run()
52
92
 
53
93
 
@@ -79,6 +119,235 @@ def whoami_cli():
79
119
  raise typer.Exit(code=exit_code)
80
120
 
81
121
 
122
+ @app.command(name="doctor")
123
+ def doctor_cli(
124
+ no_restart: bool = typer.Option(False, "--no-restart", help="Only clear cache, don't restart daemons"),
125
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
126
+ ):
127
+ """
128
+ Fix common issues after code updates.
129
+
130
+ This command:
131
+ - Clears Python bytecode cache (.pyc files)
132
+ - Updates Claude Code hooks (Stop, UserPromptSubmit, PermissionRequest)
133
+ - Updates skills to latest version
134
+ - Restarts the watcher daemon (if running)
135
+ - Restarts the worker daemon (if running)
136
+
137
+ Run this after pulling new code to ensure everything uses the latest version.
138
+ """
139
+ import shutil
140
+ import subprocess
141
+ import signal
142
+ import time
143
+
144
+ # Find the project root (where src/realign is)
145
+ project_root = Path(__file__).parent.parent.parent
146
+
147
+ # 1. Clear Python cache
148
+ console.print("[bold]1. Clearing Python cache...[/bold]")
149
+ pyc_count = 0
150
+ pycache_count = 0
151
+
152
+ for pyc_file in project_root.rglob("*.pyc"):
153
+ try:
154
+ pyc_file.unlink()
155
+ pyc_count += 1
156
+ if verbose:
157
+ console.print(f" [dim]Removed: {pyc_file}[/dim]")
158
+ except Exception as e:
159
+ if verbose:
160
+ console.print(f" [yellow]Failed to remove {pyc_file}: {e}[/yellow]")
161
+
162
+ for pycache_dir in project_root.rglob("__pycache__"):
163
+ if pycache_dir.is_dir():
164
+ try:
165
+ shutil.rmtree(pycache_dir)
166
+ pycache_count += 1
167
+ if verbose:
168
+ console.print(f" [dim]Removed: {pycache_dir}[/dim]")
169
+ except Exception as e:
170
+ if verbose:
171
+ console.print(f" [yellow]Failed to remove {pycache_dir}: {e}[/yellow]")
172
+
173
+ console.print(f" [green]✓[/green] Cleared {pyc_count} .pyc files, {pycache_count} __pycache__ directories")
174
+
175
+ # 2. Update Claude Code hooks
176
+ console.print("\n[bold]2. Updating Claude Code hooks...[/bold]")
177
+ hooks_updated = []
178
+ hooks_failed = []
179
+
180
+ # Stop hook
181
+ try:
182
+ from .claude_hooks.stop_hook_installer import install_stop_hook, get_settings_path
183
+ if install_stop_hook(get_settings_path(), quiet=True, force=True):
184
+ hooks_updated.append("Stop")
185
+ if verbose:
186
+ console.print(" [dim]Stop hook updated[/dim]")
187
+ else:
188
+ hooks_failed.append("Stop")
189
+ except Exception as e:
190
+ hooks_failed.append("Stop")
191
+ if verbose:
192
+ console.print(f" [yellow]Stop hook failed: {e}[/yellow]")
193
+
194
+ # UserPromptSubmit hook
195
+ try:
196
+ from .claude_hooks.user_prompt_submit_hook_installer import install_user_prompt_submit_hook, get_settings_path as get_submit_settings_path
197
+ if install_user_prompt_submit_hook(get_submit_settings_path(), quiet=True, force=True):
198
+ hooks_updated.append("UserPromptSubmit")
199
+ if verbose:
200
+ console.print(" [dim]UserPromptSubmit hook updated[/dim]")
201
+ else:
202
+ hooks_failed.append("UserPromptSubmit")
203
+ except Exception as e:
204
+ hooks_failed.append("UserPromptSubmit")
205
+ if verbose:
206
+ console.print(f" [yellow]UserPromptSubmit hook failed: {e}[/yellow]")
207
+
208
+ # PermissionRequest hook
209
+ try:
210
+ from .claude_hooks.permission_request_hook_installer import install_permission_request_hook, get_settings_path as get_permission_settings_path
211
+ if install_permission_request_hook(get_permission_settings_path(), quiet=True, force=True):
212
+ hooks_updated.append("PermissionRequest")
213
+ if verbose:
214
+ console.print(" [dim]PermissionRequest hook updated[/dim]")
215
+ else:
216
+ hooks_failed.append("PermissionRequest")
217
+ except Exception as e:
218
+ hooks_failed.append("PermissionRequest")
219
+ if verbose:
220
+ console.print(f" [yellow]PermissionRequest hook failed: {e}[/yellow]")
221
+
222
+ if hooks_updated:
223
+ console.print(f" [green]✓[/green] Updated hooks: {', '.join(hooks_updated)}")
224
+ if hooks_failed:
225
+ console.print(f" [yellow]![/yellow] Failed hooks: {', '.join(hooks_failed)}")
226
+
227
+ # 3. Update skills
228
+ console.print("\n[bold]3. Updating skills...[/bold]")
229
+ try:
230
+ from .commands.add import add_skills_command
231
+ # Capture output by redirecting - use force=True to update
232
+ import io
233
+ import contextlib
234
+
235
+ stdout_capture = io.StringIO()
236
+ with contextlib.redirect_stdout(stdout_capture):
237
+ add_skills_command(force=True)
238
+
239
+ output = stdout_capture.getvalue()
240
+ # Count updated skills from output
241
+ updated_count = output.count("✓")
242
+ if updated_count > 0:
243
+ console.print(f" [green]✓[/green] Updated {updated_count} skill(s)")
244
+ else:
245
+ console.print(" [green]✓[/green] Skills are up to date")
246
+ if verbose and output.strip():
247
+ for line in output.strip().split("\n"):
248
+ console.print(f" [dim]{line}[/dim]")
249
+ except Exception as e:
250
+ console.print(f" [yellow]![/yellow] Failed to update skills: {e}")
251
+
252
+ if no_restart:
253
+ console.print("\n[dim]Skipping daemon restart (--no-restart)[/dim]")
254
+ console.print("\n[green]Done![/green] Aline is ready with the latest code.")
255
+ raise typer.Exit(code=0)
256
+
257
+ # 4. Restart watcher daemon
258
+ console.print("\n[bold]4. Checking watcher daemon...[/bold]")
259
+ pid_file = Path.home() / ".aline" / ".logs" / "watcher.pid"
260
+ watcher_was_running = False
261
+
262
+ if pid_file.exists():
263
+ try:
264
+ pid = int(pid_file.read_text().strip())
265
+ # Check if process is running
266
+ try:
267
+ import os
268
+ os.kill(pid, 0) # Signal 0 just checks if process exists
269
+ watcher_was_running = True
270
+ console.print(f" [dim]Found watcher daemon (PID {pid}), stopping...[/dim]")
271
+ os.kill(pid, signal.SIGTERM)
272
+ time.sleep(1)
273
+ # Force kill if still running
274
+ try:
275
+ os.kill(pid, 0)
276
+ os.kill(pid, signal.SIGKILL)
277
+ time.sleep(0.5)
278
+ except ProcessLookupError:
279
+ pass
280
+ except ProcessLookupError:
281
+ console.print(" [dim]Watcher daemon not running (stale PID file)[/dim]")
282
+ except Exception as e:
283
+ if verbose:
284
+ console.print(f" [yellow]Error checking watcher: {e}[/yellow]")
285
+
286
+ if watcher_was_running:
287
+ console.print(" [dim]Starting watcher daemon...[/dim]")
288
+ try:
289
+ subprocess.Popen(
290
+ ["python", "-m", "src.realign.watcher_daemon"],
291
+ stdout=subprocess.DEVNULL,
292
+ stderr=subprocess.DEVNULL,
293
+ start_new_session=True,
294
+ cwd=str(project_root),
295
+ )
296
+ time.sleep(2)
297
+ console.print(" [green]✓[/green] Watcher daemon restarted")
298
+ except Exception as e:
299
+ console.print(f" [red]✗[/red] Failed to restart watcher: {e}")
300
+ else:
301
+ console.print(" [dim]Watcher daemon was not running[/dim]")
302
+
303
+ # 5. Restart worker daemon
304
+ console.print("\n[bold]5. Checking worker daemon...[/bold]")
305
+ worker_pid_file = Path.home() / ".aline" / ".logs" / "worker.pid"
306
+ worker_was_running = False
307
+
308
+ if worker_pid_file.exists():
309
+ try:
310
+ pid = int(worker_pid_file.read_text().strip())
311
+ try:
312
+ import os
313
+ os.kill(pid, 0)
314
+ worker_was_running = True
315
+ console.print(f" [dim]Found worker daemon (PID {pid}), stopping...[/dim]")
316
+ os.kill(pid, signal.SIGTERM)
317
+ time.sleep(1)
318
+ try:
319
+ os.kill(pid, 0)
320
+ os.kill(pid, signal.SIGKILL)
321
+ time.sleep(0.5)
322
+ except ProcessLookupError:
323
+ pass
324
+ except ProcessLookupError:
325
+ console.print(" [dim]Worker daemon not running (stale PID file)[/dim]")
326
+ except Exception as e:
327
+ if verbose:
328
+ console.print(f" [yellow]Error checking worker: {e}[/yellow]")
329
+
330
+ if worker_was_running:
331
+ console.print(" [dim]Starting worker daemon...[/dim]")
332
+ try:
333
+ subprocess.Popen(
334
+ ["python", "-m", "src.realign.worker_daemon"],
335
+ stdout=subprocess.DEVNULL,
336
+ stderr=subprocess.DEVNULL,
337
+ start_new_session=True,
338
+ cwd=str(project_root),
339
+ )
340
+ time.sleep(2)
341
+ console.print(" [green]✓[/green] Worker daemon restarted")
342
+ except Exception as e:
343
+ console.print(f" [red]✗[/red] Failed to restart worker: {e}")
344
+ else:
345
+ console.print(" [dim]Worker daemon was not running[/dim]")
346
+
347
+ console.print("\n[green]Done![/green] Aline is ready with the latest code.")
348
+ raise typer.Exit(code=0)
349
+
350
+
82
351
  @app.command(name="search")
83
352
  def search_cli(
84
353
  query: str = typer.Argument(..., help="Search query (keywords or regex pattern)"),
@@ -851,7 +1120,6 @@ def dashboard(
851
1120
  if debug:
852
1121
  os.environ["REALIGN_LOG_LEVEL"] = "DEBUG"
853
1122
 
854
- from .dashboard.tmux_manager import bootstrap_dashboard_into_tmux
855
1123
  from .logging_config import setup_logger
856
1124
 
857
1125
  # Initialize logger before dashboard
@@ -859,13 +1127,32 @@ def dashboard(
859
1127
  logger.info(f"Dashboard command invoked (dev={dev}, debug={debug})")
860
1128
 
861
1129
  try:
862
- bootstrap_dashboard_into_tmux()
1130
+ # Check terminal mode
1131
+ terminal_mode = os.environ.get("ALINE_TERMINAL_MODE", "").strip().lower()
1132
+ use_native_terminal = terminal_mode in {"native", "iterm2", "iterm", "kitty"}
1133
+
1134
+ if not use_native_terminal:
1135
+ # Only bootstrap tmux for tmux mode (default)
1136
+ from .dashboard.tmux_manager import bootstrap_dashboard_into_tmux
1137
+
1138
+ bootstrap_dashboard_into_tmux()
1139
+ elif terminal_mode in {"iterm2", "iterm"}:
1140
+ # Set up split pane layout for iTerm2
1141
+ try:
1142
+ from .dashboard.backends.iterm2 import setup_split_pane_layout_sync
1143
+
1144
+ right_pane_session_id = setup_split_pane_layout_sync()
1145
+ if right_pane_session_id:
1146
+ os.environ["ALINE_ITERM2_RIGHT_PANE"] = right_pane_session_id
1147
+ logger.info(f"Set up split pane with right pane: {right_pane_session_id}")
1148
+ except Exception as e:
1149
+ logger.warning(f"Could not set up split pane: {e}")
863
1150
 
864
1151
  from .dashboard.app import AlineDashboard
865
1152
 
866
1153
  # Use dev flag from this command or inherit from parent context
867
1154
  dev_mode = dev or (ctx.obj.get("dev", False) if ctx.obj else False)
868
- dash = AlineDashboard(dev_mode=dev_mode)
1155
+ dash = AlineDashboard(dev_mode=dev_mode, use_native_terminal=use_native_terminal)
869
1156
  dash.run()
870
1157
  except Exception as e:
871
1158
  logger.error(f"Dashboard crashed: {e}\n{traceback.format_exc()}")
realign/commands/auth.py CHANGED
@@ -25,6 +25,7 @@ from ..auth import (
25
25
  save_credentials,
26
26
  clear_credentials,
27
27
  open_login_page,
28
+ open_logout_page,
28
29
  validate_cli_token,
29
30
  find_free_port,
30
31
  start_callback_server,
@@ -176,6 +177,8 @@ def logout_command() -> int:
176
177
  """
177
178
  Logout from Aline and clear local credentials.
178
179
 
180
+ Also stops watcher and worker daemons since they require authentication.
181
+
179
182
  Returns:
180
183
  0 on success, 1 on error
181
184
  """
@@ -190,6 +193,14 @@ def logout_command() -> int:
190
193
 
191
194
  email = credentials.email
192
195
 
196
+ # Stop watcher and worker daemons before logout
197
+ if console:
198
+ console.print("[dim]Stopping daemons...[/dim]")
199
+ else:
200
+ print("Stopping daemons...")
201
+
202
+ _stop_daemons()
203
+
193
204
  if not clear_credentials():
194
205
  if console:
195
206
  console.print("[red]Error: Failed to clear credentials[/red]")
@@ -197,6 +208,14 @@ def logout_command() -> int:
197
208
  print("Error: Failed to clear credentials", file=sys.stderr)
198
209
  return 1
199
210
 
211
+ # Open browser to sign out from web session
212
+ if console:
213
+ console.print("[dim]Signing out from web session...[/dim]")
214
+ else:
215
+ print("Signing out from web session...")
216
+
217
+ open_logout_page()
218
+
200
219
  if console:
201
220
  console.print(f"[green]Logged out successfully.[/green]")
202
221
  console.print(f"Cleared credentials for: {email}")
@@ -208,6 +227,97 @@ def logout_command() -> int:
208
227
  return 0
209
228
 
210
229
 
230
+ def _stop_daemons() -> None:
231
+ """Stop watcher and worker daemons."""
232
+ import os
233
+ import signal
234
+ import time
235
+ from pathlib import Path
236
+
237
+ def stop_daemon(name: str, pid_file: Path) -> None:
238
+ """Stop a daemon by PID file, wait for it to terminate."""
239
+ if not pid_file.exists():
240
+ return
241
+
242
+ try:
243
+ pid = int(pid_file.read_text().strip())
244
+ except Exception:
245
+ return
246
+
247
+ # Check if process is running
248
+ try:
249
+ os.kill(pid, 0)
250
+ except ProcessLookupError:
251
+ # Process already gone, clean up PID file
252
+ try:
253
+ pid_file.unlink(missing_ok=True)
254
+ except Exception:
255
+ pass
256
+ return
257
+ except PermissionError:
258
+ pass # Can't check, try to stop anyway
259
+
260
+ # Send SIGTERM
261
+ try:
262
+ os.kill(pid, signal.SIGTERM)
263
+ if console:
264
+ console.print(f" [dim]Stopping {name} daemon (PID {pid})...[/dim]")
265
+ logger.info(f"Sent SIGTERM to {name} daemon (PID {pid})")
266
+ except ProcessLookupError:
267
+ try:
268
+ pid_file.unlink(missing_ok=True)
269
+ except Exception:
270
+ pass
271
+ return
272
+ except Exception as e:
273
+ logger.debug(f"Error sending SIGTERM to {name}: {e}")
274
+ return
275
+
276
+ # Wait for process to terminate (up to 5 seconds)
277
+ for _ in range(50): # 50 * 0.1s = 5 seconds
278
+ time.sleep(0.1)
279
+ try:
280
+ os.kill(pid, 0)
281
+ except ProcessLookupError:
282
+ # Process terminated
283
+ if console:
284
+ console.print(f" [dim]{name.capitalize()} daemon stopped[/dim]")
285
+ logger.info(f"{name.capitalize()} daemon (PID {pid}) stopped")
286
+ try:
287
+ pid_file.unlink(missing_ok=True)
288
+ except Exception:
289
+ pass
290
+ return
291
+ except PermissionError:
292
+ break # Can't check anymore
293
+
294
+ # Process still running, try SIGKILL
295
+ try:
296
+ os.kill(pid, signal.SIGKILL)
297
+ time.sleep(0.5)
298
+ if console:
299
+ console.print(f" [dim]{name.capitalize()} daemon force killed[/dim]")
300
+ logger.info(f"{name.capitalize()} daemon (PID {pid}) force killed")
301
+ except ProcessLookupError:
302
+ pass
303
+ except Exception as e:
304
+ logger.debug(f"Error sending SIGKILL to {name}: {e}")
305
+
306
+ # Clean up PID file
307
+ try:
308
+ pid_file.unlink(missing_ok=True)
309
+ except Exception:
310
+ pass
311
+
312
+ # Stop watcher daemon
313
+ watcher_pid_file = Path.home() / ".aline" / ".logs" / "watcher.pid"
314
+ stop_daemon("watcher", watcher_pid_file)
315
+
316
+ # Stop worker daemon
317
+ worker_pid_file = Path.home() / ".aline" / ".logs" / "worker.pid"
318
+ stop_daemon("worker", worker_pid_file)
319
+
320
+
211
321
  def whoami_command() -> int:
212
322
  """
213
323
  Display current login status.
@@ -239,6 +239,9 @@ def import_v2_data(
239
239
 
240
240
  logger.info("Starting v2.0 data import")
241
241
 
242
+ # Load config to get current user's UID for shared_by
243
+ config = ReAlignConfig.load()
244
+
242
245
  # 1. Create Event
243
246
  event_data = data.get("event", {})
244
247
  event_id = event_data.get("event_id")
@@ -366,6 +369,9 @@ def import_session_with_turns(
366
369
  Returns:
367
370
  Dict with counts: {'sessions': int, 'turns': int, 'skipped': int}
368
371
  """
372
+ # Load config to get current user's UID for shared_by
373
+ config = ReAlignConfig.load()
374
+
369
375
  session_id = session_data.get("session_id")
370
376
  imported_sessions = 0
371
377
  imported_turns = 0
@@ -658,6 +658,14 @@ def watcher_start_command() -> int:
658
658
  int: Exit code (0 = success, 1 = error)
659
659
  """
660
660
  try:
661
+ # Check login status first
662
+ from ..auth import is_logged_in
663
+
664
+ if not is_logged_in():
665
+ console.print("[red]✗ Not logged in. Watcher requires authentication.[/red]")
666
+ console.print("[dim]Run 'aline login' first.[/dim]")
667
+ return 1
668
+
661
669
  # Check if already running
662
670
  is_running, pid, mode = detect_watcher_process()
663
671
 
@@ -461,6 +461,14 @@ def worker_repair_command(*, force: bool = False) -> int:
461
461
 
462
462
  def worker_start_command() -> int:
463
463
  try:
464
+ # Check login status first
465
+ from ..auth import is_logged_in
466
+
467
+ if not is_logged_in():
468
+ console.print("[red]✗ Not logged in. Worker requires authentication.[/red]")
469
+ console.print("[dim]Run 'aline login' first.[/dim]")
470
+ return 1
471
+
464
472
  is_running, pid, mode = detect_worker_process()
465
473
  if is_running:
466
474
  console.print(f"[yellow]Worker is already running (PID: {pid}, mode: {mode})[/yellow]")