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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
aline_ai-0.5.5.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
|
|
2
|
+
realign/__init__.py,sha256=bcfFHKLZs3gqPRuWwo5pAfBK0K2BuS6pB2bwjqmpoCQ,1623
|
|
3
|
+
realign/claude_detector.py,sha256=ZLSJacMo6zzQclXByABKA70UNpstxqIv3fPGqdpA934,2792
|
|
4
|
+
realign/cli.py,sha256=j_G_FcwbTgCOE0c2kHjX_dma6IdcPcB-maYjr82y1XE,30050
|
|
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=J1oQz63dpH1aEScUsjLRi32mwg3dywm1hSm-gquAk8U,11936
|
|
48
|
+
realign/dashboard/tmux_manager.py,sha256=DdCiumQ7YQZnje5VfOQ60585C0X6Va_AhBQi_zmhE0Y,24035
|
|
49
|
+
realign/dashboard/screens/__init__.py,sha256=x42K31sqL5KVMtufOnZjG8LnFN7hQyN5-z8CySqbwlM,304
|
|
50
|
+
realign/dashboard/screens/create_event.py,sha256=oiQY1zKpUYnQU-5fQLeuZH9BV5NClE5B5XZIVBYG5A8,5506
|
|
51
|
+
realign/dashboard/screens/event_detail.py,sha256=OLaL3-FgAohDdzVlfuUw5yh2SR49IHIpCtiqXJhBTc0,20992
|
|
52
|
+
realign/dashboard/screens/session_detail.py,sha256=gfpUIhMO00ecMlMyzpkxDdvGb9zhESEvxwrJvqLuHOI,9603
|
|
53
|
+
realign/dashboard/screens/share_import.py,sha256=hl2x0yGVycsoUI76AmdZTAV-br3Q6191g5xHHrZ8hOA,6318
|
|
54
|
+
realign/dashboard/styles/dashboard.tcss,sha256=9sSIs3r4V8eeTwCK56s7fnYxjMEuASP8EcmK1fhpUmA,3454
|
|
55
|
+
realign/dashboard/widgets/__init__.py,sha256=3Pf2_K9obrertgv_psfxradgkI9RXlmjoXYQH7oBKm0,583
|
|
56
|
+
realign/dashboard/widgets/config_panel.py,sha256=Afezfd6nvHo0Q44IS2UZTPJsYmHbqzjx7bi5jWrCDPA,11182
|
|
57
|
+
realign/dashboard/widgets/events_table.py,sha256=zzX08U2zSNnGX925BA1Bksem-xk2Nd_ujo61rbqGIm8,21904
|
|
58
|
+
realign/dashboard/widgets/header.py,sha256=1I8XUf_2qbmpMDuteSvcG3amqeQ4nWa_8Zfev0dXXNI,1583
|
|
59
|
+
realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
|
|
60
|
+
realign/dashboard/widgets/search_panel.py,sha256=ZNJDfwDSxUFnCeltYQYsQsPJ6t4HDeNWpENoTOoBdVM,8951
|
|
61
|
+
realign/dashboard/widgets/sessions_table.py,sha256=syM0CjP79Xd9HuJ2ttCa4rwON2q3jXmROjcFtJ0hA9w,22069
|
|
62
|
+
realign/dashboard/widgets/terminal_panel.py,sha256=a0-XQ7IUVhC8rQAhZd9gA3N7ue87Q74jLqGzEPFWntU,30897
|
|
63
|
+
realign/dashboard/widgets/watcher_panel.py,sha256=O_mdDacgc87xA-5KEfta53Ik_Xsk_B2OfwenMOTtGw8,19722
|
|
64
|
+
realign/dashboard/widgets/worker_panel.py,sha256=F_jKWABuCNmjQgeeuCr4KnFRKdY4CLTNcEXMYwsNaSk,18691
|
|
65
|
+
realign/db/__init__.py,sha256=-1d-Zc4IOUVokbdTXi3R-bIwlkFEPAz_qTHAdcsdp6g,1870
|
|
66
|
+
realign/db/base.py,sha256=4OkwPi6qL_8ZJb1ATNkHr-JaIxh98UYTSZ6fSYFff6s,12033
|
|
67
|
+
realign/db/locks.py,sha256=yzCiPJZ4eOQX-Q4mXB6s76U2U7lXAzIBBy1t59w-AVU,1698
|
|
68
|
+
realign/db/migration.py,sha256=af1QFEfIh_qX0pFyXzm5gWFVbQn0sKOUNLSJHlr__FU,13405
|
|
69
|
+
realign/db/schema.py,sha256=Qj8nRs7plc8MXXTq7D4vi4L0joaiEjaI0mZMzUC4z78,18066
|
|
70
|
+
realign/db/sqlite_db.py,sha256=UmUjo3OW7F6YEeOSdl0-fGOXNFn_tC7d3EYEEUNzNZU,81793
|
|
71
|
+
realign/events/__init__.py,sha256=IM-NxF4Zk2hYFD07k4WrfNRuuiC9ihGjf4GBpJhjd2E,35
|
|
72
|
+
realign/events/debouncer.py,sha256=U3Q7dYpnMsAgWsW_E_IbSC4lrdEoi6H_SFLGLOAazs4,3062
|
|
73
|
+
realign/events/event_summarizer.py,sha256=ZLiwOXWN8eawep3cQs3Wh9QLSypvU1SRbe8GTJXJQaY,8272
|
|
74
|
+
realign/events/session_summarizer.py,sha256=IWYcDHGbsPtZEeDcQMWy4V-IKi5QBqpA5uuOIGy4Sls,10386
|
|
75
|
+
realign/models/event.py,sha256=ypz74D4l6U2U0RhgL8fzEhiq7iQjhHybmAdLUNDY7P4,5521
|
|
76
|
+
realign/prompts/__init__.py,sha256=PpYR7f-T96fd-QyNYJDRS1U6h9O0rIt_SMsREy9i3aA,443
|
|
77
|
+
realign/prompts/presets.py,sha256=h9oEy0XP4JQ4DCnp8HN_FfF0LmI-yOV6xWJLknIghJ8,7256
|
|
78
|
+
realign/tracker/__init__.py,sha256=Apd-xxomkiOjSIthseqZpVQ8l0yT4nkaErsUyA1ptIE,161
|
|
79
|
+
realign/triggers/__init__.py,sha256=esF-rMxaxzKDAEX4eLJRdUcVkpSU2_r6U37igS7jrIQ,674
|
|
80
|
+
realign/triggers/antigravity_trigger.py,sha256=sj9OKu2TihNlgOAd2B9XLy5wfJpY5VfJSast7Krl4bg,5195
|
|
81
|
+
realign/triggers/base.py,sha256=Q72nlPMnCB3SP14gd4hm6AjS0mVyq5lbtlLUxSxAvrY,4148
|
|
82
|
+
realign/triggers/claude_trigger.py,sha256=-LY2sR-FTEXlCBx8SOI6TxP5-RI2BtlfzqvLeT5U3mw,20065
|
|
83
|
+
realign/triggers/codex_trigger.py,sha256=X8WNphoav86XNd05SJBoxpHySlFtEYCrjsEF-b6pw8M,13967
|
|
84
|
+
realign/triggers/gemini_trigger.py,sha256=878GDjxuJ8WCXEBbCLSN2k2l_3BSGzvykhZi3gfIOqw,7939
|
|
85
|
+
realign/triggers/next_turn_trigger.py,sha256=BpP0PWn4mU1MZd6mv89jWcjs8Jtv0zEWapW32O0wcHk,4333
|
|
86
|
+
realign/triggers/registry.py,sha256=cb-AVLbYB2pqwfWL3q1DQxLv4kOw7g7m-GshTdfFESc,3827
|
|
87
|
+
realign/triggers/turn_status.py,sha256=wAZEhXDAmDoX5F-ohWfSnZZ0eA6DAJ9svSPiSv_f6sg,6041
|
|
88
|
+
realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
|
|
89
|
+
aline_ai-0.5.5.dist-info/METADATA,sha256=iIDlswhdjHF_CCaONwDX0iRFFZ1VvNxxabYxzKeTWC8,1597
|
|
90
|
+
aline_ai-0.5.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
91
|
+
aline_ai-0.5.5.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
|
|
92
|
+
aline_ai-0.5.5.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
|
|
93
|
+
aline_ai-0.5.5.dist-info/RECORD,,
|
realign/__init__.py
CHANGED
realign/adapters/antigravity.py
CHANGED
|
@@ -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(
|
|
41
|
-
|
|
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
|
|
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
|
|
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}'>"
|
realign/adapters/claude.py
CHANGED
|
@@ -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
|
|
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 =
|
|
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,
|
|
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(
|
|
51
|
-
cwd = data.get(
|
|
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)
|