foundry-mcp 0.3.3__py3-none-any.whl
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.
- foundry_mcp/__init__.py +7 -0
- foundry_mcp/cli/__init__.py +80 -0
- foundry_mcp/cli/__main__.py +9 -0
- foundry_mcp/cli/agent.py +96 -0
- foundry_mcp/cli/commands/__init__.py +37 -0
- foundry_mcp/cli/commands/cache.py +137 -0
- foundry_mcp/cli/commands/dashboard.py +148 -0
- foundry_mcp/cli/commands/dev.py +446 -0
- foundry_mcp/cli/commands/journal.py +377 -0
- foundry_mcp/cli/commands/lifecycle.py +274 -0
- foundry_mcp/cli/commands/modify.py +824 -0
- foundry_mcp/cli/commands/plan.py +633 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +652 -0
- foundry_mcp/cli/commands/session.py +479 -0
- foundry_mcp/cli/commands/specs.py +856 -0
- foundry_mcp/cli/commands/tasks.py +807 -0
- foundry_mcp/cli/commands/testing.py +676 -0
- foundry_mcp/cli/commands/validate.py +982 -0
- foundry_mcp/cli/config.py +98 -0
- foundry_mcp/cli/context.py +259 -0
- foundry_mcp/cli/flags.py +266 -0
- foundry_mcp/cli/logging.py +212 -0
- foundry_mcp/cli/main.py +44 -0
- foundry_mcp/cli/output.py +122 -0
- foundry_mcp/cli/registry.py +110 -0
- foundry_mcp/cli/resilience.py +178 -0
- foundry_mcp/cli/transcript.py +217 -0
- foundry_mcp/config.py +850 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1636 -0
- foundry_mcp/core/cache.py +195 -0
- foundry_mcp/core/capabilities.py +446 -0
- foundry_mcp/core/concurrency.py +898 -0
- foundry_mcp/core/context.py +540 -0
- foundry_mcp/core/discovery.py +1603 -0
- foundry_mcp/core/error_collection.py +728 -0
- foundry_mcp/core/error_store.py +592 -0
- foundry_mcp/core/feature_flags.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/journal.py +694 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1350 -0
- foundry_mcp/core/llm_patterns.py +510 -0
- foundry_mcp/core/llm_provider.py +1569 -0
- foundry_mcp/core/logging_config.py +374 -0
- foundry_mcp/core/metrics_persistence.py +584 -0
- foundry_mcp/core/metrics_registry.py +327 -0
- foundry_mcp/core/metrics_store.py +641 -0
- foundry_mcp/core/modifications.py +224 -0
- foundry_mcp/core/naming.py +123 -0
- foundry_mcp/core/observability.py +1216 -0
- foundry_mcp/core/otel.py +452 -0
- foundry_mcp/core/otel_stubs.py +264 -0
- foundry_mcp/core/pagination.py +255 -0
- foundry_mcp/core/progress.py +317 -0
- foundry_mcp/core/prometheus.py +577 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +546 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
- foundry_mcp/core/prompts/plan_review.py +623 -0
- foundry_mcp/core/providers/__init__.py +225 -0
- foundry_mcp/core/providers/base.py +476 -0
- foundry_mcp/core/providers/claude.py +460 -0
- foundry_mcp/core/providers/codex.py +619 -0
- foundry_mcp/core/providers/cursor_agent.py +642 -0
- foundry_mcp/core/providers/detectors.py +488 -0
- foundry_mcp/core/providers/gemini.py +405 -0
- foundry_mcp/core/providers/opencode.py +616 -0
- foundry_mcp/core/providers/opencode_wrapper.js +302 -0
- foundry_mcp/core/providers/package-lock.json +24 -0
- foundry_mcp/core/providers/package.json +25 -0
- foundry_mcp/core/providers/registry.py +607 -0
- foundry_mcp/core/providers/test_provider.py +171 -0
- foundry_mcp/core/providers/validation.py +729 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +934 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +1650 -0
- foundry_mcp/core/task.py +1289 -0
- foundry_mcp/core/testing.py +450 -0
- foundry_mcp/core/validation.py +2081 -0
- foundry_mcp/dashboard/__init__.py +32 -0
- foundry_mcp/dashboard/app.py +119 -0
- foundry_mcp/dashboard/components/__init__.py +17 -0
- foundry_mcp/dashboard/components/cards.py +88 -0
- foundry_mcp/dashboard/components/charts.py +234 -0
- foundry_mcp/dashboard/components/filters.py +136 -0
- foundry_mcp/dashboard/components/tables.py +195 -0
- foundry_mcp/dashboard/data/__init__.py +11 -0
- foundry_mcp/dashboard/data/stores.py +433 -0
- foundry_mcp/dashboard/launcher.py +289 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +174 -0
- foundry_mcp/dashboard/views/overview.py +160 -0
- foundry_mcp/dashboard/views/providers.py +83 -0
- foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
- foundry_mcp/dashboard/views/tool_usage.py +139 -0
- foundry_mcp/prompts/__init__.py +9 -0
- foundry_mcp/prompts/workflows.py +525 -0
- foundry_mcp/resources/__init__.py +9 -0
- foundry_mcp/resources/specs.py +591 -0
- foundry_mcp/schemas/__init__.py +38 -0
- foundry_mcp/schemas/sdd-spec-schema.json +386 -0
- foundry_mcp/server.py +164 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +71 -0
- foundry_mcp/tools/unified/authoring.py +1487 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +198 -0
- foundry_mcp/tools/unified/environment.py +939 -0
- foundry_mcp/tools/unified/error.py +462 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +632 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +745 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +629 -0
- foundry_mcp/tools/unified/review.py +685 -0
- foundry_mcp/tools/unified/review_helpers.py +299 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +580 -0
- foundry_mcp/tools/unified/spec.py +808 -0
- foundry_mcp/tools/unified/task.py +2202 -0
- foundry_mcp/tools/unified/test.py +370 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.3.3.dist-info/METADATA +337 -0
- foundry_mcp-0.3.3.dist-info/RECORD +135 -0
- foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
- foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
- foundry_mcp-0.3.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Parse Claude Code transcript files to extract token usage metrics.
|
|
2
|
+
|
|
3
|
+
Ported from claude-sdd-toolkit context_tracker module.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Sequence
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class TokenMetrics:
|
|
15
|
+
"""Token usage metrics extracted from a transcript."""
|
|
16
|
+
|
|
17
|
+
input_tokens: int
|
|
18
|
+
output_tokens: int
|
|
19
|
+
cached_tokens: int
|
|
20
|
+
total_tokens: int
|
|
21
|
+
context_length: int
|
|
22
|
+
|
|
23
|
+
def context_percentage(self, max_context: int = 155000) -> float:
|
|
24
|
+
"""Calculate context usage percentage."""
|
|
25
|
+
return (self.context_length / max_context) * 100 if max_context > 0 else 0.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_clear_command(entry: dict) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Check if a transcript entry is a /clear command.
|
|
31
|
+
|
|
32
|
+
The /clear command resets the conversation context, so we should
|
|
33
|
+
reset token counters when we encounter it.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
entry: A parsed JSONL entry from the transcript
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
True if this entry represents a /clear command
|
|
40
|
+
"""
|
|
41
|
+
if entry.get("type") != "user":
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
message = entry.get("message", {})
|
|
45
|
+
content = message.get("content", "")
|
|
46
|
+
|
|
47
|
+
# Handle both string content and list content
|
|
48
|
+
if isinstance(content, str):
|
|
49
|
+
return "<command-name>/clear</command-name>" in content
|
|
50
|
+
|
|
51
|
+
if isinstance(content, list):
|
|
52
|
+
for item in content:
|
|
53
|
+
if isinstance(item, dict):
|
|
54
|
+
text = item.get("text", "")
|
|
55
|
+
if "<command-name>/clear</command-name>" in text:
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_transcript(transcript_path: str | Path) -> Optional[TokenMetrics]:
|
|
62
|
+
"""
|
|
63
|
+
Parse a Claude Code transcript JSONL file and extract token metrics.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
transcript_path: Path to the transcript JSONL file
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
TokenMetrics object with aggregated token data, or None if parsing fails
|
|
70
|
+
"""
|
|
71
|
+
transcript_path = Path(transcript_path)
|
|
72
|
+
|
|
73
|
+
if not transcript_path.exists():
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
input_tokens = 0
|
|
77
|
+
output_tokens = 0
|
|
78
|
+
cached_tokens = 0
|
|
79
|
+
context_length = 0
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
with open(transcript_path, "r", encoding="utf-8") as f:
|
|
83
|
+
for line in f:
|
|
84
|
+
line = line.strip()
|
|
85
|
+
if not line:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
entry = json.loads(line)
|
|
90
|
+
except json.JSONDecodeError:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
# Check for /clear command - reset all counters
|
|
94
|
+
if is_clear_command(entry):
|
|
95
|
+
input_tokens = 0
|
|
96
|
+
output_tokens = 0
|
|
97
|
+
cached_tokens = 0
|
|
98
|
+
context_length = 0
|
|
99
|
+
continue # Don't process /clear entry itself
|
|
100
|
+
|
|
101
|
+
# Skip sidechain and error messages
|
|
102
|
+
if entry.get("isSidechain") or entry.get("isApiErrorMessage"):
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# Extract usage data
|
|
106
|
+
message = entry.get("message", {})
|
|
107
|
+
usage = message.get("usage", {})
|
|
108
|
+
|
|
109
|
+
if usage:
|
|
110
|
+
# Accumulate token counts
|
|
111
|
+
input_tokens += usage.get("input_tokens", 0)
|
|
112
|
+
output_tokens += usage.get("output_tokens", 0)
|
|
113
|
+
|
|
114
|
+
# Cached tokens come from both read and creation
|
|
115
|
+
cache_read = usage.get("cache_read_input_tokens", 0)
|
|
116
|
+
cache_creation = usage.get("cache_creation_input_tokens", 0)
|
|
117
|
+
cached_tokens += cache_read + cache_creation
|
|
118
|
+
|
|
119
|
+
# Context length is from the most recent valid entry
|
|
120
|
+
# (input tokens + cached tokens, excluding output)
|
|
121
|
+
context_length = (
|
|
122
|
+
usage.get("input_tokens", 0)
|
|
123
|
+
+ usage.get("cache_read_input_tokens", 0)
|
|
124
|
+
+ usage.get("cache_creation_input_tokens", 0)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
except Exception:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
total_tokens = input_tokens + output_tokens + cached_tokens
|
|
131
|
+
|
|
132
|
+
return TokenMetrics(
|
|
133
|
+
input_tokens=input_tokens,
|
|
134
|
+
output_tokens=output_tokens,
|
|
135
|
+
cached_tokens=cached_tokens,
|
|
136
|
+
total_tokens=total_tokens,
|
|
137
|
+
context_length=context_length,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def find_transcript_by_marker(
|
|
142
|
+
cwd: Path,
|
|
143
|
+
marker: str,
|
|
144
|
+
max_retries: int = 10,
|
|
145
|
+
search_dirs: Optional[Sequence[Path]] = None,
|
|
146
|
+
allow_home_search: bool = False,
|
|
147
|
+
) -> Optional[Path]:
|
|
148
|
+
"""
|
|
149
|
+
Search transcripts for a specific SESSION_MARKER to identify current session.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
cwd: Current working directory (used to derive default project path)
|
|
153
|
+
marker: Specific marker to search for (e.g., "SESSION_MARKER_abc12345")
|
|
154
|
+
max_retries: Maximum number of retry attempts (default: 10)
|
|
155
|
+
search_dirs: Explicit directories to search (takes precedence over defaults)
|
|
156
|
+
allow_home_search: Whether to scan ~/.claude/projects derived paths
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Path to transcript containing the marker, or None if not found
|
|
160
|
+
"""
|
|
161
|
+
candidate_dirs: list[Path] = []
|
|
162
|
+
|
|
163
|
+
if search_dirs:
|
|
164
|
+
for directory in search_dirs:
|
|
165
|
+
resolved = Path(directory).expanduser().resolve()
|
|
166
|
+
if resolved.is_dir() and resolved not in candidate_dirs:
|
|
167
|
+
candidate_dirs.append(resolved)
|
|
168
|
+
|
|
169
|
+
if allow_home_search:
|
|
170
|
+
current_path = cwd.resolve()
|
|
171
|
+
while True:
|
|
172
|
+
project_dir_name = str(current_path).replace("/", "-").replace("_", "-")
|
|
173
|
+
transcript_dir = Path.home() / ".claude" / "projects" / project_dir_name
|
|
174
|
+
if transcript_dir.exists() and transcript_dir not in candidate_dirs:
|
|
175
|
+
candidate_dirs.append(transcript_dir)
|
|
176
|
+
|
|
177
|
+
if current_path.parent == current_path or len(candidate_dirs) >= 5:
|
|
178
|
+
break
|
|
179
|
+
current_path = current_path.parent
|
|
180
|
+
|
|
181
|
+
if not candidate_dirs:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
delays = [min(0.1 * (2**i), 10.0) for i in range(max_retries)]
|
|
185
|
+
|
|
186
|
+
for attempt in range(max_retries):
|
|
187
|
+
current_time = time.time()
|
|
188
|
+
|
|
189
|
+
for transcript_dir in candidate_dirs:
|
|
190
|
+
try:
|
|
191
|
+
transcript_files = []
|
|
192
|
+
for transcript_path in transcript_dir.glob("*.jsonl"):
|
|
193
|
+
try:
|
|
194
|
+
mtime = transcript_path.stat().st_mtime
|
|
195
|
+
if (current_time - mtime) > 86400:
|
|
196
|
+
continue
|
|
197
|
+
transcript_files.append((transcript_path, mtime))
|
|
198
|
+
except (OSError, IOError):
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
transcript_files.sort(key=lambda x: x[1], reverse=True)
|
|
202
|
+
|
|
203
|
+
for transcript_path, _ in transcript_files:
|
|
204
|
+
try:
|
|
205
|
+
with open(transcript_path, "r", encoding="utf-8") as f:
|
|
206
|
+
for line in f:
|
|
207
|
+
if marker in line:
|
|
208
|
+
return transcript_path
|
|
209
|
+
except (OSError, IOError, UnicodeDecodeError):
|
|
210
|
+
continue
|
|
211
|
+
except (OSError, IOError):
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
if attempt < max_retries - 1:
|
|
215
|
+
time.sleep(delays[attempt])
|
|
216
|
+
|
|
217
|
+
return None
|