aline-ai 0.5.8__py3-none-any.whl → 0.5.10__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.8.dist-info → aline_ai-0.5.10.dist-info}/METADATA +1 -1
- {aline_ai-0.5.8.dist-info → aline_ai-0.5.10.dist-info}/RECORD +19 -18
- realign/__init__.py +1 -1
- realign/claude_hooks/terminal_state.py +86 -3
- realign/context.py +136 -6
- realign/dashboard/screens/create_agent.py +69 -4
- realign/dashboard/styles/dashboard.tcss +4 -4
- realign/dashboard/tmux_manager.py +73 -3
- realign/dashboard/widgets/events_table.py +45 -29
- realign/dashboard/widgets/terminal_panel.py +18 -11
- realign/db/__init__.py +44 -0
- realign/db/base.py +36 -0
- realign/db/migrate_agents.py +297 -0
- realign/db/schema.py +127 -1
- realign/db/sqlite_db.py +617 -0
- {aline_ai-0.5.8.dist-info → aline_ai-0.5.10.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.8.dist-info → aline_ai-0.5.10.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.8.dist-info → aline_ai-0.5.10.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.8.dist-info → aline_ai-0.5.10.dist-info}/top_level.txt +0 -0
|
@@ -6,6 +6,7 @@ import json
|
|
|
6
6
|
import os
|
|
7
7
|
import shutil
|
|
8
8
|
import subprocess
|
|
9
|
+
import traceback
|
|
9
10
|
from datetime import datetime
|
|
10
11
|
from typing import Optional, Set
|
|
11
12
|
from urllib.parse import urlparse
|
|
@@ -18,8 +19,11 @@ from textual.reactive import reactive
|
|
|
18
19
|
from textual.worker import Worker, WorkerState
|
|
19
20
|
from textual.widgets import Button, DataTable, Static
|
|
20
21
|
|
|
22
|
+
from ...logging_config import setup_logger
|
|
21
23
|
from .openable_table import OpenableDataTable
|
|
22
24
|
|
|
25
|
+
logger = setup_logger("realign.dashboard.events", "dashboard.log")
|
|
26
|
+
|
|
23
27
|
|
|
24
28
|
class EventsListTable(OpenableDataTable):
|
|
25
29
|
"""Events list table with multi-select behavior."""
|
|
@@ -123,38 +127,50 @@ class EventsTable(Container):
|
|
|
123
127
|
|
|
124
128
|
def compose(self) -> ComposeResult:
|
|
125
129
|
"""Compose the events table layout."""
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
yield
|
|
147
|
-
|
|
148
|
-
|
|
130
|
+
logger.debug("EventsTable.compose() started")
|
|
131
|
+
try:
|
|
132
|
+
with Vertical(classes="action-section"):
|
|
133
|
+
yield Button(
|
|
134
|
+
"Load selected context to current agent",
|
|
135
|
+
id="load-context-btn",
|
|
136
|
+
variant="primary",
|
|
137
|
+
disabled=True,
|
|
138
|
+
)
|
|
139
|
+
yield Button(
|
|
140
|
+
"Share selected events to others",
|
|
141
|
+
id="share-event-btn",
|
|
142
|
+
variant="primary",
|
|
143
|
+
disabled=True,
|
|
144
|
+
)
|
|
145
|
+
yield Button(
|
|
146
|
+
"Import context from others",
|
|
147
|
+
id="share-import-btn",
|
|
148
|
+
variant="primary",
|
|
149
|
+
)
|
|
150
|
+
yield Static(id="section-header", classes="section-header")
|
|
151
|
+
with Container(classes="table-container"):
|
|
152
|
+
yield EventsListTable(id="events-table")
|
|
153
|
+
yield Static(id="pagination-info", classes="pagination-info")
|
|
154
|
+
yield Static(id="stats-info", classes="stats-info")
|
|
155
|
+
logger.debug("EventsTable.compose() completed")
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.error(f"EventsTable.compose() failed: {e}\n{traceback.format_exc()}")
|
|
158
|
+
raise
|
|
149
159
|
|
|
150
160
|
def on_mount(self) -> None:
|
|
151
161
|
"""Set up the table on mount."""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
logger.debug("EventsTable.on_mount() started")
|
|
163
|
+
try:
|
|
164
|
+
table = self.query_one("#events-table", EventsListTable)
|
|
165
|
+
table.owner = self
|
|
166
|
+
self._setup_table_columns(table)
|
|
167
|
+
|
|
168
|
+
# Calculate initial rows per page
|
|
169
|
+
self._calculate_rows_per_page()
|
|
170
|
+
logger.debug("EventsTable.on_mount() completed")
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"EventsTable.on_mount() failed: {e}\n{traceback.format_exc()}")
|
|
173
|
+
raise
|
|
158
174
|
|
|
159
175
|
def on_resize(self) -> None:
|
|
160
176
|
"""Handle window resize to adjust rows per page."""
|
|
@@ -203,8 +203,8 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
TerminalPanel .terminal-row Button.terminal-close {
|
|
206
|
-
width:
|
|
207
|
-
min-width:
|
|
206
|
+
width: 3;
|
|
207
|
+
min-width: 3;
|
|
208
208
|
height: 2;
|
|
209
209
|
margin-left: 1;
|
|
210
210
|
padding: 0;
|
|
@@ -221,8 +221,8 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
TerminalPanel .terminal-row Button.terminal-toggle {
|
|
224
|
-
width:
|
|
225
|
-
min-width:
|
|
224
|
+
width: 3;
|
|
225
|
+
min-width: 3;
|
|
226
226
|
height: 2;
|
|
227
227
|
margin-left: 1;
|
|
228
228
|
padding: 0;
|
|
@@ -623,22 +623,24 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
623
623
|
"""Wrap a command to run in a specific directory."""
|
|
624
624
|
return f"cd {shlex.quote(directory)} && {command}"
|
|
625
625
|
|
|
626
|
-
def _on_create_agent_result(self, result: tuple[str, str] | None) -> None:
|
|
626
|
+
def _on_create_agent_result(self, result: tuple[str, str, bool] | None) -> None:
|
|
627
627
|
"""Handle the result from CreateAgentScreen modal."""
|
|
628
628
|
if result is None:
|
|
629
629
|
return
|
|
630
630
|
|
|
631
|
-
agent_type, workspace = result
|
|
631
|
+
agent_type, workspace, skip_permissions = result
|
|
632
632
|
self.run_worker(
|
|
633
|
-
self._create_agent(agent_type, workspace),
|
|
633
|
+
self._create_agent(agent_type, workspace, skip_permissions=skip_permissions),
|
|
634
634
|
group="terminal-panel-create",
|
|
635
635
|
exclusive=True,
|
|
636
636
|
)
|
|
637
637
|
|
|
638
|
-
async def _create_agent(
|
|
638
|
+
async def _create_agent(
|
|
639
|
+
self, agent_type: str, workspace: str, *, skip_permissions: bool = False
|
|
640
|
+
) -> None:
|
|
639
641
|
"""Create a new agent terminal based on the selected type and workspace."""
|
|
640
642
|
if agent_type == "claude":
|
|
641
|
-
await self._create_claude_terminal(workspace)
|
|
643
|
+
await self._create_claude_terminal(workspace, skip_permissions=skip_permissions)
|
|
642
644
|
elif agent_type == "codex":
|
|
643
645
|
await self._create_codex_terminal(workspace)
|
|
644
646
|
elif agent_type == "opencode":
|
|
@@ -647,7 +649,9 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
647
649
|
await self._create_zsh_terminal(workspace)
|
|
648
650
|
await self.refresh_data()
|
|
649
651
|
|
|
650
|
-
async def _create_claude_terminal(
|
|
652
|
+
async def _create_claude_terminal(
|
|
653
|
+
self, workspace: str, *, skip_permissions: bool = False
|
|
654
|
+
) -> None:
|
|
651
655
|
"""Create a new Claude terminal."""
|
|
652
656
|
terminal_id = tmux_manager.new_terminal_id()
|
|
653
657
|
context_id = tmux_manager.new_context_id("cc")
|
|
@@ -708,8 +712,11 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
708
712
|
except Exception:
|
|
709
713
|
pass
|
|
710
714
|
|
|
715
|
+
claude_cmd = "claude"
|
|
716
|
+
if skip_permissions:
|
|
717
|
+
claude_cmd = "claude --dangerously-skip-permissions"
|
|
711
718
|
command = self._command_in_directory(
|
|
712
|
-
tmux_manager.zsh_run_and_keep_open(
|
|
719
|
+
tmux_manager.zsh_run_and_keep_open(claude_cmd), workspace
|
|
713
720
|
)
|
|
714
721
|
created = tmux_manager.create_inner_window(
|
|
715
722
|
"cc",
|
realign/db/__init__.py
CHANGED
|
@@ -8,6 +8,47 @@ from .base import DatabaseInterface
|
|
|
8
8
|
from .sqlite_db import SQLiteDatabase
|
|
9
9
|
|
|
10
10
|
_DB_INSTANCE = None
|
|
11
|
+
_MIGRATION_DONE = False
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _auto_migrate_agents_data(db: SQLiteDatabase) -> None:
|
|
15
|
+
"""Auto-migrate terminal.json and load.json data to SQLite (runs once).
|
|
16
|
+
|
|
17
|
+
This is triggered automatically when the schema is upgraded to V15.
|
|
18
|
+
Uses a marker file to avoid running multiple times.
|
|
19
|
+
"""
|
|
20
|
+
global _MIGRATION_DONE
|
|
21
|
+
if _MIGRATION_DONE:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
# Check marker file
|
|
25
|
+
marker_path = Path.home() / ".aline" / ".agents_migrated_v15"
|
|
26
|
+
if marker_path.exists():
|
|
27
|
+
_MIGRATION_DONE = True
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
# Skip during tests
|
|
31
|
+
if os.getenv("PYTEST_CURRENT_TEST"):
|
|
32
|
+
_MIGRATION_DONE = True
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
from .migrate_agents import migrate_terminal_json, migrate_load_json
|
|
37
|
+
|
|
38
|
+
# Run migrations silently (no dry_run)
|
|
39
|
+
agents_count = migrate_terminal_json(db, dry_run=False, silent=True)
|
|
40
|
+
contexts_count = migrate_load_json(db, dry_run=False, silent=True)
|
|
41
|
+
|
|
42
|
+
# Create marker file
|
|
43
|
+
marker_path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
marker_path.write_text(
|
|
45
|
+
f"Migrated: {agents_count} agents, {contexts_count} contexts\n"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
_MIGRATION_DONE = True
|
|
49
|
+
except Exception:
|
|
50
|
+
# Don't fail if migration fails - JSON fallback will still work
|
|
51
|
+
_MIGRATION_DONE = True
|
|
11
52
|
|
|
12
53
|
|
|
13
54
|
def get_database(
|
|
@@ -54,4 +95,7 @@ def get_database(
|
|
|
54
95
|
_DB_INSTANCE = SQLiteDatabase(db_path, connect_timeout_seconds=timeout)
|
|
55
96
|
_DB_INSTANCE.initialize()
|
|
56
97
|
|
|
98
|
+
# Auto-migrate JSON data to SQLite (runs once after V15 upgrade)
|
|
99
|
+
_auto_migrate_agents_data(_DB_INSTANCE)
|
|
100
|
+
|
|
57
101
|
return _DB_INSTANCE
|
realign/db/base.py
CHANGED
|
@@ -107,6 +107,42 @@ class EventRecord:
|
|
|
107
107
|
creator_id: Optional[str] = None
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
@dataclass
|
|
111
|
+
class AgentRecord:
|
|
112
|
+
"""Represents a terminal/agent mapping (V15: replaces terminal.json)."""
|
|
113
|
+
|
|
114
|
+
id: str # terminal_id (UUID)
|
|
115
|
+
provider: str # 'claude', 'codex', 'opencode', 'zsh'
|
|
116
|
+
session_type: str
|
|
117
|
+
created_at: datetime
|
|
118
|
+
updated_at: datetime
|
|
119
|
+
session_id: Optional[str] = None # FK to sessions.id
|
|
120
|
+
context_id: Optional[str] = None
|
|
121
|
+
transcript_path: Optional[str] = None
|
|
122
|
+
cwd: Optional[str] = None
|
|
123
|
+
project_dir: Optional[str] = None
|
|
124
|
+
status: str = "active" # 'active', 'stopped'
|
|
125
|
+
attention: Optional[str] = None # 'permission_request', 'stop', or None
|
|
126
|
+
source: Optional[str] = None
|
|
127
|
+
creator_name: Optional[str] = None
|
|
128
|
+
creator_id: Optional[str] = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class AgentContextRecord:
|
|
133
|
+
"""Represents a context entry (V15: replaces load.json)."""
|
|
134
|
+
|
|
135
|
+
id: str # context_id
|
|
136
|
+
created_at: datetime
|
|
137
|
+
updated_at: datetime
|
|
138
|
+
workspace: Optional[str] = None
|
|
139
|
+
loaded_at: Optional[str] = None
|
|
140
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
141
|
+
# Populated when reading (from M2M tables)
|
|
142
|
+
session_ids: Optional[List[str]] = None
|
|
143
|
+
event_ids: Optional[List[str]] = None
|
|
144
|
+
|
|
145
|
+
|
|
110
146
|
class DatabaseInterface(ABC):
|
|
111
147
|
"""Abstract interface for ReAlign storage backend."""
|
|
112
148
|
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Migration script to import terminal.json and load.json data into SQLite.
|
|
2
|
+
|
|
3
|
+
This script migrates:
|
|
4
|
+
- ~/.aline/terminal.json -> agents table
|
|
5
|
+
- ~/.aline/load.json -> agent_contexts, agent_context_sessions, agent_context_events tables
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m realign.db.migrate_agents [--backup] [--dry-run]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def migrate_terminal_json(db, dry_run: bool = False, silent: bool = False) -> int:
|
|
22
|
+
"""Migrate terminal.json to agents table.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
db: Database instance
|
|
26
|
+
dry_run: If True, don't actually migrate
|
|
27
|
+
silent: If True, don't print any output
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Number of agents migrated
|
|
31
|
+
"""
|
|
32
|
+
path = Path.home() / ".aline" / "terminal.json"
|
|
33
|
+
if not path.exists():
|
|
34
|
+
if not silent:
|
|
35
|
+
print(f"[migrate] terminal.json not found at {path}, skipping")
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
40
|
+
except Exception as e:
|
|
41
|
+
if not silent:
|
|
42
|
+
print(f"[migrate] Failed to read terminal.json: {e}")
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
terminals = payload.get("terminals", {})
|
|
46
|
+
if not isinstance(terminals, dict):
|
|
47
|
+
if not silent:
|
|
48
|
+
print("[migrate] terminal.json has no 'terminals' dict, skipping")
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
migrated = 0
|
|
52
|
+
for terminal_id, data in terminals.items():
|
|
53
|
+
if not isinstance(terminal_id, str) or not isinstance(data, dict):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
provider = data.get("provider", "unknown")
|
|
57
|
+
session_type = data.get("session_type", provider)
|
|
58
|
+
session_id = data.get("session_id") or None
|
|
59
|
+
transcript_path = data.get("transcript_path") or None
|
|
60
|
+
cwd = data.get("cwd") or None
|
|
61
|
+
project_dir = data.get("project_dir") or None
|
|
62
|
+
source = data.get("source") or None
|
|
63
|
+
context_id = data.get("context_id") or None
|
|
64
|
+
attention = data.get("attention") or None
|
|
65
|
+
|
|
66
|
+
if dry_run:
|
|
67
|
+
if not silent:
|
|
68
|
+
print(f"[dry-run] Would migrate agent: {terminal_id[:8]}... ({provider})")
|
|
69
|
+
migrated += 1
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
existing = db.get_agent_by_id(terminal_id)
|
|
74
|
+
if existing:
|
|
75
|
+
if not silent:
|
|
76
|
+
print(f"[migrate] Agent {terminal_id[:8]}... already exists, skipping")
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
db.get_or_create_agent(
|
|
80
|
+
terminal_id,
|
|
81
|
+
provider=provider,
|
|
82
|
+
session_type=session_type,
|
|
83
|
+
session_id=session_id,
|
|
84
|
+
context_id=context_id,
|
|
85
|
+
transcript_path=transcript_path,
|
|
86
|
+
cwd=cwd,
|
|
87
|
+
project_dir=project_dir,
|
|
88
|
+
source=source,
|
|
89
|
+
attention=attention,
|
|
90
|
+
)
|
|
91
|
+
if not silent:
|
|
92
|
+
print(f"[migrate] Migrated agent: {terminal_id[:8]}... ({provider})")
|
|
93
|
+
migrated += 1
|
|
94
|
+
except Exception as e:
|
|
95
|
+
if not silent:
|
|
96
|
+
print(f"[migrate] Failed to migrate agent {terminal_id[:8]}...: {e}")
|
|
97
|
+
|
|
98
|
+
return migrated
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def migrate_load_json(db, dry_run: bool = False, silent: bool = False) -> int:
|
|
102
|
+
"""Migrate load.json to agent_contexts tables.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
db: Database instance
|
|
106
|
+
dry_run: If True, don't actually migrate
|
|
107
|
+
silent: If True, don't print any output
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Number of contexts migrated
|
|
111
|
+
"""
|
|
112
|
+
path = Path.home() / ".aline" / "load.json"
|
|
113
|
+
if not path.exists():
|
|
114
|
+
if not silent:
|
|
115
|
+
print(f"[migrate] load.json not found at {path}, skipping")
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
120
|
+
except Exception as e:
|
|
121
|
+
if not silent:
|
|
122
|
+
print(f"[migrate] Failed to read load.json: {e}")
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
contexts = payload.get("contexts", [])
|
|
126
|
+
if not isinstance(contexts, list):
|
|
127
|
+
if not silent:
|
|
128
|
+
print("[migrate] load.json has no 'contexts' list, skipping")
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
migrated = 0
|
|
132
|
+
for ctx_data in contexts:
|
|
133
|
+
if not isinstance(ctx_data, dict):
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
context_id = ctx_data.get("context_id")
|
|
137
|
+
if not context_id:
|
|
138
|
+
# Generate context_id from workspace if not present
|
|
139
|
+
workspace = ctx_data.get("workspace")
|
|
140
|
+
if workspace:
|
|
141
|
+
# Create a deterministic ID from workspace
|
|
142
|
+
import hashlib
|
|
143
|
+
|
|
144
|
+
context_id = f"ws-{hashlib.sha256(workspace.encode()).hexdigest()[:12]}"
|
|
145
|
+
else:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
workspace = ctx_data.get("workspace")
|
|
149
|
+
loaded_at = ctx_data.get("loaded_at")
|
|
150
|
+
context_sessions = ctx_data.get("context_sessions", [])
|
|
151
|
+
context_events = ctx_data.get("context_events", [])
|
|
152
|
+
|
|
153
|
+
if dry_run:
|
|
154
|
+
if not silent:
|
|
155
|
+
print(
|
|
156
|
+
f"[dry-run] Would migrate context: {context_id} "
|
|
157
|
+
f"(sessions={len(context_sessions)}, events={len(context_events)})"
|
|
158
|
+
)
|
|
159
|
+
migrated += 1
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
existing = db.get_agent_context_by_id(context_id)
|
|
164
|
+
if existing:
|
|
165
|
+
if not silent:
|
|
166
|
+
print(f"[migrate] Context {context_id} already exists, updating links")
|
|
167
|
+
else:
|
|
168
|
+
db.get_or_create_agent_context(
|
|
169
|
+
context_id,
|
|
170
|
+
workspace=workspace,
|
|
171
|
+
loaded_at=loaded_at,
|
|
172
|
+
)
|
|
173
|
+
if not silent:
|
|
174
|
+
print(f"[migrate] Created context: {context_id}")
|
|
175
|
+
|
|
176
|
+
# Update session links (silently skips if session not in DB)
|
|
177
|
+
if context_sessions:
|
|
178
|
+
for session_id in context_sessions:
|
|
179
|
+
db.link_session_to_agent_context(context_id, session_id)
|
|
180
|
+
if not silent:
|
|
181
|
+
print(f"[migrate] Linked {len(context_sessions)} sessions to {context_id}")
|
|
182
|
+
|
|
183
|
+
# Update event links (silently skips if event not in DB)
|
|
184
|
+
if context_events:
|
|
185
|
+
for event_id in context_events:
|
|
186
|
+
db.link_event_to_agent_context(context_id, event_id)
|
|
187
|
+
if not silent:
|
|
188
|
+
print(f"[migrate] Linked {len(context_events)} events to {context_id}")
|
|
189
|
+
|
|
190
|
+
migrated += 1
|
|
191
|
+
except Exception as e:
|
|
192
|
+
if not silent:
|
|
193
|
+
print(f"[migrate] Failed to migrate context {context_id}: {e}")
|
|
194
|
+
|
|
195
|
+
return migrated
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def backup_json_files() -> bool:
|
|
199
|
+
"""Create backup of JSON files.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
True if backup was successful or files don't exist
|
|
203
|
+
"""
|
|
204
|
+
files = [
|
|
205
|
+
Path.home() / ".aline" / "terminal.json",
|
|
206
|
+
Path.home() / ".aline" / "load.json",
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
210
|
+
|
|
211
|
+
for path in files:
|
|
212
|
+
if not path.exists():
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
backup_path = path.with_suffix(f".json.bak.{timestamp}")
|
|
216
|
+
try:
|
|
217
|
+
backup_path.write_text(path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
218
|
+
print(f"[backup] Created backup: {backup_path}")
|
|
219
|
+
except Exception as e:
|
|
220
|
+
print(f"[backup] Failed to backup {path}: {e}")
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
return True
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main():
|
|
227
|
+
parser = argparse.ArgumentParser(
|
|
228
|
+
description="Migrate terminal.json and load.json to SQLite database"
|
|
229
|
+
)
|
|
230
|
+
parser.add_argument(
|
|
231
|
+
"--backup",
|
|
232
|
+
action="store_true",
|
|
233
|
+
help="Create backup of JSON files before migration",
|
|
234
|
+
)
|
|
235
|
+
parser.add_argument(
|
|
236
|
+
"--dry-run",
|
|
237
|
+
action="store_true",
|
|
238
|
+
help="Show what would be migrated without actually doing it",
|
|
239
|
+
)
|
|
240
|
+
parser.add_argument(
|
|
241
|
+
"--db-path",
|
|
242
|
+
type=str,
|
|
243
|
+
default=str(Path.home() / ".aline" / "realign.db"),
|
|
244
|
+
help="Path to SQLite database",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
args = parser.parse_args()
|
|
248
|
+
|
|
249
|
+
print("=" * 60)
|
|
250
|
+
print("Aline Agents Migration: JSON -> SQLite")
|
|
251
|
+
print("=" * 60)
|
|
252
|
+
|
|
253
|
+
if args.dry_run:
|
|
254
|
+
print("[mode] DRY RUN - no changes will be made")
|
|
255
|
+
|
|
256
|
+
# Backup if requested
|
|
257
|
+
if args.backup and not args.dry_run:
|
|
258
|
+
print("\n[step] Creating backups...")
|
|
259
|
+
if not backup_json_files():
|
|
260
|
+
print("[error] Backup failed, aborting migration")
|
|
261
|
+
sys.exit(1)
|
|
262
|
+
|
|
263
|
+
# Initialize database
|
|
264
|
+
if not args.dry_run:
|
|
265
|
+
from .sqlite_db import SQLiteDatabase
|
|
266
|
+
|
|
267
|
+
print(f"\n[step] Initializing database at {args.db_path}")
|
|
268
|
+
db = SQLiteDatabase(args.db_path)
|
|
269
|
+
if not db.initialize():
|
|
270
|
+
print("[error] Database initialization failed")
|
|
271
|
+
sys.exit(1)
|
|
272
|
+
else:
|
|
273
|
+
db = None
|
|
274
|
+
|
|
275
|
+
# Migrate terminal.json
|
|
276
|
+
print("\n[step] Migrating terminal.json -> agents table")
|
|
277
|
+
agents_migrated = migrate_terminal_json(db, dry_run=args.dry_run)
|
|
278
|
+
print(f"[result] Migrated {agents_migrated} agents")
|
|
279
|
+
|
|
280
|
+
# Migrate load.json
|
|
281
|
+
print("\n[step] Migrating load.json -> agent_contexts tables")
|
|
282
|
+
contexts_migrated = migrate_load_json(db, dry_run=args.dry_run)
|
|
283
|
+
print(f"[result] Migrated {contexts_migrated} contexts")
|
|
284
|
+
|
|
285
|
+
# Cleanup
|
|
286
|
+
if db:
|
|
287
|
+
db.close()
|
|
288
|
+
|
|
289
|
+
print("\n" + "=" * 60)
|
|
290
|
+
print("Migration complete!")
|
|
291
|
+
print(f" Agents migrated: {agents_migrated}")
|
|
292
|
+
print(f" Contexts migrated: {contexts_migrated}")
|
|
293
|
+
print("=" * 60)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
if __name__ == "__main__":
|
|
297
|
+
main()
|