@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,222 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Cost Configuration - Settings for cost tracking system.
4
+
5
+ Loads configuration from environment variables and config files.
6
+
7
+ Usage:
8
+ from lib.cost_config import CostConfig, get_config
9
+
10
+ config = get_config()
11
+ print(config.budget_limit)
12
+ print(config.currency_rates)
13
+ """
14
+
15
+ import json
16
+ import os
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import Optional
20
+
21
+ # Default values
22
+ DEFAULT_BUDGET_LIMIT = 15.00
23
+ DEFAULT_WARNING_PERCENT = 75
24
+ DEFAULT_CRITICAL_PERCENT = 90
25
+ DEFAULT_AUTO_STOP = True
26
+
27
+ DEFAULT_CURRENCY_RATES = {
28
+ "USD": 1.0,
29
+ "EUR": 0.92,
30
+ "GBP": 0.79,
31
+ "BRL": 6.10,
32
+ "CAD": 1.36,
33
+ "AUD": 1.53,
34
+ "JPY": 149.50,
35
+ }
36
+
37
+ DEFAULT_DISPLAY_CURRENCIES = ["USD", "EUR", "GBP", "BRL"]
38
+
39
+
40
+ @dataclass
41
+ class CostConfig:
42
+ """Cost tracking configuration."""
43
+
44
+ # Budget settings
45
+ budget_context: float = 3.00
46
+ budget_dev: float = 15.00
47
+ budget_review: float = 5.00
48
+
49
+ # Alert thresholds
50
+ warning_percent: int = DEFAULT_WARNING_PERCENT
51
+ critical_percent: int = DEFAULT_CRITICAL_PERCENT
52
+ auto_stop: bool = DEFAULT_AUTO_STOP
53
+
54
+ # Currency settings
55
+ display_currency: str = "USD"
56
+ currency_rates: dict[str, float] = field(default_factory=lambda: DEFAULT_CURRENCY_RATES.copy())
57
+ display_currencies: list[str] = field(default_factory=lambda: DEFAULT_DISPLAY_CURRENCIES.copy())
58
+
59
+ @classmethod
60
+ def from_env(cls) -> "CostConfig":
61
+ """Load configuration from environment variables."""
62
+ config = cls()
63
+
64
+ # Budget limits
65
+ if os.getenv("MAX_BUDGET_CONTEXT"):
66
+ config.budget_context = float(os.getenv("MAX_BUDGET_CONTEXT"))
67
+ if os.getenv("MAX_BUDGET_DEV"):
68
+ config.budget_dev = float(os.getenv("MAX_BUDGET_DEV"))
69
+ if os.getenv("MAX_BUDGET_REVIEW"):
70
+ config.budget_review = float(os.getenv("MAX_BUDGET_REVIEW"))
71
+
72
+ # Alert thresholds
73
+ if os.getenv("COST_WARNING_PERCENT"):
74
+ config.warning_percent = int(os.getenv("COST_WARNING_PERCENT"))
75
+ if os.getenv("COST_CRITICAL_PERCENT"):
76
+ config.critical_percent = int(os.getenv("COST_CRITICAL_PERCENT"))
77
+ if os.getenv("COST_AUTO_STOP"):
78
+ config.auto_stop = os.getenv("COST_AUTO_STOP").lower() in ("true", "1", "yes")
79
+
80
+ # Currency settings
81
+ if os.getenv("COST_DISPLAY_CURRENCY"):
82
+ config.display_currency = os.getenv("COST_DISPLAY_CURRENCY")
83
+
84
+ # Currency rates from environment
85
+ if os.getenv("CURRENCY_RATE_EUR"):
86
+ config.currency_rates["EUR"] = float(os.getenv("CURRENCY_RATE_EUR"))
87
+ if os.getenv("CURRENCY_RATE_GBP"):
88
+ config.currency_rates["GBP"] = float(os.getenv("CURRENCY_RATE_GBP"))
89
+ if os.getenv("CURRENCY_RATE_BRL"):
90
+ config.currency_rates["BRL"] = float(os.getenv("CURRENCY_RATE_BRL"))
91
+
92
+ return config
93
+
94
+ @classmethod
95
+ def from_file(cls, config_path: Path) -> "CostConfig":
96
+ """Load configuration from JSON file."""
97
+ config = cls()
98
+
99
+ if not config_path.exists():
100
+ return config
101
+
102
+ try:
103
+ with open(config_path) as f:
104
+ data = json.load(f)
105
+
106
+ # Budget limits
107
+ if "budget_context" in data:
108
+ config.budget_context = float(data["budget_context"])
109
+ if "budget_dev" in data:
110
+ config.budget_dev = float(data["budget_dev"])
111
+ if "budget_review" in data:
112
+ config.budget_review = float(data["budget_review"])
113
+
114
+ # Alert thresholds
115
+ if "warning_percent" in data:
116
+ config.warning_percent = int(data["warning_percent"])
117
+ if "critical_percent" in data:
118
+ config.critical_percent = int(data["critical_percent"])
119
+ if "auto_stop" in data:
120
+ config.auto_stop = bool(data["auto_stop"])
121
+
122
+ # Currency settings
123
+ if "display_currency" in data:
124
+ config.display_currency = data["display_currency"]
125
+ if "currency_rates" in data:
126
+ config.currency_rates.update(data["currency_rates"])
127
+ if "display_currencies" in data:
128
+ config.display_currencies = data["display_currencies"]
129
+
130
+ except Exception as e:
131
+ print(f"Warning: Could not load config file: {e}")
132
+
133
+ return config
134
+
135
+ def save(self, config_path: Path):
136
+ """Save configuration to JSON file."""
137
+ data = {
138
+ "budget_context": self.budget_context,
139
+ "budget_dev": self.budget_dev,
140
+ "budget_review": self.budget_review,
141
+ "warning_percent": self.warning_percent,
142
+ "critical_percent": self.critical_percent,
143
+ "auto_stop": self.auto_stop,
144
+ "display_currency": self.display_currency,
145
+ "currency_rates": self.currency_rates,
146
+ "display_currencies": self.display_currencies,
147
+ }
148
+
149
+ config_path.parent.mkdir(parents=True, exist_ok=True)
150
+
151
+ with open(config_path, "w") as f:
152
+ json.dump(data, f, indent=2)
153
+
154
+ def get_budget_for_phase(self, phase: str) -> float:
155
+ """Get budget limit for a specific phase."""
156
+ phase = phase.lower()
157
+ if phase in ("context", "sm"):
158
+ return self.budget_context
159
+ elif phase in ("dev", "development", "implement"):
160
+ return self.budget_dev
161
+ elif phase in ("review", "qa"):
162
+ return self.budget_review
163
+ return self.budget_dev # Default
164
+
165
+ def get_thresholds(self) -> dict[str, float]:
166
+ """Get budget thresholds as decimal values."""
167
+ return {
168
+ "warning": self.warning_percent / 100.0,
169
+ "critical": self.critical_percent / 100.0,
170
+ "stop": 1.0,
171
+ }
172
+
173
+
174
+ # Global configuration instance
175
+ _config: Optional[CostConfig] = None
176
+
177
+
178
+ def get_config() -> CostConfig:
179
+ """Get or create the global configuration instance."""
180
+ global _config
181
+ if _config is None:
182
+ # Try to load from file first, then overlay with env vars
183
+ config_file = Path(__file__).parent.parent.parent / ".automation" / "costs" / "config.json"
184
+ if config_file.exists():
185
+ _config = CostConfig.from_file(config_file)
186
+ else:
187
+ _config = CostConfig.from_env()
188
+ return _config
189
+
190
+
191
+ def set_config(config: CostConfig):
192
+ """Set the global configuration instance."""
193
+ global _config
194
+ _config = config
195
+
196
+
197
+ def reset_config():
198
+ """Reset the global configuration instance."""
199
+ global _config
200
+ _config = None
201
+
202
+
203
+ if __name__ == "__main__":
204
+ # Demo/test
205
+ config = get_config()
206
+
207
+ print("Cost Configuration")
208
+ print("=" * 40)
209
+ print(f"Budget - Context: ${config.budget_context:.2f}")
210
+ print(f"Budget - Dev: ${config.budget_dev:.2f}")
211
+ print(f"Budget - Review: ${config.budget_review:.2f}")
212
+ print()
213
+ print(f"Warning at: {config.warning_percent}%")
214
+ print(f"Critical at: {config.critical_percent}%")
215
+ print(f"Auto-stop: {config.auto_stop}")
216
+ print()
217
+ print(f"Display Currency: {config.display_currency}")
218
+ print(f"Display Currencies: {config.display_currencies}")
219
+ print()
220
+ print("Currency Rates:")
221
+ for code, rate in config.currency_rates.items():
222
+ print(f" {code}: {rate}")
@@ -0,0 +1,443 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Cost Display - Real-time terminal display for cost monitoring.
4
+
5
+ Provides a rich, updating display of cost information during agent runs.
6
+
7
+ Usage:
8
+ from lib.cost_display import CostDisplay
9
+ from lib.cost_tracker import CostTracker
10
+
11
+ tracker = CostTracker(story_key="3-5", budget_limit_usd=15.00)
12
+ display = CostDisplay(tracker)
13
+ display.refresh() # Update display
14
+ """
15
+
16
+ import os
17
+ import sys
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+ from typing import Optional
21
+
22
+ # Add parent for imports
23
+ sys.path.insert(0, str(Path(__file__).parent))
24
+
25
+ from cost_tracker import PRICING, CostTracker
26
+ from currency_converter import CurrencyConverter, get_converter
27
+
28
+
29
+ class Colors:
30
+ """ANSI color codes for terminal output."""
31
+
32
+ # Reset
33
+ RESET = "\033[0m"
34
+
35
+ # Regular colors
36
+ BLACK = "\033[30m"
37
+ RED = "\033[31m"
38
+ GREEN = "\033[32m"
39
+ YELLOW = "\033[33m"
40
+ BLUE = "\033[34m"
41
+ MAGENTA = "\033[35m"
42
+ CYAN = "\033[36m"
43
+ WHITE = "\033[37m"
44
+
45
+ # Bold colors
46
+ BOLD = "\033[1m"
47
+ BOLD_RED = "\033[1;31m"
48
+ BOLD_GREEN = "\033[1;32m"
49
+ BOLD_YELLOW = "\033[1;33m"
50
+ BOLD_BLUE = "\033[1;34m"
51
+ BOLD_CYAN = "\033[1;36m"
52
+ BOLD_WHITE = "\033[1;37m"
53
+
54
+ # Background
55
+ BG_RED = "\033[41m"
56
+ BG_GREEN = "\033[42m"
57
+ BG_YELLOW = "\033[43m"
58
+
59
+ # Dim
60
+ DIM = "\033[2m"
61
+
62
+ @staticmethod
63
+ def strip(text: str) -> str:
64
+ """Remove ANSI codes from text."""
65
+ import re
66
+
67
+ return re.sub(r"\033\[[0-9;]*m", "", text)
68
+
69
+
70
+ class CostDisplay:
71
+ """
72
+ Real-time cost display for terminal.
73
+
74
+ Shows live updating cost information, token usage, and budget status.
75
+ """
76
+
77
+ # Box drawing characters
78
+ BOX_TOP_LEFT = "╔"
79
+ BOX_TOP_RIGHT = "╗"
80
+ BOX_BOTTOM_LEFT = "╚"
81
+ BOX_BOTTOM_RIGHT = "╝"
82
+ BOX_HORIZONTAL = "═"
83
+ BOX_VERTICAL = "║"
84
+ BOX_T_LEFT = "╠"
85
+ BOX_T_RIGHT = "╣"
86
+ BOX_LINE = "─"
87
+
88
+ def __init__(
89
+ self,
90
+ tracker: CostTracker,
91
+ converter: Optional[CurrencyConverter] = None,
92
+ width: int = 70,
93
+ compact: bool = False,
94
+ display_currency: Optional[str] = None,
95
+ ):
96
+ """
97
+ Initialize display.
98
+
99
+ Args:
100
+ tracker: CostTracker instance to display
101
+ converter: CurrencyConverter for multi-currency display
102
+ width: Display width in characters
103
+ compact: Use compact display mode
104
+ display_currency: Single currency to display (e.g., "USD", "EUR")
105
+ If None, shows all currencies
106
+ """
107
+ self.tracker = tracker
108
+ self.converter = converter or get_converter()
109
+ self.width = width
110
+ self.compact = compact
111
+ self.start_time = datetime.now()
112
+ self.last_refresh = None
113
+
114
+ # Get display currency from environment or parameter
115
+ self.display_currency = display_currency or os.environ.get("COST_DISPLAY_CURRENCY")
116
+
117
+ def _box_line(self, left: str, right: str, fill: str = BOX_HORIZONTAL) -> str:
118
+ """Create a box line."""
119
+ return f"{left}{fill * (self.width - 2)}{right}"
120
+
121
+ def _content_line(self, content: str, align: str = "left") -> str:
122
+ """Create a content line within the box."""
123
+ # Remove color codes for length calculation
124
+ clean_content = Colors.strip(content)
125
+ padding = self.width - 4 - len(clean_content)
126
+
127
+ if align == "center":
128
+ left_pad = padding // 2
129
+ right_pad = padding - left_pad
130
+ return f"{self.BOX_VERTICAL} {' ' * left_pad}{content}{' ' * right_pad} {self.BOX_VERTICAL}"
131
+ elif align == "right":
132
+ return f"{self.BOX_VERTICAL} {' ' * padding}{content} {self.BOX_VERTICAL}"
133
+ else: # left
134
+ return f"{self.BOX_VERTICAL} {content}{' ' * padding} {self.BOX_VERTICAL}"
135
+
136
+ def _empty_line(self) -> str:
137
+ """Create an empty content line."""
138
+ return self._content_line("")
139
+
140
+ def _section_header(self, title: str) -> str:
141
+ """Create a section header."""
142
+ line = f"{self.BOX_LINE * 3} {title} "
143
+ remaining = self.width - 6 - len(title)
144
+ return self._content_line(f"{Colors.BOLD}{line}{self.BOX_LINE * remaining}{Colors.RESET}")
145
+
146
+ def _progress_bar(self, percent: float, width: int = 40) -> str:
147
+ """Create a progress bar."""
148
+ filled = int((percent / 100) * width)
149
+ empty = width - filled
150
+
151
+ # Color based on percentage
152
+ if percent >= 90:
153
+ color = Colors.RED
154
+ elif percent >= 75:
155
+ color = Colors.YELLOW
156
+ else:
157
+ color = Colors.GREEN
158
+
159
+ bar = f"{color}{'█' * filled}{'░' * empty}{Colors.RESET}"
160
+ return f"[{bar}] {percent:.0f}%"
161
+
162
+ def _format_tokens(self, tokens: int) -> str:
163
+ """Format token count with K/M suffix."""
164
+ if tokens >= 1_000_000:
165
+ return f"{tokens / 1_000_000:.1f}M"
166
+ elif tokens >= 1_000:
167
+ return f"{tokens / 1_000:.1f}K"
168
+ return str(tokens)
169
+
170
+ def _format_elapsed(self) -> str:
171
+ """Format elapsed time."""
172
+ elapsed = datetime.now() - self.start_time
173
+ minutes = int(elapsed.total_seconds() // 60)
174
+ seconds = int(elapsed.total_seconds() % 60)
175
+ return f"{minutes:02d}:{seconds:02d}"
176
+
177
+ def _get_budget_color(self, percent: float) -> str:
178
+ """Get color based on budget usage."""
179
+ if percent >= 90:
180
+ return Colors.BOLD_RED
181
+ elif percent >= 75:
182
+ return Colors.BOLD_YELLOW
183
+ return Colors.BOLD_GREEN
184
+
185
+ def render(self) -> str:
186
+ """Render the full display as a string."""
187
+ session = self.tracker.session
188
+ lines = []
189
+
190
+ # Header
191
+ lines.append(
192
+ f"{Colors.CYAN}{self._box_line(self.BOX_TOP_LEFT, self.BOX_TOP_RIGHT)}{Colors.RESET}"
193
+ )
194
+ title = f"COST MONITOR - Story: {session.story_key}"
195
+ lines.append(
196
+ f"{Colors.CYAN}{self._content_line(Colors.BOLD + title + Colors.RESET, 'center')}{Colors.RESET}"
197
+ )
198
+ lines.append(
199
+ f"{Colors.CYAN}{self._box_line(self.BOX_T_LEFT, self.BOX_T_RIGHT)}{Colors.RESET}"
200
+ )
201
+
202
+ # Current Session Info
203
+ lines.append(self._empty_line())
204
+ lines.append(self._section_header("CURRENT SESSION"))
205
+
206
+ agent = self.tracker.current_agent or "N/A"
207
+ model = self.tracker.current_model or "N/A"
208
+ elapsed = self._format_elapsed()
209
+
210
+ lines.append(
211
+ self._content_line(
212
+ f"Agent: {Colors.BOLD_CYAN}{agent:20}{Colors.RESET} Model: {Colors.BOLD_BLUE}{model}{Colors.RESET}"
213
+ )
214
+ )
215
+ lines.append(
216
+ self._content_line(
217
+ f"Status: {Colors.BOLD_GREEN}Running{Colors.RESET} Elapsed: {Colors.BOLD}{elapsed}{Colors.RESET}"
218
+ )
219
+ )
220
+
221
+ # Tokens
222
+ lines.append(self._empty_line())
223
+ lines.append(self._section_header("TOKENS"))
224
+
225
+ input_tokens = self._format_tokens(session.total_input_tokens)
226
+ output_tokens = self._format_tokens(session.total_output_tokens)
227
+ total_tokens = self._format_tokens(session.total_tokens)
228
+
229
+ lines.append(
230
+ self._content_line(
231
+ f"Input: {Colors.BOLD}{input_tokens:>12} tokens{Colors.RESET} "
232
+ f"Output: {Colors.BOLD}{output_tokens:>12} tokens{Colors.RESET}"
233
+ )
234
+ )
235
+ lines.append(
236
+ self._content_line(
237
+ f"Total: {Colors.BOLD_WHITE}{total_tokens:>12} tokens{Colors.RESET}"
238
+ )
239
+ )
240
+
241
+ # Cost Breakdown by Agent
242
+ if not self.compact:
243
+ lines.append(self._empty_line())
244
+ lines.append(self._section_header("COST BREAKDOWN"))
245
+
246
+ # Table header
247
+ lines.append(
248
+ self._content_line(
249
+ f"{Colors.DIM}{'Agent':<10} {'Model':<10} {'Input $':>10} {'Output $':>10} {'Total':>10}{Colors.RESET}"
250
+ )
251
+ )
252
+ lines.append(self._content_line(f"{Colors.DIM}{self.BOX_LINE * 54}{Colors.RESET}"))
253
+
254
+ # Aggregate by agent+model
255
+ breakdown = {}
256
+ for entry in session.entries:
257
+ key = (entry.agent, entry.model)
258
+ if key not in breakdown:
259
+ breakdown[key] = {
260
+ "input": 0,
261
+ "output": 0,
262
+ "cost": 0,
263
+ "input_tokens": 0,
264
+ "output_tokens": 0,
265
+ }
266
+
267
+ breakdown[key]["input_tokens"] += entry.input_tokens
268
+ breakdown[key]["output_tokens"] += entry.output_tokens
269
+ breakdown[key]["cost"] += entry.cost_usd
270
+
271
+ # Calculate actual input/output costs based on model pricing
272
+ for (_agent, model), data in breakdown.items():
273
+ model_lower = model.lower()
274
+ pricing = PRICING.get(model_lower, PRICING.get("sonnet"))
275
+ if pricing:
276
+ input_cost = (data["input_tokens"] / 1_000_000) * pricing["input"]
277
+ output_cost = (data["output_tokens"] / 1_000_000) * pricing["output"]
278
+ else:
279
+ # Fallback: estimate based on token ratio
280
+ total_tokens = data["input_tokens"] + data["output_tokens"]
281
+ if total_tokens > 0:
282
+ input_ratio = data["input_tokens"] / total_tokens
283
+ input_cost = data["cost"] * input_ratio
284
+ output_cost = data["cost"] * (1 - input_ratio)
285
+ else:
286
+ input_cost = output_cost = 0
287
+ data["input"] = input_cost
288
+ data["output"] = output_cost
289
+
290
+ for (agent, model), data in breakdown.items():
291
+ lines.append(
292
+ self._content_line(
293
+ f"{agent:<10} {model:<10} "
294
+ f"${data['input']:>8.2f} ${data['output']:>8.2f} "
295
+ f"{Colors.BOLD}${data['cost']:>8.2f}{Colors.RESET}"
296
+ )
297
+ )
298
+
299
+ # Total row
300
+ total_cost = session.total_cost_usd
301
+ lines.append(self._content_line(f"{Colors.DIM}{self.BOX_LINE * 54}{Colors.RESET}"))
302
+ lines.append(
303
+ self._content_line(
304
+ f"{Colors.BOLD}{'TOTAL':<10} {'':10} "
305
+ f"{'':>10} {'':>10} ${total_cost:>8.2f}{Colors.RESET}"
306
+ )
307
+ )
308
+
309
+ # Budget
310
+ lines.append(self._empty_line())
311
+ lines.append(self._section_header("BUDGET"))
312
+
313
+ budget_pct = session.budget_used_percent
314
+ budget_color = self._get_budget_color(budget_pct)
315
+
316
+ lines.append(
317
+ self._content_line(
318
+ f"Limit: {Colors.BOLD}${session.budget_limit_usd:.2f}{Colors.RESET} "
319
+ f"Used: {budget_color}${session.total_cost_usd:.2f}{Colors.RESET} "
320
+ f"Remaining: {Colors.BOLD}${session.budget_remaining:.2f}{Colors.RESET}"
321
+ )
322
+ )
323
+ lines.append(self._content_line(self._progress_bar(budget_pct)))
324
+
325
+ # Budget status message
326
+ ok, level, msg = self.tracker.check_budget()
327
+ if level == "critical":
328
+ lines.append(self._content_line(f"{Colors.BG_RED}{Colors.WHITE} {msg} {Colors.RESET}"))
329
+ elif level == "warning":
330
+ lines.append(self._content_line(f"{Colors.YELLOW}{msg}{Colors.RESET}"))
331
+
332
+ # Currency display
333
+ lines.append(self._empty_line())
334
+ if self.display_currency:
335
+ # Show single selected currency
336
+ lines.append(self._section_header("COST"))
337
+ formatted = self.converter.format(session.total_cost_usd, self.display_currency)
338
+ lines.append(self._content_line(f"Total: {Colors.BOLD_GREEN}{formatted}{Colors.RESET}"))
339
+ else:
340
+ # Show all currencies
341
+ lines.append(self._section_header("MULTI-CURRENCY"))
342
+ lines.append(
343
+ self._content_line(self.converter.format_all(session.total_cost_usd, " │ "))
344
+ )
345
+
346
+ # Footer
347
+ lines.append(self._empty_line())
348
+ lines.append(
349
+ f"{Colors.CYAN}{self._box_line(self.BOX_BOTTOM_LEFT, self.BOX_BOTTOM_RIGHT)}{Colors.RESET}"
350
+ )
351
+ lines.append(f"{Colors.DIM}Press Ctrl+C to stop monitoring{Colors.RESET}")
352
+
353
+ return "\n".join(lines)
354
+
355
+ def refresh(self, clear: bool = True):
356
+ """Refresh the display."""
357
+ if clear:
358
+ self.clear_screen()
359
+
360
+ print(self.render())
361
+ self.last_refresh = datetime.now()
362
+
363
+ def clear_screen(self):
364
+ """Clear the terminal screen."""
365
+ if sys.platform == "win32":
366
+ os.system("cls")
367
+ else:
368
+ os.system("clear")
369
+ # Alternative: print ANSI escape
370
+ # print('\033[2J\033[H', end='')
371
+
372
+ def update_in_place(self):
373
+ """Update display in place without full clear."""
374
+ # Move cursor to top
375
+ lines = self.render().count("\n") + 1
376
+ print(f"\033[{lines}A", end="")
377
+ print(self.render())
378
+
379
+
380
+ class CompactCostDisplay:
381
+ """Compact single-line cost display for inline monitoring."""
382
+
383
+ def __init__(
384
+ self,
385
+ tracker: CostTracker,
386
+ converter: Optional[CurrencyConverter] = None,
387
+ display_currency: Optional[str] = None,
388
+ ):
389
+ self.tracker = tracker
390
+ self.converter = converter or get_converter()
391
+ # Get display currency from environment or parameter
392
+ self.display_currency = display_currency or os.environ.get("COST_DISPLAY_CURRENCY", "USD")
393
+
394
+ def render(self) -> str:
395
+ """Render compact display."""
396
+ session = self.tracker.session
397
+ pct = session.budget_used_percent
398
+
399
+ # Color based on budget
400
+ if pct >= 90:
401
+ color = Colors.RED
402
+ elif pct >= 75:
403
+ color = Colors.YELLOW
404
+ else:
405
+ color = Colors.GREEN
406
+
407
+ tokens = f"{session.total_tokens:,}"
408
+ cost = self.converter.format(session.total_cost_usd, self.display_currency)
409
+ budget = f"{pct:.0f}%"
410
+
411
+ return (
412
+ f"{Colors.BOLD}Cost:{Colors.RESET} {color}{cost}{Colors.RESET} "
413
+ f"({budget}) │ "
414
+ f"{Colors.BOLD}Tokens:{Colors.RESET} {tokens}"
415
+ )
416
+
417
+ def print(self):
418
+ """Print compact display."""
419
+ print(f"\r{self.render()}", end="", flush=True)
420
+
421
+
422
+ if __name__ == "__main__":
423
+ # Demo
424
+ from cost_tracker import CostTracker
425
+
426
+ tracker = CostTracker(story_key="demo-story", budget_limit_usd=15.00)
427
+
428
+ # Simulate usage
429
+ tracker.log_usage("SM", "sonnet", 15000, 3000)
430
+ tracker.set_current_agent("DEV", "opus")
431
+ tracker.log_usage("DEV", "opus", 50000, 15000)
432
+
433
+ # Full display
434
+ display = CostDisplay(tracker)
435
+ display.refresh()
436
+
437
+ print("\n" + "=" * 70 + "\n")
438
+
439
+ # Compact display
440
+ compact = CompactCostDisplay(tracker)
441
+ print("Compact: ", end="")
442
+ compact.print()
443
+ print()