aline-ai 0.6.6__py3-none-any.whl → 0.6.7__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.6.6.dist-info → aline_ai-0.6.7.dist-info}/METADATA +1 -1
- {aline_ai-0.6.6.dist-info → aline_ai-0.6.7.dist-info}/RECORD +25 -25
- realign/__init__.py +1 -1
- realign/agent_names.py +2 -2
- realign/claude_hooks/terminal_state.py +32 -1
- realign/codex_detector.py +17 -2
- realign/codex_home.py +24 -6
- realign/commands/doctor.py +74 -1
- realign/commands/export_shares.py +151 -0
- realign/commands/import_shares.py +203 -1
- realign/commands/sync_agent.py +347 -0
- realign/dashboard/screens/create_agent_info.py +131 -20
- realign/dashboard/styles/dashboard.tcss +0 -73
- realign/dashboard/tmux_manager.py +36 -10
- realign/dashboard/widgets/__init__.py +0 -2
- realign/dashboard/widgets/agents_panel.py +142 -23
- realign/db/base.py +43 -1
- realign/db/schema.py +60 -2
- realign/db/sqlite_db.py +176 -1
- realign/watcher_core.py +133 -2
- realign/worker_core.py +37 -2
- realign/dashboard/widgets/terminal_panel.py +0 -1688
- {aline_ai-0.6.6.dist-info → aline_ai-0.6.7.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.6.dist-info → aline_ai-0.6.7.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.6.dist-info → aline_ai-0.6.7.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.6.dist-info → aline_ai-0.6.7.dist-info}/top_level.txt +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
aline_ai-0.6.
|
|
2
|
-
realign/__init__.py,sha256=
|
|
3
|
-
realign/agent_names.py,sha256=
|
|
1
|
+
aline_ai-0.6.7.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
|
|
2
|
+
realign/__init__.py,sha256=_XHUZdw-wtPtpgm845liUkFT-VXg86ZiqDK3pdnnh9Q,1623
|
|
3
|
+
realign/agent_names.py,sha256=H4oVJMkqg1ZYCk58vD_Jh9apaAHSFJRswa-C9SPdJxc,1171
|
|
4
4
|
realign/auth.py,sha256=d_1yvCwluN5iIrdgjtuSKpOYAksDzrzNgntKacLVJrw,16583
|
|
5
5
|
realign/claude_detector.py,sha256=ZLSJacMo6zzQclXByABKA70UNpstxqIv3fPGqdpA934,2792
|
|
6
6
|
realign/cli.py,sha256=IctmQ0OTb6kLlWRFRQumdhY6-CpcpFtocdc68KiwxvM,37748
|
|
7
|
-
realign/codex_detector.py,sha256=
|
|
8
|
-
realign/codex_home.py,sha256=
|
|
7
|
+
realign/codex_detector.py,sha256=WGIClvlrFVCqJ5vR9DrKVsp1eJhOShvcaXibTHb0Nfc,6304
|
|
8
|
+
realign/codex_home.py,sha256=ljkW8uCfQD4cisEJtPNQmIgaR0yEfWSyHwoVQFY-6p4,4374
|
|
9
9
|
realign/codex_terminal_linker.py,sha256=L2Ha4drlZ7Sbq2jzXyxczOdUY3S5fu1gJqoI5WN9CKk,6211
|
|
10
10
|
realign/config.py,sha256=Znfs43AjiK90LGWnArDPWyrE859sdZQAPIb0KAcU3Ig,9252
|
|
11
11
|
realign/context.py,sha256=8hzgNOg-7_eMW22wt7OM5H9IsmMveKXCv0epG7E0G7w,13917
|
|
@@ -16,9 +16,9 @@ realign/logging_config.py,sha256=LCAigKFhTj86PSJm4-kUl3Ag9h_GENh3x2iPnMv7qUI,487
|
|
|
16
16
|
realign/mcp_server.py,sha256=LWiQ2qukYoNLsoV2ID2f0vF9jkJlBvB587HpM5jymgE,10193
|
|
17
17
|
realign/mcp_watcher.py,sha256=aK4jWStv7CoCroS4tXFHgZ_y_-q4QDjrpWgm4DxcEj4,1260
|
|
18
18
|
realign/redactor.py,sha256=Zsoi5HfYak2yPmck20JArhm-1cPSB78IdkBJiNVXfrc,17096
|
|
19
|
-
realign/watcher_core.py,sha256=
|
|
19
|
+
realign/watcher_core.py,sha256=XOJarc_jjlf51Gj8ytcdEeaDUkVIq3Ow0bMbFHbKfAM,116690
|
|
20
20
|
realign/watcher_daemon.py,sha256=OHUQ9P1LlagKJHfrf6uRnzO-zDtBRXIxt8ydMFHf5S8,3475
|
|
21
|
-
realign/worker_core.py,sha256=
|
|
21
|
+
realign/worker_core.py,sha256=IXDFvkmeboOUvWyNJ3iZ7xlfxAulPnmFlAtuuJSdgRo,12362
|
|
22
22
|
realign/worker_daemon.py,sha256=X7Xyjw_u6m6KG4E84nx0HpDFw4cWMv8ja1G8btc9PiM,3957
|
|
23
23
|
realign/adapters/__init__.py,sha256=alkJr7DRn_CrJecSJRjRJOHHnkz9EnZ5TnsU8n1Bb0k,719
|
|
24
24
|
realign/adapters/base.py,sha256=2IdAZKGjg5gPB3YLf_8r3V4XAdbK7fHpj06GjjsYEFY,7409
|
|
@@ -31,7 +31,7 @@ realign/claude_hooks/permission_request_hook.py,sha256=jMN7UtL6bMqHObUCP5A5ysvFr
|
|
|
31
31
|
realign/claude_hooks/permission_request_hook_installer.py,sha256=_8Wr_L5MES7iGukJzcaj4bqR0BH8kFL44U_X4iKtw2Y,7791
|
|
32
32
|
realign/claude_hooks/stop_hook.py,sha256=Bzf6CjHQ-0q61SrDrpIvcwt_BmDO1FE-f8cws_aA-Is,13582
|
|
33
33
|
realign/claude_hooks/stop_hook_installer.py,sha256=uyqKOqpix7CQP64ERBvvh7viSPp_wx_JVGNAX18rKh0,7228
|
|
34
|
-
realign/claude_hooks/terminal_state.py,sha256=
|
|
34
|
+
realign/claude_hooks/terminal_state.py,sha256=2ygTbVnh2b59vRLuN-TyWcXR94NKFlaVwOhS3ipqn58,6647
|
|
35
35
|
realign/claude_hooks/user_prompt_submit_hook.py,sha256=8e0zNonT95TH2uuISYp3am_RD7c84Ghh1WRPgs023DI,10625
|
|
36
36
|
realign/claude_hooks/user_prompt_submit_hook_installer.py,sha256=2xLF8yZcE7Iwib9gU-xCkA1NWxNH9Nc5CFKPYK7rtXw,5371
|
|
37
37
|
realign/commands/__init__.py,sha256=WVaVT1orM2Z0PYaG3X6tkKb_t2v3n_3siCadh1qd_QA,107
|
|
@@ -40,12 +40,13 @@ realign/commands/agent.py,sha256=3CS48bMn7tkdDWKRrfg7CYbhcJK4Pz40YjYMvwD7c2w,317
|
|
|
40
40
|
realign/commands/auth.py,sha256=QrPukpP-ogYEDSwztV0NOYI-HDgn5fPxlCQ1-e2n7gU,11082
|
|
41
41
|
realign/commands/config.py,sha256=nYnu_h2pk7GODcrzrV04K51D-s7v06FlRXHJ0HJ-gvU,6732
|
|
42
42
|
realign/commands/context.py,sha256=pM2KfZHVkB-ou4nBhFvKSwnYliLBzwN3zerLyBAbhfE,7095
|
|
43
|
-
realign/commands/doctor.py,sha256=
|
|
44
|
-
realign/commands/export_shares.py,sha256=
|
|
45
|
-
realign/commands/import_shares.py,sha256=
|
|
43
|
+
realign/commands/doctor.py,sha256=0c1TZuA_cw1CSU0yKMVRU-18uTxdqjXKJ8lP2CTTNSQ,20656
|
|
44
|
+
realign/commands/export_shares.py,sha256=b8dpVBx2HkbHVk9pSFXnErlAr0umciAOPpuxvTJyOBI,148467
|
|
45
|
+
realign/commands/import_shares.py,sha256=qAH007WCQ6bwWP09MEJVmgJlRC8c-QicB2HYvMBqyRM,32966
|
|
46
46
|
realign/commands/init.py,sha256=6rBr1LVIrQLbUH_UvoDhkF1qXmMh2xkjNWCYAUz5Tho,35274
|
|
47
47
|
realign/commands/restore.py,sha256=s2BxQZHxQw9r12NzRVsK20KlGafy5AIoSjWMo5PcnHY,11173
|
|
48
48
|
realign/commands/search.py,sha256=QlUDzRDD6ebq21LTtLe5-OZM62iwDrDqfbnXbuxfklU,27516
|
|
49
|
+
realign/commands/sync_agent.py,sha256=XRcHN00TjfzGwTw3O_OXqb9Yj0lMFfDX0S7oizVpS6E,12454
|
|
49
50
|
realign/commands/upgrade.py,sha256=L3PLOUIN5qAQTbkfoVtSsIbbzEezA_xjjk9F1GMVfjw,12781
|
|
50
51
|
realign/commands/watcher.py,sha256=4WTThIgr-Z5guKh_JqGDcPmerr97XiHrVaaijmckHsA,134350
|
|
51
52
|
realign/commands/worker.py,sha256=jTu7Pj60nTnn7SsH3oNCNnO6zl4TIFCJVNSC1OoQ_0o,23363
|
|
@@ -54,38 +55,37 @@ realign/dashboard/app.py,sha256=e257euP0gR9nA0w1susuLkG9tnYQk1IJJdgAICnIYxs,1039
|
|
|
54
55
|
realign/dashboard/clipboard.py,sha256=81frq83E_urqLkwuCvtl0hiTEjavtdQn8kCi72jJWcs,1207
|
|
55
56
|
realign/dashboard/layout.py,sha256=sZxmFj6QTbkois9MHTvBEMMcnaRVehCDqugdbiFx10k,9072
|
|
56
57
|
realign/dashboard/terminal_backend.py,sha256=MlDfwtqhftyQK6jDNizQGFjAWIo5Bx2TDpSnP3MCZVM,3375
|
|
57
|
-
realign/dashboard/tmux_manager.py,sha256=
|
|
58
|
+
realign/dashboard/tmux_manager.py,sha256=sS6fo7UVPHWxYm1RYtLDPmwsagFh5RO6TRwYd1CuHaI,34581
|
|
58
59
|
realign/dashboard/backends/__init__.py,sha256=POROX7YKtukYZcLB1pi_kO0sSEpuO3y-hwmF3WIN1Kk,163
|
|
59
60
|
realign/dashboard/backends/iterm2.py,sha256=XYYJT5lrrp4pW_MyEqPZYkRI0qyKUwJlezwMidgnsHc,21390
|
|
60
61
|
realign/dashboard/backends/kitty.py,sha256=5jdkR1f2PwB8a4SnS3EG6uOQ2XU-PB7-cpKBfIJq3hU,12066
|
|
61
62
|
realign/dashboard/screens/__init__.py,sha256=MiefFamCYRrzTwQXiCUdybaJaFxlK5XKtLHaSQmqDv0,597
|
|
62
63
|
realign/dashboard/screens/agent_detail.py,sha256=N-iUC4434C91OcDu4dkQaxS_NXQ5Yl5sqNBb2mTmoBw,10490
|
|
63
64
|
realign/dashboard/screens/create_agent.py,sha256=06uiQYvz-Xvn4Xm689o3tdhzb2HQ0gdzAA1WHVEwziM,11706
|
|
64
|
-
realign/dashboard/screens/create_agent_info.py,sha256=
|
|
65
|
+
realign/dashboard/screens/create_agent_info.py,sha256=K2Rbp4zHVdanPT3Fp82We4qlSAM-0IBZXPLuQuevuME,7838
|
|
65
66
|
realign/dashboard/screens/create_event.py,sha256=oiQY1zKpUYnQU-5fQLeuZH9BV5NClE5B5XZIVBYG5A8,5506
|
|
66
67
|
realign/dashboard/screens/event_detail.py,sha256=-pqt3NBoeTXGJKtbndZy-msklwXTeNWMS4H12oMG5ks,20175
|
|
67
68
|
realign/dashboard/screens/help_screen.py,sha256=Icrcvbgyz49R2tBiu8vBZ4CLm6iYclv_-FTa2pCFRRQ,3398
|
|
68
69
|
realign/dashboard/screens/session_detail.py,sha256=TBkHqSHyMxsLB2QdZq9m1EoiH8oRVDbPrjt-a8I9sHs,9561
|
|
69
70
|
realign/dashboard/screens/share_import.py,sha256=hl2x0yGVycsoUI76AmdZTAV-br3Q6191g5xHHrZ8hOA,6318
|
|
70
|
-
realign/dashboard/styles/dashboard.tcss,sha256=
|
|
71
|
-
realign/dashboard/widgets/__init__.py,sha256=
|
|
72
|
-
realign/dashboard/widgets/agents_panel.py,sha256=
|
|
71
|
+
realign/dashboard/styles/dashboard.tcss,sha256=9W5Tx0lgyGb4HU-z-Kn7gBdexIK0aPe0bkVn2k_AseM,3288
|
|
72
|
+
realign/dashboard/widgets/__init__.py,sha256=33qjCa6WCQ7XojRiStdR73jX2xpKV_RlBqodVDQWkxs,577
|
|
73
|
+
realign/dashboard/widgets/agents_panel.py,sha256=TtOX9RlF0CuwRTe1sXoo1xaf7ZykJA-YFmMu0-SKe2g,43299
|
|
73
74
|
realign/dashboard/widgets/config_panel.py,sha256=eRJRuqImQ8eJIKCEj4O8EvYxI-ht_anrcYbT5JskWyU,15972
|
|
74
75
|
realign/dashboard/widgets/events_table.py,sha256=0cMvE0KdZFBZyvywv7vlt005qsR0aLQnQiMf3ZzK7RY,30218
|
|
75
76
|
realign/dashboard/widgets/header.py,sha256=0HHCFXX7F3C6HII-WDwOJwWkJrajmKPWmdoMWyOkn9E,1587
|
|
76
77
|
realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
|
|
77
78
|
realign/dashboard/widgets/search_panel.py,sha256=ZNJDfwDSxUFnCeltYQYsQsPJ6t4HDeNWpENoTOoBdVM,8951
|
|
78
79
|
realign/dashboard/widgets/sessions_table.py,sha256=6y78pEkyAmNsU4_o46PbwXRFW17fc5khgheBi4LjBNg,33374
|
|
79
|
-
realign/dashboard/widgets/terminal_panel.py,sha256=at8whXa8Bsn_icbyerHG21tb2BsnQikAMlf4NfIpTGw,61504
|
|
80
80
|
realign/dashboard/widgets/watcher_panel.py,sha256=emVY1-aot9Dnf5UI9yyNeEmp4d2Gb-lrC28DjkeLjKA,19575
|
|
81
81
|
realign/dashboard/widgets/worker_panel.py,sha256=F_jKWABuCNmjQgeeuCr4KnFRKdY4CLTNcEXMYwsNaSk,18691
|
|
82
82
|
realign/db/__init__.py,sha256=65LsNdsq_rkwNC1eg1OAr3HC0ORXtelOh0I8MhNGr-g,3288
|
|
83
|
-
realign/db/base.py,sha256=
|
|
83
|
+
realign/db/base.py,sha256=ShufW-c0ntKYsTWCbiXJ5W-G_H_mWN4YlnUuspWWu34,15589
|
|
84
84
|
realign/db/locks.py,sha256=dUQu9Yo5nZstMSPXZPYzN0xqX8UXhJgNV_PmYEJ-rK0,1801
|
|
85
85
|
realign/db/migrate_agents.py,sha256=cDeVUzKW950dJ0lV74QObHuONqKwErSrXI5akU2vBmQ,9633
|
|
86
86
|
realign/db/migration.py,sha256=af1QFEfIh_qX0pFyXzm5gWFVbQn0sKOUNLSJHlr__FU,13405
|
|
87
|
-
realign/db/schema.py,sha256=
|
|
88
|
-
realign/db/sqlite_db.py,sha256=
|
|
87
|
+
realign/db/schema.py,sha256=IWPbeDYrbC1eZGQAy8k1rk0r2NnABJzXSSg8bb00XBw,33885
|
|
88
|
+
realign/db/sqlite_db.py,sha256=u4yybbXzOApYPnHkHlR59qBSyWPoIqgRppTB4ht5taM,119736
|
|
89
89
|
realign/events/__init__.py,sha256=IM-NxF4Zk2hYFD07k4WrfNRuuiC9ihGjf4GBpJhjd2E,35
|
|
90
90
|
realign/events/agent_summarizer.py,sha256=vh65tYgo1NOYsIpVPR253nnOr-MIejC4KG5dGvDzKv4,5413
|
|
91
91
|
realign/events/debouncer.py,sha256=U3Q7dYpnMsAgWsW_E_IbSC4lrdEoi6H_SFLGLOAazs4,3062
|
|
@@ -104,8 +104,8 @@ realign/triggers/next_turn_trigger.py,sha256=-x80_I-WmIjXXzQHEPBykgx_GQW6oKaLDQx
|
|
|
104
104
|
realign/triggers/registry.py,sha256=dkIjSd8Bg-hF0nxaO2Fi2K-0Zipqv6vVjc-HYSrA_fY,3656
|
|
105
105
|
realign/triggers/turn_status.py,sha256=wAZEhXDAmDoX5F-ohWfSnZZ0eA6DAJ9svSPiSv_f6sg,6041
|
|
106
106
|
realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
|
|
107
|
-
aline_ai-0.6.
|
|
108
|
-
aline_ai-0.6.
|
|
109
|
-
aline_ai-0.6.
|
|
110
|
-
aline_ai-0.6.
|
|
111
|
-
aline_ai-0.6.
|
|
107
|
+
aline_ai-0.6.7.dist-info/METADATA,sha256=GyI08kzWpN5QDEsgynocnlk8Cp5zSTiKXRltp86xrsM,1597
|
|
108
|
+
aline_ai-0.6.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
109
|
+
aline_ai-0.6.7.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
|
|
110
|
+
aline_ai-0.6.7.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
|
|
111
|
+
aline_ai-0.6.7.dist-info/RECORD,,
|
realign/__init__.py
CHANGED
realign/agent_names.py
CHANGED
|
@@ -75,5 +75,5 @@ _rng = random.SystemRandom()
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
def generate_agent_name() -> str:
|
|
78
|
-
"""Return a random
|
|
79
|
-
return f"{_rng.choice(ADJECTIVES)}
|
|
78
|
+
"""Return a random name, e.g. 'Bold Turing'."""
|
|
79
|
+
return f"{_rng.choice(ADJECTIVES).capitalize()} {_rng.choice(SURNAMES).capitalize()}"
|
|
@@ -41,7 +41,18 @@ def _get_db():
|
|
|
41
41
|
|
|
42
42
|
return get_database(read_only=False)
|
|
43
43
|
except Exception:
|
|
44
|
-
|
|
44
|
+
try:
|
|
45
|
+
import sys
|
|
46
|
+
|
|
47
|
+
root = Path(__file__).resolve().parents[2]
|
|
48
|
+
root_str = str(root)
|
|
49
|
+
if root_str not in sys.path:
|
|
50
|
+
sys.path.insert(0, root_str)
|
|
51
|
+
from realign.db import get_database # type: ignore
|
|
52
|
+
|
|
53
|
+
return get_database(read_only=False)
|
|
54
|
+
except Exception:
|
|
55
|
+
return None
|
|
45
56
|
|
|
46
57
|
|
|
47
58
|
def _write_to_db(
|
|
@@ -67,6 +78,10 @@ def _write_to_db(
|
|
|
67
78
|
if not db:
|
|
68
79
|
return False
|
|
69
80
|
|
|
81
|
+
# Force source to agent mapping when agent_id is known
|
|
82
|
+
if agent_id:
|
|
83
|
+
source = f"agent:{agent_id}"
|
|
84
|
+
|
|
70
85
|
# Check if agent exists
|
|
71
86
|
existing = db.get_agent_by_id(terminal_id)
|
|
72
87
|
if existing:
|
|
@@ -105,6 +120,19 @@ def _write_to_db(
|
|
|
105
120
|
except Exception:
|
|
106
121
|
pass
|
|
107
122
|
|
|
123
|
+
# WindowLink: record terminal/session association (V23)
|
|
124
|
+
try:
|
|
125
|
+
db.insert_window_link(
|
|
126
|
+
terminal_id=terminal_id,
|
|
127
|
+
agent_id=agent_id,
|
|
128
|
+
session_id=session_id,
|
|
129
|
+
provider=provider,
|
|
130
|
+
source=source,
|
|
131
|
+
ts=time.time(),
|
|
132
|
+
)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
108
136
|
# Note: Don't close - get_database() returns a singleton
|
|
109
137
|
return True
|
|
110
138
|
except Exception:
|
|
@@ -134,6 +162,9 @@ def update_terminal_mapping(
|
|
|
134
162
|
Concurrency: uses a simple fcntl lock file for JSON; last writer wins, but updates are atomic.
|
|
135
163
|
"""
|
|
136
164
|
# Phase 1: Write to database (best-effort, don't fail if DB unavailable)
|
|
165
|
+
if agent_id:
|
|
166
|
+
source = f"agent:{agent_id}"
|
|
167
|
+
|
|
137
168
|
_write_to_db(
|
|
138
169
|
terminal_id=terminal_id,
|
|
139
170
|
provider=provider,
|
realign/codex_detector.py
CHANGED
|
@@ -21,8 +21,23 @@ def _codex_session_roots() -> list[Path]:
|
|
|
21
21
|
homes = aline_codex_homes_dir()
|
|
22
22
|
if homes.exists():
|
|
23
23
|
for child in homes.iterdir():
|
|
24
|
-
if child.is_dir():
|
|
25
|
-
|
|
24
|
+
if not child.is_dir():
|
|
25
|
+
continue
|
|
26
|
+
if child.name.startswith("agent-"):
|
|
27
|
+
# New layout: agent-<id>/<terminal_id>/sessions
|
|
28
|
+
try:
|
|
29
|
+
for grandchild in child.iterdir():
|
|
30
|
+
if grandchild.is_dir():
|
|
31
|
+
nested_sessions = codex_sessions_dir_for_home(grandchild)
|
|
32
|
+
if nested_sessions.exists():
|
|
33
|
+
roots.append(nested_sessions)
|
|
34
|
+
except Exception:
|
|
35
|
+
continue
|
|
36
|
+
else:
|
|
37
|
+
# Terminal layout: <terminal_id>/sessions
|
|
38
|
+
direct_sessions = codex_sessions_dir_for_home(child)
|
|
39
|
+
if direct_sessions.exists():
|
|
40
|
+
roots.append(direct_sessions)
|
|
26
41
|
except Exception:
|
|
27
42
|
pass
|
|
28
43
|
|
realign/codex_home.py
CHANGED
|
@@ -39,7 +39,7 @@ def codex_home_for_agent(agent_id: str) -> Path:
|
|
|
39
39
|
|
|
40
40
|
def codex_home_for_terminal_or_agent(terminal_id: str, agent_id: Optional[str]) -> Path:
|
|
41
41
|
if agent_id:
|
|
42
|
-
return codex_home_for_agent(agent_id)
|
|
42
|
+
return codex_home_for_agent(agent_id) / _safe_id(terminal_id)
|
|
43
43
|
return codex_home_for_terminal(terminal_id)
|
|
44
44
|
|
|
45
45
|
|
|
@@ -76,11 +76,17 @@ def codex_home_owner_from_session_file(session_file: Path) -> Optional[tuple[str
|
|
|
76
76
|
owner = (parts[0] or "").strip()
|
|
77
77
|
if not owner:
|
|
78
78
|
return None
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
# Terminal layout: <homes>/<terminal_id>/sessions/...
|
|
80
|
+
if parts[1] == "sessions":
|
|
81
|
+
if owner.startswith(AGENT_HOME_PREFIX):
|
|
82
|
+
return ("agent", owner[len(AGENT_HOME_PREFIX):])
|
|
83
|
+
return ("terminal", owner)
|
|
84
|
+
# Agent/terminal layout: <homes>/agent-<agent_id>/<terminal_id>/sessions/...
|
|
85
|
+
if owner.startswith(AGENT_HOME_PREFIX) and len(parts) >= 4 and parts[2] == "sessions":
|
|
86
|
+
terminal_id = (parts[1] or "").strip()
|
|
87
|
+
if terminal_id:
|
|
88
|
+
return ("terminal", terminal_id)
|
|
89
|
+
return None
|
|
84
90
|
|
|
85
91
|
|
|
86
92
|
def terminal_id_from_codex_session_file(session_file: Path) -> Optional[str]:
|
|
@@ -113,4 +119,16 @@ def prepare_codex_home(terminal_id: str, *, agent_id: Optional[str] = None) -> P
|
|
|
113
119
|
except Exception:
|
|
114
120
|
pass
|
|
115
121
|
|
|
122
|
+
# Reuse global auth/config to avoid re-login for per-terminal homes.
|
|
123
|
+
try:
|
|
124
|
+
global_home = Path.home() / ".codex"
|
|
125
|
+
for name in ("auth.json", "config.toml"):
|
|
126
|
+
src = global_home / name
|
|
127
|
+
dst = home / name
|
|
128
|
+
if src.exists() and not dst.exists():
|
|
129
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
dst.symlink_to(src)
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
|
|
116
134
|
return home
|
realign/commands/doctor.py
CHANGED
|
@@ -435,6 +435,17 @@ def run_doctor(
|
|
|
435
435
|
except Exception as e:
|
|
436
436
|
console.print(f" [yellow]![/yellow] Failed to check jobs: {e}")
|
|
437
437
|
|
|
438
|
+
# 5b. Repair agent/session associations
|
|
439
|
+
console.print("\n[bold]5b. Repairing agent/session associations...[/bold]")
|
|
440
|
+
try:
|
|
441
|
+
repaired = _repair_agent_session_links(verbose=verbose)
|
|
442
|
+
if repaired > 0:
|
|
443
|
+
console.print(f" [green]✓[/green] Repaired {repaired} session(s)")
|
|
444
|
+
else:
|
|
445
|
+
console.print(" [green]✓[/green] No missing associations found")
|
|
446
|
+
except Exception as e:
|
|
447
|
+
console.print(f" [yellow]![/yellow] Failed to repair associations: {e}")
|
|
448
|
+
|
|
438
449
|
# 6. Restart/ensure daemons
|
|
439
450
|
if restart_daemons:
|
|
440
451
|
console.print("\n[bold]6. Checking daemons...[/bold]")
|
|
@@ -472,6 +483,69 @@ def run_doctor(
|
|
|
472
483
|
return 0
|
|
473
484
|
|
|
474
485
|
|
|
486
|
+
def _repair_agent_session_links(*, verbose: bool = False) -> int:
|
|
487
|
+
"""Backfill sessions.agent_id using windowlink and agents mappings."""
|
|
488
|
+
try:
|
|
489
|
+
from ..db import get_database
|
|
490
|
+
|
|
491
|
+
db = get_database(read_only=False)
|
|
492
|
+
except Exception:
|
|
493
|
+
return 0
|
|
494
|
+
|
|
495
|
+
repaired = 0
|
|
496
|
+
|
|
497
|
+
# 1) Use windowlink latest records
|
|
498
|
+
try:
|
|
499
|
+
links = db.list_latest_window_links(limit=5000)
|
|
500
|
+
except Exception:
|
|
501
|
+
links = []
|
|
502
|
+
|
|
503
|
+
for link in links:
|
|
504
|
+
session_id = (link.session_id or "").strip()
|
|
505
|
+
agent_id = (link.agent_id or "").strip()
|
|
506
|
+
if not session_id or not agent_id:
|
|
507
|
+
continue
|
|
508
|
+
try:
|
|
509
|
+
session = db.get_session_by_id(session_id)
|
|
510
|
+
if session and getattr(session, "agent_id", None):
|
|
511
|
+
continue
|
|
512
|
+
except Exception:
|
|
513
|
+
pass
|
|
514
|
+
try:
|
|
515
|
+
db.update_session_agent_id(session_id, agent_id)
|
|
516
|
+
repaired += 1
|
|
517
|
+
except Exception:
|
|
518
|
+
continue
|
|
519
|
+
|
|
520
|
+
# 2) Fallback: agents table mapping (session_id -> agent source)
|
|
521
|
+
try:
|
|
522
|
+
agents = db.list_agents(status=None, limit=5000)
|
|
523
|
+
except Exception:
|
|
524
|
+
agents = []
|
|
525
|
+
for agent in agents:
|
|
526
|
+
session_id = (agent.session_id or "").strip()
|
|
527
|
+
source = (agent.source or "").strip()
|
|
528
|
+
if not session_id or not source.startswith("agent:"):
|
|
529
|
+
continue
|
|
530
|
+
agent_id = source[6:]
|
|
531
|
+
try:
|
|
532
|
+
session = db.get_session_by_id(session_id)
|
|
533
|
+
if session and getattr(session, "agent_id", None):
|
|
534
|
+
continue
|
|
535
|
+
except Exception:
|
|
536
|
+
pass
|
|
537
|
+
try:
|
|
538
|
+
db.update_session_agent_id(session_id, agent_id)
|
|
539
|
+
repaired += 1
|
|
540
|
+
except Exception:
|
|
541
|
+
continue
|
|
542
|
+
|
|
543
|
+
if verbose and repaired:
|
|
544
|
+
console.print(f" [dim]Repaired session links: {repaired}[/dim]")
|
|
545
|
+
|
|
546
|
+
return repaired
|
|
547
|
+
|
|
548
|
+
|
|
475
549
|
def doctor_command(
|
|
476
550
|
no_restart: bool = typer.Option(False, "--no-restart", help="Only repair files, don't restart daemons"),
|
|
477
551
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
@@ -494,4 +568,3 @@ def doctor_command(
|
|
|
494
568
|
clear_cache=True,
|
|
495
569
|
)
|
|
496
570
|
raise typer.Exit(code=exit_code)
|
|
497
|
-
|
|
@@ -1676,6 +1676,49 @@ def _extend_share_expiry(
|
|
|
1676
1676
|
return None
|
|
1677
1677
|
|
|
1678
1678
|
|
|
1679
|
+
def _update_share_content(
|
|
1680
|
+
backend_url: str,
|
|
1681
|
+
share_id: str,
|
|
1682
|
+
token: str,
|
|
1683
|
+
conversation_data: dict,
|
|
1684
|
+
expected_version: int = 0,
|
|
1685
|
+
) -> dict:
|
|
1686
|
+
"""
|
|
1687
|
+
Push updated content to an existing share via PUT /api/share/{id}.
|
|
1688
|
+
|
|
1689
|
+
Args:
|
|
1690
|
+
backend_url: Backend server URL
|
|
1691
|
+
share_id: Share ID on the server
|
|
1692
|
+
token: Admin or contributor token for auth
|
|
1693
|
+
conversation_data: Full conversation data to replace current content
|
|
1694
|
+
expected_version: Optimistic locking version (409 on mismatch)
|
|
1695
|
+
|
|
1696
|
+
Returns:
|
|
1697
|
+
Response dict with success, version fields
|
|
1698
|
+
|
|
1699
|
+
Raises:
|
|
1700
|
+
RuntimeError on upload failure
|
|
1701
|
+
httpx.HTTPStatusError with 409 status on version conflict
|
|
1702
|
+
"""
|
|
1703
|
+
if not HTTPX_AVAILABLE:
|
|
1704
|
+
raise RuntimeError("httpx package not installed. Run: pip install httpx")
|
|
1705
|
+
|
|
1706
|
+
headers = {
|
|
1707
|
+
"X-Token": token,
|
|
1708
|
+
"X-Expected-Version": str(expected_version),
|
|
1709
|
+
"Content-Type": "application/json",
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
response = httpx.put(
|
|
1713
|
+
f"{backend_url}/api/share/{share_id}",
|
|
1714
|
+
headers=headers,
|
|
1715
|
+
json={"conversation_data": conversation_data},
|
|
1716
|
+
timeout=60.0,
|
|
1717
|
+
)
|
|
1718
|
+
response.raise_for_status()
|
|
1719
|
+
return response.json()
|
|
1720
|
+
|
|
1721
|
+
|
|
1679
1722
|
def _standard_upload(
|
|
1680
1723
|
encrypted_payload: dict,
|
|
1681
1724
|
metadata: dict,
|
|
@@ -3763,6 +3806,67 @@ def export_agent_shares_command(
|
|
|
3763
3806
|
print(f"Error: Agent not found: {agent_id}", file=sys.stderr)
|
|
3764
3807
|
return 1
|
|
3765
3808
|
|
|
3809
|
+
# Check for existing share (re-share → sync instead of new link)
|
|
3810
|
+
if (
|
|
3811
|
+
not password
|
|
3812
|
+
and agent_info.share_url
|
|
3813
|
+
and agent_info.share_id
|
|
3814
|
+
and (agent_info.share_admin_token or agent_info.share_contributor_token)
|
|
3815
|
+
):
|
|
3816
|
+
_progress("Agent already shared, syncing...")
|
|
3817
|
+
try:
|
|
3818
|
+
from .sync_agent import sync_agent_command
|
|
3819
|
+
|
|
3820
|
+
sync_result = sync_agent_command(
|
|
3821
|
+
agent_id=agent_id,
|
|
3822
|
+
backend_url=backend_url,
|
|
3823
|
+
progress_callback=progress_callback,
|
|
3824
|
+
)
|
|
3825
|
+
if sync_result.get("success"):
|
|
3826
|
+
# Extend expiry if we have admin token
|
|
3827
|
+
if agent_info.share_admin_token:
|
|
3828
|
+
try:
|
|
3829
|
+
_extend_share_expiry(
|
|
3830
|
+
backend_url=backend_url,
|
|
3831
|
+
share_id=agent_info.share_id,
|
|
3832
|
+
admin_token=agent_info.share_admin_token,
|
|
3833
|
+
expiry_days=expiry_days,
|
|
3834
|
+
)
|
|
3835
|
+
except Exception as ext_err:
|
|
3836
|
+
logger.warning(f"Failed to extend share expiry: {ext_err}")
|
|
3837
|
+
|
|
3838
|
+
if json_output:
|
|
3839
|
+
output_data = {
|
|
3840
|
+
"agent_id": agent_id,
|
|
3841
|
+
"agent_name": agent_info.name,
|
|
3842
|
+
"share_link": agent_info.share_url,
|
|
3843
|
+
"synced": True,
|
|
3844
|
+
"sessions_pulled": sync_result.get("sessions_pulled", 0),
|
|
3845
|
+
"sessions_pushed": sync_result.get("sessions_pushed", 0),
|
|
3846
|
+
}
|
|
3847
|
+
print(json.dumps(output_data, ensure_ascii=False, indent=2))
|
|
3848
|
+
else:
|
|
3849
|
+
pulled = sync_result.get("sessions_pulled", 0)
|
|
3850
|
+
pushed = sync_result.get("sessions_pushed", 0)
|
|
3851
|
+
print(f"\n🔄 Synced agent: {agent_info.name}")
|
|
3852
|
+
print(f" Pulled {pulled} session(s), pushed {pushed} session(s)")
|
|
3853
|
+
print(f"🔗 Share link: {agent_info.share_url}")
|
|
3854
|
+
copied = _copy_share_to_clipboard(agent_info.share_url, None)
|
|
3855
|
+
if copied:
|
|
3856
|
+
print("📋 Copied share link to clipboard.")
|
|
3857
|
+
return 0
|
|
3858
|
+
else:
|
|
3859
|
+
err = sync_result.get("error", "Unknown sync error")
|
|
3860
|
+
logger.warning(f"Sync failed, falling through to new share: {err}")
|
|
3861
|
+
if not json_output:
|
|
3862
|
+
print(f"⚠️ Sync failed ({err}), creating new share link...", file=sys.stderr)
|
|
3863
|
+
except ImportError:
|
|
3864
|
+
logger.warning("sync_agent module not available, creating new share")
|
|
3865
|
+
except Exception as e:
|
|
3866
|
+
logger.warning(f"Sync failed, falling through to new share: {e}")
|
|
3867
|
+
if not json_output:
|
|
3868
|
+
print(f"⚠️ Sync failed ({e}), creating new share link...", file=sys.stderr)
|
|
3869
|
+
|
|
3766
3870
|
# Get sessions for this agent
|
|
3767
3871
|
session_records = db.get_sessions_by_agent_id(agent_id)
|
|
3768
3872
|
if not session_records:
|
|
@@ -3859,6 +3963,13 @@ def export_agent_shares_command(
|
|
|
3859
3963
|
"usage": "Local AI agents can install the aline MCP server and use the 'ask_shared_conversation' tool to query this conversation programmatically.",
|
|
3860
3964
|
}
|
|
3861
3965
|
|
|
3966
|
+
# Include sync_metadata placeholder (contributor_token will be added after upload for unencrypted shares)
|
|
3967
|
+
if not password:
|
|
3968
|
+
conversation_data["sync_metadata"] = {
|
|
3969
|
+
"contributor_token": None, # Will be populated after upload
|
|
3970
|
+
"sync_version": 0,
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3862
3973
|
# Upload to backend (no encryption for agent shares by default)
|
|
3863
3974
|
_progress("Uploading to cloud...")
|
|
3864
3975
|
|
|
@@ -3894,6 +4005,46 @@ def export_agent_shares_command(
|
|
|
3894
4005
|
share_url = result.get("share_url")
|
|
3895
4006
|
slack_message = ui_metadata.get("slack_message") if ui_metadata else None
|
|
3896
4007
|
|
|
4008
|
+
# Store sync metadata for unencrypted shares
|
|
4009
|
+
if not password and share_url:
|
|
4010
|
+
share_id_result = result.get("share_id") or _extract_share_id_from_url(share_url)
|
|
4011
|
+
admin_token = result.get("admin_token")
|
|
4012
|
+
contributor_token = result.get("contributor_token")
|
|
4013
|
+
expiry_at = result.get("expiry_at")
|
|
4014
|
+
|
|
4015
|
+
if share_id_result:
|
|
4016
|
+
try:
|
|
4017
|
+
db.update_agent_sync_metadata(
|
|
4018
|
+
agent_id,
|
|
4019
|
+
share_id=share_id_result,
|
|
4020
|
+
share_url=share_url,
|
|
4021
|
+
share_admin_token=admin_token,
|
|
4022
|
+
share_contributor_token=contributor_token,
|
|
4023
|
+
share_expiry_at=expiry_at,
|
|
4024
|
+
last_synced_at=datetime.now(timezone.utc).isoformat(),
|
|
4025
|
+
sync_version=0,
|
|
4026
|
+
)
|
|
4027
|
+
except Exception as e:
|
|
4028
|
+
logger.warning(f"Failed to store sync metadata: {e}")
|
|
4029
|
+
|
|
4030
|
+
# Re-upload with contributor_token embedded in sync_metadata
|
|
4031
|
+
# so importers can get it
|
|
4032
|
+
if contributor_token:
|
|
4033
|
+
try:
|
|
4034
|
+
conversation_data["sync_metadata"] = {
|
|
4035
|
+
"contributor_token": contributor_token,
|
|
4036
|
+
"sync_version": 0,
|
|
4037
|
+
}
|
|
4038
|
+
_update_share_content(
|
|
4039
|
+
backend_url=backend_url,
|
|
4040
|
+
share_id=share_id_result,
|
|
4041
|
+
token=admin_token or contributor_token,
|
|
4042
|
+
conversation_data=conversation_data,
|
|
4043
|
+
expected_version=0,
|
|
4044
|
+
)
|
|
4045
|
+
except Exception as e:
|
|
4046
|
+
logger.warning(f"Failed to re-upload with sync metadata: {e}")
|
|
4047
|
+
|
|
3897
4048
|
# Output results
|
|
3898
4049
|
if json_output:
|
|
3899
4050
|
output_data = {
|