minimalcode 1.5.2__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 (58) hide show
  1. minimalcode/__init__.py +3 -0
  2. minimalcode/__main__.py +3 -0
  3. minimalcode/agent.py +1084 -0
  4. minimalcode/cli.py +1520 -0
  5. minimalcode/clipboard.py +212 -0
  6. minimalcode/config.py +1065 -0
  7. minimalcode/defaults.json +491 -0
  8. minimalcode/errors.py +31 -0
  9. minimalcode/highlight/__init__.py +47 -0
  10. minimalcode/highlight/code.py +261 -0
  11. minimalcode/highlight/color.py +58 -0
  12. minimalcode/highlight/markdown.py +710 -0
  13. minimalcode/highlight/palette.py +73 -0
  14. minimalcode/highlight/sanitize.py +175 -0
  15. minimalcode/highlight/thinking.py +71 -0
  16. minimalcode/httpclient.py +278 -0
  17. minimalcode/importer.py +253 -0
  18. minimalcode/prompt.py +195 -0
  19. minimalcode/provider/__init__.py +28 -0
  20. minimalcode/provider/anthropic.py +805 -0
  21. minimalcode/provider/anthropic_env.py +161 -0
  22. minimalcode/provider/chat.py +394 -0
  23. minimalcode/provider/client.py +243 -0
  24. minimalcode/provider/common.py +176 -0
  25. minimalcode/provider/deepseek_web.py +1158 -0
  26. minimalcode/provider/factory.py +63 -0
  27. minimalcode/provider/responses.py +692 -0
  28. minimalcode/provider/sha3_wasm_bg.wasm +0 -0
  29. minimalcode/provider/transport.py +241 -0
  30. minimalcode/provider/types.py +54 -0
  31. minimalcode/sandbox/__init__.py +64 -0
  32. minimalcode/sandbox/audit.py +120 -0
  33. minimalcode/sandbox/backends.py +343 -0
  34. minimalcode/sandbox/execution.py +435 -0
  35. minimalcode/sandbox/network.py +793 -0
  36. minimalcode/sandbox/policy.py +253 -0
  37. minimalcode/sandbox/runner.py +347 -0
  38. minimalcode/sandbox/sensitive.py +286 -0
  39. minimalcode/session.py +465 -0
  40. minimalcode/shell.py +333 -0
  41. minimalcode/subagent.py +120 -0
  42. minimalcode/tools/__init__.py +46 -0
  43. minimalcode/tools/files.py +1139 -0
  44. minimalcode/tools/process.py +164 -0
  45. minimalcode/tools/remote.py +74 -0
  46. minimalcode/tools/runtime.py +219 -0
  47. minimalcode/tools/schemas.py +418 -0
  48. minimalcode/tools/search.py +521 -0
  49. minimalcode/tools/search_tools.py +583 -0
  50. minimalcode/tools/validation.py +99 -0
  51. minimalcode/vision.py +231 -0
  52. minimalcode/web.py +238 -0
  53. minimalcode-1.5.2.dist-info/METADATA +646 -0
  54. minimalcode-1.5.2.dist-info/RECORD +58 -0
  55. minimalcode-1.5.2.dist-info/WHEEL +5 -0
  56. minimalcode-1.5.2.dist-info/entry_points.txt +2 -0
  57. minimalcode-1.5.2.dist-info/licenses/LICENSE +201 -0
  58. minimalcode-1.5.2.dist-info/top_level.txt +1 -0
minimalcode/vision.py ADDED
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import math
5
+ import time
6
+ from typing import Any
7
+ from urllib.parse import urlsplit
8
+
9
+ import httpx2
10
+
11
+ from .config import provider_api_key
12
+ from .errors import ToolError
13
+ from .httpclient import (
14
+ build_client,
15
+ build_headers,
16
+ join_url_path,
17
+ merge_headers,
18
+ post_json,
19
+ )
20
+
21
+ # The provider judges formats by content, not by file name, so the tool does the
22
+ # same and labels the data URL with the detected type.
23
+ SUPPORTED_IMAGE_FORMATS = "JPEG, PNG, GIF, or WebP"
24
+ _DETAIL_LEVELS = {"low", "high", "original", "auto"}
25
+
26
+
27
+ def image_media_type(data: bytes) -> str | None:
28
+ """Return the MIME type of a supported image, detected from its bytes."""
29
+ if not isinstance(data, (bytes, bytearray)):
30
+ return None
31
+ if data.startswith(b"\x89PNG\r\n\x1a\n"):
32
+ return "image/png"
33
+ if data.startswith(b"\xff\xd8\xff"):
34
+ return "image/jpeg"
35
+ if data.startswith((b"GIF87a", b"GIF89a")):
36
+ return "image/gif"
37
+ if len(data) >= 12 and data[:4] == b"RIFF" and data[8:12] == b"WEBP":
38
+ return "image/webp"
39
+ return None
40
+
41
+
42
+ def _message_text(value: Any) -> str:
43
+ if isinstance(value, str):
44
+ return value
45
+ if isinstance(value, list):
46
+ parts: list[str] = []
47
+ for item in value:
48
+ if isinstance(item, str):
49
+ parts.append(item)
50
+ elif isinstance(item, dict) and isinstance(item.get("text"), str):
51
+ parts.append(item["text"])
52
+ return "".join(parts)
53
+ return ""
54
+
55
+
56
+ class ProviderVisionClient:
57
+ """Provider-hosted image understanding over an OpenAI-compatible endpoint."""
58
+
59
+ def __init__(
60
+ self,
61
+ name: str,
62
+ config: dict[str, Any],
63
+ *,
64
+ model: str | None = None,
65
+ transport: httpx2.BaseTransport | None = None,
66
+ ):
67
+ self.name = name
68
+ self.enabled = False
69
+ self.model = ""
70
+ self.detail = "auto"
71
+ self.max_image_bytes = 0
72
+ self.max_chars = 0
73
+ self._endpoint = ""
74
+ self._client: httpx2.Client | None = None
75
+
76
+ vision = config.get("vision")
77
+ if not isinstance(vision, dict):
78
+ return
79
+ enabled = vision.get("enabled", False)
80
+ if not isinstance(enabled, bool):
81
+ raise ToolError(f"provider {name!r} vision.enabled must be a boolean")
82
+ if not enabled:
83
+ return
84
+
85
+ base_url = str(vision.get("base_url") or config.get("base_url") or "").rstrip(
86
+ "/"
87
+ )
88
+ try:
89
+ parsed = urlsplit(base_url)
90
+ hostname = parsed.hostname
91
+ _ = parsed.port
92
+ except ValueError as exc:
93
+ raise ToolError(
94
+ f"provider {name!r} has an invalid vision.base_url: {exc}"
95
+ ) from exc
96
+ if parsed.scheme not in {"http", "https"} or not hostname:
97
+ raise ToolError(f"provider {name!r} has an invalid vision.base_url")
98
+
99
+ chosen_model = str(vision.get("model") or model or config.get("model") or "")
100
+ if not chosen_model:
101
+ raise ToolError(f"provider {name!r} vision requires a model")
102
+
103
+ try:
104
+ timeout = float(vision.get("timeout_seconds", 120))
105
+ self.max_image_bytes = int(vision.get("max_image_bytes", 32 * 1024 * 1024))
106
+ self.max_chars = int(vision.get("max_chars", 8000))
107
+ except (TypeError, ValueError, OverflowError) as exc:
108
+ raise ToolError(
109
+ f"provider {name!r} has invalid vision numeric settings"
110
+ ) from exc
111
+ if not math.isfinite(timeout) or timeout <= 0:
112
+ raise ToolError(f"provider {name!r} has an invalid vision timeout")
113
+ if self.max_image_bytes < 1 or self.max_chars < 1:
114
+ raise ToolError(f"provider {name!r} has invalid vision limits")
115
+
116
+ detail = str(vision.get("detail", "auto"))
117
+ if detail not in _DETAIL_LEVELS:
118
+ raise ToolError(
119
+ f"provider {name!r} vision.detail must be one of "
120
+ f"{', '.join(sorted(_DETAIL_LEVELS))}"
121
+ )
122
+
123
+ api_key = provider_api_key(vision) or provider_api_key(config)
124
+ self._client = build_client(
125
+ base_url=base_url,
126
+ headers=build_headers(
127
+ api_key, merge_headers(config.get("headers"), vision.get("headers"))
128
+ ),
129
+ timeout=timeout,
130
+ transport=transport,
131
+ trust_env=vision.get("trust_env", config.get("trust_env")),
132
+ )
133
+ self._timeout_seconds = timeout
134
+ self._endpoint = (
135
+ base_url
136
+ if urlsplit(base_url).path.rstrip("/").endswith("/chat/completions")
137
+ else join_url_path(base_url, "chat/completions")
138
+ )
139
+ self.model = chosen_model
140
+ self.detail = detail
141
+ self.enabled = True
142
+
143
+ def close(self) -> None:
144
+ if self._client is not None:
145
+ self._client.close()
146
+
147
+ def describe(
148
+ self,
149
+ media_type: str,
150
+ data: bytes,
151
+ *,
152
+ question: str | None = None,
153
+ ) -> str:
154
+ if not self.enabled or self._client is None:
155
+ raise ToolError(f"read_image is not enabled for provider {self.name!r}")
156
+ if not isinstance(data, bytes) or not data:
157
+ raise ToolError("image data must be non-empty bytes")
158
+ if len(data) > self.max_image_bytes:
159
+ raise ToolError(
160
+ f"image is too large: {len(data)} bytes exceeds "
161
+ f"vision.max_image_bytes ({self.max_image_bytes})"
162
+ )
163
+ actual_type = image_media_type(data)
164
+ if actual_type is None:
165
+ raise ToolError(
166
+ f"image data is not one of the supported formats "
167
+ f"({SUPPORTED_IMAGE_FORMATS})"
168
+ )
169
+ if question is not None and not isinstance(question, str):
170
+ raise ToolError("question must be a string or null")
171
+ prompt = (question or "").strip() or (
172
+ "Describe this image in detail and transcribe all visible text verbatim."
173
+ )
174
+ encoded = base64.b64encode(data).decode("ascii")
175
+ payload = {
176
+ "model": self.model,
177
+ "messages": [
178
+ {
179
+ "role": "user",
180
+ "content": [
181
+ {"type": "text", "text": prompt},
182
+ {
183
+ "type": "image_url",
184
+ "image_url": {
185
+ "url": f"data:{actual_type};base64,{encoded}",
186
+ "detail": self.detail,
187
+ },
188
+ },
189
+ ],
190
+ }
191
+ ],
192
+ }
193
+ body = post_json(
194
+ self._client,
195
+ self._endpoint,
196
+ payload,
197
+ label="vision",
198
+ timeout=self._timeout_seconds,
199
+ deadline=time.monotonic() + self._timeout_seconds,
200
+ )
201
+
202
+ text = self._parse_text(body)
203
+ if not text.strip():
204
+ raise ToolError("provider returned an empty image description")
205
+ if len(text) > self.max_chars:
206
+ text = text[: self.max_chars] + "\n<description truncated>"
207
+ return text
208
+
209
+ @staticmethod
210
+ def _parse_text(body: Any) -> str:
211
+ if not isinstance(body, dict):
212
+ raise ToolError("provider vision response must be an object")
213
+ if body.get("error"):
214
+ raise ToolError(f"provider vision response failed: {body['error']}")
215
+ choices = body.get("choices")
216
+ if not isinstance(choices, list) or not choices:
217
+ raise ToolError("provider vision response has no choices")
218
+ choice = choices[0]
219
+ if not isinstance(choice, dict):
220
+ raise ToolError("provider vision response choice must be an object")
221
+ message = choice.get("message")
222
+ if not isinstance(message, dict):
223
+ raise ToolError("provider vision response has no message")
224
+ return _message_text(message.get("content"))
225
+
226
+
227
+ __all__ = [
228
+ "SUPPORTED_IMAGE_FORMATS",
229
+ "ProviderVisionClient",
230
+ "image_media_type",
231
+ ]
minimalcode/web.py ADDED
@@ -0,0 +1,238 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import time
5
+ from typing import Any
6
+ from urllib.parse import urlsplit
7
+
8
+ import httpx2
9
+
10
+ from .config import provider_api_key
11
+ from .errors import ToolError
12
+ from .httpclient import (
13
+ build_client,
14
+ build_headers,
15
+ join_url_path,
16
+ merge_headers,
17
+ post_json,
18
+ )
19
+
20
+ _MAX_NESTED_ITEMS = 32
21
+
22
+
23
+ class ProviderWebClient:
24
+ """Configurable provider-hosted web tools."""
25
+
26
+ def __init__(
27
+ self,
28
+ name: str,
29
+ config: dict[str, Any],
30
+ *,
31
+ transport: httpx2.BaseTransport | None = None,
32
+ ):
33
+ self.name = name
34
+ self.enabled = False
35
+ self.default_count = 0
36
+ self.max_count = 0
37
+ self.max_content_chars = 0
38
+ self._search_path = ""
39
+ self._fetch_path = ""
40
+ self._client: httpx2.Client | None = None
41
+
42
+ web = config.get("web_tools")
43
+ if not isinstance(web, dict):
44
+ return
45
+ enabled = web.get("enabled", False)
46
+ if not isinstance(enabled, bool):
47
+ raise ToolError(f"provider {name!r} web_tools.enabled must be a boolean")
48
+ if not enabled:
49
+ return
50
+
51
+ base_url = str(web.get("base_url") or "").rstrip("/")
52
+ try:
53
+ parsed = urlsplit(base_url)
54
+ hostname = parsed.hostname
55
+ _ = parsed.port
56
+ except ValueError as exc:
57
+ raise ToolError(
58
+ f"provider {name!r} has an invalid web_tools.base_url: {exc}"
59
+ ) from exc
60
+ if parsed.scheme not in {"http", "https"} or not hostname:
61
+ raise ToolError(f"provider {name!r} has an invalid web_tools.base_url")
62
+ required = (
63
+ web.get("search_path"),
64
+ web.get("fetch_path"),
65
+ web.get("timeout_seconds"),
66
+ web.get("default_count"),
67
+ web.get("max_count"),
68
+ web.get("max_content_chars"),
69
+ )
70
+ if any(value in (None, "") for value in required):
71
+ raise ToolError(
72
+ f"provider {name!r} web_tools requires paths, timeout, "
73
+ "counts, and max_content_chars"
74
+ )
75
+
76
+ try:
77
+ timeout = float(web["timeout_seconds"])
78
+ self.default_count = int(web["default_count"])
79
+ self.max_count = int(web["max_count"])
80
+ self.max_content_chars = int(web["max_content_chars"])
81
+ except (TypeError, ValueError, OverflowError) as exc:
82
+ raise ToolError(
83
+ f"provider {name!r} has invalid web_tools numeric settings"
84
+ ) from exc
85
+ if not math.isfinite(timeout) or timeout <= 0:
86
+ raise ToolError(f"provider {name!r} has invalid web_tools timeout")
87
+ if self.default_count < 1 or self.max_count < self.default_count:
88
+ raise ToolError(f"provider {name!r} has invalid web result limits")
89
+ if self.max_content_chars < 1:
90
+ raise ToolError(f"provider {name!r} has invalid max_content_chars")
91
+ self._timeout_seconds = timeout
92
+
93
+ api_key = provider_api_key(web) or provider_api_key(config)
94
+ self._client = build_client(
95
+ base_url=base_url,
96
+ headers=build_headers(
97
+ api_key, merge_headers(config.get("headers"), web.get("headers"))
98
+ ),
99
+ timeout=timeout,
100
+ transport=transport,
101
+ trust_env=web.get("trust_env", config.get("trust_env")),
102
+ )
103
+ self._endpoint = base_url
104
+ self._search_path = str(web["search_path"]).strip("/")
105
+ self._fetch_path = str(web["fetch_path"]).strip("/")
106
+ self.enabled = True
107
+
108
+ def close(self) -> None:
109
+ if self._client is not None:
110
+ self._client.close()
111
+
112
+ def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
113
+ if not self.enabled or self._client is None:
114
+ raise ToolError(f"web tools are not enabled for provider {self.name!r}")
115
+ return post_json(
116
+ self._client,
117
+ join_url_path(self._endpoint, path),
118
+ payload,
119
+ label="web",
120
+ timeout=self._timeout_seconds,
121
+ deadline=time.monotonic() + self._timeout_seconds,
122
+ )
123
+
124
+ def search(
125
+ self,
126
+ query: str,
127
+ count: int | None = None,
128
+ need_content: bool = False,
129
+ ori_question: str | None = None,
130
+ localization: str | None = None,
131
+ ) -> dict[str, Any]:
132
+ if not self.enabled:
133
+ raise ToolError(f"web tools are not enabled for provider {self.name!r}")
134
+ if not isinstance(query, str) or not query.strip() or len(query) > 128:
135
+ raise ToolError("query must contain 1-128 characters")
136
+ if count is not None and (
137
+ not isinstance(count, int) or isinstance(count, bool)
138
+ ):
139
+ raise ToolError("count must be an integer")
140
+ if not isinstance(need_content, bool):
141
+ raise ToolError("need_content must be a boolean")
142
+ if ori_question is not None and not isinstance(ori_question, str):
143
+ raise ToolError("ori_question must be a string or null")
144
+ if localization is not None and not isinstance(localization, str):
145
+ raise ToolError("localization must be a string or null")
146
+ effective_count = self.default_count if count is None else count
147
+ if not 1 <= effective_count <= self.max_count:
148
+ raise ToolError(f"count must be from 1 to {self.max_count}")
149
+ payload: dict[str, Any] = {
150
+ "query": query.strip(),
151
+ "count": effective_count,
152
+ "need_content": need_content,
153
+ }
154
+ if ori_question:
155
+ payload["ori_question"] = ori_question
156
+ if localization:
157
+ payload["localization"] = localization
158
+ data = self._post(self._search_path, payload)
159
+ results = data.get("results", [])
160
+ if not isinstance(results, list):
161
+ raise ToolError("provider web search results must be a list")
162
+ # Provider output is untrusted: keep only object results, cap the count,
163
+ # and bound every text field so one search cannot flood the context.
164
+ objects = [item for item in results if isinstance(item, dict)]
165
+ normalized = [
166
+ self._normalize_result(item)
167
+ for item in objects[: min(effective_count, self.max_count)]
168
+ ]
169
+ return {"status": "success", "results": normalized}
170
+
171
+ def _normalize_result(self, item: dict[str, Any]) -> dict[str, Any]:
172
+ return {key: self._normalize_value(value) for key, value in item.items()}
173
+
174
+ def _normalize_value(self, value: Any, depth: int = 0) -> Any:
175
+ if isinstance(value, str):
176
+ if len(value) > self.max_content_chars:
177
+ return value[: self.max_content_chars] + "\n<content truncated>"
178
+ return value
179
+ if depth >= 32:
180
+ return "<nested value omitted>"
181
+ if isinstance(value, dict):
182
+ items = list(value.items())
183
+ normalized = {
184
+ key: self._normalize_value(item, depth + 1)
185
+ for key, item in items[:_MAX_NESTED_ITEMS]
186
+ }
187
+ if len(items) > _MAX_NESTED_ITEMS:
188
+ normalized.setdefault("<truncated>", "<more object keys omitted>")
189
+ return normalized
190
+ if isinstance(value, list):
191
+ normalized = [
192
+ self._normalize_value(item, depth + 1)
193
+ for item in value[:_MAX_NESTED_ITEMS]
194
+ ]
195
+ if len(value) > _MAX_NESTED_ITEMS:
196
+ normalized.append("<more list items omitted>")
197
+ return normalized
198
+ return value
199
+
200
+ def fetch(self, url: str) -> dict[str, Any]:
201
+ if not self.enabled:
202
+ raise ToolError(f"web tools are not enabled for provider {self.name!r}")
203
+ try:
204
+ parsed = urlsplit(url) if isinstance(url, str) else None
205
+ hostname = parsed.hostname if parsed is not None else None
206
+ if parsed is not None:
207
+ _ = parsed.port
208
+ except ValueError as exc:
209
+ raise ToolError(
210
+ f"url must be an absolute HTTP or HTTPS URL: {exc}"
211
+ ) from exc
212
+ if parsed is None or parsed.scheme not in {"http", "https"} or not hostname:
213
+ raise ToolError("url must be an absolute HTTP or HTTPS URL")
214
+ data = self._post(self._fetch_path, {"url": url})
215
+ title = data.get("title")
216
+ if title is None:
217
+ title = ""
218
+ content = data.get("content")
219
+ if content is None:
220
+ # An explicit JSON null means "no readable content", the same way a
221
+ # null title is tolerated above.
222
+ content = ""
223
+ if not isinstance(title, str):
224
+ raise ToolError("provider web fetch title must be a string")
225
+ if not isinstance(content, str):
226
+ raise ToolError("provider web fetch content must be a string")
227
+ title_truncated = len(title) > self.max_content_chars
228
+ if title_truncated:
229
+ title = title[: self.max_content_chars] + "...<title truncated>"
230
+ truncated = len(content) > self.max_content_chars
231
+ if truncated:
232
+ content = content[: self.max_content_chars] + "\n<content truncated>"
233
+ return {
234
+ "status": "success",
235
+ "title": title,
236
+ "content": content,
237
+ "truncated": truncated or title_truncated,
238
+ }