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,479 @@
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 and feature flags.
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.flags import flags_for_discovery, get_cli_flags
192
+ from foundry_mcp.cli.main import cli
193
+
194
+ cli_ctx = get_context(ctx)
195
+
196
+ # Get registered command groups
197
+ command_groups = {}
198
+ for name, cmd in cli.commands.items():
199
+ if hasattr(cmd, "commands"): # It's a group
200
+ command_groups[name] = {
201
+ "type": "group",
202
+ "subcommands": list(cmd.commands.keys()),
203
+ }
204
+ else:
205
+ command_groups[name] = {"type": "command"}
206
+
207
+ # Get feature flags
208
+ get_cli_flags()
209
+ flags = flags_for_discovery()
210
+
211
+ # Known CLI capabilities
212
+ capabilities = {
213
+ "json_output": True, # All output is JSON
214
+ "spec_driven": True, # SDD methodology supported
215
+ "feature_flags": True, # Feature flag system available
216
+ "session_tracking": True, # Session/context tracking
217
+ "rate_limiting": True, # Rate limiting built-in
218
+ }
219
+
220
+ emit_success(
221
+ {
222
+ "version": "0.1.0",
223
+ "name": "foundry-cli",
224
+ "capabilities": capabilities,
225
+ "feature_flags": flags,
226
+ "command_groups": list(command_groups.keys()),
227
+ "command_count": len(cli.commands),
228
+ "specs_dir": str(cli_ctx.specs_dir) if cli_ctx.specs_dir else None,
229
+ }
230
+ )
231
+
232
+
233
+ def get_work_mode() -> str:
234
+ """Get the configured work mode from environment.
235
+
236
+ Work mode controls how sdd-next executes tasks:
237
+ - "single": Execute one task at a time, pause for approval
238
+ - "autonomous": Execute all tasks in a phase without pausing
239
+
240
+ Set via MCP server config:
241
+ "env": {"FOUNDRY_MCP_WORK_MODE": "autonomous"}
242
+
243
+ Returns:
244
+ Work mode string ("single" or "autonomous").
245
+ """
246
+ env_mode = os.environ.get("FOUNDRY_MCP_WORK_MODE", "")
247
+ mode = env_mode.lower().strip()
248
+ return mode if mode in WORK_MODES else DEFAULT_WORK_MODE
249
+
250
+
251
+ @session.command("work-mode")
252
+ @click.pass_context
253
+ @cli_command("work-mode")
254
+ @handle_keyboard_interrupt()
255
+ @with_sync_timeout(FAST_TIMEOUT, "Work mode lookup timed out")
256
+ def work_mode_cmd(ctx: click.Context) -> None:
257
+ """Get the current work mode for task execution.
258
+
259
+ Work mode is configured via FOUNDRY_MCP_WORK_MODE environment variable
260
+ in the MCP server configuration.
261
+
262
+ Modes:
263
+ - single: Execute one task at a time, pause for approval
264
+ - autonomous: Execute all tasks in a phase without pausing
265
+ """
266
+ mode = get_work_mode()
267
+ agent = get_agent_type()
268
+
269
+ emit_success(
270
+ {
271
+ "work_mode": mode,
272
+ "agent_type": agent,
273
+ "modes_available": list(WORK_MODES),
274
+ "configured_via": "FOUNDRY_MCP_WORK_MODE",
275
+ }
276
+ )
277
+
278
+
279
+ @session.command("token-usage")
280
+ @agent_gated("claude-code")
281
+ @click.option("--session-marker", help="Session marker from generate-marker command.")
282
+ @click.pass_context
283
+ @cli_command("token-usage")
284
+ @handle_keyboard_interrupt()
285
+ @with_sync_timeout(FAST_TIMEOUT, "Token usage lookup timed out")
286
+ def token_usage_cmd(ctx: click.Context, session_marker: Optional[str]) -> None:
287
+ """Monitor token and context usage (Claude Code only).
288
+
289
+ Parses Claude Code transcript files to extract token usage metrics.
290
+ Requires agent_type=claude-code in MCP configuration.
291
+
292
+ Use --session-marker to filter to a specific session.
293
+ """
294
+ # Note: Full implementation requires transcript parsing logic
295
+ # For now, return a placeholder indicating the feature is available
296
+ emit_success(
297
+ {
298
+ "available": True,
299
+ "agent_type": "claude-code",
300
+ "session_marker": session_marker,
301
+ "message": "Token usage tracking available. Full metrics require transcript access.",
302
+ "hint": "Use generate-marker to create a session marker for tracking.",
303
+ }
304
+ )
305
+
306
+
307
+ @session.command("generate-marker")
308
+ @agent_gated("claude-code")
309
+ @click.pass_context
310
+ @cli_command("generate-marker")
311
+ @handle_keyboard_interrupt()
312
+ @with_sync_timeout(FAST_TIMEOUT, "Marker generation timed out")
313
+ def generate_marker_cmd(ctx: click.Context) -> None:
314
+ """Generate a session marker for transcript identification (Claude Code only).
315
+
316
+ Creates a unique marker that can be used to identify and filter
317
+ transcript entries for token usage tracking.
318
+
319
+ Requires agent_type=claude-code in MCP configuration.
320
+ """
321
+ marker = f"SESSION_MARKER_{secrets.token_hex(4).upper()}"
322
+
323
+ emit_success(
324
+ {
325
+ "marker": marker,
326
+ "usage": "Include this marker in your prompts to track context usage.",
327
+ "hint": "Pass to 'session token-usage --session-marker' to filter metrics.",
328
+ }
329
+ )
330
+
331
+
332
+ @session.command("context")
333
+ @agent_gated("claude-code")
334
+ @click.option(
335
+ "--session-marker",
336
+ required=True,
337
+ help="Session marker from generate-marker command for context tracking.",
338
+ )
339
+ @click.option(
340
+ "--check-limits",
341
+ is_flag=True,
342
+ help="Include limit checking and recommendations.",
343
+ )
344
+ @click.option(
345
+ "--transcript-dir",
346
+ type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
347
+ help="Explicit directory containing transcript JSONL files.",
348
+ )
349
+ @click.option(
350
+ "--allow-home-transcripts",
351
+ is_flag=True,
352
+ help="Allow scanning ~/.claude/projects for transcripts (requires opt-in).",
353
+ )
354
+ @click.pass_context
355
+ @cli_command("context")
356
+ @handle_keyboard_interrupt()
357
+ @with_sync_timeout(MEDIUM_TIMEOUT, "Context check timed out")
358
+ def context_cmd(
359
+ ctx: click.Context,
360
+ session_marker: str,
361
+ check_limits: bool,
362
+ transcript_dir: Optional[Path],
363
+ allow_home_transcripts: bool,
364
+ ) -> None:
365
+ """Check current context usage percentage (Claude Code only).
366
+
367
+ Completes the two-step context tracking contract:
368
+ 1. Run 'sdd session generate-marker' to get a marker
369
+ 2. Run 'sdd session context --session-marker <marker>' to check usage
370
+
371
+ The session marker is logged to the transcript and used to calculate
372
+ context percentage by analyzing conversation length.
373
+
374
+ Example:
375
+ sdd session generate-marker
376
+ # Returns: SESSION_MARKER_ABCD1234
377
+ sdd session context --session-marker SESSION_MARKER_ABCD1234
378
+ # Returns: {"context_percentage_used": 45}
379
+ """
380
+ # Validate marker format
381
+ if not session_marker.startswith("SESSION_MARKER_"):
382
+ emit_error(
383
+ "Invalid session marker format",
384
+ code="INVALID_MARKER",
385
+ error_type="validation",
386
+ remediation="Use a marker from 'sdd session generate-marker'",
387
+ details={"provided_marker": session_marker},
388
+ )
389
+ return
390
+
391
+ transcript_dirs: Optional[List[Path]] = None
392
+ if transcript_dir is not None:
393
+ resolved_dir = transcript_dir.expanduser().resolve()
394
+ if not resolved_dir.exists() or not resolved_dir.is_dir():
395
+ emit_error(
396
+ "Transcript directory not found",
397
+ code="VALIDATION_ERROR",
398
+ error_type="validation",
399
+ remediation="Pass a directory containing transcript JSONL files",
400
+ details={"transcript_dir": str(transcript_dir)},
401
+ )
402
+ return
403
+ transcript_dirs = [resolved_dir]
404
+
405
+ allow_home_search = allow_home_transcripts or bool(
406
+ os.environ.get(TRANSCRIPT_OPT_IN_ENV, "").strip()
407
+ )
408
+
409
+ if transcript_dirs is None and not allow_home_search:
410
+ emit_error(
411
+ "Transcript access disabled",
412
+ code="TRANSCRIPTS_DISABLED",
413
+ error_type="forbidden",
414
+ remediation=(
415
+ "Pass --transcript-dir, use --allow-home-transcripts, or set FOUNDRY_MCP_ALLOW_TRANSCRIPTS=1"
416
+ ),
417
+ details={"session_marker": session_marker},
418
+ )
419
+ return
420
+
421
+ # Find transcript containing the session marker
422
+ transcript_path = find_transcript_by_marker(
423
+ Path.cwd(),
424
+ session_marker,
425
+ search_dirs=transcript_dirs,
426
+ allow_home_search=allow_home_search,
427
+ )
428
+ if transcript_path is None:
429
+ emit_error(
430
+ "Could not find transcript containing marker",
431
+ code="TRANSCRIPT_NOT_FOUND",
432
+ error_type="not_found",
433
+ remediation=(
434
+ "Ensure you run 'sdd session generate-marker' first, then wait for "
435
+ "the marker to be logged before running 'sdd session context'."
436
+ ),
437
+ details={
438
+ "session_marker": session_marker,
439
+ "cwd": str(Path.cwd()),
440
+ },
441
+ )
442
+ return
443
+
444
+ # Parse the transcript to get token metrics
445
+ metrics = parse_transcript(transcript_path)
446
+ if metrics is None:
447
+ emit_error(
448
+ "Failed to parse transcript file",
449
+ code="PARSE_ERROR",
450
+ error_type="internal",
451
+ remediation="Check that the transcript file is valid JSONL.",
452
+ details={"transcript_path": str(transcript_path)},
453
+ )
454
+ return
455
+
456
+ # Calculate context percentage (default max context: 155,000 tokens)
457
+ max_context = 155000
458
+ context_percentage = round(metrics.context_percentage(max_context))
459
+ recommendations = []
460
+
461
+ if check_limits:
462
+ if context_percentage >= 85:
463
+ recommendations.append(
464
+ "Context at or above 85%. Consider '/clear' and '/sdd-begin'."
465
+ )
466
+ elif context_percentage >= 70:
467
+ recommendations.append("Context above 70%. Monitor usage closely.")
468
+
469
+ result = {"context_percentage_used": int(context_percentage)}
470
+
471
+ if check_limits:
472
+ result["session_marker"] = session_marker
473
+ result["recommendations"] = recommendations
474
+ result["threshold_warning"] = 85
475
+ result["threshold_stop"] = 90
476
+ result["context_length"] = metrics.context_length
477
+ result["max_context"] = max_context
478
+
479
+ emit_success(result)