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.
- foundry_mcp/__init__.py +13 -0
- foundry_mcp/cli/__init__.py +67 -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 +640 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +667 -0
- foundry_mcp/cli/commands/session.py +472 -0
- foundry_mcp/cli/commands/specs.py +686 -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 +298 -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 +1454 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1773 -0
- foundry_mcp/core/batch_operations.py +1202 -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/health.py +749 -0
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/journal.py +700 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1376 -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 +146 -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 +387 -0
- foundry_mcp/core/prometheus.py +564 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +691 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
- foundry_mcp/core/prompts/plan_review.py +627 -0
- foundry_mcp/core/providers/__init__.py +237 -0
- foundry_mcp/core/providers/base.py +515 -0
- foundry_mcp/core/providers/claude.py +472 -0
- foundry_mcp/core/providers/codex.py +637 -0
- foundry_mcp/core/providers/cursor_agent.py +630 -0
- foundry_mcp/core/providers/detectors.py +515 -0
- foundry_mcp/core/providers/gemini.py +426 -0
- foundry_mcp/core/providers/opencode.py +718 -0
- foundry_mcp/core/providers/opencode_wrapper.js +308 -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 +857 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1234 -0
- foundry_mcp/core/research/providers/__init__.py +40 -0
- foundry_mcp/core/research/providers/base.py +242 -0
- foundry_mcp/core/research/providers/google.py +507 -0
- foundry_mcp/core/research/providers/perplexity.py +442 -0
- foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
- foundry_mcp/core/research/providers/tavily.py +383 -0
- foundry_mcp/core/research/workflows/__init__.py +25 -0
- foundry_mcp/core/research/workflows/base.py +298 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +539 -0
- foundry_mcp/core/research/workflows/deep_research.py +4142 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +1624 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +4119 -0
- foundry_mcp/core/task.py +2463 -0
- foundry_mcp/core/testing.py +839 -0
- foundry_mcp/core/validation.py +2357 -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 +177 -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 +300 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +164 -0
- foundry_mcp/dashboard/views/overview.py +96 -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/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +414 -0
- foundry_mcp/server.py +150 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +92 -0
- foundry_mcp/tools/unified/authoring.py +3620 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +268 -0
- foundry_mcp/tools/unified/environment.py +1341 -0
- foundry_mcp/tools/unified/error.py +479 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +640 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +876 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +589 -0
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +1042 -0
- foundry_mcp/tools/unified/review_helpers.py +314 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +565 -0
- foundry_mcp/tools/unified/spec.py +1283 -0
- foundry_mcp/tools/unified/task.py +3846 -0
- foundry_mcp/tools/unified/test.py +431 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.8.22.dist-info/METADATA +344 -0
- foundry_mcp-0.8.22.dist-info/RECORD +153 -0
- foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
- foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
- 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)
|