gator-command 1.0.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.
Files changed (110) hide show
  1. gator_command/__init__.py +2 -0
  2. gator_command/cli.py +137 -0
  3. gator_command/scripts/crawler.py +633 -0
  4. gator_command/scripts/dashboard/dashboard.css +982 -0
  5. gator_command/scripts/dashboard/dashboard.html +84 -0
  6. gator_command/scripts/dashboard/dashboard.js +419 -0
  7. gator_command/scripts/dashboard/views/audit.js +270 -0
  8. gator_command/scripts/dashboard/views/fleet.js +307 -0
  9. gator_command/scripts/dashboard/views/repo.js +599 -0
  10. gator_command/scripts/dashboard/views/settings.js +173 -0
  11. gator_command/scripts/dashboard/views/updates.js +308 -0
  12. gator_command/scripts/enforcer-prompt.md +22 -0
  13. gator_command/scripts/extract-claude-sessions.py +489 -0
  14. gator_command/scripts/extract-codex-sessions.py +477 -0
  15. gator_command/scripts/extract-gemini-sessions.py +410 -0
  16. gator_command/scripts/gator-audit.py +956 -0
  17. gator_command/scripts/gator-charter-draft.py +919 -0
  18. gator_command/scripts/gator-charter-lint.py +427 -0
  19. gator_command/scripts/gator-charter-verify.py +606 -0
  20. gator_command/scripts/gator-dashboard.py +1271 -0
  21. gator_command/scripts/gator-deploy.py +916 -0
  22. gator_command/scripts/gator-drift.py +569 -0
  23. gator_command/scripts/gator-enforce.py +82 -0
  24. gator_command/scripts/gator-fleet-intel.py +460 -0
  25. gator_command/scripts/gator-fleet-report.py +615 -0
  26. gator_command/scripts/gator-init-command-post.py +315 -0
  27. gator_command/scripts/gator-init.py +434 -0
  28. gator_command/scripts/gator-machine-id.py +153 -0
  29. gator_command/scripts/gator-policy-status.py +631 -0
  30. gator_command/scripts/gator-pulse.py +459 -0
  31. gator_command/scripts/gator-repo-status.py +649 -0
  32. gator_command/scripts/gator-session-common.py +372 -0
  33. gator_command/scripts/gator-session-sink.py +831 -0
  34. gator_command/scripts/gator-sessions.py +1244 -0
  35. gator_command/scripts/gator-update.py +615 -0
  36. gator_command/scripts/gator-version.py +38 -0
  37. gator_command/scripts/gator_core.py +489 -0
  38. gator_command/scripts/gator_remote.py +381 -0
  39. gator_command/scripts/gator_runtime.py +142 -0
  40. gator_command/scripts/gatorize-actions.sh +989 -0
  41. gator_command/scripts/gatorize-lib.sh +166 -0
  42. gator_command/scripts/gatorize-post.sh +394 -0
  43. gator_command/scripts/gatorize.py +1163 -0
  44. gator_command/scripts/gatorize.sh +185 -0
  45. gator_command/scripts/generate_markdown.py +212 -0
  46. gator_command/scripts/generate_wiki.py +424 -0
  47. gator_command/scripts/graph_health.py +780 -0
  48. gator_command/scripts/memex-lint.py +286 -0
  49. gator_command/scripts/memex-lint.sh +205 -0
  50. gator_command/scripts/memex.py +1472 -0
  51. gator_command/scripts/memex_formatters.py +191 -0
  52. gator_command/scripts/memex_state.py +236 -0
  53. gator_command/scripts/spawn.py +650 -0
  54. gator_command/templates/gator-starter/blueprints/README.md +32 -0
  55. gator_command/templates/gator-starter/charterignore +53 -0
  56. gator_command/templates/gator-starter/charters/README.md +178 -0
  57. gator_command/templates/gator-starter/charters/_template.md +31 -0
  58. gator_command/templates/gator-starter/commands/commit.md +33 -0
  59. gator_command/templates/gator-starter/commands/init.md +11 -0
  60. gator_command/templates/gator-starter/commands/update.md +5 -0
  61. gator_command/templates/gator-starter/constitution.md +165 -0
  62. gator_command/templates/gator-starter/field-guides/README.md +25 -0
  63. gator_command/templates/gator-starter/gator-start-up.md +119 -0
  64. gator_command/templates/gator-starter/procedures/charter-alignment.md +83 -0
  65. gator_command/templates/gator-starter/procedures/enforcer-review.md +317 -0
  66. gator_command/templates/gator-starter/procedures/field-guide-generation.md +176 -0
  67. gator_command/templates/gator-starter/procedures/knowledge-capture.md +57 -0
  68. gator_command/templates/gator-starter/procedures/significance-check.md +69 -0
  69. gator_command/templates/gator-starter/reference-notes/concierge-responses.md +535 -0
  70. gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +91 -0
  71. gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +22 -0
  72. gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +232 -0
  73. gator_command/templates/gator-starter/reference-notes/example-project.md +289 -0
  74. gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +72 -0
  75. gator_command/templates/gator-starter/reference-notes/git-workflow.md +60 -0
  76. gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +37 -0
  77. gator_command/templates/gator-starter/reference-notes/refactor-approach.md +155 -0
  78. gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +108 -0
  79. gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +99 -0
  80. gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +155 -0
  81. gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
  82. gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
  83. gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
  84. gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
  85. gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
  86. gator_command/templates/gator-starter/scripts/enforcer-prompt.md +55 -0
  87. gator_command/templates/gator-starter/scripts/enforcer-review.py +1551 -0
  88. gator_command/templates/gator-starter/scripts/gator-approve.py +139 -0
  89. gator_command/templates/gator-starter/scripts/gator-enforce.py +82 -0
  90. gator_command/templates/gator-starter/scripts/gator-init.py +434 -0
  91. gator_command/templates/gator-starter/scripts/gator-pre-commit.py +2670 -0
  92. gator_command/templates/gator-starter/scripts/gator-pulse.py +459 -0
  93. gator_command/templates/gator-starter/scripts/gator-update.py +615 -0
  94. gator_command/templates/gator-starter/scripts/gator-version.py +38 -0
  95. gator_command/templates/gator-starter/scripts/gator_core.py +487 -0
  96. gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
  97. gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
  98. gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
  99. gator_command/templates/gator-starter/scripts/hooks/commit-msg +5 -0
  100. gator_command/templates/gator-starter/scripts/hooks/post-commit +7 -0
  101. gator_command/templates/gator-starter/scripts/hooks/pre-commit +5 -0
  102. gator_command/templates/gator-starter/sessions/.gitignore +7 -0
  103. gator_command/templates/gator-starter/vault/.gitkeep +0 -0
  104. gator_command/templates/gator-starter/whiteboard.md +5 -0
  105. gator_command-1.0.0.dist-info/METADATA +122 -0
  106. gator_command-1.0.0.dist-info/RECORD +110 -0
  107. gator_command-1.0.0.dist-info/WHEEL +5 -0
  108. gator_command-1.0.0.dist-info/entry_points.txt +2 -0
  109. gator_command-1.0.0.dist-info/licenses/LICENSE +21 -0
  110. gator_command-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,489 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ extract-claude-sessions.py — Session archaeology for Claude Code.
4
+
5
+ Reads Claude Code's native session storage (~/.claude/projects/),
6
+ extracts structured turn data, and produces standardized markdown
7
+ session logs. This is the first cross-vendor archaeology script —
8
+ nobody else produces clean audit records from AI coding sessions.
9
+
10
+ Storage layout:
11
+ ~/.claude/projects/<project-slug>/
12
+ <session-uuid>.jsonl — conversation turns (user, assistant, system, progress)
13
+ <session-uuid>/ — file history snapshots (not used here)
14
+ ~/.claude/history.jsonl — prompt history (initial messages)
15
+
16
+ JSONL turn format:
17
+ type: user|assistant|system|progress|file-history-snapshot
18
+ message: {role, content} — content is string (user) or list of blocks (assistant)
19
+ timestamp: ISO string
20
+ cwd: working directory
21
+ gitBranch: current branch
22
+ sessionId: UUID
23
+
24
+ Usage:
25
+ python gator-command/scripts/extract-claude-sessions.py
26
+ python gator-command/scripts/extract-claude-sessions.py --project gator-command
27
+ python gator-command/scripts/extract-claude-sessions.py --session <uuid>
28
+ python gator-command/scripts/extract-claude-sessions.py --json
29
+ python gator-command/scripts/extract-claude-sessions.py --list
30
+
31
+ @reads: ~/.claude/projects/, ~/.claude/history.jsonl
32
+ @writes: stdout (or session log files when --output specified)
33
+ """
34
+
35
+ import argparse
36
+ import io
37
+ import json
38
+ import os
39
+ import re
40
+ import sys
41
+ from datetime import datetime, timezone
42
+ from pathlib import Path
43
+
44
+ # Shared session utilities — use gator_core.import_sibling for clean imports
45
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
46
+ from gator_core import import_sibling
47
+
48
+ _COMMON = None
49
+ def _get_common():
50
+ global _COMMON
51
+ if _COMMON is None:
52
+ _COMMON = import_sibling("gator-session-common")
53
+ return _COMMON
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Constants
58
+ # ---------------------------------------------------------------------------
59
+
60
+ CLAUDE_DIR = Path.home() / ".claude"
61
+ PROJECTS_DIR = CLAUDE_DIR / "projects"
62
+ HISTORY_FILE = CLAUDE_DIR / "history.jsonl"
63
+
64
+ # Turn types to extract (skip file-history-snapshot and progress)
65
+ EXTRACT_TYPES = {"user", "assistant", "system"}
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Discovery
70
+ # ---------------------------------------------------------------------------
71
+
72
+ def discover_projects():
73
+ """Find all projects with session data.
74
+
75
+ Returns list of {name, path, sessions: [{uuid, path, lines, modified}]}
76
+ """
77
+ if not PROJECTS_DIR.is_dir():
78
+ return []
79
+
80
+ projects = []
81
+ for project_dir in sorted(PROJECTS_DIR.iterdir()):
82
+ if not project_dir.is_dir():
83
+ continue
84
+
85
+ # Find JSONL session files
86
+ sessions = []
87
+ for f in sorted(project_dir.iterdir()):
88
+ if f.suffix == ".jsonl" and f.stem != "memory":
89
+ try:
90
+ line_count = sum(1 for _ in open(f, encoding="utf-8", errors="replace"))
91
+ modified = datetime.fromtimestamp(f.stat().st_mtime)
92
+ sessions.append({
93
+ "uuid": f.stem,
94
+ "path": f,
95
+ "lines": line_count,
96
+ "modified": modified.strftime("%Y-%m-%d %H:%M"),
97
+ })
98
+ except OSError:
99
+ continue
100
+
101
+ if sessions:
102
+ # Get repo name from the first session's cwd (most reliable)
103
+ readable = project_dir.name # fallback
104
+ for s in sessions:
105
+ try:
106
+ with open(s["path"], encoding="utf-8", errors="replace") as sf:
107
+ for line in sf:
108
+ data = json.loads(line.strip())
109
+ cwd = data.get("cwd", "")
110
+ if cwd:
111
+ readable = Path(cwd).name
112
+ break
113
+ break
114
+ except Exception:
115
+ continue
116
+
117
+ projects.append({
118
+ "name": readable,
119
+ "slug": project_dir.name,
120
+ "path": project_dir,
121
+ "sessions": sessions,
122
+ })
123
+
124
+ return projects
125
+
126
+
127
+ def find_project(projects, name):
128
+ """Find a project by name (partial match)."""
129
+ for p in projects:
130
+ if name.lower() in p["name"].lower() or name.lower() in p["slug"].lower():
131
+ return p
132
+ return None
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # Extraction
137
+ # ---------------------------------------------------------------------------
138
+
139
+ def extract_session(session_path):
140
+ """Extract structured turns from a Claude Code JSONL session file.
141
+
142
+ Returns list of turn dicts with: type, timestamp, role, content,
143
+ tool_calls, cwd, branch, session_id.
144
+ """
145
+ turns = []
146
+
147
+ with open(session_path, encoding="utf-8", errors="replace") as f:
148
+ for line in f:
149
+ line = line.strip()
150
+ if not line:
151
+ continue
152
+ try:
153
+ data = json.loads(line)
154
+ except json.JSONDecodeError:
155
+ continue
156
+
157
+ turn_type = data.get("type")
158
+ if turn_type not in EXTRACT_TYPES:
159
+ continue
160
+
161
+ msg = data.get("message", {})
162
+ if not isinstance(msg, dict):
163
+ continue
164
+
165
+ role = msg.get("role", turn_type)
166
+ content = msg.get("content", "")
167
+
168
+ # Parse content — string for user, list of blocks for assistant
169
+ text_content = ""
170
+ tool_calls = []
171
+
172
+ if isinstance(content, str):
173
+ text_content = content
174
+ elif isinstance(content, list):
175
+ text_parts = []
176
+ for block in content:
177
+ if not isinstance(block, dict):
178
+ continue
179
+ if block.get("type") == "text":
180
+ text_parts.append(block.get("text", ""))
181
+ elif block.get("type") == "tool_use":
182
+ tool_calls.append({
183
+ "tool": block.get("name", "unknown"),
184
+ "input_keys": list(block.get("input", {}).keys()),
185
+ })
186
+ elif block.get("type") == "tool_result":
187
+ # Tool results contain the output
188
+ result_content = block.get("content", "")
189
+ if isinstance(result_content, str) and result_content:
190
+ text_parts.append(f"[tool result: {result_content[:200]}]")
191
+ elif isinstance(result_content, list):
192
+ for rc in result_content:
193
+ if isinstance(rc, dict) and rc.get("text"):
194
+ text_parts.append(f"[tool result: {rc['text'][:200]}]")
195
+ text_content = "\n".join(text_parts)
196
+
197
+ # Parse timestamp
198
+ ts_raw = data.get("timestamp", "")
199
+ if isinstance(ts_raw, str) and ts_raw:
200
+ timestamp = ts_raw
201
+ elif isinstance(ts_raw, (int, float)):
202
+ timestamp = datetime.fromtimestamp(
203
+ ts_raw / 1000, tz=timezone.utc
204
+ ).strftime("%Y-%m-%dT%H:%M:%SZ")
205
+ else:
206
+ timestamp = ""
207
+
208
+ turns.append({
209
+ "type": turn_type,
210
+ "role": role,
211
+ "timestamp": timestamp,
212
+ "content": text_content,
213
+ "tool_calls": tool_calls,
214
+ "cwd": data.get("cwd", ""),
215
+ "branch": data.get("gitBranch", ""),
216
+ "session_id": data.get("sessionId", ""),
217
+ })
218
+
219
+ return turns
220
+
221
+
222
+ def extract_session_metadata(turns):
223
+ """Extract session-level metadata from turns."""
224
+ if not turns:
225
+ return {}
226
+
227
+ # First and last timestamps
228
+ timestamps = [t["timestamp"] for t in turns if t["timestamp"]]
229
+ start = timestamps[0] if timestamps else ""
230
+ end = timestamps[-1] if timestamps else ""
231
+
232
+ # Working directory and branch
233
+ cwds = set(t["cwd"] for t in turns if t["cwd"])
234
+ branches = set(t["branch"] for t in turns if t["branch"])
235
+
236
+ # Count by role
237
+ user_count = sum(1 for t in turns if t["role"] == "user")
238
+ assistant_count = sum(1 for t in turns if t["role"] == "assistant")
239
+
240
+ # Tools used
241
+ all_tools = set()
242
+ for t in turns:
243
+ for tc in t.get("tool_calls", []):
244
+ all_tools.add(tc["tool"])
245
+
246
+ # Session ID
247
+ session_ids = set(t["session_id"] for t in turns if t["session_id"])
248
+
249
+ # Extract repo name from cwd
250
+ repo = ""
251
+ if cwds:
252
+ cwd = list(cwds)[0]
253
+ repo = Path(cwd).name
254
+
255
+ return {
256
+ "session_id": list(session_ids)[0] if session_ids else "",
257
+ "vendor": "claude",
258
+ "agent": "Claude Code",
259
+ "architect": "AG",
260
+ "start": start,
261
+ "end": end,
262
+ "repo": repo,
263
+ "branch": sorted(branches)[0] if branches else "",
264
+ "cwds": sorted(cwds),
265
+ "branches": sorted(branches),
266
+ "user_turns": user_count,
267
+ "assistant_turns": assistant_count,
268
+ "total_turns": len(turns),
269
+ "tools_used": sorted(all_tools),
270
+ }
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # Output: Markdown
275
+ # ---------------------------------------------------------------------------
276
+
277
+ def format_session_markdown(turns, metadata):
278
+ """Format a session as a standardized markdown log."""
279
+ lines = []
280
+
281
+ # Frontmatter
282
+ lines.append("---")
283
+ lines.append(f"session-id: {metadata.get('session_id', '')}")
284
+ lines.append(f"date: {metadata.get('start', '')[:10]}")
285
+ lines.append(f"start: {metadata.get('start', '')}")
286
+ lines.append(f"end: {metadata.get('end', '')}")
287
+ lines.append(f"repo: {metadata.get('repo', '')}")
288
+ lines.append(f"agent: Claude Code")
289
+ lines.append(f"turns: {metadata.get('total_turns', 0)}")
290
+ lines.append(f"tools: {', '.join(metadata.get('tools_used', []))}")
291
+ lines.append("---")
292
+ lines.append("")
293
+ lines.append(f"# Session Log — {metadata.get('start', '')[:10]}")
294
+ lines.append("")
295
+ lines.append(f"**Repo**: {metadata.get('repo', 'unknown')}")
296
+ lines.append(f"**Branch**: {', '.join(metadata.get('branches', ['unknown']))}")
297
+ lines.append(f"**Turns**: {metadata.get('user_turns', 0)} user, {metadata.get('assistant_turns', 0)} assistant")
298
+ lines.append(f"**Tools**: {', '.join(metadata.get('tools_used', [])) or 'none'}")
299
+ lines.append("")
300
+ lines.append("---")
301
+ lines.append("")
302
+
303
+ # Turns
304
+ for t in turns:
305
+ ts = t["timestamp"][:19] if t["timestamp"] else "?"
306
+ role = t["role"].upper()
307
+
308
+ # Role prefix
309
+ if role == "USER":
310
+ lines.append(f"### [{ts}] Architect")
311
+ elif role == "ASSISTANT":
312
+ lines.append(f"### [{ts}] Agent")
313
+ else:
314
+ lines.append(f"### [{ts}] {role}")
315
+ lines.append("")
316
+
317
+ # Content (truncate very long content)
318
+ content = t["content"].strip()
319
+ if content:
320
+ if len(content) > 2000:
321
+ content = content[:2000] + "\n\n*[truncated]*"
322
+ lines.append(content)
323
+ lines.append("")
324
+
325
+ # Tool calls
326
+ if t["tool_calls"]:
327
+ for tc in t["tool_calls"]:
328
+ lines.append(f"**Tool**: `{tc['tool']}` ({', '.join(tc['input_keys'])})")
329
+ lines.append("")
330
+
331
+ return "\n".join(lines)
332
+
333
+
334
+ def format_session_summary(turns, metadata):
335
+ """Structured summary — delegates to shared canonical formatter."""
336
+ common = _get_common()
337
+ return common.format_session_summary_dict(turns, metadata)
338
+
339
+
340
+ def format_summary_markdown(turns, metadata):
341
+ """Compact summary — delegates to shared canonical formatter."""
342
+ common = _get_common()
343
+ return common.format_summary_markdown(turns, metadata)
344
+
345
+
346
+ # ---------------------------------------------------------------------------
347
+ # Output: List
348
+ # ---------------------------------------------------------------------------
349
+
350
+ def print_session_list(projects):
351
+ """Print a summary of all discoverable sessions."""
352
+ print()
353
+ print(" Claude Code sessions")
354
+ print(f" {len(projects)} projects found")
355
+ print()
356
+
357
+ total_sessions = 0
358
+ for p in projects:
359
+ print(f" {p['name']} ({len(p['sessions'])} sessions)")
360
+ for s in p["sessions"]:
361
+ print(f" {s['uuid'][:8]}... {s['lines']:>5} lines {s['modified']}")
362
+ total_sessions += 1
363
+ print()
364
+
365
+ print(f" total: {total_sessions} sessions across {len(projects)} projects")
366
+ print()
367
+
368
+
369
+ # ---------------------------------------------------------------------------
370
+ # Entry point
371
+ # ---------------------------------------------------------------------------
372
+
373
+ def main():
374
+ if sys.stdout.encoding != "utf-8":
375
+ sys.stdout = io.TextIOWrapper(
376
+ sys.stdout.buffer, encoding="utf-8", errors="replace"
377
+ )
378
+
379
+ parser = argparse.ArgumentParser(
380
+ description="Extract session logs from Claude Code."
381
+ )
382
+ parser.add_argument(
383
+ "--list", "-l",
384
+ action="store_true",
385
+ help="List all discoverable sessions",
386
+ )
387
+ parser.add_argument(
388
+ "--project", "-p",
389
+ help="Filter by project name (partial match)",
390
+ )
391
+ parser.add_argument(
392
+ "--session", "-s",
393
+ help="Extract a specific session by UUID (partial match)",
394
+ )
395
+ parser.add_argument(
396
+ "--json", "-j",
397
+ action="store_true",
398
+ help="Output as JSON instead of markdown",
399
+ )
400
+ parser.add_argument(
401
+ "--latest", "-n",
402
+ type=int,
403
+ default=0,
404
+ help="Extract only the N most recent sessions",
405
+ )
406
+ parser.add_argument(
407
+ "--summary",
408
+ action="store_true",
409
+ help="Extract compact summaries (for git) instead of full transcripts",
410
+ )
411
+ args = parser.parse_args()
412
+
413
+ # Discover
414
+ projects = discover_projects()
415
+ if not projects:
416
+ print(" No Claude Code sessions found.", file=sys.stderr)
417
+ print(f" Looked in: {PROJECTS_DIR}", file=sys.stderr)
418
+ sys.exit(1)
419
+
420
+ # List mode
421
+ if args.list:
422
+ print_session_list(projects)
423
+ return
424
+
425
+ # Filter by project
426
+ if args.project:
427
+ p = find_project(projects, args.project)
428
+ if not p:
429
+ print(f" Project '{args.project}' not found.", file=sys.stderr)
430
+ print(f" Available: {', '.join(p['name'] for p in projects)}", file=sys.stderr)
431
+ sys.exit(1)
432
+ projects = [p]
433
+
434
+ # Collect all sessions to extract
435
+ all_sessions = []
436
+ for p in projects:
437
+ for s in p["sessions"]:
438
+ all_sessions.append((p, s))
439
+
440
+ # Filter by session UUID
441
+ if args.session:
442
+ all_sessions = [
443
+ (p, s) for p, s in all_sessions
444
+ if args.session.lower() in s["uuid"].lower()
445
+ ]
446
+ if not all_sessions:
447
+ print(f" Session '{args.session}' not found.", file=sys.stderr)
448
+ sys.exit(1)
449
+
450
+ # Sort by modified date (most recent first) and limit
451
+ all_sessions.sort(key=lambda x: x[1]["modified"], reverse=True)
452
+ if args.latest > 0:
453
+ all_sessions = all_sessions[:args.latest]
454
+
455
+ # Extract
456
+ results = []
457
+ for p, s in all_sessions:
458
+ turns = extract_session(s["path"])
459
+ if not turns:
460
+ continue
461
+ metadata = extract_session_metadata(turns)
462
+ metadata["project"] = p["name"]
463
+
464
+ summary = format_session_summary(turns, metadata)
465
+
466
+ if args.json:
467
+ result_item = {
468
+ "project": p["name"],
469
+ "metadata": metadata,
470
+ "summary": summary,
471
+ }
472
+ if not args.summary:
473
+ result_item["turns"] = turns
474
+ results.append(result_item)
475
+ elif args.summary:
476
+ md = format_summary_markdown(turns, metadata)
477
+ print(md)
478
+ print("\n" + "=" * 60 + "\n")
479
+ else:
480
+ md = format_session_markdown(turns, metadata)
481
+ print(md)
482
+ print("\n" + "=" * 60 + "\n")
483
+
484
+ if args.json:
485
+ print(json.dumps(results, indent=2, default=str))
486
+
487
+
488
+ if __name__ == "__main__":
489
+ main()