@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +526 -0
  2. package/LICENSE +21 -0
  3. package/README.md +620 -0
  4. package/bin/devflow-checkpoint.js +10 -0
  5. package/bin/devflow-collab.js +10 -0
  6. package/bin/devflow-cost.js +10 -0
  7. package/bin/devflow-create-persona.js +10 -0
  8. package/bin/devflow-init.js +10 -0
  9. package/bin/devflow-memory.js +10 -0
  10. package/bin/devflow-new-doc.js +10 -0
  11. package/bin/devflow-personalize.js +10 -0
  12. package/bin/devflow-setup-checkpoint.js +10 -0
  13. package/bin/devflow-story.js +10 -0
  14. package/bin/devflow-tech-debt.js +10 -0
  15. package/bin/devflow-validate-overrides.js +10 -0
  16. package/bin/devflow-validate.js +10 -0
  17. package/bin/devflow-version.js +10 -0
  18. package/lib/constants.js +30 -0
  19. package/lib/exec-python.js +78 -0
  20. package/lib/python-check.js +178 -0
  21. package/package.json +64 -0
  22. package/tooling/.automation/agents/architect.md +135 -0
  23. package/tooling/.automation/agents/ba.md +70 -0
  24. package/tooling/.automation/agents/dev.md +79 -0
  25. package/tooling/.automation/agents/maintainer.md +97 -0
  26. package/tooling/.automation/agents/pm.md +116 -0
  27. package/tooling/.automation/agents/reviewer.md +141 -0
  28. package/tooling/.automation/agents/sm.md +61 -0
  29. package/tooling/.automation/agents/writer.md +193 -0
  30. package/tooling/.automation/config.ps1.template +61 -0
  31. package/tooling/.automation/config.sh.template +48 -0
  32. package/tooling/.automation/memory/.gitkeep +6 -0
  33. package/tooling/.automation/memory/knowledge/kg_integration-test.json +94 -0
  34. package/tooling/.automation/memory/knowledge/kg_test-story.json +300 -0
  35. package/tooling/.automation/memory/shared/shared_integration-test.json +30 -0
  36. package/tooling/.automation/memory/shared/shared_test-story.json +78 -0
  37. package/tooling/.automation/overrides/templates/README.md +113 -0
  38. package/tooling/.automation/overrides/templates/architect/README.md +27 -0
  39. package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +92 -0
  40. package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +85 -0
  41. package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +88 -0
  42. package/tooling/.automation/overrides/templates/ba/README.md +27 -0
  43. package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +86 -0
  44. package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +91 -0
  45. package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +89 -0
  46. package/tooling/.automation/overrides/templates/dev/README.md +32 -0
  47. package/tooling/.automation/overrides/templates/dev/junior-mentored.yaml +39 -0
  48. package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +43 -0
  49. package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +52 -0
  50. package/tooling/.automation/overrides/templates/dev/security-focused.yaml +43 -0
  51. package/tooling/.automation/overrides/templates/dev/senior-fullstack.yaml +39 -0
  52. package/tooling/.automation/overrides/templates/maintainer/README.md +27 -0
  53. package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +113 -0
  54. package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +94 -0
  55. package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +94 -0
  56. package/tooling/.automation/overrides/templates/pm/README.md +27 -0
  57. package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +91 -0
  58. package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +87 -0
  59. package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +91 -0
  60. package/tooling/.automation/overrides/templates/reviewer/README.md +11 -0
  61. package/tooling/.automation/overrides/templates/reviewer/mentoring-reviewer.yaml +45 -0
  62. package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +50 -0
  63. package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +48 -0
  64. package/tooling/.automation/overrides/templates/sm/README.md +11 -0
  65. package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +52 -0
  66. package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +50 -0
  67. package/tooling/.automation/overrides/templates/sm/technical-lead.yaml +47 -0
  68. package/tooling/.automation/overrides/templates/user-profile.template.yaml +62 -0
  69. package/tooling/.automation/overrides/templates/writer/README.md +27 -0
  70. package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +99 -0
  71. package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +108 -0
  72. package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +100 -0
  73. package/tooling/completions/DevflowCompletion.ps1 +213 -0
  74. package/tooling/completions/_run-story +116 -0
  75. package/tooling/completions/run-story-completion.bash +136 -0
  76. package/tooling/docs/DOC-STANDARD.md +717 -0
  77. package/tooling/docs/sprint-status.yaml.template +24 -0
  78. package/tooling/docs/templates/bug-report.md +234 -0
  79. package/tooling/docs/templates/migration-spec.md +274 -0
  80. package/tooling/docs/templates/refactor-spec.md +86 -0
  81. package/tooling/docs/templates/tech-debt.md +86 -0
  82. package/tooling/scripts/context_checkpoint.py +556 -0
  83. package/tooling/scripts/cost_dashboard.py +617 -0
  84. package/tooling/scripts/create-persona.py +690 -0
  85. package/tooling/scripts/create-persona.sh +435 -0
  86. package/tooling/scripts/init-project-workflow.ps1 +651 -0
  87. package/tooling/scripts/init-project-workflow.py +70 -0
  88. package/tooling/scripts/init-project-workflow.sh +746 -0
  89. package/tooling/scripts/lib/__init__.py +35 -0
  90. package/tooling/scripts/lib/agent_handoff.py +526 -0
  91. package/tooling/scripts/lib/agent_router.py +698 -0
  92. package/tooling/scripts/lib/checkpoint-integration.ps1 +245 -0
  93. package/tooling/scripts/lib/checkpoint-integration.sh +191 -0
  94. package/tooling/scripts/lib/claude-cli.ps1 +952 -0
  95. package/tooling/scripts/lib/claude-cli.sh +1293 -0
  96. package/tooling/scripts/lib/cost_config.py +222 -0
  97. package/tooling/scripts/lib/cost_display.py +443 -0
  98. package/tooling/scripts/lib/cost_tracker.py +710 -0
  99. package/tooling/scripts/lib/currency_converter.py +328 -0
  100. package/tooling/scripts/lib/errors.py +438 -0
  101. package/tooling/scripts/lib/override-loader.sh +286 -0
  102. package/tooling/scripts/lib/pair_programming.py +589 -0
  103. package/tooling/scripts/lib/shared_memory.py +637 -0
  104. package/tooling/scripts/lib/swarm_orchestrator.py +689 -0
  105. package/tooling/scripts/memory_summarize.py +324 -0
  106. package/tooling/scripts/new-doc.ps1 +405 -0
  107. package/tooling/scripts/new-doc.py +93 -0
  108. package/tooling/scripts/new-doc.sh +534 -0
  109. package/tooling/scripts/personalize_agent.py +385 -0
  110. package/tooling/scripts/rollback-migration.sh +540 -0
  111. package/tooling/scripts/run-collab.ps1 +251 -0
  112. package/tooling/scripts/run-collab.py +605 -0
  113. package/tooling/scripts/run-collab.sh +110 -0
  114. package/tooling/scripts/run-story.ps1 +490 -0
  115. package/tooling/scripts/run-story.py +387 -0
  116. package/tooling/scripts/run-story.sh +467 -0
  117. package/tooling/scripts/setup-checkpoint-service.ps1 +219 -0
  118. package/tooling/scripts/setup-checkpoint-service.py +87 -0
  119. package/tooling/scripts/setup-checkpoint-service.sh +236 -0
  120. package/tooling/scripts/tech-debt-tracker.py +608 -0
  121. package/tooling/scripts/update_version.py +244 -0
  122. package/tooling/scripts/validate-overrides.py +511 -0
  123. package/tooling/scripts/validate-overrides.sh +432 -0
  124. 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())