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/watcher.py
CHANGED
|
@@ -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
|
-
"
|
|
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["
|
|
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
|
-
"
|
|
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
|
-
#
|
|
1625
|
+
# V18: Display created_by UID (truncate if too long)
|
|
1626
1626
|
creator_display = "-"
|
|
1627
1627
|
if info["status"] in ("partial", "tracked"):
|
|
1628
|
-
|
|
1629
|
-
if
|
|
1630
|
-
creator_display =
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
#
|
|
2181
|
-
creator_display = event.
|
|
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),
|
realign/config.py
CHANGED
|
@@ -27,9 +27,9 @@ class ReAlignConfig:
|
|
|
27
27
|
"https://realign-server.vercel.app" # Backend URL for interactive share export
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
-
# User identity (V9)
|
|
31
|
-
user_name: str = "" # User's display name (set during init)
|
|
32
|
-
|
|
30
|
+
# User identity (V9, renamed in V17: user_id -> uid)
|
|
31
|
+
user_name: str = "" # User's display name (set during init or login)
|
|
32
|
+
uid: str = "" # User's UUID (from Supabase login)
|
|
33
33
|
|
|
34
34
|
# Session catch-up settings
|
|
35
35
|
max_catchup_sessions: int = 3 # Max sessions to auto-import on watcher startup
|
|
@@ -92,7 +92,7 @@ class ReAlignConfig:
|
|
|
92
92
|
"enable_temp_turn_titles": os.getenv("REALIGN_ENABLE_TEMP_TURN_TITLES"),
|
|
93
93
|
"share_backend_url": os.getenv("REALIGN_SHARE_BACKEND_URL"),
|
|
94
94
|
"user_name": os.getenv("REALIGN_USER_NAME"),
|
|
95
|
-
"
|
|
95
|
+
"uid": os.getenv("REALIGN_UID"),
|
|
96
96
|
"max_catchup_sessions": os.getenv("REALIGN_MAX_CATCHUP_SESSIONS"),
|
|
97
97
|
"anthropic_api_key": os.getenv("REALIGN_ANTHROPIC_API_KEY"),
|
|
98
98
|
"openai_api_key": os.getenv("REALIGN_OPENAI_API_KEY"),
|
|
@@ -124,6 +124,13 @@ class ReAlignConfig:
|
|
|
124
124
|
else:
|
|
125
125
|
config_dict[key] = value
|
|
126
126
|
|
|
127
|
+
# Migration: user_id -> uid (V17)
|
|
128
|
+
if "user_id" in config_dict and "uid" not in config_dict:
|
|
129
|
+
config_dict["uid"] = config_dict.pop("user_id")
|
|
130
|
+
elif "user_id" in config_dict:
|
|
131
|
+
# Both exist, prefer uid, discard user_id
|
|
132
|
+
config_dict.pop("user_id")
|
|
133
|
+
|
|
127
134
|
return cls(**{k: v for k, v in config_dict.items() if k in cls.__annotations__})
|
|
128
135
|
|
|
129
136
|
def save(self, config_path: Optional[Path] = None):
|
|
@@ -148,7 +155,7 @@ class ReAlignConfig:
|
|
|
148
155
|
"enable_temp_turn_titles": self.enable_temp_turn_titles,
|
|
149
156
|
"share_backend_url": self.share_backend_url,
|
|
150
157
|
"user_name": self.user_name,
|
|
151
|
-
"
|
|
158
|
+
"uid": self.uid,
|
|
152
159
|
"max_catchup_sessions": self.max_catchup_sessions,
|
|
153
160
|
"anthropic_api_key": self.anthropic_api_key,
|
|
154
161
|
"openai_api_key": self.openai_api_key,
|
|
@@ -163,30 +170,6 @@ class ReAlignConfig:
|
|
|
163
170
|
yaml.dump(config_dict, f, default_flow_style=False, allow_unicode=True)
|
|
164
171
|
|
|
165
172
|
|
|
166
|
-
def generate_user_id() -> str:
|
|
167
|
-
"""
|
|
168
|
-
Generate a persistent user UUID based on MAC address.
|
|
169
|
-
|
|
170
|
-
Uses uuid.getnode() to get the MAC address, then generates a UUID5
|
|
171
|
-
using DNS namespace. If MAC address retrieval fails, falls back to
|
|
172
|
-
a random UUID.
|
|
173
|
-
|
|
174
|
-
Returns:
|
|
175
|
-
str: User UUID as a string
|
|
176
|
-
"""
|
|
177
|
-
import uuid
|
|
178
|
-
|
|
179
|
-
try:
|
|
180
|
-
mac = uuid.getnode()
|
|
181
|
-
# Use MAC address with DNS namespace to generate UUID5
|
|
182
|
-
namespace = uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8") # DNS namespace
|
|
183
|
-
user_uuid = uuid.uuid5(namespace, str(mac))
|
|
184
|
-
return str(user_uuid)
|
|
185
|
-
except Exception:
|
|
186
|
-
# Fallback to random UUID if MAC address retrieval fails
|
|
187
|
-
return str(uuid.uuid4())
|
|
188
|
-
|
|
189
|
-
|
|
190
173
|
def generate_random_username() -> str:
|
|
191
174
|
"""
|
|
192
175
|
Generate a random username with format: 3 lowercase letters + 3 digits.
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
"""Config Panel Widget for viewing and editing configuration."""
|
|
2
2
|
|
|
3
|
+
import threading
|
|
4
|
+
import webbrowser
|
|
3
5
|
from dataclasses import fields
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import Optional
|
|
6
8
|
|
|
7
9
|
from textual.app import ComposeResult
|
|
8
|
-
from textual.containers import Horizontal
|
|
10
|
+
from textual.containers import Horizontal, Vertical
|
|
9
11
|
from textual.widgets import Button, DataTable, Input, Static, Switch
|
|
10
12
|
|
|
11
13
|
from ..tmux_manager import _run_outer_tmux, OUTER_SESSION
|
|
14
|
+
from ...auth import (
|
|
15
|
+
load_credentials,
|
|
16
|
+
save_credentials,
|
|
17
|
+
clear_credentials,
|
|
18
|
+
open_login_page,
|
|
19
|
+
is_logged_in,
|
|
20
|
+
get_current_user,
|
|
21
|
+
find_free_port,
|
|
22
|
+
start_callback_server,
|
|
23
|
+
validate_cli_token,
|
|
24
|
+
)
|
|
25
|
+
from ...config import ReAlignConfig
|
|
26
|
+
from ...logging_config import setup_logger
|
|
27
|
+
|
|
28
|
+
logger = setup_logger("realign.dashboard.widgets.config_panel", "dashboard.log")
|
|
12
29
|
|
|
13
30
|
|
|
14
31
|
class ConfigPanel(Static):
|
|
@@ -74,6 +91,21 @@ class ConfigPanel(Static):
|
|
|
74
91
|
ConfigPanel .tmux-settings Switch {
|
|
75
92
|
width: auto;
|
|
76
93
|
}
|
|
94
|
+
|
|
95
|
+
ConfigPanel .account-section {
|
|
96
|
+
height: auto;
|
|
97
|
+
margin-top: 1;
|
|
98
|
+
padding: 1;
|
|
99
|
+
border: solid $success;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
ConfigPanel .account-section .account-status {
|
|
103
|
+
margin-bottom: 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ConfigPanel .account-section Button {
|
|
107
|
+
margin-right: 1;
|
|
108
|
+
}
|
|
77
109
|
"""
|
|
78
110
|
|
|
79
111
|
def __init__(self) -> None:
|
|
@@ -83,6 +115,8 @@ class ConfigPanel(Static):
|
|
|
83
115
|
self._selected_key: Optional[str] = None
|
|
84
116
|
self._border_resize_enabled: bool = True # Track tmux border resize state
|
|
85
117
|
self._syncing_switch: bool = False # Flag to prevent recursive switch updates
|
|
118
|
+
self._login_in_progress: bool = False # Track login state
|
|
119
|
+
self._refresh_timer = None # Timer for auto-refresh
|
|
86
120
|
|
|
87
121
|
def compose(self) -> ComposeResult:
|
|
88
122
|
"""Compose the config panel layout."""
|
|
@@ -96,6 +130,14 @@ class ConfigPanel(Static):
|
|
|
96
130
|
yield Button("Save", id="save-btn", variant="primary")
|
|
97
131
|
yield Button("Reload", id="reload-btn", variant="default")
|
|
98
132
|
|
|
133
|
+
# Account section
|
|
134
|
+
with Static(classes="account-section"):
|
|
135
|
+
yield Static("[bold]Account[/bold]", classes="section-title")
|
|
136
|
+
yield Static(id="account-status", classes="account-status")
|
|
137
|
+
with Horizontal(classes="button-row"):
|
|
138
|
+
yield Button("Login", id="login-btn", variant="primary")
|
|
139
|
+
yield Button("Logout", id="logout-btn", variant="warning")
|
|
140
|
+
|
|
99
141
|
# Tmux settings section
|
|
100
142
|
with Static(classes="tmux-settings"):
|
|
101
143
|
yield Static("[bold]Tmux Settings[/bold]", classes="section-title")
|
|
@@ -112,9 +154,15 @@ class ConfigPanel(Static):
|
|
|
112
154
|
# Load initial data
|
|
113
155
|
self.refresh_data()
|
|
114
156
|
|
|
157
|
+
# Update account status display
|
|
158
|
+
self._update_account_status()
|
|
159
|
+
|
|
115
160
|
# Query and set the actual tmux border resize state
|
|
116
161
|
self._sync_border_resize_switch()
|
|
117
162
|
|
|
163
|
+
# Start timer to periodically refresh account status (every 5 seconds)
|
|
164
|
+
self._refresh_timer = self.set_interval(5.0, self._update_account_status)
|
|
165
|
+
|
|
118
166
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
119
167
|
"""Handle row selection in the config table."""
|
|
120
168
|
table = self.query_one("#config-table", DataTable)
|
|
@@ -147,7 +195,12 @@ class ConfigPanel(Static):
|
|
|
147
195
|
self._save_config()
|
|
148
196
|
elif event.button.id == "reload-btn":
|
|
149
197
|
self.refresh_data()
|
|
198
|
+
self._update_account_status()
|
|
150
199
|
self.app.notify("Configuration reloaded", title="Config")
|
|
200
|
+
elif event.button.id == "login-btn":
|
|
201
|
+
self._handle_login()
|
|
202
|
+
elif event.button.id == "logout-btn":
|
|
203
|
+
self._handle_logout()
|
|
151
204
|
|
|
152
205
|
def on_switch_changed(self, event: Switch.Changed) -> None:
|
|
153
206
|
"""Handle switch toggle events."""
|
|
@@ -156,6 +209,129 @@ class ConfigPanel(Static):
|
|
|
156
209
|
if event.switch.id == "border-resize-switch":
|
|
157
210
|
self._toggle_border_resize(event.value)
|
|
158
211
|
|
|
212
|
+
def _update_account_status(self) -> None:
|
|
213
|
+
"""Update the account status display."""
|
|
214
|
+
try:
|
|
215
|
+
status_widget = self.query_one("#account-status", Static)
|
|
216
|
+
login_btn = self.query_one("#login-btn", Button)
|
|
217
|
+
logout_btn = self.query_one("#logout-btn", Button)
|
|
218
|
+
except Exception:
|
|
219
|
+
# Widget not ready yet
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
# Don't update if login is in progress
|
|
223
|
+
if self._login_in_progress:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
credentials = get_current_user()
|
|
227
|
+
if credentials:
|
|
228
|
+
status_widget.update(
|
|
229
|
+
f"[green]Logged in as:[/green] [bold]{credentials.email}[/bold]"
|
|
230
|
+
)
|
|
231
|
+
login_btn.disabled = True
|
|
232
|
+
logout_btn.disabled = False
|
|
233
|
+
else:
|
|
234
|
+
status_widget.update("[yellow]Not logged in[/yellow]")
|
|
235
|
+
login_btn.disabled = False
|
|
236
|
+
logout_btn.disabled = True
|
|
237
|
+
|
|
238
|
+
def _handle_login(self) -> None:
|
|
239
|
+
"""Handle login button click - start login flow in background."""
|
|
240
|
+
if self._login_in_progress:
|
|
241
|
+
self.app.notify("Login already in progress...", title="Login")
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
self._login_in_progress = True
|
|
245
|
+
|
|
246
|
+
# Update UI to show login in progress
|
|
247
|
+
login_btn = self.query_one("#login-btn", Button)
|
|
248
|
+
login_btn.disabled = True
|
|
249
|
+
status_widget = self.query_one("#account-status", Static)
|
|
250
|
+
status_widget.update("[cyan]Opening browser for login...[/cyan]")
|
|
251
|
+
|
|
252
|
+
# Start login flow in background thread
|
|
253
|
+
def do_login():
|
|
254
|
+
try:
|
|
255
|
+
port = find_free_port()
|
|
256
|
+
open_login_page(callback_port=port)
|
|
257
|
+
|
|
258
|
+
# Wait for callback (up to 5 minutes)
|
|
259
|
+
cli_token, error = start_callback_server(port, timeout=300)
|
|
260
|
+
|
|
261
|
+
if error:
|
|
262
|
+
self.app.call_from_thread(
|
|
263
|
+
self.app.notify, f"Login failed: {error}", title="Login", severity="error"
|
|
264
|
+
)
|
|
265
|
+
self.app.call_from_thread(self._update_account_status)
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
if not cli_token:
|
|
269
|
+
self.app.call_from_thread(
|
|
270
|
+
self.app.notify, "No token received", title="Login", severity="error"
|
|
271
|
+
)
|
|
272
|
+
self.app.call_from_thread(self._update_account_status)
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
# Validate token
|
|
276
|
+
credentials = validate_cli_token(cli_token)
|
|
277
|
+
if not credentials:
|
|
278
|
+
self.app.call_from_thread(
|
|
279
|
+
self.app.notify, "Invalid token", title="Login", severity="error"
|
|
280
|
+
)
|
|
281
|
+
self.app.call_from_thread(self._update_account_status)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
# Save credentials
|
|
285
|
+
if save_credentials(credentials):
|
|
286
|
+
# Sync Supabase uid to local config
|
|
287
|
+
try:
|
|
288
|
+
config = ReAlignConfig.load()
|
|
289
|
+
old_uid = config.uid
|
|
290
|
+
config.uid = credentials.user_id
|
|
291
|
+
if not config.user_name:
|
|
292
|
+
config.user_name = credentials.email.split("@")[0]
|
|
293
|
+
config.save()
|
|
294
|
+
logger.info(f"Synced Supabase uid to config: {credentials.user_id[:8]}...")
|
|
295
|
+
|
|
296
|
+
# V18: Upsert user info to users table
|
|
297
|
+
try:
|
|
298
|
+
from ...db import get_database
|
|
299
|
+
db = get_database()
|
|
300
|
+
db.upsert_user(config.uid, config.user_name)
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.debug(f"Failed to upsert user to users table: {e}")
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.warning(f"Failed to sync uid to config: {e}")
|
|
305
|
+
|
|
306
|
+
self.app.call_from_thread(
|
|
307
|
+
self.app.notify, f"Logged in as {credentials.email}", title="Login"
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
self.app.call_from_thread(
|
|
311
|
+
self.app.notify, "Failed to save credentials", title="Login", severity="error"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
self.app.call_from_thread(self._update_account_status)
|
|
315
|
+
|
|
316
|
+
finally:
|
|
317
|
+
self._login_in_progress = False
|
|
318
|
+
|
|
319
|
+
thread = threading.Thread(target=do_login, daemon=True)
|
|
320
|
+
thread.start()
|
|
321
|
+
|
|
322
|
+
self.app.notify("Complete login in browser...", title="Login")
|
|
323
|
+
|
|
324
|
+
def _handle_logout(self) -> None:
|
|
325
|
+
"""Handle logout button click - clear credentials."""
|
|
326
|
+
credentials = load_credentials()
|
|
327
|
+
email = credentials.email if credentials else "user"
|
|
328
|
+
|
|
329
|
+
if clear_credentials():
|
|
330
|
+
self._update_account_status()
|
|
331
|
+
self.app.notify(f"Logged out: {email}", title="Account")
|
|
332
|
+
else:
|
|
333
|
+
self.app.notify("Failed to logout", title="Account", severity="error")
|
|
334
|
+
|
|
159
335
|
def _sync_border_resize_switch(self) -> None:
|
|
160
336
|
"""Query tmux state and sync the switch to match."""
|
|
161
337
|
try:
|
realign/db/base.py
CHANGED
|
@@ -49,9 +49,9 @@ class SessionRecord:
|
|
|
49
49
|
summary_status: Optional[str] = None
|
|
50
50
|
summary_locked_until: Optional[datetime] = None
|
|
51
51
|
summary_error: Optional[str] = None
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
# V18: user identity
|
|
53
|
+
created_by: Optional[str] = None # Creator UID
|
|
54
|
+
shared_by: Optional[str] = None # Sharer UID (who imported this)
|
|
55
55
|
temp_title: Optional[str] = None
|
|
56
56
|
# V10: cached total turn count for session list performance
|
|
57
57
|
total_turns: Optional[int] = None
|
|
@@ -78,9 +78,6 @@ class TurnRecord:
|
|
|
78
78
|
git_commit_hash: Optional[str] = None
|
|
79
79
|
# V12: temporary title stored in DB turns.temp_title
|
|
80
80
|
temp_title: Optional[str] = None
|
|
81
|
-
# V9: creator information
|
|
82
|
-
creator_name: Optional[str] = None
|
|
83
|
-
creator_id: Optional[str] = None
|
|
84
81
|
|
|
85
82
|
|
|
86
83
|
@dataclass
|
|
@@ -102,9 +99,9 @@ class EventRecord:
|
|
|
102
99
|
share_id: Optional[str] = None # V14: Share ID on server (for reuse)
|
|
103
100
|
share_admin_token: Optional[str] = None # V14: Admin token for extending expiry
|
|
104
101
|
share_expiry_at: Optional[datetime] = None # V14: Last known expiry timestamp
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
# V18: user identity
|
|
103
|
+
created_by: Optional[str] = None # Creator UID
|
|
104
|
+
shared_by: Optional[str] = None # Sharer UID (who imported this)
|
|
108
105
|
|
|
109
106
|
|
|
110
107
|
@dataclass
|
|
@@ -124,8 +121,8 @@ class AgentRecord:
|
|
|
124
121
|
status: str = "active" # 'active', 'stopped'
|
|
125
122
|
attention: Optional[str] = None # 'permission_request', 'stop', or None
|
|
126
123
|
source: Optional[str] = None
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
# V18: user identity
|
|
125
|
+
created_by: Optional[str] = None # Creator UID
|
|
129
126
|
|
|
130
127
|
|
|
131
128
|
@dataclass
|
|
@@ -143,6 +140,16 @@ class AgentContextRecord:
|
|
|
143
140
|
event_ids: Optional[List[str]] = None
|
|
144
141
|
|
|
145
142
|
|
|
143
|
+
@dataclass
|
|
144
|
+
class UserRecord:
|
|
145
|
+
"""Represents a user in the users table (V18)."""
|
|
146
|
+
|
|
147
|
+
uid: str
|
|
148
|
+
user_name: Optional[str] = None
|
|
149
|
+
created_at: Optional[datetime] = None
|
|
150
|
+
updated_at: Optional[datetime] = None
|
|
151
|
+
|
|
152
|
+
|
|
146
153
|
class DatabaseInterface(ABC):
|
|
147
154
|
"""Abstract interface for ReAlign storage backend."""
|
|
148
155
|
|
|
@@ -420,3 +427,13 @@ class DatabaseInterface(ABC):
|
|
|
420
427
|
def get_all_locks(self, include_expired: bool = False) -> List[LockRecord]:
|
|
421
428
|
"""Get all locks, optionally including expired ones."""
|
|
422
429
|
pass
|
|
430
|
+
|
|
431
|
+
@abstractmethod
|
|
432
|
+
def upsert_user(self, uid: str, user_name: Optional[str] = None) -> None:
|
|
433
|
+
"""Insert or update a user in the users table (V18)."""
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
@abstractmethod
|
|
437
|
+
def get_user(self, uid: str) -> Optional[UserRecord]:
|
|
438
|
+
"""Get a user by UID from the users table (V18)."""
|
|
439
|
+
pass
|
realign/db/schema.py
CHANGED
|
@@ -62,9 +62,23 @@ Schema V15: Agents and contexts tables (replaces terminal.json and load.json).
|
|
|
62
62
|
Schema V16: Remove FK constraints from agent_context_sessions/events.
|
|
63
63
|
- Context may reference sessions/events not yet imported to DB
|
|
64
64
|
- Recreate M2M tables without FK constraints on session_id/event_id
|
|
65
|
+
|
|
66
|
+
Schema V17: Rename creator_id/creator_name to uid/user_name.
|
|
67
|
+
- sessions.creator_id -> uid, sessions.creator_name -> user_name
|
|
68
|
+
- turns.creator_id -> uid, turns.creator_name -> user_name
|
|
69
|
+
- events.creator_id -> uid, events.creator_name -> user_name
|
|
70
|
+
- agents.creator_id -> uid, agents.creator_name -> user_name
|
|
71
|
+
- Update indexes accordingly
|
|
72
|
+
|
|
73
|
+
Schema V18: UID refactor - created_by/shared_by with users table.
|
|
74
|
+
- New users table: uid -> user_name mapping
|
|
75
|
+
- sessions/events: uid -> created_by, drop user_name, add shared_by
|
|
76
|
+
- turns: drop uid and user_name (inherit from session)
|
|
77
|
+
- agents: uid -> created_by, drop user_name
|
|
78
|
+
- Update indexes accordingly
|
|
65
79
|
"""
|
|
66
80
|
|
|
67
|
-
SCHEMA_VERSION =
|
|
81
|
+
SCHEMA_VERSION = 18
|
|
68
82
|
|
|
69
83
|
FTS_EVENTS_SCRIPTS = [
|
|
70
84
|
# Full Text Search for Events
|
|
@@ -115,7 +129,7 @@ INIT_SCRIPTS = [
|
|
|
115
129
|
metadata TEXT -- JSON metadata
|
|
116
130
|
);
|
|
117
131
|
""",
|
|
118
|
-
# Sessions table (V2: decoupled from projects, V3: summary fields,
|
|
132
|
+
# Sessions table (V2: decoupled from projects, V3: summary fields, V18: created_by/shared_by, V10: total_turns cache)
|
|
119
133
|
"""
|
|
120
134
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
121
135
|
id TEXT PRIMARY KEY, -- session ID (filename stem)
|
|
@@ -133,13 +147,13 @@ INIT_SCRIPTS = [
|
|
|
133
147
|
summary_status TEXT DEFAULT 'idle', -- V7: 'idle' | 'processing' | 'completed' | 'failed'
|
|
134
148
|
summary_locked_until TEXT, -- V7: lease/TTL to avoid stuck processing
|
|
135
149
|
summary_error TEXT, -- V7: last error message if failed
|
|
136
|
-
|
|
137
|
-
|
|
150
|
+
created_by TEXT, -- V18: Creator UID (FK to users.uid)
|
|
151
|
+
shared_by TEXT, -- V18: Sharer UID (who imported this)
|
|
138
152
|
total_turns INTEGER DEFAULT 0, -- V10: Cached total turn count (avoids reading files)
|
|
139
153
|
total_turns_mtime REAL -- V12: File mtime when total_turns was cached (for validation)
|
|
140
154
|
);
|
|
141
155
|
""",
|
|
142
|
-
# Turns table (corresponds to git commits,
|
|
156
|
+
# Turns table (corresponds to git commits, V18: uid/user_name removed)
|
|
143
157
|
"""
|
|
144
158
|
CREATE TABLE IF NOT EXISTS turns (
|
|
145
159
|
id TEXT PRIMARY KEY, -- UUID
|
|
@@ -158,8 +172,6 @@ INIT_SCRIPTS = [
|
|
|
158
172
|
timestamp TEXT NOT NULL,
|
|
159
173
|
created_at TEXT DEFAULT (datetime('now')),
|
|
160
174
|
git_commit_hash TEXT, -- Linked git commit hash
|
|
161
|
-
creator_name TEXT, -- V9: Username who created the turn
|
|
162
|
-
creator_id TEXT, -- V9: User UUID
|
|
163
175
|
UNIQUE(session_id, turn_number)
|
|
164
176
|
);
|
|
165
177
|
""",
|
|
@@ -209,12 +221,11 @@ INIT_SCRIPTS = [
|
|
|
209
221
|
"CREATE INDEX IF NOT EXISTS idx_sessions_workspace ON sessions(workspace_path);",
|
|
210
222
|
"CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(last_activity_at DESC);",
|
|
211
223
|
"CREATE INDEX IF NOT EXISTS idx_sessions_type ON sessions(session_type);",
|
|
212
|
-
"CREATE INDEX IF NOT EXISTS
|
|
224
|
+
"CREATE INDEX IF NOT EXISTS idx_sessions_created_by ON sessions(created_by);", # V18
|
|
213
225
|
"CREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id);",
|
|
214
226
|
"CREATE INDEX IF NOT EXISTS idx_turns_timestamp ON turns(timestamp DESC);",
|
|
215
227
|
"CREATE INDEX IF NOT EXISTS idx_turns_hash ON turns(content_hash);",
|
|
216
|
-
|
|
217
|
-
# Events table (V9: creator fields)
|
|
228
|
+
# Events table (V18: created_by/shared_by)
|
|
218
229
|
"""
|
|
219
230
|
CREATE TABLE IF NOT EXISTS events (
|
|
220
231
|
id TEXT PRIMARY KEY, -- UUID
|
|
@@ -233,8 +244,8 @@ INIT_SCRIPTS = [
|
|
|
233
244
|
share_id TEXT, -- V14: Server share ID (for reuse)
|
|
234
245
|
share_admin_token TEXT, -- V14: Server admin token (extend expiry)
|
|
235
246
|
share_expiry_at TEXT, -- V14: Last known expiry timestamp
|
|
236
|
-
|
|
237
|
-
|
|
247
|
+
created_by TEXT, -- V18: Creator UID (FK to users.uid)
|
|
248
|
+
shared_by TEXT -- V18: Sharer UID (who imported this)
|
|
238
249
|
);
|
|
239
250
|
""",
|
|
240
251
|
# Event-Commit relationship (Many-to-Many)
|
|
@@ -256,7 +267,8 @@ INIT_SCRIPTS = [
|
|
|
256
267
|
""",
|
|
257
268
|
"CREATE INDEX IF NOT EXISTS idx_event_sessions_event ON event_sessions(event_id);",
|
|
258
269
|
"CREATE INDEX IF NOT EXISTS idx_event_sessions_session ON event_sessions(session_id);",
|
|
259
|
-
|
|
270
|
+
"CREATE INDEX IF NOT EXISTS idx_events_created_by ON events(created_by);", # V18
|
|
271
|
+
# Agents table (V15: replaces terminal.json, V18: created_by)
|
|
260
272
|
"""
|
|
261
273
|
CREATE TABLE IF NOT EXISTS agents (
|
|
262
274
|
id TEXT PRIMARY KEY, -- terminal_id (UUID)
|
|
@@ -272,8 +284,7 @@ INIT_SCRIPTS = [
|
|
|
272
284
|
source TEXT,
|
|
273
285
|
created_at TEXT DEFAULT (datetime('now')),
|
|
274
286
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
275
|
-
|
|
276
|
-
creator_id TEXT
|
|
287
|
+
created_by TEXT -- V18: Creator UID (FK to users.uid)
|
|
277
288
|
);
|
|
278
289
|
""",
|
|
279
290
|
"CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id);",
|
|
@@ -315,6 +326,15 @@ INIT_SCRIPTS = [
|
|
|
315
326
|
""",
|
|
316
327
|
"CREATE INDEX IF NOT EXISTS idx_agent_context_events_context ON agent_context_events(context_id);",
|
|
317
328
|
"CREATE INDEX IF NOT EXISTS idx_agent_context_events_event ON agent_context_events(event_id);",
|
|
329
|
+
# Users table (V18: UID-to-user-info mapping)
|
|
330
|
+
"""
|
|
331
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
332
|
+
uid TEXT PRIMARY KEY,
|
|
333
|
+
user_name TEXT,
|
|
334
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
335
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
336
|
+
);
|
|
337
|
+
""",
|
|
318
338
|
*FTS_EVENTS_SCRIPTS,
|
|
319
339
|
]
|
|
320
340
|
|
|
@@ -570,6 +590,67 @@ MIGRATION_V15_TO_V16 = [
|
|
|
570
590
|
"CREATE INDEX IF NOT EXISTS idx_agent_context_events_event ON agent_context_events(event_id);",
|
|
571
591
|
]
|
|
572
592
|
|
|
593
|
+
# V16 to V17: Rename creator_id/creator_name to uid/user_name
|
|
594
|
+
MIGRATION_V16_TO_V17 = [
|
|
595
|
+
# Sessions table: rename columns
|
|
596
|
+
"ALTER TABLE sessions RENAME COLUMN creator_id TO uid;",
|
|
597
|
+
"ALTER TABLE sessions RENAME COLUMN creator_name TO user_name;",
|
|
598
|
+
# Turns table: rename columns
|
|
599
|
+
"ALTER TABLE turns RENAME COLUMN creator_id TO uid;",
|
|
600
|
+
"ALTER TABLE turns RENAME COLUMN creator_name TO user_name;",
|
|
601
|
+
# Events table: rename columns
|
|
602
|
+
"ALTER TABLE events RENAME COLUMN creator_id TO uid;",
|
|
603
|
+
"ALTER TABLE events RENAME COLUMN creator_name TO user_name;",
|
|
604
|
+
# Agents table: rename columns
|
|
605
|
+
"ALTER TABLE agents RENAME COLUMN creator_id TO uid;",
|
|
606
|
+
"ALTER TABLE agents RENAME COLUMN creator_name TO user_name;",
|
|
607
|
+
# Update indexes: drop old, create new
|
|
608
|
+
"DROP INDEX IF EXISTS idx_sessions_creator;",
|
|
609
|
+
"DROP INDEX IF EXISTS idx_turns_creator;",
|
|
610
|
+
"DROP INDEX IF EXISTS idx_events_creator;",
|
|
611
|
+
"CREATE INDEX IF NOT EXISTS idx_sessions_uid ON sessions(uid);",
|
|
612
|
+
"CREATE INDEX IF NOT EXISTS idx_turns_uid ON turns(uid);",
|
|
613
|
+
"CREATE INDEX IF NOT EXISTS idx_events_uid ON events(uid);",
|
|
614
|
+
]
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
# V17 to V18: uid/user_name → created_by/shared_by, users table, remove turns uid
|
|
618
|
+
MIGRATION_V17_TO_V18 = [
|
|
619
|
+
# 1. Create users table
|
|
620
|
+
"""
|
|
621
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
622
|
+
uid TEXT PRIMARY KEY,
|
|
623
|
+
user_name TEXT,
|
|
624
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
625
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
626
|
+
);
|
|
627
|
+
""",
|
|
628
|
+
# 2. Extract user info from existing data into users table
|
|
629
|
+
"INSERT OR IGNORE INTO users (uid, user_name) SELECT DISTINCT uid, user_name FROM sessions WHERE uid IS NOT NULL AND uid != '';",
|
|
630
|
+
"INSERT OR IGNORE INTO users (uid, user_name) SELECT DISTINCT uid, user_name FROM events WHERE uid IS NOT NULL AND uid != '' AND uid NOT IN (SELECT uid FROM users);",
|
|
631
|
+
"INSERT OR IGNORE INTO users (uid, user_name) SELECT DISTINCT uid, user_name FROM turns WHERE uid IS NOT NULL AND uid != '' AND uid NOT IN (SELECT uid FROM users);",
|
|
632
|
+
"INSERT OR IGNORE INTO users (uid, user_name) SELECT DISTINCT uid, user_name FROM agents WHERE uid IS NOT NULL AND uid != '' AND uid NOT IN (SELECT uid FROM users);",
|
|
633
|
+
# 3. Sessions: uid → created_by, drop user_name, add shared_by
|
|
634
|
+
"DROP INDEX IF EXISTS idx_sessions_uid;",
|
|
635
|
+
"ALTER TABLE sessions RENAME COLUMN uid TO created_by;",
|
|
636
|
+
"ALTER TABLE sessions DROP COLUMN user_name;",
|
|
637
|
+
"ALTER TABLE sessions ADD COLUMN shared_by TEXT;",
|
|
638
|
+
"CREATE INDEX IF NOT EXISTS idx_sessions_created_by ON sessions(created_by);",
|
|
639
|
+
# 4. Events: uid → created_by, drop user_name, add shared_by
|
|
640
|
+
"DROP INDEX IF EXISTS idx_events_uid;",
|
|
641
|
+
"ALTER TABLE events RENAME COLUMN uid TO created_by;",
|
|
642
|
+
"ALTER TABLE events DROP COLUMN user_name;",
|
|
643
|
+
"ALTER TABLE events ADD COLUMN shared_by TEXT;",
|
|
644
|
+
"CREATE INDEX IF NOT EXISTS idx_events_created_by ON events(created_by);",
|
|
645
|
+
# 5. Turns: drop uid and user_name
|
|
646
|
+
"DROP INDEX IF EXISTS idx_turns_uid;",
|
|
647
|
+
"ALTER TABLE turns DROP COLUMN uid;",
|
|
648
|
+
"ALTER TABLE turns DROP COLUMN user_name;",
|
|
649
|
+
# 6. Agents: uid → created_by, drop user_name (no shared_by needed)
|
|
650
|
+
"ALTER TABLE agents RENAME COLUMN uid TO created_by;",
|
|
651
|
+
"ALTER TABLE agents DROP COLUMN user_name;",
|
|
652
|
+
]
|
|
653
|
+
|
|
573
654
|
|
|
574
655
|
def get_migration_scripts(from_version: int, to_version: int) -> list:
|
|
575
656
|
"""Get migration scripts for upgrading between versions."""
|
|
@@ -629,4 +710,10 @@ def get_migration_scripts(from_version: int, to_version: int) -> list:
|
|
|
629
710
|
if from_version == 15:
|
|
630
711
|
scripts.extend(MIGRATION_V15_TO_V16)
|
|
631
712
|
|
|
713
|
+
if from_version < 17 and to_version >= 17:
|
|
714
|
+
scripts.extend(MIGRATION_V16_TO_V17)
|
|
715
|
+
|
|
716
|
+
if from_version < 18 and to_version >= 18:
|
|
717
|
+
scripts.extend(MIGRATION_V17_TO_V18)
|
|
718
|
+
|
|
632
719
|
return scripts
|