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/redactor.py
CHANGED
|
@@ -14,7 +14,7 @@ from typing import List, Dict, Tuple, Optional
|
|
|
14
14
|
from .logging_config import setup_logger
|
|
15
15
|
|
|
16
16
|
# Initialize logger for redactor
|
|
17
|
-
logger = setup_logger(
|
|
17
|
+
logger = setup_logger("realign.redactor", "redactor.log")
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class SecretMatch:
|
|
@@ -44,24 +44,27 @@ def _detect_custom_api_keys(content: str) -> List[SecretMatch]:
|
|
|
44
44
|
import re
|
|
45
45
|
|
|
46
46
|
secrets = []
|
|
47
|
-
lines = content.split(
|
|
47
|
+
lines = content.split("\n")
|
|
48
48
|
|
|
49
49
|
# Common API key patterns
|
|
50
50
|
patterns = [
|
|
51
51
|
# OpenAI API keys (sk-, sk-proj-)
|
|
52
|
-
(r
|
|
52
|
+
(r"\bsk-[a-zA-Z0-9]{20,}", "OpenAI API Key"),
|
|
53
53
|
# Anthropic API keys (sk-ant-api03-...)
|
|
54
|
-
(r
|
|
54
|
+
(r"\bsk-ant-[a-zA-Z0-9\-]{50,}", "Anthropic API Key"),
|
|
55
55
|
# Generic API keys with common prefixes
|
|
56
|
-
(
|
|
56
|
+
(
|
|
57
|
+
r'\b(?:api[_-]?key|apikey|api[_-]?secret)[\s:=]+["\']?([a-zA-Z0-9_\-]{32,})["\']?',
|
|
58
|
+
"Generic API Key",
|
|
59
|
+
),
|
|
57
60
|
# Bearer tokens
|
|
58
|
-
(r
|
|
61
|
+
(r"\bBearer\s+[a-zA-Z0-9\-._~+/]+=*", "Bearer Token"),
|
|
59
62
|
# GitHub tokens
|
|
60
|
-
(r
|
|
63
|
+
(r"\bgh[ps]_[a-zA-Z0-9]{36,}", "GitHub Token"),
|
|
61
64
|
# Slack tokens
|
|
62
|
-
(r
|
|
65
|
+
(r"\bxox[baprs]-[a-zA-Z0-9\-]{10,}", "Slack Token"),
|
|
63
66
|
# Generic long alphanumeric strings that look like secrets (60+ chars, mixed case)
|
|
64
|
-
(r
|
|
67
|
+
(r"\b[a-zA-Z0-9]{60,}\b", "Potential Secret (Long String)"),
|
|
65
68
|
]
|
|
66
69
|
|
|
67
70
|
for line_num, line in enumerate(lines, start=1):
|
|
@@ -71,15 +74,24 @@ def _detect_custom_api_keys(content: str) -> List[SecretMatch]:
|
|
|
71
74
|
matched_text = match.group(0)
|
|
72
75
|
|
|
73
76
|
# Skip if it looks like a UUID (has hyphens in UUID pattern)
|
|
74
|
-
if re.match(
|
|
77
|
+
if re.match(
|
|
78
|
+
r"^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$",
|
|
79
|
+
matched_text,
|
|
80
|
+
re.IGNORECASE,
|
|
81
|
+
):
|
|
75
82
|
continue
|
|
76
83
|
|
|
77
84
|
# Skip common false positives
|
|
78
|
-
if matched_text.lower() in [
|
|
85
|
+
if matched_text.lower() in [
|
|
86
|
+
"example",
|
|
87
|
+
"placeholder",
|
|
88
|
+
"your_api_key_here",
|
|
89
|
+
"your-api-key",
|
|
90
|
+
]:
|
|
79
91
|
continue
|
|
80
92
|
|
|
81
93
|
# For "Potential Secret (Long String)", require mixed case to reduce false positives
|
|
82
|
-
if secret_type ==
|
|
94
|
+
if secret_type == "Potential Secret (Long String)":
|
|
83
95
|
has_upper = any(c.isupper() for c in matched_text)
|
|
84
96
|
has_lower = any(c.islower() for c in matched_text)
|
|
85
97
|
has_digit = any(c.isdigit() for c in matched_text)
|
|
@@ -89,13 +101,12 @@ def _detect_custom_api_keys(content: str) -> List[SecretMatch]:
|
|
|
89
101
|
|
|
90
102
|
# Create a hash of the secret for identification
|
|
91
103
|
import hashlib
|
|
104
|
+
|
|
92
105
|
secret_hash = hashlib.sha256(matched_text.encode()).hexdigest()[:16]
|
|
93
106
|
|
|
94
107
|
secrets.append(
|
|
95
108
|
SecretMatch(
|
|
96
|
-
secret_type=secret_type,
|
|
97
|
-
line_number=line_num,
|
|
98
|
-
secret_hash=secret_hash
|
|
109
|
+
secret_type=secret_type, line_number=line_num, secret_hash=secret_hash
|
|
99
110
|
)
|
|
100
111
|
)
|
|
101
112
|
logger.debug(f"Custom pattern detected: {secret_type} at line {line_num}")
|
|
@@ -118,6 +129,7 @@ def detect_secrets(content: str) -> Tuple[List[SecretMatch], bool]:
|
|
|
118
129
|
try:
|
|
119
130
|
from detect_secrets import SecretsCollection
|
|
120
131
|
from detect_secrets.settings import default_settings
|
|
132
|
+
|
|
121
133
|
logger.debug("detect-secrets library loaded successfully")
|
|
122
134
|
except ImportError:
|
|
123
135
|
logger.warning("detect-secrets library not installed")
|
|
@@ -133,10 +145,7 @@ def detect_secrets(content: str) -> Tuple[List[SecretMatch], bool]:
|
|
|
133
145
|
# Create a temporary file for scanning
|
|
134
146
|
try:
|
|
135
147
|
with tempfile.NamedTemporaryFile(
|
|
136
|
-
mode=
|
|
137
|
-
suffix='.jsonl',
|
|
138
|
-
delete=False,
|
|
139
|
-
encoding='utf-8'
|
|
148
|
+
mode="w", suffix=".jsonl", delete=False, encoding="utf-8"
|
|
140
149
|
) as f:
|
|
141
150
|
f.write(content)
|
|
142
151
|
temp_path = f.name
|
|
@@ -153,15 +162,17 @@ def detect_secrets(content: str) -> Tuple[List[SecretMatch], bool]:
|
|
|
153
162
|
for secret in secret_list:
|
|
154
163
|
# Filter out high-entropy detectors that cause false positives with UUIDs
|
|
155
164
|
# Note: detect-secrets uses "High Entropy" (with space) in type names like "Base64 High Entropy String"
|
|
156
|
-
if
|
|
157
|
-
logger.debug(
|
|
165
|
+
if "High Entropy" in secret.type or "HighEntropy" in secret.type:
|
|
166
|
+
logger.debug(
|
|
167
|
+
f"Filtering out high-entropy detection: {secret.type} at line {secret.line_number}"
|
|
168
|
+
)
|
|
158
169
|
continue
|
|
159
170
|
|
|
160
171
|
secrets.append(
|
|
161
172
|
SecretMatch(
|
|
162
173
|
secret_type=secret.type,
|
|
163
174
|
line_number=secret.line_number,
|
|
164
|
-
secret_hash=secret.secret_hash
|
|
175
|
+
secret_hash=secret.secret_hash,
|
|
165
176
|
)
|
|
166
177
|
)
|
|
167
178
|
|
|
@@ -194,7 +205,7 @@ def detect_secrets(content: str) -> Tuple[List[SecretMatch], bool]:
|
|
|
194
205
|
finally:
|
|
195
206
|
# Clean up temp file
|
|
196
207
|
try:
|
|
197
|
-
if
|
|
208
|
+
if "temp_path" in locals():
|
|
198
209
|
os.unlink(temp_path)
|
|
199
210
|
logger.debug("Temporary file cleaned up")
|
|
200
211
|
except Exception as e:
|
|
@@ -206,32 +217,65 @@ def detect_secrets(content: str) -> Tuple[List[SecretMatch], bool]:
|
|
|
206
217
|
# Fields that should NOT be redacted (metadata and non-sensitive data)
|
|
207
218
|
NON_SENSITIVE_FIELDS = {
|
|
208
219
|
# Message structure
|
|
209
|
-
|
|
220
|
+
"type",
|
|
221
|
+
"role",
|
|
222
|
+
"stop_reason",
|
|
223
|
+
"stop_sequence",
|
|
210
224
|
# Model metadata
|
|
211
|
-
|
|
225
|
+
"model",
|
|
226
|
+
"id",
|
|
227
|
+
"service_tier",
|
|
212
228
|
# Session metadata
|
|
213
|
-
|
|
229
|
+
"isSidechain",
|
|
230
|
+
"userType",
|
|
231
|
+
"version",
|
|
232
|
+
"gitBranch",
|
|
233
|
+
"cwd",
|
|
234
|
+
"slug",
|
|
214
235
|
# Identifiers (UUIDs, timestamps - not actual secrets)
|
|
215
|
-
|
|
236
|
+
"parentUuid",
|
|
237
|
+
"uuid",
|
|
238
|
+
"sessionId",
|
|
239
|
+
"requestId",
|
|
240
|
+
"timestamp",
|
|
216
241
|
# Token usage (not sensitive)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
242
|
+
"usage",
|
|
243
|
+
"input_tokens",
|
|
244
|
+
"output_tokens",
|
|
245
|
+
"cache_read_input_tokens",
|
|
246
|
+
"cache_creation_input_tokens",
|
|
247
|
+
"cache_creation",
|
|
248
|
+
"ephemeral_5m_input_tokens",
|
|
249
|
+
"ephemeral_1h_input_tokens",
|
|
220
250
|
# Tool metadata
|
|
221
|
-
|
|
251
|
+
"tool_use_id",
|
|
252
|
+
"name",
|
|
253
|
+
"is_error",
|
|
254
|
+
"interrupted",
|
|
255
|
+
"isImage",
|
|
222
256
|
# File/process info
|
|
223
|
-
|
|
224
|
-
|
|
257
|
+
"filenames",
|
|
258
|
+
"durationMs",
|
|
259
|
+
"numFiles",
|
|
260
|
+
"truncated",
|
|
261
|
+
"stdout",
|
|
262
|
+
"stderr",
|
|
263
|
+
"returnCodeInterpretation",
|
|
225
264
|
# Other metadata
|
|
226
|
-
|
|
227
|
-
|
|
265
|
+
"todos",
|
|
266
|
+
"oldTodos",
|
|
267
|
+
"newTodos",
|
|
268
|
+
"toolUseResult",
|
|
269
|
+
"context_management",
|
|
270
|
+
"applied_edits",
|
|
271
|
+
"operation",
|
|
228
272
|
}
|
|
229
273
|
|
|
230
274
|
# Fields that contain potentially sensitive content (user input, file contents, etc.)
|
|
231
275
|
SENSITIVE_CONTENT_FIELDS = {
|
|
232
276
|
# These fields may contain actual secrets and should be redacted if secrets detected
|
|
233
|
-
|
|
234
|
-
|
|
277
|
+
"content", # Main content field
|
|
278
|
+
"text", # Text content in messages
|
|
235
279
|
}
|
|
236
280
|
|
|
237
281
|
|
|
@@ -252,7 +296,7 @@ def redact_content(content: str, secrets: List[SecretMatch]) -> str:
|
|
|
252
296
|
|
|
253
297
|
logger.info(f"Redacting {len(secrets)} secret(s) from content")
|
|
254
298
|
|
|
255
|
-
lines = content.split(
|
|
299
|
+
lines = content.split("\n")
|
|
256
300
|
original_size = len(content)
|
|
257
301
|
|
|
258
302
|
# Group secrets by line number
|
|
@@ -322,27 +366,29 @@ def redact_content(content: str, secrets: List[SecretMatch]) -> str:
|
|
|
322
366
|
logger.warning(f"Failed to parse line {line_num + 1} as JSON, using regex redaction")
|
|
323
367
|
|
|
324
368
|
# Try to preserve structure by using regex to find and replace values
|
|
325
|
-
if
|
|
369
|
+
if ":" in original_line:
|
|
326
370
|
# Find the value part after the colon, preserving the closing braces/brackets
|
|
327
371
|
# Match pattern: : "value" or : value, capture trailing punctuation
|
|
328
372
|
pattern = r':\s*"[^"]*"(\s*[,}\]])'
|
|
329
373
|
if re.search(pattern, original_line):
|
|
330
374
|
lines[line_num] = re.sub(
|
|
331
|
-
pattern,
|
|
332
|
-
rf': "[REDACTED: {", ".join(set(secret_types))}]"\1',
|
|
333
|
-
original_line
|
|
375
|
+
pattern, rf': "[REDACTED: {", ".join(set(secret_types))}]"\1', original_line
|
|
334
376
|
)
|
|
335
377
|
else:
|
|
336
378
|
# Fallback: replace from colon onwards but keep trailing punctuation
|
|
337
|
-
match = re.search(r
|
|
379
|
+
match = re.search(r"(.*?:\s*)(.+?)(\s*[}\]]*\s*)$", original_line)
|
|
338
380
|
if match:
|
|
339
|
-
lines[line_num] =
|
|
381
|
+
lines[line_num] = (
|
|
382
|
+
f'{match.group(1)}"[REDACTED: {", ".join(set(secret_types))}]"{match.group(3)}'
|
|
383
|
+
)
|
|
340
384
|
else:
|
|
341
|
-
lines[line_num] =
|
|
385
|
+
lines[line_num] = (
|
|
386
|
+
f'{original_line}: "[REDACTED: {", ".join(set(secret_types))}]"'
|
|
387
|
+
)
|
|
342
388
|
else:
|
|
343
389
|
lines[line_num] = f"[REDACTED LINE - {', '.join(set(secret_types))}]"
|
|
344
390
|
|
|
345
|
-
redacted_content =
|
|
391
|
+
redacted_content = "\n".join(lines)
|
|
346
392
|
redacted_size = len(redacted_content)
|
|
347
393
|
logger.info(f"Redaction complete: {original_size} bytes -> {redacted_size} bytes")
|
|
348
394
|
|
|
@@ -350,9 +396,7 @@ def redact_content(content: str, secrets: List[SecretMatch]) -> str:
|
|
|
350
396
|
|
|
351
397
|
|
|
352
398
|
def check_and_redact_session(
|
|
353
|
-
session_content: str,
|
|
354
|
-
redact_mode: str = "auto",
|
|
355
|
-
quiet: bool = False
|
|
399
|
+
session_content: str, redact_mode: str = "auto", quiet: bool = False
|
|
356
400
|
) -> Tuple[str, bool, List[SecretMatch]]:
|
|
357
401
|
"""
|
|
358
402
|
Check session content for secrets and optionally redact them.
|
|
@@ -383,12 +427,9 @@ def check_and_redact_session(
|
|
|
383
427
|
|
|
384
428
|
# Print warning about detected secrets
|
|
385
429
|
logger.warning(f"Found {len(secrets)} potential secret(s) in session")
|
|
386
|
-
|
|
430
|
+
|
|
387
431
|
if not quiet:
|
|
388
|
-
print(
|
|
389
|
-
f"⚠️ Detected {len(secrets)} potential secret(s) in session:",
|
|
390
|
-
file=sys.stderr
|
|
391
|
-
)
|
|
432
|
+
print(f"⚠️ Detected {len(secrets)} potential secret(s) in session:", file=sys.stderr)
|
|
392
433
|
for secret in secrets:
|
|
393
434
|
print(f" - {secret.type} at line {secret.line}", file=sys.stderr)
|
|
394
435
|
|
|
@@ -400,17 +441,14 @@ def check_and_redact_session(
|
|
|
400
441
|
# Auto mode: redact the secrets
|
|
401
442
|
logger.info("Auto mode: redacting secrets")
|
|
402
443
|
redacted_content = redact_content(session_content, secrets)
|
|
403
|
-
|
|
444
|
+
|
|
404
445
|
if not quiet:
|
|
405
446
|
print(" ✅ Secrets have been automatically redacted", file=sys.stderr)
|
|
406
447
|
|
|
407
448
|
return redacted_content, True, secrets
|
|
408
449
|
|
|
409
450
|
|
|
410
|
-
def save_original_session(
|
|
411
|
-
session_path: Path,
|
|
412
|
-
repo_root: Path
|
|
413
|
-
) -> Optional[Path]:
|
|
451
|
+
def save_original_session(session_path: Path, repo_root: Path) -> Optional[Path]:
|
|
414
452
|
"""
|
|
415
453
|
Save a copy of the original session before redaction.
|
|
416
454
|
|
|
@@ -425,6 +463,7 @@ def save_original_session(
|
|
|
425
463
|
|
|
426
464
|
try:
|
|
427
465
|
from realign import get_realign_dir
|
|
466
|
+
|
|
428
467
|
realign_dir = get_realign_dir(repo_root)
|
|
429
468
|
backup_dir = realign_dir / "sessions-original"
|
|
430
469
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -433,6 +472,7 @@ def save_original_session(
|
|
|
433
472
|
|
|
434
473
|
# Copy original file to backup
|
|
435
474
|
import shutil
|
|
475
|
+
|
|
436
476
|
shutil.copy2(session_path, backup_path)
|
|
437
477
|
|
|
438
478
|
logger.info(f"Backup saved to: {backup_path}")
|
realign/triggers/__init__.py
CHANGED
|
@@ -13,13 +13,13 @@ from .next_turn_trigger import NextTurnTrigger
|
|
|
13
13
|
from .registry import TriggerRegistry, get_global_registry
|
|
14
14
|
|
|
15
15
|
__all__ = [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
"TurnTrigger",
|
|
17
|
+
"TurnInfo",
|
|
18
|
+
"NextTurnTrigger",
|
|
19
|
+
"ClaudeTrigger",
|
|
20
|
+
"CodexTrigger",
|
|
21
|
+
"GeminiTrigger",
|
|
22
|
+
"AntigravityTrigger",
|
|
23
|
+
"TriggerRegistry",
|
|
24
|
+
"get_global_registry",
|
|
25
25
|
]
|
|
@@ -30,13 +30,13 @@ class AntigravityTrigger(TurnTrigger):
|
|
|
30
30
|
if session_file.parent.name == "brain" and "antigravity" in str(session_file):
|
|
31
31
|
return "antigravity_markdown"
|
|
32
32
|
return None
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
# Legacy/Fallback: Check if file
|
|
35
|
-
if session_file.suffix ==
|
|
35
|
+
if session_file.suffix == ".md":
|
|
36
36
|
path_str = str(session_file)
|
|
37
|
-
if
|
|
37
|
+
if "gemini" in path_str and "brain" in path_str:
|
|
38
38
|
return "antigravity_markdown"
|
|
39
|
-
if
|
|
39
|
+
if ".antigravity" in path_str or "antigravity" in path_str.lower():
|
|
40
40
|
return "antigravity_markdown"
|
|
41
41
|
return None
|
|
42
42
|
except Exception:
|
|
@@ -46,29 +46,29 @@ class AntigravityTrigger(TurnTrigger):
|
|
|
46
46
|
"""
|
|
47
47
|
Antigravity sessions are effectively "single-turn" persistent states.
|
|
48
48
|
We return 1 if the artifacts exist, 0 otherwise.
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
The watcher will handle change detection via content hashing or mtime,
|
|
51
51
|
even if this count stays at 1.
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
Args:
|
|
54
54
|
session_file: Path to brain directory (or file)
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
Returns:
|
|
57
57
|
int: 1 if artifacts exist, 0 otherwise.
|
|
58
58
|
"""
|
|
59
59
|
if not session_file.exists():
|
|
60
60
|
return 0
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
session_dir = session_file if session_file.is_dir() else session_file.parent
|
|
63
63
|
artifacts = ["task.md", "walkthrough.md", "implementation_plan.md"]
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
has_artifacts = False
|
|
66
66
|
for filename in artifacts:
|
|
67
67
|
path = session_dir / filename
|
|
68
68
|
if path.exists():
|
|
69
69
|
has_artifacts = True
|
|
70
70
|
break
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
return 1 if has_artifacts else 0
|
|
73
73
|
|
|
74
74
|
def extract_turn_info(self, session_file: Path, turn_number: int) -> Optional[TurnInfo]:
|
|
@@ -81,14 +81,14 @@ class AntigravityTrigger(TurnTrigger):
|
|
|
81
81
|
|
|
82
82
|
content_parts = []
|
|
83
83
|
artifacts = ["task.md", "walkthrough.md", "implementation_plan.md"]
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
# Aggregate content
|
|
86
86
|
for filename in artifacts:
|
|
87
87
|
path = session_dir / filename
|
|
88
88
|
if path.exists():
|
|
89
|
-
text = path.read_text(encoding=
|
|
89
|
+
text = path.read_text(encoding="utf-8")
|
|
90
90
|
content_parts.append(f"--- {filename} ---\n{text}")
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
full_content = "\n\n".join(content_parts)
|
|
93
93
|
if not full_content:
|
|
94
94
|
return None
|
|
@@ -101,7 +101,7 @@ class AntigravityTrigger(TurnTrigger):
|
|
|
101
101
|
user_message="", # Empty - full content used elsewhere for summary generation
|
|
102
102
|
start_line=1,
|
|
103
103
|
end_line=len(full_content.splitlines()) if full_content else 0,
|
|
104
|
-
timestamp=timestamp
|
|
104
|
+
timestamp=timestamp,
|
|
105
105
|
)
|
|
106
106
|
|
|
107
107
|
def is_turn_complete(self, session_file: Path, turn_number: int) -> bool:
|
|
@@ -114,25 +114,27 @@ class AntigravityTrigger(TurnTrigger):
|
|
|
114
114
|
Since we treat the state as a single accumulated turn, we return one turn group.
|
|
115
115
|
"""
|
|
116
116
|
current_turn = self.count_complete_turns(session_file)
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
groups = []
|
|
119
119
|
# Return a single entry representing the current state
|
|
120
120
|
if current_turn > 0:
|
|
121
121
|
info = self.extract_turn_info(session_file, 1)
|
|
122
122
|
if info:
|
|
123
|
-
groups.append(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
+
)
|
|
132
134
|
|
|
133
135
|
return {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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",
|
|
138
140
|
}
|
realign/triggers/base.py
CHANGED
|
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
|
|
|
16
16
|
@dataclass
|
|
17
17
|
class TurnInfo:
|
|
18
18
|
"""单个对话轮次的信息"""
|
|
19
|
+
|
|
19
20
|
turn_number: int # 轮次编号(1-based)
|
|
20
21
|
user_message: str # 用户消息内容
|
|
21
22
|
start_line: int # 在session文件中的起始行
|
|
@@ -126,7 +127,7 @@ class TurnTrigger(ABC):
|
|
|
126
127
|
import json
|
|
127
128
|
|
|
128
129
|
try:
|
|
129
|
-
with open(session_file,
|
|
130
|
+
with open(session_file, "r", encoding="utf-8") as f:
|
|
130
131
|
first_line = f.readline().strip()
|
|
131
132
|
if not first_line:
|
|
132
133
|
return None
|
|
@@ -134,11 +135,11 @@ class TurnTrigger(ABC):
|
|
|
134
135
|
data = json.loads(first_line)
|
|
135
136
|
|
|
136
137
|
# 检测Claude Code格式
|
|
137
|
-
if
|
|
138
|
+
if "version" in data and data["version"].startswith("2.0"):
|
|
138
139
|
return f"claude_code_{data['version']}"
|
|
139
140
|
|
|
140
141
|
# 检测Codex格式
|
|
141
|
-
if data.get(
|
|
142
|
+
if data.get("type") == "event_msg" and "payload" in data:
|
|
142
143
|
return "codex"
|
|
143
144
|
|
|
144
145
|
return None
|