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.
- gator_command/__init__.py +2 -0
- gator_command/cli.py +137 -0
- gator_command/scripts/crawler.py +633 -0
- gator_command/scripts/dashboard/dashboard.css +982 -0
- gator_command/scripts/dashboard/dashboard.html +84 -0
- gator_command/scripts/dashboard/dashboard.js +419 -0
- gator_command/scripts/dashboard/views/audit.js +270 -0
- gator_command/scripts/dashboard/views/fleet.js +307 -0
- gator_command/scripts/dashboard/views/repo.js +599 -0
- gator_command/scripts/dashboard/views/settings.js +173 -0
- gator_command/scripts/dashboard/views/updates.js +308 -0
- gator_command/scripts/enforcer-prompt.md +22 -0
- gator_command/scripts/extract-claude-sessions.py +489 -0
- gator_command/scripts/extract-codex-sessions.py +477 -0
- gator_command/scripts/extract-gemini-sessions.py +410 -0
- gator_command/scripts/gator-audit.py +956 -0
- gator_command/scripts/gator-charter-draft.py +919 -0
- gator_command/scripts/gator-charter-lint.py +427 -0
- gator_command/scripts/gator-charter-verify.py +606 -0
- gator_command/scripts/gator-dashboard.py +1271 -0
- gator_command/scripts/gator-deploy.py +916 -0
- gator_command/scripts/gator-drift.py +569 -0
- gator_command/scripts/gator-enforce.py +82 -0
- gator_command/scripts/gator-fleet-intel.py +460 -0
- gator_command/scripts/gator-fleet-report.py +615 -0
- gator_command/scripts/gator-init-command-post.py +315 -0
- gator_command/scripts/gator-init.py +434 -0
- gator_command/scripts/gator-machine-id.py +153 -0
- gator_command/scripts/gator-policy-status.py +631 -0
- gator_command/scripts/gator-pulse.py +459 -0
- gator_command/scripts/gator-repo-status.py +649 -0
- gator_command/scripts/gator-session-common.py +372 -0
- gator_command/scripts/gator-session-sink.py +831 -0
- gator_command/scripts/gator-sessions.py +1244 -0
- gator_command/scripts/gator-update.py +615 -0
- gator_command/scripts/gator-version.py +38 -0
- gator_command/scripts/gator_core.py +489 -0
- gator_command/scripts/gator_remote.py +381 -0
- gator_command/scripts/gator_runtime.py +142 -0
- gator_command/scripts/gatorize-actions.sh +989 -0
- gator_command/scripts/gatorize-lib.sh +166 -0
- gator_command/scripts/gatorize-post.sh +394 -0
- gator_command/scripts/gatorize.py +1163 -0
- gator_command/scripts/gatorize.sh +185 -0
- gator_command/scripts/generate_markdown.py +212 -0
- gator_command/scripts/generate_wiki.py +424 -0
- gator_command/scripts/graph_health.py +780 -0
- gator_command/scripts/memex-lint.py +286 -0
- gator_command/scripts/memex-lint.sh +205 -0
- gator_command/scripts/memex.py +1472 -0
- gator_command/scripts/memex_formatters.py +191 -0
- gator_command/scripts/memex_state.py +236 -0
- gator_command/scripts/spawn.py +650 -0
- gator_command/templates/gator-starter/blueprints/README.md +32 -0
- gator_command/templates/gator-starter/charterignore +53 -0
- gator_command/templates/gator-starter/charters/README.md +178 -0
- gator_command/templates/gator-starter/charters/_template.md +31 -0
- gator_command/templates/gator-starter/commands/commit.md +33 -0
- gator_command/templates/gator-starter/commands/init.md +11 -0
- gator_command/templates/gator-starter/commands/update.md +5 -0
- gator_command/templates/gator-starter/constitution.md +165 -0
- gator_command/templates/gator-starter/field-guides/README.md +25 -0
- gator_command/templates/gator-starter/gator-start-up.md +119 -0
- gator_command/templates/gator-starter/procedures/charter-alignment.md +83 -0
- gator_command/templates/gator-starter/procedures/enforcer-review.md +317 -0
- gator_command/templates/gator-starter/procedures/field-guide-generation.md +176 -0
- gator_command/templates/gator-starter/procedures/knowledge-capture.md +57 -0
- gator_command/templates/gator-starter/procedures/significance-check.md +69 -0
- gator_command/templates/gator-starter/reference-notes/concierge-responses.md +535 -0
- gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +91 -0
- gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +22 -0
- gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +232 -0
- gator_command/templates/gator-starter/reference-notes/example-project.md +289 -0
- gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +72 -0
- gator_command/templates/gator-starter/reference-notes/git-workflow.md +60 -0
- gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +37 -0
- gator_command/templates/gator-starter/reference-notes/refactor-approach.md +155 -0
- gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +108 -0
- gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +99 -0
- gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +155 -0
- gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/enforcer-prompt.md +55 -0
- gator_command/templates/gator-starter/scripts/enforcer-review.py +1551 -0
- gator_command/templates/gator-starter/scripts/gator-approve.py +139 -0
- gator_command/templates/gator-starter/scripts/gator-enforce.py +82 -0
- gator_command/templates/gator-starter/scripts/gator-init.py +434 -0
- gator_command/templates/gator-starter/scripts/gator-pre-commit.py +2670 -0
- gator_command/templates/gator-starter/scripts/gator-pulse.py +459 -0
- gator_command/templates/gator-starter/scripts/gator-update.py +615 -0
- gator_command/templates/gator-starter/scripts/gator-version.py +38 -0
- gator_command/templates/gator-starter/scripts/gator_core.py +487 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/commit-msg +5 -0
- gator_command/templates/gator-starter/scripts/hooks/post-commit +7 -0
- gator_command/templates/gator-starter/scripts/hooks/pre-commit +5 -0
- gator_command/templates/gator-starter/sessions/.gitignore +7 -0
- gator_command/templates/gator-starter/vault/.gitkeep +0 -0
- gator_command/templates/gator-starter/whiteboard.md +5 -0
- gator_command-1.0.0.dist-info/METADATA +122 -0
- gator_command-1.0.0.dist-info/RECORD +110 -0
- gator_command-1.0.0.dist-info/WHEEL +5 -0
- gator_command-1.0.0.dist-info/entry_points.txt +2 -0
- gator_command-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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()
|