aline-ai 0.6.5__py3-none-any.whl → 0.6.7__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.6.5.dist-info → aline_ai-0.6.7.dist-info}/METADATA +1 -1
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/RECORD +41 -34
- realign/__init__.py +1 -1
- realign/agent_names.py +79 -0
- realign/claude_hooks/stop_hook.py +3 -0
- realign/claude_hooks/terminal_state.py +43 -1
- realign/claude_hooks/user_prompt_submit_hook.py +3 -0
- realign/cli.py +62 -0
- realign/codex_detector.py +18 -3
- realign/codex_home.py +65 -16
- realign/codex_terminal_linker.py +18 -7
- realign/commands/agent.py +109 -0
- realign/commands/doctor.py +74 -1
- realign/commands/export_shares.py +448 -0
- realign/commands/import_shares.py +203 -1
- realign/commands/search.py +58 -29
- realign/commands/sync_agent.py +347 -0
- realign/dashboard/app.py +9 -9
- realign/dashboard/clipboard.py +54 -0
- realign/dashboard/screens/__init__.py +4 -0
- realign/dashboard/screens/agent_detail.py +333 -0
- realign/dashboard/screens/create_agent_info.py +244 -0
- realign/dashboard/screens/event_detail.py +6 -27
- realign/dashboard/styles/dashboard.tcss +22 -28
- realign/dashboard/tmux_manager.py +36 -10
- realign/dashboard/widgets/__init__.py +2 -2
- realign/dashboard/widgets/agents_panel.py +1248 -0
- realign/dashboard/widgets/events_table.py +4 -27
- realign/dashboard/widgets/sessions_table.py +4 -27
- realign/db/base.py +69 -0
- realign/db/locks.py +4 -0
- realign/db/schema.py +111 -2
- realign/db/sqlite_db.py +360 -2
- realign/events/agent_summarizer.py +157 -0
- realign/events/session_summarizer.py +25 -0
- realign/watcher_core.py +193 -5
- realign/worker_core.py +59 -1
- realign/dashboard/widgets/terminal_panel.py +0 -1653
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/top_level.txt +0 -0
realign/codex_terminal_linker.py
CHANGED
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
import json
|
|
14
14
|
from dataclasses import dataclass
|
|
15
15
|
from datetime import datetime, timezone
|
|
16
|
+
import os
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Iterable, Optional, Protocol
|
|
18
19
|
|
|
@@ -42,6 +43,16 @@ def _parse_iso8601(ts: str) -> Optional[datetime]:
|
|
|
42
43
|
return dt
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
def _normalize_cwd(cwd: str | None) -> str:
|
|
47
|
+
raw = (cwd or "").strip()
|
|
48
|
+
if not raw:
|
|
49
|
+
return ""
|
|
50
|
+
try:
|
|
51
|
+
return os.path.normpath(raw)
|
|
52
|
+
except Exception:
|
|
53
|
+
return raw.rstrip("/\\")
|
|
54
|
+
|
|
55
|
+
|
|
45
56
|
def read_codex_session_meta(session_file: Path) -> Optional[CodexSessionMeta]:
|
|
46
57
|
"""Extract Codex session metadata from a session file (best-effort)."""
|
|
47
58
|
try:
|
|
@@ -108,10 +119,10 @@ def select_agent_for_codex_session(
|
|
|
108
119
|
agents: Iterable[_AgentLike],
|
|
109
120
|
*,
|
|
110
121
|
session: CodexSessionMeta,
|
|
111
|
-
max_time_delta_seconds: int = 6 * 60 * 60,
|
|
122
|
+
max_time_delta_seconds: Optional[int] = 6 * 60 * 60,
|
|
112
123
|
) -> Optional[str]:
|
|
113
124
|
"""Pick the best active Codex terminal for a Codex session file (best-effort)."""
|
|
114
|
-
cwd = (session.cwd
|
|
125
|
+
cwd = _normalize_cwd(session.cwd)
|
|
115
126
|
if not cwd:
|
|
116
127
|
return None
|
|
117
128
|
|
|
@@ -122,7 +133,7 @@ def select_agent_for_codex_session(
|
|
|
122
133
|
continue
|
|
123
134
|
if getattr(a, "provider", "") != "codex":
|
|
124
135
|
continue
|
|
125
|
-
if (getattr(a, "cwd", None)
|
|
136
|
+
if _normalize_cwd(getattr(a, "cwd", None)) != cwd:
|
|
126
137
|
continue
|
|
127
138
|
# Avoid clobbering an existing binding to a different session.
|
|
128
139
|
existing_sid = (getattr(a, "session_id", None) or "").strip()
|
|
@@ -165,8 +176,8 @@ def select_agent_for_codex_session(
|
|
|
165
176
|
|
|
166
177
|
if best_id is None:
|
|
167
178
|
return None
|
|
168
|
-
if
|
|
169
|
-
|
|
170
|
-
|
|
179
|
+
if max_time_delta_seconds is not None and best_delta is not None:
|
|
180
|
+
if best_delta > max_time_delta_seconds:
|
|
181
|
+
# Ambiguous: don't bind if terminals are too far from the session start.
|
|
182
|
+
return None
|
|
171
183
|
return best_id
|
|
172
|
-
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Agent management commands."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_db():
|
|
12
|
+
"""Get database instance."""
|
|
13
|
+
from ..config import ReAlignConfig
|
|
14
|
+
from ..db.sqlite_db import SQLiteDatabase
|
|
15
|
+
|
|
16
|
+
config = ReAlignConfig.load()
|
|
17
|
+
db_path = Path(config.sqlite_db_path).expanduser()
|
|
18
|
+
db = SQLiteDatabase(str(db_path))
|
|
19
|
+
db.initialize()
|
|
20
|
+
return db
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def agent_new_command(name: str | None = None, desc: str = "") -> int:
|
|
24
|
+
"""Create a new agent profile.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
name: Display name (random Docker-style name if None).
|
|
28
|
+
desc: Agent description.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Exit code (0 = success).
|
|
32
|
+
"""
|
|
33
|
+
from ..agent_names import generate_agent_name
|
|
34
|
+
|
|
35
|
+
agent_id = str(uuid.uuid4())
|
|
36
|
+
display_name = name or generate_agent_name()
|
|
37
|
+
|
|
38
|
+
db = _get_db()
|
|
39
|
+
try:
|
|
40
|
+
record = db.get_or_create_agent_info(agent_id, name=display_name)
|
|
41
|
+
if desc:
|
|
42
|
+
record = db.update_agent_info(agent_id, description=desc)
|
|
43
|
+
|
|
44
|
+
console.print(f"[bold green]Agent created[/bold green]")
|
|
45
|
+
console.print(f" id: {record.id}")
|
|
46
|
+
console.print(f" name: {record.name}")
|
|
47
|
+
console.print(f" description: {record.description or '(none)'}")
|
|
48
|
+
return 0
|
|
49
|
+
finally:
|
|
50
|
+
db.close()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def agent_list_command(*, include_invisible: bool = False) -> int:
|
|
54
|
+
"""List agent profiles.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Exit code (0 = success).
|
|
58
|
+
"""
|
|
59
|
+
from rich.table import Table
|
|
60
|
+
|
|
61
|
+
db = _get_db()
|
|
62
|
+
try:
|
|
63
|
+
agents = db.list_agent_info(include_invisible=include_invisible)
|
|
64
|
+
|
|
65
|
+
if not agents:
|
|
66
|
+
console.print("[dim]No agents yet.[/dim]")
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
table = Table(title="Agents")
|
|
70
|
+
table.add_column("ID", style="dim", width=8)
|
|
71
|
+
table.add_column("Name", style="bold")
|
|
72
|
+
table.add_column("Description")
|
|
73
|
+
table.add_column("Sessions", style="cyan")
|
|
74
|
+
table.add_column("Created", style="dim")
|
|
75
|
+
|
|
76
|
+
def _unique_prefixes(ids: list[str], min_len: int = 8) -> list[str]:
|
|
77
|
+
if not ids:
|
|
78
|
+
return []
|
|
79
|
+
max_len = max(len(i) for i in ids)
|
|
80
|
+
length = min_len
|
|
81
|
+
while length <= max_len:
|
|
82
|
+
prefixes = [i[:length] for i in ids]
|
|
83
|
+
if len(set(prefixes)) == len(ids):
|
|
84
|
+
return prefixes
|
|
85
|
+
length += 2
|
|
86
|
+
return ids
|
|
87
|
+
|
|
88
|
+
for agent in agents:
|
|
89
|
+
created_str = agent.created_at.strftime("%Y-%m-%d %H:%M")
|
|
90
|
+
sessions = db.get_sessions_by_agent_id(agent.id)
|
|
91
|
+
if sessions:
|
|
92
|
+
raw_ids = [s.id for s in sessions]
|
|
93
|
+
short_ids = _unique_prefixes(raw_ids, min_len=8)
|
|
94
|
+
session_ids = ", ".join(short_ids)
|
|
95
|
+
sessions_display = f"{len(sessions)} ({session_ids})"
|
|
96
|
+
else:
|
|
97
|
+
sessions_display = "0"
|
|
98
|
+
table.add_row(
|
|
99
|
+
agent.id[:8],
|
|
100
|
+
agent.name,
|
|
101
|
+
agent.description or "",
|
|
102
|
+
sessions_display,
|
|
103
|
+
created_str,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
console.print(table)
|
|
107
|
+
return 0
|
|
108
|
+
finally:
|
|
109
|
+
db.close()
|
realign/commands/doctor.py
CHANGED
|
@@ -435,6 +435,17 @@ def run_doctor(
|
|
|
435
435
|
except Exception as e:
|
|
436
436
|
console.print(f" [yellow]![/yellow] Failed to check jobs: {e}")
|
|
437
437
|
|
|
438
|
+
# 5b. Repair agent/session associations
|
|
439
|
+
console.print("\n[bold]5b. Repairing agent/session associations...[/bold]")
|
|
440
|
+
try:
|
|
441
|
+
repaired = _repair_agent_session_links(verbose=verbose)
|
|
442
|
+
if repaired > 0:
|
|
443
|
+
console.print(f" [green]✓[/green] Repaired {repaired} session(s)")
|
|
444
|
+
else:
|
|
445
|
+
console.print(" [green]✓[/green] No missing associations found")
|
|
446
|
+
except Exception as e:
|
|
447
|
+
console.print(f" [yellow]![/yellow] Failed to repair associations: {e}")
|
|
448
|
+
|
|
438
449
|
# 6. Restart/ensure daemons
|
|
439
450
|
if restart_daemons:
|
|
440
451
|
console.print("\n[bold]6. Checking daemons...[/bold]")
|
|
@@ -472,6 +483,69 @@ def run_doctor(
|
|
|
472
483
|
return 0
|
|
473
484
|
|
|
474
485
|
|
|
486
|
+
def _repair_agent_session_links(*, verbose: bool = False) -> int:
|
|
487
|
+
"""Backfill sessions.agent_id using windowlink and agents mappings."""
|
|
488
|
+
try:
|
|
489
|
+
from ..db import get_database
|
|
490
|
+
|
|
491
|
+
db = get_database(read_only=False)
|
|
492
|
+
except Exception:
|
|
493
|
+
return 0
|
|
494
|
+
|
|
495
|
+
repaired = 0
|
|
496
|
+
|
|
497
|
+
# 1) Use windowlink latest records
|
|
498
|
+
try:
|
|
499
|
+
links = db.list_latest_window_links(limit=5000)
|
|
500
|
+
except Exception:
|
|
501
|
+
links = []
|
|
502
|
+
|
|
503
|
+
for link in links:
|
|
504
|
+
session_id = (link.session_id or "").strip()
|
|
505
|
+
agent_id = (link.agent_id or "").strip()
|
|
506
|
+
if not session_id or not agent_id:
|
|
507
|
+
continue
|
|
508
|
+
try:
|
|
509
|
+
session = db.get_session_by_id(session_id)
|
|
510
|
+
if session and getattr(session, "agent_id", None):
|
|
511
|
+
continue
|
|
512
|
+
except Exception:
|
|
513
|
+
pass
|
|
514
|
+
try:
|
|
515
|
+
db.update_session_agent_id(session_id, agent_id)
|
|
516
|
+
repaired += 1
|
|
517
|
+
except Exception:
|
|
518
|
+
continue
|
|
519
|
+
|
|
520
|
+
# 2) Fallback: agents table mapping (session_id -> agent source)
|
|
521
|
+
try:
|
|
522
|
+
agents = db.list_agents(status=None, limit=5000)
|
|
523
|
+
except Exception:
|
|
524
|
+
agents = []
|
|
525
|
+
for agent in agents:
|
|
526
|
+
session_id = (agent.session_id or "").strip()
|
|
527
|
+
source = (agent.source or "").strip()
|
|
528
|
+
if not session_id or not source.startswith("agent:"):
|
|
529
|
+
continue
|
|
530
|
+
agent_id = source[6:]
|
|
531
|
+
try:
|
|
532
|
+
session = db.get_session_by_id(session_id)
|
|
533
|
+
if session and getattr(session, "agent_id", None):
|
|
534
|
+
continue
|
|
535
|
+
except Exception:
|
|
536
|
+
pass
|
|
537
|
+
try:
|
|
538
|
+
db.update_session_agent_id(session_id, agent_id)
|
|
539
|
+
repaired += 1
|
|
540
|
+
except Exception:
|
|
541
|
+
continue
|
|
542
|
+
|
|
543
|
+
if verbose and repaired:
|
|
544
|
+
console.print(f" [dim]Repaired session links: {repaired}[/dim]")
|
|
545
|
+
|
|
546
|
+
return repaired
|
|
547
|
+
|
|
548
|
+
|
|
475
549
|
def doctor_command(
|
|
476
550
|
no_restart: bool = typer.Option(False, "--no-restart", help="Only repair files, don't restart daemons"),
|
|
477
551
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
@@ -494,4 +568,3 @@ def doctor_command(
|
|
|
494
568
|
clear_cache=True,
|
|
495
569
|
)
|
|
496
570
|
raise typer.Exit(code=exit_code)
|
|
497
|
-
|