aline-ai 0.2.6__py3-none-any.whl → 0.3.0__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.2.6.dist-info → aline_ai-0.3.0.dist-info}/METADATA +3 -1
- aline_ai-0.3.0.dist-info/RECORD +41 -0
- aline_ai-0.3.0.dist-info/entry_points.txt +3 -0
- realign/__init__.py +32 -1
- realign/cli.py +203 -19
- realign/commands/__init__.py +2 -2
- realign/commands/clean.py +149 -0
- realign/commands/config.py +1 -1
- realign/commands/export_shares.py +1785 -0
- realign/commands/hide.py +112 -24
- realign/commands/import_history.py +873 -0
- realign/commands/init.py +104 -217
- realign/commands/mirror.py +131 -0
- realign/commands/pull.py +101 -0
- realign/commands/push.py +155 -245
- realign/commands/review.py +216 -54
- realign/commands/session_utils.py +139 -4
- realign/commands/share.py +965 -0
- realign/commands/status.py +559 -0
- realign/commands/sync.py +91 -0
- realign/commands/undo.py +423 -0
- realign/commands/watcher.py +805 -0
- realign/config.py +21 -10
- realign/file_lock.py +3 -1
- realign/hash_registry.py +310 -0
- realign/hooks.py +115 -411
- realign/logging_config.py +2 -2
- realign/mcp_server.py +263 -549
- realign/mcp_watcher.py +997 -139
- realign/mirror_utils.py +322 -0
- realign/prompts/__init__.py +21 -0
- realign/prompts/presets.py +238 -0
- realign/redactor.py +168 -16
- realign/tracker/__init__.py +9 -0
- realign/tracker/git_tracker.py +1123 -0
- realign/watcher_daemon.py +115 -0
- aline_ai-0.2.6.dist-info/RECORD +0 -28
- aline_ai-0.2.6.dist-info/entry_points.txt +0 -5
- realign/commands/auto_commit.py +0 -242
- realign/commands/commit.py +0 -379
- realign/commands/search.py +0 -449
- realign/commands/show.py +0 -416
- {aline_ai-0.2.6.dist-info → aline_ai-0.3.0.dist-info}/WHEEL +0 -0
- {aline_ai-0.2.6.dist-info → aline_ai-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.2.6.dist-info → aline_ai-0.3.0.dist-info}/top_level.txt +0 -0
realign/commands/show.py
DELETED
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
"""ReAlign show command - Display agent sessions from commits or files."""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
import json
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
7
|
-
import typer
|
|
8
|
-
from rich.console import Console
|
|
9
|
-
from rich.syntax import Syntax
|
|
10
|
-
from rich.panel import Panel
|
|
11
|
-
from rich.markdown import Markdown
|
|
12
|
-
|
|
13
|
-
console = Console()
|
|
14
|
-
from .session_utils import find_session_paths_for_commit
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def calculate_line_range(
|
|
18
|
-
from_line: Optional[int],
|
|
19
|
-
to_line: Optional[int],
|
|
20
|
-
around_line: Optional[int],
|
|
21
|
-
context: int,
|
|
22
|
-
first: Optional[int],
|
|
23
|
-
last: Optional[int],
|
|
24
|
-
total_lines: int,
|
|
25
|
-
) -> Optional[tuple[int, int]]:
|
|
26
|
-
"""
|
|
27
|
-
Calculate the line range to display based on various parameters.
|
|
28
|
-
|
|
29
|
-
Returns (start_line, end_line) tuple (1-indexed, inclusive) or None for all lines.
|
|
30
|
-
"""
|
|
31
|
-
# Priority: around > from/to > first/last
|
|
32
|
-
|
|
33
|
-
if around_line is not None:
|
|
34
|
-
# Show lines around a specific line
|
|
35
|
-
start = max(1, around_line - context)
|
|
36
|
-
end = min(total_lines, around_line + context)
|
|
37
|
-
return (start, end)
|
|
38
|
-
|
|
39
|
-
if from_line is not None or to_line is not None:
|
|
40
|
-
# Show range from start to end
|
|
41
|
-
start = from_line if from_line is not None else 1
|
|
42
|
-
end = to_line if to_line is not None else total_lines
|
|
43
|
-
return (start, end)
|
|
44
|
-
|
|
45
|
-
if first is not None:
|
|
46
|
-
# Show first N lines
|
|
47
|
-
return (1, min(first, total_lines))
|
|
48
|
-
|
|
49
|
-
if last is not None:
|
|
50
|
-
# Show last N lines
|
|
51
|
-
return (max(1, total_lines - last + 1), total_lines)
|
|
52
|
-
|
|
53
|
-
# No range specified, show all
|
|
54
|
-
return None
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def show_command(
|
|
58
|
-
commit: Optional[str] = typer.Argument(None, help="Commit hash to show session from"),
|
|
59
|
-
session: Optional[str] = typer.Option(None, "--session", "-s", help="Direct path to session file"),
|
|
60
|
-
format_output: str = typer.Option("pretty", "--format", "-f", help="Output format: pretty, json, raw"),
|
|
61
|
-
pager: bool = typer.Option(False, "--pager", "-p", help="Use pager (less) for output"),
|
|
62
|
-
from_line: Optional[int] = typer.Option(None, "--from", help="Start from line number (inclusive)"),
|
|
63
|
-
to_line: Optional[int] = typer.Option(None, "--to", help="End at line number (inclusive)"),
|
|
64
|
-
around_line: Optional[int] = typer.Option(None, "--around", help="Show lines around this line number"),
|
|
65
|
-
context: int = typer.Option(5, "--context", "-C", help="Number of lines before/after when using --around (default: 5)"),
|
|
66
|
-
first: Optional[int] = typer.Option(None, "--first", help="Show only first N lines"),
|
|
67
|
-
last: Optional[int] = typer.Option(None, "--last", help="Show only last N lines"),
|
|
68
|
-
):
|
|
69
|
-
"""Display agent sessions from commits or files."""
|
|
70
|
-
if not commit and not session:
|
|
71
|
-
console.print("[red]Error: Must specify either a commit hash or --session path[/red]")
|
|
72
|
-
raise typer.Exit(1)
|
|
73
|
-
|
|
74
|
-
# Check if we're in a git repository
|
|
75
|
-
try:
|
|
76
|
-
subprocess.run(
|
|
77
|
-
["git", "rev-parse", "--git-dir"],
|
|
78
|
-
check=True,
|
|
79
|
-
capture_output=True,
|
|
80
|
-
text=True,
|
|
81
|
-
)
|
|
82
|
-
except subprocess.CalledProcessError:
|
|
83
|
-
console.print("[red]Error: Not in a git repository.[/red]")
|
|
84
|
-
raise typer.Exit(1)
|
|
85
|
-
|
|
86
|
-
repo_root = Path(
|
|
87
|
-
subprocess.run(
|
|
88
|
-
["git", "rev-parse", "--show-toplevel"],
|
|
89
|
-
check=True,
|
|
90
|
-
capture_output=True,
|
|
91
|
-
text=True,
|
|
92
|
-
).stdout.strip()
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
session_path = None
|
|
96
|
-
session_content = None
|
|
97
|
-
|
|
98
|
-
# Get session from commit
|
|
99
|
-
if commit:
|
|
100
|
-
console.print(f"[blue]Fetching session for commit:[/blue] {commit}")
|
|
101
|
-
|
|
102
|
-
session_paths = find_session_paths_for_commit(repo_root, commit)
|
|
103
|
-
if not session_paths:
|
|
104
|
-
console.print("[yellow]No agent session files tracked in this commit.[/yellow]")
|
|
105
|
-
raise typer.Exit(0)
|
|
106
|
-
|
|
107
|
-
session_path = session_paths[0]
|
|
108
|
-
if len(session_paths) > 1:
|
|
109
|
-
other_paths = ", ".join(session_paths[1:])
|
|
110
|
-
console.print(f"[yellow]Multiple session files found; showing first. Others: {other_paths}[/yellow]")
|
|
111
|
-
console.print(f"[green]Found session:[/green] {session_path}")
|
|
112
|
-
|
|
113
|
-
# Try to read from working tree first
|
|
114
|
-
full_session_path = repo_root / session_path
|
|
115
|
-
if full_session_path.exists():
|
|
116
|
-
with open(full_session_path, "r", encoding="utf-8") as f:
|
|
117
|
-
session_content = f.read()
|
|
118
|
-
else:
|
|
119
|
-
# Try to get from git
|
|
120
|
-
result = subprocess.run(
|
|
121
|
-
["git", "show", f"{commit}:{session_path}"],
|
|
122
|
-
capture_output=True,
|
|
123
|
-
text=True,
|
|
124
|
-
check=False,
|
|
125
|
-
cwd=repo_root,
|
|
126
|
-
)
|
|
127
|
-
if result.returncode == 0:
|
|
128
|
-
session_content = result.stdout
|
|
129
|
-
else:
|
|
130
|
-
console.print(f"[red]Could not find session file:[/red] {session_path}")
|
|
131
|
-
raise typer.Exit(1)
|
|
132
|
-
|
|
133
|
-
# Get session from direct path
|
|
134
|
-
elif session:
|
|
135
|
-
session_path = session
|
|
136
|
-
full_path = Path(session) if Path(session).is_absolute() else repo_root / session
|
|
137
|
-
|
|
138
|
-
if not full_path.exists():
|
|
139
|
-
console.print(f"[red]Session file not found:[/red] {session}")
|
|
140
|
-
raise typer.Exit(1)
|
|
141
|
-
|
|
142
|
-
console.print(f"[blue]Reading session:[/blue] {session}")
|
|
143
|
-
with open(full_path, "r", encoding="utf-8") as f:
|
|
144
|
-
session_content = f.read()
|
|
145
|
-
|
|
146
|
-
# Calculate line range
|
|
147
|
-
line_range = calculate_line_range(
|
|
148
|
-
from_line=from_line,
|
|
149
|
-
to_line=to_line,
|
|
150
|
-
around_line=around_line,
|
|
151
|
-
context=context,
|
|
152
|
-
first=first,
|
|
153
|
-
last=last,
|
|
154
|
-
total_lines=len(session_content.strip().split("\n")) if session_content else 0,
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
# Display session content
|
|
158
|
-
if session_content:
|
|
159
|
-
display_session(session_content, format_output, pager, session_path, line_range)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def display_session(content: str, format_type: str, use_pager: bool, session_path: Optional[str], line_range: Optional[tuple[int, int]] = None):
|
|
163
|
-
"""Display session content in specified format."""
|
|
164
|
-
# Filter content by line range if specified
|
|
165
|
-
if line_range is not None:
|
|
166
|
-
start_line, end_line = line_range
|
|
167
|
-
all_lines = content.strip().split("\n")
|
|
168
|
-
filtered_lines = all_lines[start_line - 1:end_line] # Convert to 0-indexed
|
|
169
|
-
content = "\n".join(filtered_lines)
|
|
170
|
-
|
|
171
|
-
# Show range info
|
|
172
|
-
total_lines = len(all_lines)
|
|
173
|
-
console.print(f"[dim]Showing lines {start_line}-{end_line} of {total_lines}[/dim]\n")
|
|
174
|
-
|
|
175
|
-
if format_type == "raw":
|
|
176
|
-
if use_pager:
|
|
177
|
-
try:
|
|
178
|
-
subprocess.run(["less", "-R"], input=content, text=True, check=True)
|
|
179
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
180
|
-
console.print(content)
|
|
181
|
-
else:
|
|
182
|
-
console.print(content)
|
|
183
|
-
elif format_type == "json":
|
|
184
|
-
try:
|
|
185
|
-
# Try to parse and pretty-print as JSON/JSONL
|
|
186
|
-
lines = content.strip().split("\n")
|
|
187
|
-
formatted_lines = []
|
|
188
|
-
for line in lines:
|
|
189
|
-
try:
|
|
190
|
-
obj = json.loads(line)
|
|
191
|
-
formatted_lines.append(json.dumps(obj, indent=2, ensure_ascii=False))
|
|
192
|
-
except json.JSONDecodeError:
|
|
193
|
-
formatted_lines.append(line)
|
|
194
|
-
output = "\n".join(formatted_lines)
|
|
195
|
-
if use_pager:
|
|
196
|
-
try:
|
|
197
|
-
subprocess.run(["less", "-R"], input=output, text=True, check=True)
|
|
198
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
199
|
-
console.print(output)
|
|
200
|
-
else:
|
|
201
|
-
console.print(output)
|
|
202
|
-
except Exception:
|
|
203
|
-
console.print(content)
|
|
204
|
-
else: # pretty format
|
|
205
|
-
# For pretty format, render directly (can't use pager with rich objects)
|
|
206
|
-
format_session_pretty_direct(content, session_path, line_range)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def extract_username_from_filename(file_path: str) -> Optional[str]:
|
|
210
|
-
"""
|
|
211
|
-
Extract username from session filename.
|
|
212
|
-
Supports two formats:
|
|
213
|
-
- New: username_agent_shortid.jsonl (e.g., alice_claude_a1b2c3d4.jsonl)
|
|
214
|
-
- Old: timestamp_username_agent_shortid.jsonl (e.g., 1234567890_alice_claude_a1b2c3d4.jsonl)
|
|
215
|
-
"""
|
|
216
|
-
try:
|
|
217
|
-
from pathlib import Path
|
|
218
|
-
filename = Path(file_path).stem # Get filename without extension
|
|
219
|
-
parts = filename.split('_')
|
|
220
|
-
|
|
221
|
-
if len(parts) >= 3:
|
|
222
|
-
if parts[0].isdigit():
|
|
223
|
-
# Old format: timestamp_username_agent_shortid
|
|
224
|
-
return parts[1]
|
|
225
|
-
else:
|
|
226
|
-
# New format: username_agent_shortid
|
|
227
|
-
return parts[0]
|
|
228
|
-
except Exception:
|
|
229
|
-
pass
|
|
230
|
-
# Return None if username cannot be extracted (e.g., UUID format)
|
|
231
|
-
return None
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def extract_agent_from_filename(file_path: str) -> Optional[str]:
|
|
235
|
-
"""
|
|
236
|
-
Extract agent type from session filename.
|
|
237
|
-
Supports two formats:
|
|
238
|
-
- New: username_agent_shortid.jsonl (e.g., alice_claude_a1b2c3d4.jsonl)
|
|
239
|
-
- Old: timestamp_username_agent_shortid.jsonl (e.g., 1234567890_alice_claude_a1b2c3d4.jsonl)
|
|
240
|
-
Returns 'claude', 'codex', or None if not extractable.
|
|
241
|
-
"""
|
|
242
|
-
try:
|
|
243
|
-
from pathlib import Path
|
|
244
|
-
filename = Path(file_path).stem # Get filename without extension
|
|
245
|
-
parts = filename.split('_')
|
|
246
|
-
|
|
247
|
-
if len(parts) >= 3:
|
|
248
|
-
if parts[0].isdigit():
|
|
249
|
-
# Old format: timestamp_username_agent_shortid
|
|
250
|
-
agent = parts[2]
|
|
251
|
-
else:
|
|
252
|
-
# New format: username_agent_shortid
|
|
253
|
-
agent = parts[1]
|
|
254
|
-
|
|
255
|
-
# Normalize agent name
|
|
256
|
-
agent_lower = agent.lower()
|
|
257
|
-
if agent_lower in ('claude', 'codex', 'unknown'):
|
|
258
|
-
return agent_lower
|
|
259
|
-
except Exception:
|
|
260
|
-
pass
|
|
261
|
-
return None
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def extract_text_from_content(content):
|
|
265
|
-
"""Extract text from various content formats."""
|
|
266
|
-
if isinstance(content, str):
|
|
267
|
-
return content
|
|
268
|
-
if isinstance(content, list):
|
|
269
|
-
texts = []
|
|
270
|
-
for item in content:
|
|
271
|
-
if isinstance(item, dict):
|
|
272
|
-
if item.get("type") == "text":
|
|
273
|
-
texts.append(item.get("text", ""))
|
|
274
|
-
elif "text" in item:
|
|
275
|
-
texts.append(item.get("text", ""))
|
|
276
|
-
elif isinstance(item, str):
|
|
277
|
-
texts.append(item)
|
|
278
|
-
return "\n".join(texts) if texts else ""
|
|
279
|
-
if isinstance(content, dict):
|
|
280
|
-
# Handle nested content structure
|
|
281
|
-
if "content" in content:
|
|
282
|
-
return extract_text_from_content(content["content"])
|
|
283
|
-
elif "text" in content:
|
|
284
|
-
return content["text"]
|
|
285
|
-
return str(content)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def format_session_pretty_direct(content: str, session_path: Optional[str], line_range: Optional[tuple[int, int]] = None):
|
|
289
|
-
"""Format and display session content in a pretty, readable way."""
|
|
290
|
-
lines = content.strip().split("\n")
|
|
291
|
-
|
|
292
|
-
# Extract username and agent from filename if available
|
|
293
|
-
username = None
|
|
294
|
-
agent_from_filename = None
|
|
295
|
-
if session_path:
|
|
296
|
-
username = extract_username_from_filename(session_path)
|
|
297
|
-
agent_from_filename = extract_agent_from_filename(session_path)
|
|
298
|
-
console.print(f"\n[bold cyan]Session: {session_path}[/bold cyan]\n")
|
|
299
|
-
|
|
300
|
-
# Calculate starting line number for display
|
|
301
|
-
start_line_num = line_range[0] if line_range else 1
|
|
302
|
-
|
|
303
|
-
for i, line in enumerate(lines, start_line_num):
|
|
304
|
-
try:
|
|
305
|
-
# Try to parse as JSON (for JSONL format)
|
|
306
|
-
obj = json.loads(line)
|
|
307
|
-
|
|
308
|
-
role = None
|
|
309
|
-
content_text = ""
|
|
310
|
-
timestamp = obj.get("timestamp", "")
|
|
311
|
-
model = None
|
|
312
|
-
|
|
313
|
-
# Handle different message formats
|
|
314
|
-
# Format 1: Claude Code format with type and message
|
|
315
|
-
if obj.get("type") in ("user", "assistant"):
|
|
316
|
-
role = obj.get("type")
|
|
317
|
-
message = obj.get("message", {})
|
|
318
|
-
if isinstance(message, dict):
|
|
319
|
-
content_text = extract_text_from_content(message.get("content", ""))
|
|
320
|
-
if role == "assistant":
|
|
321
|
-
model = message.get("model", "")
|
|
322
|
-
# Format 2: Codex format
|
|
323
|
-
elif obj.get("type") == "response_item":
|
|
324
|
-
payload = obj.get("payload", {})
|
|
325
|
-
if payload.get("type") == "message":
|
|
326
|
-
role = payload.get("role")
|
|
327
|
-
content = payload.get("content", [])
|
|
328
|
-
# Extract text from Codex content format
|
|
329
|
-
texts = []
|
|
330
|
-
for item in content if isinstance(content, list) else []:
|
|
331
|
-
if isinstance(item, dict):
|
|
332
|
-
# Codex uses "input_text" and "output_text" types
|
|
333
|
-
if item.get("type") in ("input_text", "output_text"):
|
|
334
|
-
texts.append(item.get("text", ""))
|
|
335
|
-
content_text = "\n".join(texts)
|
|
336
|
-
# Codex doesn't store model info in session files
|
|
337
|
-
model = None
|
|
338
|
-
else:
|
|
339
|
-
# Skip non-message response_items (reasoning, session_meta, etc.)
|
|
340
|
-
continue
|
|
341
|
-
# Format 3: Simple format with role and content
|
|
342
|
-
elif "role" in obj and "content" in obj:
|
|
343
|
-
role = obj.get("role")
|
|
344
|
-
content_text = extract_text_from_content(obj.get("content"))
|
|
345
|
-
if role == "assistant":
|
|
346
|
-
model = obj.get("model", "")
|
|
347
|
-
else:
|
|
348
|
-
# Skip non-message types (session_meta, etc.)
|
|
349
|
-
obj_type = obj.get("type")
|
|
350
|
-
if obj_type in ("session_meta", "reasoning", "session_start", "session_end"):
|
|
351
|
-
continue
|
|
352
|
-
role = obj.get("role", "unknown")
|
|
353
|
-
content_text = extract_text_from_content(obj.get("content", ""))
|
|
354
|
-
|
|
355
|
-
# Skip if no role extracted or no content
|
|
356
|
-
if not role or not content_text or not content_text.strip():
|
|
357
|
-
continue
|
|
358
|
-
|
|
359
|
-
# Build title with username/model info and line number
|
|
360
|
-
if role == "user":
|
|
361
|
-
display_username = username or "unknown"
|
|
362
|
-
title = f"[bold blue]User ({display_username})[/bold blue] [dim]line {i}[/dim] {timestamp}"
|
|
363
|
-
console.print(
|
|
364
|
-
Panel(
|
|
365
|
-
content_text,
|
|
366
|
-
title=title,
|
|
367
|
-
border_style="blue",
|
|
368
|
-
padding=(1, 2),
|
|
369
|
-
)
|
|
370
|
-
)
|
|
371
|
-
elif role == "assistant":
|
|
372
|
-
# Try to get model from content first, fallback to agent from filename
|
|
373
|
-
if model:
|
|
374
|
-
# Check if it's just an agent name or full model name
|
|
375
|
-
if model.lower() in ('codex', 'claude', 'unknown'):
|
|
376
|
-
display_model = model
|
|
377
|
-
else:
|
|
378
|
-
# Full model name - extract short version
|
|
379
|
-
display_model = model.split('-2024')[0].split('-2025')[0]
|
|
380
|
-
elif agent_from_filename:
|
|
381
|
-
# Use agent type from filename
|
|
382
|
-
display_model = agent_from_filename
|
|
383
|
-
else:
|
|
384
|
-
display_model = "unknown"
|
|
385
|
-
title = f"[bold green]Assistant ({display_model})[/bold green] [dim]line {i}[/dim] {timestamp}"
|
|
386
|
-
console.print(
|
|
387
|
-
Panel(
|
|
388
|
-
content_text,
|
|
389
|
-
title=title,
|
|
390
|
-
border_style="green",
|
|
391
|
-
padding=(1, 2),
|
|
392
|
-
)
|
|
393
|
-
)
|
|
394
|
-
else:
|
|
395
|
-
console.print(
|
|
396
|
-
Panel(
|
|
397
|
-
content_text,
|
|
398
|
-
title=f"[bold yellow]{role.title()}[/bold yellow] [dim]line {i}[/dim] {timestamp}",
|
|
399
|
-
border_style="yellow",
|
|
400
|
-
padding=(1, 2),
|
|
401
|
-
)
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
except json.JSONDecodeError:
|
|
405
|
-
# Not JSON, display as plain text
|
|
406
|
-
console.print(f"[dim]{i:4d}:[/dim] {line}")
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
def format_session_pretty(content: str, session_path: Optional[str]) -> str:
|
|
410
|
-
"""Format session content in a pretty, readable way (deprecated - use format_session_pretty_direct)."""
|
|
411
|
-
# This is kept for compatibility but not used
|
|
412
|
-
return content
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if __name__ == "__main__":
|
|
416
|
-
typer.run(show_command)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|