aline-ai 0.5.11__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.
- {aline_ai-0.5.11.dist-info → aline_ai-0.5.13.dist-info}/METADATA +1 -1
- {aline_ai-0.5.11.dist-info → aline_ai-0.5.13.dist-info}/RECORD +20 -18
- realign/__init__.py +1 -1
- realign/auth.py +539 -0
- realign/cli.py +23 -1
- realign/commands/auth.py +242 -0
- realign/commands/export_shares.py +44 -21
- realign/commands/import_shares.py +10 -10
- realign/commands/init.py +26 -40
- realign/commands/watcher.py +11 -16
- realign/config.py +12 -29
- realign/dashboard/widgets/config_panel.py +177 -1
- realign/db/base.py +28 -11
- realign/db/schema.py +102 -15
- realign/db/sqlite_db.py +108 -58
- realign/watcher_core.py +1 -9
- {aline_ai-0.5.11.dist-info → aline_ai-0.5.13.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.11.dist-info → aline_ai-0.5.13.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.11.dist-info → aline_ai-0.5.13.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.11.dist-info → aline_ai-0.5.13.dist-info}/top_level.txt +0 -0
realign/commands/auth.py
ADDED
|
@@ -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
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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["
|
|
807
|
-
event_data["
|
|
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 (
|
|
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
|
-
"
|
|
888
|
-
"
|
|
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("
|
|
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("
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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.
|
|
438
|
-
session.
|
|
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
|
|
|
@@ -19,7 +18,7 @@ console = Console()
|
|
|
19
18
|
# tmux config template for Aline-managed dashboard sessions.
|
|
20
19
|
# Stored at ~/.aline/tmux/tmux.conf and sourced by the dashboard tmux bootstrap.
|
|
21
20
|
# Bump this version when the tmux config changes to trigger auto-update on `aline init`.
|
|
22
|
-
_TMUX_CONFIG_VERSION =
|
|
21
|
+
_TMUX_CONFIG_VERSION = 8
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
def _get_tmux_config() -> str:
|
|
@@ -53,16 +52,25 @@ set -s escape-time 0
|
|
|
53
52
|
# Better scrolling: enter copy-mode with -e so scrolling to bottom exits it.
|
|
54
53
|
bind-key -n WheelUpPane if-shell -F -t = "#{mouse_any_flag}" "send-keys -M" "if -Ft= '#{pane_in_mode}' 'send-keys -M' 'copy-mode -e -t ='"
|
|
55
54
|
|
|
56
|
-
# macOS clipboard
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
' ''
|
|
55
|
+
# macOS clipboard: copy selection to clipboard when drag ends.
|
|
56
|
+
# Use copy-pipe-no-clear to preserve selection highlight after copying.
|
|
57
|
+
bind -T copy-mode-vi MouseDragEnd1Pane send -X copy-pipe-no-clear "pbcopy"
|
|
58
|
+
bind -T copy-mode MouseDragEnd1Pane send -X copy-pipe-no-clear "pbcopy"
|
|
61
59
|
|
|
62
|
-
#
|
|
60
|
+
# MouseDrag1Pane: Clear old selection and start new one when dragging begins.
|
|
61
|
+
# This ensures selection only clears when starting a NEW drag, not on click.
|
|
62
|
+
bind -T copy-mode-vi MouseDrag1Pane select-pane \; send -X clear-selection \; send -X begin-selection
|
|
63
|
+
bind -T copy-mode MouseDrag1Pane select-pane \; send -X clear-selection \; send -X begin-selection
|
|
64
|
+
|
|
65
|
+
# MouseDown1Pane: Click clears selection but stays in copy-mode (no scroll).
|
|
66
|
+
# To exit copy-mode: scroll to bottom (auto-exit) or press q/Escape.
|
|
63
67
|
bind -T copy-mode-vi MouseDown1Pane select-pane \; send -X clear-selection
|
|
64
68
|
bind -T copy-mode MouseDown1Pane select-pane \; send -X clear-selection
|
|
65
69
|
|
|
70
|
+
# Escape key: exit copy-mode (also use this before Cmd+V paste in copy-mode).
|
|
71
|
+
bind -T copy-mode-vi Escape send -X cancel
|
|
72
|
+
bind -T copy-mode Escape send -X cancel
|
|
73
|
+
|
|
66
74
|
# Type-to-Exit: Typing any alphanumeric character exits copy-mode and sends the key.
|
|
67
75
|
"""
|
|
68
76
|
def _tmux_quote(value: str) -> str:
|
|
@@ -638,45 +646,23 @@ def init_global(
|
|
|
638
646
|
# Load config
|
|
639
647
|
config = ReAlignConfig.load()
|
|
640
648
|
|
|
641
|
-
# User identity setup (
|
|
642
|
-
if not config.
|
|
649
|
+
# User identity setup (V17: uid from Supabase login)
|
|
650
|
+
if not config.uid:
|
|
643
651
|
console.print("\n[bold blue]═══ User Identity Setup ═══[/bold blue]")
|
|
644
652
|
console.print(
|
|
645
|
-
"
|
|
653
|
+
"Aline requires login for user identification.\n"
|
|
646
654
|
)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
# Prompt user for username
|
|
653
|
-
try:
|
|
654
|
-
from rich.prompt import Prompt
|
|
655
|
-
|
|
656
|
-
user_input = Prompt.ask(
|
|
657
|
-
"[cyan]Enter your username (or press Enter for auto-generated)[/cyan]",
|
|
658
|
-
default="",
|
|
659
|
-
)
|
|
660
|
-
except ImportError:
|
|
661
|
-
user_input = input(
|
|
662
|
-
"Enter your username (or press Enter for auto-generated): "
|
|
663
|
-
).strip()
|
|
664
|
-
|
|
665
|
-
if user_input:
|
|
666
|
-
config.user_name = user_input
|
|
667
|
-
else:
|
|
668
|
-
# 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:
|
|
669
660
|
config.user_name = generate_random_username()
|
|
661
|
+
config.save()
|
|
670
662
|
console.print(
|
|
671
|
-
f"Auto-generated username: [yellow]{config.user_name}[/yellow]"
|
|
663
|
+
f"Auto-generated username: [yellow]{config.user_name}[/yellow] (will update on login)\n"
|
|
672
664
|
)
|
|
673
665
|
|
|
674
|
-
# Save config with user identity
|
|
675
|
-
config.save()
|
|
676
|
-
console.print(
|
|
677
|
-
f"[green]✓[/green] User identity saved: [bold]{config.user_name}[/bold] ([dim]{config.user_id[:8]}...[/dim])\n"
|
|
678
|
-
)
|
|
679
|
-
|
|
680
666
|
# Initialize database
|
|
681
667
|
db_path = Path(config.sqlite_db_path).expanduser()
|
|
682
668
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|