aline-ai 0.6.4__py3-none-any.whl → 0.6.6__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.4.dist-info → aline_ai-0.6.6.dist-info}/METADATA +1 -1
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.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 +11 -0
- realign/claude_hooks/user_prompt_submit_hook.py +3 -0
- realign/cli.py +62 -0
- realign/codex_detector.py +1 -1
- realign/codex_home.py +46 -15
- realign/codex_terminal_linker.py +18 -7
- realign/commands/agent.py +109 -0
- realign/commands/doctor.py +3 -1
- realign/commands/export_shares.py +297 -0
- realign/commands/search.py +58 -29
- realign/dashboard/app.py +9 -158
- 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 +133 -0
- realign/dashboard/screens/event_detail.py +6 -27
- realign/dashboard/styles/dashboard.tcss +67 -0
- realign/dashboard/tmux_manager.py +49 -8
- realign/dashboard/widgets/__init__.py +2 -0
- realign/dashboard/widgets/agents_panel.py +1129 -0
- realign/dashboard/widgets/config_panel.py +17 -11
- realign/dashboard/widgets/events_table.py +4 -27
- realign/dashboard/widgets/sessions_table.py +4 -27
- realign/dashboard/widgets/terminal_panel.py +109 -31
- realign/db/base.py +27 -0
- realign/db/locks.py +4 -0
- realign/db/schema.py +53 -2
- realign/db/sqlite_db.py +185 -2
- realign/events/agent_summarizer.py +157 -0
- realign/events/session_summarizer.py +25 -0
- realign/watcher_core.py +60 -3
- realign/worker_core.py +24 -1
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/top_level.txt +0 -0
|
@@ -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
|
@@ -308,6 +308,7 @@ def run_doctor(
|
|
|
308
308
|
start_if_not_running: bool,
|
|
309
309
|
verbose: bool,
|
|
310
310
|
clear_cache: bool,
|
|
311
|
+
auto_fix: bool = False,
|
|
311
312
|
) -> int:
|
|
312
313
|
"""
|
|
313
314
|
Core doctor logic.
|
|
@@ -317,6 +318,7 @@ def run_doctor(
|
|
|
317
318
|
start_if_not_running: If True and restart_daemons is True, start daemons even if not running.
|
|
318
319
|
verbose: Print details.
|
|
319
320
|
clear_cache: Clear Python bytecode cache for the installed package directory.
|
|
321
|
+
auto_fix: If True, automatically fix failed jobs without prompting.
|
|
320
322
|
"""
|
|
321
323
|
from ..auth import is_logged_in
|
|
322
324
|
from . import watcher as watcher_cmd
|
|
@@ -417,7 +419,7 @@ def run_doctor(
|
|
|
417
419
|
console.print(f" [yellow]![/yellow] Found {llm_error_count} turn(s) with LLM API errors")
|
|
418
420
|
|
|
419
421
|
# Ask user if they want to fix
|
|
420
|
-
if typer.confirm("\n Do you want to requeue these for regeneration?", default=True):
|
|
422
|
+
if auto_fix or typer.confirm("\n Do you want to requeue these for regeneration?", default=True):
|
|
421
423
|
requeued_jobs = 0
|
|
422
424
|
requeued_turns = 0
|
|
423
425
|
|
|
@@ -15,6 +15,7 @@ import secrets
|
|
|
15
15
|
import hashlib
|
|
16
16
|
import base64
|
|
17
17
|
import threading
|
|
18
|
+
import shutil
|
|
18
19
|
from urllib.parse import urlparse
|
|
19
20
|
from collections import defaultdict
|
|
20
21
|
from dataclasses import dataclass
|
|
@@ -2523,6 +2524,59 @@ def display_share_result(
|
|
|
2523
2524
|
console.print(f"[bold]👁️ Max Views:[/bold] {max_views}\n")
|
|
2524
2525
|
|
|
2525
2526
|
|
|
2527
|
+
def _run_clipboard_command(command: list[str], text: str) -> bool:
|
|
2528
|
+
try:
|
|
2529
|
+
return (
|
|
2530
|
+
subprocess.run(
|
|
2531
|
+
command,
|
|
2532
|
+
input=text,
|
|
2533
|
+
text=True,
|
|
2534
|
+
capture_output=False,
|
|
2535
|
+
check=False,
|
|
2536
|
+
).returncode
|
|
2537
|
+
== 0
|
|
2538
|
+
)
|
|
2539
|
+
except Exception:
|
|
2540
|
+
return False
|
|
2541
|
+
|
|
2542
|
+
|
|
2543
|
+
def _copy_text_to_clipboard(text: str) -> bool:
|
|
2544
|
+
if not text:
|
|
2545
|
+
return False
|
|
2546
|
+
|
|
2547
|
+
if shutil.which("pbcopy"):
|
|
2548
|
+
if _run_clipboard_command(["pbcopy"], text):
|
|
2549
|
+
return True
|
|
2550
|
+
|
|
2551
|
+
if os.name == "nt" and shutil.which("clip"):
|
|
2552
|
+
if _run_clipboard_command(["clip"], text):
|
|
2553
|
+
return True
|
|
2554
|
+
|
|
2555
|
+
if shutil.which("wl-copy"):
|
|
2556
|
+
if _run_clipboard_command(["wl-copy"], text):
|
|
2557
|
+
return True
|
|
2558
|
+
|
|
2559
|
+
if shutil.which("xclip"):
|
|
2560
|
+
if _run_clipboard_command(["xclip", "-selection", "clipboard"], text):
|
|
2561
|
+
return True
|
|
2562
|
+
|
|
2563
|
+
if shutil.which("xsel"):
|
|
2564
|
+
if _run_clipboard_command(["xsel", "--clipboard", "--input"], text):
|
|
2565
|
+
return True
|
|
2566
|
+
|
|
2567
|
+
return False
|
|
2568
|
+
|
|
2569
|
+
|
|
2570
|
+
def _copy_share_to_clipboard(share_url: Optional[str], slack_message: Optional[str]) -> bool:
|
|
2571
|
+
if not share_url:
|
|
2572
|
+
return False
|
|
2573
|
+
if slack_message:
|
|
2574
|
+
text_to_copy = f"{slack_message}\n\n{share_url}"
|
|
2575
|
+
else:
|
|
2576
|
+
text_to_copy = share_url
|
|
2577
|
+
return _copy_text_to_clipboard(text_to_copy)
|
|
2578
|
+
|
|
2579
|
+
|
|
2526
2580
|
def _export_by_events_interactive(
|
|
2527
2581
|
all_commits: List,
|
|
2528
2582
|
shadow_git: Path,
|
|
@@ -3616,7 +3670,250 @@ def export_shares_interactive_command(
|
|
|
3616
3670
|
max_views=max_views,
|
|
3617
3671
|
admin_token=result.get("admin_token"),
|
|
3618
3672
|
)
|
|
3673
|
+
copied = _copy_share_to_clipboard(
|
|
3674
|
+
result.get("share_url"),
|
|
3675
|
+
ui_metadata.get("slack_message") if ui_metadata else None,
|
|
3676
|
+
)
|
|
3677
|
+
if copied:
|
|
3678
|
+
print("📋 Copied Slack message and share link to clipboard.")
|
|
3619
3679
|
|
|
3620
3680
|
if not json_output:
|
|
3621
3681
|
logger.info(f"======== Interactive export completed: {result['share_url']} ========")
|
|
3622
3682
|
return 0
|
|
3683
|
+
|
|
3684
|
+
|
|
3685
|
+
def export_agent_shares_command(
|
|
3686
|
+
agent_id: str,
|
|
3687
|
+
password: Optional[str] = None,
|
|
3688
|
+
expiry_days: int = 7,
|
|
3689
|
+
max_views: int = 100,
|
|
3690
|
+
backend_url: Optional[str] = None,
|
|
3691
|
+
enable_mcp: bool = True,
|
|
3692
|
+
json_output: bool = False,
|
|
3693
|
+
compact: bool = True,
|
|
3694
|
+
max_tool_result_chars: int = 8_000,
|
|
3695
|
+
max_tool_command_chars: int = 2_000,
|
|
3696
|
+
progress_callback: Optional[Callable[[str], None]] = None,
|
|
3697
|
+
) -> int:
|
|
3698
|
+
"""
|
|
3699
|
+
Export all sessions associated with an agent and generate a share link.
|
|
3700
|
+
|
|
3701
|
+
This function creates a synthetic event structure from agent sessions,
|
|
3702
|
+
generates UI metadata (Slack message), uploads to backend, and returns
|
|
3703
|
+
the share link.
|
|
3704
|
+
|
|
3705
|
+
Args:
|
|
3706
|
+
agent_id: The agent_info ID to export sessions for
|
|
3707
|
+
password: Encryption password (if None, no encryption)
|
|
3708
|
+
expiry_days: Share expiry in days
|
|
3709
|
+
max_views: Maximum number of views
|
|
3710
|
+
backend_url: Backend server URL (uses config default if None)
|
|
3711
|
+
enable_mcp: Whether to include MCP instructions
|
|
3712
|
+
json_output: If True, output JSON format
|
|
3713
|
+
compact: Whether to compact the export data
|
|
3714
|
+
max_tool_result_chars: Max chars for tool results (with compact)
|
|
3715
|
+
max_tool_command_chars: Max chars for tool commands (with compact)
|
|
3716
|
+
progress_callback: Optional callback for progress updates (message: str) -> None
|
|
3717
|
+
|
|
3718
|
+
Returns:
|
|
3719
|
+
0 on success, 1 on error
|
|
3720
|
+
"""
|
|
3721
|
+
def _progress(msg: str) -> None:
|
|
3722
|
+
if progress_callback:
|
|
3723
|
+
progress_callback(msg)
|
|
3724
|
+
|
|
3725
|
+
if not json_output:
|
|
3726
|
+
logger.info(f"======== Export agent shares command started for agent {agent_id} ========")
|
|
3727
|
+
|
|
3728
|
+
# Check dependencies
|
|
3729
|
+
if not CRYPTO_AVAILABLE:
|
|
3730
|
+
if not json_output:
|
|
3731
|
+
print("Error: cryptography package not installed", file=sys.stderr)
|
|
3732
|
+
return 1
|
|
3733
|
+
|
|
3734
|
+
if not HTTPX_AVAILABLE:
|
|
3735
|
+
if not json_output:
|
|
3736
|
+
print("Error: httpx package not installed", file=sys.stderr)
|
|
3737
|
+
return 1
|
|
3738
|
+
|
|
3739
|
+
# Check authentication
|
|
3740
|
+
if not is_logged_in():
|
|
3741
|
+
if not json_output:
|
|
3742
|
+
print("Error: Not logged in. Please run 'aline login' first.", file=sys.stderr)
|
|
3743
|
+
return 1
|
|
3744
|
+
|
|
3745
|
+
_progress("Fetching agent info...")
|
|
3746
|
+
|
|
3747
|
+
# Get backend URL
|
|
3748
|
+
if backend_url is None:
|
|
3749
|
+
from ..config import ReAlignConfig
|
|
3750
|
+
|
|
3751
|
+
config = ReAlignConfig.load()
|
|
3752
|
+
backend_url = config.share_backend_url
|
|
3753
|
+
|
|
3754
|
+
# Get database
|
|
3755
|
+
from ..db import get_database
|
|
3756
|
+
|
|
3757
|
+
db = get_database()
|
|
3758
|
+
|
|
3759
|
+
# Get agent info
|
|
3760
|
+
agent_info = db.get_agent_info(agent_id)
|
|
3761
|
+
if not agent_info:
|
|
3762
|
+
if not json_output:
|
|
3763
|
+
print(f"Error: Agent not found: {agent_id}", file=sys.stderr)
|
|
3764
|
+
return 1
|
|
3765
|
+
|
|
3766
|
+
# Get sessions for this agent
|
|
3767
|
+
session_records = db.get_sessions_by_agent_id(agent_id)
|
|
3768
|
+
if not session_records:
|
|
3769
|
+
if not json_output:
|
|
3770
|
+
print(f"Error: Agent has no sessions to share", file=sys.stderr)
|
|
3771
|
+
return 1
|
|
3772
|
+
|
|
3773
|
+
_progress(f"Found {len(session_records)} session(s)")
|
|
3774
|
+
|
|
3775
|
+
# Build exportable sessions
|
|
3776
|
+
selected_sessions = build_exportable_sessions_from_records(session_records)
|
|
3777
|
+
if not selected_sessions:
|
|
3778
|
+
if not json_output:
|
|
3779
|
+
print("Error: No sessions found for agent", file=sys.stderr)
|
|
3780
|
+
return 1
|
|
3781
|
+
|
|
3782
|
+
# Create a synthetic event structure for the agent
|
|
3783
|
+
# We use the agent name/description as event title/description
|
|
3784
|
+
event_title = agent_info.name or "Agent Sessions"
|
|
3785
|
+
event_description = agent_info.description or f"Sessions from agent: {agent_info.name}"
|
|
3786
|
+
|
|
3787
|
+
# Create a synthetic ExportableEvent
|
|
3788
|
+
synthetic_event = ExportableEvent(
|
|
3789
|
+
index=1,
|
|
3790
|
+
event_id=f"agent-{agent_id}",
|
|
3791
|
+
title=event_title,
|
|
3792
|
+
description=event_description,
|
|
3793
|
+
event_type="agent",
|
|
3794
|
+
status="active",
|
|
3795
|
+
updated_at=datetime.now(timezone.utc),
|
|
3796
|
+
sessions=session_records,
|
|
3797
|
+
)
|
|
3798
|
+
|
|
3799
|
+
# Build conversation data
|
|
3800
|
+
_progress("Building conversation data...")
|
|
3801
|
+
|
|
3802
|
+
username = os.environ.get("USER") or os.environ.get("USERNAME") or "anonymous"
|
|
3803
|
+
|
|
3804
|
+
compaction = None
|
|
3805
|
+
if compact:
|
|
3806
|
+
compaction = ExportCompactionConfig(
|
|
3807
|
+
enabled=True,
|
|
3808
|
+
max_tool_result_chars=max_tool_result_chars,
|
|
3809
|
+
max_tool_command_chars=max_tool_command_chars,
|
|
3810
|
+
)
|
|
3811
|
+
|
|
3812
|
+
try:
|
|
3813
|
+
conversation_data = build_enhanced_conversation_data(
|
|
3814
|
+
selected_event=synthetic_event,
|
|
3815
|
+
selected_sessions=selected_sessions,
|
|
3816
|
+
username=username,
|
|
3817
|
+
db=db,
|
|
3818
|
+
compaction=compaction,
|
|
3819
|
+
)
|
|
3820
|
+
except Exception as e:
|
|
3821
|
+
if not json_output:
|
|
3822
|
+
print(f"Error: Failed to build conversation data: {e}", file=sys.stderr)
|
|
3823
|
+
logger.error(f"Failed to build conversation data: {e}", exc_info=True)
|
|
3824
|
+
return 1
|
|
3825
|
+
|
|
3826
|
+
if not conversation_data.get("sessions"):
|
|
3827
|
+
if not json_output:
|
|
3828
|
+
print("Error: No sessions found in conversation data", file=sys.stderr)
|
|
3829
|
+
return 1
|
|
3830
|
+
|
|
3831
|
+
# Generate UI metadata with LLM
|
|
3832
|
+
_progress("Generating share message...")
|
|
3833
|
+
|
|
3834
|
+
from ..config import ReAlignConfig
|
|
3835
|
+
|
|
3836
|
+
config = ReAlignConfig.load()
|
|
3837
|
+
ui_metadata, _ = generate_ui_metadata_with_llm(
|
|
3838
|
+
conversation_data,
|
|
3839
|
+
[], # No commits
|
|
3840
|
+
event_title=event_title,
|
|
3841
|
+
event_description=event_description,
|
|
3842
|
+
provider=config.llm_provider,
|
|
3843
|
+
preset_id="default",
|
|
3844
|
+
silent=json_output,
|
|
3845
|
+
)
|
|
3846
|
+
|
|
3847
|
+
if ui_metadata:
|
|
3848
|
+
conversation_data["ui_metadata"] = ui_metadata
|
|
3849
|
+
else:
|
|
3850
|
+
conversation_data["ui_metadata"] = {
|
|
3851
|
+
"title": event_title,
|
|
3852
|
+
"description": event_description,
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
# Add MCP instructions if enabled
|
|
3856
|
+
if enable_mcp:
|
|
3857
|
+
conversation_data["ui_metadata"]["mcp_instructions"] = {
|
|
3858
|
+
"tool_name": "ask_shared_conversation",
|
|
3859
|
+
"usage": "Local AI agents can install the aline MCP server and use the 'ask_shared_conversation' tool to query this conversation programmatically.",
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
# Upload to backend (no encryption for agent shares by default)
|
|
3863
|
+
_progress("Uploading to cloud...")
|
|
3864
|
+
|
|
3865
|
+
metadata = {
|
|
3866
|
+
"username": username,
|
|
3867
|
+
"expiry_days": expiry_days,
|
|
3868
|
+
"max_views": max_views,
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
try:
|
|
3872
|
+
if password:
|
|
3873
|
+
encrypted_payload = encrypt_conversation_data(conversation_data, password)
|
|
3874
|
+
result = upload_to_backend(
|
|
3875
|
+
encrypted_payload=encrypted_payload,
|
|
3876
|
+
metadata=metadata,
|
|
3877
|
+
backend_url=backend_url,
|
|
3878
|
+
ui_metadata=conversation_data.get("ui_metadata"),
|
|
3879
|
+
background=True,
|
|
3880
|
+
)
|
|
3881
|
+
else:
|
|
3882
|
+
result = upload_to_backend_unencrypted(
|
|
3883
|
+
conversation_data=conversation_data,
|
|
3884
|
+
metadata=metadata,
|
|
3885
|
+
backend_url=backend_url,
|
|
3886
|
+
background=True,
|
|
3887
|
+
)
|
|
3888
|
+
except Exception as e:
|
|
3889
|
+
if not json_output:
|
|
3890
|
+
print(f"Error: Upload failed: {e}", file=sys.stderr)
|
|
3891
|
+
logger.error(f"Upload failed: {e}", exc_info=True)
|
|
3892
|
+
return 1
|
|
3893
|
+
|
|
3894
|
+
share_url = result.get("share_url")
|
|
3895
|
+
slack_message = ui_metadata.get("slack_message") if ui_metadata else None
|
|
3896
|
+
|
|
3897
|
+
# Output results
|
|
3898
|
+
if json_output:
|
|
3899
|
+
output_data = {
|
|
3900
|
+
"agent_id": agent_id,
|
|
3901
|
+
"agent_name": agent_info.name,
|
|
3902
|
+
"share_link": share_url,
|
|
3903
|
+
"slack_message": slack_message,
|
|
3904
|
+
"session_count": len(selected_sessions),
|
|
3905
|
+
"password": password,
|
|
3906
|
+
}
|
|
3907
|
+
print(json.dumps(output_data, ensure_ascii=False, indent=2))
|
|
3908
|
+
else:
|
|
3909
|
+
print(f"\n✅ Shared {len(selected_sessions)} session(s) from agent: {agent_info.name}")
|
|
3910
|
+
print(f"🔗 Share link: {share_url}")
|
|
3911
|
+
if slack_message:
|
|
3912
|
+
print(f"\n📝 Slack message:\n{slack_message}")
|
|
3913
|
+
copied = _copy_share_to_clipboard(share_url, slack_message)
|
|
3914
|
+
if copied:
|
|
3915
|
+
print("📋 Copied Slack message and share link to clipboard.")
|
|
3916
|
+
|
|
3917
|
+
if not json_output:
|
|
3918
|
+
logger.info(f"======== Agent export completed: {share_url} ========")
|
|
3919
|
+
return 0
|
realign/commands/search.py
CHANGED
|
@@ -335,9 +335,28 @@ def search_command(
|
|
|
335
335
|
context_session_ids = get_context_session_ids()
|
|
336
336
|
context_event_ids = get_context_event_ids()
|
|
337
337
|
|
|
338
|
+
# Apply agent scoping if ALINE_AGENT_ID is set
|
|
339
|
+
agent_session_ids = None
|
|
340
|
+
if not no_context:
|
|
341
|
+
import os
|
|
342
|
+
|
|
343
|
+
agent_id = os.environ.get("ALINE_AGENT_ID")
|
|
344
|
+
if agent_id:
|
|
345
|
+
agent_sessions = db.get_sessions_by_agent_id(agent_id)
|
|
346
|
+
# Always set agent_session_ids when agent_id exists
|
|
347
|
+
# (empty list means no sessions for this agent -> empty results)
|
|
348
|
+
agent_session_ids = [s.id for s in agent_sessions]
|
|
349
|
+
|
|
338
350
|
# Parse session IDs if provided (resolve prefixes)
|
|
339
351
|
session_ids = _resolve_id_prefixes(db, "sessions", sessions) or None
|
|
340
352
|
|
|
353
|
+
# Intersect with agent sessions first (highest priority)
|
|
354
|
+
if agent_session_ids is not None:
|
|
355
|
+
if session_ids:
|
|
356
|
+
session_ids = list(set(session_ids) & set(agent_session_ids))
|
|
357
|
+
else:
|
|
358
|
+
session_ids = agent_session_ids if agent_session_ids else []
|
|
359
|
+
|
|
341
360
|
# Intersect with context sessions
|
|
342
361
|
if context_session_ids:
|
|
343
362
|
if session_ids:
|
|
@@ -372,43 +391,53 @@ def search_command(
|
|
|
372
391
|
turn_ids = _resolve_id_prefixes(db, "turns", turns) or None
|
|
373
392
|
|
|
374
393
|
# 1. Search Events (events don't have session scope, skip if sessions/events filter is active)
|
|
375
|
-
|
|
394
|
+
# Use 'is None' to distinguish "no filter" from "empty filter results"
|
|
395
|
+
if type in ("all", "event") and session_ids is None and event_ids is None:
|
|
376
396
|
events = db.search_events(query, limit=limit, use_regex=regex, ignore_case=ignore_case)
|
|
377
397
|
results["events"] = events
|
|
378
398
|
|
|
379
|
-
# 2. Search Turns
|
|
399
|
+
# 2. Search Turns (skip if session filter results in empty list)
|
|
380
400
|
if type in ("all", "turn"):
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
401
|
+
if session_ids is not None and len(session_ids) == 0:
|
|
402
|
+
results["turns"] = []
|
|
403
|
+
else:
|
|
404
|
+
turns = db.search_conversations(
|
|
405
|
+
query,
|
|
406
|
+
limit=limit,
|
|
407
|
+
use_regex=regex,
|
|
408
|
+
ignore_case=ignore_case,
|
|
409
|
+
session_ids=session_ids if session_ids else None,
|
|
410
|
+
)
|
|
411
|
+
results["turns"] = turns
|
|
389
412
|
|
|
390
|
-
# 3. Search Sessions
|
|
413
|
+
# 3. Search Sessions (skip if session filter results in empty list)
|
|
391
414
|
if type in ("all", "session"):
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
415
|
+
if session_ids is not None and len(session_ids) == 0:
|
|
416
|
+
results["sessions"] = []
|
|
417
|
+
else:
|
|
418
|
+
sessions_results = db.search_sessions(
|
|
419
|
+
query,
|
|
420
|
+
limit=limit,
|
|
421
|
+
use_regex=regex,
|
|
422
|
+
ignore_case=ignore_case,
|
|
423
|
+
session_ids=session_ids if session_ids else None,
|
|
424
|
+
)
|
|
425
|
+
results["sessions"] = sessions_results
|
|
400
426
|
|
|
401
|
-
# 4. Search Turn Content
|
|
427
|
+
# 4. Search Turn Content (skip if session filter results in empty list)
|
|
402
428
|
if type == "content":
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
429
|
+
if session_ids is not None and len(session_ids) == 0:
|
|
430
|
+
results["content"] = []
|
|
431
|
+
else:
|
|
432
|
+
content_results = db.search_turn_content(
|
|
433
|
+
query,
|
|
434
|
+
limit=limit,
|
|
435
|
+
use_regex=regex,
|
|
436
|
+
ignore_case=ignore_case,
|
|
437
|
+
session_ids=session_ids if session_ids else None,
|
|
438
|
+
turn_ids=turn_ids,
|
|
439
|
+
)
|
|
440
|
+
results["content"] = content_results
|
|
412
441
|
|
|
413
442
|
# === Grep-style output for regex mode ===
|
|
414
443
|
if regex:
|