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,426 @@
1
+ """
2
+ Gemini CLI provider implementation.
3
+
4
+ Bridges the `gemini` command-line interface to the ProviderContext contract by
5
+ handling availability checks, safe command construction, response parsing, and
6
+ token usage normalization.
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 = "gemini"
37
+ DEFAULT_TIMEOUT_SECONDS = 360
38
+ AVAILABILITY_OVERRIDE_ENV = "GEMINI_CLI_AVAILABLE_OVERRIDE"
39
+ CUSTOM_BINARY_ENV = "GEMINI_CLI_BINARY"
40
+
41
+ # Read-only tools allowed for safe codebase exploration
42
+ # Based on Gemini CLI tool names (both class names and function names supported)
43
+ ALLOWED_TOOLS = [
44
+ # Core file operations (read-only)
45
+ "ReadFileTool",
46
+ "read_file",
47
+ "ReadManyFilesTool",
48
+ "read_many_files",
49
+ "LSTool",
50
+ "list_directory",
51
+ "GlobTool",
52
+ "glob",
53
+ "GrepTool",
54
+ "search_file_content",
55
+ # Shell commands - file viewing
56
+ "ShellTool(cat)",
57
+ "ShellTool(head)",
58
+ "ShellTool(tail)",
59
+ "ShellTool(bat)",
60
+ # Shell commands - directory listing/navigation
61
+ "ShellTool(ls)",
62
+ "ShellTool(tree)",
63
+ "ShellTool(pwd)",
64
+ "ShellTool(which)",
65
+ "ShellTool(whereis)",
66
+ # Shell commands - search/find
67
+ "ShellTool(grep)",
68
+ "ShellTool(rg)",
69
+ "ShellTool(ag)",
70
+ "ShellTool(find)",
71
+ "ShellTool(fd)",
72
+ # Shell commands - git operations (read-only)
73
+ "ShellTool(git log)",
74
+ "ShellTool(git show)",
75
+ "ShellTool(git diff)",
76
+ "ShellTool(git status)",
77
+ "ShellTool(git grep)",
78
+ "ShellTool(git blame)",
79
+ # Shell commands - text processing
80
+ "ShellTool(wc)",
81
+ "ShellTool(cut)",
82
+ "ShellTool(paste)",
83
+ "ShellTool(column)",
84
+ "ShellTool(sort)",
85
+ "ShellTool(uniq)",
86
+ # Shell commands - data formats
87
+ "ShellTool(jq)",
88
+ "ShellTool(yq)",
89
+ # Shell commands - file analysis
90
+ "ShellTool(file)",
91
+ "ShellTool(stat)",
92
+ "ShellTool(du)",
93
+ "ShellTool(df)",
94
+ # Shell commands - checksums/hashing
95
+ "ShellTool(md5sum)",
96
+ "ShellTool(shasum)",
97
+ "ShellTool(sha256sum)",
98
+ "ShellTool(sha512sum)",
99
+ ]
100
+
101
+ # System prompt addition warning about piped command vulnerability
102
+ PIPED_COMMAND_WARNING = """
103
+ IMPORTANT SECURITY NOTE: When using shell commands, avoid piped commands (e.g., cat file.txt | wc -l).
104
+ Piped commands bypass the tool allowlist checks in Gemini CLI - only the first command in a pipe is validated.
105
+ Instead, use sequential commands or alternative approaches to achieve the same result safely.
106
+ """
107
+
108
+
109
+ class RunnerProtocol(Protocol):
110
+ """Callable signature used for executing Gemini CLI commands."""
111
+
112
+ def __call__(
113
+ self,
114
+ command: Sequence[str],
115
+ *,
116
+ timeout: Optional[int] = None,
117
+ env: Optional[Dict[str, str]] = None,
118
+ input_data: Optional[str] = None,
119
+ ) -> subprocess.CompletedProcess[str]:
120
+ raise NotImplementedError
121
+
122
+
123
+ def _default_runner(
124
+ command: Sequence[str],
125
+ *,
126
+ timeout: Optional[int] = None,
127
+ env: Optional[Dict[str, str]] = None,
128
+ input_data: Optional[str] = None,
129
+ ) -> subprocess.CompletedProcess[str]:
130
+ """Invoke the Gemini CLI via subprocess."""
131
+ return subprocess.run( # noqa: S603,S607 - intentional CLI invocation
132
+ list(command),
133
+ capture_output=True,
134
+ text=True,
135
+ input=input_data,
136
+ timeout=timeout,
137
+ env=env,
138
+ check=False,
139
+ )
140
+
141
+
142
+ GEMINI_METADATA = ProviderMetadata(
143
+ provider_id="gemini",
144
+ display_name="Google Gemini CLI",
145
+ models=[], # Model validation delegated to CLI
146
+ default_model="pro",
147
+ capabilities={ProviderCapability.TEXT, ProviderCapability.STREAMING, ProviderCapability.VISION},
148
+ security_flags={"writes_allowed": False},
149
+ extra={"cli": "gemini", "output_format": "json"},
150
+ )
151
+
152
+
153
+ class GeminiProvider(ProviderContext):
154
+ """ProviderContext implementation backed by the Gemini CLI."""
155
+
156
+ def __init__(
157
+ self,
158
+ metadata: ProviderMetadata,
159
+ hooks: ProviderHooks,
160
+ *,
161
+ model: Optional[str] = None,
162
+ binary: Optional[str] = None,
163
+ runner: Optional[RunnerProtocol] = None,
164
+ env: Optional[Dict[str, str]] = None,
165
+ timeout: Optional[int] = None,
166
+ ):
167
+ super().__init__(metadata, hooks)
168
+ self._runner = runner or _default_runner
169
+ self._binary = binary or os.environ.get(CUSTOM_BINARY_ENV, DEFAULT_BINARY)
170
+ self._env = env
171
+ self._timeout = timeout or DEFAULT_TIMEOUT_SECONDS
172
+ self._model = model or metadata.default_model or "pro"
173
+
174
+ def _validate_request(self, request: ProviderRequest) -> None:
175
+ """Validate and normalize request, ignoring unsupported parameters."""
176
+ unsupported: List[str] = []
177
+ if request.temperature is not None:
178
+ unsupported.append("temperature")
179
+ if request.max_tokens is not None:
180
+ unsupported.append("max_tokens")
181
+ if request.attachments:
182
+ unsupported.append("attachments")
183
+ if unsupported:
184
+ # Log warning but continue - ignore unsupported parameters
185
+ logger.warning(
186
+ f"Gemini CLI ignoring unsupported parameters: {', '.join(unsupported)}"
187
+ )
188
+
189
+ def _build_prompt(self, request: ProviderRequest) -> str:
190
+ # Build the system prompt with security warning
191
+ system_parts = []
192
+ if request.system_prompt:
193
+ system_parts.append(request.system_prompt.strip())
194
+ system_parts.append(PIPED_COMMAND_WARNING.strip())
195
+
196
+ if system_parts:
197
+ return f"{chr(10).join(system_parts)}\n\n{request.prompt}"
198
+ return request.prompt
199
+
200
+ def _build_command(self, model: str) -> List[str]:
201
+ """
202
+ Build Gemini CLI command with read-only tool restrictions.
203
+
204
+ Prompt is passed via stdin to avoid CLI argument length limits.
205
+ """
206
+ command = [self._binary, "--output-format", "json"]
207
+
208
+ # Add allowed tools for read-only enforcement
209
+ for tool in ALLOWED_TOOLS:
210
+ command.extend(["--allowed-tools", tool])
211
+
212
+ # Insert model if specified
213
+ if model:
214
+ command[1:1] = ["-m", model]
215
+
216
+ return command
217
+
218
+ def _run(
219
+ self, command: Sequence[str], timeout: Optional[float], input_data: Optional[str] = None
220
+ ) -> subprocess.CompletedProcess[str]:
221
+ try:
222
+ return self._runner(
223
+ command, timeout=int(timeout) if timeout else None, env=self._env, input_data=input_data
224
+ )
225
+ except FileNotFoundError as exc:
226
+ raise ProviderUnavailableError(
227
+ f"Gemini CLI '{self._binary}' is not available on PATH.",
228
+ provider=self.metadata.provider_id,
229
+ ) from exc
230
+ except subprocess.TimeoutExpired as exc:
231
+ raise ProviderTimeoutError(
232
+ f"Command timed out after {exc.timeout} seconds",
233
+ provider=self.metadata.provider_id,
234
+ ) from exc
235
+
236
+ def _parse_output(self, raw: str) -> Dict[str, Any]:
237
+ text = raw.strip()
238
+ if not text:
239
+ raise ProviderExecutionError(
240
+ "Gemini CLI returned empty output.",
241
+ provider=self.metadata.provider_id,
242
+ )
243
+ try:
244
+ return json.loads(text)
245
+ except json.JSONDecodeError as exc:
246
+ logger.debug(f"Gemini CLI JSON parse error: {exc}")
247
+ raise ProviderExecutionError(
248
+ "Gemini CLI returned invalid JSON response",
249
+ provider=self.metadata.provider_id,
250
+ ) from exc
251
+
252
+ def _extract_usage(self, payload: Dict[str, Any]) -> TokenUsage:
253
+ stats = payload.get("stats") or {}
254
+ models_section = stats.get("models") or {}
255
+ first_model = next(iter(models_section.values()), {})
256
+ tokens = first_model.get("tokens") or {}
257
+ return TokenUsage(
258
+ input_tokens=int(tokens.get("prompt") or tokens.get("input") or 0),
259
+ output_tokens=int(tokens.get("candidates") or tokens.get("output") or 0),
260
+ total_tokens=int(tokens.get("total") or 0),
261
+ )
262
+
263
+ def _resolve_model(self, request: ProviderRequest) -> str:
264
+ # 1. Check request.model first (from ProviderRequest constructor)
265
+ if request.model:
266
+ return str(request.model)
267
+ # 2. Fallback to metadata override (legacy/alternative path)
268
+ model_override = request.metadata.get("model") if request.metadata else None
269
+ if model_override:
270
+ return str(model_override)
271
+ # 3. Fallback to instance default
272
+ return self._model
273
+
274
+ def _emit_stream_if_requested(self, content: str, *, stream: bool) -> None:
275
+ if not stream or not content:
276
+ return
277
+ self._emit_stream_chunk(StreamChunk(content=content, index=0))
278
+
279
+ def _extract_error_from_output(self, stdout: str) -> Optional[str]:
280
+ """
281
+ Extract error message from Gemini CLI output.
282
+
283
+ Gemini CLI outputs errors as text lines followed by JSON. Example:
284
+ 'Error when talking to Gemini API Full report available at: /tmp/...
285
+ {"error": {"type": "Error", "message": "[object Object]", "code": 1}}'
286
+
287
+ The JSON message field is often unhelpful ("[object Object]"), so we
288
+ prefer the text prefix which contains the actual error description.
289
+ """
290
+ if not stdout:
291
+ return None
292
+
293
+ lines = stdout.strip().split("\n")
294
+ error_parts: List[str] = []
295
+
296
+ for line in lines:
297
+ line = line.strip()
298
+ if not line:
299
+ continue
300
+
301
+ # Skip "Loaded cached credentials" info line
302
+ if line.startswith("Loaded cached"):
303
+ continue
304
+
305
+ # Try to parse as JSON
306
+ if line.startswith("{"):
307
+ try:
308
+ payload = json.loads(line)
309
+ error = payload.get("error", {})
310
+ if isinstance(error, dict):
311
+ msg = error.get("message", "")
312
+ # Skip unhelpful "[object Object]" message
313
+ if msg and msg != "[object Object]":
314
+ error_parts.append(msg)
315
+ except json.JSONDecodeError:
316
+ pass
317
+ else:
318
+ # Text line - likely contains the actual error message
319
+ # Extract the part before "Full report available at:"
320
+ if "Full report available at:" in line:
321
+ line = line.split("Full report available at:")[0].strip()
322
+ if line:
323
+ error_parts.append(line)
324
+
325
+ return "; ".join(error_parts) if error_parts else None
326
+
327
+ def _execute(self, request: ProviderRequest) -> ProviderResult:
328
+ self._validate_request(request)
329
+ model = self._resolve_model(request)
330
+ prompt = self._build_prompt(request)
331
+ command = self._build_command(model)
332
+ timeout = request.timeout or self._timeout
333
+ # Pass prompt via stdin to avoid CLI argument length limits
334
+ completed = self._run(command, timeout=timeout, input_data=prompt)
335
+
336
+ if completed.returncode != 0:
337
+ stderr = (completed.stderr or "").strip()
338
+ logger.debug(f"Gemini CLI stderr: {stderr or 'no stderr'}")
339
+
340
+ # Extract error from stdout (Gemini outputs errors as text + JSON)
341
+ stdout_error = self._extract_error_from_output(completed.stdout)
342
+
343
+ error_msg = f"Gemini CLI exited with code {completed.returncode}"
344
+ if stdout_error:
345
+ error_msg += f": {stdout_error[:500]}"
346
+ elif stderr:
347
+ error_msg += f": {stderr[:500]}"
348
+ raise ProviderExecutionError(
349
+ error_msg,
350
+ provider=self.metadata.provider_id,
351
+ )
352
+
353
+ payload = self._parse_output(completed.stdout)
354
+ content = str(payload.get("response") or payload.get("content") or "").strip()
355
+ reported_model = payload.get("model") or next(
356
+ iter((payload.get("stats") or {}).get("models") or {}), model
357
+ )
358
+ usage = self._extract_usage(payload)
359
+
360
+ self._emit_stream_if_requested(content, stream=request.stream)
361
+
362
+ return ProviderResult(
363
+ content=content,
364
+ provider_id=self.metadata.provider_id,
365
+ model_used=f"{self.metadata.provider_id}:{reported_model}",
366
+ status=ProviderStatus.SUCCESS,
367
+ tokens=usage,
368
+ stderr=(completed.stderr or "").strip() or None,
369
+ raw_payload=payload,
370
+ )
371
+
372
+
373
+ def is_gemini_available() -> bool:
374
+ """Gemini CLI availability check."""
375
+ return detect_provider_availability("gemini")
376
+
377
+
378
+ def create_provider(
379
+ *,
380
+ hooks: ProviderHooks,
381
+ model: Optional[str] = None,
382
+ dependencies: Optional[Dict[str, object]] = None,
383
+ overrides: Optional[Dict[str, object]] = None,
384
+ ) -> GeminiProvider:
385
+ """
386
+ Factory used by the provider registry.
387
+
388
+ dependencies/overrides allow callers (or tests) to inject runner/env/binary.
389
+ """
390
+ dependencies = dependencies or {}
391
+ overrides = overrides or {}
392
+ runner = dependencies.get("runner")
393
+ env = dependencies.get("env")
394
+ binary = overrides.get("binary") or dependencies.get("binary")
395
+ timeout = overrides.get("timeout")
396
+ selected_model = overrides.get("model") if overrides.get("model") else model
397
+
398
+ return GeminiProvider(
399
+ metadata=GEMINI_METADATA,
400
+ hooks=hooks,
401
+ model=selected_model, # type: ignore[arg-type]
402
+ binary=binary, # type: ignore[arg-type]
403
+ runner=runner if runner is not None else None, # type: ignore[arg-type]
404
+ env=env if env is not None else None, # type: ignore[arg-type]
405
+ timeout=timeout if timeout is not None else None, # type: ignore[arg-type]
406
+ )
407
+
408
+
409
+ # Register the provider immediately so consumers can resolve it by id.
410
+ register_provider(
411
+ "gemini",
412
+ factory=create_provider,
413
+ metadata=GEMINI_METADATA,
414
+ availability_check=is_gemini_available,
415
+ description="Google Gemini CLI adapter",
416
+ tags=("cli", "text", "vision"),
417
+ replace=True,
418
+ )
419
+
420
+
421
+ __all__ = [
422
+ "GeminiProvider",
423
+ "create_provider",
424
+ "is_gemini_available",
425
+ "GEMINI_METADATA",
426
+ ]