code-meter 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {code_meter-0.3.0 → code_meter-0.4.0}/PKG-INFO +3 -2
- {code_meter-0.3.0 → code_meter-0.4.0}/README.md +2 -1
- {code_meter-0.3.0 → code_meter-0.4.0}/pyproject.toml +1 -1
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/cli.py +23 -3
- code_meter-0.4.0/src/code_meter/ui/web/__init__.py +5 -0
- code_meter-0.4.0/src/code_meter/ui/web/server.py +194 -0
- code_meter-0.4.0/src/code_meter/ui/web/static.py +497 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter.egg-info/PKG-INFO +3 -2
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter.egg-info/SOURCES.txt +3 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/setup.cfg +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/setup.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/analytics/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/analytics/costs.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/analytics/reports.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/analytics/tokens.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/config.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/models/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/models/project.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/models/usage.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/pricing/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/pricing/anthropic.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/pricing/engine.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/pricing/google.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/pricing/live.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/pricing/openai.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/providers/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/providers/antigravity.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/providers/base.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/providers/claude_code.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/providers/codex.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/storage/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/storage/database.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/storage/repository.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/ui/__init__.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/ui/dashboard.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/ui/tables.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter/watcher.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter.egg-info/dependency_links.txt +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter.egg-info/entry_points.txt +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter.egg-info/requires.txt +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/src/code_meter.egg-info/top_level.txt +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/tests/test_analytics.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/tests/test_database.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/tests/test_pricing.py +0 -0
- {code_meter-0.3.0 → code_meter-0.4.0}/tests/test_scanner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-meter
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Local AI coding token usage tracker and cost estimator for Claude Code and future agents.
|
|
5
5
|
Author-email: Dhanush Nayak <dhanushnayak.ram@mail.com>
|
|
6
6
|
License: MIT
|
|
@@ -106,7 +106,8 @@ This launches the interactive Rich terminal dashboard displaying key metrics, mo
|
|
|
106
106
|
| Command | Description |
|
|
107
107
|
| :--- | :--- |
|
|
108
108
|
| `code-meter` | Open main interactive Rich terminal dashboard UI |
|
|
109
|
-
| `code-meter ui` | Launch live
|
|
109
|
+
| `code-meter ui` | Launch live Web Dashboard on **http://localhost:5005** (auto-opens browser) |
|
|
110
|
+
| `code-meter ui --terminal` | Launch live interactive Rich terminal UI |
|
|
110
111
|
| `code-meter watch` | Live watch mode (auto-refreshes UI as AI agents write logs) |
|
|
111
112
|
| `code-meter update-pricing` | Fetch latest real-time token pricing for 2,800+ models |
|
|
112
113
|
| `code-meter scan` | Force an incremental scan of local usage directories |
|
|
@@ -84,7 +84,8 @@ This launches the interactive Rich terminal dashboard displaying key metrics, mo
|
|
|
84
84
|
| Command | Description |
|
|
85
85
|
| :--- | :--- |
|
|
86
86
|
| `code-meter` | Open main interactive Rich terminal dashboard UI |
|
|
87
|
-
| `code-meter ui` | Launch live
|
|
87
|
+
| `code-meter ui` | Launch live Web Dashboard on **http://localhost:5005** (auto-opens browser) |
|
|
88
|
+
| `code-meter ui --terminal` | Launch live interactive Rich terminal UI |
|
|
88
89
|
| `code-meter watch` | Live watch mode (auto-refreshes UI as AI agents write logs) |
|
|
89
90
|
| `code-meter update-pricing` | Fetch latest real-time token pricing for 2,800+ models |
|
|
90
91
|
| `code-meter scan` | Force an incremental scan of local usage directories |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-meter"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Local AI coding token usage tracker and cost estimator for Claude Code and future agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Dhanush Nayak", email = "dhanushnayak.ram@mail.com" }]
|
|
@@ -423,9 +423,29 @@ def update_pricing_cmd():
|
|
|
423
423
|
|
|
424
424
|
|
|
425
425
|
@app.command("ui")
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
426
|
+
@app.command("dashboard")
|
|
427
|
+
@app.command("web")
|
|
428
|
+
def ui_cmd(
|
|
429
|
+
port: int = typer.Option(5005, "--port", "-p", help="Port number for local dashboard server"),
|
|
430
|
+
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host address for dashboard server"),
|
|
431
|
+
terminal: bool = typer.Option(False, "--terminal", "-t", help="Launch Rich terminal UI instead of Web UI"),
|
|
432
|
+
no_browser: bool = typer.Option(False, "--no-browser", help="Do not open web browser automatically"),
|
|
433
|
+
):
|
|
434
|
+
"""Launch local web dashboard UI on http://localhost:5005 (or terminal UI with --terminal)."""
|
|
435
|
+
if terminal:
|
|
436
|
+
_launch_live_ui()
|
|
437
|
+
else:
|
|
438
|
+
from code_meter.ui.web.server import start_web_server
|
|
439
|
+
config, repo, pricing_engine, _, report_gen = _get_services()
|
|
440
|
+
start_web_server(
|
|
441
|
+
host=host,
|
|
442
|
+
port=port,
|
|
443
|
+
open_browser=not no_browser,
|
|
444
|
+
config=config,
|
|
445
|
+
repo=repo,
|
|
446
|
+
pricing_engine=pricing_engine,
|
|
447
|
+
report_gen=report_gen,
|
|
448
|
+
)
|
|
429
449
|
|
|
430
450
|
|
|
431
451
|
@app.command("watch")
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Lightweight Web Dashboard HTTP Server running on http://localhost:5005 for code-meter."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import webbrowser
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from code_meter.analytics.reports import ReportGenerator
|
|
13
|
+
from code_meter.pricing.engine import PricingEngine
|
|
14
|
+
from code_meter.providers.antigravity import AntigravityProvider
|
|
15
|
+
from code_meter.providers.claude_code import ClaudeCodeProvider
|
|
16
|
+
from code_meter.providers.codex import CodexProvider
|
|
17
|
+
from code_meter.storage.repository import UsageRepository
|
|
18
|
+
from code_meter.ui.web.static import HTML_DASHBOARD
|
|
19
|
+
from code_meter.watcher import run_watch_loop
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_web_handler(repo: UsageRepository, pricing_engine: PricingEngine, report_gen: ReportGenerator):
|
|
25
|
+
"""Factory creating HTTPRequestHandler with injected service dependencies."""
|
|
26
|
+
|
|
27
|
+
class CodeMeterWebHandler(BaseHTTPRequestHandler):
|
|
28
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
29
|
+
# Suppress default noisy HTTP request logs
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def do_GET(self) -> None:
|
|
33
|
+
path = self.path.split("?")[0]
|
|
34
|
+
|
|
35
|
+
if path in ("/", "/index.html"):
|
|
36
|
+
self.send_response(200)
|
|
37
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
38
|
+
self.end_headers()
|
|
39
|
+
self.wfile.write(HTML_DASHBOARD.encode("utf-8"))
|
|
40
|
+
|
|
41
|
+
elif path == "/api/summary":
|
|
42
|
+
self.handle_api_summary()
|
|
43
|
+
|
|
44
|
+
elif path == "/api/prompts":
|
|
45
|
+
self.handle_api_prompts()
|
|
46
|
+
|
|
47
|
+
elif path == "/api/changes":
|
|
48
|
+
self.handle_api_changes()
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
self.send_error(404, "Not Found")
|
|
52
|
+
|
|
53
|
+
def handle_api_summary(self) -> None:
|
|
54
|
+
records = repo.query_usage_events()
|
|
55
|
+
prompts = repo.query_prompts(limit=15)
|
|
56
|
+
code_changes = repo.query_code_changes(limit=15)
|
|
57
|
+
|
|
58
|
+
costs = [
|
|
59
|
+
pricing_engine.calculate_cost(
|
|
60
|
+
r.provider, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, r.cache_write_tokens, r.timestamp
|
|
61
|
+
)
|
|
62
|
+
for r in records
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
summary = report_gen.generate_summary(records, filter_date_str=datetime.now(timezone.utc).strftime("%Y-%m-%d"))
|
|
66
|
+
by_model = report_gen.aggregate_by_model(records, costs)
|
|
67
|
+
by_project = report_gen.aggregate_by_project(records, costs)
|
|
68
|
+
|
|
69
|
+
payload = {
|
|
70
|
+
"summary": {
|
|
71
|
+
"total_requests": summary.total_requests,
|
|
72
|
+
"total_input_tokens": summary.input_tokens,
|
|
73
|
+
"total_output_tokens": summary.output_tokens,
|
|
74
|
+
"total_cache_read_tokens": summary.cache_read_tokens,
|
|
75
|
+
"total_cache_write_tokens": summary.cache_write_tokens,
|
|
76
|
+
"total_tokens": summary.total_tokens,
|
|
77
|
+
"total_cost": summary.estimated_cost_usd or 0.0,
|
|
78
|
+
},
|
|
79
|
+
"by_model": [
|
|
80
|
+
{
|
|
81
|
+
"model": m.model,
|
|
82
|
+
"requests": m.request_count,
|
|
83
|
+
"total_tokens": m.total_tokens,
|
|
84
|
+
"total_cost": m.estimated_cost_usd or 0.0,
|
|
85
|
+
}
|
|
86
|
+
for m in by_model
|
|
87
|
+
],
|
|
88
|
+
"by_project": [
|
|
89
|
+
{
|
|
90
|
+
"project_name": p.project_name,
|
|
91
|
+
"requests": p.request_count,
|
|
92
|
+
"total_tokens": p.total_tokens,
|
|
93
|
+
"total_cost": p.estimated_cost_usd or 0.0,
|
|
94
|
+
}
|
|
95
|
+
for p in by_project
|
|
96
|
+
],
|
|
97
|
+
"prompts": [
|
|
98
|
+
{
|
|
99
|
+
"timestamp": p.timestamp,
|
|
100
|
+
"provider": p.provider,
|
|
101
|
+
"prompt_text": p.prompt_text,
|
|
102
|
+
"project_id": p.project_id or "",
|
|
103
|
+
}
|
|
104
|
+
for p in prompts
|
|
105
|
+
],
|
|
106
|
+
"code_changes": [
|
|
107
|
+
{
|
|
108
|
+
"timestamp": c.timestamp,
|
|
109
|
+
"provider": c.provider,
|
|
110
|
+
"file_path": c.file_path,
|
|
111
|
+
"change_type": c.change_type,
|
|
112
|
+
"diff_summary": c.diff_summary,
|
|
113
|
+
}
|
|
114
|
+
for c in code_changes
|
|
115
|
+
],
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
self.send_json_response(payload)
|
|
119
|
+
|
|
120
|
+
def handle_api_prompts(self) -> None:
|
|
121
|
+
prompts = repo.query_prompts(limit=50)
|
|
122
|
+
self.send_json_response({"prompts": [p.model_dump() for p in prompts]})
|
|
123
|
+
|
|
124
|
+
def handle_api_changes(self) -> None:
|
|
125
|
+
changes = repo.query_code_changes(limit=50)
|
|
126
|
+
self.send_json_response({"code_changes": [c.model_dump() for c in changes]})
|
|
127
|
+
|
|
128
|
+
def send_json_response(self, data: Dict[str, Any]) -> None:
|
|
129
|
+
body = json.dumps(data, indent=2, default=str).encode("utf-8")
|
|
130
|
+
self.send_response(200)
|
|
131
|
+
self.send_header("Content-Type", "application/json")
|
|
132
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
133
|
+
self.end_headers()
|
|
134
|
+
self.wfile.write(body)
|
|
135
|
+
|
|
136
|
+
return CodeMeterWebHandler
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def start_web_server(
|
|
140
|
+
host: str = "127.0.0.1",
|
|
141
|
+
port: int = 5005,
|
|
142
|
+
open_browser: bool = True,
|
|
143
|
+
config: Optional[Any] = None,
|
|
144
|
+
repo: Optional[UsageRepository] = None,
|
|
145
|
+
pricing_engine: Optional[PricingEngine] = None,
|
|
146
|
+
report_gen: Optional[ReportGenerator] = None,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Launch HTTP server on specified host/port serving localhost web dashboard."""
|
|
149
|
+
if not (repo and pricing_engine and report_gen and config):
|
|
150
|
+
from code_meter.cli import _get_services
|
|
151
|
+
config, repo, pricing_engine, providers, report_gen = _get_services()
|
|
152
|
+
else:
|
|
153
|
+
providers = [
|
|
154
|
+
ClaudeCodeProvider(config.get_claude_dir()),
|
|
155
|
+
CodexProvider(config.get_codex_dir()),
|
|
156
|
+
AntigravityProvider(config.get_antigravity_dir()),
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
# Background scanner loop
|
|
160
|
+
def background_scan_and_watch():
|
|
161
|
+
def do_scan():
|
|
162
|
+
states = repo.get_scan_states()
|
|
163
|
+
for p in providers:
|
|
164
|
+
res = p.scan(states)
|
|
165
|
+
repo.save_records(res.records)
|
|
166
|
+
repo.save_prompts(res.prompts)
|
|
167
|
+
repo.save_code_changes(res.code_changes)
|
|
168
|
+
repo.update_scan_states(res.updated_states)
|
|
169
|
+
|
|
170
|
+
target_dirs = [config.get_claude_dir(), config.get_codex_dir(), config.get_antigravity_dir()]
|
|
171
|
+
run_watch_loop(target_dir=target_dirs, on_change_callback=do_scan, refresh_seconds=config.get_refresh_seconds())
|
|
172
|
+
|
|
173
|
+
watch_thread = threading.Thread(target=background_scan_and_watch, daemon=True)
|
|
174
|
+
watch_thread.start()
|
|
175
|
+
|
|
176
|
+
handler_cls = create_web_handler(repo, pricing_engine, report_gen)
|
|
177
|
+
server = ThreadingHTTPServer((host, port), handler_cls)
|
|
178
|
+
|
|
179
|
+
url = f"http://localhost:{port}"
|
|
180
|
+
print(f"\n[code-meter] Web Dashboard running at: {url}")
|
|
181
|
+
print("Press Ctrl+C to stop the web server.\n")
|
|
182
|
+
|
|
183
|
+
if open_browser:
|
|
184
|
+
def open_browser_later():
|
|
185
|
+
time.sleep(0.5)
|
|
186
|
+
webbrowser.open(url)
|
|
187
|
+
threading.Thread(target=open_browser_later, daemon=True).start()
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
server.serve_forever()
|
|
191
|
+
except KeyboardInterrupt:
|
|
192
|
+
print("\nShutting down web dashboard server...")
|
|
193
|
+
server.shutdown()
|
|
194
|
+
server.server_close()
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
"""Embedded Single-Page Web Dashboard HTML/CSS/JS template for code-meter on http://localhost:5005."""
|
|
2
|
+
|
|
3
|
+
HTML_DASHBOARD = """<!DOCTYPE html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<title>code-meter | AI Token & Cost Dashboard</title>
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg-dark: #0f172a;
|
|
12
|
+
--card-bg: rgba(30, 41, 59, 0.7);
|
|
13
|
+
--border-color: rgba(255, 255, 255, 0.1);
|
|
14
|
+
--text-main: #f8fafc;
|
|
15
|
+
--text-muted: #94a3b8;
|
|
16
|
+
--accent-blue: #38bdf8;
|
|
17
|
+
--accent-emerald: #34d399;
|
|
18
|
+
--accent-purple: #c084fc;
|
|
19
|
+
--accent-amber: #fbbf24;
|
|
20
|
+
--shadow-glow: 0 10px 25px -5px rgba(56, 189, 248, 0.15);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
* {
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 0;
|
|
27
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
body {
|
|
31
|
+
background-color: var(--bg-dark);
|
|
32
|
+
color: var(--text-main);
|
|
33
|
+
padding: 24px;
|
|
34
|
+
min-height: 100vh;
|
|
35
|
+
background-image:
|
|
36
|
+
radial-gradient(at 0% 0%, rgba(56, 189, 248, 0.08) 0px, transparent 50%),
|
|
37
|
+
radial-gradient(at 100% 100%, rgba(192, 132, 252, 0.08) 0px, transparent 50%);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.container {
|
|
41
|
+
max-width: 1300px;
|
|
42
|
+
margin: 0 auto;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
header {
|
|
46
|
+
display: flex;
|
|
47
|
+
justify-content: space-between;
|
|
48
|
+
align-items: center;
|
|
49
|
+
margin-bottom: 28px;
|
|
50
|
+
padding-bottom: 16px;
|
|
51
|
+
border-bottom: 1px solid var(--border-color);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.brand {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 12px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.logo-badge {
|
|
61
|
+
background: linear-gradient(135deg, #0284c7, #7e22ce);
|
|
62
|
+
width: 42px;
|
|
63
|
+
height: 42px;
|
|
64
|
+
border-radius: 10px;
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
font-weight: 800;
|
|
69
|
+
font-size: 20px;
|
|
70
|
+
box-shadow: 0 4px 12px rgba(126, 34, 206, 0.3);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
h1 {
|
|
74
|
+
font-size: 24px;
|
|
75
|
+
font-weight: 700;
|
|
76
|
+
letter-spacing: -0.5px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.subtitle {
|
|
80
|
+
font-size: 13px;
|
|
81
|
+
color: var(--text-muted);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.live-status {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 8px;
|
|
88
|
+
background: rgba(52, 211, 153, 0.1);
|
|
89
|
+
border: 1px solid rgba(52, 211, 153, 0.2);
|
|
90
|
+
padding: 6px 14px;
|
|
91
|
+
border-radius: 20px;
|
|
92
|
+
font-size: 13px;
|
|
93
|
+
color: var(--accent-emerald);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.pulse {
|
|
97
|
+
width: 8px;
|
|
98
|
+
height: 8px;
|
|
99
|
+
background-color: var(--accent-emerald);
|
|
100
|
+
border-radius: 50%;
|
|
101
|
+
box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.7);
|
|
102
|
+
animation: pulse-ring 2s infinite;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@keyframes pulse-ring {
|
|
106
|
+
0% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.7); }
|
|
107
|
+
70% { box-shadow: 0 0 0 10px rgba(52, 211, 153, 0); }
|
|
108
|
+
100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Metrics Grid */
|
|
112
|
+
.metrics-grid {
|
|
113
|
+
display: grid;
|
|
114
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
115
|
+
gap: 16px;
|
|
116
|
+
margin-bottom: 28px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.metric-card {
|
|
120
|
+
background: var(--card-bg);
|
|
121
|
+
backdrop-filter: blur(12px);
|
|
122
|
+
border: 1px solid var(--border-color);
|
|
123
|
+
border-radius: 14px;
|
|
124
|
+
padding: 20px;
|
|
125
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.metric-card:hover {
|
|
129
|
+
transform: translateY(-2px);
|
|
130
|
+
box-shadow: var(--shadow-glow);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.metric-label {
|
|
134
|
+
font-size: 12px;
|
|
135
|
+
text-transform: uppercase;
|
|
136
|
+
letter-spacing: 0.5px;
|
|
137
|
+
color: var(--text-muted);
|
|
138
|
+
margin-bottom: 8px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.metric-value {
|
|
142
|
+
font-size: 26px;
|
|
143
|
+
font-weight: 700;
|
|
144
|
+
letter-spacing: -0.5px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.metric-value.highlight {
|
|
148
|
+
color: var(--accent-emerald);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Dashboard Layout */
|
|
152
|
+
.dashboard-row {
|
|
153
|
+
display: grid;
|
|
154
|
+
grid-template-columns: 1fr 1fr;
|
|
155
|
+
gap: 24px;
|
|
156
|
+
margin-bottom: 28px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@media (max-width: 900px) {
|
|
160
|
+
.dashboard-row {
|
|
161
|
+
grid-template-columns: 1fr;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.panel {
|
|
166
|
+
background: var(--card-bg);
|
|
167
|
+
backdrop-filter: blur(12px);
|
|
168
|
+
border: 1px solid var(--border-color);
|
|
169
|
+
border-radius: 16px;
|
|
170
|
+
padding: 24px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.panel-header {
|
|
174
|
+
display: flex;
|
|
175
|
+
justify-content: space-between;
|
|
176
|
+
align-items: center;
|
|
177
|
+
margin-bottom: 18px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.panel-title {
|
|
181
|
+
font-size: 16px;
|
|
182
|
+
font-weight: 600;
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: 8px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Tables */
|
|
189
|
+
table {
|
|
190
|
+
width: 100%;
|
|
191
|
+
border-collapse: collapse;
|
|
192
|
+
font-size: 14px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
th {
|
|
196
|
+
text-align: left;
|
|
197
|
+
padding: 10px 12px;
|
|
198
|
+
color: var(--text-muted);
|
|
199
|
+
font-weight: 500;
|
|
200
|
+
border-bottom: 1px solid var(--border-color);
|
|
201
|
+
font-size: 12px;
|
|
202
|
+
text-transform: uppercase;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
td {
|
|
206
|
+
padding: 12px;
|
|
207
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
tr:last-child td {
|
|
211
|
+
border-bottom: none;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.badge {
|
|
215
|
+
display: inline-block;
|
|
216
|
+
padding: 3px 8px;
|
|
217
|
+
border-radius: 6px;
|
|
218
|
+
font-size: 11px;
|
|
219
|
+
font-weight: 600;
|
|
220
|
+
text-transform: uppercase;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.badge-claude { background: rgba(249, 115, 22, 0.15); color: #fb923c; border: 1px solid rgba(249, 115, 22, 0.3); }
|
|
224
|
+
.badge-codex { background: rgba(56, 189, 248, 0.15); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.3); }
|
|
225
|
+
.badge-antigravity { background: rgba(192, 132, 252, 0.15); color: #c084fc; border: 1px solid rgba(192, 132, 252, 0.3); }
|
|
226
|
+
|
|
227
|
+
.prompt-text {
|
|
228
|
+
font-family: monospace;
|
|
229
|
+
font-size: 13px;
|
|
230
|
+
color: #e2e8f0;
|
|
231
|
+
word-break: break-word;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.file-path {
|
|
235
|
+
font-family: monospace;
|
|
236
|
+
font-size: 12px;
|
|
237
|
+
color: var(--accent-blue);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.diff-summary {
|
|
241
|
+
font-family: monospace;
|
|
242
|
+
font-size: 12px;
|
|
243
|
+
color: var(--accent-emerald);
|
|
244
|
+
white-space: pre-wrap;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
footer {
|
|
248
|
+
text-align: center;
|
|
249
|
+
margin-top: 40px;
|
|
250
|
+
padding-top: 20px;
|
|
251
|
+
border-top: 1px solid var(--border-color);
|
|
252
|
+
font-size: 13px;
|
|
253
|
+
color: var(--text-muted);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
footer a {
|
|
257
|
+
color: var(--accent-blue);
|
|
258
|
+
text-decoration: none;
|
|
259
|
+
}
|
|
260
|
+
</style>
|
|
261
|
+
</head>
|
|
262
|
+
<body>
|
|
263
|
+
<div class="container">
|
|
264
|
+
<header>
|
|
265
|
+
<div class="brand">
|
|
266
|
+
<div class="logo-badge">⚡</div>
|
|
267
|
+
<div>
|
|
268
|
+
<h1>code-meter</h1>
|
|
269
|
+
<div class="subtitle">Local AI Coding Token & Cost Dashboard</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
<div class="live-status">
|
|
273
|
+
<div class="pulse"></div>
|
|
274
|
+
<span>Live Dashboard (localhost:5005)</span>
|
|
275
|
+
</div>
|
|
276
|
+
</header>
|
|
277
|
+
|
|
278
|
+
<!-- Metrics Cards -->
|
|
279
|
+
<div class="metrics-grid">
|
|
280
|
+
<div class="metric-card">
|
|
281
|
+
<div class="metric-label">Total Requests</div>
|
|
282
|
+
<div class="metric-value" id="val-requests">0</div>
|
|
283
|
+
</div>
|
|
284
|
+
<div class="metric-card">
|
|
285
|
+
<div class="metric-label">Input Tokens</div>
|
|
286
|
+
<div class="metric-value" id="val-input">0</div>
|
|
287
|
+
</div>
|
|
288
|
+
<div class="metric-card">
|
|
289
|
+
<div class="metric-label">Output Tokens</div>
|
|
290
|
+
<div class="metric-value" id="val-output">0</div>
|
|
291
|
+
</div>
|
|
292
|
+
<div class="metric-card">
|
|
293
|
+
<div class="metric-label">Cache Read / Write</div>
|
|
294
|
+
<div class="metric-value" id="val-cache">0</div>
|
|
295
|
+
</div>
|
|
296
|
+
<div class="metric-card">
|
|
297
|
+
<div class="metric-label">Total Cost</div>
|
|
298
|
+
<div class="metric-value highlight" id="val-cost">$0.00</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<!-- Row 1: Model & Project Breakdown -->
|
|
303
|
+
<div class="dashboard-row">
|
|
304
|
+
<div class="panel">
|
|
305
|
+
<div class="panel-header">
|
|
306
|
+
<div class="panel-title">🤖 Model Breakdown & Costs</div>
|
|
307
|
+
</div>
|
|
308
|
+
<table>
|
|
309
|
+
<thead>
|
|
310
|
+
<tr>
|
|
311
|
+
<th>Model</th>
|
|
312
|
+
<th>Provider</th>
|
|
313
|
+
<th>Tokens</th>
|
|
314
|
+
<th>Est. Cost</th>
|
|
315
|
+
</tr>
|
|
316
|
+
</thead>
|
|
317
|
+
<tbody id="tbl-models">
|
|
318
|
+
<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">Loading data...</td></tr>
|
|
319
|
+
</tbody>
|
|
320
|
+
</table>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div class="panel">
|
|
324
|
+
<div class="panel-header">
|
|
325
|
+
<div class="panel-title">📁 Project Usage Breakdown</div>
|
|
326
|
+
</div>
|
|
327
|
+
<table>
|
|
328
|
+
<thead>
|
|
329
|
+
<tr>
|
|
330
|
+
<th>Project</th>
|
|
331
|
+
<th>Requests</th>
|
|
332
|
+
<th>Tokens</th>
|
|
333
|
+
<th>Est. Cost</th>
|
|
334
|
+
</tr>
|
|
335
|
+
</thead>
|
|
336
|
+
<tbody id="tbl-projects">
|
|
337
|
+
<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">Loading data...</td></tr>
|
|
338
|
+
</tbody>
|
|
339
|
+
</table>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<!-- Row 2: User Prompts & Code Edits -->
|
|
344
|
+
<div class="dashboard-row">
|
|
345
|
+
<div class="panel">
|
|
346
|
+
<div class="panel-header">
|
|
347
|
+
<div class="panel-title">💬 Recent User Queries & Prompts</div>
|
|
348
|
+
</div>
|
|
349
|
+
<table>
|
|
350
|
+
<thead>
|
|
351
|
+
<tr>
|
|
352
|
+
<th>Time</th>
|
|
353
|
+
<th>Provider</th>
|
|
354
|
+
<th>Prompt / Query</th>
|
|
355
|
+
</tr>
|
|
356
|
+
</thead>
|
|
357
|
+
<tbody id="tbl-prompts">
|
|
358
|
+
<tr><td colspan="3" style="text-align:center; color: var(--text-muted);">Loading prompts...</td></tr>
|
|
359
|
+
</tbody>
|
|
360
|
+
</table>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<div class="panel">
|
|
364
|
+
<div class="panel-header">
|
|
365
|
+
<div class="panel-title">🛠️ Output Code Edits & Actions</div>
|
|
366
|
+
</div>
|
|
367
|
+
<table>
|
|
368
|
+
<thead>
|
|
369
|
+
<tr>
|
|
370
|
+
<th>Time</th>
|
|
371
|
+
<th>Action</th>
|
|
372
|
+
<th>File & Diff Summary</th>
|
|
373
|
+
</tr>
|
|
374
|
+
</thead>
|
|
375
|
+
<tbody id="tbl-changes">
|
|
376
|
+
<tr><td colspan="3" style="text-align:center; color: var(--text-muted);">Loading code edits...</td></tr>
|
|
377
|
+
</tbody>
|
|
378
|
+
</table>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
<footer>
|
|
383
|
+
code-meter v0.3.0 | Running on <strong>http://localhost:5005</strong> | <a href="https://github.com/dhanushnayak/code-meter" target="_blank">GitHub Repository</a>
|
|
384
|
+
</footer>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<script>
|
|
388
|
+
function formatNumber(num) {
|
|
389
|
+
if (num >= 1000000) return (num / 1000000).toFixed(2) + 'M';
|
|
390
|
+
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
|
|
391
|
+
return num.toLocaleString();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function formatTime(isoStr) {
|
|
395
|
+
if (!isoStr) return '';
|
|
396
|
+
try {
|
|
397
|
+
const d = new Date(isoStr);
|
|
398
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
399
|
+
} catch (e) {
|
|
400
|
+
return isoStr.substring(11, 16);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function getBadgeClass(provider) {
|
|
405
|
+
const p = (provider || '').toLowerCase();
|
|
406
|
+
if (p.includes('claude')) return 'badge-claude';
|
|
407
|
+
if (p.includes('codex')) return 'badge-codex';
|
|
408
|
+
if (p.includes('antigravity')) return 'badge-antigravity';
|
|
409
|
+
return 'badge-codex';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function updateDashboard() {
|
|
413
|
+
try {
|
|
414
|
+
const res = await fetch('/api/summary');
|
|
415
|
+
const data = await res.json();
|
|
416
|
+
|
|
417
|
+
// Metrics
|
|
418
|
+
const s = data.summary || {};
|
|
419
|
+
document.getElementById('val-requests').textContent = formatNumber(s.total_requests || 0);
|
|
420
|
+
document.getElementById('val-input').textContent = formatNumber(s.total_input_tokens || 0);
|
|
421
|
+
document.getElementById('val-output').textContent = formatNumber(s.total_output_tokens || 0);
|
|
422
|
+
document.getElementById('val-cache').textContent = formatNumber((s.total_cache_read_tokens || 0) + (s.total_cache_write_tokens || 0));
|
|
423
|
+
document.getElementById('val-cost').textContent = '$' + (s.total_cost || 0).toFixed(4);
|
|
424
|
+
|
|
425
|
+
// Models Table
|
|
426
|
+
const tblModels = document.getElementById('tbl-models');
|
|
427
|
+
if (data.by_model && data.by_model.length > 0) {
|
|
428
|
+
tblModels.innerHTML = data.by_model.map(m => `
|
|
429
|
+
<tr>
|
|
430
|
+
<td><strong>${m.model}</strong></td>
|
|
431
|
+
<td><span class="badge ${getBadgeClass(m.provider)}">${m.provider}</span></td>
|
|
432
|
+
<td>${formatNumber(m.total_tokens)}</td>
|
|
433
|
+
<td><strong>$${(m.total_cost || 0).toFixed(4)}</strong></td>
|
|
434
|
+
</tr>
|
|
435
|
+
`).join('');
|
|
436
|
+
} else {
|
|
437
|
+
tblModels.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">No model data recorded yet</td></tr>';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Projects Table
|
|
441
|
+
const tblProjects = document.getElementById('tbl-projects');
|
|
442
|
+
if (data.by_project && data.by_project.length > 0) {
|
|
443
|
+
tblProjects.innerHTML = data.by_project.map(p => `
|
|
444
|
+
<tr>
|
|
445
|
+
<td><span class="file-path">${p.project_id || 'default'}</span></td>
|
|
446
|
+
<td>${formatNumber(p.requests)}</td>
|
|
447
|
+
<td>${formatNumber(p.total_tokens)}</td>
|
|
448
|
+
<td><strong>$${(p.total_cost || 0).toFixed(4)}</strong></td>
|
|
449
|
+
</tr>
|
|
450
|
+
`).join('');
|
|
451
|
+
} else {
|
|
452
|
+
tblProjects.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-muted);">No project data recorded yet</td></tr>';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Prompts Table
|
|
456
|
+
const tblPrompts = document.getElementById('tbl-prompts');
|
|
457
|
+
if (data.prompts && data.prompts.length > 0) {
|
|
458
|
+
tblPrompts.innerHTML = data.prompts.map(p => `
|
|
459
|
+
<tr>
|
|
460
|
+
<td style="white-space:nowrap; color:var(--text-muted);">${formatTime(p.timestamp)}</td>
|
|
461
|
+
<td><span class="badge ${getBadgeClass(p.provider)}">${p.provider}</span></td>
|
|
462
|
+
<td><div class="prompt-text">${p.prompt_text}</div></td>
|
|
463
|
+
</tr>
|
|
464
|
+
`).join('');
|
|
465
|
+
} else {
|
|
466
|
+
tblPrompts.innerHTML = '<tr><td colspan="3" style="text-align:center; color: var(--text-muted);">No prompt history recorded yet</td></tr>';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Code Changes Table
|
|
470
|
+
const tblChanges = document.getElementById('tbl-changes');
|
|
471
|
+
if (data.code_changes && data.code_changes.length > 0) {
|
|
472
|
+
tblChanges.innerHTML = data.code_changes.map(c => `
|
|
473
|
+
<tr>
|
|
474
|
+
<td style="white-space:nowrap; color:var(--text-muted);">${formatTime(c.timestamp)}</td>
|
|
475
|
+
<td><span class="badge badge-emerald">${c.change_type}</span></td>
|
|
476
|
+
<td>
|
|
477
|
+
<div class="file-path">${c.file_path}</div>
|
|
478
|
+
<div class="diff-summary">${c.diff_summary}</div>
|
|
479
|
+
</td>
|
|
480
|
+
</tr>
|
|
481
|
+
`).join('');
|
|
482
|
+
} else {
|
|
483
|
+
tblChanges.innerHTML = '<tr><td colspan="3" style="text-align:center; color: var(--text-muted);">No code edits recorded yet</td></tr>';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
} catch (err) {
|
|
487
|
+
console.error('Error fetching dashboard update:', err);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Initial update and auto-poll every 2 seconds
|
|
492
|
+
updateDashboard();
|
|
493
|
+
setInterval(updateDashboard, 2000);
|
|
494
|
+
</script>
|
|
495
|
+
</body>
|
|
496
|
+
</html>
|
|
497
|
+
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-meter
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Local AI coding token usage tracker and cost estimator for Claude Code and future agents.
|
|
5
5
|
Author-email: Dhanush Nayak <dhanushnayak.ram@mail.com>
|
|
6
6
|
License: MIT
|
|
@@ -106,7 +106,8 @@ This launches the interactive Rich terminal dashboard displaying key metrics, mo
|
|
|
106
106
|
| Command | Description |
|
|
107
107
|
| :--- | :--- |
|
|
108
108
|
| `code-meter` | Open main interactive Rich terminal dashboard UI |
|
|
109
|
-
| `code-meter ui` | Launch live
|
|
109
|
+
| `code-meter ui` | Launch live Web Dashboard on **http://localhost:5005** (auto-opens browser) |
|
|
110
|
+
| `code-meter ui --terminal` | Launch live interactive Rich terminal UI |
|
|
110
111
|
| `code-meter watch` | Live watch mode (auto-refreshes UI as AI agents write logs) |
|
|
111
112
|
| `code-meter update-pricing` | Fetch latest real-time token pricing for 2,800+ models |
|
|
112
113
|
| `code-meter scan` | Force an incremental scan of local usage directories |
|
|
@@ -35,6 +35,9 @@ src/code_meter/storage/repository.py
|
|
|
35
35
|
src/code_meter/ui/__init__.py
|
|
36
36
|
src/code_meter/ui/dashboard.py
|
|
37
37
|
src/code_meter/ui/tables.py
|
|
38
|
+
src/code_meter/ui/web/__init__.py
|
|
39
|
+
src/code_meter/ui/web/server.py
|
|
40
|
+
src/code_meter/ui/web/static.py
|
|
38
41
|
tests/test_analytics.py
|
|
39
42
|
tests/test_database.py
|
|
40
43
|
tests/test_pricing.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|