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.
Files changed (135) hide show
  1. foundry_mcp/__init__.py +7 -0
  2. foundry_mcp/cli/__init__.py +80 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +633 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +652 -0
  15. foundry_mcp/cli/commands/session.py +479 -0
  16. foundry_mcp/cli/commands/specs.py +856 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +259 -0
  22. foundry_mcp/cli/flags.py +266 -0
  23. foundry_mcp/cli/logging.py +212 -0
  24. foundry_mcp/cli/main.py +44 -0
  25. foundry_mcp/cli/output.py +122 -0
  26. foundry_mcp/cli/registry.py +110 -0
  27. foundry_mcp/cli/resilience.py +178 -0
  28. foundry_mcp/cli/transcript.py +217 -0
  29. foundry_mcp/config.py +850 -0
  30. foundry_mcp/core/__init__.py +144 -0
  31. foundry_mcp/core/ai_consultation.py +1636 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/feature_flags.py +592 -0
  40. foundry_mcp/core/health.py +749 -0
  41. foundry_mcp/core/journal.py +694 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1350 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +123 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +317 -0
  57. foundry_mcp/core/prometheus.py +577 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +546 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
  61. foundry_mcp/core/prompts/plan_review.py +623 -0
  62. foundry_mcp/core/providers/__init__.py +225 -0
  63. foundry_mcp/core/providers/base.py +476 -0
  64. foundry_mcp/core/providers/claude.py +460 -0
  65. foundry_mcp/core/providers/codex.py +619 -0
  66. foundry_mcp/core/providers/cursor_agent.py +642 -0
  67. foundry_mcp/core/providers/detectors.py +488 -0
  68. foundry_mcp/core/providers/gemini.py +405 -0
  69. foundry_mcp/core/providers/opencode.py +616 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +302 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +729 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/resilience.py +600 -0
  78. foundry_mcp/core/responses.py +934 -0
  79. foundry_mcp/core/review.py +366 -0
  80. foundry_mcp/core/security.py +438 -0
  81. foundry_mcp/core/spec.py +1650 -0
  82. foundry_mcp/core/task.py +1289 -0
  83. foundry_mcp/core/testing.py +450 -0
  84. foundry_mcp/core/validation.py +2081 -0
  85. foundry_mcp/dashboard/__init__.py +32 -0
  86. foundry_mcp/dashboard/app.py +119 -0
  87. foundry_mcp/dashboard/components/__init__.py +17 -0
  88. foundry_mcp/dashboard/components/cards.py +88 -0
  89. foundry_mcp/dashboard/components/charts.py +234 -0
  90. foundry_mcp/dashboard/components/filters.py +136 -0
  91. foundry_mcp/dashboard/components/tables.py +195 -0
  92. foundry_mcp/dashboard/data/__init__.py +11 -0
  93. foundry_mcp/dashboard/data/stores.py +433 -0
  94. foundry_mcp/dashboard/launcher.py +289 -0
  95. foundry_mcp/dashboard/views/__init__.py +12 -0
  96. foundry_mcp/dashboard/views/errors.py +217 -0
  97. foundry_mcp/dashboard/views/metrics.py +174 -0
  98. foundry_mcp/dashboard/views/overview.py +160 -0
  99. foundry_mcp/dashboard/views/providers.py +83 -0
  100. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  101. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  102. foundry_mcp/prompts/__init__.py +9 -0
  103. foundry_mcp/prompts/workflows.py +525 -0
  104. foundry_mcp/resources/__init__.py +9 -0
  105. foundry_mcp/resources/specs.py +591 -0
  106. foundry_mcp/schemas/__init__.py +38 -0
  107. foundry_mcp/schemas/sdd-spec-schema.json +386 -0
  108. foundry_mcp/server.py +164 -0
  109. foundry_mcp/tools/__init__.py +10 -0
  110. foundry_mcp/tools/unified/__init__.py +71 -0
  111. foundry_mcp/tools/unified/authoring.py +1487 -0
  112. foundry_mcp/tools/unified/context_helpers.py +98 -0
  113. foundry_mcp/tools/unified/documentation_helpers.py +198 -0
  114. foundry_mcp/tools/unified/environment.py +939 -0
  115. foundry_mcp/tools/unified/error.py +462 -0
  116. foundry_mcp/tools/unified/health.py +225 -0
  117. foundry_mcp/tools/unified/journal.py +841 -0
  118. foundry_mcp/tools/unified/lifecycle.py +632 -0
  119. foundry_mcp/tools/unified/metrics.py +777 -0
  120. foundry_mcp/tools/unified/plan.py +745 -0
  121. foundry_mcp/tools/unified/pr.py +294 -0
  122. foundry_mcp/tools/unified/provider.py +629 -0
  123. foundry_mcp/tools/unified/review.py +685 -0
  124. foundry_mcp/tools/unified/review_helpers.py +299 -0
  125. foundry_mcp/tools/unified/router.py +102 -0
  126. foundry_mcp/tools/unified/server.py +580 -0
  127. foundry_mcp/tools/unified/spec.py +808 -0
  128. foundry_mcp/tools/unified/task.py +2202 -0
  129. foundry_mcp/tools/unified/test.py +370 -0
  130. foundry_mcp/tools/unified/verification.py +520 -0
  131. foundry_mcp-0.3.3.dist-info/METADATA +337 -0
  132. foundry_mcp-0.3.3.dist-info/RECORD +135 -0
  133. foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
  134. foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
  135. 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