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