aline-ai 0.5.3__py3-none-any.whl → 0.5.5__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.3.dist-info → aline_ai-0.5.5.dist-info}/METADATA +1 -1
- aline_ai-0.5.5.dist-info/RECORD +93 -0
- realign/__init__.py +1 -1
- realign/adapters/antigravity.py +28 -20
- realign/adapters/base.py +46 -50
- realign/adapters/claude.py +14 -14
- realign/adapters/codex.py +7 -7
- realign/adapters/gemini.py +11 -11
- realign/adapters/registry.py +14 -10
- realign/claude_detector.py +2 -2
- realign/claude_hooks/__init__.py +3 -3
- realign/claude_hooks/permission_request_hook.py +35 -0
- realign/claude_hooks/permission_request_hook_installer.py +31 -32
- realign/claude_hooks/stop_hook.py +4 -1
- realign/claude_hooks/stop_hook_installer.py +30 -31
- realign/cli.py +24 -0
- realign/codex_detector.py +11 -11
- realign/commands/add.py +361 -35
- realign/commands/config.py +3 -12
- realign/commands/context.py +3 -1
- realign/commands/export_shares.py +86 -127
- realign/commands/import_shares.py +145 -155
- realign/commands/init.py +166 -30
- realign/commands/restore.py +18 -6
- realign/commands/search.py +14 -42
- realign/commands/upgrade.py +155 -11
- realign/commands/watcher.py +98 -219
- realign/commands/worker.py +29 -6
- realign/config.py +25 -20
- realign/context.py +1 -3
- realign/dashboard/app.py +4 -4
- realign/dashboard/screens/create_event.py +3 -1
- realign/dashboard/screens/event_detail.py +14 -6
- realign/dashboard/screens/session_detail.py +3 -1
- realign/dashboard/screens/share_import.py +7 -3
- realign/dashboard/tmux_manager.py +91 -22
- realign/dashboard/widgets/config_panel.py +85 -1
- realign/dashboard/widgets/events_table.py +3 -1
- realign/dashboard/widgets/header.py +1 -0
- realign/dashboard/widgets/search_panel.py +37 -27
- realign/dashboard/widgets/sessions_table.py +24 -15
- realign/dashboard/widgets/terminal_panel.py +207 -17
- realign/dashboard/widgets/watcher_panel.py +6 -2
- realign/dashboard/widgets/worker_panel.py +10 -1
- realign/db/__init__.py +1 -1
- realign/db/base.py +5 -15
- realign/db/locks.py +0 -1
- realign/db/migration.py +82 -76
- realign/db/schema.py +2 -6
- realign/db/sqlite_db.py +23 -41
- realign/events/__init__.py +0 -1
- realign/events/event_summarizer.py +27 -15
- realign/events/session_summarizer.py +29 -15
- realign/file_lock.py +1 -0
- realign/hooks.py +150 -60
- realign/logging_config.py +12 -15
- realign/mcp_server.py +30 -51
- realign/mcp_watcher.py +0 -1
- realign/models/event.py +29 -20
- realign/prompts/__init__.py +7 -7
- realign/prompts/presets.py +15 -11
- realign/redactor.py +99 -59
- realign/triggers/__init__.py +9 -9
- realign/triggers/antigravity_trigger.py +30 -28
- realign/triggers/base.py +4 -3
- realign/triggers/claude_trigger.py +104 -85
- realign/triggers/codex_trigger.py +15 -5
- realign/triggers/gemini_trigger.py +57 -47
- realign/triggers/next_turn_trigger.py +3 -1
- realign/triggers/registry.py +6 -2
- realign/triggers/turn_status.py +3 -1
- realign/watcher_core.py +306 -131
- realign/watcher_daemon.py +8 -8
- realign/worker_core.py +3 -1
- realign/worker_daemon.py +3 -1
- aline_ai-0.5.3.dist-info/RECORD +0 -93
- {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/top_level.txt +0 -0
realign/commands/init.py
CHANGED
|
@@ -2,16 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Dict, Any, Optional, Tuple
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
import re
|
|
5
6
|
import typer
|
|
6
7
|
from rich.console import Console
|
|
7
8
|
|
|
8
|
-
from ..config import
|
|
9
|
+
from ..config import (
|
|
10
|
+
ReAlignConfig,
|
|
11
|
+
get_default_config_content,
|
|
12
|
+
generate_user_id,
|
|
13
|
+
generate_random_username,
|
|
14
|
+
)
|
|
9
15
|
|
|
10
16
|
console = Console()
|
|
11
17
|
|
|
18
|
+
|
|
12
19
|
# tmux config template for Aline-managed dashboard sessions.
|
|
13
20
|
# Stored at ~/.aline/tmux/tmux.conf and sourced by the dashboard tmux bootstrap.
|
|
14
|
-
|
|
21
|
+
def _get_tmux_config() -> str:
|
|
22
|
+
"""Generate tmux config with Type-to-Exit bindings."""
|
|
23
|
+
conf = r"""# Aline tmux config
|
|
15
24
|
#
|
|
16
25
|
# Goal: make mouse selection copy to the system clipboard (macOS Terminal friendly).
|
|
17
26
|
# - Drag-select text with the mouse; when you release, it is copied to the clipboard.
|
|
@@ -21,9 +30,88 @@ TMUX_CONF = """# Aline tmux config
|
|
|
21
30
|
|
|
22
31
|
set -g mouse on
|
|
23
32
|
|
|
33
|
+
# Pane border styling - use double lines for wider, more visible borders (tmux 3.2+).
|
|
34
|
+
# This helps users identify the resizable border area more easily.
|
|
35
|
+
set -g pane-border-lines double
|
|
36
|
+
set -g pane-border-style "fg=brightblack"
|
|
37
|
+
set -g pane-active-border-style "fg=blue"
|
|
38
|
+
|
|
39
|
+
# Add a small indicator showing where the border is (tmux 3.2+).
|
|
40
|
+
# This creates a visual "dead zone" that's more obvious for resizing.
|
|
41
|
+
set -g pane-border-indicators arrows
|
|
42
|
+
|
|
43
|
+
# Disable paste-time detection so key bindings work during paste.
|
|
44
|
+
set -g assume-paste-time 0
|
|
45
|
+
|
|
46
|
+
# Fast escape time so ESC is processed immediately (helps with paste detection).
|
|
47
|
+
set -s escape-time 0
|
|
48
|
+
|
|
49
|
+
# Better scrolling: enter copy-mode with -e so scrolling to bottom exits it.
|
|
50
|
+
bind-key -n WheelUpPane if-shell -F -t = "#{mouse_any_flag}" "send-keys -M" "if -Ft= '#{pane_in_mode}' 'send-keys -M' 'copy-mode -e -t ='"
|
|
51
|
+
|
|
24
52
|
# macOS clipboard (pbcopy). Only set bindings if pbcopy is available.
|
|
25
|
-
if-shell 'command -v pbcopy >/dev/null 2>&1' '
|
|
53
|
+
if-shell 'command -v pbcopy >/dev/null 2>&1' '
|
|
54
|
+
bind -T copy-mode-vi MouseDragEnd1Pane send -X copy-pipe "pbcopy" \; if -F "#{==:#{scroll_position},0}" "send -X cancel"
|
|
55
|
+
bind -T copy-mode MouseDragEnd1Pane send -X copy-pipe "pbcopy" \; if -F "#{==:#{scroll_position},0}" "send -X cancel"
|
|
56
|
+
' ''
|
|
57
|
+
|
|
58
|
+
# Prevent mouse click from exiting copy-mode (stay in copy mode, just clear selection).
|
|
59
|
+
bind -T copy-mode-vi MouseDown1Pane select-pane \; send -X clear-selection
|
|
60
|
+
bind -T copy-mode MouseDown1Pane select-pane \; send -X clear-selection
|
|
61
|
+
|
|
62
|
+
# Type-to-Exit: Typing any alphanumeric character exits copy-mode and sends the key.
|
|
26
63
|
"""
|
|
64
|
+
def _tmux_quote(value: str) -> str:
|
|
65
|
+
# tmux config treats `#` as a comment delimiter; quote args so keys like `#` don't disappear.
|
|
66
|
+
# Note: `~` is special in tmux config parsing; use an escaped form instead of quotes.
|
|
67
|
+
if value == "~":
|
|
68
|
+
return r"\~"
|
|
69
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
70
|
+
return f'"{escaped}"'
|
|
71
|
+
|
|
72
|
+
def _bind_cancel_and_send(key: str) -> str:
|
|
73
|
+
key_token = _tmux_quote(key)
|
|
74
|
+
return (
|
|
75
|
+
f"bind -T copy-mode-vi {key_token} send -X cancel \\; send-keys -- {key_token}\n"
|
|
76
|
+
f"bind -T copy-mode {key_token} send -X cancel \\; send-keys -- {key_token}\n"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Generate bindings for common characters.
|
|
80
|
+
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.@/!#$%^&*()+=,<>?[]{}|~`;\\\"'"
|
|
81
|
+
for c in chars:
|
|
82
|
+
conf += _bind_cancel_and_send(c)
|
|
83
|
+
|
|
84
|
+
# Space: use tmux key name.
|
|
85
|
+
conf += "bind -T copy-mode-vi Space send -X cancel \\; send-keys Space\n"
|
|
86
|
+
conf += "bind -T copy-mode Space send -X cancel \\; send-keys Space\n"
|
|
87
|
+
|
|
88
|
+
# Cmd+V (paste): exit copy-mode when paste is detected.
|
|
89
|
+
# Different terminals handle Cmd+V differently:
|
|
90
|
+
# - Some send M-v (Meta+V)
|
|
91
|
+
# - Some use bracketed paste mode and send the content directly
|
|
92
|
+
# Since we bind all printable chars above, pasting text starting with a letter/number will exit.
|
|
93
|
+
# For other cases, bind Enter to also exit (multiline paste often starts with newline).
|
|
94
|
+
conf += "bind -T copy-mode-vi M-v send -X cancel \\; run 'sleep 0.05' \\; send-keys M-v\n"
|
|
95
|
+
conf += "bind -T copy-mode M-v send -X cancel \\; run 'sleep 0.05' \\; send-keys M-v\n"
|
|
96
|
+
|
|
97
|
+
return conf
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_TMUX_CONF_REPAIR_TILDE_KEY_RE = re.compile(
|
|
101
|
+
r'^(bind(?:-key)?\s+-T\s+copy-mode(?:-vi)?\s+)(?:"~"|~)(\s+send\s+-X\s+cancel\s+\\;\s+send-keys\s+)(?:"~"|~)\s*$',
|
|
102
|
+
re.MULTILINE,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
_TMUX_CONF_REPAIR_KEY_NEEDS_QUOTE_RE = re.compile(
|
|
106
|
+
r"^(bind(?:-key)?\s+-T\s+copy-mode(?:-vi)?\s+)([#{}])(\s+send\s+-X\s+cancel\s+\\;\s+send-keys\s+)\2\s*$",
|
|
107
|
+
re.MULTILINE,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
_TMUX_CONF_REPAIR_SEND_KEYS_DASHDASH_RE = re.compile(
|
|
111
|
+
r"^(bind(?:-key)?\s+-T\s+copy-mode(?:-vi)?\s+.+\s+send\s+-X\s+cancel\s+\\;\s+send-keys\s+)(?!--\s)(.+)$",
|
|
112
|
+
re.MULTILINE,
|
|
113
|
+
)
|
|
114
|
+
|
|
27
115
|
|
|
28
116
|
# Prompt templates for ~/.aline/prompts/
|
|
29
117
|
PROMPT_README = """# Custom Prompts for ReAlign
|
|
@@ -331,6 +419,7 @@ Make it shorter and mention that we also added tests
|
|
|
331
419
|
Remember: Focus on what the user wants changed while keeping the message grounded in the original event context.
|
|
332
420
|
"""
|
|
333
421
|
|
|
422
|
+
|
|
334
423
|
def _initialize_prompts_directory() -> None:
|
|
335
424
|
"""
|
|
336
425
|
Initialize ~/.aline/prompts/ directory with example prompt files.
|
|
@@ -364,7 +453,25 @@ def _initialize_tmux_config() -> Path:
|
|
|
364
453
|
tmux_conf_path = Path.home() / ".aline" / "tmux" / "tmux.conf"
|
|
365
454
|
tmux_conf_path.parent.mkdir(parents=True, exist_ok=True)
|
|
366
455
|
if not tmux_conf_path.exists():
|
|
367
|
-
tmux_conf_path.write_text(
|
|
456
|
+
tmux_conf_path.write_text(_get_tmux_config(), encoding="utf-8")
|
|
457
|
+
return tmux_conf_path
|
|
458
|
+
|
|
459
|
+
# Best-effort repair for older Aline-generated configs that used unquoted `#` keys.
|
|
460
|
+
# tmux parses `#` as a comment delimiter, turning `bind ... # ...` into `bind ...` (invalid).
|
|
461
|
+
try:
|
|
462
|
+
existing = tmux_conf_path.read_text(encoding="utf-8")
|
|
463
|
+
except Exception:
|
|
464
|
+
return tmux_conf_path
|
|
465
|
+
|
|
466
|
+
if "# Aline tmux config" not in existing:
|
|
467
|
+
return tmux_conf_path
|
|
468
|
+
|
|
469
|
+
repaired = existing
|
|
470
|
+
repaired = _TMUX_CONF_REPAIR_TILDE_KEY_RE.sub(r"\1\\~\2\\~", repaired)
|
|
471
|
+
repaired = _TMUX_CONF_REPAIR_KEY_NEEDS_QUOTE_RE.sub(r'\1"\2"\3"\2"', repaired)
|
|
472
|
+
repaired = _TMUX_CONF_REPAIR_SEND_KEYS_DASHDASH_RE.sub(r"\1-- \2", repaired)
|
|
473
|
+
if repaired != existing:
|
|
474
|
+
tmux_conf_path.write_text(repaired, encoding="utf-8")
|
|
368
475
|
return tmux_conf_path
|
|
369
476
|
|
|
370
477
|
|
|
@@ -413,16 +520,10 @@ def _initialize_skills() -> Path:
|
|
|
413
520
|
Returns:
|
|
414
521
|
Path to the skills root directory
|
|
415
522
|
"""
|
|
416
|
-
from .add import
|
|
417
|
-
|
|
418
|
-
skill_root = Path.home() / ".claude" / "skills"
|
|
523
|
+
from .add import add_skills_command
|
|
419
524
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if not skill_path.exists():
|
|
423
|
-
_install_skill_to_path(skill_root, skill_name, skill_content)
|
|
424
|
-
|
|
425
|
-
return skill_root
|
|
525
|
+
add_skills_command(force=False)
|
|
526
|
+
return Path.home() / ".claude" / "skills"
|
|
426
527
|
|
|
427
528
|
|
|
428
529
|
def _initialize_claude_hooks() -> Tuple[bool, list]:
|
|
@@ -439,6 +540,7 @@ def _initialize_claude_hooks() -> Tuple[bool, list]:
|
|
|
439
540
|
|
|
440
541
|
try:
|
|
441
542
|
from ..claude_hooks.stop_hook_installer import ensure_stop_hook_installed
|
|
543
|
+
|
|
442
544
|
if ensure_stop_hook_installed(quiet=True):
|
|
443
545
|
installed_hooks.append("Stop")
|
|
444
546
|
else:
|
|
@@ -447,7 +549,10 @@ def _initialize_claude_hooks() -> Tuple[bool, list]:
|
|
|
447
549
|
all_success = False
|
|
448
550
|
|
|
449
551
|
try:
|
|
450
|
-
from ..claude_hooks.user_prompt_submit_hook_installer import
|
|
552
|
+
from ..claude_hooks.user_prompt_submit_hook_installer import (
|
|
553
|
+
ensure_user_prompt_submit_hook_installed,
|
|
554
|
+
)
|
|
555
|
+
|
|
451
556
|
if ensure_user_prompt_submit_hook_installed(quiet=True):
|
|
452
557
|
installed_hooks.append("UserPromptSubmit")
|
|
453
558
|
else:
|
|
@@ -456,7 +561,10 @@ def _initialize_claude_hooks() -> Tuple[bool, list]:
|
|
|
456
561
|
all_success = False
|
|
457
562
|
|
|
458
563
|
try:
|
|
459
|
-
from ..claude_hooks.permission_request_hook_installer import
|
|
564
|
+
from ..claude_hooks.permission_request_hook_installer import (
|
|
565
|
+
ensure_permission_request_hook_installed,
|
|
566
|
+
)
|
|
567
|
+
|
|
460
568
|
if ensure_permission_request_hook_installed(quiet=True):
|
|
461
569
|
installed_hooks.append("PermissionRequest")
|
|
462
570
|
else:
|
|
@@ -496,7 +604,9 @@ def init_global(
|
|
|
496
604
|
global_config_path = Path.home() / ".aline" / "config.yaml"
|
|
497
605
|
if force or not global_config_path.exists():
|
|
498
606
|
global_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
499
|
-
global_config_path.write_text(
|
|
607
|
+
global_config_path.write_text(
|
|
608
|
+
get_default_config_content(), encoding="utf-8"
|
|
609
|
+
)
|
|
500
610
|
result["config_path"] = str(global_config_path)
|
|
501
611
|
|
|
502
612
|
# Load config
|
|
@@ -505,7 +615,9 @@ def init_global(
|
|
|
505
615
|
# User identity setup (V9)
|
|
506
616
|
if not config.user_id:
|
|
507
617
|
console.print("\n[bold blue]═══ User Identity Setup ═══[/bold blue]")
|
|
508
|
-
console.print(
|
|
618
|
+
console.print(
|
|
619
|
+
"ReAlign needs to identify you for tracking session ownership.\n"
|
|
620
|
+
)
|
|
509
621
|
|
|
510
622
|
# Generate user UUID (based on MAC address)
|
|
511
623
|
config.user_id = generate_user_id()
|
|
@@ -514,23 +626,30 @@ def init_global(
|
|
|
514
626
|
# Prompt user for username
|
|
515
627
|
try:
|
|
516
628
|
from rich.prompt import Prompt
|
|
629
|
+
|
|
517
630
|
user_input = Prompt.ask(
|
|
518
631
|
"[cyan]Enter your username (or press Enter for auto-generated)[/cyan]",
|
|
519
|
-
default=""
|
|
632
|
+
default="",
|
|
520
633
|
)
|
|
521
634
|
except ImportError:
|
|
522
|
-
user_input = input(
|
|
635
|
+
user_input = input(
|
|
636
|
+
"Enter your username (or press Enter for auto-generated): "
|
|
637
|
+
).strip()
|
|
523
638
|
|
|
524
639
|
if user_input:
|
|
525
640
|
config.user_name = user_input
|
|
526
641
|
else:
|
|
527
642
|
# Auto-generate username
|
|
528
643
|
config.user_name = generate_random_username()
|
|
529
|
-
console.print(
|
|
644
|
+
console.print(
|
|
645
|
+
f"Auto-generated username: [yellow]{config.user_name}[/yellow]"
|
|
646
|
+
)
|
|
530
647
|
|
|
531
648
|
# Save config with user identity
|
|
532
649
|
config.save()
|
|
533
|
-
console.print(
|
|
650
|
+
console.print(
|
|
651
|
+
f"[green]✓[/green] User identity saved: [bold]{config.user_name}[/bold] ([dim]{config.user_id[:8]}...[/dim])\n"
|
|
652
|
+
)
|
|
534
653
|
|
|
535
654
|
# Initialize database
|
|
536
655
|
db_path = Path(config.sqlite_db_path).expanduser()
|
|
@@ -539,6 +658,7 @@ def init_global(
|
|
|
539
658
|
|
|
540
659
|
# Create/upgrade database schema
|
|
541
660
|
from ..db.sqlite_db import SQLiteDatabase
|
|
661
|
+
|
|
542
662
|
db = SQLiteDatabase(str(db_path))
|
|
543
663
|
db.initialize()
|
|
544
664
|
db.close()
|
|
@@ -562,7 +682,9 @@ def init_global(
|
|
|
562
682
|
result["errors"].append("Some Claude Code hooks failed to install")
|
|
563
683
|
|
|
564
684
|
result["success"] = True
|
|
565
|
-
result["message"] =
|
|
685
|
+
result["message"] = (
|
|
686
|
+
"Aline initialized successfully (global config + database + prompts + tmux + skills + hooks ready)"
|
|
687
|
+
)
|
|
566
688
|
|
|
567
689
|
except Exception as e:
|
|
568
690
|
result["errors"].append(f"Initialization failed: {e}")
|
|
@@ -572,7 +694,9 @@ def init_global(
|
|
|
572
694
|
|
|
573
695
|
|
|
574
696
|
def init_command(
|
|
575
|
-
force: bool = typer.Option(
|
|
697
|
+
force: bool = typer.Option(
|
|
698
|
+
False, "--force", "-f", help="Overwrite global config with defaults"
|
|
699
|
+
),
|
|
576
700
|
start_watcher: Optional[bool] = typer.Option(
|
|
577
701
|
None,
|
|
578
702
|
"--start-watcher/--no-start-watcher",
|
|
@@ -607,7 +731,7 @@ def init_command(
|
|
|
607
731
|
from . import watcher as watcher_cmd
|
|
608
732
|
|
|
609
733
|
watcher_start_exit = watcher_cmd.watcher_start_command()
|
|
610
|
-
watcher_started =
|
|
734
|
+
watcher_started = watcher_start_exit == 0
|
|
611
735
|
except Exception:
|
|
612
736
|
watcher_started = False
|
|
613
737
|
watcher_start_exit = 1
|
|
@@ -617,7 +741,7 @@ def init_command(
|
|
|
617
741
|
from . import worker as worker_cmd
|
|
618
742
|
|
|
619
743
|
worker_start_exit = worker_cmd.worker_start_command()
|
|
620
|
-
worker_started =
|
|
744
|
+
worker_started = worker_start_exit == 0
|
|
621
745
|
except Exception:
|
|
622
746
|
worker_started = False
|
|
623
747
|
worker_start_exit = 1
|
|
@@ -652,7 +776,9 @@ def init_command(
|
|
|
652
776
|
else:
|
|
653
777
|
console.print(" Status: [red]FAILED TO START[/red]")
|
|
654
778
|
console.print(" Try: [cyan]aline watcher start[/cyan]", style="dim")
|
|
655
|
-
console.print(
|
|
779
|
+
console.print(
|
|
780
|
+
" Logs: [cyan]~/.aline/.logs/watcher_*.log[/cyan]", style="dim"
|
|
781
|
+
)
|
|
656
782
|
|
|
657
783
|
console.print("\n[bold]Worker:[/bold]")
|
|
658
784
|
if worker_started:
|
|
@@ -661,7 +787,9 @@ def init_command(
|
|
|
661
787
|
else:
|
|
662
788
|
console.print(" Status: [red]FAILED TO START[/red]")
|
|
663
789
|
console.print(" Try: [cyan]aline worker start[/cyan]", style="dim")
|
|
664
|
-
console.print(
|
|
790
|
+
console.print(
|
|
791
|
+
" Logs: [cyan]~/.aline/.logs/worker_*.log[/cyan]", style="dim"
|
|
792
|
+
)
|
|
665
793
|
|
|
666
794
|
if result.get("errors"):
|
|
667
795
|
console.print("\n[bold red]Errors:[/bold red]")
|
|
@@ -672,9 +800,17 @@ def init_command(
|
|
|
672
800
|
|
|
673
801
|
if result["success"]:
|
|
674
802
|
console.print("[bold]Next steps:[/bold]")
|
|
675
|
-
console.print(
|
|
676
|
-
|
|
677
|
-
|
|
803
|
+
console.print(
|
|
804
|
+
" 1. Start Claude Code or Codex - sessions are tracked automatically",
|
|
805
|
+
style="dim",
|
|
806
|
+
)
|
|
807
|
+
console.print(
|
|
808
|
+
" 2. Search history with: [cyan]aline search <query>[/cyan]", style="dim"
|
|
809
|
+
)
|
|
810
|
+
console.print(
|
|
811
|
+
" 3. Customize prompts (optional): [cyan]~/.aline/prompts/[/cyan]",
|
|
812
|
+
style="dim",
|
|
813
|
+
)
|
|
678
814
|
|
|
679
815
|
# If the user explicitly asked to start the watcher, failing to do so should fail init too.
|
|
680
816
|
if start_watcher is True and watcher_start_exit not in (0, None):
|
realign/commands/restore.py
CHANGED
|
@@ -117,7 +117,9 @@ def restore_claude_command(
|
|
|
117
117
|
output_dir = Path("/tmp/aline_restore")
|
|
118
118
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
119
119
|
|
|
120
|
-
console.print(
|
|
120
|
+
console.print(
|
|
121
|
+
f"[bold]Restoring {len(selected_sessions)} Claude session(s) to {output_dir}[/bold]\n"
|
|
122
|
+
)
|
|
121
123
|
|
|
122
124
|
restored_count = 0
|
|
123
125
|
for session in selected_sessions:
|
|
@@ -141,7 +143,9 @@ def restore_claude_command(
|
|
|
141
143
|
all_lines.append(line)
|
|
142
144
|
|
|
143
145
|
if not all_lines:
|
|
144
|
-
console.print(
|
|
146
|
+
console.print(
|
|
147
|
+
f" [yellow]Session {session_id[:12]}... has no content, skipping[/yellow]"
|
|
148
|
+
)
|
|
145
149
|
continue
|
|
146
150
|
|
|
147
151
|
# Write to output file
|
|
@@ -156,7 +160,9 @@ def restore_claude_command(
|
|
|
156
160
|
f"({len(turns)} turns, {len(all_lines)} lines)"
|
|
157
161
|
)
|
|
158
162
|
|
|
159
|
-
console.print(
|
|
163
|
+
console.print(
|
|
164
|
+
f"\n[bold green]Restored {restored_count} session(s) to {output_dir}[/bold green]"
|
|
165
|
+
)
|
|
160
166
|
return 0
|
|
161
167
|
|
|
162
168
|
|
|
@@ -252,7 +258,9 @@ def restore_codex_command(
|
|
|
252
258
|
output_dir = Path("/tmp/aline_restore")
|
|
253
259
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
254
260
|
|
|
255
|
-
console.print(
|
|
261
|
+
console.print(
|
|
262
|
+
f"[bold]Restoring {len(selected_sessions)} Codex session(s) to {output_dir}[/bold]\n"
|
|
263
|
+
)
|
|
256
264
|
|
|
257
265
|
restored_count = 0
|
|
258
266
|
for session in selected_sessions:
|
|
@@ -276,7 +284,9 @@ def restore_codex_command(
|
|
|
276
284
|
all_lines.append(line)
|
|
277
285
|
|
|
278
286
|
if not all_lines:
|
|
279
|
-
console.print(
|
|
287
|
+
console.print(
|
|
288
|
+
f" [yellow]Session {session_id[:12]}... has no content, skipping[/yellow]"
|
|
289
|
+
)
|
|
280
290
|
continue
|
|
281
291
|
|
|
282
292
|
# Write to output file
|
|
@@ -291,7 +301,9 @@ def restore_codex_command(
|
|
|
291
301
|
f"({len(turns)} turns, {len(all_lines)} lines)"
|
|
292
302
|
)
|
|
293
303
|
|
|
294
|
-
console.print(
|
|
304
|
+
console.print(
|
|
305
|
+
f"\n[bold green]Restored {restored_count} session(s) to {output_dir}[/bold green]"
|
|
306
|
+
)
|
|
295
307
|
return 0
|
|
296
308
|
|
|
297
309
|
|
realign/commands/search.py
CHANGED
|
@@ -199,9 +199,7 @@ def _grep_search_content(
|
|
|
199
199
|
"""
|
|
200
200
|
# Use raw lines instead of extracting text from JSONL
|
|
201
201
|
match_count = 0
|
|
202
|
-
lines = [
|
|
203
|
-
(i, line) for i, line in enumerate(content.splitlines(), 1) if line.strip()
|
|
204
|
-
]
|
|
202
|
+
lines = [(i, line) for i, line in enumerate(content.splitlines(), 1) if line.strip()]
|
|
205
203
|
|
|
206
204
|
for line_no, text in lines:
|
|
207
205
|
matches = _find_matches(text, pattern, is_regex, ignore_case)
|
|
@@ -269,9 +267,7 @@ def search_command(
|
|
|
269
267
|
ignore_case: bool = typer.Option(
|
|
270
268
|
True, "-i/--case-sensitive", help="Ignore case (default: True)"
|
|
271
269
|
),
|
|
272
|
-
count_only: bool = typer.Option(
|
|
273
|
-
False, "--count", "-c", help="Only show match count"
|
|
274
|
-
),
|
|
270
|
+
count_only: bool = typer.Option(False, "--count", "-c", help="Only show match count"),
|
|
275
271
|
line_numbers: bool = typer.Option(
|
|
276
272
|
True, "-n/--no-line-numbers", help="Show line numbers (default: True)"
|
|
277
273
|
),
|
|
@@ -370,18 +366,14 @@ def search_command(
|
|
|
370
366
|
# Union: sessions from both --sessions and --events
|
|
371
367
|
session_ids = list(set(session_ids) | set(event_session_ids))
|
|
372
368
|
else:
|
|
373
|
-
session_ids = (
|
|
374
|
-
list(set(event_session_ids)) if event_session_ids else None
|
|
375
|
-
)
|
|
369
|
+
session_ids = list(set(event_session_ids)) if event_session_ids else None
|
|
376
370
|
|
|
377
371
|
# Parse turn IDs if provided (for content search)
|
|
378
372
|
turn_ids = _resolve_id_prefixes(db, "turns", turns) or None
|
|
379
373
|
|
|
380
374
|
# 1. Search Events (events don't have session scope, skip if sessions/events filter is active)
|
|
381
375
|
if type in ("all", "event") and not session_ids and not event_ids:
|
|
382
|
-
events = db.search_events(
|
|
383
|
-
query, limit=limit, use_regex=regex, ignore_case=ignore_case
|
|
384
|
-
)
|
|
376
|
+
events = db.search_events(query, limit=limit, use_regex=regex, ignore_case=ignore_case)
|
|
385
377
|
results["events"] = events
|
|
386
378
|
|
|
387
379
|
# 2. Search Turns
|
|
@@ -432,9 +424,7 @@ def search_command(
|
|
|
432
424
|
("desc", event.description),
|
|
433
425
|
]:
|
|
434
426
|
if field_value:
|
|
435
|
-
matches = _find_matches(
|
|
436
|
-
field_value, query, True, ignore_case
|
|
437
|
-
)
|
|
427
|
+
matches = _find_matches(field_value, query, True, ignore_case)
|
|
438
428
|
if matches:
|
|
439
429
|
if not count_only:
|
|
440
430
|
_print_grep_line(
|
|
@@ -457,9 +447,7 @@ def search_command(
|
|
|
457
447
|
("summary", turn.get("summary")),
|
|
458
448
|
]:
|
|
459
449
|
if field_value:
|
|
460
|
-
matches = _find_matches(
|
|
461
|
-
field_value, query, True, ignore_case
|
|
462
|
-
)
|
|
450
|
+
matches = _find_matches(field_value, query, True, ignore_case)
|
|
463
451
|
if matches:
|
|
464
452
|
if not count_only:
|
|
465
453
|
_print_grep_line(
|
|
@@ -484,9 +472,7 @@ def search_command(
|
|
|
484
472
|
("summary", session.session_summary),
|
|
485
473
|
]:
|
|
486
474
|
if field_value:
|
|
487
|
-
matches = _find_matches(
|
|
488
|
-
field_value, query, True, ignore_case
|
|
489
|
-
)
|
|
475
|
+
matches = _find_matches(field_value, query, True, ignore_case)
|
|
490
476
|
if matches:
|
|
491
477
|
if not count_only:
|
|
492
478
|
_print_grep_line(
|
|
@@ -549,9 +535,7 @@ def search_command(
|
|
|
549
535
|
|
|
550
536
|
# -- Events Output --
|
|
551
537
|
if results.get("events"):
|
|
552
|
-
console.print(
|
|
553
|
-
f"[bold cyan]Events ({len(results['events'])})[/bold cyan]"
|
|
554
|
-
)
|
|
538
|
+
console.print(f"[bold cyan]Events ({len(results['events'])})[/bold cyan]")
|
|
555
539
|
for event in results["events"]:
|
|
556
540
|
console.print(f"• [bold]{event.title}[/bold] (ID: {event.id[:8]})")
|
|
557
541
|
if event.description:
|
|
@@ -572,15 +556,11 @@ def search_command(
|
|
|
572
556
|
|
|
573
557
|
# -- Turns Output --
|
|
574
558
|
if results.get("turns"):
|
|
575
|
-
console.print(
|
|
576
|
-
f"[bold green]Turns ({len(results['turns'])})[/bold green]"
|
|
577
|
-
)
|
|
559
|
+
console.print(f"[bold green]Turns ({len(results['turns'])})[/bold green]")
|
|
578
560
|
for turn in results["turns"]:
|
|
579
561
|
title = turn.get("title") or "(No Title)"
|
|
580
562
|
summary = turn.get("summary") or ""
|
|
581
|
-
console.print(
|
|
582
|
-
f"• [bold]{title}[/bold] (Turn #{turn['turn_number']})"
|
|
583
|
-
)
|
|
563
|
+
console.print(f"• [bold]{title}[/bold] (Turn #{turn['turn_number']})")
|
|
584
564
|
if summary:
|
|
585
565
|
console.print(f" {summary}")
|
|
586
566
|
console.print(f" [dim]ID: {turn['turn_id'][:8]}[/dim]")
|
|
@@ -591,18 +571,14 @@ def search_command(
|
|
|
591
571
|
|
|
592
572
|
# -- Sessions Output --
|
|
593
573
|
if results.get("sessions"):
|
|
594
|
-
console.print(
|
|
595
|
-
f"[bold magenta]Sessions ({len(results['sessions'])})[/bold magenta]"
|
|
596
|
-
)
|
|
574
|
+
console.print(f"[bold magenta]Sessions ({len(results['sessions'])})[/bold magenta]")
|
|
597
575
|
for session in results["sessions"]:
|
|
598
576
|
title = session.session_title or "(No Title)"
|
|
599
577
|
summary = session.session_summary or ""
|
|
600
578
|
console.print(f"• [bold]{title}[/bold] (ID: {session.id[:8]})")
|
|
601
579
|
if summary:
|
|
602
580
|
# Truncate long summaries
|
|
603
|
-
summary_preview = (
|
|
604
|
-
summary[:200] + "..." if len(summary) > 200 else summary
|
|
605
|
-
)
|
|
581
|
+
summary_preview = summary[:200] + "..." if len(summary) > 200 else summary
|
|
606
582
|
console.print(f" {summary_preview}")
|
|
607
583
|
console.print("")
|
|
608
584
|
elif type == "session" or (type == "all" and not results.get("sessions")):
|
|
@@ -611,14 +587,10 @@ def search_command(
|
|
|
611
587
|
|
|
612
588
|
# -- Content Output --
|
|
613
589
|
if results.get("content"):
|
|
614
|
-
console.print(
|
|
615
|
-
f"[bold yellow]Content ({len(results['content'])})[/bold yellow]"
|
|
616
|
-
)
|
|
590
|
+
console.print(f"[bold yellow]Content ({len(results['content'])})[/bold yellow]")
|
|
617
591
|
for item in results["content"]:
|
|
618
592
|
title = item.get("title") or "(No Title)"
|
|
619
|
-
console.print(
|
|
620
|
-
f"• [bold]{title}[/bold] (Turn #{item['turn_number']})"
|
|
621
|
-
)
|
|
593
|
+
console.print(f"• [bold]{title}[/bold] (Turn #{item['turn_number']})")
|
|
622
594
|
console.print(f" [dim]ID: {item['turn_id'][:8]}[/dim]")
|
|
623
595
|
if verbose:
|
|
624
596
|
console.print(Markdown(item["content_preview"]))
|