foundry-mcp 0.8.22__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.

Potentially problematic release.


This version of foundry-mcp might be problematic. Click here for more details.

Files changed (153) hide show
  1. foundry_mcp/__init__.py +13 -0
  2. foundry_mcp/cli/__init__.py +67 -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 +640 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +667 -0
  15. foundry_mcp/cli/commands/session.py +472 -0
  16. foundry_mcp/cli/commands/specs.py +686 -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 +298 -0
  22. foundry_mcp/cli/logging.py +212 -0
  23. foundry_mcp/cli/main.py +44 -0
  24. foundry_mcp/cli/output.py +122 -0
  25. foundry_mcp/cli/registry.py +110 -0
  26. foundry_mcp/cli/resilience.py +178 -0
  27. foundry_mcp/cli/transcript.py +217 -0
  28. foundry_mcp/config.py +1454 -0
  29. foundry_mcp/core/__init__.py +144 -0
  30. foundry_mcp/core/ai_consultation.py +1773 -0
  31. foundry_mcp/core/batch_operations.py +1202 -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/health.py +749 -0
  40. foundry_mcp/core/intake.py +933 -0
  41. foundry_mcp/core/journal.py +700 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1376 -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 +146 -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 +387 -0
  57. foundry_mcp/core/prometheus.py +564 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +691 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
  61. foundry_mcp/core/prompts/plan_review.py +627 -0
  62. foundry_mcp/core/providers/__init__.py +237 -0
  63. foundry_mcp/core/providers/base.py +515 -0
  64. foundry_mcp/core/providers/claude.py +472 -0
  65. foundry_mcp/core/providers/codex.py +637 -0
  66. foundry_mcp/core/providers/cursor_agent.py +630 -0
  67. foundry_mcp/core/providers/detectors.py +515 -0
  68. foundry_mcp/core/providers/gemini.py +426 -0
  69. foundry_mcp/core/providers/opencode.py +718 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +308 -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 +857 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/research/__init__.py +68 -0
  78. foundry_mcp/core/research/memory.py +528 -0
  79. foundry_mcp/core/research/models.py +1234 -0
  80. foundry_mcp/core/research/providers/__init__.py +40 -0
  81. foundry_mcp/core/research/providers/base.py +242 -0
  82. foundry_mcp/core/research/providers/google.py +507 -0
  83. foundry_mcp/core/research/providers/perplexity.py +442 -0
  84. foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
  85. foundry_mcp/core/research/providers/tavily.py +383 -0
  86. foundry_mcp/core/research/workflows/__init__.py +25 -0
  87. foundry_mcp/core/research/workflows/base.py +298 -0
  88. foundry_mcp/core/research/workflows/chat.py +271 -0
  89. foundry_mcp/core/research/workflows/consensus.py +539 -0
  90. foundry_mcp/core/research/workflows/deep_research.py +4142 -0
  91. foundry_mcp/core/research/workflows/ideate.py +682 -0
  92. foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
  93. foundry_mcp/core/resilience.py +600 -0
  94. foundry_mcp/core/responses.py +1624 -0
  95. foundry_mcp/core/review.py +366 -0
  96. foundry_mcp/core/security.py +438 -0
  97. foundry_mcp/core/spec.py +4119 -0
  98. foundry_mcp/core/task.py +2463 -0
  99. foundry_mcp/core/testing.py +839 -0
  100. foundry_mcp/core/validation.py +2357 -0
  101. foundry_mcp/dashboard/__init__.py +32 -0
  102. foundry_mcp/dashboard/app.py +119 -0
  103. foundry_mcp/dashboard/components/__init__.py +17 -0
  104. foundry_mcp/dashboard/components/cards.py +88 -0
  105. foundry_mcp/dashboard/components/charts.py +177 -0
  106. foundry_mcp/dashboard/components/filters.py +136 -0
  107. foundry_mcp/dashboard/components/tables.py +195 -0
  108. foundry_mcp/dashboard/data/__init__.py +11 -0
  109. foundry_mcp/dashboard/data/stores.py +433 -0
  110. foundry_mcp/dashboard/launcher.py +300 -0
  111. foundry_mcp/dashboard/views/__init__.py +12 -0
  112. foundry_mcp/dashboard/views/errors.py +217 -0
  113. foundry_mcp/dashboard/views/metrics.py +164 -0
  114. foundry_mcp/dashboard/views/overview.py +96 -0
  115. foundry_mcp/dashboard/views/providers.py +83 -0
  116. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  117. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  118. foundry_mcp/prompts/__init__.py +9 -0
  119. foundry_mcp/prompts/workflows.py +525 -0
  120. foundry_mcp/resources/__init__.py +9 -0
  121. foundry_mcp/resources/specs.py +591 -0
  122. foundry_mcp/schemas/__init__.py +38 -0
  123. foundry_mcp/schemas/intake-schema.json +89 -0
  124. foundry_mcp/schemas/sdd-spec-schema.json +414 -0
  125. foundry_mcp/server.py +150 -0
  126. foundry_mcp/tools/__init__.py +10 -0
  127. foundry_mcp/tools/unified/__init__.py +92 -0
  128. foundry_mcp/tools/unified/authoring.py +3620 -0
  129. foundry_mcp/tools/unified/context_helpers.py +98 -0
  130. foundry_mcp/tools/unified/documentation_helpers.py +268 -0
  131. foundry_mcp/tools/unified/environment.py +1341 -0
  132. foundry_mcp/tools/unified/error.py +479 -0
  133. foundry_mcp/tools/unified/health.py +225 -0
  134. foundry_mcp/tools/unified/journal.py +841 -0
  135. foundry_mcp/tools/unified/lifecycle.py +640 -0
  136. foundry_mcp/tools/unified/metrics.py +777 -0
  137. foundry_mcp/tools/unified/plan.py +876 -0
  138. foundry_mcp/tools/unified/pr.py +294 -0
  139. foundry_mcp/tools/unified/provider.py +589 -0
  140. foundry_mcp/tools/unified/research.py +1283 -0
  141. foundry_mcp/tools/unified/review.py +1042 -0
  142. foundry_mcp/tools/unified/review_helpers.py +314 -0
  143. foundry_mcp/tools/unified/router.py +102 -0
  144. foundry_mcp/tools/unified/server.py +565 -0
  145. foundry_mcp/tools/unified/spec.py +1283 -0
  146. foundry_mcp/tools/unified/task.py +3846 -0
  147. foundry_mcp/tools/unified/test.py +431 -0
  148. foundry_mcp/tools/unified/verification.py +520 -0
  149. foundry_mcp-0.8.22.dist-info/METADATA +344 -0
  150. foundry_mcp-0.8.22.dist-info/RECORD +153 -0
  151. foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
  152. foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
  153. foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,472 @@
1
+ """Session management commands for SDD CLI.
2
+
3
+ Provides commands for session tracking, context limits, and consultation monitoring.
4
+ """
5
+
6
+ import os
7
+ import secrets
8
+ from pathlib import Path
9
+ from typing import List, Optional
10
+
11
+ import click
12
+
13
+ from foundry_mcp.cli.agent import agent_gated, get_agent_type
14
+ from foundry_mcp.cli.transcript import find_transcript_by_marker, parse_transcript
15
+
16
+ TRANSCRIPT_OPT_IN_ENV = "FOUNDRY_MCP_ALLOW_TRANSCRIPTS"
17
+ from foundry_mcp.cli.context import (
18
+ get_context_tracker,
19
+ get_session_status,
20
+ record_consultation,
21
+ )
22
+ from foundry_mcp.cli.logging import cli_command, get_cli_logger
23
+ from foundry_mcp.cli.output import emit_error, emit_success
24
+ from foundry_mcp.cli.registry import get_context
25
+ from foundry_mcp.cli.resilience import (
26
+ FAST_TIMEOUT,
27
+ MEDIUM_TIMEOUT,
28
+ handle_keyboard_interrupt,
29
+ with_sync_timeout,
30
+ )
31
+
32
+ logger = get_cli_logger()
33
+
34
+
35
+ # Valid work modes
36
+ WORK_MODES = frozenset({"single", "autonomous"})
37
+ DEFAULT_WORK_MODE = "single"
38
+
39
+
40
+ @click.group("session")
41
+ def session() -> None:
42
+ """Session and context management commands."""
43
+ pass
44
+
45
+
46
+ @session.command("start")
47
+ @click.option("--id", "session_id", help="Custom session ID.")
48
+ @click.option(
49
+ "--max-consultations", type=int, help="Maximum LLM consultations allowed."
50
+ )
51
+ @click.option("--max-tokens", type=int, help="Maximum context tokens allowed.")
52
+ @click.pass_context
53
+ @cli_command("start")
54
+ @handle_keyboard_interrupt()
55
+ @with_sync_timeout(FAST_TIMEOUT, "Session start timed out")
56
+ def start_session_cmd(
57
+ ctx: click.Context,
58
+ session_id: Optional[str],
59
+ max_consultations: Optional[int],
60
+ max_tokens: Optional[int],
61
+ ) -> None:
62
+ """Start a new CLI session with optional limits.
63
+
64
+ Sessions track consultation usage and context budget for LLM workflows.
65
+ """
66
+ from foundry_mcp.cli.context import SessionLimits
67
+
68
+ tracker = get_context_tracker()
69
+
70
+ # Build limits if any overrides provided
71
+ limits = None
72
+ if max_consultations is not None or max_tokens is not None:
73
+ limits = SessionLimits(
74
+ max_consultations=max_consultations or 50,
75
+ max_context_tokens=max_tokens or 100000,
76
+ )
77
+
78
+ session = tracker.start_session(session_id=session_id, limits=limits)
79
+
80
+ emit_success(
81
+ {
82
+ "session_id": session.session_id,
83
+ "started_at": session.started_at,
84
+ "limits": {
85
+ "max_consultations": session.limits.max_consultations,
86
+ "max_context_tokens": session.limits.max_context_tokens,
87
+ },
88
+ }
89
+ )
90
+
91
+
92
+ @session.command("status")
93
+ @click.pass_context
94
+ @cli_command("status")
95
+ @handle_keyboard_interrupt()
96
+ @with_sync_timeout(FAST_TIMEOUT, "Session status lookup timed out")
97
+ def session_status_cmd(ctx: click.Context) -> None:
98
+ """Get current session status and usage."""
99
+ status = get_session_status()
100
+ emit_success(status)
101
+
102
+
103
+ @session.command("record")
104
+ @click.option("--tokens", type=int, default=0, help="Estimated tokens used.")
105
+ @click.pass_context
106
+ @cli_command("record")
107
+ @handle_keyboard_interrupt()
108
+ @with_sync_timeout(FAST_TIMEOUT, "Record consultation timed out")
109
+ def record_consultation_cmd(ctx: click.Context, tokens: int) -> None:
110
+ """Record an LLM consultation.
111
+
112
+ Tracks consultation count and token usage against session limits.
113
+ """
114
+ result = record_consultation(tokens)
115
+ emit_success(result)
116
+
117
+
118
+ @session.command("reset")
119
+ @click.pass_context
120
+ @cli_command("reset")
121
+ @handle_keyboard_interrupt()
122
+ @with_sync_timeout(FAST_TIMEOUT, "Session reset timed out")
123
+ def reset_session_cmd(ctx: click.Context) -> None:
124
+ """Reset the current session."""
125
+ tracker = get_context_tracker()
126
+ tracker.reset()
127
+ emit_success({"message": "Session reset"})
128
+
129
+
130
+ @session.command("limits")
131
+ @click.pass_context
132
+ @cli_command("limits")
133
+ @handle_keyboard_interrupt()
134
+ @with_sync_timeout(FAST_TIMEOUT, "Limits lookup timed out")
135
+ def show_limits_cmd(ctx: click.Context) -> None:
136
+ """Show current session limits and remaining budget."""
137
+ tracker = get_context_tracker()
138
+ session = tracker.get_session()
139
+
140
+ if session is None:
141
+ emit_success(
142
+ {
143
+ "active": False,
144
+ "message": "No active session. Use 'sdd session start' to begin.",
145
+ "default_limits": {
146
+ "max_consultations": tracker._default_limits.max_consultations,
147
+ "max_context_tokens": tracker._default_limits.max_context_tokens,
148
+ "warn_at_percentage": tracker._default_limits.warn_at_percentage,
149
+ },
150
+ }
151
+ )
152
+ else:
153
+ emit_success(
154
+ {
155
+ "active": True,
156
+ "session_id": session.session_id,
157
+ "limits": {
158
+ "max_consultations": session.limits.max_consultations,
159
+ "max_context_tokens": session.limits.max_context_tokens,
160
+ "warn_at_percentage": session.limits.warn_at_percentage,
161
+ },
162
+ "usage": {
163
+ "consultations_used": session.stats.consultation_count,
164
+ "consultations_remaining": session.consultations_remaining,
165
+ "tokens_used": session.stats.estimated_tokens_used,
166
+ "tokens_remaining": session.tokens_remaining,
167
+ },
168
+ "status": {
169
+ "consultation_percentage": round(
170
+ session.consultation_usage_percentage, 1
171
+ ),
172
+ "token_percentage": round(session.token_usage_percentage, 1),
173
+ "should_warn": session.should_warn,
174
+ "at_limit": session.at_limit,
175
+ },
176
+ }
177
+ )
178
+
179
+
180
+ @session.command("capabilities")
181
+ @click.pass_context
182
+ @cli_command("capabilities")
183
+ @handle_keyboard_interrupt()
184
+ @with_sync_timeout(FAST_TIMEOUT, "Capabilities lookup timed out")
185
+ def session_capabilities_cmd(ctx: click.Context) -> None:
186
+ """Show CLI capabilities.
187
+
188
+ Returns a manifest of available features, commands, and their status
189
+ for AI coding assistants to understand available functionality.
190
+ """
191
+ from foundry_mcp.cli.main import cli
192
+
193
+ cli_ctx = get_context(ctx)
194
+
195
+ # Get registered command groups
196
+ command_groups = {}
197
+ for name, cmd in cli.commands.items():
198
+ if hasattr(cmd, "commands"): # It's a group
199
+ command_groups[name] = {
200
+ "type": "group",
201
+ "subcommands": list(cmd.commands.keys()),
202
+ }
203
+ else:
204
+ command_groups[name] = {"type": "command"}
205
+
206
+ # Known CLI capabilities
207
+ capabilities = {
208
+ "json_output": True, # All output is JSON
209
+ "spec_driven": True, # SDD methodology supported
210
+ "session_tracking": True, # Session/context tracking
211
+ "rate_limiting": True, # Rate limiting built-in
212
+ }
213
+
214
+ emit_success(
215
+ {
216
+ "version": "0.1.0",
217
+ "name": "foundry-cli",
218
+ "capabilities": capabilities,
219
+ "command_groups": list(command_groups.keys()),
220
+ "command_count": len(cli.commands),
221
+ "specs_dir": str(cli_ctx.specs_dir) if cli_ctx.specs_dir else None,
222
+ }
223
+ )
224
+
225
+
226
+ def get_work_mode() -> str:
227
+ """Get the configured work mode from environment.
228
+
229
+ Work mode controls how sdd-next executes tasks:
230
+ - "single": Execute one task at a time, pause for approval
231
+ - "autonomous": Execute all tasks in a phase without pausing
232
+
233
+ Set via MCP server config:
234
+ "env": {"FOUNDRY_MCP_WORK_MODE": "autonomous"}
235
+
236
+ Returns:
237
+ Work mode string ("single" or "autonomous").
238
+ """
239
+ env_mode = os.environ.get("FOUNDRY_MCP_WORK_MODE", "")
240
+ mode = env_mode.lower().strip()
241
+ return mode if mode in WORK_MODES else DEFAULT_WORK_MODE
242
+
243
+
244
+ @session.command("work-mode")
245
+ @click.pass_context
246
+ @cli_command("work-mode")
247
+ @handle_keyboard_interrupt()
248
+ @with_sync_timeout(FAST_TIMEOUT, "Work mode lookup timed out")
249
+ def work_mode_cmd(ctx: click.Context) -> None:
250
+ """Get the current work mode for task execution.
251
+
252
+ Work mode is configured via FOUNDRY_MCP_WORK_MODE environment variable
253
+ in the MCP server configuration.
254
+
255
+ Modes:
256
+ - single: Execute one task at a time, pause for approval
257
+ - autonomous: Execute all tasks in a phase without pausing
258
+ """
259
+ mode = get_work_mode()
260
+ agent = get_agent_type()
261
+
262
+ emit_success(
263
+ {
264
+ "work_mode": mode,
265
+ "agent_type": agent,
266
+ "modes_available": list(WORK_MODES),
267
+ "configured_via": "FOUNDRY_MCP_WORK_MODE",
268
+ }
269
+ )
270
+
271
+
272
+ @session.command("token-usage")
273
+ @agent_gated("claude-code")
274
+ @click.option("--session-marker", help="Session marker from generate-marker command.")
275
+ @click.pass_context
276
+ @cli_command("token-usage")
277
+ @handle_keyboard_interrupt()
278
+ @with_sync_timeout(FAST_TIMEOUT, "Token usage lookup timed out")
279
+ def token_usage_cmd(ctx: click.Context, session_marker: Optional[str]) -> None:
280
+ """Monitor token and context usage (Claude Code only).
281
+
282
+ Parses Claude Code transcript files to extract token usage metrics.
283
+ Requires agent_type=claude-code in MCP configuration.
284
+
285
+ Use --session-marker to filter to a specific session.
286
+ """
287
+ # Note: Full implementation requires transcript parsing logic
288
+ # For now, return a placeholder indicating the feature is available
289
+ emit_success(
290
+ {
291
+ "available": True,
292
+ "agent_type": "claude-code",
293
+ "session_marker": session_marker,
294
+ "message": "Token usage tracking available. Full metrics require transcript access.",
295
+ "hint": "Use generate-marker to create a session marker for tracking.",
296
+ }
297
+ )
298
+
299
+
300
+ @session.command("generate-marker")
301
+ @agent_gated("claude-code")
302
+ @click.pass_context
303
+ @cli_command("generate-marker")
304
+ @handle_keyboard_interrupt()
305
+ @with_sync_timeout(FAST_TIMEOUT, "Marker generation timed out")
306
+ def generate_marker_cmd(ctx: click.Context) -> None:
307
+ """Generate a session marker for transcript identification (Claude Code only).
308
+
309
+ Creates a unique marker that can be used to identify and filter
310
+ transcript entries for token usage tracking.
311
+
312
+ Requires agent_type=claude-code in MCP configuration.
313
+ """
314
+ marker = f"SESSION_MARKER_{secrets.token_hex(4).upper()}"
315
+
316
+ emit_success(
317
+ {
318
+ "marker": marker,
319
+ "usage": "Include this marker in your prompts to track context usage.",
320
+ "hint": "Pass to 'session token-usage --session-marker' to filter metrics.",
321
+ }
322
+ )
323
+
324
+
325
+ @session.command("context")
326
+ @agent_gated("claude-code")
327
+ @click.option(
328
+ "--session-marker",
329
+ required=True,
330
+ help="Session marker from generate-marker command for context tracking.",
331
+ )
332
+ @click.option(
333
+ "--check-limits",
334
+ is_flag=True,
335
+ help="Include limit checking and recommendations.",
336
+ )
337
+ @click.option(
338
+ "--transcript-dir",
339
+ type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
340
+ help="Explicit directory containing transcript JSONL files.",
341
+ )
342
+ @click.option(
343
+ "--allow-home-transcripts",
344
+ is_flag=True,
345
+ help="Allow scanning ~/.claude/projects for transcripts (requires opt-in).",
346
+ )
347
+ @click.pass_context
348
+ @cli_command("context")
349
+ @handle_keyboard_interrupt()
350
+ @with_sync_timeout(MEDIUM_TIMEOUT, "Context check timed out")
351
+ def context_cmd(
352
+ ctx: click.Context,
353
+ session_marker: str,
354
+ check_limits: bool,
355
+ transcript_dir: Optional[Path],
356
+ allow_home_transcripts: bool,
357
+ ) -> None:
358
+ """Check current context usage percentage (Claude Code only).
359
+
360
+ Completes the two-step context tracking contract:
361
+ 1. Run 'sdd session generate-marker' to get a marker
362
+ 2. Run 'sdd session context --session-marker <marker>' to check usage
363
+
364
+ The session marker is logged to the transcript and used to calculate
365
+ context percentage by analyzing conversation length.
366
+
367
+ Example:
368
+ sdd session generate-marker
369
+ # Returns: SESSION_MARKER_ABCD1234
370
+ sdd session context --session-marker SESSION_MARKER_ABCD1234
371
+ # Returns: {"context_percentage_used": 45}
372
+ """
373
+ # Validate marker format
374
+ if not session_marker.startswith("SESSION_MARKER_"):
375
+ emit_error(
376
+ "Invalid session marker format",
377
+ code="INVALID_MARKER",
378
+ error_type="validation",
379
+ remediation="Use a marker from 'sdd session generate-marker'",
380
+ details={"provided_marker": session_marker},
381
+ )
382
+ return
383
+
384
+ transcript_dirs: Optional[List[Path]] = None
385
+ if transcript_dir is not None:
386
+ resolved_dir = transcript_dir.expanduser().resolve()
387
+ if not resolved_dir.exists() or not resolved_dir.is_dir():
388
+ emit_error(
389
+ "Transcript directory not found",
390
+ code="VALIDATION_ERROR",
391
+ error_type="validation",
392
+ remediation="Pass a directory containing transcript JSONL files",
393
+ details={"transcript_dir": str(transcript_dir)},
394
+ )
395
+ return
396
+ transcript_dirs = [resolved_dir]
397
+
398
+ allow_home_search = allow_home_transcripts or bool(
399
+ os.environ.get(TRANSCRIPT_OPT_IN_ENV, "").strip()
400
+ )
401
+
402
+ if transcript_dirs is None and not allow_home_search:
403
+ emit_error(
404
+ "Transcript access disabled",
405
+ code="TRANSCRIPTS_DISABLED",
406
+ error_type="forbidden",
407
+ remediation=(
408
+ "Pass --transcript-dir, use --allow-home-transcripts, or set FOUNDRY_MCP_ALLOW_TRANSCRIPTS=1"
409
+ ),
410
+ details={"session_marker": session_marker},
411
+ )
412
+ return
413
+
414
+ # Find transcript containing the session marker
415
+ transcript_path = find_transcript_by_marker(
416
+ Path.cwd(),
417
+ session_marker,
418
+ search_dirs=transcript_dirs,
419
+ allow_home_search=allow_home_search,
420
+ )
421
+ if transcript_path is None:
422
+ emit_error(
423
+ "Could not find transcript containing marker",
424
+ code="TRANSCRIPT_NOT_FOUND",
425
+ error_type="not_found",
426
+ remediation=(
427
+ "Ensure you run 'sdd session generate-marker' first, then wait for "
428
+ "the marker to be logged before running 'sdd session context'."
429
+ ),
430
+ details={
431
+ "session_marker": session_marker,
432
+ "cwd": str(Path.cwd()),
433
+ },
434
+ )
435
+ return
436
+
437
+ # Parse the transcript to get token metrics
438
+ metrics = parse_transcript(transcript_path)
439
+ if metrics is None:
440
+ emit_error(
441
+ "Failed to parse transcript file",
442
+ code="PARSE_ERROR",
443
+ error_type="internal",
444
+ remediation="Check that the transcript file is valid JSONL.",
445
+ details={"transcript_path": str(transcript_path)},
446
+ )
447
+ return
448
+
449
+ # Calculate context percentage (default max context: 155,000 tokens)
450
+ max_context = 155000
451
+ context_percentage = round(metrics.context_percentage(max_context))
452
+ recommendations = []
453
+
454
+ if check_limits:
455
+ if context_percentage >= 85:
456
+ recommendations.append(
457
+ "Context at or above 85%. Consider '/clear' and '/sdd-begin'."
458
+ )
459
+ elif context_percentage >= 70:
460
+ recommendations.append("Context above 70%. Monitor usage closely.")
461
+
462
+ result = {"context_percentage_used": int(context_percentage)}
463
+
464
+ if check_limits:
465
+ result["session_marker"] = session_marker
466
+ result["recommendations"] = recommendations
467
+ result["threshold_warning"] = 85
468
+ result["threshold_stop"] = 90
469
+ result["context_length"] = metrics.context_length
470
+ result["max_context"] = max_context
471
+
472
+ emit_success(result)