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.
- {aline_ai-0.5.13.dist-info → aline_ai-0.6.1.dist-info}/METADATA +1 -1
- {aline_ai-0.5.13.dist-info → aline_ai-0.6.1.dist-info}/RECORD +24 -19
- realign/__init__.py +1 -1
- realign/auth.py +21 -0
- realign/cli.py +293 -6
- realign/commands/auth.py +110 -0
- realign/commands/import_shares.py +6 -0
- realign/commands/watcher.py +8 -0
- realign/commands/worker.py +8 -0
- realign/dashboard/app.py +68 -6
- realign/dashboard/backends/__init__.py +6 -0
- realign/dashboard/backends/iterm2.py +599 -0
- realign/dashboard/backends/kitty.py +372 -0
- realign/dashboard/layout.py +320 -0
- realign/dashboard/terminal_backend.py +110 -0
- realign/dashboard/widgets/events_table.py +44 -5
- realign/dashboard/widgets/sessions_table.py +76 -16
- realign/dashboard/widgets/terminal_panel.py +566 -104
- realign/watcher_daemon.py +11 -0
- realign/worker_daemon.py +11 -0
- {aline_ai-0.5.13.dist-info → aline_ai-0.6.1.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.13.dist-info → aline_ai-0.6.1.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.13.dist-info → aline_ai-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.13.dist-info → aline_ai-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
aline_ai-0.
|
|
2
|
-
realign/__init__.py,sha256=
|
|
3
|
-
realign/auth.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
47
|
-
realign/commands/worker.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
66
|
-
realign/dashboard/widgets/terminal_panel.py,sha256=
|
|
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.
|
|
95
|
-
aline_ai-0.
|
|
96
|
-
aline_ai-0.
|
|
97
|
-
aline_ai-0.
|
|
98
|
-
aline_ai-0.
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
realign/commands/watcher.py
CHANGED
|
@@ -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
|
|
realign/commands/worker.py
CHANGED
|
@@ -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]")
|