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.
Files changed (42) hide show
  1. {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/METADATA +1 -1
  2. {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/RECORD +41 -34
  3. realign/__init__.py +1 -1
  4. realign/agent_names.py +79 -0
  5. realign/claude_hooks/stop_hook.py +3 -0
  6. realign/claude_hooks/terminal_state.py +43 -1
  7. realign/claude_hooks/user_prompt_submit_hook.py +3 -0
  8. realign/cli.py +62 -0
  9. realign/codex_detector.py +18 -3
  10. realign/codex_home.py +65 -16
  11. realign/codex_terminal_linker.py +18 -7
  12. realign/commands/agent.py +109 -0
  13. realign/commands/doctor.py +74 -1
  14. realign/commands/export_shares.py +448 -0
  15. realign/commands/import_shares.py +203 -1
  16. realign/commands/search.py +58 -29
  17. realign/commands/sync_agent.py +347 -0
  18. realign/dashboard/app.py +9 -9
  19. realign/dashboard/clipboard.py +54 -0
  20. realign/dashboard/screens/__init__.py +4 -0
  21. realign/dashboard/screens/agent_detail.py +333 -0
  22. realign/dashboard/screens/create_agent_info.py +244 -0
  23. realign/dashboard/screens/event_detail.py +6 -27
  24. realign/dashboard/styles/dashboard.tcss +22 -28
  25. realign/dashboard/tmux_manager.py +36 -10
  26. realign/dashboard/widgets/__init__.py +2 -2
  27. realign/dashboard/widgets/agents_panel.py +1248 -0
  28. realign/dashboard/widgets/events_table.py +4 -27
  29. realign/dashboard/widgets/sessions_table.py +4 -27
  30. realign/db/base.py +69 -0
  31. realign/db/locks.py +4 -0
  32. realign/db/schema.py +111 -2
  33. realign/db/sqlite_db.py +360 -2
  34. realign/events/agent_summarizer.py +157 -0
  35. realign/events/session_summarizer.py +25 -0
  36. realign/watcher_core.py +193 -5
  37. realign/worker_core.py +59 -1
  38. realign/dashboard/widgets/terminal_panel.py +0 -1653
  39. {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/WHEEL +0 -0
  40. {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/entry_points.txt +0 -0
  41. {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/licenses/LICENSE +0 -0
  42. {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/top_level.txt +0 -0
@@ -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 or "").strip()
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) or "").strip() != cwd:
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 best_delta is not None and best_delta > max_time_delta_seconds:
169
- # Ambiguous: don't bind if terminals are too far from the session start.
170
- return None
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()
@@ -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
-