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,477 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ extract-codex-sessions.py — Session archaeology for OpenAI Codex CLI.
4
+
5
+ Reads Codex CLI's native session storage (~/.codex/sessions/),
6
+ extracts structured turn data, and produces standardized markdown
7
+ session logs matching the same format as extract-claude-sessions.py.
8
+
9
+ Storage layout:
10
+ ~/.codex/sessions/YYYY/MM/DD/
11
+ rollout-<timestamp>-<uuid>.jsonl
12
+ ~/.codex/history.jsonl — prompt history
13
+
14
+ JSONL turn format:
15
+ timestamp: ISO string
16
+ type: session_meta|response_item|event_msg|turn_context
17
+ payload:
18
+ session_meta: {id, cwd, cli_version, model_provider, git: {commit_hash, branch}}
19
+ response_item: {type: message|function_call|function_call_output|reasoning,
20
+ role: user|developer|assistant, content: [{type, text}]}
21
+ event_msg: {type: task_started|task_complete, turn_id}
22
+ turn_context: {turn_id, cwd, model, approval_policy, sandbox_policy}
23
+
24
+ Role mapping:
25
+ user/developer → Architect (both are human input)
26
+ assistant → Agent
27
+ function_call → Tool use
28
+ function_call_output → Tool result
29
+ reasoning → Agent thinking (internal)
30
+
31
+ Usage:
32
+ python gator-command/scripts/extract-codex-sessions.py
33
+ python gator-command/scripts/extract-codex-sessions.py --list
34
+ python gator-command/scripts/extract-codex-sessions.py --date 2026-05-28
35
+ python gator-command/scripts/extract-codex-sessions.py --latest 3 --summary
36
+ python gator-command/scripts/extract-codex-sessions.py --json
37
+
38
+ @reads: ~/.codex/sessions/, ~/.codex/history.jsonl
39
+ @writes: stdout (or session log files when --output specified)
40
+ """
41
+
42
+ import argparse
43
+ import glob
44
+ import io
45
+ import json
46
+ import os
47
+ import re
48
+ import sys
49
+ from datetime import datetime, timezone
50
+ from pathlib import Path
51
+
52
+ # Shared session utilities — use gator_core.import_sibling for clean imports
53
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
54
+ from gator_core import import_sibling
55
+
56
+ _COMMON = None
57
+ def _get_common():
58
+ global _COMMON
59
+ if _COMMON is None:
60
+ _COMMON = import_sibling("gator-session-common")
61
+ return _COMMON
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Constants
66
+ # ---------------------------------------------------------------------------
67
+
68
+ CODEX_DIR = Path.home() / ".codex"
69
+ SESSIONS_DIR = CODEX_DIR / "sessions"
70
+ HISTORY_FILE = CODEX_DIR / "history.jsonl"
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Discovery
75
+ # ---------------------------------------------------------------------------
76
+
77
+ def discover_sessions():
78
+ """Find all Codex session JSONL files.
79
+
80
+ Returns list of {uuid, path, date, lines, size, modified}
81
+ organized by date.
82
+ """
83
+ if not SESSIONS_DIR.is_dir():
84
+ return []
85
+
86
+ sessions = []
87
+ for jsonl in sorted(SESSIONS_DIR.rglob("rollout-*.jsonl")):
88
+ try:
89
+ line_count = sum(1 for _ in open(jsonl, encoding="utf-8", errors="replace"))
90
+ size = jsonl.stat().st_size
91
+ modified = datetime.fromtimestamp(jsonl.stat().st_mtime)
92
+
93
+ # Extract UUID from filename: rollout-<timestamp>-<uuid>.jsonl
94
+ name = jsonl.stem # rollout-2026-05-28T08-33-48-<uuid>
95
+ parts = name.split("-", 7) # split on hyphens
96
+ uuid = parts[-1] if len(parts) > 1 else name
97
+
98
+ # Extract date from path: sessions/YYYY/MM/DD/
99
+ rel = jsonl.relative_to(SESSIONS_DIR)
100
+ date_parts = list(rel.parts[:-1]) # ['2026', '05', '28']
101
+ date_str = "-".join(date_parts) if len(date_parts) >= 3 else ""
102
+
103
+ sessions.append({
104
+ "uuid": uuid,
105
+ "path": jsonl,
106
+ "date": date_str,
107
+ "lines": line_count,
108
+ "size": size,
109
+ "modified": modified.strftime("%Y-%m-%d %H:%M"),
110
+ })
111
+ except OSError:
112
+ continue
113
+
114
+ return sessions
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Extraction
119
+ # ---------------------------------------------------------------------------
120
+
121
+ def extract_session(session_path):
122
+ """Extract structured turns from a Codex JSONL session file."""
123
+ turns = []
124
+ session_meta = {}
125
+
126
+ with open(session_path, encoding="utf-8", errors="replace") as f:
127
+ for line in f:
128
+ line = line.strip()
129
+ if not line:
130
+ continue
131
+ try:
132
+ data = json.loads(line)
133
+ except json.JSONDecodeError:
134
+ continue
135
+
136
+ msg_type = data.get("type")
137
+ timestamp = data.get("timestamp", "")
138
+ payload = data.get("payload", {})
139
+
140
+ if msg_type == "session_meta":
141
+ session_meta = payload
142
+ continue
143
+
144
+ if msg_type == "turn_context":
145
+ # Capture model info but don't emit as a turn
146
+ session_meta["model"] = payload.get("model", "")
147
+ session_meta["approval_policy"] = payload.get("approval_policy", "")
148
+ continue
149
+
150
+ if msg_type == "event_msg":
151
+ # Skip task lifecycle events
152
+ continue
153
+
154
+ if msg_type == "response_item":
155
+ role = payload.get("role", "")
156
+ item_type = payload.get("type", "")
157
+
158
+ # Extract text content
159
+ text_content = ""
160
+ tool_calls = []
161
+ content = payload.get("content") or []
162
+
163
+ if isinstance(content, list):
164
+ text_parts = []
165
+ for block in content:
166
+ if not isinstance(block, dict):
167
+ continue
168
+ btype = block.get("type", "")
169
+ if btype in ("input_text", "output_text", "text"):
170
+ text = block.get("text", "")
171
+ if text:
172
+ text_parts.append(text)
173
+ text_content = "\n".join(text_parts)
174
+ elif isinstance(content, str):
175
+ text_content = content
176
+
177
+ # Function calls
178
+ if item_type == "function_call":
179
+ func_name = payload.get("name", "unknown")
180
+ func_args = payload.get("arguments", "")
181
+ tool_calls.append({
182
+ "tool": func_name,
183
+ "input_keys": _extract_arg_keys(func_args),
184
+ })
185
+ text_content = f"[calling {func_name}]"
186
+
187
+ if item_type == "function_call_output":
188
+ output = payload.get("output", "")
189
+ if isinstance(output, str):
190
+ text_content = f"[tool result: {output[:200]}]"
191
+
192
+ if item_type == "reasoning":
193
+ # Internal reasoning — include but mark
194
+ text_content = payload.get("summary", "")
195
+ if not text_content:
196
+ # Try content blocks
197
+ for block in (payload.get("content") or []):
198
+ if isinstance(block, dict) and block.get("text"):
199
+ text_content = block["text"]
200
+ break
201
+
202
+ # Map roles
203
+ if role in ("user", "developer"):
204
+ mapped_role = "user"
205
+ elif role == "assistant":
206
+ mapped_role = "assistant"
207
+ elif item_type == "function_call":
208
+ mapped_role = "assistant"
209
+ elif item_type == "function_call_output":
210
+ mapped_role = "system"
211
+ elif item_type == "reasoning":
212
+ mapped_role = "assistant"
213
+ else:
214
+ mapped_role = role or "unknown"
215
+
216
+ # Skip system/developer messages that are just instructions
217
+ if mapped_role == "user" and text_content:
218
+ # Skip if it's the AGENTS.md injection or sandbox config
219
+ if text_content.startswith("# AGENTS.md") or \
220
+ text_content.startswith("<permissions") or \
221
+ text_content.startswith("<environment_context"):
222
+ continue
223
+
224
+ turns.append({
225
+ "type": msg_type,
226
+ "role": mapped_role,
227
+ "timestamp": timestamp,
228
+ "content": text_content,
229
+ "tool_calls": tool_calls,
230
+ "item_type": item_type,
231
+ })
232
+
233
+ return turns, session_meta
234
+
235
+
236
+ def _extract_arg_keys(args_str):
237
+ """Extract key names from a JSON arguments string."""
238
+ if not args_str:
239
+ return []
240
+ try:
241
+ args = json.loads(args_str)
242
+ if isinstance(args, dict):
243
+ return list(args.keys())
244
+ except (json.JSONDecodeError, TypeError):
245
+ pass
246
+ return []
247
+
248
+
249
+ def extract_session_metadata(turns, session_meta):
250
+ """Extract session-level metadata."""
251
+ if not turns:
252
+ return {}
253
+
254
+ timestamps = [t["timestamp"] for t in turns if t["timestamp"]]
255
+ start = timestamps[0] if timestamps else ""
256
+ end = timestamps[-1] if timestamps else ""
257
+
258
+ user_count = sum(1 for t in turns if t["role"] == "user")
259
+ assistant_count = sum(1 for t in turns if t["role"] == "assistant")
260
+
261
+ all_tools = set()
262
+ for t in turns:
263
+ for tc in t.get("tool_calls", []):
264
+ all_tools.add(tc["tool"])
265
+
266
+ # Get repo from session_meta
267
+ cwd = session_meta.get("cwd", "")
268
+ repo = Path(cwd).name if cwd else ""
269
+ branch = ""
270
+ git_info = session_meta.get("git", {})
271
+ if isinstance(git_info, dict):
272
+ branch = git_info.get("branch", "")
273
+
274
+ model = session_meta.get("model", "unknown")
275
+ cli_version = session_meta.get("cli_version", "")
276
+
277
+ return {
278
+ "session_id": session_meta.get("id", ""),
279
+ "vendor": "codex",
280
+ "agent": f"Codex CLI ({model})",
281
+ "architect": "AG",
282
+ "start": start,
283
+ "end": end,
284
+ "repo": repo,
285
+ "cwd": cwd,
286
+ "branch": branch,
287
+ "user_turns": user_count,
288
+ "assistant_turns": assistant_count,
289
+ "total_turns": len(turns),
290
+ "tools_used": sorted(all_tools),
291
+ "model": model,
292
+ "cli_version": cli_version,
293
+ }
294
+
295
+
296
+ # ---------------------------------------------------------------------------
297
+ # Output formatting
298
+ # ---------------------------------------------------------------------------
299
+
300
+ def format_session_markdown(turns, metadata):
301
+ """Full transcript as markdown."""
302
+ lines = []
303
+
304
+ lines.append("---")
305
+ lines.append(f"session-id: {metadata.get('session_id', '')}")
306
+ lines.append(f"date: {metadata.get('start', '')[:10]}")
307
+ lines.append(f"start: {metadata.get('start', '')}")
308
+ lines.append(f"end: {metadata.get('end', '')}")
309
+ lines.append(f"repo: {metadata.get('repo', '')}")
310
+ lines.append(f"agent: Codex CLI ({metadata.get('model', 'unknown')})")
311
+ lines.append(f"turns: {metadata.get('total_turns', 0)}")
312
+ lines.append(f"tools: {', '.join(metadata.get('tools_used', []))}")
313
+ lines.append("---")
314
+ lines.append("")
315
+ lines.append(f"# Session Log — {metadata.get('start', '')[:10]}")
316
+ lines.append("")
317
+ lines.append(f"**Repo**: {metadata.get('repo', 'unknown')}")
318
+ lines.append(f"**Branch**: {metadata.get('branch', 'unknown')}")
319
+ lines.append(f"**Model**: {metadata.get('model', 'unknown')}")
320
+ lines.append(f"**Turns**: {metadata.get('user_turns', 0)} user, {metadata.get('assistant_turns', 0)} assistant")
321
+ lines.append(f"**Tools**: {', '.join(metadata.get('tools_used', [])) or 'none'}")
322
+ lines.append("")
323
+ lines.append("---")
324
+ lines.append("")
325
+
326
+ for t in turns:
327
+ ts = t["timestamp"][:19] if t["timestamp"] else "?"
328
+ role = t["role"]
329
+ item_type = t.get("item_type", "")
330
+
331
+ if role == "user":
332
+ lines.append(f"### [{ts}] Architect")
333
+ elif role == "assistant" and item_type == "reasoning":
334
+ lines.append(f"### [{ts}] Agent (thinking)")
335
+ elif role == "assistant":
336
+ lines.append(f"### [{ts}] Agent")
337
+ else:
338
+ lines.append(f"### [{ts}] {role}")
339
+ lines.append("")
340
+
341
+ content = t["content"].strip()
342
+ if content:
343
+ if len(content) > 2000:
344
+ content = content[:2000] + "\n\n*[content truncated]*"
345
+ lines.append(content)
346
+ lines.append("")
347
+
348
+ if t["tool_calls"]:
349
+ for tc in t["tool_calls"]:
350
+ lines.append(f"**Tool**: `{tc['tool']}` ({', '.join(tc['input_keys'])})")
351
+ lines.append("")
352
+
353
+ return "\n".join(lines)
354
+
355
+
356
+ def format_summary_markdown(turns, metadata):
357
+ """Compact summary — delegates to shared canonical formatter."""
358
+ common = _get_common()
359
+ return common.format_summary_markdown(turns, metadata)
360
+
361
+
362
+ def format_session_summary(turns, metadata):
363
+ """Structured summary — delegates to shared canonical formatter."""
364
+ common = _get_common()
365
+ return common.format_session_summary_dict(turns, metadata)
366
+
367
+
368
+ # ---------------------------------------------------------------------------
369
+ # List
370
+ # ---------------------------------------------------------------------------
371
+
372
+ def print_session_list(sessions):
373
+ """Print summary of all discoverable sessions."""
374
+ print()
375
+ print(" Codex CLI sessions")
376
+ print(f" {len(sessions)} sessions found")
377
+ print()
378
+
379
+ by_date = {}
380
+ for s in sessions:
381
+ by_date.setdefault(s["date"], []).append(s)
382
+
383
+ for date in sorted(by_date.keys()):
384
+ items = by_date[date]
385
+ print(f" {date} ({len(items)} sessions)")
386
+ for s in items:
387
+ size_kb = s["size"] // 1024
388
+ print(f" {s['uuid'][:8]}... {s['lines']:>4} lines {size_kb:>4} KB {s['modified']}")
389
+ print()
390
+
391
+ print(f" total: {len(sessions)} sessions")
392
+ print()
393
+
394
+
395
+ # ---------------------------------------------------------------------------
396
+ # Entry point
397
+ # ---------------------------------------------------------------------------
398
+
399
+ def main():
400
+ if sys.stdout.encoding != "utf-8":
401
+ sys.stdout = io.TextIOWrapper(
402
+ sys.stdout.buffer, encoding="utf-8", errors="replace"
403
+ )
404
+
405
+ parser = argparse.ArgumentParser(
406
+ description="Extract session logs from Codex CLI."
407
+ )
408
+ parser.add_argument("--list", "-l", action="store_true", help="List all sessions")
409
+ parser.add_argument("--date", "-d", help="Filter by date (YYYY-MM-DD)")
410
+ parser.add_argument("--session", "-s", help="Extract specific session (UUID partial match)")
411
+ parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
412
+ parser.add_argument("--latest", "-n", type=int, default=0, help="Extract N most recent")
413
+ parser.add_argument("--summary", action="store_true", help="Compact summary for git")
414
+ args = parser.parse_args()
415
+
416
+ sessions = discover_sessions()
417
+ if not sessions:
418
+ print(" No Codex CLI sessions found.", file=sys.stderr)
419
+ print(f" Looked in: {SESSIONS_DIR}", file=sys.stderr)
420
+ sys.exit(1)
421
+
422
+ if args.list:
423
+ print_session_list(sessions)
424
+ return
425
+
426
+ # Filter by date
427
+ if args.date:
428
+ sessions = [s for s in sessions if args.date in s["date"]]
429
+ if not sessions:
430
+ print(f" No sessions found for date '{args.date}'.", file=sys.stderr)
431
+ sys.exit(1)
432
+
433
+ # Filter by UUID
434
+ if args.session:
435
+ sessions = [s for s in sessions if args.session.lower() in s["uuid"].lower()]
436
+ if not sessions:
437
+ print(f" Session '{args.session}' not found.", file=sys.stderr)
438
+ sys.exit(1)
439
+
440
+ # Sort by modified (most recent first) and limit
441
+ sessions.sort(key=lambda x: x["modified"], reverse=True)
442
+ if args.latest > 0:
443
+ sessions = sessions[:args.latest]
444
+
445
+ # Extract
446
+ results = []
447
+ for s in sessions:
448
+ turns, session_meta = extract_session(s["path"])
449
+ if not turns:
450
+ continue
451
+ metadata = extract_session_metadata(turns, session_meta)
452
+
453
+ summary = format_session_summary(turns, metadata)
454
+
455
+ if args.json:
456
+ result_item = {
457
+ "metadata": metadata,
458
+ "summary": summary,
459
+ }
460
+ if not args.summary:
461
+ result_item["turns"] = turns
462
+ results.append(result_item)
463
+ elif args.summary:
464
+ md = format_summary_markdown(turns, metadata)
465
+ print(md)
466
+ print("\n" + "=" * 60 + "\n")
467
+ else:
468
+ md = format_session_markdown(turns, metadata)
469
+ print(md)
470
+ print("\n" + "=" * 60 + "\n")
471
+
472
+ if args.json:
473
+ print(json.dumps(results, indent=2, default=str))
474
+
475
+
476
+ if __name__ == "__main__":
477
+ main()