costcut 0.0.9__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.
- costcut-0.0.9/.gitignore +56 -0
- costcut-0.0.9/PKG-INFO +132 -0
- costcut-0.0.9/README.md +108 -0
- costcut-0.0.9/pyproject.toml +51 -0
- costcut-0.0.9/src/cost_guard/__init__.py +1 -0
- costcut-0.0.9/src/cost_guard/auth.py +75 -0
- costcut-0.0.9/src/cost_guard/cli.py +139 -0
- costcut-0.0.9/src/cost_guard/init_cmd.py +194 -0
- costcut-0.0.9/src/cost_guard/parser.py +64 -0
- costcut-0.0.9/src/cost_guard/pricing.py +90 -0
- costcut-0.0.9/src/cost_guard/push.py +114 -0
- costcut-0.0.9/src/cost_guard/report.py +185 -0
- costcut-0.0.9/src/cost_guard/sessions.py +82 -0
- costcut-0.0.9/tests/__init__.py +0 -0
- costcut-0.0.9/tests/fixtures/golden_session.jsonl +4 -0
- costcut-0.0.9/tests/fixtures/sample_session.jsonl +6 -0
- costcut-0.0.9/tests/test_auth.py +124 -0
- costcut-0.0.9/tests/test_cli_stub.py +38 -0
- costcut-0.0.9/tests/test_golden.py +60 -0
- costcut-0.0.9/tests/test_hook_stubs.py +69 -0
- costcut-0.0.9/tests/test_init.py +228 -0
- costcut-0.0.9/tests/test_init_integration.py +154 -0
- costcut-0.0.9/tests/test_parser.py +43 -0
- costcut-0.0.9/tests/test_pricing.py +51 -0
- costcut-0.0.9/tests/test_push.py +101 -0
- costcut-0.0.9/tests/test_real_data_smoke.py +56 -0
- costcut-0.0.9/tests/test_report.py +187 -0
- costcut-0.0.9/tests/test_sessions.py +92 -0
costcut-0.0.9/.gitignore
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
.coverage
|
|
11
|
+
htmlcov/
|
|
12
|
+
dist/
|
|
13
|
+
build/
|
|
14
|
+
|
|
15
|
+
# Node
|
|
16
|
+
node_modules/
|
|
17
|
+
.next/
|
|
18
|
+
out/
|
|
19
|
+
.turbo/
|
|
20
|
+
*.tsbuildinfo
|
|
21
|
+
|
|
22
|
+
# Playwright
|
|
23
|
+
frontend/test-results/
|
|
24
|
+
frontend/playwright-report/
|
|
25
|
+
|
|
26
|
+
# IDE
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
29
|
+
*.swp
|
|
30
|
+
.DS_Store
|
|
31
|
+
|
|
32
|
+
# Env / secrets (only encrypted files are committed)
|
|
33
|
+
.env
|
|
34
|
+
.env.local
|
|
35
|
+
.env.*.local
|
|
36
|
+
!.env.example
|
|
37
|
+
!*.encrypted
|
|
38
|
+
|
|
39
|
+
# Sops decrypted artifacts
|
|
40
|
+
.decrypted/
|
|
41
|
+
|
|
42
|
+
# Logs
|
|
43
|
+
*.log
|
|
44
|
+
logs/
|
|
45
|
+
|
|
46
|
+
# Local Docker volumes
|
|
47
|
+
postgres-data/
|
|
48
|
+
loki-data/
|
|
49
|
+
grafana-data/
|
|
50
|
+
|
|
51
|
+
# Placeholders
|
|
52
|
+
__placeholder__
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# MkDocs build output
|
|
56
|
+
docs/site/
|
costcut-0.0.9/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: costcut
|
|
3
|
+
Version: 0.0.9
|
|
4
|
+
Summary: Claude Code observability + cost optimization CLI
|
|
5
|
+
Project-URL: Homepage, https://costcut.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/kuznetsov-ai/cost-guard
|
|
7
|
+
Author-email: Eugene Kuznetsov <eugene@costcut.dev>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: claude-code,cost,llm,observability,tokens
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Topic :: Software Development
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: httpx>=0.27.0
|
|
16
|
+
Requires-Dist: pydantic>=2.7.0
|
|
17
|
+
Requires-Dist: rich>=13.7.0
|
|
18
|
+
Requires-Dist: typer>=0.12.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# cost-guard CLI
|
|
26
|
+
|
|
27
|
+
OSS CLI for Cost Guard. Tracks Claude Code session tokens, cost, hook compliance.
|
|
28
|
+
|
|
29
|
+
## Install (Week 6+)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install cost-guard # PyPI (Week 6 release)
|
|
33
|
+
brew install cost-guard # Homebrew tap (Week 6 release)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Dev install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cd cli
|
|
40
|
+
python -m venv .venv && source .venv/bin/activate
|
|
41
|
+
pip install -e ".[dev]"
|
|
42
|
+
pytest
|
|
43
|
+
cost-guard --version
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
### `cost-guard init` — install hooks bundle (Week 3 ✅)
|
|
49
|
+
|
|
50
|
+
Installs 5 bundled hook scripts + shared library into `.claude/hooks/`, registers them in `.claude/settings.json`.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cost-guard init # install to .claude/hooks (default)
|
|
54
|
+
cost-guard init --target ~/.claude/hooks # explicit target path
|
|
55
|
+
cost-guard init --dry-run # preview changes, write nothing
|
|
56
|
+
cost-guard init --force # overwrite existing hooks
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Installs:
|
|
60
|
+
- **Hooks:** `parallel_agents_guard.sh`, `test_before_push_guard.sh`, `large_read_warning.sh`, `token_alert.sh`, `session_cost_check.sh`
|
|
61
|
+
- **Library:** `_lib/cost.sh` (shared utilities)
|
|
62
|
+
|
|
63
|
+
Registers hooks in `.claude/settings.json` with event + matcher + command path. Idempotent: running twice adds no duplicates.
|
|
64
|
+
|
|
65
|
+
Exit code 0 on success, 1 on error.
|
|
66
|
+
|
|
67
|
+
### `cost-guard report` — session analytics (Week 2 ✅)
|
|
68
|
+
|
|
69
|
+
Scans Claude Code JSONL transcripts in `~/.claude/projects/`, aggregates tokens + cost per session, renders a Rich table.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
cost-guard report # default: last 20 sessions for cwd
|
|
73
|
+
cost-guard report --project ~/Projects/MyApp # specific project
|
|
74
|
+
cost-guard report --since 2026-05-01 # only sessions since date
|
|
75
|
+
cost-guard report --last 5 # top 5 most recent
|
|
76
|
+
cost-guard report --all # no limit
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Output columns: `Started | Duration | Model | Msgs | In | Out | Cache | Cost | Session`. Final row = TOTAL.
|
|
80
|
+
|
|
81
|
+
### `cost-guard auth` — token management (Week 7 ✅)
|
|
82
|
+
|
|
83
|
+
Manages authentication with backend API.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cost-guard auth set <api_key> # save API key (must start with cg_live_)
|
|
87
|
+
cost-guard auth status # check login status + show masked token
|
|
88
|
+
cost-guard auth logout # remove stored token
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Token storage: `~/.config/cost-guard/token` (0o600 permissions). Env override: `COST_GUARD_TOKEN=cg_live_...` takes precedence. Backend: `GET /v1/auth/me` (Bearer JWT).
|
|
92
|
+
|
|
93
|
+
### `cost-guard push` — upload sessions to backend (Week 7 ✅)
|
|
94
|
+
|
|
95
|
+
Aggregates sessions from local JSONL and POSTs to backend API.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
cost-guard push # default: last 30 days
|
|
99
|
+
cost-guard push --project ~/Projects/MyApp # specific project
|
|
100
|
+
cost-guard push --since 7 # only last 7 days
|
|
101
|
+
cost-guard push --dry-run # preview without HTTP
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Requires authenticated token. Filters by `--since` (days). POSTs `{id, project, primary_model, started_at, ended_at, input_tokens, output_tokens, cost_usd}`. Continues on errors, returns 1 if any failed.
|
|
105
|
+
|
|
106
|
+
### Stubs (later weeks)
|
|
107
|
+
<!-- delegation-guard: ok -->
|
|
108
|
+
|
|
109
|
+
- `cost-guard live` — real-time TUI dashboard (Week 2.5)
|
|
110
|
+
- `cost-guard alert --budget 5` — daily spend cap (Week 3)
|
|
111
|
+
|
|
112
|
+
## Pricing
|
|
113
|
+
|
|
114
|
+
Per-million-token rates (verified 2026-05-23 against https://docs.claude.com/en/docs/about-claude/models):
|
|
115
|
+
|
|
116
|
+
| Model | Input | Output | Cache read | Cache write 5m | Cache write 1h |
|
|
117
|
+
|---|---|---|---|---|---|
|
|
118
|
+
| `claude-opus-4-7` / `4-6` | $15 | $75 | $1.50 | $18.75 | $30 |
|
|
119
|
+
| `claude-sonnet-4-6` / `4-5` | $3 | $15 | $0.30 | $3.75 | $6 |
|
|
120
|
+
| `claude-haiku-4-5` | $1 | $5 | $0.10 | $1.25 | $2 |
|
|
121
|
+
|
|
122
|
+
Unknown models return cost=0 (not None) — see `cost_guard.pricing.compute_cost_usd`.
|
|
123
|
+
|
|
124
|
+
## Tests
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
cd cli
|
|
128
|
+
pytest # 50 tests, ~1s
|
|
129
|
+
COSTGUARD_REAL_SMOKE=1 pytest tests/test_real_data_smoke.py # opt-in: scans real ~/.claude/projects
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
<!-- delegation-guard: ok -->
|
costcut-0.0.9/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# cost-guard CLI
|
|
2
|
+
|
|
3
|
+
OSS CLI for Cost Guard. Tracks Claude Code session tokens, cost, hook compliance.
|
|
4
|
+
|
|
5
|
+
## Install (Week 6+)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install cost-guard # PyPI (Week 6 release)
|
|
9
|
+
brew install cost-guard # Homebrew tap (Week 6 release)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Dev install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cd cli
|
|
16
|
+
python -m venv .venv && source .venv/bin/activate
|
|
17
|
+
pip install -e ".[dev]"
|
|
18
|
+
pytest
|
|
19
|
+
cost-guard --version
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
### `cost-guard init` — install hooks bundle (Week 3 ✅)
|
|
25
|
+
|
|
26
|
+
Installs 5 bundled hook scripts + shared library into `.claude/hooks/`, registers them in `.claude/settings.json`.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cost-guard init # install to .claude/hooks (default)
|
|
30
|
+
cost-guard init --target ~/.claude/hooks # explicit target path
|
|
31
|
+
cost-guard init --dry-run # preview changes, write nothing
|
|
32
|
+
cost-guard init --force # overwrite existing hooks
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Installs:
|
|
36
|
+
- **Hooks:** `parallel_agents_guard.sh`, `test_before_push_guard.sh`, `large_read_warning.sh`, `token_alert.sh`, `session_cost_check.sh`
|
|
37
|
+
- **Library:** `_lib/cost.sh` (shared utilities)
|
|
38
|
+
|
|
39
|
+
Registers hooks in `.claude/settings.json` with event + matcher + command path. Idempotent: running twice adds no duplicates.
|
|
40
|
+
|
|
41
|
+
Exit code 0 on success, 1 on error.
|
|
42
|
+
|
|
43
|
+
### `cost-guard report` — session analytics (Week 2 ✅)
|
|
44
|
+
|
|
45
|
+
Scans Claude Code JSONL transcripts in `~/.claude/projects/`, aggregates tokens + cost per session, renders a Rich table.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cost-guard report # default: last 20 sessions for cwd
|
|
49
|
+
cost-guard report --project ~/Projects/MyApp # specific project
|
|
50
|
+
cost-guard report --since 2026-05-01 # only sessions since date
|
|
51
|
+
cost-guard report --last 5 # top 5 most recent
|
|
52
|
+
cost-guard report --all # no limit
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Output columns: `Started | Duration | Model | Msgs | In | Out | Cache | Cost | Session`. Final row = TOTAL.
|
|
56
|
+
|
|
57
|
+
### `cost-guard auth` — token management (Week 7 ✅)
|
|
58
|
+
|
|
59
|
+
Manages authentication with backend API.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cost-guard auth set <api_key> # save API key (must start with cg_live_)
|
|
63
|
+
cost-guard auth status # check login status + show masked token
|
|
64
|
+
cost-guard auth logout # remove stored token
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Token storage: `~/.config/cost-guard/token` (0o600 permissions). Env override: `COST_GUARD_TOKEN=cg_live_...` takes precedence. Backend: `GET /v1/auth/me` (Bearer JWT).
|
|
68
|
+
|
|
69
|
+
### `cost-guard push` — upload sessions to backend (Week 7 ✅)
|
|
70
|
+
|
|
71
|
+
Aggregates sessions from local JSONL and POSTs to backend API.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
cost-guard push # default: last 30 days
|
|
75
|
+
cost-guard push --project ~/Projects/MyApp # specific project
|
|
76
|
+
cost-guard push --since 7 # only last 7 days
|
|
77
|
+
cost-guard push --dry-run # preview without HTTP
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Requires authenticated token. Filters by `--since` (days). POSTs `{id, project, primary_model, started_at, ended_at, input_tokens, output_tokens, cost_usd}`. Continues on errors, returns 1 if any failed.
|
|
81
|
+
|
|
82
|
+
### Stubs (later weeks)
|
|
83
|
+
<!-- delegation-guard: ok -->
|
|
84
|
+
|
|
85
|
+
- `cost-guard live` — real-time TUI dashboard (Week 2.5)
|
|
86
|
+
- `cost-guard alert --budget 5` — daily spend cap (Week 3)
|
|
87
|
+
|
|
88
|
+
## Pricing
|
|
89
|
+
|
|
90
|
+
Per-million-token rates (verified 2026-05-23 against https://docs.claude.com/en/docs/about-claude/models):
|
|
91
|
+
|
|
92
|
+
| Model | Input | Output | Cache read | Cache write 5m | Cache write 1h |
|
|
93
|
+
|---|---|---|---|---|---|
|
|
94
|
+
| `claude-opus-4-7` / `4-6` | $15 | $75 | $1.50 | $18.75 | $30 |
|
|
95
|
+
| `claude-sonnet-4-6` / `4-5` | $3 | $15 | $0.30 | $3.75 | $6 |
|
|
96
|
+
| `claude-haiku-4-5` | $1 | $5 | $0.10 | $1.25 | $2 |
|
|
97
|
+
|
|
98
|
+
Unknown models return cost=0 (not None) — see `cost_guard.pricing.compute_cost_usd`.
|
|
99
|
+
|
|
100
|
+
## Tests
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
cd cli
|
|
104
|
+
pytest # 50 tests, ~1s
|
|
105
|
+
COSTGUARD_REAL_SMOKE=1 pytest tests/test_real_data_smoke.py # opt-in: scans real ~/.claude/projects
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
<!-- delegation-guard: ok -->
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "costcut"
|
|
7
|
+
version = "0.0.9"
|
|
8
|
+
description = "Claude Code observability + cost optimization CLI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Eugene Kuznetsov", email = "eugene@costcut.dev" }]
|
|
13
|
+
keywords = ["claude-code", "observability", "tokens", "cost", "llm"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Topic :: Software Development",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"typer>=0.12.0",
|
|
22
|
+
"httpx>=0.27.0",
|
|
23
|
+
"pydantic>=2.7.0",
|
|
24
|
+
"rich>=13.7.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=8.0.0",
|
|
30
|
+
"ruff>=0.4.0",
|
|
31
|
+
"mypy>=1.10.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
costcut = "cost_guard.cli:app"
|
|
36
|
+
cost-guard = "cost_guard.cli:app"
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://costcut.dev"
|
|
40
|
+
Repository = "https://github.com/kuznetsov-ai/cost-guard"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/cost_guard"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
line-length = 100
|
|
47
|
+
target-version = "py312"
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.9"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# delegation-guard: ok
|
|
2
|
+
"""Authentication and token management for cost-guard."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_token_path() -> Path:
|
|
14
|
+
"""Get the path to the token file."""
|
|
15
|
+
config_dir = Path.home() / ".config" / "cost-guard"
|
|
16
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
return config_dir / "token"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_token() -> Optional[str]:
|
|
21
|
+
"""Load token from env or file."""
|
|
22
|
+
# Env takes precedence
|
|
23
|
+
if env_token := os.getenv("COST_GUARD_TOKEN"):
|
|
24
|
+
return env_token
|
|
25
|
+
|
|
26
|
+
# Then file
|
|
27
|
+
token_path = _get_token_path()
|
|
28
|
+
if token_path.exists():
|
|
29
|
+
return token_path.read_text().strip()
|
|
30
|
+
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_token(token: str) -> Path:
|
|
35
|
+
"""Save token to file with restricted permissions."""
|
|
36
|
+
token_path = _get_token_path()
|
|
37
|
+
token_path.write_text(token)
|
|
38
|
+
token_path.chmod(0o600)
|
|
39
|
+
return token_path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def clear_token() -> bool:
|
|
43
|
+
"""Remove token file. Returns True if file existed."""
|
|
44
|
+
token_path = _get_token_path()
|
|
45
|
+
if token_path.exists():
|
|
46
|
+
token_path.unlink()
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_api_url() -> str:
|
|
52
|
+
"""Get the API URL from env or default."""
|
|
53
|
+
return os.getenv("COST_GUARD_API_URL", "https://api.costcut.dev")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def verify_token(token: str) -> dict | None:
|
|
57
|
+
"""Verify token by calling GET /v1/auth/me. Returns user dict or None."""
|
|
58
|
+
url = get_api_url()
|
|
59
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
63
|
+
resp = await client.get(f"{url}/v1/auth/me", headers=headers)
|
|
64
|
+
if resp.status_code == 200:
|
|
65
|
+
return resp.json()
|
|
66
|
+
return None
|
|
67
|
+
except Exception:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def mask_token(token: str) -> str:
|
|
72
|
+
"""Mask token for display."""
|
|
73
|
+
if len(token) <= 8:
|
|
74
|
+
return "***"
|
|
75
|
+
return f"{token[:8]}{'*' * 4}"
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# delegation-guard: ok
|
|
2
|
+
"""cost-guard CLI entry point."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from cost_guard import __version__
|
|
12
|
+
from cost_guard.auth import clear_token, load_token, mask_token, save_token, verify_token
|
|
13
|
+
from cost_guard.init_cmd import run_init
|
|
14
|
+
from cost_guard.push import run_push
|
|
15
|
+
from cost_guard.report import run_report
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
name="cost-guard",
|
|
19
|
+
help="Claude Code observability + cost optimization.",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
22
|
+
auth_app = typer.Typer(help="Authentication management")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _version_callback(value: bool) -> None:
|
|
26
|
+
if value:
|
|
27
|
+
typer.echo(f"cost-guard {__version__}")
|
|
28
|
+
raise typer.Exit()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.callback()
|
|
32
|
+
def main(
|
|
33
|
+
version: bool = typer.Option(
|
|
34
|
+
False,
|
|
35
|
+
"--version",
|
|
36
|
+
callback=_version_callback,
|
|
37
|
+
is_eager=True,
|
|
38
|
+
help="Show version and exit.",
|
|
39
|
+
),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Cost Guard CLI."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def init(
|
|
46
|
+
target: str = typer.Option(".claude/hooks", help="Hook install path"),
|
|
47
|
+
force: bool = typer.Option(False, help="Overwrite existing hook files"),
|
|
48
|
+
dry_run: bool = typer.Option(False, help="Print what would be done, write nothing"),
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Install hooks bundle into target directory."""
|
|
51
|
+
exit_code = run_init(target=target, force=force, dry_run=dry_run)
|
|
52
|
+
raise typer.Exit(exit_code)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command()
|
|
56
|
+
def report(
|
|
57
|
+
project: str = typer.Option(".", help="Project path to scan"),
|
|
58
|
+
since: Optional[str] = typer.Option(None, help="Only include sessions since YYYY-MM-DD"),
|
|
59
|
+
last: Optional[int] = typer.Option(None, help="Show only last N sessions (default 20)"),
|
|
60
|
+
all: bool = typer.Option(False, help="Show all sessions"),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Show session analytics report."""
|
|
63
|
+
exit_code = run_report(project=project, since=since, last=last, all=all)
|
|
64
|
+
raise typer.Exit(exit_code)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@auth_app.command()
|
|
68
|
+
def set(api_key: str = typer.Argument(..., help="API key (starts with cg_live_)")) -> None:
|
|
69
|
+
"""Set API token for authentication."""
|
|
70
|
+
if not api_key.startswith("cg_live_"):
|
|
71
|
+
typer.echo("Error: API key must start with 'cg_live_'", err=True)
|
|
72
|
+
raise typer.Exit(1)
|
|
73
|
+
|
|
74
|
+
save_token(api_key)
|
|
75
|
+
typer.echo(f"Token saved. Masked: {mask_token(api_key)}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@auth_app.command()
|
|
79
|
+
def status() -> None:
|
|
80
|
+
"""Check authentication status."""
|
|
81
|
+
token = load_token()
|
|
82
|
+
if not token:
|
|
83
|
+
typer.echo("Not logged in.", err=True)
|
|
84
|
+
raise typer.Exit(1)
|
|
85
|
+
|
|
86
|
+
user = asyncio.run(verify_token(token))
|
|
87
|
+
if user is None:
|
|
88
|
+
typer.echo("Error: Invalid token (401 or connection failed)", err=True)
|
|
89
|
+
raise typer.Exit(1)
|
|
90
|
+
|
|
91
|
+
email = user.get("email", "unknown")
|
|
92
|
+
plan = user.get("plan", "unknown")
|
|
93
|
+
typer.echo(f"Logged in as: {email} (plan: {plan})")
|
|
94
|
+
typer.echo(f"Token: {mask_token(token)}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@auth_app.command()
|
|
98
|
+
def logout() -> None:
|
|
99
|
+
"""Remove stored authentication token."""
|
|
100
|
+
if clear_token():
|
|
101
|
+
typer.echo("Logged out.")
|
|
102
|
+
else:
|
|
103
|
+
typer.echo("Not logged in (no token file found).")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
app.add_typer(auth_app, name="auth")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.command()
|
|
110
|
+
def push(
|
|
111
|
+
project: str = typer.Option(".", help="Project path to scan"),
|
|
112
|
+
since: Optional[int] = typer.Option(30, help="Only push sessions from last N days"),
|
|
113
|
+
dry_run: bool = typer.Option(False, help="Print what would be pushed without HTTP"),
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Push session data to backend."""
|
|
116
|
+
exit_code = run_push(project=project, since=since, dry_run=dry_run)
|
|
117
|
+
raise typer.Exit(exit_code)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.command()
|
|
121
|
+
def live() -> None:
|
|
122
|
+
"""Live TUI dashboard of current session (Week 2 implementation)."""
|
|
123
|
+
typer.echo("live (stub; Week 2)")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.command()
|
|
127
|
+
def alert(budget: float = typer.Option(5.0, help="USD daily cap")) -> None:
|
|
128
|
+
"""Set daily spend cap; warn or block when exceeded (Week 3)."""
|
|
129
|
+
typer.echo(f"alert budget=${budget:.2f} (stub; Week 3)")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.command()
|
|
133
|
+
def sync() -> None:
|
|
134
|
+
"""Push session data to cloud (Week 4 implementation)."""
|
|
135
|
+
typer.echo("sync (stub; Week 4)")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
app()
|