aline-ai 0.5.4__py3-none-any.whl → 0.5.6__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.4.dist-info → aline_ai-0.5.6.dist-info}/METADATA +1 -1
- aline_ai-0.5.6.dist-info/RECORD +95 -0
- realign/__init__.py +1 -1
- realign/adapters/antigravity.py +28 -20
- realign/adapters/base.py +46 -50
- realign/adapters/claude.py +14 -14
- realign/adapters/codex.py +7 -7
- realign/adapters/gemini.py +11 -11
- realign/adapters/registry.py +14 -10
- realign/claude_detector.py +2 -2
- realign/claude_hooks/__init__.py +3 -3
- realign/claude_hooks/permission_request_hook_installer.py +31 -32
- realign/claude_hooks/stop_hook.py +4 -1
- realign/claude_hooks/stop_hook_installer.py +30 -31
- realign/cli.py +23 -4
- realign/codex_detector.py +11 -11
- realign/commands/add.py +88 -65
- realign/commands/config.py +3 -12
- realign/commands/context.py +3 -1
- realign/commands/export_shares.py +86 -127
- realign/commands/import_shares.py +145 -155
- realign/commands/init.py +166 -30
- realign/commands/restore.py +18 -6
- realign/commands/search.py +14 -42
- realign/commands/upgrade.py +155 -11
- realign/commands/watcher.py +98 -219
- realign/commands/worker.py +29 -6
- realign/config.py +25 -20
- realign/context.py +1 -3
- realign/dashboard/app.py +34 -24
- realign/dashboard/screens/__init__.py +10 -1
- realign/dashboard/screens/create_agent.py +244 -0
- realign/dashboard/screens/create_event.py +3 -1
- realign/dashboard/screens/event_detail.py +14 -6
- realign/dashboard/screens/help_screen.py +114 -0
- realign/dashboard/screens/session_detail.py +3 -1
- realign/dashboard/screens/share_import.py +7 -3
- realign/dashboard/tmux_manager.py +54 -9
- realign/dashboard/widgets/config_panel.py +85 -1
- realign/dashboard/widgets/events_table.py +314 -70
- realign/dashboard/widgets/header.py +2 -1
- realign/dashboard/widgets/search_panel.py +37 -27
- realign/dashboard/widgets/sessions_table.py +404 -85
- realign/dashboard/widgets/terminal_panel.py +155 -175
- realign/dashboard/widgets/watcher_panel.py +6 -2
- realign/dashboard/widgets/worker_panel.py +10 -1
- realign/db/__init__.py +1 -1
- realign/db/base.py +5 -15
- realign/db/locks.py +0 -1
- realign/db/migration.py +82 -76
- realign/db/schema.py +2 -6
- realign/db/sqlite_db.py +23 -41
- realign/events/__init__.py +0 -1
- realign/events/event_summarizer.py +27 -15
- realign/events/session_summarizer.py +29 -15
- realign/file_lock.py +1 -0
- realign/hooks.py +150 -60
- realign/logging_config.py +12 -15
- realign/mcp_server.py +30 -51
- realign/mcp_watcher.py +0 -1
- realign/models/event.py +29 -20
- realign/prompts/__init__.py +7 -7
- realign/prompts/presets.py +15 -11
- realign/redactor.py +99 -59
- realign/triggers/__init__.py +9 -9
- realign/triggers/antigravity_trigger.py +30 -28
- realign/triggers/base.py +4 -3
- realign/triggers/claude_trigger.py +104 -85
- realign/triggers/codex_trigger.py +15 -5
- realign/triggers/gemini_trigger.py +57 -47
- realign/triggers/next_turn_trigger.py +3 -1
- realign/triggers/registry.py +6 -2
- realign/triggers/turn_status.py +3 -1
- realign/watcher_core.py +306 -131
- realign/watcher_daemon.py +8 -8
- realign/worker_core.py +3 -1
- realign/worker_daemon.py +3 -1
- aline_ai-0.5.4.dist-info/RECORD +0 -93
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.4.dist-info → aline_ai-0.5.6.dist-info}/top_level.txt +0 -0
realign/adapters/gemini.py
CHANGED
|
@@ -14,18 +14,18 @@ from ..triggers.gemini_trigger import GeminiTrigger
|
|
|
14
14
|
|
|
15
15
|
class GeminiAdapter(SessionAdapter):
|
|
16
16
|
"""Adapter for Gemini CLI sessions."""
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
name = "gemini"
|
|
19
19
|
trigger_class = GeminiTrigger
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
def discover_sessions(self) -> List[Path]:
|
|
22
22
|
"""Find all active Gemini CLI sessions."""
|
|
23
23
|
sessions = []
|
|
24
24
|
gemini_tmp = Path.home() / ".gemini" / "tmp"
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
if not gemini_tmp.exists():
|
|
27
27
|
return sessions
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
try:
|
|
30
30
|
for project_dir in gemini_tmp.iterdir():
|
|
31
31
|
if project_dir.is_dir():
|
|
@@ -34,13 +34,13 @@ class GeminiAdapter(SessionAdapter):
|
|
|
34
34
|
sessions.extend(chats_dir.glob("session-*.json"))
|
|
35
35
|
except Exception:
|
|
36
36
|
pass
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
return sessions
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
def discover_sessions_for_project(self, project_path: Path) -> List[Path]:
|
|
41
41
|
"""
|
|
42
42
|
Find sessions for a specific project.
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
For Gemini CLI, project directories are hashed, so it's easier to scan all
|
|
45
45
|
and check the encoded project path or project hash if we knew it.
|
|
46
46
|
Actually GeminiTrigger might have some logic for this.
|
|
@@ -49,18 +49,18 @@ class GeminiAdapter(SessionAdapter):
|
|
|
49
49
|
all_sessions = self.discover_sessions()
|
|
50
50
|
matching = []
|
|
51
51
|
abs_project = str(project_path.resolve())
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
for s in all_sessions:
|
|
54
54
|
path = self.extract_project_path(s)
|
|
55
55
|
if path and str(path.resolve()) == abs_project:
|
|
56
56
|
matching.append(s)
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
return matching
|
|
59
59
|
|
|
60
60
|
def extract_project_path(self, session_file: Path) -> Optional[Path]:
|
|
61
61
|
"""
|
|
62
62
|
Extract project path from Gemini session file.
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
Gemini CLI JSON files usually don't have the full path, but they might
|
|
65
65
|
have a projectHash. Wait, let me check the GeminiTrigger logic again.
|
|
66
66
|
"""
|
|
@@ -72,5 +72,5 @@ class GeminiAdapter(SessionAdapter):
|
|
|
72
72
|
"""Check if this is a Gemini session file."""
|
|
73
73
|
if not session_file.name.startswith("session-") or not session_file.name.endswith(".json"):
|
|
74
74
|
return False
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
return super().is_session_valid(session_file)
|
realign/adapters/registry.py
CHANGED
|
@@ -22,7 +22,7 @@ class AdapterRegistry:
|
|
|
22
22
|
def register(self, adapter_class: Type[SessionAdapter]):
|
|
23
23
|
"""
|
|
24
24
|
Register an adapter class.
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
Args:
|
|
27
27
|
adapter_class: The adapter class (not instance)
|
|
28
28
|
"""
|
|
@@ -34,11 +34,11 @@ class AdapterRegistry:
|
|
|
34
34
|
def get_adapter(self, name: str, config: Optional[Dict] = None) -> Optional[SessionAdapter]:
|
|
35
35
|
"""
|
|
36
36
|
Get an adapter instance by name.
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
Args:
|
|
39
39
|
name: Adapter name
|
|
40
40
|
config: Optional configuration
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
Returns:
|
|
43
43
|
Adapter instance or None
|
|
44
44
|
"""
|
|
@@ -51,13 +51,15 @@ class AdapterRegistry:
|
|
|
51
51
|
"""List all registered adapter names."""
|
|
52
52
|
return list(self._adapters.keys())
|
|
53
53
|
|
|
54
|
-
def discover_all_sessions(
|
|
54
|
+
def discover_all_sessions(
|
|
55
|
+
self, config: Optional[Dict] = None
|
|
56
|
+
) -> List[Tuple[Path, SessionAdapter]]:
|
|
55
57
|
"""
|
|
56
58
|
Discover sessions from all registered adapters.
|
|
57
|
-
|
|
59
|
+
|
|
58
60
|
Args:
|
|
59
61
|
config: Optional configuration (passed to adapters)
|
|
60
|
-
|
|
62
|
+
|
|
61
63
|
Returns:
|
|
62
64
|
List of (session_path, adapter_instance) tuples
|
|
63
65
|
"""
|
|
@@ -72,17 +74,19 @@ class AdapterRegistry:
|
|
|
72
74
|
all_sessions.append((session_path, adapter))
|
|
73
75
|
except Exception as e:
|
|
74
76
|
logger.warning(f"Error discovering sessions for adapter {name}: {e}")
|
|
75
|
-
|
|
77
|
+
|
|
76
78
|
return all_sessions
|
|
77
79
|
|
|
78
|
-
def auto_detect_adapter(
|
|
80
|
+
def auto_detect_adapter(
|
|
81
|
+
self, session_file: Path, config: Optional[Dict] = None
|
|
82
|
+
) -> Optional[SessionAdapter]:
|
|
79
83
|
"""
|
|
80
84
|
Detect which adapter should handle a given session file.
|
|
81
|
-
|
|
85
|
+
|
|
82
86
|
Args:
|
|
83
87
|
session_file: Path to the session file
|
|
84
88
|
config: Optional configuration
|
|
85
|
-
|
|
89
|
+
|
|
86
90
|
Returns:
|
|
87
91
|
Matching Adapter instance or None
|
|
88
92
|
"""
|
realign/claude_detector.py
CHANGED
|
@@ -25,10 +25,10 @@ def get_claude_project_name(project_path: Path) -> str:
|
|
|
25
25
|
# Remove leading '/' and replace all '/' and '_' with '-'
|
|
26
26
|
# Claude Code replaces both slashes and underscores with dashes
|
|
27
27
|
path_str = str(abs_path)
|
|
28
|
-
if path_str.startswith(
|
|
28
|
+
if path_str.startswith("/"):
|
|
29
29
|
path_str = path_str[1:]
|
|
30
30
|
|
|
31
|
-
return
|
|
31
|
+
return "-" + path_str.replace("/", "-").replace("_", "-")
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def find_claude_sessions_dir(project_path: Path) -> Optional[Path]:
|
realign/claude_hooks/__init__.py
CHANGED
|
@@ -10,7 +10,7 @@ This module contains hooks for integration with AI coding assistants:
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
# Signal directory for inter-process communication
|
|
13
|
-
SIGNAL_DIR = Path.home() /
|
|
13
|
+
SIGNAL_DIR = Path.home() / ".aline" / ".signals"
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def get_signal_dir() -> Path:
|
|
@@ -20,6 +20,6 @@ def get_signal_dir() -> Path:
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
"SIGNAL_DIR",
|
|
24
|
+
"get_signal_dir",
|
|
25
25
|
]
|
|
@@ -12,7 +12,7 @@ from typing import Optional
|
|
|
12
12
|
|
|
13
13
|
from ..logging_config import setup_logger
|
|
14
14
|
|
|
15
|
-
logger = setup_logger(
|
|
15
|
+
logger = setup_logger("realign.hooks.permission_request_installer", "hooks_installer.log")
|
|
16
16
|
|
|
17
17
|
# Marker to identify Aline hook for later uninstallation
|
|
18
18
|
ALINE_HOOK_MARKER = "aline-permission-request-hook"
|
|
@@ -20,7 +20,7 @@ ALINE_HOOK_MARKER = "aline-permission-request-hook"
|
|
|
20
20
|
|
|
21
21
|
def get_permission_request_hook_script_path() -> Path:
|
|
22
22
|
"""Get path to permission_request_hook.py script"""
|
|
23
|
-
return Path(__file__).parent /
|
|
23
|
+
return Path(__file__).parent / "permission_request_hook.py"
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def get_permission_request_hook_command() -> str:
|
|
@@ -44,8 +44,8 @@ def get_settings_path(project_path: Optional[Path] = None) -> Path:
|
|
|
44
44
|
Path to settings.json
|
|
45
45
|
"""
|
|
46
46
|
if project_path:
|
|
47
|
-
return project_path /
|
|
48
|
-
return Path.home() /
|
|
47
|
+
return project_path / ".claude" / "settings.local.json"
|
|
48
|
+
return Path.home() / ".claude" / "settings.json"
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def is_hook_installed(settings_path: Path) -> bool:
|
|
@@ -63,13 +63,13 @@ def is_hook_installed(settings_path: Path) -> bool:
|
|
|
63
63
|
|
|
64
64
|
try:
|
|
65
65
|
settings = json.loads(settings_path.read_text())
|
|
66
|
-
permission_hooks = settings.get(
|
|
66
|
+
permission_hooks = settings.get("hooks", {}).get("PermissionRequest", [])
|
|
67
67
|
|
|
68
68
|
for hook_config in permission_hooks:
|
|
69
|
-
hooks_list = hook_config.get(
|
|
69
|
+
hooks_list = hook_config.get("hooks", [])
|
|
70
70
|
for h in hooks_list:
|
|
71
|
-
command = h.get(
|
|
72
|
-
if ALINE_HOOK_MARKER in command or
|
|
71
|
+
command = h.get("command", "")
|
|
72
|
+
if ALINE_HOOK_MARKER in command or "permission_request_hook" in command:
|
|
73
73
|
return True
|
|
74
74
|
|
|
75
75
|
return False
|
|
@@ -115,37 +115,35 @@ def install_permission_request_hook(
|
|
|
115
115
|
settings = {}
|
|
116
116
|
|
|
117
117
|
# Ensure hooks structure exists
|
|
118
|
-
if
|
|
119
|
-
settings[
|
|
120
|
-
if
|
|
121
|
-
settings[
|
|
118
|
+
if "hooks" not in settings:
|
|
119
|
+
settings["hooks"] = {}
|
|
120
|
+
if "PermissionRequest" not in settings["hooks"]:
|
|
121
|
+
settings["hooks"]["PermissionRequest"] = []
|
|
122
122
|
|
|
123
123
|
# If force install, remove old Aline hook first
|
|
124
124
|
if force:
|
|
125
|
-
permission_hooks = settings[
|
|
125
|
+
permission_hooks = settings["hooks"]["PermissionRequest"]
|
|
126
126
|
new_hooks = []
|
|
127
127
|
for hook_config in permission_hooks:
|
|
128
|
-
hooks_list = hook_config.get(
|
|
128
|
+
hooks_list = hook_config.get("hooks", [])
|
|
129
129
|
filtered = [
|
|
130
|
-
h
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
h
|
|
131
|
+
for h in hooks_list
|
|
132
|
+
if ALINE_HOOK_MARKER not in h.get("command", "")
|
|
133
|
+
and "permission_request_hook" not in h.get("command", "")
|
|
133
134
|
]
|
|
134
135
|
if filtered:
|
|
135
|
-
hook_config[
|
|
136
|
+
hook_config["hooks"] = filtered
|
|
136
137
|
new_hooks.append(hook_config)
|
|
137
|
-
settings[
|
|
138
|
+
settings["hooks"]["PermissionRequest"] = new_hooks
|
|
138
139
|
|
|
139
140
|
# Append Aline hook with matcher for all tools
|
|
140
141
|
hook_command = get_permission_request_hook_command()
|
|
141
142
|
aline_hook = {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
'type': 'command',
|
|
145
|
-
'command': f'{hook_command} # {ALINE_HOOK_MARKER}'
|
|
146
|
-
}]
|
|
143
|
+
"matcher": "*", # Match all tools
|
|
144
|
+
"hooks": [{"type": "command", "command": f"{hook_command} # {ALINE_HOOK_MARKER}"}],
|
|
147
145
|
}
|
|
148
|
-
settings[
|
|
146
|
+
settings["hooks"]["PermissionRequest"].append(aline_hook)
|
|
149
147
|
|
|
150
148
|
# Write back settings
|
|
151
149
|
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -186,26 +184,27 @@ def uninstall_permission_request_hook(
|
|
|
186
184
|
return True
|
|
187
185
|
|
|
188
186
|
settings = json.loads(settings_path.read_text())
|
|
189
|
-
permission_hooks = settings.get(
|
|
187
|
+
permission_hooks = settings.get("hooks", {}).get("PermissionRequest", [])
|
|
190
188
|
|
|
191
189
|
# Filter out Aline hook
|
|
192
190
|
new_hooks = []
|
|
193
191
|
removed = False
|
|
194
192
|
for hook_config in permission_hooks:
|
|
195
|
-
hooks_list = hook_config.get(
|
|
193
|
+
hooks_list = hook_config.get("hooks", [])
|
|
196
194
|
filtered = [
|
|
197
|
-
h
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
h
|
|
196
|
+
for h in hooks_list
|
|
197
|
+
if ALINE_HOOK_MARKER not in h.get("command", "")
|
|
198
|
+
and "permission_request_hook" not in h.get("command", "")
|
|
200
199
|
]
|
|
201
200
|
if len(filtered) < len(hooks_list):
|
|
202
201
|
removed = True
|
|
203
202
|
if filtered:
|
|
204
|
-
hook_config[
|
|
203
|
+
hook_config["hooks"] = filtered
|
|
205
204
|
new_hooks.append(hook_config)
|
|
206
205
|
|
|
207
206
|
if removed:
|
|
208
|
-
settings[
|
|
207
|
+
settings["hooks"]["PermissionRequest"] = new_hooks
|
|
209
208
|
settings_path.write_text(json.dumps(settings, indent=2))
|
|
210
209
|
logger.info("Aline PermissionRequest hook uninstalled")
|
|
211
210
|
if not quiet:
|
|
@@ -54,7 +54,10 @@ def main():
|
|
|
54
54
|
session = data.get("session") or {}
|
|
55
55
|
session_id = session.get("id") or data.get("session_id") or data.get("sessionId") or ""
|
|
56
56
|
transcript_path = (
|
|
57
|
-
session.get("transcript_path")
|
|
57
|
+
session.get("transcript_path")
|
|
58
|
+
or data.get("transcript_path")
|
|
59
|
+
or data.get("transcriptPath")
|
|
60
|
+
or ""
|
|
58
61
|
)
|
|
59
62
|
cwd = data.get("cwd") or ""
|
|
60
63
|
|
|
@@ -12,7 +12,7 @@ from typing import Optional
|
|
|
12
12
|
|
|
13
13
|
from ..logging_config import setup_logger
|
|
14
14
|
|
|
15
|
-
logger = setup_logger(
|
|
15
|
+
logger = setup_logger("realign.hooks.installer", "hooks_installer.log")
|
|
16
16
|
|
|
17
17
|
# 用于标识 Aline hook 的标记,方便后续识别和卸载
|
|
18
18
|
ALINE_HOOK_MARKER = "aline-stop-hook"
|
|
@@ -21,7 +21,7 @@ ALINE_HOOK_MARKER = "aline-stop-hook"
|
|
|
21
21
|
def get_stop_hook_script_path() -> Path:
|
|
22
22
|
"""获取 stop_hook.py 脚本的路径"""
|
|
23
23
|
# 脚本与此文件在同一目录
|
|
24
|
-
return Path(__file__).parent /
|
|
24
|
+
return Path(__file__).parent / "stop_hook.py"
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def get_stop_hook_command() -> str:
|
|
@@ -45,8 +45,8 @@ def get_settings_path(project_path: Optional[Path] = None) -> Path:
|
|
|
45
45
|
settings.json 的路径
|
|
46
46
|
"""
|
|
47
47
|
if project_path:
|
|
48
|
-
return project_path /
|
|
49
|
-
return Path.home() /
|
|
48
|
+
return project_path / ".claude" / "settings.local.json"
|
|
49
|
+
return Path.home() / ".claude" / "settings.json"
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def is_hook_installed(settings_path: Path) -> bool:
|
|
@@ -64,13 +64,13 @@ def is_hook_installed(settings_path: Path) -> bool:
|
|
|
64
64
|
|
|
65
65
|
try:
|
|
66
66
|
settings = json.loads(settings_path.read_text())
|
|
67
|
-
stop_hooks = settings.get(
|
|
67
|
+
stop_hooks = settings.get("hooks", {}).get("Stop", [])
|
|
68
68
|
|
|
69
69
|
for hook_config in stop_hooks:
|
|
70
|
-
hooks_list = hook_config.get(
|
|
70
|
+
hooks_list = hook_config.get("hooks", [])
|
|
71
71
|
for h in hooks_list:
|
|
72
|
-
command = h.get(
|
|
73
|
-
if ALINE_HOOK_MARKER in command or
|
|
72
|
+
command = h.get("command", "")
|
|
73
|
+
if ALINE_HOOK_MARKER in command or "realign.hooks.stop_hook" in command:
|
|
74
74
|
return True
|
|
75
75
|
|
|
76
76
|
return False
|
|
@@ -116,36 +116,34 @@ def install_stop_hook(
|
|
|
116
116
|
settings = {}
|
|
117
117
|
|
|
118
118
|
# 确保 hooks 结构存在
|
|
119
|
-
if
|
|
120
|
-
settings[
|
|
121
|
-
if
|
|
122
|
-
settings[
|
|
119
|
+
if "hooks" not in settings:
|
|
120
|
+
settings["hooks"] = {}
|
|
121
|
+
if "Stop" not in settings["hooks"]:
|
|
122
|
+
settings["hooks"]["Stop"] = []
|
|
123
123
|
|
|
124
124
|
# 如果强制安装,先移除旧的 Aline hook
|
|
125
125
|
if force:
|
|
126
|
-
stop_hooks = settings[
|
|
126
|
+
stop_hooks = settings["hooks"]["Stop"]
|
|
127
127
|
new_hooks = []
|
|
128
128
|
for hook_config in stop_hooks:
|
|
129
|
-
hooks_list = hook_config.get(
|
|
129
|
+
hooks_list = hook_config.get("hooks", [])
|
|
130
130
|
filtered = [
|
|
131
|
-
h
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
h
|
|
132
|
+
for h in hooks_list
|
|
133
|
+
if ALINE_HOOK_MARKER not in h.get("command", "")
|
|
134
|
+
and "realign.hooks.stop_hook" not in h.get("command", "")
|
|
134
135
|
]
|
|
135
136
|
if filtered:
|
|
136
|
-
hook_config[
|
|
137
|
+
hook_config["hooks"] = filtered
|
|
137
138
|
new_hooks.append(hook_config)
|
|
138
|
-
settings[
|
|
139
|
+
settings["hooks"]["Stop"] = new_hooks
|
|
139
140
|
|
|
140
141
|
# 追加 Aline hook
|
|
141
142
|
hook_command = get_stop_hook_command()
|
|
142
143
|
aline_hook = {
|
|
143
|
-
|
|
144
|
-
'type': 'command',
|
|
145
|
-
'command': f'{hook_command} # {ALINE_HOOK_MARKER}'
|
|
146
|
-
}]
|
|
144
|
+
"hooks": [{"type": "command", "command": f"{hook_command} # {ALINE_HOOK_MARKER}"}]
|
|
147
145
|
}
|
|
148
|
-
settings[
|
|
146
|
+
settings["hooks"]["Stop"].append(aline_hook)
|
|
149
147
|
|
|
150
148
|
# 写回设置
|
|
151
149
|
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -186,26 +184,27 @@ def uninstall_stop_hook(
|
|
|
186
184
|
return True
|
|
187
185
|
|
|
188
186
|
settings = json.loads(settings_path.read_text())
|
|
189
|
-
stop_hooks = settings.get(
|
|
187
|
+
stop_hooks = settings.get("hooks", {}).get("Stop", [])
|
|
190
188
|
|
|
191
189
|
# 过滤掉 Aline hook
|
|
192
190
|
new_hooks = []
|
|
193
191
|
removed = False
|
|
194
192
|
for hook_config in stop_hooks:
|
|
195
|
-
hooks_list = hook_config.get(
|
|
193
|
+
hooks_list = hook_config.get("hooks", [])
|
|
196
194
|
filtered = [
|
|
197
|
-
h
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
h
|
|
196
|
+
for h in hooks_list
|
|
197
|
+
if ALINE_HOOK_MARKER not in h.get("command", "")
|
|
198
|
+
and "realign.hooks.stop_hook" not in h.get("command", "")
|
|
200
199
|
]
|
|
201
200
|
if len(filtered) < len(hooks_list):
|
|
202
201
|
removed = True
|
|
203
202
|
if filtered:
|
|
204
|
-
hook_config[
|
|
203
|
+
hook_config["hooks"] = filtered
|
|
205
204
|
new_hooks.append(hook_config)
|
|
206
205
|
|
|
207
206
|
if removed:
|
|
208
|
-
settings[
|
|
207
|
+
settings["hooks"]["Stop"] = new_hooks
|
|
209
208
|
settings_path.write_text(json.dumps(settings, indent=2))
|
|
210
209
|
logger.info("Aline Stop hook uninstalled")
|
|
211
210
|
if not quiet:
|
realign/cli.py
CHANGED
|
@@ -19,13 +19,27 @@ console = Console()
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@app.callback()
|
|
22
|
-
def main(
|
|
22
|
+
def main(
|
|
23
|
+
ctx: typer.Context,
|
|
24
|
+
dev: bool = typer.Option(False, "--dev", help="Enable developer mode (shows Watcher and Worker tabs)"),
|
|
25
|
+
):
|
|
23
26
|
"""
|
|
24
27
|
Aline CLI - Shared AI Memory for teams.
|
|
25
28
|
|
|
26
29
|
Run 'aline' without arguments to open the interactive dashboard.
|
|
27
30
|
"""
|
|
31
|
+
# Store dev mode in context for subcommands
|
|
32
|
+
ctx.ensure_object(dict)
|
|
33
|
+
ctx.obj["dev"] = dev
|
|
34
|
+
|
|
28
35
|
if ctx.invoked_subcommand is None:
|
|
36
|
+
# Check for updates before launching dashboard
|
|
37
|
+
from .commands.upgrade import check_and_prompt_update
|
|
38
|
+
|
|
39
|
+
if check_and_prompt_update():
|
|
40
|
+
# Update was performed, exit so user can restart with new version
|
|
41
|
+
raise typer.Exit(0)
|
|
42
|
+
|
|
29
43
|
# Launch the dashboard when no subcommand is provided
|
|
30
44
|
from .dashboard.tmux_manager import bootstrap_dashboard_into_tmux
|
|
31
45
|
|
|
@@ -33,7 +47,7 @@ def main(ctx: typer.Context):
|
|
|
33
47
|
|
|
34
48
|
from .dashboard.app import AlineDashboard
|
|
35
49
|
|
|
36
|
-
dashboard = AlineDashboard()
|
|
50
|
+
dashboard = AlineDashboard(dev_mode=dev)
|
|
37
51
|
dashboard.run()
|
|
38
52
|
|
|
39
53
|
|
|
@@ -802,7 +816,10 @@ def version():
|
|
|
802
816
|
|
|
803
817
|
|
|
804
818
|
@app.command()
|
|
805
|
-
def dashboard(
|
|
819
|
+
def dashboard(
|
|
820
|
+
ctx: typer.Context,
|
|
821
|
+
dev: bool = typer.Option(False, "--dev", help="Enable developer mode (shows Watcher and Worker tabs)"),
|
|
822
|
+
):
|
|
806
823
|
"""Open the interactive TUI dashboard."""
|
|
807
824
|
from .dashboard.tmux_manager import bootstrap_dashboard_into_tmux
|
|
808
825
|
|
|
@@ -810,7 +827,9 @@ def dashboard():
|
|
|
810
827
|
|
|
811
828
|
from .dashboard.app import AlineDashboard
|
|
812
829
|
|
|
813
|
-
|
|
830
|
+
# Use dev flag from this command or inherit from parent context
|
|
831
|
+
dev_mode = dev or (ctx.obj.get("dev", False) if ctx.obj else False)
|
|
832
|
+
dash = AlineDashboard(dev_mode=dev_mode)
|
|
814
833
|
dash.run()
|
|
815
834
|
|
|
816
835
|
|
realign/codex_detector.py
CHANGED
|
@@ -7,10 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Optional, List
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def find_codex_sessions_for_project(
|
|
11
|
-
project_path: Path,
|
|
12
|
-
days_back: int = 7
|
|
13
|
-
) -> List[Path]:
|
|
10
|
+
def find_codex_sessions_for_project(project_path: Path, days_back: int = 7) -> List[Path]:
|
|
14
11
|
"""
|
|
15
12
|
Find Codex sessions for a given project path.
|
|
16
13
|
|
|
@@ -38,7 +35,12 @@ def find_codex_sessions_for_project(
|
|
|
38
35
|
# Search through recent days
|
|
39
36
|
for days_ago in range(days_back + 1):
|
|
40
37
|
target_date = datetime.now() - timedelta(days=days_ago)
|
|
41
|
-
date_path =
|
|
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
|
+
)
|
|
42
44
|
|
|
43
45
|
if not date_path.exists():
|
|
44
46
|
continue
|
|
@@ -47,12 +49,12 @@ def find_codex_sessions_for_project(
|
|
|
47
49
|
for session_file in date_path.glob("rollout-*.jsonl"):
|
|
48
50
|
try:
|
|
49
51
|
# Read first line to get session metadata
|
|
50
|
-
with open(session_file,
|
|
52
|
+
with open(session_file, "r", encoding="utf-8") as f:
|
|
51
53
|
first_line = f.readline()
|
|
52
54
|
if first_line:
|
|
53
55
|
data = json.loads(first_line)
|
|
54
|
-
if data.get(
|
|
55
|
-
session_cwd = data.get(
|
|
56
|
+
if data.get("type") == "session_meta":
|
|
57
|
+
session_cwd = data.get("payload", {}).get("cwd", "")
|
|
56
58
|
# Match the project path
|
|
57
59
|
if session_cwd == abs_project_path:
|
|
58
60
|
matching_sessions.append(session_file)
|
|
@@ -93,9 +95,7 @@ def get_codex_sessions_dir() -> Optional[Path]:
|
|
|
93
95
|
|
|
94
96
|
|
|
95
97
|
def auto_detect_codex_sessions(
|
|
96
|
-
project_path: Path,
|
|
97
|
-
fallback_path: Optional[str] = None,
|
|
98
|
-
days_back: int = 7
|
|
98
|
+
project_path: Path, fallback_path: Optional[str] = None, days_back: int = 7
|
|
99
99
|
) -> Optional[Path]:
|
|
100
100
|
"""
|
|
101
101
|
Auto-detect the most recent Codex session for a project.
|