aline-ai 0.5.3__py3-none-any.whl → 0.5.5__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.
Files changed (80) hide show
  1. {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/METADATA +1 -1
  2. aline_ai-0.5.5.dist-info/RECORD +93 -0
  3. realign/__init__.py +1 -1
  4. realign/adapters/antigravity.py +28 -20
  5. realign/adapters/base.py +46 -50
  6. realign/adapters/claude.py +14 -14
  7. realign/adapters/codex.py +7 -7
  8. realign/adapters/gemini.py +11 -11
  9. realign/adapters/registry.py +14 -10
  10. realign/claude_detector.py +2 -2
  11. realign/claude_hooks/__init__.py +3 -3
  12. realign/claude_hooks/permission_request_hook.py +35 -0
  13. realign/claude_hooks/permission_request_hook_installer.py +31 -32
  14. realign/claude_hooks/stop_hook.py +4 -1
  15. realign/claude_hooks/stop_hook_installer.py +30 -31
  16. realign/cli.py +24 -0
  17. realign/codex_detector.py +11 -11
  18. realign/commands/add.py +361 -35
  19. realign/commands/config.py +3 -12
  20. realign/commands/context.py +3 -1
  21. realign/commands/export_shares.py +86 -127
  22. realign/commands/import_shares.py +145 -155
  23. realign/commands/init.py +166 -30
  24. realign/commands/restore.py +18 -6
  25. realign/commands/search.py +14 -42
  26. realign/commands/upgrade.py +155 -11
  27. realign/commands/watcher.py +98 -219
  28. realign/commands/worker.py +29 -6
  29. realign/config.py +25 -20
  30. realign/context.py +1 -3
  31. realign/dashboard/app.py +4 -4
  32. realign/dashboard/screens/create_event.py +3 -1
  33. realign/dashboard/screens/event_detail.py +14 -6
  34. realign/dashboard/screens/session_detail.py +3 -1
  35. realign/dashboard/screens/share_import.py +7 -3
  36. realign/dashboard/tmux_manager.py +91 -22
  37. realign/dashboard/widgets/config_panel.py +85 -1
  38. realign/dashboard/widgets/events_table.py +3 -1
  39. realign/dashboard/widgets/header.py +1 -0
  40. realign/dashboard/widgets/search_panel.py +37 -27
  41. realign/dashboard/widgets/sessions_table.py +24 -15
  42. realign/dashboard/widgets/terminal_panel.py +207 -17
  43. realign/dashboard/widgets/watcher_panel.py +6 -2
  44. realign/dashboard/widgets/worker_panel.py +10 -1
  45. realign/db/__init__.py +1 -1
  46. realign/db/base.py +5 -15
  47. realign/db/locks.py +0 -1
  48. realign/db/migration.py +82 -76
  49. realign/db/schema.py +2 -6
  50. realign/db/sqlite_db.py +23 -41
  51. realign/events/__init__.py +0 -1
  52. realign/events/event_summarizer.py +27 -15
  53. realign/events/session_summarizer.py +29 -15
  54. realign/file_lock.py +1 -0
  55. realign/hooks.py +150 -60
  56. realign/logging_config.py +12 -15
  57. realign/mcp_server.py +30 -51
  58. realign/mcp_watcher.py +0 -1
  59. realign/models/event.py +29 -20
  60. realign/prompts/__init__.py +7 -7
  61. realign/prompts/presets.py +15 -11
  62. realign/redactor.py +99 -59
  63. realign/triggers/__init__.py +9 -9
  64. realign/triggers/antigravity_trigger.py +30 -28
  65. realign/triggers/base.py +4 -3
  66. realign/triggers/claude_trigger.py +104 -85
  67. realign/triggers/codex_trigger.py +15 -5
  68. realign/triggers/gemini_trigger.py +57 -47
  69. realign/triggers/next_turn_trigger.py +3 -1
  70. realign/triggers/registry.py +6 -2
  71. realign/triggers/turn_status.py +3 -1
  72. realign/watcher_core.py +306 -131
  73. realign/watcher_daemon.py +8 -8
  74. realign/worker_core.py +3 -1
  75. realign/worker_daemon.py +3 -1
  76. aline_ai-0.5.3.dist-info/RECORD +0 -93
  77. {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/WHEEL +0 -0
  78. {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/entry_points.txt +0 -0
  79. {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.dist-info}/licenses/LICENSE +0 -0
  80. {aline_ai-0.5.3.dist-info → aline_ai-0.5.5.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('realign.redactor', 'redactor.log')
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('\n')
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'\bsk-[a-zA-Z0-9]{20,}', 'OpenAI API Key'),
52
+ (r"\bsk-[a-zA-Z0-9]{20,}", "OpenAI API Key"),
53
53
  # Anthropic API keys (sk-ant-api03-...)
54
- (r'\bsk-ant-[a-zA-Z0-9\-]{50,}', 'Anthropic API Key'),
54
+ (r"\bsk-ant-[a-zA-Z0-9\-]{50,}", "Anthropic API Key"),
55
55
  # Generic API keys with common prefixes
56
- (r'\b(?:api[_-]?key|apikey|api[_-]?secret)[\s:=]+["\']?([a-zA-Z0-9_\-]{32,})["\']?', 'Generic API Key'),
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'\bBearer\s+[a-zA-Z0-9\-._~+/]+=*', 'Bearer Token'),
61
+ (r"\bBearer\s+[a-zA-Z0-9\-._~+/]+=*", "Bearer Token"),
59
62
  # GitHub tokens
60
- (r'\bgh[ps]_[a-zA-Z0-9]{36,}', 'GitHub Token'),
63
+ (r"\bgh[ps]_[a-zA-Z0-9]{36,}", "GitHub Token"),
61
64
  # Slack tokens
62
- (r'\bxox[baprs]-[a-zA-Z0-9\-]{10,}', 'Slack Token'),
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'\b[a-zA-Z0-9]{60,}\b', 'Potential Secret (Long String)'),
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(r'^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$', matched_text, re.IGNORECASE):
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 ['example', 'placeholder', 'your_api_key_here', 'your-api-key']:
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 == 'Potential Secret (Long String)':
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='w',
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 'High Entropy' in secret.type or 'HighEntropy' in secret.type:
157
- logger.debug(f"Filtering out high-entropy detection: {secret.type} at line {secret.line_number}")
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 'temp_path' in locals():
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
- 'type', 'role', 'stop_reason', 'stop_sequence',
220
+ "type",
221
+ "role",
222
+ "stop_reason",
223
+ "stop_sequence",
210
224
  # Model metadata
211
- 'model', 'id', 'service_tier',
225
+ "model",
226
+ "id",
227
+ "service_tier",
212
228
  # Session metadata
213
- 'isSidechain', 'userType', 'version', 'gitBranch', 'cwd', 'slug',
229
+ "isSidechain",
230
+ "userType",
231
+ "version",
232
+ "gitBranch",
233
+ "cwd",
234
+ "slug",
214
235
  # Identifiers (UUIDs, timestamps - not actual secrets)
215
- 'parentUuid', 'uuid', 'sessionId', 'requestId', 'timestamp',
236
+ "parentUuid",
237
+ "uuid",
238
+ "sessionId",
239
+ "requestId",
240
+ "timestamp",
216
241
  # Token usage (not sensitive)
217
- 'usage', 'input_tokens', 'output_tokens',
218
- 'cache_read_input_tokens', 'cache_creation_input_tokens',
219
- 'cache_creation', 'ephemeral_5m_input_tokens', 'ephemeral_1h_input_tokens',
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
- 'tool_use_id', 'name', 'is_error', 'interrupted', 'isImage',
251
+ "tool_use_id",
252
+ "name",
253
+ "is_error",
254
+ "interrupted",
255
+ "isImage",
222
256
  # File/process info
223
- 'filenames', 'durationMs', 'numFiles', 'truncated',
224
- 'stdout', 'stderr', 'returnCodeInterpretation',
257
+ "filenames",
258
+ "durationMs",
259
+ "numFiles",
260
+ "truncated",
261
+ "stdout",
262
+ "stderr",
263
+ "returnCodeInterpretation",
225
264
  # Other metadata
226
- 'todos', 'oldTodos', 'newTodos', 'toolUseResult',
227
- 'context_management', 'applied_edits', 'operation',
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
- 'content', # Main content field
234
- 'text', # Text content in messages
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('\n')
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 ':' in original_line:
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'(.*?:\s*)(.+?)(\s*[}\]]*\s*)$', original_line)
379
+ match = re.search(r"(.*?:\s*)(.+?)(\s*[}\]]*\s*)$", original_line)
338
380
  if match:
339
- lines[line_num] = f'{match.group(1)}"[REDACTED: {", ".join(set(secret_types))}]"{match.group(3)}'
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] = f'{original_line}: "[REDACTED: {", ".join(set(secret_types))}]"'
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 = '\n'.join(lines)
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}")
@@ -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
- 'TurnTrigger',
17
- 'TurnInfo',
18
- 'NextTurnTrigger',
19
- 'ClaudeTrigger',
20
- 'CodexTrigger',
21
- 'GeminiTrigger',
22
- 'AntigravityTrigger',
23
- 'TriggerRegistry',
24
- 'get_global_registry',
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 == '.md':
35
+ if session_file.suffix == ".md":
36
36
  path_str = str(session_file)
37
- if 'gemini' in path_str and 'brain' in path_str:
37
+ if "gemini" in path_str and "brain" in path_str:
38
38
  return "antigravity_markdown"
39
- if '.antigravity' in path_str or 'antigravity' in path_str.lower():
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='utf-8')
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
- 'turn_number': 1,
125
- 'user_message': info.user_message,
126
- 'summary_message': 'Antigravity Session State',
127
- 'turn_status': 'completed',
128
- 'start_line': info.start_line,
129
- 'end_line': info.end_line,
130
- 'timestamp': info.timestamp,
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
- 'groups': groups,
135
- 'total_turns': 1 if current_turn > 0 else 0, # Conceptually one continuous session
136
- 'latest_turn_id': 1 if current_turn > 0 else 0,
137
- 'format': 'antigravity_markdown',
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, 'r', encoding='utf-8') as f:
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 'version' in data and data['version'].startswith('2.0'):
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('type') == 'event_msg' and 'payload' in data:
142
+ if data.get("type") == "event_msg" and "payload" in data:
142
143
  return "codex"
143
144
 
144
145
  return None