aline-ai 0.5.4__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.
Files changed (79) hide show
  1. {aline_ai-0.5.4.dist-info → aline_ai-0.5.5.dist-info}/METADATA +1 -1
  2. aline_ai-0.5.5.dist-info/RECORD +93 -0
  3. realign/__init__.py +1 -1
  4. realign/adapters/antigravity.py +28 -20
  5. realign/adapters/base.py +46 -50
  6. realign/adapters/claude.py +14 -14
  7. realign/adapters/codex.py +7 -7
  8. realign/adapters/gemini.py +11 -11
  9. realign/adapters/registry.py +14 -10
  10. realign/claude_detector.py +2 -2
  11. realign/claude_hooks/__init__.py +3 -3
  12. realign/claude_hooks/permission_request_hook_installer.py +31 -32
  13. realign/claude_hooks/stop_hook.py +4 -1
  14. realign/claude_hooks/stop_hook_installer.py +30 -31
  15. realign/cli.py +7 -0
  16. realign/codex_detector.py +11 -11
  17. realign/commands/add.py +88 -65
  18. realign/commands/config.py +3 -12
  19. realign/commands/context.py +3 -1
  20. realign/commands/export_shares.py +86 -127
  21. realign/commands/import_shares.py +145 -155
  22. realign/commands/init.py +166 -30
  23. realign/commands/restore.py +18 -6
  24. realign/commands/search.py +14 -42
  25. realign/commands/upgrade.py +155 -11
  26. realign/commands/watcher.py +98 -219
  27. realign/commands/worker.py +29 -6
  28. realign/config.py +25 -20
  29. realign/context.py +1 -3
  30. realign/dashboard/app.py +4 -4
  31. realign/dashboard/screens/create_event.py +3 -1
  32. realign/dashboard/screens/event_detail.py +14 -6
  33. realign/dashboard/screens/session_detail.py +3 -1
  34. realign/dashboard/screens/share_import.py +7 -3
  35. realign/dashboard/tmux_manager.py +54 -9
  36. realign/dashboard/widgets/config_panel.py +85 -1
  37. realign/dashboard/widgets/events_table.py +3 -1
  38. realign/dashboard/widgets/header.py +1 -0
  39. realign/dashboard/widgets/search_panel.py +37 -27
  40. realign/dashboard/widgets/sessions_table.py +24 -15
  41. realign/dashboard/widgets/terminal_panel.py +66 -22
  42. realign/dashboard/widgets/watcher_panel.py +6 -2
  43. realign/dashboard/widgets/worker_panel.py +10 -1
  44. realign/db/__init__.py +1 -1
  45. realign/db/base.py +5 -15
  46. realign/db/locks.py +0 -1
  47. realign/db/migration.py +82 -76
  48. realign/db/schema.py +2 -6
  49. realign/db/sqlite_db.py +23 -41
  50. realign/events/__init__.py +0 -1
  51. realign/events/event_summarizer.py +27 -15
  52. realign/events/session_summarizer.py +29 -15
  53. realign/file_lock.py +1 -0
  54. realign/hooks.py +150 -60
  55. realign/logging_config.py +12 -15
  56. realign/mcp_server.py +30 -51
  57. realign/mcp_watcher.py +0 -1
  58. realign/models/event.py +29 -20
  59. realign/prompts/__init__.py +7 -7
  60. realign/prompts/presets.py +15 -11
  61. realign/redactor.py +99 -59
  62. realign/triggers/__init__.py +9 -9
  63. realign/triggers/antigravity_trigger.py +30 -28
  64. realign/triggers/base.py +4 -3
  65. realign/triggers/claude_trigger.py +104 -85
  66. realign/triggers/codex_trigger.py +15 -5
  67. realign/triggers/gemini_trigger.py +57 -47
  68. realign/triggers/next_turn_trigger.py +3 -1
  69. realign/triggers/registry.py +6 -2
  70. realign/triggers/turn_status.py +3 -1
  71. realign/watcher_core.py +306 -131
  72. realign/watcher_daemon.py +8 -8
  73. realign/worker_core.py +3 -1
  74. realign/worker_daemon.py +3 -1
  75. aline_ai-0.5.4.dist-info/RECORD +0 -93
  76. {aline_ai-0.5.4.dist-info → aline_ai-0.5.5.dist-info}/WHEEL +0 -0
  77. {aline_ai-0.5.4.dist-info → aline_ai-0.5.5.dist-info}/entry_points.txt +0 -0
  78. {aline_ai-0.5.4.dist-info → aline_ai-0.5.5.dist-info}/licenses/LICENSE +0 -0
  79. {aline_ai-0.5.4.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 ReAlignConfig, get_default_config_content, generate_user_id, generate_random_username
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
- TMUX_CONF = """# Aline tmux config
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' 'bind -T copy-mode-vi MouseDragEnd1Pane send -X copy-pipe-and-cancel "pbcopy"; bind -T copy-mode MouseDragEnd1Pane send -X copy-pipe-and-cancel "pbcopy"' ''
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(TMUX_CONF, encoding="utf-8")
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 _install_skill_to_path, SKILLS_REGISTRY
417
-
418
- skill_root = Path.home() / ".claude" / "skills"
523
+ from .add import add_skills_command
419
524
 
420
- for skill_name, skill_content in SKILLS_REGISTRY.items():
421
- skill_path = skill_root / skill_name / "SKILL.md"
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 ensure_user_prompt_submit_hook_installed
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 ensure_permission_request_hook_installed
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(get_default_config_content(), encoding="utf-8")
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("ReAlign needs to identify you for tracking session ownership.\n")
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("Enter your username (or press Enter for auto-generated): ").strip()
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(f"Auto-generated username: [yellow]{config.user_name}[/yellow]")
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(f"[green]✓[/green] User identity saved: [bold]{config.user_name}[/bold] ([dim]{config.user_id[:8]}...[/dim])\n")
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"] = "Aline initialized successfully (global config + database + prompts + tmux + skills + hooks ready)"
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(False, "--force", "-f", help="Overwrite global config with defaults"),
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 = (watcher_start_exit == 0)
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 = (worker_start_exit == 0)
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(" Logs: [cyan]~/.aline/.logs/watcher_*.log[/cyan]", style="dim")
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(" Logs: [cyan]~/.aline/.logs/worker_*.log[/cyan]", style="dim")
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(" 1. Start Claude Code or Codex - sessions are tracked automatically", style="dim")
676
- console.print(" 2. Search history with: [cyan]aline search <query>[/cyan]", style="dim")
677
- console.print(" 3. Customize prompts (optional): [cyan]~/.aline/prompts/[/cyan]", style="dim")
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):
@@ -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(f"[bold]Restoring {len(selected_sessions)} Claude session(s) to {output_dir}[/bold]\n")
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(f" [yellow]Session {session_id[:12]}... has no content, skipping[/yellow]")
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(f"\n[bold green]Restored {restored_count} session(s) to {output_dir}[/bold green]")
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(f"[bold]Restoring {len(selected_sessions)} Codex session(s) to {output_dir}[/bold]\n")
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(f" [yellow]Session {session_id[:12]}... has no content, skipping[/yellow]")
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(f"\n[bold green]Restored {restored_count} session(s) to {output_dir}[/bold green]")
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
 
@@ -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"]))