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,460 @@
1
+ """
2
+ Claude CLI provider implementation.
3
+
4
+ Bridges the `claude` command-line interface to the ProviderContext contract by
5
+ handling availability checks, safe command construction, response parsing, and
6
+ token usage normalization. Restricts to read-only operations for security.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import os
14
+ import subprocess
15
+ from typing import Any, Dict, List, Optional, Protocol, Sequence
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ from .base import (
20
+ ModelDescriptor,
21
+ ProviderCapability,
22
+ ProviderContext,
23
+ ProviderExecutionError,
24
+ ProviderHooks,
25
+ ProviderMetadata,
26
+ ProviderRequest,
27
+ ProviderResult,
28
+ ProviderStatus,
29
+ ProviderTimeoutError,
30
+ ProviderUnavailableError,
31
+ StreamChunk,
32
+ TokenUsage,
33
+ )
34
+ from .detectors import detect_provider_availability
35
+ from .registry import register_provider
36
+
37
+ DEFAULT_BINARY = "claude"
38
+ DEFAULT_TIMEOUT_SECONDS = 360
39
+ AVAILABILITY_OVERRIDE_ENV = "CLAUDE_CLI_AVAILABLE_OVERRIDE"
40
+ CUSTOM_BINARY_ENV = "CLAUDE_CLI_BINARY"
41
+
42
+ # Read-only tools allowed for Claude provider
43
+ # Core tools
44
+ ALLOWED_TOOLS = [
45
+ # File operations (read-only)
46
+ "Read",
47
+ "Grep",
48
+ "Glob",
49
+ # Task delegation
50
+ "Task",
51
+ # Bash commands - file viewing
52
+ "Bash(cat)",
53
+ "Bash(head:*)",
54
+ "Bash(tail:*)",
55
+ "Bash(bat:*)",
56
+ # Bash commands - directory listing/navigation
57
+ "Bash(ls:*)",
58
+ "Bash(tree:*)",
59
+ "Bash(pwd)",
60
+ "Bash(which:*)",
61
+ "Bash(whereis:*)",
62
+ # Bash commands - search/find
63
+ "Bash(grep:*)",
64
+ "Bash(rg:*)",
65
+ "Bash(ag:*)",
66
+ "Bash(find:*)",
67
+ "Bash(fd:*)",
68
+ # Bash commands - git operations (read-only)
69
+ "Bash(git log:*)",
70
+ "Bash(git show:*)",
71
+ "Bash(git diff:*)",
72
+ "Bash(git status:*)",
73
+ "Bash(git grep:*)",
74
+ "Bash(git blame:*)",
75
+ "Bash(git branch:*)",
76
+ "Bash(git rev-parse:*)",
77
+ "Bash(git describe:*)",
78
+ "Bash(git ls-tree:*)",
79
+ # Bash commands - text processing
80
+ "Bash(wc:*)",
81
+ "Bash(cut:*)",
82
+ "Bash(paste:*)",
83
+ "Bash(column:*)",
84
+ "Bash(sort:*)",
85
+ "Bash(uniq:*)",
86
+ # Bash commands - data formats
87
+ "Bash(jq:*)",
88
+ "Bash(yq:*)",
89
+ # Bash commands - file analysis
90
+ "Bash(file:*)",
91
+ "Bash(stat:*)",
92
+ "Bash(du:*)",
93
+ "Bash(df:*)",
94
+ # Bash commands - checksums/hashing
95
+ "Bash(md5sum:*)",
96
+ "Bash(shasum:*)",
97
+ "Bash(sha256sum:*)",
98
+ "Bash(sha512sum:*)",
99
+ ]
100
+
101
+ # Tools that should be explicitly blocked
102
+ DISALLOWED_TOOLS = [
103
+ "Write",
104
+ "Edit",
105
+ # Web operations (data exfiltration risk)
106
+ "WebSearch",
107
+ "WebFetch",
108
+ # Dangerous file operations
109
+ "Bash(rm:*)",
110
+ "Bash(rmdir:*)",
111
+ "Bash(dd:*)",
112
+ "Bash(mkfs:*)",
113
+ "Bash(fdisk:*)",
114
+ # File modifications
115
+ "Bash(touch:*)",
116
+ "Bash(mkdir:*)",
117
+ "Bash(mv:*)",
118
+ "Bash(cp:*)",
119
+ "Bash(chmod:*)",
120
+ "Bash(chown:*)",
121
+ "Bash(sed:*)",
122
+ "Bash(awk:*)",
123
+ # Git write operations
124
+ "Bash(git add:*)",
125
+ "Bash(git commit:*)",
126
+ "Bash(git push:*)",
127
+ "Bash(git pull:*)",
128
+ "Bash(git merge:*)",
129
+ "Bash(git rebase:*)",
130
+ "Bash(git reset:*)",
131
+ "Bash(git checkout:*)",
132
+ # Package installations
133
+ "Bash(npm install:*)",
134
+ "Bash(pip install:*)",
135
+ "Bash(apt install:*)",
136
+ "Bash(brew install:*)",
137
+ # System operations
138
+ "Bash(sudo:*)",
139
+ "Bash(halt:*)",
140
+ "Bash(reboot:*)",
141
+ "Bash(shutdown:*)",
142
+ ]
143
+
144
+ # System prompt warning about shell command limitations
145
+ SHELL_COMMAND_WARNING = """
146
+ IMPORTANT SECURITY NOTE: When using shell commands, be aware of the following restrictions:
147
+ 1. Only specific read-only commands are allowed (cat, grep, git log, etc.)
148
+ 2. Write operations, file modifications, and destructive commands are blocked
149
+ 3. Avoid using piped commands as they may bypass some security checks
150
+ 4. Use sequential commands or alternative approaches when possible
151
+ """
152
+
153
+
154
+ class RunnerProtocol(Protocol):
155
+ """Callable signature used for executing Claude CLI commands."""
156
+
157
+ def __call__(
158
+ self,
159
+ command: Sequence[str],
160
+ *,
161
+ timeout: Optional[int] = None,
162
+ env: Optional[Dict[str, str]] = None,
163
+ ) -> subprocess.CompletedProcess[str]:
164
+ raise NotImplementedError
165
+
166
+
167
+ def _default_runner(
168
+ command: Sequence[str],
169
+ *,
170
+ timeout: Optional[int] = None,
171
+ env: Optional[Dict[str, str]] = None,
172
+ ) -> subprocess.CompletedProcess[str]:
173
+ """Invoke the Claude CLI via subprocess."""
174
+ return subprocess.run( # noqa: S603,S607 - intentional CLI invocation
175
+ list(command),
176
+ capture_output=True,
177
+ text=True,
178
+ timeout=timeout,
179
+ env=env,
180
+ check=False,
181
+ )
182
+
183
+
184
+ CLAUDE_MODELS: List[ModelDescriptor] = [
185
+ ModelDescriptor(
186
+ id="sonnet",
187
+ display_name="Sonnet 4.5",
188
+ capabilities={
189
+ ProviderCapability.TEXT,
190
+ ProviderCapability.STREAMING,
191
+ ProviderCapability.VISION,
192
+ ProviderCapability.THINKING,
193
+ },
194
+ routing_hints={"tier": "default", "description": "Smartest model for daily use"},
195
+ ),
196
+ ModelDescriptor(
197
+ id="haiku",
198
+ display_name="Haiku 4.5",
199
+ capabilities={
200
+ ProviderCapability.TEXT,
201
+ ProviderCapability.STREAMING,
202
+ },
203
+ routing_hints={"tier": "fast", "description": "Fastest model for simple tasks"},
204
+ ),
205
+ ]
206
+
207
+ CLAUDE_METADATA = ProviderMetadata(
208
+ provider_id="claude",
209
+ display_name="Anthropic Claude CLI",
210
+ models=CLAUDE_MODELS,
211
+ default_model="sonnet",
212
+ capabilities={
213
+ ProviderCapability.TEXT,
214
+ ProviderCapability.STREAMING,
215
+ ProviderCapability.VISION,
216
+ ProviderCapability.THINKING,
217
+ },
218
+ security_flags={"writes_allowed": False, "read_only": True},
219
+ extra={"cli": "claude", "output_format": "json", "allowed_tools": ALLOWED_TOOLS},
220
+ )
221
+
222
+
223
+ class ClaudeProvider(ProviderContext):
224
+ """ProviderContext implementation backed by the Claude CLI with read-only restrictions."""
225
+
226
+ def __init__(
227
+ self,
228
+ metadata: ProviderMetadata,
229
+ hooks: ProviderHooks,
230
+ *,
231
+ model: Optional[str] = None,
232
+ binary: Optional[str] = None,
233
+ runner: Optional[RunnerProtocol] = None,
234
+ env: Optional[Dict[str, str]] = None,
235
+ timeout: Optional[int] = None,
236
+ ):
237
+ super().__init__(metadata, hooks)
238
+ self._runner = runner or _default_runner
239
+ self._binary = binary or os.environ.get(CUSTOM_BINARY_ENV, DEFAULT_BINARY)
240
+ self._env = env
241
+ self._timeout = timeout or DEFAULT_TIMEOUT_SECONDS
242
+ self._model = self._ensure_model(model or metadata.default_model or self._first_model_id())
243
+
244
+ def _first_model_id(self) -> str:
245
+ if not self.metadata.models:
246
+ raise ProviderUnavailableError(
247
+ "Claude provider metadata is missing model descriptors.",
248
+ provider=self.metadata.provider_id,
249
+ )
250
+ return self.metadata.models[0].id
251
+
252
+ def _ensure_model(self, candidate: str) -> str:
253
+ available = {descriptor.id for descriptor in self.metadata.models}
254
+ if candidate not in available:
255
+ raise ProviderExecutionError(
256
+ f"Unsupported Claude model '{candidate}'. Available: {', '.join(sorted(available))}",
257
+ provider=self.metadata.provider_id,
258
+ )
259
+ return candidate
260
+
261
+ def _validate_request(self, request: ProviderRequest) -> None:
262
+ """Validate and normalize request, ignoring unsupported parameters."""
263
+ unsupported: List[str] = []
264
+ # Note: Claude CLI may not support these parameters via flags
265
+ if request.temperature is not None:
266
+ unsupported.append("temperature")
267
+ if request.max_tokens is not None:
268
+ unsupported.append("max_tokens")
269
+ if request.attachments:
270
+ unsupported.append("attachments")
271
+ if unsupported:
272
+ # Log warning but continue - ignore unsupported parameters
273
+ logger.warning(
274
+ f"Claude CLI ignoring unsupported parameters: {', '.join(unsupported)}"
275
+ )
276
+
277
+ def _build_command(
278
+ self, model: str, prompt: str, system_prompt: Optional[str] = None
279
+ ) -> List[str]:
280
+ """
281
+ Build Claude CLI command with read-only tool restrictions.
282
+
283
+ Command structure:
284
+ claude --print [prompt] --output-format json --allowed-tools Read Grep ... --disallowed-tools Write Edit Bash
285
+ """
286
+ command = [self._binary, "--print", prompt, "--output-format", "json"]
287
+
288
+ # Add read-only tool restrictions
289
+ command.extend(["--allowed-tools"] + ALLOWED_TOOLS)
290
+ command.extend(["--disallowed-tools"] + DISALLOWED_TOOLS)
291
+
292
+ # Build system prompt with security warning
293
+ full_system_prompt = system_prompt or ""
294
+ if full_system_prompt:
295
+ full_system_prompt = f"{full_system_prompt.strip()}\n\n{SHELL_COMMAND_WARNING.strip()}"
296
+ else:
297
+ full_system_prompt = SHELL_COMMAND_WARNING.strip()
298
+
299
+ # Add system prompt
300
+ command.extend(["--system-prompt", full_system_prompt])
301
+
302
+ # Add model if specified and not default
303
+ if model and model != self.metadata.default_model:
304
+ command.extend(["--model", model])
305
+
306
+ return command
307
+
308
+ def _run(self, command: Sequence[str], timeout: Optional[float]) -> subprocess.CompletedProcess[str]:
309
+ try:
310
+ return self._runner(command, timeout=int(timeout) if timeout else None, env=self._env)
311
+ except FileNotFoundError as exc:
312
+ raise ProviderUnavailableError(
313
+ f"Claude CLI '{self._binary}' is not available on PATH.",
314
+ provider=self.metadata.provider_id,
315
+ ) from exc
316
+ except subprocess.TimeoutExpired as exc:
317
+ raise ProviderTimeoutError(
318
+ f"Command timed out after {exc.timeout} seconds",
319
+ provider=self.metadata.provider_id,
320
+ ) from exc
321
+
322
+ def _parse_output(self, raw: str) -> Dict[str, Any]:
323
+ text = raw.strip()
324
+ if not text:
325
+ raise ProviderExecutionError(
326
+ "Claude CLI returned empty output.",
327
+ provider=self.metadata.provider_id,
328
+ )
329
+ try:
330
+ return json.loads(text)
331
+ except json.JSONDecodeError as exc:
332
+ logger.debug(f"Claude CLI JSON parse error: {exc}")
333
+ raise ProviderExecutionError(
334
+ "Claude CLI returned invalid JSON response",
335
+ provider=self.metadata.provider_id,
336
+ ) from exc
337
+
338
+ def _extract_usage(self, payload: Dict[str, Any]) -> TokenUsage:
339
+ """
340
+ Extract token usage from Claude CLI JSON response.
341
+
342
+ Expected structure:
343
+ {
344
+ "usage": {"input_tokens": 10, "output_tokens": 50, ...},
345
+ "modelUsage": {"claude-sonnet-4-5-20250929": {...}},
346
+ ...
347
+ }
348
+ """
349
+ usage = payload.get("usage") or {}
350
+ return TokenUsage(
351
+ input_tokens=int(usage.get("input_tokens") or 0),
352
+ output_tokens=int(usage.get("output_tokens") or 0),
353
+ cached_input_tokens=int(usage.get("cached_input_tokens") or 0),
354
+ total_tokens=int(usage.get("input_tokens") or 0) + int(usage.get("output_tokens") or 0),
355
+ )
356
+
357
+ def _resolve_model(self, request: ProviderRequest) -> str:
358
+ model_override = request.metadata.get("model") if request.metadata else None
359
+ if model_override:
360
+ return self._ensure_model(str(model_override))
361
+ return self._model
362
+
363
+ def _emit_stream_if_requested(self, content: str, *, stream: bool) -> None:
364
+ if not stream or not content:
365
+ return
366
+ self._emit_stream_chunk(StreamChunk(content=content, index=0))
367
+
368
+ def _execute(self, request: ProviderRequest) -> ProviderResult:
369
+ self._validate_request(request)
370
+ model = self._resolve_model(request)
371
+ command = self._build_command(model, request.prompt, system_prompt=request.system_prompt)
372
+ timeout = request.timeout or self._timeout
373
+ completed = self._run(command, timeout=timeout)
374
+
375
+ if completed.returncode != 0:
376
+ stderr = (completed.stderr or "").strip()
377
+ logger.debug(f"Claude CLI stderr: {stderr or 'no stderr'}")
378
+ raise ProviderExecutionError(
379
+ f"Claude CLI exited with code {completed.returncode}",
380
+ provider=self.metadata.provider_id,
381
+ )
382
+
383
+ payload = self._parse_output(completed.stdout)
384
+
385
+ # Extract content from "result" field (as per claude-model-chorus pattern)
386
+ content = str(payload.get("result") or payload.get("content") or "").strip()
387
+
388
+ # Extract model from modelUsage if available
389
+ model_usage = payload.get("modelUsage") or {}
390
+ reported_model = list(model_usage.keys())[0] if model_usage else model
391
+
392
+ usage = self._extract_usage(payload)
393
+
394
+ self._emit_stream_if_requested(content, stream=request.stream)
395
+
396
+ return ProviderResult(
397
+ content=content,
398
+ provider_id=self.metadata.provider_id,
399
+ model_used=f"{self.metadata.provider_id}:{reported_model}",
400
+ status=ProviderStatus.SUCCESS,
401
+ tokens=usage,
402
+ stderr=(completed.stderr or "").strip() or None,
403
+ raw_payload=payload,
404
+ )
405
+
406
+
407
+ def is_claude_available() -> bool:
408
+ """Claude CLI availability check."""
409
+ return detect_provider_availability("claude")
410
+
411
+
412
+ def create_provider(
413
+ *,
414
+ hooks: ProviderHooks,
415
+ model: Optional[str] = None,
416
+ dependencies: Optional[Dict[str, object]] = None,
417
+ overrides: Optional[Dict[str, object]] = None,
418
+ ) -> ClaudeProvider:
419
+ """
420
+ Factory used by the provider registry.
421
+
422
+ dependencies/overrides allow callers (or tests) to inject runner/env/binary.
423
+ """
424
+ dependencies = dependencies or {}
425
+ overrides = overrides or {}
426
+ runner = dependencies.get("runner")
427
+ env = dependencies.get("env")
428
+ binary = overrides.get("binary") or dependencies.get("binary")
429
+ timeout = overrides.get("timeout")
430
+ selected_model = overrides.get("model") if overrides.get("model") else model
431
+
432
+ return ClaudeProvider(
433
+ metadata=CLAUDE_METADATA,
434
+ hooks=hooks,
435
+ model=selected_model, # type: ignore[arg-type]
436
+ binary=binary, # type: ignore[arg-type]
437
+ runner=runner if runner is not None else None, # type: ignore[arg-type]
438
+ env=env if env is not None else None, # type: ignore[arg-type]
439
+ timeout=timeout if timeout is not None else None, # type: ignore[arg-type]
440
+ )
441
+
442
+
443
+ # Register the provider immediately so consumers can resolve it by id.
444
+ register_provider(
445
+ "claude",
446
+ factory=create_provider,
447
+ metadata=CLAUDE_METADATA,
448
+ availability_check=is_claude_available,
449
+ description="Anthropic Claude CLI adapter with read-only tool restrictions",
450
+ tags=("cli", "text", "vision", "thinking", "read-only"),
451
+ replace=True,
452
+ )
453
+
454
+
455
+ __all__ = [
456
+ "ClaudeProvider",
457
+ "create_provider",
458
+ "is_claude_available",
459
+ "CLAUDE_METADATA",
460
+ ]