kiwimatecoder 0.1.0__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 (104) hide show
  1. kiwimatecoder/__init__.py +8 -0
  2. kiwimatecoder/__main__.py +12 -0
  3. kiwimatecoder/acp/__init__.py +13 -0
  4. kiwimatecoder/acp/protocol.py +115 -0
  5. kiwimatecoder/acp/server.py +823 -0
  6. kiwimatecoder/acp/stdio.py +32 -0
  7. kiwimatecoder/agent.py +1162 -0
  8. kiwimatecoder/ai.py +68 -0
  9. kiwimatecoder/audit.py +65 -0
  10. kiwimatecoder/browser.py +261 -0
  11. kiwimatecoder/catalog.py +445 -0
  12. kiwimatecoder/checkpoints.py +103 -0
  13. kiwimatecoder/client.py +820 -0
  14. kiwimatecoder/clipboard.py +99 -0
  15. kiwimatecoder/commands.py +6757 -0
  16. kiwimatecoder/config.py +5335 -0
  17. kiwimatecoder/diagnostics.py +295 -0
  18. kiwimatecoder/documents.py +299 -0
  19. kiwimatecoder/evals/__init__.py +39 -0
  20. kiwimatecoder/evals/cases.py +230 -0
  21. kiwimatecoder/evals/render.py +46 -0
  22. kiwimatecoder/evals/runner.py +319 -0
  23. kiwimatecoder/events.py +95 -0
  24. kiwimatecoder/forge.py +172 -0
  25. kiwimatecoder/git.py +164 -0
  26. kiwimatecoder/headless.py +255 -0
  27. kiwimatecoder/hooks.py +170 -0
  28. kiwimatecoder/hunks.py +260 -0
  29. kiwimatecoder/i18n.py +368 -0
  30. kiwimatecoder/images.py +195 -0
  31. kiwimatecoder/index/__init__.py +14 -0
  32. kiwimatecoder/index/builder.py +376 -0
  33. kiwimatecoder/index/embeddings.py +151 -0
  34. kiwimatecoder/index/search.py +247 -0
  35. kiwimatecoder/index/store.py +251 -0
  36. kiwimatecoder/instructions.py +83 -0
  37. kiwimatecoder/jobs.py +594 -0
  38. kiwimatecoder/lsp/__init__.py +56 -0
  39. kiwimatecoder/lsp/client.py +548 -0
  40. kiwimatecoder/lsp/manager.py +353 -0
  41. kiwimatecoder/lsp/protocol.py +79 -0
  42. kiwimatecoder/main.py +4258 -0
  43. kiwimatecoder/mcp/__init__.py +45 -0
  44. kiwimatecoder/mcp/client.py +385 -0
  45. kiwimatecoder/mcp/errors.py +7 -0
  46. kiwimatecoder/mcp/manager.py +253 -0
  47. kiwimatecoder/mcp/protocol.py +132 -0
  48. kiwimatecoder/media.py +904 -0
  49. kiwimatecoder/memory.py +141 -0
  50. kiwimatecoder/network.py +112 -0
  51. kiwimatecoder/notify.py +139 -0
  52. kiwimatecoder/permissions.py +171 -0
  53. kiwimatecoder/pickers.py +417 -0
  54. kiwimatecoder/plugins.py +245 -0
  55. kiwimatecoder/pricing.py +167 -0
  56. kiwimatecoder/prompts.py +213 -0
  57. kiwimatecoder/providers.py +342 -0
  58. kiwimatecoder/redaction.py +66 -0
  59. kiwimatecoder/remote.py +294 -0
  60. kiwimatecoder/repl.py +1388 -0
  61. kiwimatecoder/routing.py +65 -0
  62. kiwimatecoder/sandbox.py +169 -0
  63. kiwimatecoder/sdk.py +146 -0
  64. kiwimatecoder/session.py +574 -0
  65. kiwimatecoder/share.py +386 -0
  66. kiwimatecoder/shell.py +660 -0
  67. kiwimatecoder/skills.py +138 -0
  68. kiwimatecoder/sync.py +513 -0
  69. kiwimatecoder/team.py +249 -0
  70. kiwimatecoder/telemetry.py +392 -0
  71. kiwimatecoder/templates.py +108 -0
  72. kiwimatecoder/tools/__init__.py +190 -0
  73. kiwimatecoder/tools/ask.py +62 -0
  74. kiwimatecoder/tools/base.py +69 -0
  75. kiwimatecoder/tools/browser.py +261 -0
  76. kiwimatecoder/tools/edit_file.py +143 -0
  77. kiwimatecoder/tools/forge.py +207 -0
  78. kiwimatecoder/tools/git.py +158 -0
  79. kiwimatecoder/tools/image.py +300 -0
  80. kiwimatecoder/tools/io.py +346 -0
  81. kiwimatecoder/tools/list_dir.py +67 -0
  82. kiwimatecoder/tools/lsp.py +254 -0
  83. kiwimatecoder/tools/memory.py +115 -0
  84. kiwimatecoder/tools/paths.py +234 -0
  85. kiwimatecoder/tools/read_file.py +113 -0
  86. kiwimatecoder/tools/registry.py +91 -0
  87. kiwimatecoder/tools/run_bash.py +189 -0
  88. kiwimatecoder/tools/search.py +224 -0
  89. kiwimatecoder/tools/selection.py +108 -0
  90. kiwimatecoder/tools/shell.py +158 -0
  91. kiwimatecoder/tools/skill.py +46 -0
  92. kiwimatecoder/tools/task.py +61 -0
  93. kiwimatecoder/tools/todo.py +77 -0
  94. kiwimatecoder/tools/web.py +146 -0
  95. kiwimatecoder/tools/write_file.py +83 -0
  96. kiwimatecoder/ui.py +189 -0
  97. kiwimatecoder/updater.py +502 -0
  98. kiwimatecoder/web.py +499 -0
  99. kiwimatecoder-0.1.0.dist-info/METADATA +2016 -0
  100. kiwimatecoder-0.1.0.dist-info/RECORD +104 -0
  101. kiwimatecoder-0.1.0.dist-info/WHEEL +5 -0
  102. kiwimatecoder-0.1.0.dist-info/entry_points.txt +2 -0
  103. kiwimatecoder-0.1.0.dist-info/licenses/LICENSE +21 -0
  104. kiwimatecoder-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,342 @@
1
+ """Built-in registry of model providers.
2
+
3
+ Most providers expose an OpenAI-compatible ``/chat/completions`` API so a single
4
+ :class:`~kiwimatecoder.client.UnifiedClient` can drive them. Providers whose
5
+ ``compat`` is ``"anthropic"`` (Anthropic itself, or a custom provider configured
6
+ that way) are driven through the native Messages API with SSE streaming and
7
+ tool-use support instead.
8
+
9
+ There are no default models. The user chooses a model when they add a
10
+ provider (``kiwimatecoder setup``, ``/provider`` in the REPL, or
11
+ ``config provider add``), and that choice is persisted per provider (see
12
+ ``config.get_provider_model``). The ``models`` tuples below were verified in
13
+ July 2026 and are only suggestions for the pickers when the live listing is
14
+ unavailable: once a provider has a key, :mod:`kiwimatecoder.catalog` fetches
15
+ its live ``/models`` listing so newly released ids are offered and retired
16
+ ones disappear (see ``config.get_model_catalog``). Any id can still be set by
17
+ name with ``/model`` or ``config model set``, and the offered list can be
18
+ reshaped with ``/config models allow|deny``.
19
+
20
+ Local providers (Ollama, LM Studio, Unsloth) serve whatever models are loaded,
21
+ so when no model has been chosen the session model is read live from the
22
+ running server (see ``config.resolve_model``). Ollama and LM Studio need no API
23
+ key; Unsloth enforces auth even locally (``requires_key=True``), so its
24
+ ``sk-unsloth-…`` key must be configured before the server can be used.
25
+
26
+ Experimental providers (``experimental=True``, currently KiwiMate) are listed
27
+ first and drawn in their own "Experimental" box by the provider pickers.
28
+ Providers with ``supports_tools=False`` are chat-only: the client sends them no
29
+ tool schemas and flattens any tool traffic in the history into plain text.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from dataclasses import dataclass, field
35
+ from urllib.parse import quote, urlparse
36
+
37
+ # Hosts that serve the local machine (LM Studio, Ollama, llama.cpp, Unsloth,
38
+ # ...). ``*.local`` hosts are treated the same way. Local does not imply
39
+ # keyless: see ``ProviderConfig.requires_key``.
40
+ _LOCAL_HOSTS = {"localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"}
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class ProviderConfig:
45
+ """Static configuration for a single model provider."""
46
+
47
+ id: str
48
+ name: str
49
+ base_url: str # includes /v1, never a trailing /chat/completions
50
+ key_env: str
51
+ # Set for local servers that enforce auth anyway (Unsloth's sk-unsloth-…
52
+ # key). Keyless locals (Ollama, LM Studio) and custom providers leave it off.
53
+ requires_key: bool = False
54
+ compat: str = "openai" # "openai" | "anthropic" (native Messages API)
55
+ extra_headers: dict[str, str] = field(default_factory=dict)
56
+ # Suggested models offered by the pickers until a live listing is
57
+ # available; not exhaustive, and any id can still be set by name. None of
58
+ # them is a default: the user always chooses.
59
+ models: tuple[str, ...] = ()
60
+ # Auth header used for OpenAI-compatible requests. Cloud providers want
61
+ # ``Authorization: Bearer <key>``; Azure OpenAI wants ``api-key: <key>``.
62
+ # Both fields are ignored for native Anthropic providers (x-api-key).
63
+ key_header: str = "Authorization"
64
+ key_prefix: str = "Bearer "
65
+ # Azure-style API versioning: appended as ``?api-version=<value>`` to chat,
66
+ # catalog, and embedding URLs when non-empty.
67
+ api_version: str = ""
68
+ # Shown in its own "Experimental" box at the top of the provider pickers.
69
+ experimental: bool = False
70
+ # False for chat-only endpoints: no tool schemas are sent and tool calls /
71
+ # results already in the history are flattened into plain text.
72
+ supports_tools: bool = True
73
+ # Short note shown next to the provider in pickers and tables.
74
+ description: str = ""
75
+ # Per-message image limits enforced by the endpoint (0 = no limit). The
76
+ # client drops what would be rejected and says so in a text part.
77
+ max_images_per_message: int = 0
78
+ max_image_url_chars: int = 0
79
+
80
+ @property
81
+ def is_local(self) -> bool:
82
+ """Whether the provider serves the local machine."""
83
+ host = (urlparse(self.base_url).hostname or "").lower()
84
+ return host in _LOCAL_HOSTS or host.endswith(".local")
85
+
86
+ @property
87
+ def needs_key(self) -> bool:
88
+ """Whether requests fail without an API key (cloud or auth-enforcing local server)."""
89
+ return self.requires_key or not self.is_local
90
+
91
+ def versioned_url(self, url: str) -> str:
92
+ """Append ``api_version`` as an ``api-version`` query when set.
93
+
94
+ Azure OpenAI versions its endpoint with a date query parameter, so the
95
+ client, catalog, and embeddings all build URLs through here. Providers
96
+ without an ``api_version`` get the URL back untouched.
97
+ """
98
+ if not self.api_version:
99
+ return url
100
+ separator = "&" if "?" in url else "?"
101
+ return f"{url}{separator}api-version={quote(self.api_version, safe='')}"
102
+
103
+
104
+ class UnknownProviderError(KeyError):
105
+ """KeyError subclass for unknown provider IDs.
106
+
107
+ Subclassing preserves all existing ``except KeyError`` sites (in main,
108
+ commands, config, and tests). Overrides __str__ so f"{exc}" and the red
109
+ error prints produce clean messages without Python's extra repr quotes.
110
+ """
111
+
112
+ def __str__(self) -> str:
113
+ return self.args[0] if self.args else super().__str__()
114
+
115
+
116
+ REGISTRY: dict[str, ProviderConfig] = {
117
+ # Experimental: KiwiMate's own OpenAI-compatible API (kiwimate.net, keys
118
+ # start with sk-km-; see kiwimate.net/developers/docs). The endpoint is
119
+ # chat-only for now — it ignores tool schemas, rejects tool-role
120
+ # messages, drops system prompts, and caps replies at 1,024 tokens — so
121
+ # it runs with supports_tools=False and cannot edit files or run commands.
122
+ "kiwimate": ProviderConfig(
123
+ id="kiwimate",
124
+ name="KiwiMate",
125
+ base_url="https://gznrhppouxwpfihlfgpb.supabase.co/functions/v1/api-chat-completions",
126
+ key_env="KIWIMATE_API_KEY",
127
+ models=(
128
+ "kiwimate-mini-1-0",
129
+ "kiwimate-small-1-0",
130
+ "kiwimate-medium-1-0",
131
+ "kiwimate-large-1-0",
132
+ ),
133
+ experimental=True,
134
+ supports_tools=False,
135
+ description="kiwimate.net · chat only, no tool use yet",
136
+ # data: URLs only, at most 4 per message, each under 3,000,000 chars.
137
+ max_images_per_message=4,
138
+ max_image_url_chars=3_000_000,
139
+ ),
140
+ "openai": ProviderConfig(
141
+ id="openai",
142
+ name="OpenAI",
143
+ base_url="https://api.openai.com/v1",
144
+ key_env="OPENAI_API_KEY",
145
+ models=("gpt-5.6-sol", "gpt-5.5"),
146
+ ),
147
+ "anthropic": ProviderConfig(
148
+ id="anthropic",
149
+ name="Anthropic",
150
+ base_url="https://api.anthropic.com/v1",
151
+ key_env="ANTHROPIC_API_KEY",
152
+ compat="anthropic",
153
+ models=("claude-sonnet-5", "claude-opus-4-8", "claude-haiku-4-5"),
154
+ ),
155
+ "google": ProviderConfig(
156
+ id="google",
157
+ name="Google Gemini",
158
+ base_url="https://generativelanguage.googleapis.com/v1beta/openai",
159
+ key_env="GEMINI_API_KEY",
160
+ models=("gemini-3.5-flash", "gemini-3.5-pro"),
161
+ ),
162
+ "xai": ProviderConfig(
163
+ id="xai",
164
+ name="xAI Grok",
165
+ base_url="https://api.x.ai/v1",
166
+ key_env="XAI_API_KEY",
167
+ models=("grok-4.5", "grok-build-0.1"),
168
+ ),
169
+ "mistral": ProviderConfig(
170
+ id="mistral",
171
+ name="Mistral",
172
+ base_url="https://api.mistral.ai/v1",
173
+ key_env="MISTRAL_API_KEY",
174
+ models=("mistral-medium-3.5", "devstral-2512"),
175
+ ),
176
+ "deepseek": ProviderConfig(
177
+ id="deepseek",
178
+ name="DeepSeek",
179
+ base_url="https://api.deepseek.com/v1",
180
+ key_env="DEEPSEEK_API_KEY",
181
+ models=("deepseek-v4-pro", "deepseek-chat", "deepseek-reasoner"),
182
+ ),
183
+ "qwen": ProviderConfig(
184
+ id="qwen",
185
+ name="Qwen (Alibaba DashScope)",
186
+ base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
187
+ key_env="DASHSCOPE_API_KEY",
188
+ models=("qwen3.7-max", "qwen-plus", "qwen-turbo"),
189
+ ),
190
+ "moonshot": ProviderConfig(
191
+ id="moonshot",
192
+ name="Moonshot (Kimi)",
193
+ base_url="https://api.moonshot.ai/v1",
194
+ key_env="MOONSHOT_API_KEY",
195
+ models=("kimi-k2.7-code", "kimi-latest"),
196
+ ),
197
+ "openrouter": ProviderConfig(
198
+ id="openrouter",
199
+ name="OpenRouter",
200
+ base_url="https://openrouter.ai/api/v1",
201
+ key_env="OPENROUTER_API_KEY",
202
+ extra_headers={
203
+ "HTTP-Referer": "https://kiwimatecoder.com",
204
+ "X-Title": "KiwiMateCoder",
205
+ },
206
+ models=(
207
+ "anthropic/claude-sonnet-5",
208
+ "anthropic/claude-opus-4-8",
209
+ "openai/gpt-5.6-sol",
210
+ "google/gemini-3.5-flash",
211
+ "x-ai/grok-4.5",
212
+ "deepseek/deepseek-v4-pro",
213
+ "qwen/qwen3.7-max",
214
+ "moonshotai/kimi-k2.7-code",
215
+ "mistralai/devstral-2512",
216
+ "z-ai/glm-5.2",
217
+ ),
218
+ ),
219
+ "azure": ProviderConfig(
220
+ id="azure",
221
+ name="Azure OpenAI",
222
+ # Placeholder only: replace <resource> with your Azure resource name.
223
+ # Because built-in entries are not editable, the supported path is a
224
+ # custom provider pointed at your resource, e.g.
225
+ # ``config provider add my-azure "My Azure" \
226
+ # https://my-resource.openai.azure.com/openai/v1 my-deployment \
227
+ # --key-env AZURE_OPENAI_API_KEY --key-header api-key \
228
+ # --key-prefix "" --api-version 2024-10-21`` (the fourth argument is
229
+ # the model — your deployment name — to use with it).
230
+ base_url="https://<resource>.openai.azure.com/openai/v1",
231
+ key_env="AZURE_OPENAI_API_KEY",
232
+ key_header="api-key",
233
+ key_prefix="",
234
+ api_version="2024-10-21",
235
+ models=("gpt-5.6-sol", "gpt-5.5"),
236
+ ),
237
+ "bedrock": ProviderConfig(
238
+ id="bedrock",
239
+ name="AWS Bedrock",
240
+ # Placeholder only: replace <region> with your AWS region. Bedrock's
241
+ # OpenAI-compatible runtime accepts a bearer token; SigV4/IAM signing is
242
+ # out of scope (use a signing proxy or custom provider if you need it).
243
+ base_url="https://bedrock-runtime.<region>.amazonaws.com/openai/v1",
244
+ key_env="AWS_BEARER_TOKEN_BEDROCK",
245
+ models=("openai.gpt-5.6-sol", "mistral.devstral-2512"),
246
+ ),
247
+ "groq": ProviderConfig(
248
+ id="groq",
249
+ name="Groq",
250
+ base_url="https://api.groq.com/openai/v1",
251
+ key_env="GROQ_API_KEY",
252
+ models=("llama-4.1-70b-versatile", "qwen3.7-32b"),
253
+ ),
254
+ "together": ProviderConfig(
255
+ id="together",
256
+ name="Together AI",
257
+ base_url="https://api.together.xyz/v1",
258
+ key_env="TOGETHER_API_KEY",
259
+ models=(
260
+ "meta-llama/Llama-4.1-70B-Instruct-Turbo",
261
+ "Qwen/Qwen3.7-72B-Instruct-Turbo",
262
+ ),
263
+ ),
264
+ "fireworks": ProviderConfig(
265
+ id="fireworks",
266
+ name="Fireworks AI",
267
+ base_url="https://api.fireworks.ai/inference/v1",
268
+ key_env="FIREWORKS_API_KEY",
269
+ models=(
270
+ "accounts/fireworks/models/llama-v4-70b-instruct",
271
+ "accounts/fireworks/models/qwen3.7-32b-instruct",
272
+ ),
273
+ ),
274
+ "cerebras": ProviderConfig(
275
+ id="cerebras",
276
+ name="Cerebras",
277
+ base_url="https://api.cerebras.ai/v1",
278
+ key_env="CEREBRAS_API_KEY",
279
+ models=("llama-4.1-70b", "qwen-3.7-32b"),
280
+ ),
281
+ "deepinfra": ProviderConfig(
282
+ id="deepinfra",
283
+ name="DeepInfra",
284
+ base_url="https://api.deepinfra.com/v1/openai",
285
+ key_env="DEEPINFRA_API_KEY",
286
+ models=(
287
+ "meta-llama/Llama-4.1-70B-Instruct",
288
+ "Qwen/Qwen3.7-72B-Instruct",
289
+ ),
290
+ ),
291
+ "ollama": ProviderConfig(
292
+ id="ollama",
293
+ name="Ollama (local)",
294
+ base_url="http://localhost:11434/v1",
295
+ # With no chosen model, the session uses whatever the running server
296
+ # lists first. The suggested tuple only fills the offline picker.
297
+ key_env="OLLAMA_API_KEY", # optional; only if the server enforces auth
298
+ models=("llama3.1:8b", "qwen3:8b", "deepseek-r1:8b"),
299
+ ),
300
+ "lmstudio": ProviderConfig(
301
+ id="lmstudio",
302
+ name="LM Studio (local)",
303
+ base_url="http://localhost:1234/v1",
304
+ key_env="LMSTUDIO_API_KEY", # optional; only if the server enforces auth
305
+ models=("qwen2.5-coder-7b-instruct", "llama-3.1-8b-instruct"),
306
+ ),
307
+ "unsloth": ProviderConfig(
308
+ id="unsloth",
309
+ name="Unsloth (local)",
310
+ base_url="http://localhost:8888/v1",
311
+ # With no chosen model, the session uses whatever GGUF is loaded in
312
+ # Unsloth Studio. The suggested tuple only fills the offline picker.
313
+ # Required: Unsloth's local server enforces auth (Settings → API,
314
+ # the key starts with sk-unsloth-).
315
+ key_env="UNSLOTH_API_KEY",
316
+ requires_key=True,
317
+ models=("unsloth/Qwen3.6-27B-GGUF", "unsloth/gemma-4-26B-A4B-it-GGUF"),
318
+ ),
319
+ }
320
+
321
+ DEFAULT_PROVIDER_ID = "openrouter"
322
+
323
+
324
+ def get_provider(provider_id: str) -> ProviderConfig:
325
+ """Return the provider config for ``provider_id`` or raise ``UnknownProviderError`` (a ``KeyError`` subclass)."""
326
+ try:
327
+ return REGISTRY[provider_id]
328
+ except KeyError:
329
+ raise UnknownProviderError(
330
+ f"Unknown provider '{provider_id}'. "
331
+ + f"Known providers: {', '.join(sorted(REGISTRY))}"
332
+ ) from None
333
+
334
+
335
+ def list_providers() -> list[ProviderConfig]:
336
+ """Return all registered providers in a stable order."""
337
+ return list(REGISTRY.values())
338
+
339
+
340
+ def default_provider() -> ProviderConfig:
341
+ """Return the default provider (OpenRouter, preserving legacy behavior)."""
342
+ return REGISTRY[DEFAULT_PROVIDER_ID]
@@ -0,0 +1,66 @@
1
+ """Secret detection and redaction for logs and audit records.
2
+
3
+ Patterns cover common provider keys, cloud credentials, bearer tokens, and
4
+ ``key = value`` assignments. Redaction is intentionally conservative: it is
5
+ applied to audit records, never to tool output that the model needs verbatim.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import dataclass
12
+
13
+ PLACEHOLDER = "[REDACTED]"
14
+
15
+ _ASSIGNMENT = re.compile(
16
+ r"(?i)\b(api[_-]?key|access[_-]?key|secret|password|passwd|token)"
17
+ r"(\s*[:=]\s*)['\"]?([A-Za-z0-9._\-/+]{12,})"
18
+ )
19
+
20
+ _PATTERNS: list[tuple[str, re.Pattern[str]]] = [
21
+ ("anthropic_key", re.compile(r"\bsk-ant-[A-Za-z0-9_\-]{8,}")),
22
+ ("openrouter_key", re.compile(r"\bsk-or-[A-Za-z0-9_\-]{8,}")),
23
+ ("openai_key", re.compile(r"\bsk-[A-Za-z0-9_\-]{16,}")),
24
+ (
25
+ "github_token",
26
+ re.compile(
27
+ r"\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}"
28
+ r"|\bgithub_pat_[A-Za-z0-9_]{20,}"
29
+ ),
30
+ ),
31
+ ("aws_key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
32
+ ("slack_token", re.compile(r"\bxox[baprs]-[A-Za-z0-9\-]{10,}")),
33
+ ("bearer_token", re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._\-]{20,}")),
34
+ ]
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Finding:
39
+ """One detected secret, with a short non-reversible preview."""
40
+
41
+ kind: str
42
+ preview: str
43
+
44
+
45
+ def find_secrets(text: str) -> list[Finding]:
46
+ """Return the secrets detected in ``text``."""
47
+ findings: list[Finding] = []
48
+ for kind, pattern in _PATTERNS:
49
+ for match in pattern.finditer(text):
50
+ value = match.group(0)
51
+ findings.append(Finding(kind=kind, preview=value[:4] + "…"))
52
+ for match in _ASSIGNMENT.finditer(text):
53
+ value = match.group(3)
54
+ findings.append(Finding(kind="assignment", preview=value[:4] + "…"))
55
+ return findings
56
+
57
+
58
+ def redact(text: str, placeholder: str = PLACEHOLDER) -> str:
59
+ """Replace detected secrets in ``text`` with ``placeholder``."""
60
+ result = text
61
+ for _kind, pattern in _PATTERNS:
62
+ result = pattern.sub(placeholder, result)
63
+ result = _ASSIGNMENT.sub(
64
+ lambda match: f"{match.group(1)}{match.group(2)}{placeholder}", result
65
+ )
66
+ return result