aline-ai 0.5.1__py3-none-any.whl → 0.5.4__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.1.dist-info → aline_ai-0.5.4.dist-info}/METADATA +1 -1
- {aline_ai-0.5.1.dist-info → aline_ai-0.5.4.dist-info}/RECORD +15 -13
- {aline_ai-0.5.1.dist-info → aline_ai-0.5.4.dist-info}/WHEEL +1 -1
- realign/__init__.py +1 -1
- realign/claude_hooks/permission_request_hook.py +189 -0
- realign/claude_hooks/permission_request_hook_installer.py +241 -0
- realign/claude_hooks/stop_hook.py +28 -0
- realign/cli.py +17 -0
- realign/commands/add.py +303 -0
- realign/commands/init.py +56 -1
- realign/dashboard/tmux_manager.py +88 -15
- realign/dashboard/widgets/terminal_panel.py +187 -5
- {aline_ai-0.5.1.dist-info → aline_ai-0.5.4.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.1.dist-info → aline_ai-0.5.4.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.1.dist-info → aline_ai-0.5.4.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
aline_ai-0.5.
|
|
2
|
-
realign/__init__.py,sha256=
|
|
1
|
+
aline_ai-0.5.4.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
|
|
2
|
+
realign/__init__.py,sha256=IahJlCDpOv2BJxOCpmx8UfhzQxweVN7I9N7YQyT7zMo,1623
|
|
3
3
|
realign/claude_detector.py,sha256=hU5OcFO7JH9BCVbmajAmz4TIP-EQuvz9VlpsRYuSoVM,2792
|
|
4
|
-
realign/cli.py,sha256=
|
|
4
|
+
realign/cli.py,sha256=81zMPACOurb9YzDKdhHyNmnye5lHOv3dbNSuXmKqJ7Y,29783
|
|
5
5
|
realign/codex_detector.py,sha256=vDjRWHycjzX9Pavv8IwrznMf5oyFKBn4FvqYYy3I0s4,4141
|
|
6
6
|
realign/config.py,sha256=fDwXstNF80yNSUOtNJYAqkDEWZOQkzNC7cN0-_2W0KU,12223
|
|
7
7
|
realign/context.py,sha256=ttQL4_Q9FNv6JA85aRslBuu97LeJPoKAgRbShaj_UiU,10006
|
|
@@ -24,18 +24,20 @@ realign/adapters/codex.py,sha256=o9XyZEPDVqskHgXSkCDovu3yJM8x6HTIIobU90MGE3s,207
|
|
|
24
24
|
realign/adapters/gemini.py,sha256=Oaucgz6l_6Cb0GDdt0ant_Pun6B1CfrDMN9B4irIHXc,2636
|
|
25
25
|
realign/adapters/registry.py,sha256=gJf9MSfd0clt653eBfcM17snrSDXeQDwVXal0N2NrHo,3303
|
|
26
26
|
realign/claude_hooks/__init__.py,sha256=-2CiH5UIjPQzok2pBQ8yIVXB5oFxkzLhFXQ8NDDVy0c,594
|
|
27
|
-
realign/claude_hooks/
|
|
27
|
+
realign/claude_hooks/permission_request_hook.py,sha256=jMN7UtL6bMqHObUCP5A5ysvFrooDEcd9KxtmF2-3nCw,6448
|
|
28
|
+
realign/claude_hooks/permission_request_hook_installer.py,sha256=B05ey_7OT3tlJBGzBlmy6DFJdW3OGMZCCrk1HSXl0Cs,7800
|
|
29
|
+
realign/claude_hooks/stop_hook.py,sha256=bcWr9jCDZErijSzVSP-kdfmoZ6DHYc6hkRaJ_A2PmQ8,11989
|
|
28
30
|
realign/claude_hooks/stop_hook_installer.py,sha256=BjzabUrAPLCA0m_8ZQWqDW4eCujdPTzlwUNdVGxtRk4,7238
|
|
29
31
|
realign/claude_hooks/terminal_state.py,sha256=ZvdQ-ZmqEltdMoNk3lXVsbpvbAQEmf2hxTCY_8WFu9g,2586
|
|
30
32
|
realign/claude_hooks/user_prompt_submit_hook.py,sha256=WD-UavhBTueN2TPfnZrnPC7DFYGEeptjUEF21EJn7Qo,10312
|
|
31
33
|
realign/claude_hooks/user_prompt_submit_hook_installer.py,sha256=2xLF8yZcE7Iwib9gU-xCkA1NWxNH9Nc5CFKPYK7rtXw,5371
|
|
32
34
|
realign/commands/__init__.py,sha256=sx_ck55oxaoiF4N3LugG0ZXwonUDxeEZ5uHbBKCC7K8,89
|
|
33
|
-
realign/commands/add.py,sha256=
|
|
35
|
+
realign/commands/add.py,sha256=nkkHETnNprCMKoJuqirddVzJjAiLew7oatLSt3kJOV4,23620
|
|
34
36
|
realign/commands/config.py,sha256=rVwWUgLQDoRh25bjNzsN2eC77aiaPB5D77UtxgS3RlY,6798
|
|
35
37
|
realign/commands/context.py,sha256=tD5jQG4kXPfV57PrS1Du3JUzvY6RCbNbxsOQWdomrPg,7057
|
|
36
38
|
realign/commands/export_shares.py,sha256=6omUtPK8OJsWfrzeQAx5LbM8-lOiONZtWa9btpVumVQ,136179
|
|
37
39
|
realign/commands/import_shares.py,sha256=bDf-_1hnlFbqBvVnLTg8Qm4JheQcjTFhhmiBlg75M7k,25314
|
|
38
|
-
realign/commands/init.py,sha256=
|
|
40
|
+
realign/commands/init.py,sha256=ZJpmB0QhKHzEi8d8g8i5QKgnEFl8V6Kt7vFEPOWHzxM,27454
|
|
39
41
|
realign/commands/restore.py,sha256=59WXsS09EN6wbYgmc1xYdArF-qhCMbmEwyj8vGLmxTc,11057
|
|
40
42
|
realign/commands/search.py,sha256=_ovwHzDNSZEVP4EdOcg9ELQ5y_84BvXRHhKKALHD738,25173
|
|
41
43
|
realign/commands/upgrade.py,sha256=7FuuLTQ3KyH33GD8RWMh1JMwFnVV3naolS-SQ5vxCjE,8547
|
|
@@ -43,7 +45,7 @@ realign/commands/watcher.py,sha256=6EBzIc439ClqS4UG8iGd_tfb5MQMPeieAgjCc-M-NEI,1
|
|
|
43
45
|
realign/commands/worker.py,sha256=LErEjB9T9_XatuLTS9Wn0BbSFc3ah0O9lFTzQOin4qU,22673
|
|
44
46
|
realign/dashboard/__init__.py,sha256=QZkHTsGityH8UkF8rmvA3xW7dMXNe0swEWr443qfgCM,128
|
|
45
47
|
realign/dashboard/app.py,sha256=SOlQ6QHt01-Jj5Daa5xyigrNhR-gXC3jupH24lcbeLY,11935
|
|
46
|
-
realign/dashboard/tmux_manager.py,sha256=
|
|
48
|
+
realign/dashboard/tmux_manager.py,sha256=zptH81f62tita2h0Yj3HUMcbBQoDNxHDlID4s6KQXAk,22022
|
|
47
49
|
realign/dashboard/screens/__init__.py,sha256=x42K31sqL5KVMtufOnZjG8LnFN7hQyN5-z8CySqbwlM,304
|
|
48
50
|
realign/dashboard/screens/create_event.py,sha256=nPMZMOekduxLXBTjMmLJEQMhb33RXWRIY0uHbD5fmmM,5484
|
|
49
51
|
realign/dashboard/screens/event_detail.py,sha256=WJFO7pryO0DZIMtyA-IOriFYtSLZ5Ri5AVSzjYiW1BQ,20842
|
|
@@ -57,7 +59,7 @@ realign/dashboard/widgets/header.py,sha256=ESejMT53T3sbtRrlCGJk8smUv0ts8binkshzC
|
|
|
57
59
|
realign/dashboard/widgets/openable_table.py,sha256=GeJPDEYp0kRHShqvmPMzAePpYXRZHUNqcWNnxqsqxjA,1963
|
|
58
60
|
realign/dashboard/widgets/search_panel.py,sha256=D0NXVIdNXpcnnVETfA44Muue4CyZq5XkMj4vfpNb3LQ,8635
|
|
59
61
|
realign/dashboard/widgets/sessions_table.py,sha256=CfSy93AbAqGGDFxEQW4u59twZDoVnHyuMmPcSOZTiW4,21852
|
|
60
|
-
realign/dashboard/widgets/terminal_panel.py,sha256=
|
|
62
|
+
realign/dashboard/widgets/terminal_panel.py,sha256=obVB9ONnR2Voun_druPiyVG-hd6gf4idXN9iOU84Jq4,29704
|
|
61
63
|
realign/dashboard/widgets/watcher_panel.py,sha256=ThQfVi_GOP-KR1GDNh5WsSmPJ11TEiP_fuuyxd2sdEk,19662
|
|
62
64
|
realign/dashboard/widgets/worker_panel.py,sha256=ufJYpW0nIPcek7GHa1nuMkrpNU9AY0WvtnWzhNZneW0,18547
|
|
63
65
|
realign/db/__init__.py,sha256=dqFKTskcVA7qCn7JAXeUz_c5T0_g--e9BAVokmk2Ys0,1874
|
|
@@ -84,8 +86,8 @@ realign/triggers/next_turn_trigger.py,sha256=CU6jsIxA_uoV-ROIke0iwEh4-on8vuataLG
|
|
|
84
86
|
realign/triggers/registry.py,sha256=AVlMm5xjzWLCnJMuzvfw4hMdNGlLqSTsgd3VCOZ-cHs,3799
|
|
85
87
|
realign/triggers/turn_status.py,sha256=tPHUB3NnnBfMVCIYdrFt5W1840IUGEZ-3v2GEtC981g,5987
|
|
86
88
|
realign/triggers/turn_summary.py,sha256=f3hEUshgv9skJ9AbfWpoYs417lsv_HK2A_vpPjgryO4,4467
|
|
87
|
-
aline_ai-0.5.
|
|
88
|
-
aline_ai-0.5.
|
|
89
|
-
aline_ai-0.5.
|
|
90
|
-
aline_ai-0.5.
|
|
91
|
-
aline_ai-0.5.
|
|
89
|
+
aline_ai-0.5.4.dist-info/METADATA,sha256=hpo9KnzjijOm4LWcK9gS851AwfbHhETS3DFarY-FONk,1597
|
|
90
|
+
aline_ai-0.5.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
91
|
+
aline_ai-0.5.4.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
|
|
92
|
+
aline_ai-0.5.4.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
|
|
93
|
+
aline_ai-0.5.4.dist-info/RECORD,,
|
realign/__init__.py
CHANGED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Claude Code PermissionRequest Hook - Set attention state when permission is needed
|
|
4
|
+
|
|
5
|
+
When Claude Code needs user approval for a tool, this script is called.
|
|
6
|
+
It sets the @aline_attention tmux window option to notify the dashboard.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
This script is called via Claude Code's PermissionRequest hook mechanism,
|
|
10
|
+
receiving stdin JSON and environment variables.
|
|
11
|
+
|
|
12
|
+
Environment variables:
|
|
13
|
+
ALINE_TERMINAL_ID - Terminal ID for the current window
|
|
14
|
+
ALINE_INNER_TMUX_SOCKET - Inner tmux socket name
|
|
15
|
+
ALINE_INNER_TMUX_SESSION - Inner tmux session name
|
|
16
|
+
|
|
17
|
+
stdin JSON format:
|
|
18
|
+
{
|
|
19
|
+
"session_id": "...",
|
|
20
|
+
"tool_name": "...",
|
|
21
|
+
"tool_input": {...}
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
import json
|
|
28
|
+
import time
|
|
29
|
+
import subprocess
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_signal_dir() -> Path:
|
|
34
|
+
"""Get the signal directory for permission requests."""
|
|
35
|
+
signal_dir = Path.home() / ".aline" / ".signals" / "permission_request"
|
|
36
|
+
signal_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
return signal_dir
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def write_signal_file(terminal_id: str, tool_name: str = "") -> None:
|
|
41
|
+
"""Write a signal file to notify the dashboard of a permission request."""
|
|
42
|
+
try:
|
|
43
|
+
signal_dir = get_signal_dir()
|
|
44
|
+
timestamp_ms = int(time.time() * 1000)
|
|
45
|
+
signal_file = signal_dir / f"{terminal_id}_{timestamp_ms}.signal"
|
|
46
|
+
tmp_file = signal_dir / f"{terminal_id}_{timestamp_ms}.signal.tmp"
|
|
47
|
+
|
|
48
|
+
signal_data = {
|
|
49
|
+
"terminal_id": terminal_id,
|
|
50
|
+
"tool_name": tool_name,
|
|
51
|
+
"timestamp": time.time(),
|
|
52
|
+
"hook_event": "PermissionRequest",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
tmp_file.write_text(json.dumps(signal_data, indent=2))
|
|
56
|
+
tmp_file.replace(signal_file)
|
|
57
|
+
except Exception:
|
|
58
|
+
pass # Best effort
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main():
|
|
62
|
+
"""Main function"""
|
|
63
|
+
try:
|
|
64
|
+
# Read stdin JSON (optional, we mainly care about setting attention)
|
|
65
|
+
stdin_data = sys.stdin.read()
|
|
66
|
+
try:
|
|
67
|
+
data = json.loads(stdin_data) if stdin_data.strip() else {}
|
|
68
|
+
except json.JSONDecodeError:
|
|
69
|
+
data = {}
|
|
70
|
+
|
|
71
|
+
# From environment
|
|
72
|
+
terminal_id = os.environ.get("ALINE_TERMINAL_ID", "")
|
|
73
|
+
inner_socket = os.environ.get("ALINE_INNER_TMUX_SOCKET", "")
|
|
74
|
+
inner_session = os.environ.get("ALINE_INNER_TMUX_SESSION", "")
|
|
75
|
+
|
|
76
|
+
# Try to get terminal_id from tmux if not in env
|
|
77
|
+
if not terminal_id:
|
|
78
|
+
try:
|
|
79
|
+
terminal_id = (
|
|
80
|
+
subprocess.run(
|
|
81
|
+
["tmux", "display-message", "-p", "#{@aline_terminal_id}"],
|
|
82
|
+
text=True,
|
|
83
|
+
capture_output=True,
|
|
84
|
+
check=False,
|
|
85
|
+
).stdout
|
|
86
|
+
or ""
|
|
87
|
+
).strip()
|
|
88
|
+
except Exception:
|
|
89
|
+
terminal_id = ""
|
|
90
|
+
|
|
91
|
+
# Set attention state on the tmux window
|
|
92
|
+
try:
|
|
93
|
+
if terminal_id and inner_socket and inner_session:
|
|
94
|
+
# Find the window with matching terminal_id
|
|
95
|
+
proc = subprocess.run(
|
|
96
|
+
[
|
|
97
|
+
"tmux",
|
|
98
|
+
"-L",
|
|
99
|
+
inner_socket,
|
|
100
|
+
"list-windows",
|
|
101
|
+
"-t",
|
|
102
|
+
inner_session,
|
|
103
|
+
"-F",
|
|
104
|
+
"#{window_id}\t#{@aline_terminal_id}",
|
|
105
|
+
],
|
|
106
|
+
text=True,
|
|
107
|
+
capture_output=True,
|
|
108
|
+
check=False,
|
|
109
|
+
)
|
|
110
|
+
for line in (proc.stdout or "").splitlines():
|
|
111
|
+
parts = line.split("\t", 1)
|
|
112
|
+
if len(parts) != 2:
|
|
113
|
+
continue
|
|
114
|
+
window_id, win_terminal_id = parts
|
|
115
|
+
if win_terminal_id == terminal_id:
|
|
116
|
+
subprocess.run(
|
|
117
|
+
[
|
|
118
|
+
"tmux",
|
|
119
|
+
"-L",
|
|
120
|
+
inner_socket,
|
|
121
|
+
"set-option",
|
|
122
|
+
"-w",
|
|
123
|
+
"-t",
|
|
124
|
+
window_id,
|
|
125
|
+
"@aline_attention",
|
|
126
|
+
"permission_request",
|
|
127
|
+
],
|
|
128
|
+
check=False,
|
|
129
|
+
)
|
|
130
|
+
break
|
|
131
|
+
else:
|
|
132
|
+
# Fallback: try to set on current window
|
|
133
|
+
window_id = (
|
|
134
|
+
subprocess.run(
|
|
135
|
+
["tmux", "display-message", "-p", "#{window_id}"],
|
|
136
|
+
text=True,
|
|
137
|
+
capture_output=True,
|
|
138
|
+
check=False,
|
|
139
|
+
).stdout
|
|
140
|
+
or ""
|
|
141
|
+
).strip()
|
|
142
|
+
|
|
143
|
+
should_tag = bool(terminal_id)
|
|
144
|
+
if not should_tag:
|
|
145
|
+
try:
|
|
146
|
+
context_id = (
|
|
147
|
+
subprocess.run(
|
|
148
|
+
["tmux", "display-message", "-p", "#{@aline_context_id}"],
|
|
149
|
+
text=True,
|
|
150
|
+
capture_output=True,
|
|
151
|
+
check=False,
|
|
152
|
+
).stdout
|
|
153
|
+
or ""
|
|
154
|
+
).strip()
|
|
155
|
+
should_tag = bool(context_id)
|
|
156
|
+
except Exception:
|
|
157
|
+
should_tag = False
|
|
158
|
+
|
|
159
|
+
if window_id and should_tag:
|
|
160
|
+
subprocess.run(
|
|
161
|
+
[
|
|
162
|
+
"tmux",
|
|
163
|
+
"set-option",
|
|
164
|
+
"-w",
|
|
165
|
+
"-t",
|
|
166
|
+
window_id,
|
|
167
|
+
"@aline_attention",
|
|
168
|
+
"permission_request",
|
|
169
|
+
],
|
|
170
|
+
check=False,
|
|
171
|
+
)
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
# Write signal file to notify dashboard (triggers file watcher refresh)
|
|
176
|
+
if terminal_id:
|
|
177
|
+
tool_name = data.get("tool_name", "")
|
|
178
|
+
write_signal_file(terminal_id, tool_name)
|
|
179
|
+
|
|
180
|
+
# Exit 0 - don't block the permission request
|
|
181
|
+
sys.exit(0)
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
# Fail silently to not affect Claude Code's normal operation
|
|
185
|
+
sys.exit(0)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if __name__ == "__main__":
|
|
189
|
+
main()
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude Code PermissionRequest Hook Auto-installer
|
|
3
|
+
|
|
4
|
+
Automatically installs Aline's PermissionRequest hook into Claude Code's configuration.
|
|
5
|
+
Uses append mode - does not overwrite user's existing hooks configuration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from ..logging_config import setup_logger
|
|
14
|
+
|
|
15
|
+
logger = setup_logger('realign.hooks.permission_request_installer', 'hooks_installer.log')
|
|
16
|
+
|
|
17
|
+
# Marker to identify Aline hook for later uninstallation
|
|
18
|
+
ALINE_HOOK_MARKER = "aline-permission-request-hook"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_permission_request_hook_script_path() -> Path:
|
|
22
|
+
"""Get path to permission_request_hook.py script"""
|
|
23
|
+
return Path(__file__).parent / 'permission_request_hook.py'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_permission_request_hook_command() -> str:
|
|
27
|
+
"""
|
|
28
|
+
Get the PermissionRequest hook execution command.
|
|
29
|
+
|
|
30
|
+
Uses direct script path to avoid module import issues.
|
|
31
|
+
"""
|
|
32
|
+
script_path = get_permission_request_hook_script_path()
|
|
33
|
+
return f"{sys.executable} {script_path}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_settings_path(project_path: Optional[Path] = None) -> Path:
|
|
37
|
+
"""
|
|
38
|
+
Get Claude Code settings.json path.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
project_path: If provided, returns project-level config path; otherwise global config.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Path to settings.json
|
|
45
|
+
"""
|
|
46
|
+
if project_path:
|
|
47
|
+
return project_path / '.claude' / 'settings.local.json'
|
|
48
|
+
return Path.home() / '.claude' / 'settings.json'
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_hook_installed(settings_path: Path) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Check if Aline PermissionRequest hook is installed.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
settings_path: Path to settings.json
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if installed
|
|
60
|
+
"""
|
|
61
|
+
if not settings_path.exists():
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
settings = json.loads(settings_path.read_text())
|
|
66
|
+
permission_hooks = settings.get('hooks', {}).get('PermissionRequest', [])
|
|
67
|
+
|
|
68
|
+
for hook_config in permission_hooks:
|
|
69
|
+
hooks_list = hook_config.get('hooks', [])
|
|
70
|
+
for h in hooks_list:
|
|
71
|
+
command = h.get('command', '')
|
|
72
|
+
if ALINE_HOOK_MARKER in command or 'permission_request_hook' in command:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
except Exception:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def install_permission_request_hook(
|
|
82
|
+
settings_path: Optional[Path] = None,
|
|
83
|
+
force: bool = False,
|
|
84
|
+
quiet: bool = False,
|
|
85
|
+
) -> bool:
|
|
86
|
+
"""
|
|
87
|
+
Install Claude Code PermissionRequest hook.
|
|
88
|
+
|
|
89
|
+
Uses append mode: does not overwrite user's existing hooks configuration.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
settings_path: Path to settings.json (defaults to global config)
|
|
93
|
+
force: If True, reinstall even if already installed
|
|
94
|
+
quiet: If True, don't output any messages
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if installation successful or already installed
|
|
98
|
+
"""
|
|
99
|
+
if settings_path is None:
|
|
100
|
+
settings_path = get_settings_path()
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
# Check if already installed
|
|
104
|
+
if not force and is_hook_installed(settings_path):
|
|
105
|
+
logger.debug("Aline PermissionRequest hook already installed")
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
# Read existing settings
|
|
109
|
+
settings = {}
|
|
110
|
+
if settings_path.exists():
|
|
111
|
+
try:
|
|
112
|
+
settings = json.loads(settings_path.read_text())
|
|
113
|
+
except json.JSONDecodeError:
|
|
114
|
+
logger.warning(f"Invalid JSON in {settings_path}, creating new settings")
|
|
115
|
+
settings = {}
|
|
116
|
+
|
|
117
|
+
# Ensure hooks structure exists
|
|
118
|
+
if 'hooks' not in settings:
|
|
119
|
+
settings['hooks'] = {}
|
|
120
|
+
if 'PermissionRequest' not in settings['hooks']:
|
|
121
|
+
settings['hooks']['PermissionRequest'] = []
|
|
122
|
+
|
|
123
|
+
# If force install, remove old Aline hook first
|
|
124
|
+
if force:
|
|
125
|
+
permission_hooks = settings['hooks']['PermissionRequest']
|
|
126
|
+
new_hooks = []
|
|
127
|
+
for hook_config in permission_hooks:
|
|
128
|
+
hooks_list = hook_config.get('hooks', [])
|
|
129
|
+
filtered = [
|
|
130
|
+
h for h in hooks_list
|
|
131
|
+
if ALINE_HOOK_MARKER not in h.get('command', '')
|
|
132
|
+
and 'permission_request_hook' not in h.get('command', '')
|
|
133
|
+
]
|
|
134
|
+
if filtered:
|
|
135
|
+
hook_config['hooks'] = filtered
|
|
136
|
+
new_hooks.append(hook_config)
|
|
137
|
+
settings['hooks']['PermissionRequest'] = new_hooks
|
|
138
|
+
|
|
139
|
+
# Append Aline hook with matcher for all tools
|
|
140
|
+
hook_command = get_permission_request_hook_command()
|
|
141
|
+
aline_hook = {
|
|
142
|
+
'matcher': '*', # Match all tools
|
|
143
|
+
'hooks': [{
|
|
144
|
+
'type': 'command',
|
|
145
|
+
'command': f'{hook_command} # {ALINE_HOOK_MARKER}'
|
|
146
|
+
}]
|
|
147
|
+
}
|
|
148
|
+
settings['hooks']['PermissionRequest'].append(aline_hook)
|
|
149
|
+
|
|
150
|
+
# Write back settings
|
|
151
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
settings_path.write_text(json.dumps(settings, indent=2))
|
|
153
|
+
|
|
154
|
+
logger.info(f"Aline PermissionRequest hook installed to {settings_path}")
|
|
155
|
+
if not quiet:
|
|
156
|
+
print(f"[Aline] PermissionRequest hook installed to {settings_path}", file=sys.stderr)
|
|
157
|
+
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"Failed to install PermissionRequest hook: {e}")
|
|
162
|
+
if not quiet:
|
|
163
|
+
print(f"[Aline] Failed to install PermissionRequest hook: {e}", file=sys.stderr)
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def uninstall_permission_request_hook(
|
|
168
|
+
settings_path: Optional[Path] = None,
|
|
169
|
+
quiet: bool = False,
|
|
170
|
+
) -> bool:
|
|
171
|
+
"""
|
|
172
|
+
Uninstall Aline PermissionRequest hook.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
settings_path: Path to settings.json (defaults to global config)
|
|
176
|
+
quiet: If True, don't output any messages
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
True if uninstallation successful
|
|
180
|
+
"""
|
|
181
|
+
if settings_path is None:
|
|
182
|
+
settings_path = get_settings_path()
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
if not settings_path.exists():
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
settings = json.loads(settings_path.read_text())
|
|
189
|
+
permission_hooks = settings.get('hooks', {}).get('PermissionRequest', [])
|
|
190
|
+
|
|
191
|
+
# Filter out Aline hook
|
|
192
|
+
new_hooks = []
|
|
193
|
+
removed = False
|
|
194
|
+
for hook_config in permission_hooks:
|
|
195
|
+
hooks_list = hook_config.get('hooks', [])
|
|
196
|
+
filtered = [
|
|
197
|
+
h for h in hooks_list
|
|
198
|
+
if ALINE_HOOK_MARKER not in h.get('command', '')
|
|
199
|
+
and 'permission_request_hook' not in h.get('command', '')
|
|
200
|
+
]
|
|
201
|
+
if len(filtered) < len(hooks_list):
|
|
202
|
+
removed = True
|
|
203
|
+
if filtered:
|
|
204
|
+
hook_config['hooks'] = filtered
|
|
205
|
+
new_hooks.append(hook_config)
|
|
206
|
+
|
|
207
|
+
if removed:
|
|
208
|
+
settings['hooks']['PermissionRequest'] = new_hooks
|
|
209
|
+
settings_path.write_text(json.dumps(settings, indent=2))
|
|
210
|
+
logger.info("Aline PermissionRequest hook uninstalled")
|
|
211
|
+
if not quiet:
|
|
212
|
+
print("[Aline] PermissionRequest hook uninstalled", file=sys.stderr)
|
|
213
|
+
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.error(f"Failed to uninstall PermissionRequest hook: {e}")
|
|
218
|
+
if not quiet:
|
|
219
|
+
print(f"[Aline] Failed to uninstall PermissionRequest hook: {e}", file=sys.stderr)
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def ensure_permission_request_hook_installed(quiet: bool = True) -> bool:
|
|
224
|
+
"""
|
|
225
|
+
Ensure PermissionRequest hook is installed (called at watcher startup).
|
|
226
|
+
|
|
227
|
+
This is an idempotent operation - will not reinstall if already installed.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
quiet: If True, only output message on first install
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
True if hook is available
|
|
234
|
+
"""
|
|
235
|
+
settings_path = get_settings_path()
|
|
236
|
+
|
|
237
|
+
if is_hook_installed(settings_path):
|
|
238
|
+
logger.debug("PermissionRequest hook already installed, skipping")
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
return install_permission_request_hook(settings_path, quiet=quiet)
|
|
@@ -175,6 +175,21 @@ def main():
|
|
|
175
175
|
],
|
|
176
176
|
check=False,
|
|
177
177
|
)
|
|
178
|
+
# Set attention state to notify dashboard
|
|
179
|
+
subprocess.run(
|
|
180
|
+
[
|
|
181
|
+
"tmux",
|
|
182
|
+
"-L",
|
|
183
|
+
inner_socket,
|
|
184
|
+
"set-option",
|
|
185
|
+
"-w",
|
|
186
|
+
"-t",
|
|
187
|
+
window_id,
|
|
188
|
+
"@aline_attention",
|
|
189
|
+
"stop",
|
|
190
|
+
],
|
|
191
|
+
check=False,
|
|
192
|
+
)
|
|
178
193
|
if transcript_path:
|
|
179
194
|
subprocess.run(
|
|
180
195
|
[
|
|
@@ -254,6 +269,19 @@ def main():
|
|
|
254
269
|
],
|
|
255
270
|
check=False,
|
|
256
271
|
)
|
|
272
|
+
# Set attention state to notify dashboard
|
|
273
|
+
subprocess.run(
|
|
274
|
+
[
|
|
275
|
+
"tmux",
|
|
276
|
+
"set-option",
|
|
277
|
+
"-w",
|
|
278
|
+
"-t",
|
|
279
|
+
window_id,
|
|
280
|
+
"@aline_attention",
|
|
281
|
+
"stop",
|
|
282
|
+
],
|
|
283
|
+
check=False,
|
|
284
|
+
)
|
|
257
285
|
if transcript_path:
|
|
258
286
|
subprocess.run(
|
|
259
287
|
[
|
realign/cli.py
CHANGED
|
@@ -157,6 +157,23 @@ def add_skills_cli(
|
|
|
157
157
|
raise typer.Exit(code=exit_code)
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
@add_app.command(name="skills-dev")
|
|
161
|
+
def add_skills_dev_cli(
|
|
162
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing skills"),
|
|
163
|
+
):
|
|
164
|
+
"""Install developer skills from skill-dev/ directory.
|
|
165
|
+
|
|
166
|
+
Scans skill-dev/ for SKILL.md files and installs them to ~/.claude/skills/.
|
|
167
|
+
This is for developer use only.
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
aline add skills-dev # Install dev skills
|
|
171
|
+
aline add skills-dev --force # Reinstall/update dev skills
|
|
172
|
+
"""
|
|
173
|
+
exit_code = add.add_skills_dev_command(force=force)
|
|
174
|
+
raise typer.Exit(code=exit_code)
|
|
175
|
+
|
|
176
|
+
|
|
160
177
|
@context_app.command(name="load")
|
|
161
178
|
def context_load_cli(
|
|
162
179
|
sessions: Optional[str] = typer.Option(
|