aline-ai 0.6.2__py3-none-any.whl → 0.6.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.6.2.dist-info → aline_ai-0.6.4.dist-info}/METADATA +1 -1
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/RECORD +38 -37
- realign/__init__.py +1 -1
- realign/adapters/__init__.py +0 -3
- realign/adapters/codex.py +14 -9
- realign/cli.py +42 -236
- realign/codex_detector.py +72 -32
- realign/codex_home.py +85 -0
- realign/codex_terminal_linker.py +172 -0
- realign/commands/__init__.py +2 -2
- realign/commands/add.py +89 -9
- realign/commands/doctor.py +495 -0
- realign/commands/export_shares.py +154 -226
- realign/commands/init.py +66 -4
- realign/commands/watcher.py +30 -80
- realign/config.py +9 -46
- realign/dashboard/app.py +7 -11
- realign/dashboard/screens/event_detail.py +0 -3
- realign/dashboard/screens/session_detail.py +0 -1
- realign/dashboard/tmux_manager.py +129 -4
- realign/dashboard/widgets/config_panel.py +175 -241
- realign/dashboard/widgets/events_table.py +71 -128
- realign/dashboard/widgets/sessions_table.py +77 -136
- realign/dashboard/widgets/terminal_panel.py +349 -27
- realign/dashboard/widgets/watcher_panel.py +0 -2
- realign/db/sqlite_db.py +77 -2
- realign/events/event_summarizer.py +76 -35
- realign/events/session_summarizer.py +73 -32
- realign/hooks.py +334 -647
- realign/llm_client.py +201 -520
- realign/triggers/__init__.py +0 -2
- realign/triggers/next_turn_trigger.py +4 -5
- realign/triggers/registry.py +1 -4
- realign/watcher_core.py +53 -35
- realign/adapters/antigravity.py +0 -159
- realign/triggers/antigravity_trigger.py +0 -140
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/top_level.txt +0 -0
realign/triggers/__init__.py
CHANGED
|
@@ -8,7 +8,6 @@ from .base import TurnTrigger, TurnInfo
|
|
|
8
8
|
from .claude_trigger import ClaudeTrigger
|
|
9
9
|
from .codex_trigger import CodexTrigger
|
|
10
10
|
from .gemini_trigger import GeminiTrigger
|
|
11
|
-
from .antigravity_trigger import AntigravityTrigger
|
|
12
11
|
from .next_turn_trigger import NextTurnTrigger
|
|
13
12
|
from .registry import TriggerRegistry, get_global_registry
|
|
14
13
|
|
|
@@ -19,7 +18,6 @@ __all__ = [
|
|
|
19
18
|
"ClaudeTrigger",
|
|
20
19
|
"CodexTrigger",
|
|
21
20
|
"GeminiTrigger",
|
|
22
|
-
"AntigravityTrigger",
|
|
23
21
|
"TriggerRegistry",
|
|
24
22
|
"get_global_registry",
|
|
25
23
|
]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
NextTurnTrigger
|
|
3
3
|
|
|
4
4
|
A generic delegating trigger that auto-detects the session format and dispatches
|
|
5
|
-
to the appropriate concrete trigger (Claude/Codex/Gemini
|
|
5
|
+
to the appropriate concrete trigger (Claude/Codex/Gemini).
|
|
6
6
|
|
|
7
7
|
This is used as a stable default trigger name ("next_turn") across the codebase.
|
|
8
8
|
"""
|
|
@@ -17,16 +17,15 @@ from .base import TurnInfo, TurnTrigger
|
|
|
17
17
|
from .claude_trigger import ClaudeTrigger
|
|
18
18
|
from .codex_trigger import CodexTrigger
|
|
19
19
|
from .gemini_trigger import GeminiTrigger
|
|
20
|
-
from .antigravity_trigger import AntigravityTrigger
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
class NextTurnTrigger(TurnTrigger):
|
|
24
23
|
"""Auto-detect trigger for any supported session type."""
|
|
25
24
|
|
|
26
25
|
def select_trigger(self, session_file: Path) -> Optional[TurnTrigger]:
|
|
27
|
-
#
|
|
26
|
+
# Skip directories (not a supported session format)
|
|
28
27
|
if session_file.is_dir():
|
|
29
|
-
return
|
|
28
|
+
return None
|
|
30
29
|
|
|
31
30
|
# Heuristics based on path
|
|
32
31
|
path_str = str(session_file).lower()
|
|
@@ -87,7 +86,7 @@ class NextTurnTrigger(TurnTrigger):
|
|
|
87
86
|
def get_supported_formats(self) -> List[str]:
|
|
88
87
|
# Union of underlying triggers.
|
|
89
88
|
formats: List[str] = []
|
|
90
|
-
for cls in (ClaudeTrigger, CodexTrigger, GeminiTrigger
|
|
89
|
+
for cls in (ClaudeTrigger, CodexTrigger, GeminiTrigger):
|
|
91
90
|
try:
|
|
92
91
|
formats.extend(cls(self.config).get_supported_formats())
|
|
93
92
|
except Exception:
|
realign/triggers/registry.py
CHANGED
|
@@ -11,7 +11,6 @@ from .base import TurnTrigger
|
|
|
11
11
|
from .claude_trigger import ClaudeTrigger
|
|
12
12
|
from .codex_trigger import CodexTrigger
|
|
13
13
|
from .gemini_trigger import GeminiTrigger
|
|
14
|
-
from .antigravity_trigger import AntigravityTrigger
|
|
15
14
|
from .next_turn_trigger import NextTurnTrigger
|
|
16
15
|
|
|
17
16
|
logger = logging.getLogger(__name__)
|
|
@@ -28,7 +27,6 @@ class TriggerRegistry:
|
|
|
28
27
|
"claude": ClaudeTrigger,
|
|
29
28
|
"codex": CodexTrigger,
|
|
30
29
|
"gemini": GeminiTrigger,
|
|
31
|
-
"antigravity": AntigravityTrigger,
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
# 注册所有triggers (用于list_triggers和get_trigger)
|
|
@@ -36,7 +34,6 @@ class TriggerRegistry:
|
|
|
36
34
|
self.register("claude", ClaudeTrigger)
|
|
37
35
|
self.register("codex", CodexTrigger)
|
|
38
36
|
self.register("gemini", GeminiTrigger)
|
|
39
|
-
self.register("antigravity", AntigravityTrigger)
|
|
40
37
|
|
|
41
38
|
def register(self, name: str, trigger_class: Type[TurnTrigger]):
|
|
42
39
|
"""
|
|
@@ -71,7 +68,7 @@ class TriggerRegistry:
|
|
|
71
68
|
根据session类型获取对应的trigger
|
|
72
69
|
|
|
73
70
|
Args:
|
|
74
|
-
session_type: session类型 ("claude", "codex", "gemini"
|
|
71
|
+
session_type: session类型 ("claude", "codex", "gemini")
|
|
75
72
|
config: 可选的配置字典
|
|
76
73
|
|
|
77
74
|
Returns:
|
realign/watcher_core.py
CHANGED
|
@@ -23,7 +23,7 @@ logger = setup_logger("realign.watcher_core", "watcher_core.log")
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
# Session type detection
|
|
26
|
-
SessionType = Literal["claude", "codex", "gemini", "
|
|
26
|
+
SessionType = Literal["claude", "codex", "gemini", "unknown"]
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def is_path_blacklisted(project_path: Path) -> bool:
|
|
@@ -251,6 +251,46 @@ class DialogueWatcher:
|
|
|
251
251
|
self.user_prompt_signal_dir = self.signal_dir / "user_prompt_submit"
|
|
252
252
|
self.user_prompt_signal_dir.mkdir(parents=True, exist_ok=True)
|
|
253
253
|
|
|
254
|
+
def _maybe_link_codex_terminal(self, session_file: Path) -> None:
|
|
255
|
+
"""Best-effort: bind a Codex session file to the most likely active Codex terminal."""
|
|
256
|
+
try:
|
|
257
|
+
if self._detect_session_type(session_file) != "codex":
|
|
258
|
+
return
|
|
259
|
+
except Exception:
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
from .codex_home import terminal_id_from_codex_session_file
|
|
264
|
+
from .codex_terminal_linker import read_codex_session_meta, select_agent_for_codex_session
|
|
265
|
+
from .db import get_database
|
|
266
|
+
|
|
267
|
+
meta = read_codex_session_meta(session_file)
|
|
268
|
+
if meta is None:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
db = get_database(read_only=False)
|
|
272
|
+
agents = db.list_agents(status="active", limit=1000)
|
|
273
|
+
# Deterministic mapping: session file stored under ~/.aline/codex_homes/<terminal_id>/...
|
|
274
|
+
agent_id = terminal_id_from_codex_session_file(session_file)
|
|
275
|
+
if not agent_id:
|
|
276
|
+
# Fallback heuristic mapping (legacy default ~/.codex/sessions).
|
|
277
|
+
agent_id = select_agent_for_codex_session(agents, session=meta)
|
|
278
|
+
if not agent_id:
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
db.update_agent(
|
|
282
|
+
agent_id,
|
|
283
|
+
provider="codex",
|
|
284
|
+
session_type="codex",
|
|
285
|
+
session_id=session_file.stem,
|
|
286
|
+
transcript_path=str(session_file),
|
|
287
|
+
cwd=meta.cwd,
|
|
288
|
+
project_dir=meta.cwd,
|
|
289
|
+
source="codex:auto-link",
|
|
290
|
+
)
|
|
291
|
+
except Exception:
|
|
292
|
+
return
|
|
293
|
+
|
|
254
294
|
async def start(self):
|
|
255
295
|
"""Start watching session files."""
|
|
256
296
|
if not self.config.mcp_auto_commit:
|
|
@@ -540,9 +580,6 @@ class DialogueWatcher:
|
|
|
540
580
|
current_count = self._count_complete_turns(session_file)
|
|
541
581
|
last_count = self.last_stop_reason_counts.get(session_path, 0)
|
|
542
582
|
|
|
543
|
-
if session_type == "antigravity":
|
|
544
|
-
return [1] if current_count >= 1 else []
|
|
545
|
-
|
|
546
583
|
if current_count <= last_count:
|
|
547
584
|
return []
|
|
548
585
|
|
|
@@ -974,9 +1011,9 @@ class DialogueWatcher:
|
|
|
974
1011
|
adapter = registry.auto_detect_adapter(session_file)
|
|
975
1012
|
if adapter:
|
|
976
1013
|
# Map adapter name to SessionType
|
|
977
|
-
# Adapter names: "claude", "codex", "gemini"
|
|
1014
|
+
# Adapter names: "claude", "codex", "gemini"
|
|
978
1015
|
name = adapter.name
|
|
979
|
-
if name in ["claude", "codex", "gemini"
|
|
1016
|
+
if name in ["claude", "codex", "gemini"]:
|
|
980
1017
|
return name
|
|
981
1018
|
|
|
982
1019
|
return "unknown"
|
|
@@ -1050,6 +1087,11 @@ class DialogueWatcher:
|
|
|
1050
1087
|
if old_size is None or old_mtime is None:
|
|
1051
1088
|
changed_files.append(Path(path))
|
|
1052
1089
|
logger.debug(f"Session file first seen: {Path(path).name} ({size} bytes)")
|
|
1090
|
+
# Best-effort: link newly discovered Codex sessions to an active Codex terminal.
|
|
1091
|
+
try:
|
|
1092
|
+
self._maybe_link_codex_terminal(Path(path))
|
|
1093
|
+
except Exception:
|
|
1094
|
+
pass
|
|
1053
1095
|
# Reset idle final-commit attempt tracking for new files
|
|
1054
1096
|
self.last_final_commit_times.pop(path, None)
|
|
1055
1097
|
continue
|
|
@@ -1110,6 +1152,11 @@ class DialogueWatcher:
|
|
|
1110
1152
|
for session_file in changed_files:
|
|
1111
1153
|
if not session_file.exists():
|
|
1112
1154
|
continue
|
|
1155
|
+
# Best-effort: keep terminal bindings fresh (especially after watcher restarts).
|
|
1156
|
+
try:
|
|
1157
|
+
self._maybe_link_codex_terminal(session_file)
|
|
1158
|
+
except Exception:
|
|
1159
|
+
pass
|
|
1113
1160
|
new_turns = self._get_new_completed_turn_numbers(session_file)
|
|
1114
1161
|
if new_turns:
|
|
1115
1162
|
sessions_to_enqueue.append((session_file, new_turns))
|
|
@@ -1956,35 +2003,6 @@ class DialogueWatcher:
|
|
|
1956
2003
|
indent=2,
|
|
1957
2004
|
)
|
|
1958
2005
|
|
|
1959
|
-
if session_format == "antigravity_markdown":
|
|
1960
|
-
print(
|
|
1961
|
-
f"[Debug] Extracting Antigravity content for turn {turn_number}",
|
|
1962
|
-
file=sys.stderr,
|
|
1963
|
-
)
|
|
1964
|
-
# Directly read artifact files from directory (don't rely on trigger.user_message which is empty)
|
|
1965
|
-
session_dir = session_file if session_file.is_dir() else session_file.parent
|
|
1966
|
-
artifacts = ["task.md", "walkthrough.md", "implementation_plan.md"]
|
|
1967
|
-
content_parts = []
|
|
1968
|
-
for filename in artifacts:
|
|
1969
|
-
path = session_dir / filename
|
|
1970
|
-
if path.exists():
|
|
1971
|
-
try:
|
|
1972
|
-
text = path.read_text(encoding="utf-8")
|
|
1973
|
-
content_parts.append(f"--- {filename} ---\n{text}")
|
|
1974
|
-
except Exception as e:
|
|
1975
|
-
print(f"[Debug] Failed to read {filename}: {e}", file=sys.stderr)
|
|
1976
|
-
full_content = "\n\n".join(content_parts)
|
|
1977
|
-
if full_content:
|
|
1978
|
-
print(
|
|
1979
|
-
f"[Debug] Antigravity content extracted: {len(full_content)} chars",
|
|
1980
|
-
file=sys.stderr,
|
|
1981
|
-
)
|
|
1982
|
-
return full_content
|
|
1983
|
-
print(
|
|
1984
|
-
f"[Debug] Antigravity content empty - no artifact files found", file=sys.stderr
|
|
1985
|
-
)
|
|
1986
|
-
return ""
|
|
1987
|
-
|
|
1988
2006
|
# For JSONL formats, extract by line numbers
|
|
1989
2007
|
start_line = group.get("start_line") or (group.get("lines") or [None])[0]
|
|
1990
2008
|
end_line = group.get("end_line") or (group.get("lines") or [None])[-1]
|
realign/adapters/antigravity.py
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Antigravity IDE Adapter
|
|
3
|
-
|
|
4
|
-
Handles session discovery for Antigravity IDE (Gemini in IDE).
|
|
5
|
-
Since .pb conversation files are encrypted, this adapter uses
|
|
6
|
-
readable brain artifacts (walkthrough.md, task.md) as session indicators.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import re
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import List, Optional, Dict, Any
|
|
12
|
-
|
|
13
|
-
from .base import SessionAdapter
|
|
14
|
-
from ..triggers.antigravity_trigger import AntigravityTrigger
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class AntigravityAdapter(SessionAdapter):
|
|
18
|
-
"""Adapter for Antigravity IDE sessions."""
|
|
19
|
-
|
|
20
|
-
name = "antigravity"
|
|
21
|
-
trigger_class = AntigravityTrigger
|
|
22
|
-
|
|
23
|
-
def discover_sessions(self) -> List[Path]:
|
|
24
|
-
"""
|
|
25
|
-
Find all active Antigravity IDE sessions.
|
|
26
|
-
Returns the directory path for each conversation that contains relevant artifacts.
|
|
27
|
-
"""
|
|
28
|
-
sessions = []
|
|
29
|
-
gemini_brain = Path.home() / ".gemini" / "antigravity" / "brain"
|
|
30
|
-
|
|
31
|
-
if not gemini_brain.exists():
|
|
32
|
-
return sessions
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
for conv_dir in gemini_brain.iterdir():
|
|
36
|
-
if not conv_dir.is_dir():
|
|
37
|
-
continue
|
|
38
|
-
|
|
39
|
-
# Check for key artifacts
|
|
40
|
-
has_artifacts = any(
|
|
41
|
-
(conv_dir / filename).exists()
|
|
42
|
-
for filename in ["task.md", "walkthrough.md", "implementation_plan.md"]
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
if has_artifacts:
|
|
46
|
-
sessions.append(conv_dir)
|
|
47
|
-
except Exception:
|
|
48
|
-
pass
|
|
49
|
-
|
|
50
|
-
return sessions
|
|
51
|
-
|
|
52
|
-
def discover_sessions_for_project(self, project_path: Path) -> List[Path]:
|
|
53
|
-
"""
|
|
54
|
-
Find sessions for a specific project.
|
|
55
|
-
"""
|
|
56
|
-
all_sessions = self.discover_sessions()
|
|
57
|
-
project_sessions = []
|
|
58
|
-
|
|
59
|
-
for session in all_sessions:
|
|
60
|
-
extracted_path = self.extract_project_path(session)
|
|
61
|
-
if extracted_path and extracted_path == project_path:
|
|
62
|
-
project_sessions.append(session)
|
|
63
|
-
|
|
64
|
-
return project_sessions
|
|
65
|
-
|
|
66
|
-
def extract_project_path(self, session_file: Path) -> Optional[Path]:
|
|
67
|
-
"""
|
|
68
|
-
Infer project root from Antigravity brain artifacts.
|
|
69
|
-
|
|
70
|
-
Antigravity stores readable artifacts (task.md / walkthrough.md / implementation_plan.md).
|
|
71
|
-
We attempt to extract a `file://...` reference, then walk upward to find a `.git` root.
|
|
72
|
-
"""
|
|
73
|
-
session_dir = session_file if session_file.is_dir() else session_file.parent
|
|
74
|
-
candidates: List[Path] = []
|
|
75
|
-
|
|
76
|
-
artifact_names = ["task.md", "walkthrough.md", "implementation_plan.md"]
|
|
77
|
-
for name in artifact_names:
|
|
78
|
-
p = session_dir / name
|
|
79
|
-
if not p.exists():
|
|
80
|
-
continue
|
|
81
|
-
try:
|
|
82
|
-
text = p.read_text(encoding="utf-8", errors="ignore")
|
|
83
|
-
except Exception:
|
|
84
|
-
continue
|
|
85
|
-
|
|
86
|
-
# Capture file:// paths from Markdown links or plain text.
|
|
87
|
-
# Example: file:///Users/me/Project/src/main.py or file://.../src/main.py
|
|
88
|
-
for match in re.findall(r"file://([^)\s]+)", text):
|
|
89
|
-
raw = match.strip()
|
|
90
|
-
if raw.startswith("/"):
|
|
91
|
-
candidates.append(Path(raw))
|
|
92
|
-
else:
|
|
93
|
-
# Some tools emit file://Users/... (missing leading slash)
|
|
94
|
-
candidates.append(Path("/" + raw))
|
|
95
|
-
|
|
96
|
-
for candidate in candidates:
|
|
97
|
-
try:
|
|
98
|
-
# Prefer the nearest VCS root
|
|
99
|
-
path = candidate
|
|
100
|
-
if path.is_file():
|
|
101
|
-
path = path.parent
|
|
102
|
-
for parent in [path] + list(path.parents):
|
|
103
|
-
git_dir = parent / ".git"
|
|
104
|
-
if git_dir.exists():
|
|
105
|
-
return parent
|
|
106
|
-
except Exception:
|
|
107
|
-
continue
|
|
108
|
-
|
|
109
|
-
# Fallback: if we found a candidate that exists, return its parent directory.
|
|
110
|
-
for candidate in candidates:
|
|
111
|
-
try:
|
|
112
|
-
if candidate.exists():
|
|
113
|
-
return candidate.parent if candidate.is_file() else candidate
|
|
114
|
-
except Exception:
|
|
115
|
-
continue
|
|
116
|
-
|
|
117
|
-
return None
|
|
118
|
-
|
|
119
|
-
def get_session_metadata(self, session_file: Path) -> Dict[str, Any]:
|
|
120
|
-
"""Extract rich metadata from Antigravity brain artifacts."""
|
|
121
|
-
metadata = super().get_session_metadata(session_file)
|
|
122
|
-
|
|
123
|
-
if not session_file.exists():
|
|
124
|
-
return metadata
|
|
125
|
-
|
|
126
|
-
session_dir = session_file if session_file.is_dir() else session_file.parent
|
|
127
|
-
|
|
128
|
-
try:
|
|
129
|
-
# We just return the turn count (always 1 if exists)
|
|
130
|
-
# No task progress parsing required
|
|
131
|
-
metadata["turn_count"] = self.trigger.count_complete_turns(session_file)
|
|
132
|
-
|
|
133
|
-
except Exception:
|
|
134
|
-
pass
|
|
135
|
-
|
|
136
|
-
return metadata
|
|
137
|
-
|
|
138
|
-
def is_session_valid(self, session_file: Path) -> bool:
|
|
139
|
-
"""Check if this is an Antigravity artifact directory."""
|
|
140
|
-
if not session_file.is_dir():
|
|
141
|
-
if (
|
|
142
|
-
session_file.parent.name == "brain"
|
|
143
|
-
and session_file.parent.parent.name == "antigravity"
|
|
144
|
-
):
|
|
145
|
-
# It's a directory inside brain
|
|
146
|
-
return True
|
|
147
|
-
return False
|
|
148
|
-
|
|
149
|
-
# Check parent hierarchy
|
|
150
|
-
# .../.gemini/antigravity/brain/<uuid>
|
|
151
|
-
try:
|
|
152
|
-
if (
|
|
153
|
-
session_file.parent.name == "brain"
|
|
154
|
-
and session_file.parent.parent.name == "antigravity"
|
|
155
|
-
):
|
|
156
|
-
return True
|
|
157
|
-
except Exception:
|
|
158
|
-
pass
|
|
159
|
-
return False
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
AntigravityTrigger - Trigger for Antigravity IDE (Markdown artifacts)
|
|
3
|
-
|
|
4
|
-
Since Antigravity produces walkthrough.md and task.md instead of turn-based JSONL,
|
|
5
|
-
this trigger uses file content/structure as a signal for "turns".
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import List, Optional, Dict, Any
|
|
11
|
-
import re
|
|
12
|
-
from .base import TurnTrigger, TurnInfo
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class AntigravityTrigger(TurnTrigger):
|
|
16
|
-
"""
|
|
17
|
-
Trigger for Antigravity IDE markdown artifacts.
|
|
18
|
-
Each distinct Task in task.md or major change in walkthrough.md can be
|
|
19
|
-
treated as a 'turn'.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
def get_supported_formats(self) -> List[str]:
|
|
23
|
-
return ["antigravity_markdown"]
|
|
24
|
-
|
|
25
|
-
def detect_session_format(self, session_file: Path) -> Optional[str]:
|
|
26
|
-
"""Detect if this is an Antigravity brain artifact directory."""
|
|
27
|
-
try:
|
|
28
|
-
# Check if directory
|
|
29
|
-
if session_file.is_dir():
|
|
30
|
-
if session_file.parent.name == "brain" and "antigravity" in str(session_file):
|
|
31
|
-
return "antigravity_markdown"
|
|
32
|
-
return None
|
|
33
|
-
|
|
34
|
-
# Legacy/Fallback: Check if file
|
|
35
|
-
if session_file.suffix == ".md":
|
|
36
|
-
path_str = str(session_file)
|
|
37
|
-
if "gemini" in path_str and "brain" in path_str:
|
|
38
|
-
return "antigravity_markdown"
|
|
39
|
-
if ".antigravity" in path_str or "antigravity" in path_str.lower():
|
|
40
|
-
return "antigravity_markdown"
|
|
41
|
-
return None
|
|
42
|
-
except Exception:
|
|
43
|
-
return None
|
|
44
|
-
|
|
45
|
-
def count_complete_turns(self, session_file: Path) -> int:
|
|
46
|
-
"""
|
|
47
|
-
Antigravity sessions are effectively "single-turn" persistent states.
|
|
48
|
-
We return 1 if the artifacts exist, 0 otherwise.
|
|
49
|
-
|
|
50
|
-
The watcher will handle change detection via content hashing or mtime,
|
|
51
|
-
even if this count stays at 1.
|
|
52
|
-
|
|
53
|
-
Args:
|
|
54
|
-
session_file: Path to brain directory (or file)
|
|
55
|
-
|
|
56
|
-
Returns:
|
|
57
|
-
int: 1 if artifacts exist, 0 otherwise.
|
|
58
|
-
"""
|
|
59
|
-
if not session_file.exists():
|
|
60
|
-
return 0
|
|
61
|
-
|
|
62
|
-
session_dir = session_file if session_file.is_dir() else session_file.parent
|
|
63
|
-
artifacts = ["task.md", "walkthrough.md", "implementation_plan.md"]
|
|
64
|
-
|
|
65
|
-
has_artifacts = False
|
|
66
|
-
for filename in artifacts:
|
|
67
|
-
path = session_dir / filename
|
|
68
|
-
if path.exists():
|
|
69
|
-
has_artifacts = True
|
|
70
|
-
break
|
|
71
|
-
|
|
72
|
-
return 1 if has_artifacts else 0
|
|
73
|
-
|
|
74
|
-
def extract_turn_info(self, session_file: Path, turn_number: int) -> Optional[TurnInfo]:
|
|
75
|
-
"""Extract information by aggregating artifacts."""
|
|
76
|
-
if not session_file.exists():
|
|
77
|
-
return None
|
|
78
|
-
|
|
79
|
-
# Ensure we point to the directory
|
|
80
|
-
session_dir = session_file if session_file.is_dir() else session_file.parent
|
|
81
|
-
|
|
82
|
-
content_parts = []
|
|
83
|
-
artifacts = ["task.md", "walkthrough.md", "implementation_plan.md"]
|
|
84
|
-
|
|
85
|
-
# Aggregate content
|
|
86
|
-
for filename in artifacts:
|
|
87
|
-
path = session_dir / filename
|
|
88
|
-
if path.exists():
|
|
89
|
-
text = path.read_text(encoding="utf-8")
|
|
90
|
-
content_parts.append(f"--- {filename} ---\n{text}")
|
|
91
|
-
|
|
92
|
-
full_content = "\n\n".join(content_parts)
|
|
93
|
-
if not full_content:
|
|
94
|
-
return None
|
|
95
|
-
|
|
96
|
-
# Always use current time for timestamp as this is an evolving state
|
|
97
|
-
timestamp = datetime.now().isoformat()
|
|
98
|
-
|
|
99
|
-
return TurnInfo(
|
|
100
|
-
turn_number=1, # Always Turn 1
|
|
101
|
-
user_message="", # Empty - full content used elsewhere for summary generation
|
|
102
|
-
start_line=1,
|
|
103
|
-
end_line=len(full_content.splitlines()) if full_content else 0,
|
|
104
|
-
timestamp=timestamp,
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
def is_turn_complete(self, session_file: Path, turn_number: int) -> bool:
|
|
108
|
-
# For Antigravity, if we have artifacts, it's "complete" in the sense that it exists.
|
|
109
|
-
return self.count_complete_turns(session_file) >= 1
|
|
110
|
-
|
|
111
|
-
def get_detailed_analysis(self, session_file: Path) -> Dict[str, Any]:
|
|
112
|
-
"""
|
|
113
|
-
Get detailed analysis of the session.
|
|
114
|
-
Since we treat the state as a single accumulated turn, we return one turn group.
|
|
115
|
-
"""
|
|
116
|
-
current_turn = self.count_complete_turns(session_file)
|
|
117
|
-
|
|
118
|
-
groups = []
|
|
119
|
-
# Return a single entry representing the current state
|
|
120
|
-
if current_turn > 0:
|
|
121
|
-
info = self.extract_turn_info(session_file, 1)
|
|
122
|
-
if info:
|
|
123
|
-
groups.append(
|
|
124
|
-
{
|
|
125
|
-
"turn_number": 1,
|
|
126
|
-
"user_message": info.user_message,
|
|
127
|
-
"summary_message": "Antigravity Session State",
|
|
128
|
-
"turn_status": "completed",
|
|
129
|
-
"start_line": info.start_line,
|
|
130
|
-
"end_line": info.end_line,
|
|
131
|
-
"timestamp": info.timestamp,
|
|
132
|
-
}
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
"groups": groups,
|
|
137
|
-
"total_turns": 1 if current_turn > 0 else 0, # Conceptually one continuous session
|
|
138
|
-
"latest_turn_id": 1 if current_turn > 0 else 0,
|
|
139
|
-
"format": "antigravity_markdown",
|
|
140
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|