aline-ai 0.5.13__py3-none-any.whl → 0.6.0__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.0
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
1
+ aline_ai-0.6.0.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
+ realign/__init__.py,sha256=a8wH_-YI_n2n6He2CL8jdy4zSUj9pxtT6cz9iNZgp24,1623
3
3
  realign/auth.py,sha256=63fdy-KsNoLZ9A6X0Mz_v-0tQOXN_1XncXBGBlEoXqE,16030
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=2BhDitD5IYUPJRGtfOXv_m0r7ZsiPU9iGtkp2pfA59M,41637
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,17 +34,17 @@ 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=B1FQjaLUKYc4DsGqrpXZ3JPmsttUXhrtxxrKuOUmP4Q,10844
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
49
  realign/dashboard/app.py,sha256=jyW6mqmItTy253CPSqInxctkWzkrGEikdy-ikuShQ14,13299
50
50
  realign/dashboard/tmux_manager.py,sha256=Vt_30WNtDg7c_9SEh8xdDtBLJ8kNq6bGSPh5r3VXpg0,26276
@@ -58,11 +58,11 @@ realign/dashboard/screens/share_import.py,sha256=hl2x0yGVycsoUI76AmdZTAV-br3Q619
58
58
  realign/dashboard/styles/dashboard.tcss,sha256=ewonevBGLN-dfSsgxUk4VBCPchtxY4rx_vj1u6Ox2Fw,3454
59
59
  realign/dashboard/widgets/__init__.py,sha256=3Pf2_K9obrertgv_psfxradgkI9RXlmjoXYQH7oBKm0,583
60
60
  realign/dashboard/widgets/config_panel.py,sha256=JRv9Hgm5V-vqVptS7gQqnjg5MbKpt_FmdZV13D4E9A4,17894
61
- realign/dashboard/widgets/events_table.py,sha256=dXN_aD94YJ1fDSV2B_5m7YMvMU3bNUhGFCMIRWvvLMg,31141
61
+ realign/dashboard/widgets/events_table.py,sha256=Ru4y5Xq04Y-3CN9Dwm-fHrHR37Aup9NFW_Yh-J1x-Jo,33247
62
62
  realign/dashboard/widgets/header.py,sha256=0HHCFXX7F3C6HII-WDwOJwWkJrajmKPWmdoMWyOkn9E,1587
63
63
  realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
64
64
  realign/dashboard/widgets/search_panel.py,sha256=ZNJDfwDSxUFnCeltYQYsQsPJ6t4HDeNWpENoTOoBdVM,8951
65
- realign/dashboard/widgets/sessions_table.py,sha256=PohOkg-ESLBa-Sq0PdLPhV-YzVXOGpUo5ETs0MYO4u8,33415
65
+ realign/dashboard/widgets/sessions_table.py,sha256=GyaWzvt-elKx1iE2YR94CBNPyjqM8B7g0j7zsukiXi0,36517
66
66
  realign/dashboard/widgets/terminal_panel.py,sha256=uoi3LjgYWyFE6Yr208KC5iKg0QxLcXpN6hCERlI6pBg,29069
67
67
  realign/dashboard/widgets/watcher_panel.py,sha256=O_mdDacgc87xA-5KEfta53Ik_Xsk_B2OfwenMOTtGw8,19722
68
68
  realign/dashboard/widgets/worker_panel.py,sha256=F_jKWABuCNmjQgeeuCr4KnFRKdY4CLTNcEXMYwsNaSk,18691
@@ -91,8 +91,8 @@ realign/triggers/next_turn_trigger.py,sha256=BpP0PWn4mU1MZd6mv89jWcjs8Jtv0zEWapW
91
91
  realign/triggers/registry.py,sha256=cb-AVLbYB2pqwfWL3q1DQxLv4kOw7g7m-GshTdfFESc,3827
92
92
  realign/triggers/turn_status.py,sha256=wAZEhXDAmDoX5F-ohWfSnZZ0eA6DAJ9svSPiSv_f6sg,6041
93
93
  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,,
94
+ aline_ai-0.6.0.dist-info/METADATA,sha256=oukhmryjZ2KFfmgLAIPBjpr8S4_8pxboUCOAgIWLx4Y,1597
95
+ aline_ai-0.6.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
96
+ aline_ai-0.6.0.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
97
+ aline_ai-0.6.0.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
98
+ aline_ai-0.6.0.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.0"
7
7
 
8
8
 
9
9
  def get_realign_dir(project_root: Path) -> Path:
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
 
@@ -79,6 +99,235 @@ def whoami_cli():
79
99
  raise typer.Exit(code=exit_code)
80
100
 
81
101
 
102
+ @app.command(name="doctor")
103
+ def doctor_cli(
104
+ no_restart: bool = typer.Option(False, "--no-restart", help="Only clear cache, don't restart daemons"),
105
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
106
+ ):
107
+ """
108
+ Fix common issues after code updates.
109
+
110
+ This command:
111
+ - Clears Python bytecode cache (.pyc files)
112
+ - Updates Claude Code hooks (Stop, UserPromptSubmit, PermissionRequest)
113
+ - Updates skills to latest version
114
+ - Restarts the watcher daemon (if running)
115
+ - Restarts the worker daemon (if running)
116
+
117
+ Run this after pulling new code to ensure everything uses the latest version.
118
+ """
119
+ import shutil
120
+ import subprocess
121
+ import signal
122
+ import time
123
+
124
+ # Find the project root (where src/realign is)
125
+ project_root = Path(__file__).parent.parent.parent
126
+
127
+ # 1. Clear Python cache
128
+ console.print("[bold]1. Clearing Python cache...[/bold]")
129
+ pyc_count = 0
130
+ pycache_count = 0
131
+
132
+ for pyc_file in project_root.rglob("*.pyc"):
133
+ try:
134
+ pyc_file.unlink()
135
+ pyc_count += 1
136
+ if verbose:
137
+ console.print(f" [dim]Removed: {pyc_file}[/dim]")
138
+ except Exception as e:
139
+ if verbose:
140
+ console.print(f" [yellow]Failed to remove {pyc_file}: {e}[/yellow]")
141
+
142
+ for pycache_dir in project_root.rglob("__pycache__"):
143
+ if pycache_dir.is_dir():
144
+ try:
145
+ shutil.rmtree(pycache_dir)
146
+ pycache_count += 1
147
+ if verbose:
148
+ console.print(f" [dim]Removed: {pycache_dir}[/dim]")
149
+ except Exception as e:
150
+ if verbose:
151
+ console.print(f" [yellow]Failed to remove {pycache_dir}: {e}[/yellow]")
152
+
153
+ console.print(f" [green]✓[/green] Cleared {pyc_count} .pyc files, {pycache_count} __pycache__ directories")
154
+
155
+ # 2. Update Claude Code hooks
156
+ console.print("\n[bold]2. Updating Claude Code hooks...[/bold]")
157
+ hooks_updated = []
158
+ hooks_failed = []
159
+
160
+ # Stop hook
161
+ try:
162
+ from .claude_hooks.stop_hook_installer import install_stop_hook, get_settings_path
163
+ if install_stop_hook(get_settings_path(), quiet=True, force=True):
164
+ hooks_updated.append("Stop")
165
+ if verbose:
166
+ console.print(" [dim]Stop hook updated[/dim]")
167
+ else:
168
+ hooks_failed.append("Stop")
169
+ except Exception as e:
170
+ hooks_failed.append("Stop")
171
+ if verbose:
172
+ console.print(f" [yellow]Stop hook failed: {e}[/yellow]")
173
+
174
+ # UserPromptSubmit hook
175
+ try:
176
+ from .claude_hooks.user_prompt_submit_hook_installer import install_user_prompt_submit_hook, get_settings_path as get_submit_settings_path
177
+ if install_user_prompt_submit_hook(get_submit_settings_path(), quiet=True, force=True):
178
+ hooks_updated.append("UserPromptSubmit")
179
+ if verbose:
180
+ console.print(" [dim]UserPromptSubmit hook updated[/dim]")
181
+ else:
182
+ hooks_failed.append("UserPromptSubmit")
183
+ except Exception as e:
184
+ hooks_failed.append("UserPromptSubmit")
185
+ if verbose:
186
+ console.print(f" [yellow]UserPromptSubmit hook failed: {e}[/yellow]")
187
+
188
+ # PermissionRequest hook
189
+ try:
190
+ from .claude_hooks.permission_request_hook_installer import install_permission_request_hook, get_settings_path as get_permission_settings_path
191
+ if install_permission_request_hook(get_permission_settings_path(), quiet=True, force=True):
192
+ hooks_updated.append("PermissionRequest")
193
+ if verbose:
194
+ console.print(" [dim]PermissionRequest hook updated[/dim]")
195
+ else:
196
+ hooks_failed.append("PermissionRequest")
197
+ except Exception as e:
198
+ hooks_failed.append("PermissionRequest")
199
+ if verbose:
200
+ console.print(f" [yellow]PermissionRequest hook failed: {e}[/yellow]")
201
+
202
+ if hooks_updated:
203
+ console.print(f" [green]✓[/green] Updated hooks: {', '.join(hooks_updated)}")
204
+ if hooks_failed:
205
+ console.print(f" [yellow]![/yellow] Failed hooks: {', '.join(hooks_failed)}")
206
+
207
+ # 3. Update skills
208
+ console.print("\n[bold]3. Updating skills...[/bold]")
209
+ try:
210
+ from .commands.add import add_skills_command
211
+ # Capture output by redirecting - use force=True to update
212
+ import io
213
+ import contextlib
214
+
215
+ stdout_capture = io.StringIO()
216
+ with contextlib.redirect_stdout(stdout_capture):
217
+ add_skills_command(force=True)
218
+
219
+ output = stdout_capture.getvalue()
220
+ # Count updated skills from output
221
+ updated_count = output.count("✓")
222
+ if updated_count > 0:
223
+ console.print(f" [green]✓[/green] Updated {updated_count} skill(s)")
224
+ else:
225
+ console.print(" [green]✓[/green] Skills are up to date")
226
+ if verbose and output.strip():
227
+ for line in output.strip().split("\n"):
228
+ console.print(f" [dim]{line}[/dim]")
229
+ except Exception as e:
230
+ console.print(f" [yellow]![/yellow] Failed to update skills: {e}")
231
+
232
+ if no_restart:
233
+ console.print("\n[dim]Skipping daemon restart (--no-restart)[/dim]")
234
+ console.print("\n[green]Done![/green] Aline is ready with the latest code.")
235
+ raise typer.Exit(code=0)
236
+
237
+ # 4. Restart watcher daemon
238
+ console.print("\n[bold]4. Checking watcher daemon...[/bold]")
239
+ pid_file = Path.home() / ".aline" / ".logs" / "watcher.pid"
240
+ watcher_was_running = False
241
+
242
+ if pid_file.exists():
243
+ try:
244
+ pid = int(pid_file.read_text().strip())
245
+ # Check if process is running
246
+ try:
247
+ import os
248
+ os.kill(pid, 0) # Signal 0 just checks if process exists
249
+ watcher_was_running = True
250
+ console.print(f" [dim]Found watcher daemon (PID {pid}), stopping...[/dim]")
251
+ os.kill(pid, signal.SIGTERM)
252
+ time.sleep(1)
253
+ # Force kill if still running
254
+ try:
255
+ os.kill(pid, 0)
256
+ os.kill(pid, signal.SIGKILL)
257
+ time.sleep(0.5)
258
+ except ProcessLookupError:
259
+ pass
260
+ except ProcessLookupError:
261
+ console.print(" [dim]Watcher daemon not running (stale PID file)[/dim]")
262
+ except Exception as e:
263
+ if verbose:
264
+ console.print(f" [yellow]Error checking watcher: {e}[/yellow]")
265
+
266
+ if watcher_was_running:
267
+ console.print(" [dim]Starting watcher daemon...[/dim]")
268
+ try:
269
+ subprocess.Popen(
270
+ ["python", "-m", "src.realign.watcher_daemon"],
271
+ stdout=subprocess.DEVNULL,
272
+ stderr=subprocess.DEVNULL,
273
+ start_new_session=True,
274
+ cwd=str(project_root),
275
+ )
276
+ time.sleep(2)
277
+ console.print(" [green]✓[/green] Watcher daemon restarted")
278
+ except Exception as e:
279
+ console.print(f" [red]✗[/red] Failed to restart watcher: {e}")
280
+ else:
281
+ console.print(" [dim]Watcher daemon was not running[/dim]")
282
+
283
+ # 5. Restart worker daemon
284
+ console.print("\n[bold]5. Checking worker daemon...[/bold]")
285
+ worker_pid_file = Path.home() / ".aline" / ".logs" / "worker.pid"
286
+ worker_was_running = False
287
+
288
+ if worker_pid_file.exists():
289
+ try:
290
+ pid = int(worker_pid_file.read_text().strip())
291
+ try:
292
+ import os
293
+ os.kill(pid, 0)
294
+ worker_was_running = True
295
+ console.print(f" [dim]Found worker daemon (PID {pid}), stopping...[/dim]")
296
+ os.kill(pid, signal.SIGTERM)
297
+ time.sleep(1)
298
+ try:
299
+ os.kill(pid, 0)
300
+ os.kill(pid, signal.SIGKILL)
301
+ time.sleep(0.5)
302
+ except ProcessLookupError:
303
+ pass
304
+ except ProcessLookupError:
305
+ console.print(" [dim]Worker daemon not running (stale PID file)[/dim]")
306
+ except Exception as e:
307
+ if verbose:
308
+ console.print(f" [yellow]Error checking worker: {e}[/yellow]")
309
+
310
+ if worker_was_running:
311
+ console.print(" [dim]Starting worker daemon...[/dim]")
312
+ try:
313
+ subprocess.Popen(
314
+ ["python", "-m", "src.realign.worker_daemon"],
315
+ stdout=subprocess.DEVNULL,
316
+ stderr=subprocess.DEVNULL,
317
+ start_new_session=True,
318
+ cwd=str(project_root),
319
+ )
320
+ time.sleep(2)
321
+ console.print(" [green]✓[/green] Worker daemon restarted")
322
+ except Exception as e:
323
+ console.print(f" [red]✗[/red] Failed to restart worker: {e}")
324
+ else:
325
+ console.print(" [dim]Worker daemon was not running[/dim]")
326
+
327
+ console.print("\n[green]Done![/green] Aline is ready with the latest code.")
328
+ raise typer.Exit(code=0)
329
+
330
+
82
331
  @app.command(name="search")
83
332
  def search_cli(
84
333
  query: str = typer.Argument(..., help="Search query (keywords or regex pattern)"),
realign/commands/auth.py CHANGED
@@ -176,6 +176,8 @@ def logout_command() -> int:
176
176
  """
177
177
  Logout from Aline and clear local credentials.
178
178
 
179
+ Also stops watcher and worker daemons since they require authentication.
180
+
179
181
  Returns:
180
182
  0 on success, 1 on error
181
183
  """
@@ -190,6 +192,14 @@ def logout_command() -> int:
190
192
 
191
193
  email = credentials.email
192
194
 
195
+ # Stop watcher and worker daemons before logout
196
+ if console:
197
+ console.print("[dim]Stopping daemons...[/dim]")
198
+ else:
199
+ print("Stopping daemons...")
200
+
201
+ _stop_daemons()
202
+
193
203
  if not clear_credentials():
194
204
  if console:
195
205
  console.print("[red]Error: Failed to clear credentials[/red]")
@@ -208,6 +218,97 @@ def logout_command() -> int:
208
218
  return 0
209
219
 
210
220
 
221
+ def _stop_daemons() -> None:
222
+ """Stop watcher and worker daemons."""
223
+ import os
224
+ import signal
225
+ import time
226
+ from pathlib import Path
227
+
228
+ def stop_daemon(name: str, pid_file: Path) -> None:
229
+ """Stop a daemon by PID file, wait for it to terminate."""
230
+ if not pid_file.exists():
231
+ return
232
+
233
+ try:
234
+ pid = int(pid_file.read_text().strip())
235
+ except Exception:
236
+ return
237
+
238
+ # Check if process is running
239
+ try:
240
+ os.kill(pid, 0)
241
+ except ProcessLookupError:
242
+ # Process already gone, clean up PID file
243
+ try:
244
+ pid_file.unlink(missing_ok=True)
245
+ except Exception:
246
+ pass
247
+ return
248
+ except PermissionError:
249
+ pass # Can't check, try to stop anyway
250
+
251
+ # Send SIGTERM
252
+ try:
253
+ os.kill(pid, signal.SIGTERM)
254
+ if console:
255
+ console.print(f" [dim]Stopping {name} daemon (PID {pid})...[/dim]")
256
+ logger.info(f"Sent SIGTERM to {name} daemon (PID {pid})")
257
+ except ProcessLookupError:
258
+ try:
259
+ pid_file.unlink(missing_ok=True)
260
+ except Exception:
261
+ pass
262
+ return
263
+ except Exception as e:
264
+ logger.debug(f"Error sending SIGTERM to {name}: {e}")
265
+ return
266
+
267
+ # Wait for process to terminate (up to 5 seconds)
268
+ for _ in range(50): # 50 * 0.1s = 5 seconds
269
+ time.sleep(0.1)
270
+ try:
271
+ os.kill(pid, 0)
272
+ except ProcessLookupError:
273
+ # Process terminated
274
+ if console:
275
+ console.print(f" [dim]{name.capitalize()} daemon stopped[/dim]")
276
+ logger.info(f"{name.capitalize()} daemon (PID {pid}) stopped")
277
+ try:
278
+ pid_file.unlink(missing_ok=True)
279
+ except Exception:
280
+ pass
281
+ return
282
+ except PermissionError:
283
+ break # Can't check anymore
284
+
285
+ # Process still running, try SIGKILL
286
+ try:
287
+ os.kill(pid, signal.SIGKILL)
288
+ time.sleep(0.5)
289
+ if console:
290
+ console.print(f" [dim]{name.capitalize()} daemon force killed[/dim]")
291
+ logger.info(f"{name.capitalize()} daemon (PID {pid}) force killed")
292
+ except ProcessLookupError:
293
+ pass
294
+ except Exception as e:
295
+ logger.debug(f"Error sending SIGKILL to {name}: {e}")
296
+
297
+ # Clean up PID file
298
+ try:
299
+ pid_file.unlink(missing_ok=True)
300
+ except Exception:
301
+ pass
302
+
303
+ # Stop watcher daemon
304
+ watcher_pid_file = Path.home() / ".aline" / ".logs" / "watcher.pid"
305
+ stop_daemon("watcher", watcher_pid_file)
306
+
307
+ # Stop worker daemon
308
+ worker_pid_file = Path.home() / ".aline" / ".logs" / "worker.pid"
309
+ stop_daemon("worker", worker_pid_file)
310
+
311
+
211
312
  def whoami_command() -> int:
212
313
  """
213
314
  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]")
@@ -232,6 +232,8 @@ class EventsTable(Container):
232
232
  table.add_column("Share", key="share", width=12)
233
233
  table.add_column("Type", key="type", width=8)
234
234
  table.add_column("Sessions", key="sessions", width=8)
235
+ table.add_column("Created By", key="created_by", width=10)
236
+ table.add_column("Shared By", key="shared_by", width=10)
235
237
  table.add_column("Event ID", key="event_id", width=12)
236
238
  table.add_column("Created", key="created", width=10)
237
239
  table.cursor_type = "row"
@@ -352,6 +354,12 @@ class EventsTable(Container):
352
354
  table.update_cell(
353
355
  eid, "sessions", self._format_cell(str(event["sessions"]), eid)
354
356
  )
357
+ table.update_cell(
358
+ eid, "created_by", self._format_cell(event.get("created_by", "-"), eid)
359
+ )
360
+ table.update_cell(
361
+ eid, "shared_by", self._format_cell(event.get("shared_by", "-"), eid)
362
+ )
355
363
  table.update_cell(
356
364
  eid, "event_id", self._format_cell(event["short_id"], eid)
357
365
  )
@@ -736,6 +744,8 @@ class EventsTable(Container):
736
744
  e.event_type,
737
745
  e.created_at,
738
746
  e.share_url,
747
+ e.created_by,
748
+ e.shared_by,
739
749
  (SELECT COUNT(*) FROM event_sessions WHERE event_id = e.id) AS session_count
740
750
  FROM events e
741
751
  ORDER BY e.created_at DESC
@@ -743,8 +753,9 @@ class EventsTable(Container):
743
753
  """,
744
754
  (int(rows_per_page), int(offset)),
745
755
  ).fetchall()
746
- has_share_url = True
756
+ has_new_columns = True
747
757
  except Exception:
758
+ # Fallback for older schema without created_by/shared_by
748
759
  rows = conn.execute(
749
760
  """
750
761
  SELECT
@@ -759,18 +770,22 @@ class EventsTable(Container):
759
770
  """,
760
771
  (int(rows_per_page), int(offset)),
761
772
  ).fetchall()
762
- has_share_url = False
773
+ has_new_columns = False
763
774
 
764
775
  for i, row in enumerate(rows):
765
776
  event_id = row[0]
766
777
  title = row[1] or "(no title)"
767
778
  event_type = row[2] or "unknown"
768
779
  created_at = row[3]
769
- if has_share_url:
780
+ if has_new_columns:
770
781
  share_url = row[4]
771
- session_count = row[5]
782
+ created_by = row[5]
783
+ shared_by = row[6]
784
+ session_count = row[7]
772
785
  else:
773
786
  share_url = None
787
+ created_by = None
788
+ shared_by = None
774
789
  session_count = row[4]
775
790
 
776
791
  # Format event type
@@ -789,6 +804,26 @@ class EventsTable(Container):
789
804
  except Exception:
790
805
  created_str = created_at
791
806
 
807
+ # Look up user names from users table
808
+ created_by_display = "-"
809
+ shared_by_display = "-"
810
+ if created_by:
811
+ try:
812
+ user_row = conn.execute(
813
+ "SELECT user_name FROM users WHERE uid = ?", (created_by,)
814
+ ).fetchone()
815
+ created_by_display = user_row[0] if user_row and user_row[0] else created_by[:8] + "..."
816
+ except Exception:
817
+ created_by_display = created_by[:8] + "..." if len(created_by) > 8 else created_by
818
+ if shared_by:
819
+ try:
820
+ user_row = conn.execute(
821
+ "SELECT user_name FROM users WHERE uid = ?", (shared_by,)
822
+ ).fetchone()
823
+ shared_by_display = user_row[0] if user_row and user_row[0] else shared_by[:8] + "..."
824
+ except Exception:
825
+ shared_by_display = shared_by[:8] + "..." if len(shared_by) > 8 else shared_by
826
+
792
827
  events.append(
793
828
  {
794
829
  "index": offset + i + 1,
@@ -799,6 +834,8 @@ class EventsTable(Container):
799
834
  "sessions": session_count,
800
835
  "share_url": share_url,
801
836
  "share_id": self._extract_share_id(share_url),
837
+ "created_by": created_by_display,
838
+ "shared_by": shared_by_display,
802
839
  "created": created_str,
803
840
  }
804
841
  )
@@ -849,7 +886,7 @@ class EventsTable(Container):
849
886
  if self.wrap_mode and event.get("share_url"):
850
887
  share_val = event.get("share_url")
851
888
 
852
- # Column order: ✓, #, Title, Share, Type, Sessions, Event ID, Created
889
+ # Column order: ✓, #, Title, Share, Type, Sessions, Created By, Shared By, Event ID, Created
853
890
  table.add_row(
854
891
  self._checkbox_cell(eid),
855
892
  self._format_cell(str(event["index"]), eid),
@@ -857,6 +894,8 @@ class EventsTable(Container):
857
894
  self._format_cell(share_val, eid),
858
895
  self._format_cell(event["type"], eid),
859
896
  self._format_cell(str(event["sessions"]), eid),
897
+ self._format_cell(event.get("created_by", "-"), eid),
898
+ self._format_cell(event.get("shared_by", "-"), eid),
860
899
  self._format_cell(event["short_id"], eid),
861
900
  self._format_cell(event["created"], eid),
862
901
  key=eid,
@@ -223,6 +223,8 @@ class SessionsTable(Container):
223
223
  table.add_column("Project", key="project", width=15)
224
224
  table.add_column("Source", key="source", width=10)
225
225
  table.add_column("Turns", key="turns", width=6)
226
+ table.add_column("Created By", key="created_by", width=10)
227
+ table.add_column("Shared By", key="shared_by", width=10)
226
228
  table.add_column("Session ID", key="session_id", width=20)
227
229
  table.add_column("Last Activity", key="last_activity", width=12)
228
230
  table.cursor_type = "row"
@@ -361,6 +363,12 @@ class SessionsTable(Container):
361
363
  table.update_cell(
362
364
  sid, "turns", self._format_cell(str(session["turns"]), sid)
363
365
  )
366
+ table.update_cell(
367
+ sid, "created_by", self._format_cell(session.get("created_by", "-"), sid)
368
+ )
369
+ table.update_cell(
370
+ sid, "shared_by", self._format_cell(session.get("shared_by", "-"), sid)
371
+ )
364
372
  table.update_cell(
365
373
  sid, "session_id", self._format_cell(session["short_id"], sid)
366
374
  )
@@ -805,21 +813,43 @@ class SessionsTable(Container):
805
813
  # Get paginated sessions
806
814
  # Use cached total_turns instead of subquery for performance
807
815
  offset = (int(page) - 1) * int(rows_per_page)
808
- rows = conn.execute(
809
- """
810
- SELECT
811
- s.id,
812
- s.session_type,
813
- s.workspace_path,
814
- s.session_title,
815
- s.last_activity_at,
816
- s.total_turns
817
- FROM sessions s
818
- ORDER BY s.last_activity_at DESC
819
- LIMIT ? OFFSET ?
820
- """,
821
- (int(rows_per_page), int(offset)),
822
- ).fetchall()
816
+ try:
817
+ rows = conn.execute(
818
+ """
819
+ SELECT
820
+ s.id,
821
+ s.session_type,
822
+ s.workspace_path,
823
+ s.session_title,
824
+ s.last_activity_at,
825
+ s.total_turns,
826
+ s.created_by,
827
+ s.shared_by
828
+ FROM sessions s
829
+ ORDER BY s.last_activity_at DESC
830
+ LIMIT ? OFFSET ?
831
+ """,
832
+ (int(rows_per_page), int(offset)),
833
+ ).fetchall()
834
+ has_new_columns = True
835
+ except Exception:
836
+ # Fallback for older schema without created_by/shared_by
837
+ rows = conn.execute(
838
+ """
839
+ SELECT
840
+ s.id,
841
+ s.session_type,
842
+ s.workspace_path,
843
+ s.session_title,
844
+ s.last_activity_at,
845
+ s.total_turns
846
+ FROM sessions s
847
+ ORDER BY s.last_activity_at DESC
848
+ LIMIT ? OFFSET ?
849
+ """,
850
+ (int(rows_per_page), int(offset)),
851
+ ).fetchall()
852
+ has_new_columns = False
823
853
 
824
854
  for i, row in enumerate(rows):
825
855
  session_id = row[0]
@@ -828,6 +858,12 @@ class SessionsTable(Container):
828
858
  title = row[3] or "(no title)"
829
859
  last_activity = row[4]
830
860
  turn_count = row[5]
861
+ if has_new_columns:
862
+ created_by = row[6]
863
+ shared_by = row[7]
864
+ else:
865
+ created_by = None
866
+ shared_by = None
831
867
 
832
868
  source_map = {
833
869
  "claude": "Claude",
@@ -846,6 +882,26 @@ class SessionsTable(Container):
846
882
  except Exception:
847
883
  activity_str = last_activity
848
884
 
885
+ # Look up user names from users table
886
+ created_by_display = "-"
887
+ shared_by_display = "-"
888
+ if created_by:
889
+ try:
890
+ user_row = conn.execute(
891
+ "SELECT user_name FROM users WHERE uid = ?", (created_by,)
892
+ ).fetchone()
893
+ created_by_display = user_row[0] if user_row and user_row[0] else created_by[:8] + "..."
894
+ except Exception:
895
+ created_by_display = created_by[:8] + "..." if len(created_by) > 8 else created_by
896
+ if shared_by:
897
+ try:
898
+ user_row = conn.execute(
899
+ "SELECT user_name FROM users WHERE uid = ?", (shared_by,)
900
+ ).fetchone()
901
+ shared_by_display = user_row[0] if user_row and user_row[0] else shared_by[:8] + "..."
902
+ except Exception:
903
+ shared_by_display = shared_by[:8] + "..." if len(shared_by) > 8 else shared_by
904
+
849
905
  sessions.append(
850
906
  {
851
907
  "index": offset + i + 1,
@@ -855,6 +911,8 @@ class SessionsTable(Container):
855
911
  "project": project,
856
912
  "turns": turn_count,
857
913
  "title": title,
914
+ "created_by": created_by_display,
915
+ "shared_by": shared_by_display,
858
916
  "last_activity": activity_str,
859
917
  }
860
918
  )
@@ -905,7 +963,7 @@ class SessionsTable(Container):
905
963
  if self.wrap_mode:
906
964
  display_id = sid
907
965
 
908
- # Column order: ✓, #, Title, Project, Source, Turns, Session ID, Last Activity
966
+ # Column order: ✓, #, Title, Project, Source, Turns, Created By, Shared By, Session ID, Last Activity
909
967
  table.add_row(
910
968
  self._checkbox_cell(sid),
911
969
  self._format_cell(str(session["index"]), sid),
@@ -913,6 +971,8 @@ class SessionsTable(Container):
913
971
  self._format_cell(session["project"], sid),
914
972
  self._format_cell(session["source"], sid),
915
973
  self._format_cell(str(session["turns"]), sid),
974
+ self._format_cell(session.get("created_by", "-"), sid),
975
+ self._format_cell(session.get("shared_by", "-"), sid),
916
976
  self._format_cell(display_id, sid),
917
977
  self._format_cell(session["last_activity"], sid),
918
978
  key=sid,
realign/watcher_daemon.py CHANGED
@@ -48,6 +48,17 @@ async def run_daemon():
48
48
  """Run the watcher daemon."""
49
49
  watcher = None
50
50
 
51
+ # Check login status before starting
52
+ try:
53
+ from .auth import is_logged_in
54
+ except ImportError:
55
+ from realign.auth import is_logged_in
56
+
57
+ if not is_logged_in():
58
+ logger.error("Not logged in. Watcher daemon requires authentication.")
59
+ print("[Watcher Daemon] Error: Not logged in. Run 'aline login' first.", file=sys.stderr)
60
+ sys.exit(1)
61
+
51
62
  # Shutdown handler
52
63
  def handle_shutdown(signum, frame):
53
64
  """Handle shutdown signals gracefully."""
realign/worker_daemon.py CHANGED
@@ -49,6 +49,17 @@ async def run_daemon() -> None:
49
49
  worker = None
50
50
  db = None
51
51
 
52
+ # Check login status before starting
53
+ try:
54
+ from .auth import is_logged_in
55
+ except ImportError:
56
+ from realign.auth import is_logged_in
57
+
58
+ if not is_logged_in():
59
+ logger.error("Not logged in. Worker daemon requires authentication.")
60
+ print("[Worker Daemon] Error: Not logged in. Run 'aline login' first.", file=sys.stderr)
61
+ sys.exit(1)
62
+
52
63
  def handle_shutdown(signum, frame):
53
64
  logger.info(f"Received signal {signum}, shutting down...")
54
65
  try: