@pjmendonca/devflow 1.9.0
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/CHANGELOG.md +526 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/bin/devflow-checkpoint.js +10 -0
- package/bin/devflow-collab.js +10 -0
- package/bin/devflow-cost.js +10 -0
- package/bin/devflow-create-persona.js +10 -0
- package/bin/devflow-init.js +10 -0
- package/bin/devflow-memory.js +10 -0
- package/bin/devflow-new-doc.js +10 -0
- package/bin/devflow-personalize.js +10 -0
- package/bin/devflow-setup-checkpoint.js +10 -0
- package/bin/devflow-story.js +10 -0
- package/bin/devflow-tech-debt.js +10 -0
- package/bin/devflow-validate-overrides.js +10 -0
- package/bin/devflow-validate.js +10 -0
- package/bin/devflow-version.js +10 -0
- package/lib/constants.js +30 -0
- package/lib/exec-python.js +78 -0
- package/lib/python-check.js +178 -0
- package/package.json +64 -0
- package/tooling/.automation/agents/architect.md +135 -0
- package/tooling/.automation/agents/ba.md +70 -0
- package/tooling/.automation/agents/dev.md +79 -0
- package/tooling/.automation/agents/maintainer.md +97 -0
- package/tooling/.automation/agents/pm.md +116 -0
- package/tooling/.automation/agents/reviewer.md +141 -0
- package/tooling/.automation/agents/sm.md +61 -0
- package/tooling/.automation/agents/writer.md +193 -0
- package/tooling/.automation/config.ps1.template +61 -0
- package/tooling/.automation/config.sh.template +48 -0
- package/tooling/.automation/memory/.gitkeep +6 -0
- package/tooling/.automation/memory/knowledge/kg_integration-test.json +94 -0
- package/tooling/.automation/memory/knowledge/kg_test-story.json +300 -0
- package/tooling/.automation/memory/shared/shared_integration-test.json +30 -0
- package/tooling/.automation/memory/shared/shared_test-story.json +78 -0
- package/tooling/.automation/overrides/templates/README.md +113 -0
- package/tooling/.automation/overrides/templates/architect/README.md +27 -0
- package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +92 -0
- package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +85 -0
- package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +88 -0
- package/tooling/.automation/overrides/templates/ba/README.md +27 -0
- package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +86 -0
- package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +91 -0
- package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +89 -0
- package/tooling/.automation/overrides/templates/dev/README.md +32 -0
- package/tooling/.automation/overrides/templates/dev/junior-mentored.yaml +39 -0
- package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +43 -0
- package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +52 -0
- package/tooling/.automation/overrides/templates/dev/security-focused.yaml +43 -0
- package/tooling/.automation/overrides/templates/dev/senior-fullstack.yaml +39 -0
- package/tooling/.automation/overrides/templates/maintainer/README.md +27 -0
- package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +113 -0
- package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +94 -0
- package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +94 -0
- package/tooling/.automation/overrides/templates/pm/README.md +27 -0
- package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +91 -0
- package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +87 -0
- package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +91 -0
- package/tooling/.automation/overrides/templates/reviewer/README.md +11 -0
- package/tooling/.automation/overrides/templates/reviewer/mentoring-reviewer.yaml +45 -0
- package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +50 -0
- package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +48 -0
- package/tooling/.automation/overrides/templates/sm/README.md +11 -0
- package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +52 -0
- package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +50 -0
- package/tooling/.automation/overrides/templates/sm/technical-lead.yaml +47 -0
- package/tooling/.automation/overrides/templates/user-profile.template.yaml +62 -0
- package/tooling/.automation/overrides/templates/writer/README.md +27 -0
- package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +99 -0
- package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +108 -0
- package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +100 -0
- package/tooling/completions/DevflowCompletion.ps1 +213 -0
- package/tooling/completions/_run-story +116 -0
- package/tooling/completions/run-story-completion.bash +136 -0
- package/tooling/docs/DOC-STANDARD.md +717 -0
- package/tooling/docs/sprint-status.yaml.template +24 -0
- package/tooling/docs/templates/bug-report.md +234 -0
- package/tooling/docs/templates/migration-spec.md +274 -0
- package/tooling/docs/templates/refactor-spec.md +86 -0
- package/tooling/docs/templates/tech-debt.md +86 -0
- package/tooling/scripts/context_checkpoint.py +556 -0
- package/tooling/scripts/cost_dashboard.py +617 -0
- package/tooling/scripts/create-persona.py +690 -0
- package/tooling/scripts/create-persona.sh +435 -0
- package/tooling/scripts/init-project-workflow.ps1 +651 -0
- package/tooling/scripts/init-project-workflow.py +70 -0
- package/tooling/scripts/init-project-workflow.sh +746 -0
- package/tooling/scripts/lib/__init__.py +35 -0
- package/tooling/scripts/lib/agent_handoff.py +526 -0
- package/tooling/scripts/lib/agent_router.py +698 -0
- package/tooling/scripts/lib/checkpoint-integration.ps1 +245 -0
- package/tooling/scripts/lib/checkpoint-integration.sh +191 -0
- package/tooling/scripts/lib/claude-cli.ps1 +952 -0
- package/tooling/scripts/lib/claude-cli.sh +1293 -0
- package/tooling/scripts/lib/cost_config.py +222 -0
- package/tooling/scripts/lib/cost_display.py +443 -0
- package/tooling/scripts/lib/cost_tracker.py +710 -0
- package/tooling/scripts/lib/currency_converter.py +328 -0
- package/tooling/scripts/lib/errors.py +438 -0
- package/tooling/scripts/lib/override-loader.sh +286 -0
- package/tooling/scripts/lib/pair_programming.py +589 -0
- package/tooling/scripts/lib/shared_memory.py +637 -0
- package/tooling/scripts/lib/swarm_orchestrator.py +689 -0
- package/tooling/scripts/memory_summarize.py +324 -0
- package/tooling/scripts/new-doc.ps1 +405 -0
- package/tooling/scripts/new-doc.py +93 -0
- package/tooling/scripts/new-doc.sh +534 -0
- package/tooling/scripts/personalize_agent.py +385 -0
- package/tooling/scripts/rollback-migration.sh +540 -0
- package/tooling/scripts/run-collab.ps1 +251 -0
- package/tooling/scripts/run-collab.py +605 -0
- package/tooling/scripts/run-collab.sh +110 -0
- package/tooling/scripts/run-story.ps1 +490 -0
- package/tooling/scripts/run-story.py +387 -0
- package/tooling/scripts/run-story.sh +467 -0
- package/tooling/scripts/setup-checkpoint-service.ps1 +219 -0
- package/tooling/scripts/setup-checkpoint-service.py +87 -0
- package/tooling/scripts/setup-checkpoint-service.sh +236 -0
- package/tooling/scripts/tech-debt-tracker.py +608 -0
- package/tooling/scripts/update_version.py +244 -0
- package/tooling/scripts/validate-overrides.py +511 -0
- package/tooling/scripts/validate-overrides.sh +432 -0
- package/tooling/scripts/validate_setup.py +539 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Cost Dashboard - CLI for viewing and managing cost data.
|
|
4
|
+
|
|
5
|
+
Provides commands for viewing session history, generating summaries,
|
|
6
|
+
and exporting cost reports in multiple formats.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python cost_dashboard.py # Show current/latest session
|
|
10
|
+
python cost_dashboard.py --history 10 # Show last 10 sessions
|
|
11
|
+
python cost_dashboard.py --summary # Show aggregate summary
|
|
12
|
+
python cost_dashboard.py --story 3-5 # Show costs for story
|
|
13
|
+
python cost_dashboard.py --export costs.csv # Export to file
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import csv
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
# Lowercase generic types for Python 3.9+ compatibility
|
|
25
|
+
# Using 'list' instead of 'List' from typing
|
|
26
|
+
|
|
27
|
+
# Add lib directory for imports
|
|
28
|
+
sys.path.insert(0, str(Path(__file__).parent / "lib"))
|
|
29
|
+
|
|
30
|
+
from cost_display import Colors
|
|
31
|
+
from cost_tracker import SESSIONS_DIR, CostTracker, SessionCost
|
|
32
|
+
from currency_converter import get_converter
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CostDashboard:
|
|
36
|
+
"""
|
|
37
|
+
CLI Dashboard for cost analysis.
|
|
38
|
+
|
|
39
|
+
Provides commands for viewing, summarizing, and exporting cost data.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Box drawing
|
|
43
|
+
BOX_TOP_LEFT = "╔"
|
|
44
|
+
BOX_TOP_RIGHT = "╗"
|
|
45
|
+
BOX_BOTTOM_LEFT = "╚"
|
|
46
|
+
BOX_BOTTOM_RIGHT = "╝"
|
|
47
|
+
BOX_HORIZONTAL = "═"
|
|
48
|
+
BOX_VERTICAL = "║"
|
|
49
|
+
BOX_T_LEFT = "╠"
|
|
50
|
+
BOX_T_RIGHT = "╣"
|
|
51
|
+
BOX_LINE = "─"
|
|
52
|
+
|
|
53
|
+
def __init__(self, width: int = 70):
|
|
54
|
+
self.width = width
|
|
55
|
+
self.converter = get_converter()
|
|
56
|
+
|
|
57
|
+
def _box_line(self, left: str, right: str, fill: str = "═") -> str:
|
|
58
|
+
"""Create a box line."""
|
|
59
|
+
return f"{left}{fill * (self.width - 2)}{right}"
|
|
60
|
+
|
|
61
|
+
def _content_line(self, content: str, align: str = "left") -> str:
|
|
62
|
+
"""Create a content line within the box."""
|
|
63
|
+
clean_content = Colors.strip(content)
|
|
64
|
+
padding = self.width - 4 - len(clean_content)
|
|
65
|
+
|
|
66
|
+
if align == "center":
|
|
67
|
+
left_pad = padding // 2
|
|
68
|
+
right_pad = padding - left_pad
|
|
69
|
+
return f"{self.BOX_VERTICAL} {' ' * left_pad}{content}{' ' * right_pad} {self.BOX_VERTICAL}"
|
|
70
|
+
elif align == "right":
|
|
71
|
+
return f"{self.BOX_VERTICAL} {' ' * padding}{content} {self.BOX_VERTICAL}"
|
|
72
|
+
else:
|
|
73
|
+
return f"{self.BOX_VERTICAL} {content}{' ' * padding} {self.BOX_VERTICAL}"
|
|
74
|
+
|
|
75
|
+
def _empty_line(self) -> str:
|
|
76
|
+
"""Create an empty content line."""
|
|
77
|
+
return self._content_line("")
|
|
78
|
+
|
|
79
|
+
def _section_header(self, title: str) -> str:
|
|
80
|
+
"""Create a section header."""
|
|
81
|
+
line = f"{self.BOX_LINE * 3} {title} "
|
|
82
|
+
remaining = self.width - 6 - len(title)
|
|
83
|
+
return self._content_line(f"{Colors.BOLD}{line}{self.BOX_LINE * remaining}{Colors.RESET}")
|
|
84
|
+
|
|
85
|
+
def _format_tokens(self, tokens: int) -> str:
|
|
86
|
+
"""Format token count with K/M suffix."""
|
|
87
|
+
if tokens >= 1_000_000:
|
|
88
|
+
return f"{tokens / 1_000_000:.1f}M"
|
|
89
|
+
elif tokens >= 1_000:
|
|
90
|
+
return f"{tokens / 1_000:.1f}K"
|
|
91
|
+
return str(tokens)
|
|
92
|
+
|
|
93
|
+
def show_session(self, session: SessionCost):
|
|
94
|
+
"""Display a single session."""
|
|
95
|
+
lines = []
|
|
96
|
+
|
|
97
|
+
# Header
|
|
98
|
+
lines.append(
|
|
99
|
+
f"{Colors.CYAN}{self._box_line(self.BOX_TOP_LEFT, self.BOX_TOP_RIGHT)}{Colors.RESET}"
|
|
100
|
+
)
|
|
101
|
+
title = f"SESSION: {session.session_id[:20]}"
|
|
102
|
+
lines.append(
|
|
103
|
+
f"{Colors.CYAN}{self._content_line(Colors.BOLD + title + Colors.RESET, 'center')}{Colors.RESET}"
|
|
104
|
+
)
|
|
105
|
+
lines.append(
|
|
106
|
+
f"{Colors.CYAN}{self._box_line(self.BOX_T_LEFT, self.BOX_T_RIGHT)}{Colors.RESET}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Session info
|
|
110
|
+
lines.append(self._empty_line())
|
|
111
|
+
lines.append(
|
|
112
|
+
self._content_line(f"Story: {Colors.BOLD_CYAN}{session.story_key}{Colors.RESET}")
|
|
113
|
+
)
|
|
114
|
+
lines.append(self._content_line(f"Start: {session.start_time[:19]}"))
|
|
115
|
+
if session.end_time:
|
|
116
|
+
lines.append(self._content_line(f"End: {session.end_time[:19]}"))
|
|
117
|
+
|
|
118
|
+
# Tokens
|
|
119
|
+
lines.append(self._empty_line())
|
|
120
|
+
lines.append(self._section_header("TOKENS"))
|
|
121
|
+
lines.append(
|
|
122
|
+
self._content_line(
|
|
123
|
+
f"Input: {Colors.BOLD}{self._format_tokens(session.total_input_tokens):>10}{Colors.RESET} "
|
|
124
|
+
f"Output: {Colors.BOLD}{self._format_tokens(session.total_output_tokens):>10}{Colors.RESET}"
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
lines.append(
|
|
128
|
+
self._content_line(
|
|
129
|
+
f"Total: {Colors.BOLD_WHITE}{self._format_tokens(session.total_tokens):>10}{Colors.RESET}"
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Cost by agent
|
|
134
|
+
lines.append(self._empty_line())
|
|
135
|
+
lines.append(self._section_header("COST BY AGENT"))
|
|
136
|
+
|
|
137
|
+
by_agent = session.get_cost_by_agent()
|
|
138
|
+
if by_agent:
|
|
139
|
+
for agent, cost in sorted(by_agent.items(), key=lambda x: -x[1]):
|
|
140
|
+
pct = (cost / session.total_cost_usd * 100) if session.total_cost_usd > 0 else 0
|
|
141
|
+
lines.append(
|
|
142
|
+
self._content_line(
|
|
143
|
+
f"{agent:<12} {Colors.BOLD}${cost:>8.2f}{Colors.RESET} ({pct:>5.1f}%)"
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
lines.append(self._content_line(f"{Colors.DIM}No entries{Colors.RESET}"))
|
|
148
|
+
|
|
149
|
+
# Cost by model
|
|
150
|
+
lines.append(self._empty_line())
|
|
151
|
+
lines.append(self._section_header("COST BY MODEL"))
|
|
152
|
+
|
|
153
|
+
by_model = session.get_cost_by_model()
|
|
154
|
+
if by_model:
|
|
155
|
+
for model, cost in sorted(by_model.items(), key=lambda x: -x[1]):
|
|
156
|
+
pct = (cost / session.total_cost_usd * 100) if session.total_cost_usd > 0 else 0
|
|
157
|
+
lines.append(
|
|
158
|
+
self._content_line(
|
|
159
|
+
f"{model:<12} {Colors.BOLD}${cost:>8.2f}{Colors.RESET} ({pct:>5.1f}%)"
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Total
|
|
164
|
+
lines.append(self._empty_line())
|
|
165
|
+
lines.append(self._section_header("TOTAL"))
|
|
166
|
+
lines.append(
|
|
167
|
+
self._content_line(
|
|
168
|
+
f"Cost: {Colors.BOLD_GREEN}${session.total_cost_usd:.2f}{Colors.RESET} "
|
|
169
|
+
f"Budget: ${session.budget_limit_usd:.2f} "
|
|
170
|
+
f"Used: {session.budget_used_percent:.0f}%"
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Multi-currency
|
|
175
|
+
lines.append(self._empty_line())
|
|
176
|
+
lines.append(self._content_line(self.converter.format_all(session.total_cost_usd, " │ ")))
|
|
177
|
+
|
|
178
|
+
# Footer
|
|
179
|
+
lines.append(self._empty_line())
|
|
180
|
+
lines.append(
|
|
181
|
+
f"{Colors.CYAN}{self._box_line(self.BOX_BOTTOM_LEFT, self.BOX_BOTTOM_RIGHT)}{Colors.RESET}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
print("\n".join(lines))
|
|
185
|
+
|
|
186
|
+
def show_history(self, sessions: list[SessionCost], limit: int = 10):
|
|
187
|
+
"""Display session history."""
|
|
188
|
+
sessions = sessions[:limit]
|
|
189
|
+
|
|
190
|
+
lines = []
|
|
191
|
+
|
|
192
|
+
# Header
|
|
193
|
+
lines.append(
|
|
194
|
+
f"{Colors.CYAN}{self._box_line(self.BOX_TOP_LEFT, self.BOX_TOP_RIGHT)}{Colors.RESET}"
|
|
195
|
+
)
|
|
196
|
+
title = f"SESSION HISTORY - Last {len(sessions)} Sessions"
|
|
197
|
+
lines.append(
|
|
198
|
+
f"{Colors.CYAN}{self._content_line(Colors.BOLD + title + Colors.RESET, 'center')}{Colors.RESET}"
|
|
199
|
+
)
|
|
200
|
+
lines.append(
|
|
201
|
+
f"{Colors.CYAN}{self._box_line(self.BOX_T_LEFT, self.BOX_T_RIGHT)}{Colors.RESET}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Table header
|
|
205
|
+
lines.append(self._empty_line())
|
|
206
|
+
lines.append(
|
|
207
|
+
self._content_line(
|
|
208
|
+
f"{Colors.DIM}{'Date':<12} {'Story':<10} {'Tokens':>10} {'Cost':>10} {'Budget%':>8}{Colors.RESET}"
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
lines.append(self._content_line(f"{Colors.DIM}{self.BOX_LINE * 54}{Colors.RESET}"))
|
|
212
|
+
|
|
213
|
+
# Sessions
|
|
214
|
+
total_cost = 0
|
|
215
|
+
total_tokens = 0
|
|
216
|
+
|
|
217
|
+
for session in sessions:
|
|
218
|
+
date_str = session.start_time[:10]
|
|
219
|
+
tokens = self._format_tokens(session.total_tokens)
|
|
220
|
+
cost = f"${session.total_cost_usd:.2f}"
|
|
221
|
+
budget_pct = f"{session.budget_used_percent:.0f}%"
|
|
222
|
+
|
|
223
|
+
# Color based on budget usage
|
|
224
|
+
if session.budget_used_percent >= 90:
|
|
225
|
+
color = Colors.RED
|
|
226
|
+
elif session.budget_used_percent >= 75:
|
|
227
|
+
color = Colors.YELLOW
|
|
228
|
+
else:
|
|
229
|
+
color = Colors.GREEN
|
|
230
|
+
|
|
231
|
+
lines.append(
|
|
232
|
+
self._content_line(
|
|
233
|
+
f"{date_str:<12} {session.story_key:<10} {tokens:>10} "
|
|
234
|
+
f"{color}{cost:>10}{Colors.RESET} {budget_pct:>8}"
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
total_cost += session.total_cost_usd
|
|
239
|
+
total_tokens += session.total_tokens
|
|
240
|
+
|
|
241
|
+
# Totals
|
|
242
|
+
lines.append(self._content_line(f"{Colors.DIM}{self.BOX_LINE * 54}{Colors.RESET}"))
|
|
243
|
+
lines.append(
|
|
244
|
+
self._content_line(
|
|
245
|
+
f"{Colors.BOLD}{'TOTAL':<12} {'':<10} {self._format_tokens(total_tokens):>10} "
|
|
246
|
+
f"${total_cost:>9.2f}{Colors.RESET}"
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Footer
|
|
251
|
+
lines.append(self._empty_line())
|
|
252
|
+
lines.append(
|
|
253
|
+
f"{Colors.CYAN}{self._box_line(self.BOX_BOTTOM_LEFT, self.BOX_BOTTOM_RIGHT)}{Colors.RESET}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
print("\n".join(lines))
|
|
257
|
+
|
|
258
|
+
def show_summary(self, days: int = 30):
|
|
259
|
+
"""Display aggregate summary."""
|
|
260
|
+
stats = CostTracker.get_aggregate_stats(days)
|
|
261
|
+
|
|
262
|
+
lines = []
|
|
263
|
+
|
|
264
|
+
# Header
|
|
265
|
+
lines.append(
|
|
266
|
+
f"{Colors.CYAN}{self._box_line(self.BOX_TOP_LEFT, self.BOX_TOP_RIGHT)}{Colors.RESET}"
|
|
267
|
+
)
|
|
268
|
+
title = f"COST SUMMARY - Last {days} Days"
|
|
269
|
+
lines.append(
|
|
270
|
+
f"{Colors.CYAN}{self._content_line(Colors.BOLD + title + Colors.RESET, 'center')}{Colors.RESET}"
|
|
271
|
+
)
|
|
272
|
+
lines.append(
|
|
273
|
+
f"{Colors.CYAN}{self._box_line(self.BOX_T_LEFT, self.BOX_T_RIGHT)}{Colors.RESET}"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Overview
|
|
277
|
+
lines.append(self._empty_line())
|
|
278
|
+
lines.append(self._section_header("OVERVIEW"))
|
|
279
|
+
lines.append(
|
|
280
|
+
self._content_line(
|
|
281
|
+
f"Total Sessions: {Colors.BOLD}{stats['total_sessions']}{Colors.RESET}"
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
lines.append(
|
|
285
|
+
self._content_line(
|
|
286
|
+
f"Total Tokens: {Colors.BOLD}{self._format_tokens(stats['total_tokens'])}{Colors.RESET}"
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
lines.append(
|
|
290
|
+
self._content_line(
|
|
291
|
+
f"Total Cost: {Colors.BOLD_GREEN}${stats['total_cost_usd']:.2f}{Colors.RESET}"
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
lines.append(
|
|
295
|
+
self._content_line(
|
|
296
|
+
f"Avg/Session: {Colors.BOLD}${stats.get('average_per_session', 0):.2f}{Colors.RESET}"
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Multi-currency total
|
|
301
|
+
lines.append(self._empty_line())
|
|
302
|
+
lines.append(self._content_line(self.converter.format_all(stats["total_cost_usd"], " │ ")))
|
|
303
|
+
|
|
304
|
+
# By Agent
|
|
305
|
+
if stats.get("by_agent"):
|
|
306
|
+
lines.append(self._empty_line())
|
|
307
|
+
lines.append(self._section_header("BY AGENT"))
|
|
308
|
+
lines.append(
|
|
309
|
+
self._content_line(
|
|
310
|
+
f"{Colors.DIM}{'Agent':<12} {'Sessions':>10} {'Cost':>12}{Colors.RESET}"
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
lines.append(self._content_line(f"{Colors.DIM}{self.BOX_LINE * 38}{Colors.RESET}"))
|
|
314
|
+
|
|
315
|
+
for agent, data in sorted(stats["by_agent"].items(), key=lambda x: -x[1]["cost"]):
|
|
316
|
+
lines.append(
|
|
317
|
+
self._content_line(
|
|
318
|
+
f"{agent:<12} {data['sessions']:>10} {Colors.BOLD}${data['cost']:>10.2f}{Colors.RESET}"
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# By Model
|
|
323
|
+
if stats.get("by_model"):
|
|
324
|
+
lines.append(self._empty_line())
|
|
325
|
+
lines.append(self._section_header("BY MODEL"))
|
|
326
|
+
lines.append(
|
|
327
|
+
self._content_line(f"{Colors.DIM}{'Model':<20} {'Cost':>12}{Colors.RESET}")
|
|
328
|
+
)
|
|
329
|
+
lines.append(self._content_line(f"{Colors.DIM}{self.BOX_LINE * 36}{Colors.RESET}"))
|
|
330
|
+
|
|
331
|
+
for model, data in sorted(stats["by_model"].items(), key=lambda x: -x[1]["cost"]):
|
|
332
|
+
lines.append(
|
|
333
|
+
self._content_line(
|
|
334
|
+
f"{model:<20} {Colors.BOLD}${data['cost']:>10.2f}{Colors.RESET}"
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Footer
|
|
339
|
+
lines.append(self._empty_line())
|
|
340
|
+
lines.append(
|
|
341
|
+
f"{Colors.CYAN}{self._box_line(self.BOX_BOTTOM_LEFT, self.BOX_BOTTOM_RIGHT)}{Colors.RESET}"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
print("\n".join(lines))
|
|
345
|
+
|
|
346
|
+
def show_story(self, story_key: str):
|
|
347
|
+
"""Display costs for a specific story."""
|
|
348
|
+
sessions = CostTracker.get_historical_sessions(days=365)
|
|
349
|
+
story_sessions = [s for s in sessions if s.story_key == story_key]
|
|
350
|
+
|
|
351
|
+
if not story_sessions:
|
|
352
|
+
print(f"{Colors.YELLOW}No sessions found for story: {story_key}{Colors.RESET}")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
lines = []
|
|
356
|
+
|
|
357
|
+
# Header
|
|
358
|
+
lines.append(
|
|
359
|
+
f"{Colors.CYAN}{self._box_line(self.BOX_TOP_LEFT, self.BOX_TOP_RIGHT)}{Colors.RESET}"
|
|
360
|
+
)
|
|
361
|
+
title = f"STORY COSTS: {story_key}"
|
|
362
|
+
lines.append(
|
|
363
|
+
f"{Colors.CYAN}{self._content_line(Colors.BOLD + title + Colors.RESET, 'center')}{Colors.RESET}"
|
|
364
|
+
)
|
|
365
|
+
lines.append(
|
|
366
|
+
f"{Colors.CYAN}{self._box_line(self.BOX_T_LEFT, self.BOX_T_RIGHT)}{Colors.RESET}"
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Summary
|
|
370
|
+
total_cost = sum(s.total_cost_usd for s in story_sessions)
|
|
371
|
+
total_tokens = sum(s.total_tokens for s in story_sessions)
|
|
372
|
+
|
|
373
|
+
lines.append(self._empty_line())
|
|
374
|
+
lines.append(
|
|
375
|
+
self._content_line(f"Sessions: {Colors.BOLD}{len(story_sessions)}{Colors.RESET}")
|
|
376
|
+
)
|
|
377
|
+
lines.append(
|
|
378
|
+
self._content_line(
|
|
379
|
+
f"Total Tokens: {Colors.BOLD}{self._format_tokens(total_tokens)}{Colors.RESET}"
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
lines.append(
|
|
383
|
+
self._content_line(f"Total Cost: {Colors.BOLD_GREEN}${total_cost:.2f}{Colors.RESET}")
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Multi-currency
|
|
387
|
+
lines.append(self._empty_line())
|
|
388
|
+
lines.append(self._content_line(self.converter.format_all(total_cost, " │ ")))
|
|
389
|
+
|
|
390
|
+
# Sessions
|
|
391
|
+
lines.append(self._empty_line())
|
|
392
|
+
lines.append(self._section_header("SESSIONS"))
|
|
393
|
+
lines.append(
|
|
394
|
+
self._content_line(
|
|
395
|
+
f"{Colors.DIM}{'Date':<12} {'Duration':<10} {'Tokens':>10} {'Cost':>10}{Colors.RESET}"
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
lines.append(self._content_line(f"{Colors.DIM}{self.BOX_LINE * 46}{Colors.RESET}"))
|
|
399
|
+
|
|
400
|
+
for session in story_sessions:
|
|
401
|
+
date_str = session.start_time[:10]
|
|
402
|
+
|
|
403
|
+
# Calculate duration
|
|
404
|
+
if session.end_time:
|
|
405
|
+
try:
|
|
406
|
+
start = datetime.fromisoformat(session.start_time)
|
|
407
|
+
end = datetime.fromisoformat(session.end_time)
|
|
408
|
+
duration = end - start
|
|
409
|
+
duration_str = f"{int(duration.total_seconds() // 60)}m"
|
|
410
|
+
except (ValueError, TypeError):
|
|
411
|
+
duration_str = "N/A"
|
|
412
|
+
else:
|
|
413
|
+
duration_str = "Running"
|
|
414
|
+
|
|
415
|
+
lines.append(
|
|
416
|
+
self._content_line(
|
|
417
|
+
f"{date_str:<12} {duration_str:<10} "
|
|
418
|
+
f"{self._format_tokens(session.total_tokens):>10} "
|
|
419
|
+
f"${session.total_cost_usd:>9.2f}"
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Footer
|
|
424
|
+
lines.append(self._empty_line())
|
|
425
|
+
lines.append(
|
|
426
|
+
f"{Colors.CYAN}{self._box_line(self.BOX_BOTTOM_LEFT, self.BOX_BOTTOM_RIGHT)}{Colors.RESET}"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
print("\n".join(lines))
|
|
430
|
+
|
|
431
|
+
def export_data(self, filepath: str, sessions: Optional[list[SessionCost]] = None):
|
|
432
|
+
"""Export cost data to file."""
|
|
433
|
+
if sessions is None:
|
|
434
|
+
sessions = CostTracker.get_historical_sessions(days=365)
|
|
435
|
+
|
|
436
|
+
path = Path(filepath)
|
|
437
|
+
ext = path.suffix.lower()
|
|
438
|
+
|
|
439
|
+
if ext == ".csv":
|
|
440
|
+
self._export_csv(path, sessions)
|
|
441
|
+
elif ext == ".json":
|
|
442
|
+
self._export_json(path, sessions)
|
|
443
|
+
elif ext == ".md":
|
|
444
|
+
self._export_markdown(path, sessions)
|
|
445
|
+
else:
|
|
446
|
+
print(f"{Colors.RED}Unsupported format: {ext}{Colors.RESET}")
|
|
447
|
+
print("Supported formats: .csv, .json, .md")
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
print(f"{Colors.GREEN}Exported to: {filepath}{Colors.RESET}")
|
|
451
|
+
print(f" Sessions: {len(sessions)}")
|
|
452
|
+
print(f" Total Cost: ${sum(s.total_cost_usd for s in sessions):.2f}")
|
|
453
|
+
|
|
454
|
+
def _export_csv(self, path: Path, sessions: list[SessionCost]):
|
|
455
|
+
"""Export to CSV."""
|
|
456
|
+
with open(path, "w", newline="") as f:
|
|
457
|
+
writer = csv.writer(f)
|
|
458
|
+
|
|
459
|
+
# Header
|
|
460
|
+
writer.writerow(
|
|
461
|
+
[
|
|
462
|
+
"session_id",
|
|
463
|
+
"story_key",
|
|
464
|
+
"start_time",
|
|
465
|
+
"end_time",
|
|
466
|
+
"input_tokens",
|
|
467
|
+
"output_tokens",
|
|
468
|
+
"total_tokens",
|
|
469
|
+
"cost_usd",
|
|
470
|
+
"budget_limit",
|
|
471
|
+
"budget_used_pct",
|
|
472
|
+
]
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Data
|
|
476
|
+
for session in sessions:
|
|
477
|
+
writer.writerow(
|
|
478
|
+
[
|
|
479
|
+
session.session_id,
|
|
480
|
+
session.story_key,
|
|
481
|
+
session.start_time,
|
|
482
|
+
session.end_time or "",
|
|
483
|
+
session.total_input_tokens,
|
|
484
|
+
session.total_output_tokens,
|
|
485
|
+
session.total_tokens,
|
|
486
|
+
round(session.total_cost_usd, 4),
|
|
487
|
+
session.budget_limit_usd,
|
|
488
|
+
round(session.budget_used_percent, 2),
|
|
489
|
+
]
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def _export_json(self, path: Path, sessions: list[SessionCost]):
|
|
493
|
+
"""Export to JSON."""
|
|
494
|
+
data = {
|
|
495
|
+
"exported_at": datetime.now().isoformat(),
|
|
496
|
+
"total_sessions": len(sessions),
|
|
497
|
+
"total_cost_usd": sum(s.total_cost_usd for s in sessions),
|
|
498
|
+
"sessions": [s.to_dict() for s in sessions],
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
with open(path, "w") as f:
|
|
502
|
+
json.dump(data, f, indent=2)
|
|
503
|
+
|
|
504
|
+
def _export_markdown(self, path: Path, sessions: list[SessionCost]):
|
|
505
|
+
"""Export to Markdown."""
|
|
506
|
+
total_cost = sum(s.total_cost_usd for s in sessions)
|
|
507
|
+
total_tokens = sum(s.total_tokens for s in sessions)
|
|
508
|
+
|
|
509
|
+
lines = [
|
|
510
|
+
"# Cost Report",
|
|
511
|
+
"",
|
|
512
|
+
f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
|
513
|
+
"",
|
|
514
|
+
"## Summary",
|
|
515
|
+
"",
|
|
516
|
+
f"- **Total Sessions**: {len(sessions)}",
|
|
517
|
+
f"- **Total Tokens**: {total_tokens:,}",
|
|
518
|
+
f"- **Total Cost**: ${total_cost:.2f}",
|
|
519
|
+
f"- **Average per Session**: ${total_cost / len(sessions):.2f}" if sessions else "",
|
|
520
|
+
"",
|
|
521
|
+
"### Multi-Currency",
|
|
522
|
+
"",
|
|
523
|
+
f"- USD: ${total_cost:.2f}",
|
|
524
|
+
f"- EUR: {self.converter.format(total_cost, 'EUR')}",
|
|
525
|
+
f"- GBP: {self.converter.format(total_cost, 'GBP')}",
|
|
526
|
+
f"- BRL: {self.converter.format(total_cost, 'BRL')}",
|
|
527
|
+
"",
|
|
528
|
+
"## Sessions",
|
|
529
|
+
"",
|
|
530
|
+
"| Date | Story | Tokens | Cost | Budget % |",
|
|
531
|
+
"|------|-------|--------|------|----------|",
|
|
532
|
+
]
|
|
533
|
+
|
|
534
|
+
for session in sessions:
|
|
535
|
+
lines.append(
|
|
536
|
+
f"| {session.start_time[:10]} | {session.story_key} | "
|
|
537
|
+
f"{session.total_tokens:,} | ${session.total_cost_usd:.2f} | "
|
|
538
|
+
f"{session.budget_used_percent:.0f}% |"
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
with open(path, "w") as f:
|
|
542
|
+
f.write("\n".join(lines))
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def main():
|
|
546
|
+
parser = argparse.ArgumentParser(
|
|
547
|
+
description="Cost Dashboard - View and manage cost data",
|
|
548
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
549
|
+
epilog="""
|
|
550
|
+
Examples:
|
|
551
|
+
python cost_dashboard.py # Show latest session
|
|
552
|
+
python cost_dashboard.py --history 10 # Show last 10 sessions
|
|
553
|
+
python cost_dashboard.py --summary # Show 30-day summary
|
|
554
|
+
python cost_dashboard.py --story 3-5 # Show costs for story
|
|
555
|
+
python cost_dashboard.py --export costs.csv # Export to CSV
|
|
556
|
+
python cost_dashboard.py --export report.md # Export to Markdown
|
|
557
|
+
""",
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
parser.add_argument("--history", "-H", type=int, metavar="N", help="Show last N sessions")
|
|
561
|
+
parser.add_argument("--summary", "-s", action="store_true", help="Show aggregate summary")
|
|
562
|
+
parser.add_argument(
|
|
563
|
+
"--days", "-d", type=int, default=30, help="Number of days for summary (default: 30)"
|
|
564
|
+
)
|
|
565
|
+
parser.add_argument("--story", type=str, metavar="KEY", help="Show costs for specific story")
|
|
566
|
+
parser.add_argument(
|
|
567
|
+
"--export", "-e", type=str, metavar="FILE", help="Export to file (.csv, .json, .md)"
|
|
568
|
+
)
|
|
569
|
+
parser.add_argument("--from-date", type=str, metavar="YYYY-MM-DD", help="Filter from date")
|
|
570
|
+
parser.add_argument("--to-date", type=str, metavar="YYYY-MM-DD", help="Filter to date")
|
|
571
|
+
|
|
572
|
+
args = parser.parse_args()
|
|
573
|
+
dashboard = CostDashboard()
|
|
574
|
+
|
|
575
|
+
# Get sessions
|
|
576
|
+
sessions = CostTracker.get_historical_sessions(days=args.days)
|
|
577
|
+
|
|
578
|
+
# Apply date filters
|
|
579
|
+
if args.from_date:
|
|
580
|
+
try:
|
|
581
|
+
from_dt = datetime.fromisoformat(args.from_date)
|
|
582
|
+
sessions = [s for s in sessions if datetime.fromisoformat(s.start_time) >= from_dt]
|
|
583
|
+
except ValueError:
|
|
584
|
+
print(f"{Colors.RED}Invalid from-date format. Use YYYY-MM-DD{Colors.RESET}")
|
|
585
|
+
return 1
|
|
586
|
+
|
|
587
|
+
if args.to_date:
|
|
588
|
+
try:
|
|
589
|
+
to_dt = datetime.fromisoformat(args.to_date)
|
|
590
|
+
sessions = [s for s in sessions if datetime.fromisoformat(s.start_time) <= to_dt]
|
|
591
|
+
except ValueError:
|
|
592
|
+
print(f"{Colors.RED}Invalid to-date format. Use YYYY-MM-DD{Colors.RESET}")
|
|
593
|
+
return 1
|
|
594
|
+
|
|
595
|
+
# Execute command
|
|
596
|
+
if args.export:
|
|
597
|
+
dashboard.export_data(args.export, sessions)
|
|
598
|
+
elif args.summary:
|
|
599
|
+
dashboard.show_summary(args.days)
|
|
600
|
+
elif args.story:
|
|
601
|
+
dashboard.show_story(args.story)
|
|
602
|
+
elif args.history:
|
|
603
|
+
dashboard.show_history(sessions, args.history)
|
|
604
|
+
else:
|
|
605
|
+
# Show latest session
|
|
606
|
+
if sessions:
|
|
607
|
+
dashboard.show_session(sessions[0])
|
|
608
|
+
else:
|
|
609
|
+
print(f"{Colors.YELLOW}No session data found.{Colors.RESET}")
|
|
610
|
+
print(f"Session data is stored in: {SESSIONS_DIR}")
|
|
611
|
+
return 1
|
|
612
|
+
|
|
613
|
+
return 0
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
if __name__ == "__main__":
|
|
617
|
+
sys.exit(main())
|