aline-ai 0.5.4__py3-none-any.whl → 0.5.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.
Files changed (82) hide show
  1. {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/METADATA +1 -1
  2. aline_ai-0.5.6.dist-info/RECORD +95 -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 +23 -4
  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 +34 -24
  31. realign/dashboard/screens/__init__.py +10 -1
  32. realign/dashboard/screens/create_agent.py +244 -0
  33. realign/dashboard/screens/create_event.py +3 -1
  34. realign/dashboard/screens/event_detail.py +14 -6
  35. realign/dashboard/screens/help_screen.py +114 -0
  36. realign/dashboard/screens/session_detail.py +3 -1
  37. realign/dashboard/screens/share_import.py +7 -3
  38. realign/dashboard/tmux_manager.py +54 -9
  39. realign/dashboard/widgets/config_panel.py +85 -1
  40. realign/dashboard/widgets/events_table.py +314 -70
  41. realign/dashboard/widgets/header.py +2 -1
  42. realign/dashboard/widgets/search_panel.py +37 -27
  43. realign/dashboard/widgets/sessions_table.py +404 -85
  44. realign/dashboard/widgets/terminal_panel.py +155 -175
  45. realign/dashboard/widgets/watcher_panel.py +6 -2
  46. realign/dashboard/widgets/worker_panel.py +10 -1
  47. realign/db/__init__.py +1 -1
  48. realign/db/base.py +5 -15
  49. realign/db/locks.py +0 -1
  50. realign/db/migration.py +82 -76
  51. realign/db/schema.py +2 -6
  52. realign/db/sqlite_db.py +23 -41
  53. realign/events/__init__.py +0 -1
  54. realign/events/event_summarizer.py +27 -15
  55. realign/events/session_summarizer.py +29 -15
  56. realign/file_lock.py +1 -0
  57. realign/hooks.py +150 -60
  58. realign/logging_config.py +12 -15
  59. realign/mcp_server.py +30 -51
  60. realign/mcp_watcher.py +0 -1
  61. realign/models/event.py +29 -20
  62. realign/prompts/__init__.py +7 -7
  63. realign/prompts/presets.py +15 -11
  64. realign/redactor.py +99 -59
  65. realign/triggers/__init__.py +9 -9
  66. realign/triggers/antigravity_trigger.py +30 -28
  67. realign/triggers/base.py +4 -3
  68. realign/triggers/claude_trigger.py +104 -85
  69. realign/triggers/codex_trigger.py +15 -5
  70. realign/triggers/gemini_trigger.py +57 -47
  71. realign/triggers/next_turn_trigger.py +3 -1
  72. realign/triggers/registry.py +6 -2
  73. realign/triggers/turn_status.py +3 -1
  74. realign/watcher_core.py +306 -131
  75. realign/watcher_daemon.py +8 -8
  76. realign/worker_core.py +3 -1
  77. realign/worker_daemon.py +3 -1
  78. aline_ai-0.5.4.dist-info/RECORD +0 -93
  79. {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/WHEEL +0 -0
  80. {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/entry_points.txt +0 -0
  81. {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/licenses/LICENSE +0 -0
  82. {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aline-ai
3
- Version: 0.5.4
3
+ Version: 0.5.6
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -0,0 +1,95 @@
1
+ aline_ai-0.5.6.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
+ realign/__init__.py,sha256=0koKhfclqNXk2boQRfbLPBFyEmPnS5B07UmqTJ-o7iU,1623
3
+ realign/claude_detector.py,sha256=ZLSJacMo6zzQclXByABKA70UNpstxqIv3fPGqdpA934,2792
4
+ realign/cli.py,sha256=yeq_a3Peoqx8N13Jo2etjJtbTCZYpuqwoMMyAPdrANs,30569
5
+ realign/codex_detector.py,sha256=N9ulgMgvTzDfXE4s4vLd6OoS0hT7R6h2bDFFXWa-2hE,4183
6
+ realign/config.py,sha256=lIKZqeOwYc_gHo760lYYX6PnapuKrCWGqT5SA8-PbeA,12044
7
+ realign/context.py,sha256=S1YEUn5HWSDTerDDMsSsRV871IZxoaxDjPTPI2z6-Xs,9976
8
+ realign/file_lock.py,sha256=kLNm1Rra4TCrTMyPM5fwjVascq-CUz2Bzh9HHKtCKOE,3444
9
+ realign/hooks.py,sha256=NR4LgWgkA6npW_B68I7OdCaZNWseYSP7ZbK4Sl5nnTo,74692
10
+ realign/llm_client.py,sha256=KPfJScQvqse-Tm-VpqnZ6C5jvajPl2n4Ddz9sUp7WIY,24564
11
+ realign/logging_config.py,sha256=LCAigKFhTj86PSJm4-kUl3Ag9h_GENh3x2iPnMv7qUI,4871
12
+ realign/mcp_server.py,sha256=LWiQ2qukYoNLsoV2ID2f0vF9jkJlBvB587HpM5jymgE,10193
13
+ realign/mcp_watcher.py,sha256=aK4jWStv7CoCroS4tXFHgZ_y_-q4QDjrpWgm4DxcEj4,1260
14
+ realign/redactor.py,sha256=Zsoi5HfYak2yPmck20JArhm-1cPSB78IdkBJiNVXfrc,17096
15
+ realign/watcher_core.py,sha256=zM6ABnc9WoyPQ7GxiciMTwRyOeC6k5_XGTcI4eqx7TI,106961
16
+ realign/watcher_daemon.py,sha256=AVOMXrlVVy7Rlx3Yfib4e-KLszIR7CLdSHpdoxDRp8c,3090
17
+ realign/worker_core.py,sha256=-GOItHE0vzExB8LZK6KeHx4tt_mIqtCoUljOtEg2x8A,10105
18
+ realign/worker_daemon.py,sha256=LpJbQDY0Z4AMtq0LmpxvFeQM4puuoGDRBayKRafvKhc,3574
19
+ realign/adapters/__init__.py,sha256=bpDm5aBxMdq4OA_beYahoUb4zfNaq3KOG6KghQJruRc,827
20
+ realign/adapters/antigravity.py,sha256=geaYxAEswpgsVtERqsQ1OwvPFsy5tRkyjx2yQ-Uq9nM,5461
21
+ realign/adapters/base.py,sha256=2IdAZKGjg5gPB3YLf_8r3V4XAdbK7fHpj06GjjsYEFY,7409
22
+ realign/adapters/claude.py,sha256=sZD-W8nAOF-2jWwgEmJpnT2EpbVbF0NfSbnnW1X0wtg,5060
23
+ realign/adapters/codex.py,sha256=5ex3zJ5Hpb_StV2CcBSHVhHleygZxzVJjYsWw8qK1Bc,2051
24
+ realign/adapters/gemini.py,sha256=NvtXQPWUtEY-DaAAMvLGvQW4FalTG-g0pD514HYnzF0,2540
25
+ realign/adapters/registry.py,sha256=yM6nf9nGTJ1vaK2Uixp-VacseK7PmxZkCdKedmWI8MA,3255
26
+ realign/claude_hooks/__init__.py,sha256=MT9c8TWjLO23xDCM-uBBMy_mOThNd7O-AgN_Khn30qs,594
27
+ realign/claude_hooks/permission_request_hook.py,sha256=jMN7UtL6bMqHObUCP5A5ysvFrooDEcd9KxtmF2-3nCw,6448
28
+ realign/claude_hooks/permission_request_hook_installer.py,sha256=_8Wr_L5MES7iGukJzcaj4bqR0BH8kFL44U_X4iKtw2Y,7791
29
+ realign/claude_hooks/stop_hook.py,sha256=2nzF2aF1p5teMJ0eV0ALEHD1K-yVj5sSh7UE8xL54ZE,12025
30
+ realign/claude_hooks/stop_hook_installer.py,sha256=uyqKOqpix7CQP64ERBvvh7viSPp_wx_JVGNAX18rKh0,7228
31
+ realign/claude_hooks/terminal_state.py,sha256=ZvdQ-ZmqEltdMoNk3lXVsbpvbAQEmf2hxTCY_8WFu9g,2586
32
+ realign/claude_hooks/user_prompt_submit_hook.py,sha256=WD-UavhBTueN2TPfnZrnPC7DFYGEeptjUEF21EJn7Qo,10312
33
+ realign/claude_hooks/user_prompt_submit_hook_installer.py,sha256=2xLF8yZcE7Iwib9gU-xCkA1NWxNH9Nc5CFKPYK7rtXw,5371
34
+ realign/commands/__init__.py,sha256=sx_ck55oxaoiF4N3LugG0ZXwonUDxeEZ5uHbBKCC7K8,89
35
+ realign/commands/add.py,sha256=njZgg3paUmOw-sb-sWkXr_eUaf5bD-hBEiRectaphPs,24332
36
+ realign/commands/config.py,sha256=nYnu_h2pk7GODcrzrV04K51D-s7v06FlRXHJ0HJ-gvU,6732
37
+ realign/commands/context.py,sha256=pM2KfZHVkB-ou4nBhFvKSwnYliLBzwN3zerLyBAbhfE,7095
38
+ realign/commands/export_shares.py,sha256=Djy1aO7MoU1_ewzn6CZ43oNhSEEonV3sTkSQbHgiaKI,135806
39
+ realign/commands/import_shares.py,sha256=ukX8huvLvEM5g0qEIoqrV1-imz1g-r0Jj2FqD-ojrIA,25297
40
+ realign/commands/init.py,sha256=0wjutL1DxA0hYMt1aCfgUSf91ZBExXCXFrwuOyQNHLg,32060
41
+ realign/commands/restore.py,sha256=s2BxQZHxQw9r12NzRVsK20KlGafy5AIoSjWMo5PcnHY,11173
42
+ realign/commands/search.py,sha256=RUdseQsjy-SNfKFkGLWrE4IhxkzgkW9IIxAX33XnCHk,24589
43
+ realign/commands/upgrade.py,sha256=L3PLOUIN5qAQTbkfoVtSsIbbzEezA_xjjk9F1GMVfjw,12781
44
+ realign/commands/watcher.py,sha256=fWL3kaRkqE03-NtFLaXlx93hJAQrAuNPSoYhOyQZfq8,136273
45
+ realign/commands/worker.py,sha256=K1DG1uZ--ebKwklHCyIFdN_axoLjL9Onx8Naq-DOZBs,23078
46
+ realign/dashboard/__init__.py,sha256=QZkHTsGityH8UkF8rmvA3xW7dMXNe0swEWr443qfgCM,128
47
+ realign/dashboard/app.py,sha256=LPgDXCqXHtJXzgFiwJvWolLJZ34EU4gejLrBIrgEk4Y,12233
48
+ realign/dashboard/tmux_manager.py,sha256=DdCiumQ7YQZnje5VfOQ60585C0X6Va_AhBQi_zmhE0Y,24035
49
+ realign/dashboard/screens/__init__.py,sha256=US6sAmQs5VVkH2tFkH_z0WDT4H8cVhLL-JckfSR1yQY,446
50
+ realign/dashboard/screens/create_agent.py,sha256=ugEs3IHrT7FsbuMEwyrqY3eoylp_pbftw42_Fu07tF4,7419
51
+ realign/dashboard/screens/create_event.py,sha256=oiQY1zKpUYnQU-5fQLeuZH9BV5NClE5B5XZIVBYG5A8,5506
52
+ realign/dashboard/screens/event_detail.py,sha256=OLaL3-FgAohDdzVlfuUw5yh2SR49IHIpCtiqXJhBTc0,20992
53
+ realign/dashboard/screens/help_screen.py,sha256=Icrcvbgyz49R2tBiu8vBZ4CLm6iYclv_-FTa2pCFRRQ,3398
54
+ realign/dashboard/screens/session_detail.py,sha256=gfpUIhMO00ecMlMyzpkxDdvGb9zhESEvxwrJvqLuHOI,9603
55
+ realign/dashboard/screens/share_import.py,sha256=hl2x0yGVycsoUI76AmdZTAV-br3Q6191g5xHHrZ8hOA,6318
56
+ realign/dashboard/styles/dashboard.tcss,sha256=9sSIs3r4V8eeTwCK56s7fnYxjMEuASP8EcmK1fhpUmA,3454
57
+ realign/dashboard/widgets/__init__.py,sha256=3Pf2_K9obrertgv_psfxradgkI9RXlmjoXYQH7oBKm0,583
58
+ realign/dashboard/widgets/config_panel.py,sha256=Afezfd6nvHo0Q44IS2UZTPJsYmHbqzjx7bi5jWrCDPA,11182
59
+ realign/dashboard/widgets/events_table.py,sha256=OG9RjwU4c50-RUMmdhXzmIMnYrt6_mCP1GNQWDAX95s,30368
60
+ realign/dashboard/widgets/header.py,sha256=0HHCFXX7F3C6HII-WDwOJwWkJrajmKPWmdoMWyOkn9E,1587
61
+ realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
62
+ realign/dashboard/widgets/search_panel.py,sha256=ZNJDfwDSxUFnCeltYQYsQsPJ6t4HDeNWpENoTOoBdVM,8951
63
+ realign/dashboard/widgets/sessions_table.py,sha256=toHE96RLwddqXE9Ykocy6loqoGld_6gFawLwdiiJ2cA,32877
64
+ realign/dashboard/widgets/terminal_panel.py,sha256=uXgPcgjWaQ2tTD6Mx6ikCXzq6wYqh-ft0Bait83_DKE,28290
65
+ realign/dashboard/widgets/watcher_panel.py,sha256=O_mdDacgc87xA-5KEfta53Ik_Xsk_B2OfwenMOTtGw8,19722
66
+ realign/dashboard/widgets/worker_panel.py,sha256=F_jKWABuCNmjQgeeuCr4KnFRKdY4CLTNcEXMYwsNaSk,18691
67
+ realign/db/__init__.py,sha256=-1d-Zc4IOUVokbdTXi3R-bIwlkFEPAz_qTHAdcsdp6g,1870
68
+ realign/db/base.py,sha256=4OkwPi6qL_8ZJb1ATNkHr-JaIxh98UYTSZ6fSYFff6s,12033
69
+ realign/db/locks.py,sha256=yzCiPJZ4eOQX-Q4mXB6s76U2U7lXAzIBBy1t59w-AVU,1698
70
+ realign/db/migration.py,sha256=af1QFEfIh_qX0pFyXzm5gWFVbQn0sKOUNLSJHlr__FU,13405
71
+ realign/db/schema.py,sha256=Qj8nRs7plc8MXXTq7D4vi4L0joaiEjaI0mZMzUC4z78,18066
72
+ realign/db/sqlite_db.py,sha256=UmUjo3OW7F6YEeOSdl0-fGOXNFn_tC7d3EYEEUNzNZU,81793
73
+ realign/events/__init__.py,sha256=IM-NxF4Zk2hYFD07k4WrfNRuuiC9ihGjf4GBpJhjd2E,35
74
+ realign/events/debouncer.py,sha256=U3Q7dYpnMsAgWsW_E_IbSC4lrdEoi6H_SFLGLOAazs4,3062
75
+ realign/events/event_summarizer.py,sha256=ZLiwOXWN8eawep3cQs3Wh9QLSypvU1SRbe8GTJXJQaY,8272
76
+ realign/events/session_summarizer.py,sha256=IWYcDHGbsPtZEeDcQMWy4V-IKi5QBqpA5uuOIGy4Sls,10386
77
+ realign/models/event.py,sha256=ypz74D4l6U2U0RhgL8fzEhiq7iQjhHybmAdLUNDY7P4,5521
78
+ realign/prompts/__init__.py,sha256=PpYR7f-T96fd-QyNYJDRS1U6h9O0rIt_SMsREy9i3aA,443
79
+ realign/prompts/presets.py,sha256=h9oEy0XP4JQ4DCnp8HN_FfF0LmI-yOV6xWJLknIghJ8,7256
80
+ realign/tracker/__init__.py,sha256=Apd-xxomkiOjSIthseqZpVQ8l0yT4nkaErsUyA1ptIE,161
81
+ realign/triggers/__init__.py,sha256=esF-rMxaxzKDAEX4eLJRdUcVkpSU2_r6U37igS7jrIQ,674
82
+ realign/triggers/antigravity_trigger.py,sha256=sj9OKu2TihNlgOAd2B9XLy5wfJpY5VfJSast7Krl4bg,5195
83
+ realign/triggers/base.py,sha256=Q72nlPMnCB3SP14gd4hm6AjS0mVyq5lbtlLUxSxAvrY,4148
84
+ realign/triggers/claude_trigger.py,sha256=-LY2sR-FTEXlCBx8SOI6TxP5-RI2BtlfzqvLeT5U3mw,20065
85
+ realign/triggers/codex_trigger.py,sha256=X8WNphoav86XNd05SJBoxpHySlFtEYCrjsEF-b6pw8M,13967
86
+ realign/triggers/gemini_trigger.py,sha256=878GDjxuJ8WCXEBbCLSN2k2l_3BSGzvykhZi3gfIOqw,7939
87
+ realign/triggers/next_turn_trigger.py,sha256=BpP0PWn4mU1MZd6mv89jWcjs8Jtv0zEWapW32O0wcHk,4333
88
+ realign/triggers/registry.py,sha256=cb-AVLbYB2pqwfWL3q1DQxLv4kOw7g7m-GshTdfFESc,3827
89
+ realign/triggers/turn_status.py,sha256=wAZEhXDAmDoX5F-ohWfSnZZ0eA6DAJ9svSPiSv_f6sg,6041
90
+ realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
91
+ aline_ai-0.5.6.dist-info/METADATA,sha256=F8huCWnwasj2O7OA4fWD7o5s8HNT_0ErOSJEkLOefQc,1597
92
+ aline_ai-0.5.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
93
+ aline_ai-0.5.6.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
94
+ aline_ai-0.5.6.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
95
+ aline_ai-0.5.6.dist-info/RECORD,,
realign/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  import hashlib
4
4
  from pathlib import Path
5
5
 
6
- __version__ = "0.5.4"
6
+ __version__ = "0.5.6"
7
7
 
8
8
 
9
9
  def get_realign_dir(project_root: Path) -> Path:
@@ -16,10 +16,10 @@ from ..triggers.antigravity_trigger import AntigravityTrigger
16
16
 
17
17
  class AntigravityAdapter(SessionAdapter):
18
18
  """Adapter for Antigravity IDE sessions."""
19
-
19
+
20
20
  name = "antigravity"
21
21
  trigger_class = AntigravityTrigger
22
-
22
+
23
23
  def discover_sessions(self) -> List[Path]:
24
24
  """
25
25
  Find all active Antigravity IDE sessions.
@@ -27,38 +27,40 @@ class AntigravityAdapter(SessionAdapter):
27
27
  """
28
28
  sessions = []
29
29
  gemini_brain = Path.home() / ".gemini" / "antigravity" / "brain"
30
-
30
+
31
31
  if not gemini_brain.exists():
32
32
  return sessions
33
-
33
+
34
34
  try:
35
35
  for conv_dir in gemini_brain.iterdir():
36
36
  if not conv_dir.is_dir():
37
37
  continue
38
-
38
+
39
39
  # Check for key artifacts
40
- has_artifacts = any((conv_dir / filename).exists()
41
- for filename in ["task.md", "walkthrough.md", "implementation_plan.md"])
42
-
40
+ has_artifacts = any(
41
+ (conv_dir / filename).exists()
42
+ for filename in ["task.md", "walkthrough.md", "implementation_plan.md"]
43
+ )
44
+
43
45
  if has_artifacts:
44
46
  sessions.append(conv_dir)
45
47
  except Exception:
46
48
  pass
47
-
49
+
48
50
  return sessions
49
-
51
+
50
52
  def discover_sessions_for_project(self, project_path: Path) -> List[Path]:
51
53
  """
52
54
  Find sessions for a specific project.
53
55
  """
54
56
  all_sessions = self.discover_sessions()
55
57
  project_sessions = []
56
-
58
+
57
59
  for session in all_sessions:
58
60
  extracted_path = self.extract_project_path(session)
59
61
  if extracted_path and extracted_path == project_path:
60
62
  project_sessions.append(session)
61
-
63
+
62
64
  return project_sessions
63
65
 
64
66
  def extract_project_path(self, session_file: Path) -> Optional[Path]:
@@ -117,34 +119,40 @@ class AntigravityAdapter(SessionAdapter):
117
119
  def get_session_metadata(self, session_file: Path) -> Dict[str, Any]:
118
120
  """Extract rich metadata from Antigravity brain artifacts."""
119
121
  metadata = super().get_session_metadata(session_file)
120
-
122
+
121
123
  if not session_file.exists():
122
124
  return metadata
123
-
125
+
124
126
  session_dir = session_file if session_file.is_dir() else session_file.parent
125
-
127
+
126
128
  try:
127
129
  # We just return the turn count (always 1 if exists)
128
130
  # No task progress parsing required
129
131
  metadata["turn_count"] = self.trigger.count_complete_turns(session_file)
130
-
132
+
131
133
  except Exception:
132
134
  pass
133
-
135
+
134
136
  return metadata
135
137
 
136
138
  def is_session_valid(self, session_file: Path) -> bool:
137
139
  """Check if this is an Antigravity artifact directory."""
138
140
  if not session_file.is_dir():
139
- if session_file.parent.name == "brain" and session_file.parent.parent.name == "antigravity":
141
+ if (
142
+ session_file.parent.name == "brain"
143
+ and session_file.parent.parent.name == "antigravity"
144
+ ):
140
145
  # It's a directory inside brain
141
146
  return True
142
147
  return False
143
-
148
+
144
149
  # Check parent hierarchy
145
150
  # .../.gemini/antigravity/brain/<uuid>
146
151
  try:
147
- if session_file.parent.name == "brain" and session_file.parent.parent.name == "antigravity":
152
+ if (
153
+ session_file.parent.name == "brain"
154
+ and session_file.parent.parent.name == "antigravity"
155
+ ):
148
156
  return True
149
157
  except Exception:
150
158
  pass
realign/adapters/base.py CHANGED
@@ -21,152 +21,150 @@ logger = logging.getLogger(__name__)
21
21
  class SessionAdapter(ABC):
22
22
  """
23
23
  Abstract base class for CLI session adapters.
24
-
24
+
25
25
  Each adapter handles all CLI-specific logic:
26
26
  1. Discovering active sessions
27
27
  2. Extracting project paths from sessions
28
28
  3. Turn detection (delegated to TurnTrigger)
29
29
  4. Session metadata extraction
30
-
30
+
31
31
  Subclasses must implement:
32
32
  - name: Unique identifier for this adapter
33
33
  - trigger_class: The TurnTrigger class to use
34
34
  - discover_sessions(): Find all active sessions
35
35
  - extract_project_path(): Extract project path from a session
36
36
  """
37
-
37
+
38
38
  # Class attributes to be defined by subclasses
39
39
  name: str = "" # e.g., "claude", "codex", "gemini"
40
40
  trigger_class: Optional[Type[TurnTrigger]] = None
41
-
41
+
42
42
  def __init__(self, config: Optional[Dict[str, Any]] = None):
43
43
  """
44
44
  Initialize the adapter.
45
-
45
+
46
46
  Args:
47
47
  config: Optional configuration dictionary
48
48
  """
49
49
  self.config = config or {}
50
50
  self._trigger: Optional[TurnTrigger] = None
51
-
51
+
52
52
  @property
53
53
  def trigger(self) -> TurnTrigger:
54
54
  """Get or create the trigger instance (lazy initialization)."""
55
55
  if self._trigger is None:
56
56
  if self.trigger_class is None:
57
- raise NotImplementedError(
58
- f"{self.__class__.__name__} must define trigger_class"
59
- )
57
+ raise NotImplementedError(f"{self.__class__.__name__} must define trigger_class")
60
58
  self._trigger = self.trigger_class(self.config)
61
59
  return self._trigger
62
-
60
+
63
61
  # ========================================================================
64
62
  # Abstract methods - must be implemented by subclasses
65
63
  # ========================================================================
66
-
64
+
67
65
  @abstractmethod
68
66
  def discover_sessions(self) -> List[Path]:
69
67
  """
70
68
  Discover all active sessions for this CLI.
71
-
69
+
72
70
  Returns:
73
71
  List of session file paths
74
72
  """
75
73
  pass
76
-
74
+
77
75
  @abstractmethod
78
76
  def discover_sessions_for_project(self, project_path: Path) -> List[Path]:
79
77
  """
80
78
  Discover sessions for a specific project.
81
-
79
+
82
80
  Args:
83
81
  project_path: Path to the project root
84
-
82
+
85
83
  Returns:
86
84
  List of session file paths for this project
87
85
  """
88
86
  pass
89
-
87
+
90
88
  @abstractmethod
91
89
  def extract_project_path(self, session_file: Path) -> Optional[Path]:
92
90
  """
93
91
  Extract the project path from a session file.
94
-
92
+
95
93
  Args:
96
94
  session_file: Path to the session file
97
-
95
+
98
96
  Returns:
99
97
  Path to the project, or None if cannot be determined
100
98
  """
101
99
  pass
102
-
100
+
103
101
  # ========================================================================
104
102
  # Delegated methods - use the trigger for turn detection
105
103
  # ========================================================================
106
-
104
+
107
105
  def count_turns(self, session_file: Path) -> int:
108
106
  """
109
107
  Count complete turns in the session.
110
-
108
+
111
109
  Delegates to the trigger's count_complete_turns method.
112
-
110
+
113
111
  Args:
114
112
  session_file: Path to session file
115
-
113
+
116
114
  Returns:
117
115
  Number of complete turns
118
116
  """
119
117
  return self.trigger.count_complete_turns(session_file)
120
-
118
+
121
119
  def is_turn_complete(self, session_file: Path, turn_number: int) -> bool:
122
120
  """
123
121
  Check if a specific turn is complete.
124
-
122
+
125
123
  Args:
126
124
  session_file: Path to session file
127
125
  turn_number: Turn number (1-based)
128
-
126
+
129
127
  Returns:
130
128
  True if the turn is complete
131
129
  """
132
130
  return self.trigger.is_turn_complete(session_file, turn_number)
133
-
131
+
134
132
  def extract_turn_info(self, session_file: Path, turn_number: int) -> Optional[TurnInfo]:
135
133
  """
136
134
  Extract information for a specific turn.
137
-
135
+
138
136
  Args:
139
137
  session_file: Path to session file
140
138
  turn_number: Turn number (1-based)
141
-
139
+
142
140
  Returns:
143
141
  TurnInfo object, or None if turn doesn't exist
144
142
  """
145
143
  return self.trigger.extract_turn_info(session_file, turn_number)
146
-
144
+
147
145
  def detect_session_format(self, session_file: Path) -> Optional[str]:
148
146
  """
149
147
  Detect the session file format.
150
-
148
+
151
149
  Args:
152
150
  session_file: Path to session file
153
-
151
+
154
152
  Returns:
155
153
  Format string, or None if unrecognized
156
154
  """
157
155
  return self.trigger.detect_session_format(session_file)
158
-
156
+
159
157
  # ========================================================================
160
158
  # Optional methods - can be overridden by subclasses
161
159
  # ========================================================================
162
-
160
+
163
161
  def get_session_metadata(self, session_file: Path) -> Dict[str, Any]:
164
162
  """
165
163
  Get metadata about a session file.
166
-
164
+
167
165
  Args:
168
166
  session_file: Path to session file
169
-
167
+
170
168
  Returns:
171
169
  Dictionary with metadata (project_path, format, turn_count, etc.)
172
170
  """
@@ -174,7 +172,7 @@ class SessionAdapter(ABC):
174
172
  project_path = self.extract_project_path(session_file)
175
173
  format_str = self.detect_session_format(session_file)
176
174
  turn_count = self.count_turns(session_file)
177
-
175
+
178
176
  return {
179
177
  "adapter": self.name,
180
178
  "session_file": str(session_file),
@@ -189,14 +187,14 @@ class SessionAdapter(ABC):
189
187
  "session_file": str(session_file),
190
188
  "error": str(e),
191
189
  }
192
-
190
+
193
191
  def is_session_valid(self, session_file: Path) -> bool:
194
192
  """
195
193
  Check if a session file is valid for this adapter.
196
-
194
+
197
195
  Args:
198
196
  session_file: Path to session file
199
-
197
+
200
198
  Returns:
201
199
  True if the session is valid
202
200
  """
@@ -204,36 +202,34 @@ class SessionAdapter(ABC):
204
202
  return self.detect_session_format(session_file) is not None
205
203
  except Exception:
206
204
  return False
207
-
205
+
208
206
  def get_latest_session(self, sessions: Optional[List[Path]] = None) -> Optional[Path]:
209
207
  """
210
208
  Get the most recently modified session.
211
-
209
+
212
210
  Args:
213
211
  sessions: Optional list of sessions (discovers if not provided)
214
-
212
+
215
213
  Returns:
216
214
  Path to the most recent session, or None
217
215
  """
218
216
  if sessions is None:
219
217
  sessions = self.discover_sessions()
220
-
218
+
221
219
  if not sessions:
222
220
  return None
223
-
221
+
224
222
  # Sort by modification time, most recent first
225
223
  try:
226
- sessions_with_mtime = [
227
- (s, s.stat().st_mtime) for s in sessions if s.exists()
228
- ]
224
+ sessions_with_mtime = [(s, s.stat().st_mtime) for s in sessions if s.exists()]
229
225
  if not sessions_with_mtime:
230
226
  return None
231
-
227
+
232
228
  sessions_with_mtime.sort(key=lambda x: x[1], reverse=True)
233
229
  return sessions_with_mtime[0][0]
234
230
  except Exception as e:
235
231
  logger.warning(f"Error finding latest session: {e}")
236
232
  return sessions[0] if sessions else None
237
-
233
+
238
234
  def __repr__(self) -> str:
239
235
  return f"<{self.__class__.__name__} name='{self.name}'>"
@@ -19,7 +19,7 @@ class ClaudeAdapter(SessionAdapter):
19
19
 
20
20
  name = "claude"
21
21
  trigger_class = ClaudeTrigger
22
-
22
+
23
23
  def discover_sessions(self) -> List[Path]:
24
24
  """
25
25
  Find all Claude Code sessions from ALL projects.
@@ -42,13 +42,13 @@ class ClaudeAdapter(SessionAdapter):
42
42
  pass
43
43
 
44
44
  return sessions
45
-
45
+
46
46
  def discover_sessions_for_project(self, project_path: Path) -> List[Path]:
47
47
  """Find sessions for a specific project."""
48
48
  claude_dir = find_claude_sessions_dir(project_path)
49
49
  if not claude_dir:
50
50
  return []
51
-
51
+
52
52
  # For Claude, we usually just care about the latest one
53
53
  latest = self.get_latest_session_in_dir(claude_dir)
54
54
  return [latest] if latest else []
@@ -56,26 +56,26 @@ class ClaudeAdapter(SessionAdapter):
56
56
  def extract_project_path(self, session_file: Path) -> Optional[Path]:
57
57
  """
58
58
  Extract project path from Claude session file.
59
-
59
+
60
60
  For Claude, the project path is encoded in the parent directory name.
61
61
  Example: -Users-name-project -> /Users/name/project
62
62
  """
63
63
  try:
64
64
  # Must be in .claude/projects/
65
- if '.claude' not in str(session_file):
65
+ if ".claude" not in str(session_file):
66
66
  return None
67
-
67
+
68
68
  parent_name = session_file.parent.name
69
- if not parent_name.startswith('-'):
69
+ if not parent_name.startswith("-"):
70
70
  return None
71
-
71
+
72
72
  # Simple decoding: replace leading '-' with '/' and all other '-' with '/'
73
73
  # This is a heuristic as we can't distinguish original '-' from '/'
74
- # But standard paths don't have '-' often in directory names, or at least this
74
+ # But standard paths don't have '-' often in directory names, or at least this
75
75
  # matches the standard encoding for /Users/...
76
- project_path_str = '/' + parent_name[1:].replace('-', '/')
76
+ project_path_str = "/" + parent_name[1:].replace("-", "/")
77
77
  project_path = Path(project_path_str)
78
-
78
+
79
79
  # Validation: check if path exists
80
80
  # We relax this slightly: if it's a valid-looking path, we return it,
81
81
  # but ideally we check existence.
@@ -105,7 +105,7 @@ class ClaudeAdapter(SessionAdapter):
105
105
  pass
106
106
 
107
107
  return None
108
-
108
+
109
109
  except Exception:
110
110
  return None
111
111
 
@@ -132,10 +132,10 @@ class ClaudeAdapter(SessionAdapter):
132
132
  # Claude format check
133
133
  if not session_file.name.endswith(".jsonl"):
134
134
  return False
135
-
135
+
136
136
  # Check parent structure ~/.claude/projects/
137
137
  parts = session_file.parts
138
138
  if ".claude" in parts and "projects" in parts:
139
139
  return True
140
-
140
+
141
141
  return super().is_session_valid(session_file)
realign/adapters/codex.py CHANGED
@@ -15,10 +15,10 @@ from ..codex_detector import find_codex_sessions_for_project
15
15
 
16
16
  class CodexAdapter(SessionAdapter):
17
17
  """Adapter for Codex CLI sessions."""
18
-
18
+
19
19
  name = "codex"
20
20
  trigger_class = CodexTrigger
21
-
21
+
22
22
  def discover_sessions(self) -> List[Path]:
23
23
  """Find all Codex sessions."""
24
24
  sessions = []
@@ -34,7 +34,7 @@ class CodexAdapter(SessionAdapter):
34
34
  pass
35
35
 
36
36
  return sessions
37
-
37
+
38
38
  def discover_sessions_for_project(self, project_path: Path) -> List[Path]:
39
39
  """Find sessions for a specific project."""
40
40
  return find_codex_sessions_for_project(project_path)
@@ -42,13 +42,13 @@ class CodexAdapter(SessionAdapter):
42
42
  def extract_project_path(self, session_file: Path) -> Optional[Path]:
43
43
  """Extract project path from Codex session file metadata."""
44
44
  try:
45
- with open(session_file, 'r', encoding='utf-8') as f:
45
+ with open(session_file, "r", encoding="utf-8") as f:
46
46
  first_line = f.readline()
47
47
  if not first_line:
48
48
  return None
49
49
  data = json.loads(first_line)
50
- if data.get('type') == 'session_meta':
51
- cwd = data.get('payload', {}).get('cwd')
50
+ if data.get("type") == "session_meta":
51
+ cwd = data.get("payload", {}).get("cwd")
52
52
  if cwd:
53
53
  return Path(cwd)
54
54
  except Exception:
@@ -59,6 +59,6 @@ class CodexAdapter(SessionAdapter):
59
59
  """Check if this is a Codex session file."""
60
60
  if not session_file.name.startswith("rollout-") or not session_file.name.endswith(".jsonl"):
61
61
  return False
62
-
62
+
63
63
  # Check first line for Codex signature
64
64
  return super().is_session_valid(session_file)