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,410 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
extract-gemini-sessions.py — Session archaeology for Gemini CLI.
|
|
4
|
+
|
|
5
|
+
Reads Gemini CLI's native session storage (~/.gemini/tmp/<project>/chats/),
|
|
6
|
+
extracts structured turn data, and produces standardized markdown
|
|
7
|
+
session logs matching the same format as the Claude and Codex scripts.
|
|
8
|
+
|
|
9
|
+
Storage layout:
|
|
10
|
+
~/.gemini/tmp/<project>/chats/
|
|
11
|
+
session-<timestamp>-<uuid>.json — single JSON file per session
|
|
12
|
+
~/.gemini/projects.json — maps cwd paths to project slugs
|
|
13
|
+
|
|
14
|
+
Session JSON format:
|
|
15
|
+
{sessionId, projectHash, startTime, lastUpdated, messages: [...], kind}
|
|
16
|
+
|
|
17
|
+
Message format:
|
|
18
|
+
type: user|gemini|info|error
|
|
19
|
+
content: [{type, text}] — text blocks
|
|
20
|
+
toolCalls: [{name, args}] — gemini tool invocations
|
|
21
|
+
model: string — gemini messages only
|
|
22
|
+
tokens: {input, output} — gemini messages only
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
python gator-command/scripts/extract-gemini-sessions.py --list
|
|
26
|
+
python gator-command/scripts/extract-gemini-sessions.py --project learnhub
|
|
27
|
+
python gator-command/scripts/extract-gemini-sessions.py --latest 3 --summary
|
|
28
|
+
python gator-command/scripts/extract-gemini-sessions.py --json
|
|
29
|
+
|
|
30
|
+
@reads: ~/.gemini/tmp/*/chats/, ~/.gemini/projects.json
|
|
31
|
+
@writes: stdout
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
import glob
|
|
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
|
+
GEMINI_DIR = Path.home() / ".gemini"
|
|
61
|
+
TMP_DIR = GEMINI_DIR / "tmp"
|
|
62
|
+
PROJECTS_FILE = GEMINI_DIR / "projects.json"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Discovery
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def load_project_map():
|
|
70
|
+
"""Load project path → slug mapping from projects.json."""
|
|
71
|
+
if not PROJECTS_FILE.exists():
|
|
72
|
+
return {}
|
|
73
|
+
try:
|
|
74
|
+
data = json.loads(PROJECTS_FILE.read_text(encoding="utf-8"))
|
|
75
|
+
return data.get("projects", {})
|
|
76
|
+
except (json.JSONDecodeError, OSError):
|
|
77
|
+
return {}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def discover_sessions():
|
|
81
|
+
"""Find all Gemini session JSON files.
|
|
82
|
+
|
|
83
|
+
Returns list of {uuid, project, path, date, messages, size, modified}
|
|
84
|
+
"""
|
|
85
|
+
if not TMP_DIR.is_dir():
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
sessions = []
|
|
89
|
+
pattern = str(TMP_DIR / "*" / "chats" / "session-*.json")
|
|
90
|
+
|
|
91
|
+
for filepath in sorted(glob.glob(pattern)):
|
|
92
|
+
filepath = Path(filepath)
|
|
93
|
+
try:
|
|
94
|
+
size = filepath.stat().st_size
|
|
95
|
+
modified = datetime.fromtimestamp(filepath.stat().st_mtime)
|
|
96
|
+
project = filepath.parent.parent.name
|
|
97
|
+
|
|
98
|
+
# Read internal sessionId from JSON payload (canonical identity)
|
|
99
|
+
# Falls back to filename stem if JSON can't be read
|
|
100
|
+
source_file = filepath.stem
|
|
101
|
+
uuid = source_file # fallback
|
|
102
|
+
try:
|
|
103
|
+
with open(filepath, encoding="utf-8", errors="replace") as sf:
|
|
104
|
+
data = json.load(sf)
|
|
105
|
+
if isinstance(data, dict) and data.get("sessionId"):
|
|
106
|
+
uuid = data["sessionId"]
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
# Extract date from filename
|
|
111
|
+
# session-2026-03-17T21-35-... → 2026-03-17
|
|
112
|
+
parts = filepath.stem.split("-")
|
|
113
|
+
date_str = ""
|
|
114
|
+
if len(parts) >= 4:
|
|
115
|
+
date_str = f"{parts[1]}-{parts[2]}-{parts[3][:2]}"
|
|
116
|
+
|
|
117
|
+
sessions.append({
|
|
118
|
+
"uuid": uuid,
|
|
119
|
+
"source_file": source_file,
|
|
120
|
+
"project": project,
|
|
121
|
+
"path": filepath,
|
|
122
|
+
"date": date_str,
|
|
123
|
+
"size": size,
|
|
124
|
+
"modified": modified.strftime("%Y-%m-%d %H:%M"),
|
|
125
|
+
})
|
|
126
|
+
except OSError:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
return sessions
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Extraction
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def extract_session(session_path):
|
|
137
|
+
"""Extract structured turns from a Gemini session JSON file."""
|
|
138
|
+
with open(session_path, encoding="utf-8", errors="replace") as f:
|
|
139
|
+
data = json.load(f)
|
|
140
|
+
|
|
141
|
+
session_meta = {
|
|
142
|
+
"session_id": data.get("sessionId", ""),
|
|
143
|
+
"start": data.get("startTime", ""),
|
|
144
|
+
"end": data.get("lastUpdated", ""),
|
|
145
|
+
"kind": data.get("kind", ""),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
turns = []
|
|
149
|
+
messages = data.get("messages", [])
|
|
150
|
+
|
|
151
|
+
for msg in messages:
|
|
152
|
+
msg_type = msg.get("type", "")
|
|
153
|
+
timestamp = msg.get("timestamp", "")
|
|
154
|
+
content_blocks = msg.get("content") or []
|
|
155
|
+
|
|
156
|
+
# Extract text content
|
|
157
|
+
text_parts = []
|
|
158
|
+
for block in content_blocks:
|
|
159
|
+
if isinstance(block, dict):
|
|
160
|
+
text = block.get("text", "")
|
|
161
|
+
if text:
|
|
162
|
+
text_parts.append(text)
|
|
163
|
+
text_content = "\n".join(text_parts)
|
|
164
|
+
|
|
165
|
+
# Extract tool calls
|
|
166
|
+
tool_calls = []
|
|
167
|
+
for tc in (msg.get("toolCalls") or []):
|
|
168
|
+
if isinstance(tc, dict):
|
|
169
|
+
tool_calls.append({
|
|
170
|
+
"tool": tc.get("name", "unknown"),
|
|
171
|
+
"input_keys": list(tc.get("args", {}).keys()),
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
# Map roles
|
|
175
|
+
if msg_type == "user":
|
|
176
|
+
role = "user"
|
|
177
|
+
elif msg_type == "gemini":
|
|
178
|
+
role = "assistant"
|
|
179
|
+
elif msg_type in ("info", "error"):
|
|
180
|
+
role = "system"
|
|
181
|
+
else:
|
|
182
|
+
role = msg_type or "unknown"
|
|
183
|
+
|
|
184
|
+
# Skip empty messages
|
|
185
|
+
if not text_content and not tool_calls:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
turn = {
|
|
189
|
+
"type": msg_type,
|
|
190
|
+
"role": role,
|
|
191
|
+
"timestamp": timestamp,
|
|
192
|
+
"content": text_content,
|
|
193
|
+
"tool_calls": tool_calls,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Gemini-specific metadata
|
|
197
|
+
if msg_type == "gemini":
|
|
198
|
+
turn["model"] = msg.get("model", "")
|
|
199
|
+
turn["tokens"] = msg.get("tokens", {})
|
|
200
|
+
|
|
201
|
+
turns.append(turn)
|
|
202
|
+
|
|
203
|
+
return turns, session_meta
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def extract_session_metadata(turns, session_meta, project_name):
|
|
207
|
+
"""Extract session-level metadata."""
|
|
208
|
+
if not turns:
|
|
209
|
+
return {}
|
|
210
|
+
|
|
211
|
+
timestamps = [t["timestamp"] for t in turns if t["timestamp"]]
|
|
212
|
+
start = session_meta.get("start", timestamps[0] if timestamps else "")
|
|
213
|
+
end = session_meta.get("end", timestamps[-1] if timestamps else "")
|
|
214
|
+
|
|
215
|
+
user_count = sum(1 for t in turns if t["role"] == "user")
|
|
216
|
+
assistant_count = sum(1 for t in turns if t["role"] == "assistant")
|
|
217
|
+
|
|
218
|
+
all_tools = set()
|
|
219
|
+
for t in turns:
|
|
220
|
+
for tc in t.get("tool_calls", []):
|
|
221
|
+
all_tools.add(tc["tool"])
|
|
222
|
+
|
|
223
|
+
# Get model from first gemini message
|
|
224
|
+
model = ""
|
|
225
|
+
for t in turns:
|
|
226
|
+
if t.get("model"):
|
|
227
|
+
model = t["model"]
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
"session_id": session_meta.get("session_id", ""),
|
|
232
|
+
"vendor": "gemini",
|
|
233
|
+
"agent": f"Gemini CLI ({model})" if model else "Gemini CLI",
|
|
234
|
+
"architect": "AG",
|
|
235
|
+
"start": start,
|
|
236
|
+
"end": end,
|
|
237
|
+
"repo": project_name,
|
|
238
|
+
"user_turns": user_count,
|
|
239
|
+
"assistant_turns": assistant_count,
|
|
240
|
+
"total_turns": len(turns),
|
|
241
|
+
"tools_used": sorted(all_tools),
|
|
242
|
+
"model": model,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
# Output formatting
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
def format_session_markdown(turns, metadata):
|
|
251
|
+
"""Full transcript as markdown."""
|
|
252
|
+
lines = []
|
|
253
|
+
lines.append("---")
|
|
254
|
+
lines.append(f"session-id: {metadata.get('session_id', '')}")
|
|
255
|
+
lines.append(f"date: {metadata.get('start', '')[:10]}")
|
|
256
|
+
lines.append(f"start: {metadata.get('start', '')}")
|
|
257
|
+
lines.append(f"end: {metadata.get('end', '')}")
|
|
258
|
+
lines.append(f"repo: {metadata.get('repo', '')}")
|
|
259
|
+
lines.append(f"agent: Gemini CLI ({metadata.get('model', 'unknown')})")
|
|
260
|
+
lines.append(f"turns: {metadata.get('total_turns', 0)}")
|
|
261
|
+
lines.append(f"tools: {', '.join(metadata.get('tools_used', []))}")
|
|
262
|
+
lines.append("---")
|
|
263
|
+
lines.append("")
|
|
264
|
+
lines.append(f"# Session Log — {metadata.get('start', '')[:10]}")
|
|
265
|
+
lines.append("")
|
|
266
|
+
lines.append(f"**Repo**: {metadata.get('repo', 'unknown')}")
|
|
267
|
+
lines.append(f"**Model**: {metadata.get('model', 'unknown')}")
|
|
268
|
+
lines.append(f"**Turns**: {metadata.get('user_turns', 0)} user, {metadata.get('assistant_turns', 0)} assistant")
|
|
269
|
+
lines.append(f"**Tools**: {', '.join(metadata.get('tools_used', [])) or 'none'}")
|
|
270
|
+
lines.append("")
|
|
271
|
+
lines.append("---")
|
|
272
|
+
lines.append("")
|
|
273
|
+
|
|
274
|
+
for t in turns:
|
|
275
|
+
ts = t["timestamp"][:19] if t["timestamp"] else "?"
|
|
276
|
+
if t["role"] == "user":
|
|
277
|
+
lines.append(f"### [{ts}] Architect")
|
|
278
|
+
elif t["role"] == "assistant":
|
|
279
|
+
lines.append(f"### [{ts}] Agent")
|
|
280
|
+
else:
|
|
281
|
+
lines.append(f"### [{ts}] {t['role']}")
|
|
282
|
+
lines.append("")
|
|
283
|
+
|
|
284
|
+
content = t["content"].strip()
|
|
285
|
+
if content:
|
|
286
|
+
if len(content) > 2000:
|
|
287
|
+
content = content[:2000] + "\n\n*[content truncated]*"
|
|
288
|
+
lines.append(content)
|
|
289
|
+
lines.append("")
|
|
290
|
+
|
|
291
|
+
if t["tool_calls"]:
|
|
292
|
+
for tc in t["tool_calls"]:
|
|
293
|
+
lines.append(f"**Tool**: `{tc['tool']}` ({', '.join(tc['input_keys'])})")
|
|
294
|
+
lines.append("")
|
|
295
|
+
|
|
296
|
+
return "\n".join(lines)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def format_summary_markdown(turns, metadata):
|
|
300
|
+
"""Compact summary — delegates to shared canonical formatter."""
|
|
301
|
+
common = _get_common()
|
|
302
|
+
return common.format_summary_markdown(turns, metadata)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def format_session_summary(turns, metadata):
|
|
306
|
+
"""Structured summary — delegates to shared canonical formatter."""
|
|
307
|
+
common = _get_common()
|
|
308
|
+
return common.format_session_summary_dict(turns, metadata)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
# List
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
def print_session_list(sessions):
|
|
316
|
+
"""Print summary of all discoverable sessions."""
|
|
317
|
+
print()
|
|
318
|
+
print(" Gemini CLI sessions")
|
|
319
|
+
print(f" {len(sessions)} sessions found")
|
|
320
|
+
print()
|
|
321
|
+
|
|
322
|
+
by_project = {}
|
|
323
|
+
for s in sessions:
|
|
324
|
+
by_project.setdefault(s["project"], []).append(s)
|
|
325
|
+
|
|
326
|
+
for project in sorted(by_project.keys()):
|
|
327
|
+
items = by_project[project]
|
|
328
|
+
print(f" {project} ({len(items)} sessions)")
|
|
329
|
+
for s in items:
|
|
330
|
+
size_kb = s["size"] // 1024
|
|
331
|
+
print(f" {s['uuid'][:8]} {s['date']} {size_kb:>4} KB {s['modified']}")
|
|
332
|
+
print()
|
|
333
|
+
|
|
334
|
+
print(f" total: {len(sessions)} sessions across {len(by_project)} projects")
|
|
335
|
+
print()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
# Entry point
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
def main():
|
|
343
|
+
if sys.stdout.encoding != "utf-8":
|
|
344
|
+
sys.stdout = io.TextIOWrapper(
|
|
345
|
+
sys.stdout.buffer, encoding="utf-8", errors="replace"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
parser = argparse.ArgumentParser(
|
|
349
|
+
description="Extract session logs from Gemini CLI."
|
|
350
|
+
)
|
|
351
|
+
parser.add_argument("--list", "-l", action="store_true", help="List all sessions")
|
|
352
|
+
parser.add_argument("--project", "-p", help="Filter by project name")
|
|
353
|
+
parser.add_argument("--session", "-s", help="Extract specific session (UUID partial match)")
|
|
354
|
+
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
|
355
|
+
parser.add_argument("--latest", "-n", type=int, default=0, help="Extract N most recent")
|
|
356
|
+
parser.add_argument("--summary", action="store_true", help="Compact summary for git")
|
|
357
|
+
args = parser.parse_args()
|
|
358
|
+
|
|
359
|
+
sessions = discover_sessions()
|
|
360
|
+
if not sessions:
|
|
361
|
+
print(" No Gemini CLI sessions found.", file=sys.stderr)
|
|
362
|
+
print(f" Looked in: {TMP_DIR}", file=sys.stderr)
|
|
363
|
+
sys.exit(1)
|
|
364
|
+
|
|
365
|
+
if args.list:
|
|
366
|
+
print_session_list(sessions)
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
if args.project:
|
|
370
|
+
sessions = [s for s in sessions if args.project.lower() in s["project"].lower()]
|
|
371
|
+
if not sessions:
|
|
372
|
+
print(f" Project '{args.project}' not found.", file=sys.stderr)
|
|
373
|
+
sys.exit(1)
|
|
374
|
+
|
|
375
|
+
if args.session:
|
|
376
|
+
sessions = [s for s in sessions if args.session.lower() in s["uuid"].lower()]
|
|
377
|
+
if not sessions:
|
|
378
|
+
print(f" Session '{args.session}' not found.", file=sys.stderr)
|
|
379
|
+
sys.exit(1)
|
|
380
|
+
|
|
381
|
+
sessions.sort(key=lambda x: x["modified"], reverse=True)
|
|
382
|
+
if args.latest > 0:
|
|
383
|
+
sessions = sessions[:args.latest]
|
|
384
|
+
|
|
385
|
+
results = []
|
|
386
|
+
for s in sessions:
|
|
387
|
+
turns, session_meta = extract_session(s["path"])
|
|
388
|
+
if not turns:
|
|
389
|
+
continue
|
|
390
|
+
metadata = extract_session_metadata(turns, session_meta, s["project"])
|
|
391
|
+
summary = format_session_summary(turns, metadata)
|
|
392
|
+
|
|
393
|
+
if args.json:
|
|
394
|
+
result_item = {"metadata": metadata, "summary": summary}
|
|
395
|
+
if not args.summary:
|
|
396
|
+
result_item["turns"] = turns
|
|
397
|
+
results.append(result_item)
|
|
398
|
+
elif args.summary:
|
|
399
|
+
print(format_summary_markdown(turns, metadata))
|
|
400
|
+
print("\n" + "=" * 60 + "\n")
|
|
401
|
+
else:
|
|
402
|
+
print(format_session_markdown(turns, metadata))
|
|
403
|
+
print("\n" + "=" * 60 + "\n")
|
|
404
|
+
|
|
405
|
+
if args.json:
|
|
406
|
+
print(json.dumps(results, indent=2, default=str))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
if __name__ == "__main__":
|
|
410
|
+
main()
|