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