@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,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()
|