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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aline-ai
3
- Version: 0.5.1
3
+ Version: 0.5.4
4
4
  Summary: Shared AI memory; everyone knows everything in teams
5
5
  Author: Sharemind
6
6
  License: MIT
@@ -1,7 +1,7 @@
1
- aline_ai-0.5.1.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
2
- realign/__init__.py,sha256=0C8PfQuAdk_vdDgO-OK3_uootZ8Q9edqLPiGDtLh-_8,1623
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=CGuAGp4s7LCV7DOuF54eF2s6xe30oie7zkCF8xttXaU,29207
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/stop_hook.py,sha256=PGpG_w00mY-Vu25mY-Z9zt45TA-SKF6RNQq1559EO6I,10895
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=QWNx78ASIvcaVrj1bv4524P25PdcJ-N2NAX1Q4v5wJw,12535
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=BY0m8sQxaQcGYRrexciaFN2mfO3B1slIkAnh_xg1CLo,25504
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=GZ1zF6xCCeNQwqhC7EQQqDacG3NSUU86HrbCE70I1Ls,19148
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=A1_-42eTmZVerwAD7o9iZF_2lzH4Ed9O9BgNm6oRWSI,22970
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.1.dist-info/METADATA,sha256=UTXP26c_IlOZXH9BMSRTK7pCspxlJbuPipUvpdwMVsI,1597
88
- aline_ai-0.5.1.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
89
- aline_ai-0.5.1.dist-info/entry_points.txt,sha256=TvYELpMoWsUTcQdMV8tBHxCbEf_LbK4sESqK3r8PM6Y,78
90
- aline_ai-0.5.1.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
91
- aline_ai-0.5.1.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
realign/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  import hashlib
4
4
  from pathlib import Path
5
5
 
6
- __version__ = "0.5.0"
6
+ __version__ = "0.5.4"
7
7
 
8
8
 
9
9
  def get_realign_dir(project_root: Path) -> Path:
@@ -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(