aline-ai 0.6.3__py3-none-any.whl → 0.6.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aline-ai
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -1,26 +1,28 @@
1
- aline_ai-0.6.3.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
- realign/__init__.py,sha256=39-DjXCr_QdXUrBANmVXTbHJs0S-ykXTuWatq35vmQE,1623
1
+ aline_ai-0.6.5.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
+ realign/__init__.py,sha256=02FiDcPQx1TGbGJO98rtDO7k-JAA9WrZKtygoavnEY8,1623
3
3
  realign/auth.py,sha256=d_1yvCwluN5iIrdgjtuSKpOYAksDzrzNgntKacLVJrw,16583
4
4
  realign/claude_detector.py,sha256=ZLSJacMo6zzQclXByABKA70UNpstxqIv3fPGqdpA934,2792
5
- realign/cli.py,sha256=PAf5aPt0IIGgwIihHllVUS4RCxJXOvDQWiO9i00K5Po,43766
6
- realign/codex_detector.py,sha256=N9ulgMgvTzDfXE4s4vLd6OoS0hT7R6h2bDFFXWa-2hE,4183
7
- realign/config.py,sha256=4A0jY3BkQ7AuptTR3T-Z2aJe77N13yH1LmKaXcVwNz0,8664
5
+ realign/cli.py,sha256=HZ_1Rm50z1oszCwvPAZcAdPt0Gl-dj0S0NMLy2sWu_4,35665
6
+ realign/codex_detector.py,sha256=xTpYgMfUwL6UL76xeHl8xF2ZBPHdjwmgXmbmZkvHA0A,5523
7
+ realign/codex_home.py,sha256=gAAosBDru4jfz0QCn12A2bZEC_lZxytpOAzk7GOXTpI,2512
8
+ realign/codex_terminal_linker.py,sha256=9cDUHhN7MhCIUOfb-3kApPY-l6s91jq2qq_WI0ccexY,5926
9
+ realign/config.py,sha256=Znfs43AjiK90LGWnArDPWyrE859sdZQAPIb0KAcU3Ig,9252
8
10
  realign/context.py,sha256=8hzgNOg-7_eMW22wt7OM5H9IsmMveKXCv0epG7E0G7w,13917
9
11
  realign/file_lock.py,sha256=kLNm1Rra4TCrTMyPM5fwjVascq-CUz2Bzh9HHKtCKOE,3444
10
- realign/hooks.py,sha256=tvhGE8EVjFsdM1WMzqrFMuU-twyoRy4lpY5nAd3Zl_U,70158
12
+ realign/hooks.py,sha256=wSSIjS5x9w7fm9LUcL63Lf7bglEfb75dHFja_znKDDQ,65134
11
13
  realign/llm_client.py,sha256=QqMPDFE-aXm7oz0QAkB90CN0Qn0uz7JOxpWbUUlHNgU,11141
12
14
  realign/logging_config.py,sha256=LCAigKFhTj86PSJm4-kUl3Ag9h_GENh3x2iPnMv7qUI,4871
13
15
  realign/mcp_server.py,sha256=LWiQ2qukYoNLsoV2ID2f0vF9jkJlBvB587HpM5jymgE,10193
14
16
  realign/mcp_watcher.py,sha256=aK4jWStv7CoCroS4tXFHgZ_y_-q4QDjrpWgm4DxcEj4,1260
15
17
  realign/redactor.py,sha256=Zsoi5HfYak2yPmck20JArhm-1cPSB78IdkBJiNVXfrc,17096
16
- realign/watcher_core.py,sha256=gt4at2fGIewmZjNUWpQi1og6TxK0BQ74DErICbvzrj0,106516
18
+ realign/watcher_core.py,sha256=0XCoA5giuie-Ytc_tlPTVbZH8EFRPmODu7DUYRhRBGo,108598
17
19
  realign/watcher_daemon.py,sha256=OHUQ9P1LlagKJHfrf6uRnzO-zDtBRXIxt8ydMFHf5S8,3475
18
20
  realign/worker_core.py,sha256=TXioUVJlOO-8EgmKssCTLIyuh0aaupRLb1sh9s3kSuc,10194
19
21
  realign/worker_daemon.py,sha256=X7Xyjw_u6m6KG4E84nx0HpDFw4cWMv8ja1G8btc9PiM,3957
20
22
  realign/adapters/__init__.py,sha256=alkJr7DRn_CrJecSJRjRJOHHnkz9EnZ5TnsU8n1Bb0k,719
21
23
  realign/adapters/base.py,sha256=2IdAZKGjg5gPB3YLf_8r3V4XAdbK7fHpj06GjjsYEFY,7409
22
24
  realign/adapters/claude.py,sha256=ksTRwC5Z8AzUcB21LFjx6DETP08cv__fjgBzm-TeZdI,5444
23
- realign/adapters/codex.py,sha256=5ex3zJ5Hpb_StV2CcBSHVhHleygZxzVJjYsWw8qK1Bc,2051
25
+ realign/adapters/codex.py,sha256=VJgmrRzOO5a6GNG6xL7gJwzcvA2CBmHdilYkj0qffBw,2233
24
26
  realign/adapters/gemini.py,sha256=NvtXQPWUtEY-DaAAMvLGvQW4FalTG-g0pD514HYnzF0,2540
25
27
  realign/adapters/registry.py,sha256=yM6nf9nGTJ1vaK2Uixp-VacseK7PmxZkCdKedmWI8MA,3255
26
28
  realign/claude_hooks/__init__.py,sha256=MT9c8TWjLO23xDCM-uBBMy_mOThNd7O-AgN_Khn30qs,594
@@ -31,24 +33,25 @@ realign/claude_hooks/stop_hook_installer.py,sha256=uyqKOqpix7CQP64ERBvvh7viSPp_w
31
33
  realign/claude_hooks/terminal_state.py,sha256=i8B6b_2_9ttPEemp7SrGdFRJSa-vm5lc7YSTRTvAWNg,5397
32
34
  realign/claude_hooks/user_prompt_submit_hook.py,sha256=kMrmhAVtfV41oTX7JZcq2HPXjgQQ5gX26iOJoHJkfqA,10474
33
35
  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/__init__.py,sha256=WVaVT1orM2Z0PYaG3X6tkKb_t2v3n_3siCadh1qd_QA,107
37
+ realign/commands/add.py,sha256=_Xzt9P15mwndA3JvBBVrki8tn9Cc0UP6SiLwM4RS8Nc,27232
36
38
  realign/commands/auth.py,sha256=QrPukpP-ogYEDSwztV0NOYI-HDgn5fPxlCQ1-e2n7gU,11082
37
39
  realign/commands/config.py,sha256=nYnu_h2pk7GODcrzrV04K51D-s7v06FlRXHJ0HJ-gvU,6732
38
40
  realign/commands/context.py,sha256=pM2KfZHVkB-ou4nBhFvKSwnYliLBzwN3zerLyBAbhfE,7095
41
+ realign/commands/doctor.py,sha256=q5UOrUR5Uai4AxgaeOnK1Hig5I5UX7m3Vt00tPnUllg,18289
39
42
  realign/commands/export_shares.py,sha256=WNOR7FBE2om9qPO_28edZKhs94lyUAcbRgP_kNaDi5M,132574
40
43
  realign/commands/import_shares.py,sha256=HiswLlYHqR0dR3wgB7Rs54_WownqahIs5IdyJOHuot8,25572
41
- realign/commands/init.py,sha256=nhP1Qjl6Xo5R1ry_iTGVu3RwMxP-pYT5Z50NdzEMKrY,32756
44
+ realign/commands/init.py,sha256=6rBr1LVIrQLbUH_UvoDhkF1qXmMh2xkjNWCYAUz5Tho,35274
42
45
  realign/commands/restore.py,sha256=s2BxQZHxQw9r12NzRVsK20KlGafy5AIoSjWMo5PcnHY,11173
43
46
  realign/commands/search.py,sha256=QJrC0hln9sCDFxXbpo0nPGMHXrud18qA5QfRyD0z6fQ,25926
44
47
  realign/commands/upgrade.py,sha256=L3PLOUIN5qAQTbkfoVtSsIbbzEezA_xjjk9F1GMVfjw,12781
45
- realign/commands/watcher.py,sha256=X3j-mVce5ZARb8EUDILV462WSV-WF0tc_S7rYQ972pQ,134244
48
+ realign/commands/watcher.py,sha256=4WTThIgr-Z5guKh_JqGDcPmerr97XiHrVaaijmckHsA,134350
46
49
  realign/commands/worker.py,sha256=jTu7Pj60nTnn7SsH3oNCNnO6zl4TIFCJVNSC1OoQ_0o,23363
47
50
  realign/dashboard/__init__.py,sha256=QZkHTsGityH8UkF8rmvA3xW7dMXNe0swEWr443qfgCM,128
48
- realign/dashboard/app.py,sha256=woiStKV2dcsKRr801GCihLbRKrI0YEBw5YpV8x24Hsw,15802
51
+ realign/dashboard/app.py,sha256=aB1pvuJu-qJ94UqNegB4lvIxUzQJovuC82WQjFnQIFc,10464
49
52
  realign/dashboard/layout.py,sha256=sZxmFj6QTbkois9MHTvBEMMcnaRVehCDqugdbiFx10k,9072
50
53
  realign/dashboard/terminal_backend.py,sha256=MlDfwtqhftyQK6jDNizQGFjAWIo5Bx2TDpSnP3MCZVM,3375
51
- realign/dashboard/tmux_manager.py,sha256=K8sjzSBtISLuWF7s4g4YieJ4oiE5wIgSztFvmhJZd0M,26849
54
+ realign/dashboard/tmux_manager.py,sha256=Fc6OQbnOO4YV47BnrIkcr0SHnQuSFwUSqhepNkpqKLs,32942
52
55
  realign/dashboard/backends/__init__.py,sha256=POROX7YKtukYZcLB1pi_kO0sSEpuO3y-hwmF3WIN1Kk,163
53
56
  realign/dashboard/backends/iterm2.py,sha256=XYYJT5lrrp4pW_MyEqPZYkRI0qyKUwJlezwMidgnsHc,21390
54
57
  realign/dashboard/backends/kitty.py,sha256=5jdkR1f2PwB8a4SnS3EG6uOQ2XU-PB7-cpKBfIJq3hU,12066
@@ -61,13 +64,13 @@ realign/dashboard/screens/session_detail.py,sha256=TBkHqSHyMxsLB2QdZq9m1EoiH8oRV
61
64
  realign/dashboard/screens/share_import.py,sha256=hl2x0yGVycsoUI76AmdZTAV-br3Q6191g5xHHrZ8hOA,6318
62
65
  realign/dashboard/styles/dashboard.tcss,sha256=ewonevBGLN-dfSsgxUk4VBCPchtxY4rx_vj1u6Ox2Fw,3454
63
66
  realign/dashboard/widgets/__init__.py,sha256=3Pf2_K9obrertgv_psfxradgkI9RXlmjoXYQH7oBKm0,583
64
- realign/dashboard/widgets/config_panel.py,sha256=Abr-SjoOgHELCiBpbuoa-nPdp6aiDd21Sx7R7EXu1gg,12890
67
+ realign/dashboard/widgets/config_panel.py,sha256=eRJRuqImQ8eJIKCEj4O8EvYxI-ht_anrcYbT5JskWyU,15972
65
68
  realign/dashboard/widgets/events_table.py,sha256=MKB1G1_xdQCujEhmMz_GKI4hs-PeEiqGEAH7Y3ZGanE,30852
66
69
  realign/dashboard/widgets/header.py,sha256=0HHCFXX7F3C6HII-WDwOJwWkJrajmKPWmdoMWyOkn9E,1587
67
70
  realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
68
71
  realign/dashboard/widgets/search_panel.py,sha256=ZNJDfwDSxUFnCeltYQYsQsPJ6t4HDeNWpENoTOoBdVM,8951
69
- realign/dashboard/widgets/sessions_table.py,sha256=bSwWmHSdxLjwcMWB1Tf_YJjcM36iuorhgC75s3BXgdM,34000
70
- realign/dashboard/widgets/terminal_panel.py,sha256=BZgh7lo9rhuJdvSGBhUPlWjS3KjcXDJP7SSnNngmFaY,45805
72
+ realign/dashboard/widgets/sessions_table.py,sha256=oMkYhQ55pUGOGYxEXM5P37mpGYA350BK8Rb8fVq9AS4,34008
73
+ realign/dashboard/widgets/terminal_panel.py,sha256=8WX2_EewlyFlxJYokw2akEqkJUjNt_-F8tzE7St3084,60132
71
74
  realign/dashboard/widgets/watcher_panel.py,sha256=emVY1-aot9Dnf5UI9yyNeEmp4d2Gb-lrC28DjkeLjKA,19575
72
75
  realign/dashboard/widgets/worker_panel.py,sha256=F_jKWABuCNmjQgeeuCr4KnFRKdY4CLTNcEXMYwsNaSk,18691
73
76
  realign/db/__init__.py,sha256=65LsNdsq_rkwNC1eg1OAr3HC0ORXtelOh0I8MhNGr-g,3288
@@ -76,7 +79,7 @@ realign/db/locks.py,sha256=yzCiPJZ4eOQX-Q4mXB6s76U2U7lXAzIBBy1t59w-AVU,1698
76
79
  realign/db/migrate_agents.py,sha256=cDeVUzKW950dJ0lV74QObHuONqKwErSrXI5akU2vBmQ,9633
77
80
  realign/db/migration.py,sha256=af1QFEfIh_qX0pFyXzm5gWFVbQn0sKOUNLSJHlr__FU,13405
78
81
  realign/db/schema.py,sha256=YHj5PGZWbCl0VG0epnMF_Ofg3jRiLHq6SLHCi1q34eQ,30181
79
- realign/db/sqlite_db.py,sha256=Aqj3AebvZ-RWA2sk_ntfuIaZB8Asf-WVU8bm2_I71L8,104790
82
+ realign/db/sqlite_db.py,sha256=nihEZ71wg1BXiVG1QU488ed9Q-ZasoVKYVS4j20hhtY,107223
80
83
  realign/events/__init__.py,sha256=IM-NxF4Zk2hYFD07k4WrfNRuuiC9ihGjf4GBpJhjd2E,35
81
84
  realign/events/debouncer.py,sha256=U3Q7dYpnMsAgWsW_E_IbSC4lrdEoi6H_SFLGLOAazs4,3062
82
85
  realign/events/event_summarizer.py,sha256=jJtWM8UWtsG4KGdzYicMqcTxrncWzGNEQs5vdBJPyew,10185
@@ -94,8 +97,8 @@ realign/triggers/next_turn_trigger.py,sha256=-x80_I-WmIjXXzQHEPBykgx_GQW6oKaLDQx
94
97
  realign/triggers/registry.py,sha256=dkIjSd8Bg-hF0nxaO2Fi2K-0Zipqv6vVjc-HYSrA_fY,3656
95
98
  realign/triggers/turn_status.py,sha256=wAZEhXDAmDoX5F-ohWfSnZZ0eA6DAJ9svSPiSv_f6sg,6041
96
99
  realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
97
- aline_ai-0.6.3.dist-info/METADATA,sha256=cJ5Q0IbiiSadcQymiarMIsC4_zdwK7lUK-vAs3a8TLE,1597
98
- aline_ai-0.6.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
99
- aline_ai-0.6.3.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
100
- aline_ai-0.6.3.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
101
- aline_ai-0.6.3.dist-info/RECORD,,
100
+ aline_ai-0.6.5.dist-info/METADATA,sha256=RmD0VjSn_0nGStyFKgkNYUL3i2foaqg8UWUT3kOUTOc,1597
101
+ aline_ai-0.6.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
102
+ aline_ai-0.6.5.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
103
+ aline_ai-0.6.5.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
104
+ aline_ai-0.6.5.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.6.3"
6
+ __version__ = "0.6.5"
7
7
 
8
8
 
9
9
  def get_realign_dir(project_root: Path) -> Path:
realign/adapters/codex.py CHANGED
@@ -21,17 +21,22 @@ class CodexAdapter(SessionAdapter):
21
21
 
22
22
  def discover_sessions(self) -> List[Path]:
23
23
  """Find all Codex sessions."""
24
- sessions = []
25
- codex_sessions_base = Path.home() / ".codex" / "sessions"
26
-
27
- if not codex_sessions_base.exists():
28
- return sessions
29
-
30
- # Find all session files recursively
24
+ sessions: list[Path] = []
25
+ roots: list[Path] = []
31
26
  try:
32
- sessions.extend(codex_sessions_base.rglob("rollout-*.jsonl"))
27
+ from ..codex_detector import _codex_session_roots # type: ignore[attr-defined]
28
+
29
+ roots = _codex_session_roots()
33
30
  except Exception:
34
- pass
31
+ roots = [Path.home() / ".codex" / "sessions"]
32
+
33
+ for root in roots:
34
+ if not root.exists():
35
+ continue
36
+ try:
37
+ sessions.extend(root.rglob("rollout-*.jsonl"))
38
+ except Exception:
39
+ continue
35
40
 
36
41
  return sessions
37
42
 
realign/cli.py CHANGED
@@ -7,7 +7,19 @@ from typing import Optional
7
7
  from rich.console import Console
8
8
  from rich.syntax import Syntax
9
9
 
10
- from .commands import init, config, watcher, worker, export_shares, search, upgrade, restore, add, auth
10
+ from .commands import (
11
+ init,
12
+ config,
13
+ watcher,
14
+ worker,
15
+ export_shares,
16
+ search,
17
+ upgrade,
18
+ restore,
19
+ add,
20
+ auth,
21
+ doctor,
22
+ )
11
23
 
12
24
  app = typer.Typer(
13
25
  name="realign",
@@ -33,6 +45,22 @@ def main(
33
45
  ctx.obj["dev"] = dev
34
46
 
35
47
  if ctx.invoked_subcommand is None:
48
+ def _needs_global_init() -> bool:
49
+ config_path = Path.home() / ".aline" / "config.yaml"
50
+ if not config_path.exists():
51
+ return True
52
+ try:
53
+ from .config import ReAlignConfig
54
+
55
+ cfg = ReAlignConfig.load(config_path)
56
+ db_path = Path(cfg.sqlite_db_path).expanduser()
57
+ if not db_path.exists():
58
+ return True
59
+ except Exception:
60
+ return True
61
+ prompts_dir = Path.home() / ".aline" / "prompts"
62
+ return not prompts_dir.exists()
63
+
36
64
  # Check login status before launching dashboard
37
65
  from .auth import is_logged_in, get_current_user
38
66
 
@@ -53,6 +81,17 @@ def main(
53
81
 
54
82
  console.print() # Add spacing before dashboard launch
55
83
 
84
+ # First run after install/upgrade: ensure global artifacts exist.
85
+ if _needs_global_init():
86
+ console.print("[dim]First run detected. Running 'aline init'...[/dim]\n")
87
+ try:
88
+ from .commands import init as init_cmd
89
+
90
+ init_cmd.init_command(force=False, start_watcher=None)
91
+ except typer.Exit as e:
92
+ if getattr(e, "exit_code", 1) != 0:
93
+ raise
94
+
56
95
  # Check for updates before launching dashboard
57
96
  from .commands.upgrade import check_and_prompt_update
58
97
 
@@ -95,6 +134,7 @@ def main(
95
134
  app.command(name="init")(init.init_command)
96
135
  app.command(name="config")(config.config_command)
97
136
  app.command(name="upgrade")(upgrade.upgrade_command)
137
+ app.command(name="doctor")(doctor.doctor_command)
98
138
 
99
139
 
100
140
  # Auth commands
@@ -119,239 +159,6 @@ def whoami_cli():
119
159
  raise typer.Exit(code=exit_code)
120
160
 
121
161
 
122
- @app.command(name="doctor")
123
- def doctor_cli(
124
- no_restart: bool = typer.Option(False, "--no-restart", help="Only clear cache, don't restart daemons"),
125
- verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
126
- ):
127
- """
128
- Fix common issues after code updates.
129
-
130
- This command:
131
- - Clears Python bytecode cache (.pyc files)
132
- - Updates Claude Code hooks (Stop, UserPromptSubmit, PermissionRequest)
133
- - Updates skills to latest version
134
- - Ensures watcher daemon is running (restarts if running, starts if not)
135
- - Ensures worker daemon is running (restarts if running, starts if not)
136
-
137
- Run this after pulling new code to ensure everything uses the latest version.
138
- """
139
- import shutil
140
- import subprocess
141
- import signal
142
- import time
143
-
144
- # Find the project root (where src/realign is)
145
- project_root = Path(__file__).parent.parent.parent
146
-
147
- # 1. Clear Python cache
148
- console.print("[bold]1. Clearing Python cache...[/bold]")
149
- pyc_count = 0
150
- pycache_count = 0
151
-
152
- for pyc_file in project_root.rglob("*.pyc"):
153
- try:
154
- pyc_file.unlink()
155
- pyc_count += 1
156
- if verbose:
157
- console.print(f" [dim]Removed: {pyc_file}[/dim]")
158
- except Exception as e:
159
- if verbose:
160
- console.print(f" [yellow]Failed to remove {pyc_file}: {e}[/yellow]")
161
-
162
- for pycache_dir in project_root.rglob("__pycache__"):
163
- if pycache_dir.is_dir():
164
- try:
165
- shutil.rmtree(pycache_dir)
166
- pycache_count += 1
167
- if verbose:
168
- console.print(f" [dim]Removed: {pycache_dir}[/dim]")
169
- except Exception as e:
170
- if verbose:
171
- console.print(f" [yellow]Failed to remove {pycache_dir}: {e}[/yellow]")
172
-
173
- console.print(f" [green]✓[/green] Cleared {pyc_count} .pyc files, {pycache_count} __pycache__ directories")
174
-
175
- # 2. Update Claude Code hooks
176
- console.print("\n[bold]2. Updating Claude Code hooks...[/bold]")
177
- hooks_updated = []
178
- hooks_failed = []
179
-
180
- # Stop hook
181
- try:
182
- from .claude_hooks.stop_hook_installer import install_stop_hook, get_settings_path
183
- if install_stop_hook(get_settings_path(), quiet=True, force=True):
184
- hooks_updated.append("Stop")
185
- if verbose:
186
- console.print(" [dim]Stop hook updated[/dim]")
187
- else:
188
- hooks_failed.append("Stop")
189
- except Exception as e:
190
- hooks_failed.append("Stop")
191
- if verbose:
192
- console.print(f" [yellow]Stop hook failed: {e}[/yellow]")
193
-
194
- # UserPromptSubmit hook
195
- try:
196
- from .claude_hooks.user_prompt_submit_hook_installer import install_user_prompt_submit_hook, get_settings_path as get_submit_settings_path
197
- if install_user_prompt_submit_hook(get_submit_settings_path(), quiet=True, force=True):
198
- hooks_updated.append("UserPromptSubmit")
199
- if verbose:
200
- console.print(" [dim]UserPromptSubmit hook updated[/dim]")
201
- else:
202
- hooks_failed.append("UserPromptSubmit")
203
- except Exception as e:
204
- hooks_failed.append("UserPromptSubmit")
205
- if verbose:
206
- console.print(f" [yellow]UserPromptSubmit hook failed: {e}[/yellow]")
207
-
208
- # PermissionRequest hook
209
- try:
210
- from .claude_hooks.permission_request_hook_installer import install_permission_request_hook, get_settings_path as get_permission_settings_path
211
- if install_permission_request_hook(get_permission_settings_path(), quiet=True, force=True):
212
- hooks_updated.append("PermissionRequest")
213
- if verbose:
214
- console.print(" [dim]PermissionRequest hook updated[/dim]")
215
- else:
216
- hooks_failed.append("PermissionRequest")
217
- except Exception as e:
218
- hooks_failed.append("PermissionRequest")
219
- if verbose:
220
- console.print(f" [yellow]PermissionRequest hook failed: {e}[/yellow]")
221
-
222
- if hooks_updated:
223
- console.print(f" [green]✓[/green] Updated hooks: {', '.join(hooks_updated)}")
224
- if hooks_failed:
225
- console.print(f" [yellow]![/yellow] Failed hooks: {', '.join(hooks_failed)}")
226
-
227
- # 3. Update skills
228
- console.print("\n[bold]3. Updating skills...[/bold]")
229
- try:
230
- from .commands.add import add_skills_command
231
- # Capture output by redirecting - use force=True to update
232
- import io
233
- import contextlib
234
-
235
- stdout_capture = io.StringIO()
236
- with contextlib.redirect_stdout(stdout_capture):
237
- add_skills_command(force=True)
238
-
239
- output = stdout_capture.getvalue()
240
- # Count updated skills from output
241
- updated_count = output.count("✓")
242
- if updated_count > 0:
243
- console.print(f" [green]✓[/green] Updated {updated_count} skill(s)")
244
- else:
245
- console.print(" [green]✓[/green] Skills are up to date")
246
- if verbose and output.strip():
247
- for line in output.strip().split("\n"):
248
- console.print(f" [dim]{line}[/dim]")
249
- except Exception as e:
250
- console.print(f" [yellow]![/yellow] Failed to update skills: {e}")
251
-
252
- if no_restart:
253
- console.print("\n[dim]Skipping daemon restart (--no-restart)[/dim]")
254
- console.print("\n[green]Done![/green] Aline is ready with the latest code.")
255
- raise typer.Exit(code=0)
256
-
257
- # 4. Restart watcher daemon
258
- console.print("\n[bold]4. Checking watcher daemon...[/bold]")
259
- pid_file = Path.home() / ".aline" / ".logs" / "watcher.pid"
260
- watcher_was_running = False
261
-
262
- if pid_file.exists():
263
- try:
264
- pid = int(pid_file.read_text().strip())
265
- # Check if process is running
266
- try:
267
- import os
268
- os.kill(pid, 0) # Signal 0 just checks if process exists
269
- watcher_was_running = True
270
- console.print(f" [dim]Found watcher daemon (PID {pid}), stopping...[/dim]")
271
- os.kill(pid, signal.SIGTERM)
272
- time.sleep(1)
273
- # Force kill if still running
274
- try:
275
- os.kill(pid, 0)
276
- os.kill(pid, signal.SIGKILL)
277
- time.sleep(0.5)
278
- except ProcessLookupError:
279
- pass
280
- except ProcessLookupError:
281
- console.print(" [dim]Watcher daemon not running (stale PID file)[/dim]")
282
- except Exception as e:
283
- if verbose:
284
- console.print(f" [yellow]Error checking watcher: {e}[/yellow]")
285
-
286
- if watcher_was_running:
287
- console.print(" [dim]Starting watcher daemon...[/dim]")
288
- else:
289
- console.print(" [dim]Watcher daemon was not running, starting...[/dim]")
290
- try:
291
- subprocess.Popen(
292
- ["python", "-m", "src.realign.watcher_daemon"],
293
- stdout=subprocess.DEVNULL,
294
- stderr=subprocess.DEVNULL,
295
- start_new_session=True,
296
- cwd=str(project_root),
297
- )
298
- time.sleep(2)
299
- action = "restarted" if watcher_was_running else "started"
300
- console.print(f" [green]✓[/green] Watcher daemon {action}")
301
- except Exception as e:
302
- action = "restart" if watcher_was_running else "start"
303
- console.print(f" [red]✗[/red] Failed to {action} watcher: {e}")
304
-
305
- # 5. Restart worker daemon
306
- console.print("\n[bold]5. Checking worker daemon...[/bold]")
307
- worker_pid_file = Path.home() / ".aline" / ".logs" / "worker.pid"
308
- worker_was_running = False
309
-
310
- if worker_pid_file.exists():
311
- try:
312
- pid = int(worker_pid_file.read_text().strip())
313
- try:
314
- import os
315
- os.kill(pid, 0)
316
- worker_was_running = True
317
- console.print(f" [dim]Found worker daemon (PID {pid}), stopping...[/dim]")
318
- os.kill(pid, signal.SIGTERM)
319
- time.sleep(1)
320
- try:
321
- os.kill(pid, 0)
322
- os.kill(pid, signal.SIGKILL)
323
- time.sleep(0.5)
324
- except ProcessLookupError:
325
- pass
326
- except ProcessLookupError:
327
- console.print(" [dim]Worker daemon not running (stale PID file)[/dim]")
328
- except Exception as e:
329
- if verbose:
330
- console.print(f" [yellow]Error checking worker: {e}[/yellow]")
331
-
332
- if worker_was_running:
333
- console.print(" [dim]Starting worker daemon...[/dim]")
334
- else:
335
- console.print(" [dim]Worker daemon was not running, starting...[/dim]")
336
- try:
337
- subprocess.Popen(
338
- ["python", "-m", "src.realign.worker_daemon"],
339
- stdout=subprocess.DEVNULL,
340
- stderr=subprocess.DEVNULL,
341
- start_new_session=True,
342
- cwd=str(project_root),
343
- )
344
- time.sleep(2)
345
- action = "restarted" if worker_was_running else "started"
346
- console.print(f" [green]✓[/green] Worker daemon {action}")
347
- except Exception as e:
348
- action = "restart" if worker_was_running else "start"
349
- console.print(f" [red]✗[/red] Failed to {action} worker: {e}")
350
-
351
- console.print("\n[green]Done![/green] Aline is ready with the latest code.")
352
- raise typer.Exit(code=0)
353
-
354
-
355
162
  @app.command(name="search")
356
163
  def search_cli(
357
164
  query: str = typer.Argument(..., help="Search query (keywords or regex pattern)"),
@@ -445,7 +252,7 @@ app.add_typer(add_app, name="add")
445
252
  @add_app.command(name="tmux")
446
253
  def add_tmux_cli():
447
254
  """Install tmux via Homebrew and set up Aline tmux clipboard bindings."""
448
- exit_code = add.add_tmux_command()
255
+ exit_code = add.add_tmux_command(install_brew=True)
449
256
  raise typer.Exit(code=exit_code)
450
257
 
451
258
 
realign/codex_detector.py CHANGED
@@ -7,6 +7,42 @@ from pathlib import Path
7
7
  from typing import Optional, List
8
8
 
9
9
 
10
+ def _codex_session_roots() -> list[Path]:
11
+ """Return all Codex session root directories to scan (best-effort)."""
12
+ roots: list[Path] = []
13
+
14
+ # Default Codex home: ~/.codex/sessions
15
+ roots.append(Path.home() / ".codex" / "sessions")
16
+
17
+ # Aline-managed per-terminal CODEX_HOME isolation: ~/.aline/codex_homes/*/sessions
18
+ try:
19
+ from .codex_home import aline_codex_homes_dir, codex_sessions_dir_for_home
20
+
21
+ homes = aline_codex_homes_dir()
22
+ if homes.exists():
23
+ for child in homes.iterdir():
24
+ if child.is_dir():
25
+ roots.append(codex_sessions_dir_for_home(child))
26
+ except Exception:
27
+ pass
28
+
29
+ # De-dup + only keep existing directories
30
+ out: list[Path] = []
31
+ seen: set[str] = set()
32
+ for r in roots:
33
+ try:
34
+ rr = r.expanduser()
35
+ except Exception:
36
+ rr = r
37
+ key = str(rr)
38
+ if key in seen:
39
+ continue
40
+ seen.add(key)
41
+ if rr.exists():
42
+ out.append(rr)
43
+ return out
44
+
45
+
10
46
  def find_codex_sessions_for_project(project_path: Path, days_back: int = 7) -> List[Path]:
11
47
  """
12
48
  Find Codex sessions for a given project path.
@@ -22,9 +58,8 @@ def find_codex_sessions_for_project(project_path: Path, days_back: int = 7) -> L
22
58
  Returns:
23
59
  List of session file paths that match the project, sorted by timestamp (newest first)
24
60
  """
25
- codex_sessions_base = Path.home() / ".codex" / "sessions"
26
-
27
- if not codex_sessions_base.exists():
61
+ codex_session_roots = _codex_session_roots()
62
+ if not codex_session_roots:
28
63
  return []
29
64
 
30
65
  # Normalize project path for comparison
@@ -32,36 +67,37 @@ def find_codex_sessions_for_project(project_path: Path, days_back: int = 7) -> L
32
67
 
33
68
  matching_sessions = []
34
69
 
35
- # Search through recent days
36
- for days_ago in range(days_back + 1):
37
- target_date = datetime.now() - timedelta(days=days_ago)
38
- date_path = (
39
- codex_sessions_base
40
- / str(target_date.year)
41
- / f"{target_date.month:02d}"
42
- / f"{target_date.day:02d}"
43
- )
44
-
45
- if not date_path.exists():
46
- continue
47
-
48
- # Check all session files in this date directory
49
- for session_file in date_path.glob("rollout-*.jsonl"):
50
- try:
51
- # Read first line to get session metadata
52
- with open(session_file, "r", encoding="utf-8") as f:
53
- first_line = f.readline()
54
- if first_line:
55
- data = json.loads(first_line)
56
- if data.get("type") == "session_meta":
57
- session_cwd = data.get("payload", {}).get("cwd", "")
58
- # Match the project path
59
- if session_cwd == abs_project_path:
60
- matching_sessions.append(session_file)
61
- except (json.JSONDecodeError, IOError):
62
- # Skip malformed or unreadable files
70
+ # Search through recent days in each root (YYYY/MM/DD layout).
71
+ for root in codex_session_roots:
72
+ for days_ago in range(days_back + 1):
73
+ target_date = datetime.now() - timedelta(days=days_ago)
74
+ date_path = (
75
+ root
76
+ / str(target_date.year)
77
+ / f"{target_date.month:02d}"
78
+ / f"{target_date.day:02d}"
79
+ )
80
+
81
+ if not date_path.exists():
63
82
  continue
64
83
 
84
+ # Check all session files in this date directory
85
+ for session_file in date_path.glob("rollout-*.jsonl"):
86
+ try:
87
+ # Read first line to get session metadata
88
+ with open(session_file, "r", encoding="utf-8") as f:
89
+ first_line = f.readline()
90
+ if first_line:
91
+ data = json.loads(first_line)
92
+ if data.get("type") == "session_meta":
93
+ session_cwd = data.get("payload", {}).get("cwd", "")
94
+ # Match the project path
95
+ if session_cwd == abs_project_path:
96
+ matching_sessions.append(session_file)
97
+ except (json.JSONDecodeError, IOError):
98
+ # Skip malformed or unreadable files
99
+ continue
100
+
65
101
  # Sort by modification time, newest first
66
102
  matching_sessions.sort(key=lambda p: p.stat().st_mtime, reverse=True)
67
103
 
@@ -90,8 +126,12 @@ def get_codex_sessions_dir() -> Optional[Path]:
90
126
  Returns:
91
127
  Path to ~/.codex/sessions if it exists, None otherwise
92
128
  """
129
+ # Preserve old API: return default location if present, else the first discovered root.
93
130
  codex_sessions = Path.home() / ".codex" / "sessions"
94
- return codex_sessions if codex_sessions.exists() else None
131
+ if codex_sessions.exists():
132
+ return codex_sessions
133
+ roots = _codex_session_roots()
134
+ return roots[0] if roots else None
95
135
 
96
136
 
97
137
  def auto_detect_codex_sessions(