@trac3er/oh-my-god 2.0.0 → 2.0.2
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.
- package/.claude-plugin/marketplace.json +8 -8
- package/.claude-plugin/plugin.json +5 -4
- package/.claude-plugin/scripts/uninstall.sh +74 -3
- package/.claude-plugin/scripts/update.sh +78 -3
- package/.coveragerc +26 -0
- package/.mcp.json +4 -4
- package/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +27 -0
- package/CONTRIBUTING.md +62 -0
- package/OMG-setup.sh +1201 -355
- package/README.md +77 -56
- package/SECURITY.md +25 -0
- package/agents/__init__.py +1 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-architect-mode.md +3 -5
- package/agents/omg-backend-engineer.md +3 -5
- package/agents/omg-database-engineer.md +3 -5
- package/agents/omg-frontend-designer.md +4 -5
- package/agents/omg-implement-mode.md +4 -5
- package/agents/omg-infra-engineer.md +3 -5
- package/agents/omg-research-mode.md +4 -6
- package/agents/omg-security-auditor.md +3 -5
- package/agents/omg-testing-engineer.md +3 -5
- package/build/lib/yaml.py +321 -0
- package/commands/OMG:ai-commit.md +101 -14
- package/commands/OMG:arch.md +302 -19
- package/commands/OMG:ccg.md +12 -7
- package/commands/OMG:compat.md +25 -17
- package/commands/OMG:cost.md +173 -13
- package/commands/OMG:crazy.md +1 -1
- package/commands/OMG:create-agent.md +170 -20
- package/commands/OMG:deps.md +235 -17
- package/commands/OMG:domain-init.md +1 -1
- package/commands/OMG:escalate.md +41 -12
- package/commands/OMG:health-check.md +37 -13
- package/commands/OMG:init.md +122 -14
- package/commands/OMG:project-init.md +1 -1
- package/commands/OMG:session-branch.md +76 -9
- package/commands/OMG:session-fork.md +42 -5
- package/commands/OMG:session-merge.md +124 -8
- package/commands/OMG:setup.md +69 -12
- package/commands/OMG:stats.md +215 -14
- package/commands/OMG:teams.md +19 -10
- package/config/lsp_languages.yaml +8 -0
- package/hooks/__init__.py +0 -0
- package/hooks/_agent_registry.py +423 -0
- package/hooks/_analytics.py +291 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +569 -0
- package/hooks/_compression_optimizer.py +119 -0
- package/hooks/_cost_ledger.py +176 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/_protected_context.py +150 -0
- package/hooks/_token_counter.py +221 -0
- package/hooks/branch_manager.py +236 -0
- package/hooks/budget_governor.py +232 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/compression_feedback.py +254 -0
- package/hooks/config-guard.py +216 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +1020 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +505 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +219 -0
- package/hooks/post_write.py +46 -0
- package/hooks/pre-compact.py +398 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/query.py +512 -0
- package/hooks/secret-guard.py +61 -0
- package/hooks/secret_audit.py +144 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +277 -0
- package/hooks/setup_wizard.py +582 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +225 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +945 -0
- package/hooks/test-validator.py +361 -0
- package/hooks/test_generator_hook.py +123 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +149 -0
- package/hooks/trust_review.py +585 -0
- package/hud/omg-hud.mjs +31 -1
- package/lab/__init__.py +1 -0
- package/lab/pipeline.py +75 -0
- package/lab/policies.py +52 -0
- package/package.json +7 -18
- package/plugins/README.md +33 -61
- package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
- package/plugins/advanced/commands/OMG:learn.md +1 -1
- package/plugins/advanced/commands/OMG:security-review.md +3 -3
- package/plugins/advanced/commands/OMG:ship.md +1 -1
- package/plugins/advanced/plugin.json +1 -1
- package/plugins/core/plugin.json +8 -3
- package/plugins/dephealth/__init__.py +0 -0
- package/plugins/dephealth/cve_scanner.py +188 -0
- package/plugins/dephealth/license_checker.py +135 -0
- package/plugins/dephealth/manifest_detector.py +423 -0
- package/plugins/dephealth/vuln_analyzer.py +169 -0
- package/plugins/testgen/__init__.py +0 -0
- package/plugins/testgen/codamosa_engine.py +402 -0
- package/plugins/testgen/edge_case_synthesizer.py +184 -0
- package/plugins/testgen/framework_detector.py +271 -0
- package/plugins/testgen/skeleton_generator.py +219 -0
- package/plugins/viz/__init__.py +0 -0
- package/plugins/viz/ast_parser.py +139 -0
- package/plugins/viz/diagram_generator.py +192 -0
- package/plugins/viz/graph_builder.py +444 -0
- package/plugins/viz/native_parsers.py +259 -0
- package/plugins/viz/regex_parser.py +112 -0
- package/pyproject.toml +81 -0
- package/rules/contextual/write-verify.md +2 -2
- package/rules/core/00-truth.md +1 -1
- package/rules/core/01-surgical.md +1 -1
- package/rules/core/02-circuit-breaker.md +2 -2
- package/rules/core/03-ensemble.md +3 -3
- package/rules/core/04-testing.md +3 -3
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/adoption.py +212 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/cli_provider.py +85 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/mcp_config_writers.py +115 -0
- package/runtime/mcp_lifecycle.py +153 -0
- package/runtime/mcp_memory_server.py +135 -0
- package/runtime/memory_parsers/__init__.py +0 -0
- package/runtime/memory_parsers/chatgpt_parser.py +257 -0
- package/runtime/memory_parsers/claude_import.py +107 -0
- package/runtime/memory_parsers/export.py +97 -0
- package/runtime/memory_parsers/gemini_import.py +91 -0
- package/runtime/memory_parsers/kimi_import.py +91 -0
- package/runtime/memory_store.py +215 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/providers/__init__.py +0 -0
- package/runtime/providers/codex_provider.py +112 -0
- package/runtime/providers/gemini_provider.py +128 -0
- package/runtime/providers/kimi_provider.py +151 -0
- package/runtime/providers/opencode_provider.py +144 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +1167 -0
- package/runtime/tmux_session_manager.py +169 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-contract-snapshot.py +12 -0
- package/scripts/check-omg-public-ready.py +193 -0
- package/scripts/check-omg-standalone-clean.py +103 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-legacy.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +492 -0
- package/scripts/settings-merge.py +283 -0
- package/scripts/verify-standalone.sh +8 -4
- package/settings.json +126 -29
- package/templates/profile.yaml +1 -1
- package/tools/__init__.py +2 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +347 -0
- package/tools/commit_splitter.py +746 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/dashboard_generator.py +300 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/pr_generator.py +404 -0
- package/tools/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- package/tools/web_search.py +622 -0
- package/yaml.py +321 -0
- package/.claude-plugin/scripts/install.sh +0 -9
- package/bun.lock +0 -23
- package/bunfig.toml +0 -3
- package/hooks/_budget.ts +0 -1
- package/hooks/_common.ts +0 -63
- package/hooks/circuit-breaker.ts +0 -101
- package/hooks/config-guard.ts +0 -4
- package/hooks/firewall.ts +0 -20
- package/hooks/policy_engine.ts +0 -156
- package/hooks/post-tool-failure.ts +0 -22
- package/hooks/post-write.ts +0 -4
- package/hooks/pre-tool-inject.ts +0 -4
- package/hooks/prompt-enhancer.ts +0 -46
- package/hooks/quality-runner.ts +0 -24
- package/hooks/secret-guard.ts +0 -4
- package/hooks/session-end-capture.ts +0 -19
- package/hooks/session-start.ts +0 -19
- package/hooks/shadow_manager.ts +0 -81
- package/hooks/stop-gate.ts +0 -22
- package/hooks/stop_dispatcher.ts +0 -147
- package/hooks/test-generator-hook.ts +0 -4
- package/hooks/tool-ledger.ts +0 -27
- package/hooks/trust_review.ts +0 -175
- package/lab/pipeline.ts +0 -75
- package/lab/policies.ts +0 -68
- package/runtime/common.ts +0 -111
- package/runtime/compat.ts +0 -174
- package/runtime/dispatcher.ts +0 -25
- package/runtime/ecosystem.ts +0 -186
- package/runtime/provider_bootstrap.ts +0 -99
- package/runtime/provider_smoke.ts +0 -34
- package/runtime/release_readiness.ts +0 -186
- package/runtime/team_router.ts +0 -144
- package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
- package/scripts/check-omg-standalone-clean.ts +0 -12
- package/scripts/check-runtime-clean.ts +0 -94
- package/scripts/omg.ts +0 -352
- package/scripts/settings-merge.ts +0 -93
- package/tools/commit_splitter.ts +0 -23
- package/tools/git_inspector.ts +0 -18
- package/tools/session_snapshot.ts +0 -47
- package/trac3er-oh-my-god-2.0.0.tgz +0 -0
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
HTML Dashboard Generator for OMG
|
|
4
|
+
|
|
5
|
+
Generates a single self-contained HTML dashboard with Chart.js (CDN)
|
|
6
|
+
from tool-ledger.jsonl and cost-ledger.jsonl data.
|
|
7
|
+
|
|
8
|
+
Feature flag: SESSION_ANALYTICS (default: False)
|
|
9
|
+
Pure stdlib — no external dependencies.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from collections import Counter, defaultdict
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
# Lazy import for feature flag helper
|
|
21
|
+
_get_feature_flag = None
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _ensure_imports():
|
|
27
|
+
"""Lazy import feature flag helper."""
|
|
28
|
+
global _get_feature_flag
|
|
29
|
+
if _get_feature_flag is None:
|
|
30
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
31
|
+
try:
|
|
32
|
+
from hooks._common import get_feature_flag as _gff
|
|
33
|
+
_get_feature_flag = _gff
|
|
34
|
+
except ImportError:
|
|
35
|
+
# Fallback: always return default
|
|
36
|
+
_get_feature_flag = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _read_jsonl(path: str) -> List[Dict[str, Any]]:
|
|
40
|
+
"""Read JSONL file and return list of parsed entries.
|
|
41
|
+
|
|
42
|
+
Skips malformed lines gracefully. Returns empty list if file missing.
|
|
43
|
+
"""
|
|
44
|
+
if not os.path.exists(path):
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
entries: List[Dict[str, Any]] = []
|
|
48
|
+
try:
|
|
49
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
50
|
+
for line in f:
|
|
51
|
+
line = line.strip()
|
|
52
|
+
if not line:
|
|
53
|
+
continue
|
|
54
|
+
try:
|
|
55
|
+
entry = json.loads(line)
|
|
56
|
+
if isinstance(entry, dict):
|
|
57
|
+
entries.append(entry)
|
|
58
|
+
except (json.JSONDecodeError, ValueError):
|
|
59
|
+
continue
|
|
60
|
+
except Exception:
|
|
61
|
+
pass # Crash isolation: return what we have
|
|
62
|
+
|
|
63
|
+
return entries
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _aggregate_tool_usage(tool_entries: List[Dict[str, Any]]) -> Dict[str, int]:
|
|
67
|
+
"""Count tool invocations by tool name."""
|
|
68
|
+
counts: Dict[str, int] = Counter()
|
|
69
|
+
for entry in tool_entries:
|
|
70
|
+
tool = entry.get("tool", "unknown")
|
|
71
|
+
if isinstance(tool, str) and tool:
|
|
72
|
+
counts[tool] += 1
|
|
73
|
+
return dict(counts)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _aggregate_cost_over_time(cost_entries: List[Dict[str, Any]]) -> List[Tuple[str, float]]:
|
|
77
|
+
"""Aggregate cost by date (YYYY-MM-DD)."""
|
|
78
|
+
by_date: Dict[str, float] = defaultdict(float)
|
|
79
|
+
for entry in cost_entries:
|
|
80
|
+
ts = entry.get("ts", "")
|
|
81
|
+
cost = entry.get("cost_usd", 0.0)
|
|
82
|
+
if not isinstance(ts, str) or not ts:
|
|
83
|
+
continue
|
|
84
|
+
try:
|
|
85
|
+
cost_val = float(cost)
|
|
86
|
+
except (TypeError, ValueError):
|
|
87
|
+
cost_val = 0.0
|
|
88
|
+
# Extract date portion
|
|
89
|
+
date_str = ts[:10] # YYYY-MM-DD
|
|
90
|
+
if len(date_str) == 10:
|
|
91
|
+
by_date[date_str] += cost_val
|
|
92
|
+
|
|
93
|
+
return sorted(by_date.items())
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _build_session_summary(
|
|
97
|
+
tool_entries: List[Dict[str, Any]],
|
|
98
|
+
cost_entries: List[Dict[str, Any]],
|
|
99
|
+
) -> Dict[str, Any]:
|
|
100
|
+
"""Build session summary stats."""
|
|
101
|
+
total_tools = len(tool_entries)
|
|
102
|
+
total_cost = sum(
|
|
103
|
+
float(e.get("cost_usd", 0.0))
|
|
104
|
+
for e in cost_entries
|
|
105
|
+
if isinstance(e.get("cost_usd"), (int, float))
|
|
106
|
+
)
|
|
107
|
+
total_tokens = sum(
|
|
108
|
+
int(e.get("tokens_in", 0)) + int(e.get("tokens_out", 0))
|
|
109
|
+
for e in cost_entries
|
|
110
|
+
if isinstance(e.get("tokens_in"), (int, float))
|
|
111
|
+
and isinstance(e.get("tokens_out"), (int, float))
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"total_tool_calls": total_tools,
|
|
116
|
+
"total_cost_usd": round(total_cost, 6),
|
|
117
|
+
"total_tokens": total_tokens,
|
|
118
|
+
"cost_entries": len(cost_entries),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _render_html(
|
|
123
|
+
tool_usage: Dict[str, int],
|
|
124
|
+
cost_over_time: List[Tuple[str, float]],
|
|
125
|
+
session_summary: Dict[str, Any],
|
|
126
|
+
cost_entries: List[Dict[str, Any]],
|
|
127
|
+
) -> str:
|
|
128
|
+
"""Render a self-contained HTML dashboard with Chart.js CDN."""
|
|
129
|
+
# Prepare JSON data for charts
|
|
130
|
+
tool_labels = json.dumps(list(tool_usage.keys()))
|
|
131
|
+
tool_data = json.dumps(list(tool_usage.values()))
|
|
132
|
+
|
|
133
|
+
cost_labels = json.dumps([item[0] for item in cost_over_time])
|
|
134
|
+
cost_data = json.dumps([round(item[1], 6) for item in cost_over_time])
|
|
135
|
+
|
|
136
|
+
# Per-entry cost data for detailed chart
|
|
137
|
+
entry_costs = []
|
|
138
|
+
for entry in cost_entries:
|
|
139
|
+
ts = entry.get("ts", "")
|
|
140
|
+
cost = entry.get("cost_usd", 0.0)
|
|
141
|
+
try:
|
|
142
|
+
cost_val = round(float(cost), 6)
|
|
143
|
+
except (TypeError, ValueError):
|
|
144
|
+
cost_val = 0.0
|
|
145
|
+
entry_costs.append({"ts": ts, "cost": cost_val})
|
|
146
|
+
entry_costs_json = json.dumps(entry_costs)
|
|
147
|
+
|
|
148
|
+
summary_html = (
|
|
149
|
+
f"<li>Total Tool Calls: <strong>{session_summary['total_tool_calls']}</strong></li>"
|
|
150
|
+
f"<li>Total Cost: <strong>${session_summary['total_cost_usd']}</strong></li>"
|
|
151
|
+
f"<li>Total Tokens: <strong>{session_summary['total_tokens']}</strong></li>"
|
|
152
|
+
f"<li>Cost Entries: <strong>{session_summary['cost_entries']}</strong></li>"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return f"""<!DOCTYPE html>
|
|
156
|
+
<html lang="en">
|
|
157
|
+
<head>
|
|
158
|
+
<meta charset="UTF-8">
|
|
159
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
160
|
+
<title>OMG Session Dashboard</title>
|
|
161
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
162
|
+
<style>
|
|
163
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
164
|
+
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; }}
|
|
165
|
+
h1 {{ text-align: center; margin-bottom: 24px; color: #38bdf8; }}
|
|
166
|
+
.grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 24px; max-width: 1200px; margin: 0 auto; }}
|
|
167
|
+
.card {{ background: #1e293b; border-radius: 12px; padding: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }}
|
|
168
|
+
.card h2 {{ margin-bottom: 16px; color: #94a3b8; font-size: 14px; text-transform: uppercase; letter-spacing: 1px; }}
|
|
169
|
+
.card.full {{ grid-column: 1 / -1; }}
|
|
170
|
+
canvas {{ max-height: 300px; }}
|
|
171
|
+
ul {{ list-style: none; }}
|
|
172
|
+
ul li {{ padding: 8px 0; border-bottom: 1px solid #334155; font-size: 15px; }}
|
|
173
|
+
ul li strong {{ color: #38bdf8; }}
|
|
174
|
+
.timestamp {{ text-align: center; color: #64748b; margin-top: 24px; font-size: 12px; }}
|
|
175
|
+
</style>
|
|
176
|
+
</head>
|
|
177
|
+
<body>
|
|
178
|
+
<h1>OMG Session Dashboard</h1>
|
|
179
|
+
<div class="grid">
|
|
180
|
+
<div class="card">
|
|
181
|
+
<h2>Tool Usage</h2>
|
|
182
|
+
<canvas id="toolChart"></canvas>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="card">
|
|
185
|
+
<h2>Cost Over Time</h2>
|
|
186
|
+
<canvas id="costChart"></canvas>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="card full">
|
|
189
|
+
<h2>Session Summary</h2>
|
|
190
|
+
<ul>
|
|
191
|
+
{summary_html}
|
|
192
|
+
</ul>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="timestamp">Generated: {datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")}</div>
|
|
196
|
+
<script>
|
|
197
|
+
const toolLabels = {tool_labels};
|
|
198
|
+
const toolData = {tool_data};
|
|
199
|
+
const costLabels = {cost_labels};
|
|
200
|
+
const costData = {cost_data};
|
|
201
|
+
|
|
202
|
+
new Chart(document.getElementById('toolChart'), {{
|
|
203
|
+
type: 'bar',
|
|
204
|
+
data: {{
|
|
205
|
+
labels: toolLabels,
|
|
206
|
+
datasets: [{{
|
|
207
|
+
label: 'Tool Calls',
|
|
208
|
+
data: toolData,
|
|
209
|
+
backgroundColor: 'rgba(56, 189, 248, 0.7)',
|
|
210
|
+
borderColor: 'rgba(56, 189, 248, 1)',
|
|
211
|
+
borderWidth: 1
|
|
212
|
+
}}]
|
|
213
|
+
}},
|
|
214
|
+
options: {{
|
|
215
|
+
responsive: true,
|
|
216
|
+
plugins: {{ legend: {{ display: false }} }},
|
|
217
|
+
scales: {{
|
|
218
|
+
y: {{ beginAtZero: true, ticks: {{ color: '#94a3b8' }}, grid: {{ color: '#334155' }} }},
|
|
219
|
+
x: {{ ticks: {{ color: '#94a3b8' }}, grid: {{ color: '#334155' }} }}
|
|
220
|
+
}}
|
|
221
|
+
}}
|
|
222
|
+
}});
|
|
223
|
+
|
|
224
|
+
new Chart(document.getElementById('costChart'), {{
|
|
225
|
+
type: 'line',
|
|
226
|
+
data: {{
|
|
227
|
+
labels: costLabels,
|
|
228
|
+
datasets: [{{
|
|
229
|
+
label: 'Cost (USD)',
|
|
230
|
+
data: costData,
|
|
231
|
+
borderColor: 'rgba(34, 197, 94, 1)',
|
|
232
|
+
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
|
233
|
+
fill: true,
|
|
234
|
+
tension: 0.3
|
|
235
|
+
}}]
|
|
236
|
+
}},
|
|
237
|
+
options: {{
|
|
238
|
+
responsive: true,
|
|
239
|
+
plugins: {{ legend: {{ display: false }} }},
|
|
240
|
+
scales: {{
|
|
241
|
+
y: {{ beginAtZero: true, ticks: {{ color: '#94a3b8' }}, grid: {{ color: '#334155' }} }},
|
|
242
|
+
x: {{ ticks: {{ color: '#94a3b8' }}, grid: {{ color: '#334155' }} }}
|
|
243
|
+
}}
|
|
244
|
+
}}
|
|
245
|
+
}});
|
|
246
|
+
</script>
|
|
247
|
+
</body>
|
|
248
|
+
</html>"""
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def generate_dashboard(
|
|
252
|
+
project_dir: str,
|
|
253
|
+
output_path: Optional[str] = None,
|
|
254
|
+
) -> str:
|
|
255
|
+
"""Generate an HTML dashboard from OMG ledger data.
|
|
256
|
+
|
|
257
|
+
Reads tool usage stats from {project_dir}/.omg/state/ledger/tool-ledger.jsonl
|
|
258
|
+
and cost data from {project_dir}/.omg/state/ledger/cost-ledger.jsonl.
|
|
259
|
+
Generates a single self-contained HTML file with Chart.js loaded from CDN.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
project_dir: Project root directory.
|
|
263
|
+
output_path: Path for the HTML file. Defaults to
|
|
264
|
+
{project_dir}/.omg/state/dashboard.html.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
The output_path string on success.
|
|
268
|
+
"""
|
|
269
|
+
_ensure_imports()
|
|
270
|
+
|
|
271
|
+
# Check feature flag — return empty string if disabled
|
|
272
|
+
if _get_feature_flag is not None:
|
|
273
|
+
if not _get_feature_flag("SESSION_ANALYTICS", default=False):
|
|
274
|
+
return ""
|
|
275
|
+
|
|
276
|
+
# Resolve default output path
|
|
277
|
+
if output_path is None:
|
|
278
|
+
output_path = os.path.join(project_dir, ".omg", "state", "dashboard.html")
|
|
279
|
+
|
|
280
|
+
# Read ledger data
|
|
281
|
+
tool_ledger_path = os.path.join(project_dir, ".omg", "state", "ledger", "tool-ledger.jsonl")
|
|
282
|
+
cost_ledger_path = os.path.join(project_dir, ".omg", "state", "ledger", "cost-ledger.jsonl")
|
|
283
|
+
|
|
284
|
+
tool_entries = _read_jsonl(tool_ledger_path)
|
|
285
|
+
cost_entries = _read_jsonl(cost_ledger_path)
|
|
286
|
+
|
|
287
|
+
# Aggregate data
|
|
288
|
+
tool_usage = _aggregate_tool_usage(tool_entries)
|
|
289
|
+
cost_over_time = _aggregate_cost_over_time(cost_entries)
|
|
290
|
+
session_summary = _build_session_summary(tool_entries, cost_entries)
|
|
291
|
+
|
|
292
|
+
# Render HTML
|
|
293
|
+
html = _render_html(tool_usage, cost_over_time, session_summary, cost_entries)
|
|
294
|
+
|
|
295
|
+
# Write file
|
|
296
|
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
297
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
298
|
+
f.write(html)
|
|
299
|
+
|
|
300
|
+
return output_path
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Git Inspection Tools for OMG
|
|
4
|
+
|
|
5
|
+
Read-only git inspection: status, log, and hunk-level diffs.
|
|
6
|
+
Feature flag: OMG_GIT_TOOLS_ENABLED (default: False)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
# Import feature flag helper
|
|
17
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
18
|
+
from hooks._common import get_feature_flag
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def git_status(cwd: str = ".") -> Dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Get git status: staged, unstaged, untracked files and current branch.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
cwd: Working directory (default: current directory)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
{
|
|
30
|
+
"skipped": True # if feature flag disabled
|
|
31
|
+
}
|
|
32
|
+
or
|
|
33
|
+
{
|
|
34
|
+
"staged": ["file1.py", "file2.py"],
|
|
35
|
+
"unstaged": ["file3.py"],
|
|
36
|
+
"untracked": ["file4.py"],
|
|
37
|
+
"branch": "main",
|
|
38
|
+
"error": None
|
|
39
|
+
}
|
|
40
|
+
or
|
|
41
|
+
{
|
|
42
|
+
"error": "git not found"
|
|
43
|
+
}
|
|
44
|
+
"""
|
|
45
|
+
# Check feature flag
|
|
46
|
+
if not get_feature_flag("git_tools", default=False):
|
|
47
|
+
return {"skipped": True}
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# Get status with porcelain format
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
["git", "status", "--porcelain"],
|
|
53
|
+
cwd=cwd,
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
timeout=10
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if result.returncode != 0:
|
|
60
|
+
return {"error": "git command failed"}
|
|
61
|
+
|
|
62
|
+
staged = []
|
|
63
|
+
unstaged = []
|
|
64
|
+
untracked = []
|
|
65
|
+
|
|
66
|
+
for line in result.stdout.split("\n"):
|
|
67
|
+
if not line:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
status_code = line[:2]
|
|
71
|
+
file_path = line[3:]
|
|
72
|
+
|
|
73
|
+
# First char: index (staged)
|
|
74
|
+
# Second char: working tree (unstaged)
|
|
75
|
+
if status_code[0] != " ":
|
|
76
|
+
staged.append(file_path)
|
|
77
|
+
if status_code[1] != " ":
|
|
78
|
+
unstaged.append(file_path)
|
|
79
|
+
if status_code == "??":
|
|
80
|
+
untracked.append(file_path)
|
|
81
|
+
|
|
82
|
+
# Get current branch
|
|
83
|
+
branch_result = subprocess.run(
|
|
84
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
85
|
+
cwd=cwd,
|
|
86
|
+
capture_output=True,
|
|
87
|
+
text=True,
|
|
88
|
+
timeout=10
|
|
89
|
+
)
|
|
90
|
+
branch = branch_result.stdout.strip() if branch_result.returncode == 0 else "unknown"
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"staged": staged,
|
|
94
|
+
"unstaged": unstaged,
|
|
95
|
+
"untracked": untracked,
|
|
96
|
+
"branch": branch,
|
|
97
|
+
"error": None
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
except FileNotFoundError:
|
|
101
|
+
return {"error": "git not found"}
|
|
102
|
+
except subprocess.TimeoutExpired:
|
|
103
|
+
return {"error": "git command timeout"}
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return {"error": str(e)}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def git_log(cwd: str = ".", n: int = 10) -> List[Dict[str, Any]]:
|
|
109
|
+
"""
|
|
110
|
+
Get recent N commits with hash, subject, author, and date.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
cwd: Working directory (default: current directory)
|
|
114
|
+
n: Number of commits to retrieve (default: 10)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of {hash, subject, author, date} dicts
|
|
118
|
+
Empty list if git not available or feature flag disabled
|
|
119
|
+
"""
|
|
120
|
+
# Check feature flag
|
|
121
|
+
if not get_feature_flag("git_tools", default=False):
|
|
122
|
+
return []
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
result = subprocess.run(
|
|
126
|
+
["git", "log", f"--oneline", f"-n", str(n),
|
|
127
|
+
"--format=%H|%s|%an|%ai"],
|
|
128
|
+
cwd=cwd,
|
|
129
|
+
capture_output=True,
|
|
130
|
+
text=True,
|
|
131
|
+
timeout=10
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if result.returncode != 0:
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
commits = []
|
|
138
|
+
for line in result.stdout.strip().split("\n"):
|
|
139
|
+
if not line:
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
parts = line.split("|", 3)
|
|
143
|
+
if len(parts) == 4:
|
|
144
|
+
commits.append({
|
|
145
|
+
"hash": parts[0],
|
|
146
|
+
"subject": parts[1],
|
|
147
|
+
"author": parts[2],
|
|
148
|
+
"date": parts[3]
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
return commits
|
|
152
|
+
|
|
153
|
+
except FileNotFoundError:
|
|
154
|
+
return []
|
|
155
|
+
except subprocess.TimeoutExpired:
|
|
156
|
+
return []
|
|
157
|
+
except Exception:
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def git_hunk(cwd: str = ".", file_path: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
162
|
+
"""
|
|
163
|
+
Get hunk-level diff for a file or all files.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
cwd: Working directory (default: current directory)
|
|
167
|
+
file_path: Specific file to diff (None for all files)
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
List of {file, old_start, old_count, new_start, new_count, context, lines} dicts
|
|
171
|
+
Empty list if no diff or git not available or feature flag disabled
|
|
172
|
+
"""
|
|
173
|
+
# Check feature flag
|
|
174
|
+
if not get_feature_flag("git_tools", default=False):
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
cmd = ["git", "diff", "--unified=3"]
|
|
179
|
+
if file_path:
|
|
180
|
+
cmd.append(file_path)
|
|
181
|
+
|
|
182
|
+
result = subprocess.run(
|
|
183
|
+
cmd,
|
|
184
|
+
cwd=cwd,
|
|
185
|
+
capture_output=True,
|
|
186
|
+
text=True,
|
|
187
|
+
timeout=10
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if result.returncode != 0:
|
|
191
|
+
return []
|
|
192
|
+
|
|
193
|
+
hunks = []
|
|
194
|
+
current_file = None
|
|
195
|
+
current_hunk = None
|
|
196
|
+
hunk_lines = []
|
|
197
|
+
|
|
198
|
+
# Regex to match hunk header: @@ -a,b +c,d @@ context
|
|
199
|
+
hunk_header_re = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@\s*(.*)")
|
|
200
|
+
|
|
201
|
+
for line in result.stdout.split("\n"):
|
|
202
|
+
# File header
|
|
203
|
+
if line.startswith("diff --git"):
|
|
204
|
+
# Save previous hunk if exists
|
|
205
|
+
if current_hunk and hunk_lines:
|
|
206
|
+
current_hunk["lines"] = hunk_lines
|
|
207
|
+
hunks.append(current_hunk)
|
|
208
|
+
hunk_lines = []
|
|
209
|
+
current_hunk = None
|
|
210
|
+
|
|
211
|
+
# Extract filename from "diff --git a/file b/file"
|
|
212
|
+
parts = line.split()
|
|
213
|
+
if len(parts) >= 4:
|
|
214
|
+
current_file = parts[3][2:] # Remove "b/" prefix
|
|
215
|
+
|
|
216
|
+
# Hunk header
|
|
217
|
+
elif line.startswith("@@"):
|
|
218
|
+
# Save previous hunk if exists
|
|
219
|
+
if current_hunk and hunk_lines:
|
|
220
|
+
current_hunk["lines"] = hunk_lines
|
|
221
|
+
hunks.append(current_hunk)
|
|
222
|
+
hunk_lines = []
|
|
223
|
+
|
|
224
|
+
match = hunk_header_re.match(line)
|
|
225
|
+
if match:
|
|
226
|
+
old_start = int(match.group(1))
|
|
227
|
+
old_count = int(match.group(2)) if match.group(2) else 1
|
|
228
|
+
new_start = int(match.group(3))
|
|
229
|
+
new_count = int(match.group(4)) if match.group(4) else 1
|
|
230
|
+
context = match.group(5).strip()
|
|
231
|
+
|
|
232
|
+
current_hunk = {
|
|
233
|
+
"file": current_file,
|
|
234
|
+
"old_start": old_start,
|
|
235
|
+
"old_count": old_count,
|
|
236
|
+
"new_start": new_start,
|
|
237
|
+
"new_count": new_count,
|
|
238
|
+
"context": context,
|
|
239
|
+
"lines": []
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# Hunk content
|
|
243
|
+
elif current_hunk is not None:
|
|
244
|
+
if line.startswith("+") or line.startswith("-") or line.startswith(" "):
|
|
245
|
+
hunk_lines.append(line)
|
|
246
|
+
|
|
247
|
+
# Save last hunk
|
|
248
|
+
if current_hunk and hunk_lines:
|
|
249
|
+
current_hunk["lines"] = hunk_lines
|
|
250
|
+
hunks.append(current_hunk)
|
|
251
|
+
|
|
252
|
+
return hunks
|
|
253
|
+
|
|
254
|
+
except FileNotFoundError:
|
|
255
|
+
return []
|
|
256
|
+
except subprocess.TimeoutExpired:
|
|
257
|
+
return []
|
|
258
|
+
except Exception:
|
|
259
|
+
return []
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def main():
|
|
263
|
+
"""CLI entry point."""
|
|
264
|
+
if len(sys.argv) < 2:
|
|
265
|
+
print("Usage:", file=sys.stderr)
|
|
266
|
+
print(" python3 git_inspector.py --overview", file=sys.stderr)
|
|
267
|
+
print(" python3 git_inspector.py --hunk [--file <path>]", file=sys.stderr)
|
|
268
|
+
sys.exit(1)
|
|
269
|
+
|
|
270
|
+
cwd = os.getcwd()
|
|
271
|
+
|
|
272
|
+
if sys.argv[1] == "--overview":
|
|
273
|
+
# Return status + log
|
|
274
|
+
status = git_status(cwd)
|
|
275
|
+
log = git_log(cwd)
|
|
276
|
+
result = {
|
|
277
|
+
"status": status,
|
|
278
|
+
"log": log
|
|
279
|
+
}
|
|
280
|
+
print(json.dumps(result, indent=2))
|
|
281
|
+
|
|
282
|
+
elif sys.argv[1] == "--hunk":
|
|
283
|
+
# Return hunk diff
|
|
284
|
+
file_path = None
|
|
285
|
+
if len(sys.argv) >= 4 and sys.argv[2] == "--file":
|
|
286
|
+
file_path = sys.argv[3]
|
|
287
|
+
|
|
288
|
+
hunks = git_hunk(cwd, file_path)
|
|
289
|
+
result = {"hunks": hunks}
|
|
290
|
+
print(json.dumps(result, indent=2))
|
|
291
|
+
|
|
292
|
+
else:
|
|
293
|
+
print(f"Unknown command: {sys.argv[1]}", file=sys.stderr)
|
|
294
|
+
sys.exit(1)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__":
|
|
298
|
+
main()
|