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,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()
|