aline-ai 0.5.12__py3-none-any.whl → 0.5.13__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.
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Authentication commands for Aline CLI.
4
+
5
+ Commands:
6
+ - aline login - Login via web browser
7
+ - aline logout - Clear local credentials
8
+ - aline whoami - Show current login status
9
+ """
10
+
11
+ import sys
12
+ from typing import Optional
13
+
14
+ try:
15
+ from rich.console import Console
16
+ from rich.prompt import Prompt
17
+ RICH_AVAILABLE = True
18
+ except ImportError:
19
+ RICH_AVAILABLE = False
20
+
21
+ from ..auth import (
22
+ is_logged_in,
23
+ get_current_user,
24
+ load_credentials,
25
+ save_credentials,
26
+ clear_credentials,
27
+ open_login_page,
28
+ validate_cli_token,
29
+ find_free_port,
30
+ start_callback_server,
31
+ HTTPX_AVAILABLE,
32
+ )
33
+ from ..config import ReAlignConfig
34
+ from ..logging_config import setup_logger
35
+
36
+ logger = setup_logger("realign.commands.auth", "auth.log")
37
+
38
+ if RICH_AVAILABLE:
39
+ console = Console()
40
+ else:
41
+ console = None
42
+
43
+
44
+ def login_command() -> int:
45
+ """
46
+ Login to Aline via web browser.
47
+
48
+ Opens the web login page with automatic callback - no manual token copy needed.
49
+
50
+ Returns:
51
+ 0 on success, 1 on error
52
+ """
53
+ # Check dependencies
54
+ if not HTTPX_AVAILABLE:
55
+ print("Error: httpx package not installed", file=sys.stderr)
56
+ print("Install it with: pip install httpx", file=sys.stderr)
57
+ return 1
58
+
59
+ # Check if already logged in
60
+ credentials = load_credentials()
61
+ if credentials and is_logged_in():
62
+ if console:
63
+ console.print(f"[yellow]Already logged in as {credentials.email}[/yellow]")
64
+ console.print("Run 'aline logout' first if you want to switch accounts.")
65
+ else:
66
+ print(f"Already logged in as {credentials.email}")
67
+ print("Run 'aline logout' first if you want to switch accounts.")
68
+ return 0
69
+
70
+ # Start local callback server
71
+ port = find_free_port()
72
+
73
+ if console:
74
+ console.print("[cyan]Opening browser for login...[/cyan]")
75
+ console.print("[dim]Waiting for authentication...[/dim]\n")
76
+ else:
77
+ print("Opening browser for login...")
78
+ print("Waiting for authentication...\n")
79
+
80
+ # Open browser with callback URL
81
+ login_url = open_login_page(callback_port=port)
82
+
83
+ if console:
84
+ console.print(f"[dim]If browser doesn't open, visit:[/dim]")
85
+ console.print(f"[link={login_url}]{login_url}[/link]\n")
86
+ else:
87
+ print(f"If browser doesn't open, visit:")
88
+ print(f"{login_url}\n")
89
+
90
+ # Wait for callback with token
91
+ cli_token, error = start_callback_server(port, timeout=300)
92
+
93
+ if error:
94
+ if console:
95
+ console.print(f"[red]Error: {error}[/red]")
96
+ else:
97
+ print(f"Error: {error}", file=sys.stderr)
98
+ return 1
99
+
100
+ if not cli_token:
101
+ if console:
102
+ console.print("[red]Error: No token received[/red]")
103
+ console.print("Please try again with 'aline login'")
104
+ else:
105
+ print("Error: No token received", file=sys.stderr)
106
+ print("Please try again with 'aline login'")
107
+ return 1
108
+
109
+ # Validate token
110
+ if console:
111
+ console.print("[cyan]Validating token...[/cyan]")
112
+ else:
113
+ print("Validating token...")
114
+
115
+ credentials = validate_cli_token(cli_token)
116
+
117
+ if not credentials:
118
+ if console:
119
+ console.print("[red]Error: Invalid or expired token[/red]")
120
+ console.print("Please try again with 'aline login'")
121
+ else:
122
+ print("Error: Invalid or expired token", file=sys.stderr)
123
+ print("Please try again with 'aline login'")
124
+ return 1
125
+
126
+ # Save credentials
127
+ if not save_credentials(credentials):
128
+ if console:
129
+ console.print("[red]Error: Failed to save credentials[/red]")
130
+ else:
131
+ print("Error: Failed to save credentials", file=sys.stderr)
132
+ return 1
133
+
134
+ # Sync Supabase uid to local config
135
+ # This ensures all new Events/Sessions/Turns use the same uid as shares
136
+ try:
137
+ config = ReAlignConfig.load()
138
+ old_uid = config.uid
139
+ config.uid = credentials.user_id
140
+ # Use email as user_name if not already set
141
+ if not config.user_name:
142
+ config.user_name = credentials.email.split("@")[0] # Use email prefix as username
143
+ config.save()
144
+ logger.info(f"Synced Supabase uid to config: {credentials.user_id[:8]}... (was: {old_uid[:8] if old_uid else 'not set'}...)")
145
+
146
+ # V18: Upsert user info to users table
147
+ try:
148
+ from ..db import get_database
149
+ db = get_database()
150
+ db.upsert_user(config.uid, config.user_name)
151
+ except Exception as e:
152
+ logger.debug(f"Failed to upsert user to users table: {e}")
153
+ except Exception as e:
154
+ # Non-fatal: continue even if config sync fails
155
+ logger.warning(f"Failed to sync uid to config: {e}")
156
+
157
+ # Success
158
+ if console:
159
+ console.print(f"\n[green]Login successful![/green]")
160
+ console.print(f"Logged in as: [bold]{credentials.email}[/bold]")
161
+ if credentials.provider and credentials.provider != "email":
162
+ console.print(f"Provider: {credentials.provider}")
163
+ console.print(f"[dim]User ID synced to local config[/dim]")
164
+ else:
165
+ print(f"\nLogin successful!")
166
+ print(f"Logged in as: {credentials.email}")
167
+ if credentials.provider and credentials.provider != "email":
168
+ print(f"Provider: {credentials.provider}")
169
+ print("User ID synced to local config")
170
+
171
+ logger.info(f"Login successful for {credentials.email}")
172
+ return 0
173
+
174
+
175
+ def logout_command() -> int:
176
+ """
177
+ Logout from Aline and clear local credentials.
178
+
179
+ Returns:
180
+ 0 on success, 1 on error
181
+ """
182
+ credentials = load_credentials()
183
+
184
+ if not credentials:
185
+ if console:
186
+ console.print("[yellow]Not currently logged in.[/yellow]")
187
+ else:
188
+ print("Not currently logged in.")
189
+ return 0
190
+
191
+ email = credentials.email
192
+
193
+ if not clear_credentials():
194
+ if console:
195
+ console.print("[red]Error: Failed to clear credentials[/red]")
196
+ else:
197
+ print("Error: Failed to clear credentials", file=sys.stderr)
198
+ return 1
199
+
200
+ if console:
201
+ console.print(f"[green]Logged out successfully.[/green]")
202
+ console.print(f"Cleared credentials for: {email}")
203
+ else:
204
+ print("Logged out successfully.")
205
+ print(f"Cleared credentials for: {email}")
206
+
207
+ logger.info(f"Logout successful for {email}")
208
+ return 0
209
+
210
+
211
+ def whoami_command() -> int:
212
+ """
213
+ Display current login status.
214
+
215
+ Returns:
216
+ 0 if logged in, 1 if not logged in
217
+ """
218
+ credentials = get_current_user()
219
+
220
+ if not credentials:
221
+ if console:
222
+ console.print("[yellow]Not logged in.[/yellow]")
223
+ console.print("Run 'aline login' to authenticate.")
224
+ else:
225
+ print("Not logged in.")
226
+ print("Run 'aline login' to authenticate.")
227
+ return 1
228
+
229
+ if console:
230
+ console.print(f"[green]Logged in as:[/green] [bold]{credentials.email}[/bold]")
231
+ console.print(f"[dim]User ID:[/dim] {credentials.user_id}")
232
+ if credentials.provider:
233
+ console.print(f"[dim]Provider:[/dim] {credentials.provider}")
234
+ console.print(f"[dim]Token expires:[/dim] {credentials.expires_at.strftime('%Y-%m-%d %H:%M:%S UTC')}")
235
+ else:
236
+ print(f"Logged in as: {credentials.email}")
237
+ print(f"User ID: {credentials.user_id}")
238
+ if credentials.provider:
239
+ print(f"Provider: {credentials.provider}")
240
+ print(f"Token expires: {credentials.expires_at.strftime('%Y-%m-%d %H:%M:%S UTC')}")
241
+
242
+ return 0
@@ -25,6 +25,7 @@ from typing import Any, List, Dict, Optional, Tuple, Set, Callable
25
25
  from ..logging_config import setup_logger
26
26
  from ..db.base import SessionRecord, TurnRecord
27
27
  from ..llm_client import call_llm, extract_json
28
+ from ..auth import get_auth_headers, is_logged_in
28
29
 
29
30
  logger = setup_logger("realign.commands.export_shares", "export_shares.log")
30
31
 
@@ -177,9 +178,9 @@ class ExportableSession:
177
178
  last_activity_at: Optional[datetime] # Last activity time
178
179
  turn_count: int # Number of turns in session
179
180
  turns: List[TurnRecord] # List of turns
180
- # V9: creator information
181
- creator_name: Optional[str] = None # Username who created the session
182
- creator_id: Optional[str] = None # User UUID
181
+ # V18: user identity
182
+ created_by: Optional[str] = None # Creator UID
183
+ shared_by: Optional[str] = None # Sharer UID
183
184
 
184
185
 
185
186
  def get_sessions_for_export(
@@ -215,8 +216,8 @@ def get_sessions_for_export(
215
216
  last_activity_at=session.last_activity_at,
216
217
  turn_count=len(turns),
217
218
  turns=turns,
218
- creator_name=session.creator_name,
219
- creator_id=session.creator_id,
219
+ created_by=session.created_by,
220
+ shared_by=session.shared_by,
220
221
  )
221
222
  )
222
223
 
@@ -298,8 +299,8 @@ def build_exportable_sessions_from_records(
298
299
  last_activity_at=session.last_activity_at,
299
300
  turn_count=len(turns),
300
301
  turns=turns,
301
- creator_name=session.creator_name,
302
- creator_id=session.creator_id,
302
+ created_by=session.created_by,
303
+ shared_by=session.shared_by,
303
304
  )
304
305
  )
305
306
  return exportable
@@ -803,8 +804,8 @@ def build_enhanced_conversation_data(
803
804
  full_event.created_at.isoformat() if full_event.created_at else None
804
805
  )
805
806
  event_data["metadata"] = full_event.metadata or {}
806
- event_data["creator_name"] = full_event.creator_name
807
- event_data["creator_id"] = full_event.creator_id
807
+ event_data["created_by"] = full_event.created_by
808
+ event_data["shared_by"] = full_event.shared_by
808
809
 
809
810
  # Build sessions data with turn structure
810
811
  sessions_data = []
@@ -867,13 +868,11 @@ def build_enhanced_conversation_data(
867
868
  ),
868
869
  "model_name": turn.model_name,
869
870
  "git_commit_hash": turn.git_commit_hash,
870
- "creator_name": turn.creator_name,
871
- "creator_id": turn.creator_id,
872
871
  "messages": messages, # Structured messages for this turn
873
872
  }
874
873
  turns_data.append(turn_data)
875
874
 
876
- # Build session data (V9: includes creator fields)
875
+ # Build session data (V18: created_by/shared_by)
877
876
  session_data = {
878
877
  "session_id": session.session_id,
879
878
  "session_type": session.session_type,
@@ -884,8 +883,8 @@ def build_enhanced_conversation_data(
884
883
  "last_activity_at": (
885
884
  session.last_activity_at.isoformat() if session.last_activity_at else None
886
885
  ),
887
- "creator_name": session.creator_name,
888
- "creator_id": session.creator_id,
886
+ "created_by": session.created_by,
887
+ "shared_by": session.shared_by,
889
888
  "turns": turns_data,
890
889
  }
891
890
  sessions_data.append(session_data)
@@ -1688,9 +1687,13 @@ def _standard_upload(
1688
1687
  if ui_metadata:
1689
1688
  payload["ui_metadata"] = ui_metadata
1690
1689
 
1690
+ # Include auth headers for Bearer token authentication
1691
+ headers = get_auth_headers()
1692
+
1691
1693
  response = httpx.post(
1692
1694
  f"{backend_url}/api/share/create",
1693
1695
  json=payload,
1696
+ headers=headers,
1694
1697
  timeout=30.0,
1695
1698
  )
1696
1699
  response.raise_for_status()
@@ -1705,12 +1708,14 @@ def _upload_chunks_and_complete(
1705
1708
  upload_id: str,
1706
1709
  backend_url: str,
1707
1710
  progress_callback: Optional[Callable] = None,
1711
+ auth_headers: Optional[Dict[str, str]] = None,
1708
1712
  ) -> None:
1709
1713
  """
1710
1714
  Helper function to upload chunks and complete the upload.
1711
1715
  Can be run in background thread.
1712
1716
  """
1713
1717
  total_chunks = len(chunks)
1718
+ headers = auth_headers or {}
1714
1719
 
1715
1720
  # Upload each chunk
1716
1721
  for i, chunk in enumerate(chunks):
@@ -1727,6 +1732,7 @@ def _upload_chunks_and_complete(
1727
1732
  response = httpx.post(
1728
1733
  f"{backend_url}/api/share/chunk/upload",
1729
1734
  json=chunk_payload,
1735
+ headers=headers,
1730
1736
  timeout=60.0, # Longer timeout for chunk uploads
1731
1737
  )
1732
1738
  response.raise_for_status()
@@ -1748,6 +1754,7 @@ def _upload_chunks_and_complete(
1748
1754
  response = httpx.post(
1749
1755
  f"{backend_url}/api/share/chunk/complete",
1750
1756
  json={"upload_id": upload_id},
1757
+ headers=headers,
1751
1758
  timeout=60.0,
1752
1759
  )
1753
1760
  response.raise_for_status()
@@ -1806,6 +1813,9 @@ def _chunked_upload(
1806
1813
  if progress_callback:
1807
1814
  progress_callback(0, total_chunks + 2, "Initializing chunked upload...")
1808
1815
 
1816
+ # Get auth headers for Bearer token authentication
1817
+ auth_headers = get_auth_headers()
1818
+
1809
1819
  # Step 1: Initialize upload session (now returns share_url immediately)
1810
1820
  try:
1811
1821
  init_payload = {
@@ -1823,6 +1833,7 @@ def _chunked_upload(
1823
1833
  response = httpx.post(
1824
1834
  f"{backend_url}/api/share/chunk/init",
1825
1835
  json=init_payload,
1836
+ headers=auth_headers,
1826
1837
  timeout=30.0,
1827
1838
  )
1828
1839
  response.raise_for_status()
@@ -1847,7 +1858,7 @@ def _chunked_upload(
1847
1858
  # but user already has the share URL displayed
1848
1859
  thread = threading.Thread(
1849
1860
  target=_upload_chunks_and_complete,
1850
- args=(chunks, upload_id, backend_url, None), # No callback in background
1861
+ args=(chunks, upload_id, backend_url, None, auth_headers), # No callback in background
1851
1862
  daemon=False, # Important: let thread complete before process exits
1852
1863
  )
1853
1864
  thread.start()
@@ -1862,7 +1873,7 @@ def _chunked_upload(
1862
1873
  }
1863
1874
 
1864
1875
  # Foreground mode: upload chunks synchronously
1865
- _upload_chunks_and_complete(chunks, upload_id, backend_url, progress_callback)
1876
+ _upload_chunks_and_complete(chunks, upload_id, backend_url, progress_callback, auth_headers)
1866
1877
 
1867
1878
  return {
1868
1879
  "share_id": share_id,
@@ -1917,11 +1928,13 @@ def upload_to_backend_unencrypted(
1917
1928
  else:
1918
1929
  logger.info(f"Payload size ({payload_size / 1024:.2f}KB), using standard upload")
1919
1930
  print(f"📤 Using standard upload...")
1920
- # Standard upload
1931
+ # Standard upload with auth headers
1921
1932
  try:
1933
+ headers = get_auth_headers()
1922
1934
  response = httpx.post(
1923
1935
  f"{backend_url}/api/share/create",
1924
1936
  json=full_payload,
1937
+ headers=headers,
1925
1938
  timeout=30.0,
1926
1939
  )
1927
1940
  response.raise_for_status()
@@ -1975,6 +1988,9 @@ def _chunked_upload_unencrypted(
1975
1988
  if progress_callback:
1976
1989
  progress_callback(0, total_chunks + 2, "Initializing chunked upload...")
1977
1990
 
1991
+ # Get auth headers for Bearer token authentication
1992
+ auth_headers = get_auth_headers()
1993
+
1978
1994
  # Step 1: Initialize upload session (now returns share_url immediately)
1979
1995
  try:
1980
1996
  init_payload = {
@@ -1988,6 +2004,7 @@ def _chunked_upload_unencrypted(
1988
2004
  response = httpx.post(
1989
2005
  f"{backend_url}/api/share/chunk/init",
1990
2006
  json=init_payload,
2007
+ headers=auth_headers,
1991
2008
  timeout=30.0,
1992
2009
  )
1993
2010
  response.raise_for_status()
@@ -2012,7 +2029,7 @@ def _chunked_upload_unencrypted(
2012
2029
  # but user already has the share URL displayed
2013
2030
  thread = threading.Thread(
2014
2031
  target=_upload_chunks_and_complete,
2015
- args=(chunks, upload_id, backend_url, None), # No callback in background
2032
+ args=(chunks, upload_id, backend_url, None, auth_headers), # No callback in background
2016
2033
  daemon=False, # Important: let thread complete before process exits
2017
2034
  )
2018
2035
  thread.start()
@@ -2027,7 +2044,7 @@ def _chunked_upload_unencrypted(
2027
2044
  }
2028
2045
 
2029
2046
  # Foreground mode: upload chunks synchronously
2030
- _upload_chunks_and_complete(chunks, upload_id, backend_url, progress_callback)
2047
+ _upload_chunks_and_complete(chunks, upload_id, backend_url, progress_callback, auth_headers)
2031
2048
 
2032
2049
  return {
2033
2050
  "share_id": share_id,
@@ -3036,16 +3053,22 @@ def export_shares_interactive_command(
3036
3053
  # Check dependencies
3037
3054
  if not CRYPTO_AVAILABLE:
3038
3055
  if not json_output:
3039
- print("Error: cryptography package not installed", file=sys.stderr)
3056
+ print("Error: cryptography package not installed", file=sys.stderr)
3040
3057
  print("Install it with: pip install cryptography", file=sys.stderr)
3041
3058
  return 1
3042
3059
 
3043
3060
  if not HTTPX_AVAILABLE:
3044
3061
  if not json_output:
3045
- print("Error: httpx package not installed", file=sys.stderr)
3062
+ print("Error: httpx package not installed", file=sys.stderr)
3046
3063
  print("Install it with: pip install httpx", file=sys.stderr)
3047
3064
  return 1
3048
3065
 
3066
+ # Check authentication - require login to create shares
3067
+ if not is_logged_in():
3068
+ if not json_output:
3069
+ print("Error: Not logged in. Please run 'aline login' first.", file=sys.stderr)
3070
+ return 1
3071
+
3049
3072
  # Get backend URL
3050
3073
  if backend_url is None:
3051
3074
  # Try to load from config
@@ -288,8 +288,9 @@ def import_v2_data(
288
288
  slack_message=None,
289
289
  share_url=share_url,
290
290
  commit_hashes=[],
291
- creator_name=event_data.get("creator_name"), # V9: preserve creator info
292
- creator_id=event_data.get("creator_id"),
291
+ # V18: user identity (with backward compatibility for old format)
292
+ created_by=event_data.get("created_by") or event_data.get("uid") or event_data.get("creator_id"),
293
+ shared_by=config.uid, # Current user is the importer
293
294
  )
294
295
 
295
296
  # Use sync_events for both create and update (upsert behavior)
@@ -393,8 +394,9 @@ def import_session_with_turns(
393
394
  summary_status="completed",
394
395
  summary_locked_until=None,
395
396
  summary_error=None,
396
- creator_name=session_data.get("creator_name"), # V9: preserve creator info
397
- creator_id=session_data.get("creator_id"),
397
+ # V18: user identity (with backward compatibility for old format)
398
+ created_by=session_data.get("created_by") or session_data.get("uid") or session_data.get("creator_id"),
399
+ shared_by=config.uid, # Current user is the importer
398
400
  )
399
401
 
400
402
  if existing_session and force:
@@ -423,8 +425,8 @@ def import_session_with_turns(
423
425
  summary_status = ?,
424
426
  summary_locked_until = ?,
425
427
  summary_error = ?,
426
- creator_name = ?,
427
- creator_id = ?
428
+ created_by = ?,
429
+ shared_by = ?
428
430
  WHERE id = ?
429
431
  """,
430
432
  (
@@ -434,8 +436,8 @@ def import_session_with_turns(
434
436
  session.summary_status,
435
437
  session.summary_locked_until,
436
438
  session.summary_error,
437
- session.creator_name,
438
- session.creator_id,
439
+ session.created_by,
440
+ session.shared_by,
439
441
  session.id,
440
442
  ),
441
443
  )
@@ -473,8 +475,6 @@ def import_session_with_turns(
473
475
  timestamp=parse_datetime(turn_data.get("timestamp")) or datetime.now(),
474
476
  created_at=datetime.now(),
475
477
  git_commit_hash=turn_data.get("git_commit_hash"),
476
- creator_name=turn_data.get("creator_name"), # V9: preserve creator info
477
- creator_id=turn_data.get("creator_id"),
478
478
  )
479
479
 
480
480
  # Store turn content (JSONL)
realign/commands/init.py CHANGED
@@ -9,7 +9,6 @@ from rich.console import Console
9
9
  from ..config import (
10
10
  ReAlignConfig,
11
11
  get_default_config_content,
12
- generate_user_id,
13
12
  generate_random_username,
14
13
  )
15
14
 
@@ -647,45 +646,23 @@ def init_global(
647
646
  # Load config
648
647
  config = ReAlignConfig.load()
649
648
 
650
- # User identity setup (V9)
651
- if not config.user_id:
649
+ # User identity setup (V17: uid from Supabase login)
650
+ if not config.uid:
652
651
  console.print("\n[bold blue]═══ User Identity Setup ═══[/bold blue]")
653
652
  console.print(
654
- "ReAlign needs to identify you for tracking session ownership.\n"
653
+ "Aline requires login for user identification.\n"
655
654
  )
656
-
657
- # Generate user UUID (based on MAC address)
658
- config.user_id = generate_user_id()
659
- console.print(f"Generated user ID: [cyan]{config.user_id[:8]}...[/cyan]\n")
660
-
661
- # Prompt user for username
662
- try:
663
- from rich.prompt import Prompt
664
-
665
- user_input = Prompt.ask(
666
- "[cyan]Enter your username (or press Enter for auto-generated)[/cyan]",
667
- default="",
668
- )
669
- except ImportError:
670
- user_input = input(
671
- "Enter your username (or press Enter for auto-generated): "
672
- ).strip()
673
-
674
- if user_input:
675
- config.user_name = user_input
676
- else:
677
- # Auto-generate username
655
+ console.print(
656
+ "[yellow]Run 'aline login' to authenticate with your account.[/yellow]\n"
657
+ )
658
+ # If user_name is also not set, generate a temporary one
659
+ if not config.user_name:
678
660
  config.user_name = generate_random_username()
661
+ config.save()
679
662
  console.print(
680
- f"Auto-generated username: [yellow]{config.user_name}[/yellow]"
663
+ f"Auto-generated username: [yellow]{config.user_name}[/yellow] (will update on login)\n"
681
664
  )
682
665
 
683
- # Save config with user identity
684
- config.save()
685
- console.print(
686
- f"[green]✓[/green] User identity saved: [bold]{config.user_name}[/bold] ([dim]{config.user_id[:8]}...[/dim])\n"
687
- )
688
-
689
666
  # Initialize database
690
667
  db_path = Path(config.sqlite_db_path).expanduser()
691
668
  db_path.parent.mkdir(parents=True, exist_ok=True)
@@ -1012,7 +1012,7 @@ def _get_imported_sessions(db, exclude_session_ids: set) -> list:
1012
1012
  "session_file": None, # No file for imported sessions
1013
1013
  "session_title": session.session_title,
1014
1014
  "session_summary": session.session_summary,
1015
- "creator_name": session.creator_name,
1015
+ "created_by": session.created_by,
1016
1016
  }
1017
1017
  )
1018
1018
 
@@ -1226,7 +1226,7 @@ def _get_session_tracking_status_batch(
1226
1226
  if record and status in ("partial", "tracked"):
1227
1227
  info["session_title"] = record.session_title
1228
1228
  info["session_summary"] = record.session_summary
1229
- info["creator_name"] = record.creator_name
1229
+ info["created_by"] = record.created_by
1230
1230
 
1231
1231
  session_infos.append(info)
1232
1232
 
@@ -1526,7 +1526,7 @@ def watcher_session_list_command(
1526
1526
  "last_activity": info["last_activity"].isoformat(),
1527
1527
  "session_title": info.get("session_title"),
1528
1528
  "session_summary": info.get("session_summary"),
1529
- "creator_name": info.get("creator_name"),
1529
+ "created_by": info.get("created_by"),
1530
1530
  "session_file": (
1531
1531
  str(info.get("session_file")) if info.get("session_file") else None
1532
1532
  ),
@@ -1622,14 +1622,12 @@ def watcher_session_list_command(
1622
1622
  title_str = info.get("session_title") or "-"
1623
1623
  title_str = title_str.strip()
1624
1624
 
1625
- # V9: Display creator (truncate if too long)
1625
+ # V18: Display created_by UID (truncate if too long)
1626
1626
  creator_display = "-"
1627
1627
  if info["status"] in ("partial", "tracked"):
1628
- creator_name = info.get("creator_name")
1629
- if creator_name:
1630
- creator_display = creator_name
1631
- if len(creator_display) > 10:
1632
- creator_display = creator_display[:10] + "..."
1628
+ created_by = info.get("created_by")
1629
+ if created_by:
1630
+ creator_display = created_by[:8] + "..."
1633
1631
 
1634
1632
  # Truncate project name
1635
1633
  project_name = info["project_name"]
@@ -1851,8 +1849,7 @@ def watcher_event_generate_command(session_selector: str, show_sessions: bool =
1851
1849
  updated_at=now,
1852
1850
  metadata={},
1853
1851
  commit_hashes=[],
1854
- creator_name=config.user_name,
1855
- creator_id=config.user_id,
1852
+ created_by=config.uid,
1856
1853
  )
1857
1854
 
1858
1855
  # Save to database
@@ -2117,7 +2114,7 @@ def watcher_event_list_command(
2117
2114
  "id": event.id,
2118
2115
  "title": event.title,
2119
2116
  "description": event.description,
2120
- "creator_name": event.creator_name,
2117
+ "created_by": event.created_by,
2121
2118
  "generated_by": generated_by,
2122
2119
  "session_count": session_count,
2123
2120
  "session_ids": session_ids,
@@ -2177,10 +2174,8 @@ def watcher_event_list_command(
2177
2174
  else:
2178
2175
  share_link_display = "[dim]-[/dim]"
2179
2176
 
2180
- # V9: Display creator (truncate if too long)
2181
- creator_display = event.creator_name or "-"
2182
- if creator_display and len(creator_display) > 12:
2183
- creator_display = creator_display[:12] + "..."
2177
+ # V18: Display created_by UID (truncate)
2178
+ creator_display = (event.created_by[:8] + "...") if event.created_by else "-"
2184
2179
 
2185
2180
  table.add_row(
2186
2181
  str(idx),