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/shell.py ADDED
@@ -0,0 +1,333 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import os
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ from collections.abc import Callable, Mapping
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ from .errors import ShellError
13
+ from .sandbox import SandboxRunner, truncate_text
14
+
15
+ __all__ = [
16
+ "MAX_TIMEOUT_SECONDS",
17
+ "ShellSpec",
18
+ "detect_shell",
19
+ "powershell_heredoc_delimiter",
20
+ "probe_shell",
21
+ "run_shell",
22
+ "select_shell",
23
+ "truncate_text",
24
+ ]
25
+
26
+ # Hard ceiling for one command. `subagent` reports the timeout it used, so the
27
+ # clamp lives here and callers read it back instead of inventing their own cap.
28
+ MAX_TIMEOUT_SECONDS = 3600.0
29
+
30
+ # Bash heredoc starts with ``<<WORD``, ``<<-WORD``, or ``<<\WORD``. A PowerShell
31
+ # bit shift such as ``$x << 2`` cannot match because the delimiter is a word.
32
+ _HEREDOC_RE = re.compile(
33
+ r"(?<!<)<<-?\s*\\?(?P<quote>['\"]?)(?P<delimiter>[A-Za-z_][A-Za-z0-9_.-]*)"
34
+ r"(?P=quote)"
35
+ )
36
+ # A PowerShell here-string is a multi-line literal. Its body may legitimately
37
+ # contain ``<<EOF``, so it must be removed before looking for bash heredocs.
38
+ _HERE_STRING_RE = re.compile(
39
+ r"(?ms)(?<!\\)@(?P<quote>['\"])[ \t]*\r?\n.*?^[ \t]*(?P=quote)@[ \t]*$"
40
+ )
41
+
42
+ # A shell that exists on disk is not necessarily a shell that can start: a
43
+ # pwsh build without libicu, or one whose .NET runtime cannot reserve memory,
44
+ # aborts before reading the command. Detection verifies each candidate once so
45
+ # a broken binary cannot make every shell command fail.
46
+ _PROBE_TIMEOUT_SECONDS = 10
47
+ _PROBE_CACHE: dict[tuple[str, str], str | None] = {}
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class ShellSpec:
52
+ kind: str
53
+ executable: str
54
+ label: str
55
+ is_windows: bool
56
+
57
+
58
+ def _is_wsl_bash_stub(executable: str) -> bool:
59
+ """Return True for the WSL launcher that lives in System32.
60
+
61
+ ``shutil.which("bash")`` finds ``C:\\Windows\\System32\\bash.exe`` on many
62
+ Windows machines even when no Git Bash is installed. Running workspace
63
+ commands through it silently switches to a Linux namespace with different
64
+ paths and interpreters, so it must not be selected as the native shell.
65
+ """
66
+ if os.name != "nt":
67
+ return False
68
+ try:
69
+ parts = [part.lower() for part in Path(executable).resolve().parts]
70
+ except (OSError, RuntimeError, ValueError):
71
+ return False
72
+ if len(parts) < 2 or parts[-1] != "bash.exe":
73
+ return False
74
+ # System32 holds the launcher, and the Store app alias under WindowsApps is
75
+ # a zero-byte reparse point that behaves the same way.
76
+ return (parts[-2] == "system32" and "windows" in parts) or "windowsapps" in parts
77
+
78
+
79
+ def _kind_for_executable(executable: str) -> tuple[str, str]:
80
+ name = Path(executable).stem.lower()
81
+ if name == "pwsh" or name.startswith("pwsh-"):
82
+ return "pwsh7", "PowerShell 7"
83
+ if name == "powershell" or name.startswith("powershell-"):
84
+ return "windows_powershell", "Windows PowerShell 5.1"
85
+ if name == "bash":
86
+ return "bash", "bash"
87
+ if name == "zsh":
88
+ return "zsh", "zsh"
89
+ return "sh", "sh"
90
+
91
+
92
+ def _resolve_override(preferred: str) -> ShellSpec:
93
+ try:
94
+ expanded = str(Path(preferred).expanduser())
95
+ except (OSError, RuntimeError, TypeError, ValueError) as exc:
96
+ raise ShellError(f"invalid configured shell path: {preferred}") from exc
97
+ resolved = shutil.which(expanded)
98
+ if resolved is None:
99
+ candidate = Path(expanded)
100
+ if candidate.is_file():
101
+ resolved = str(candidate)
102
+ if resolved is None:
103
+ raise ShellError(f"configured shell not found: {preferred}")
104
+ if Path(resolved).stem.lower() in {"cmd", "command"}:
105
+ # cmd.exe does not accept `-c`, so it would print its banner, exit 0 and
106
+ # never run the command: refuse instead of failing silently.
107
+ raise ShellError(
108
+ f"configured shell is not supported: {resolved}; "
109
+ "use pwsh, powershell, bash, or zsh"
110
+ )
111
+ kind, label = _kind_for_executable(resolved)
112
+ if kind == "bash" and _is_wsl_bash_stub(resolved):
113
+ raise ShellError(
114
+ f"configured shell is the WSL bash launcher, not a native shell: {resolved}"
115
+ )
116
+ return ShellSpec(
117
+ kind=kind,
118
+ executable=resolved,
119
+ label=label,
120
+ is_windows=os.name == "nt",
121
+ )
122
+
123
+
124
+ def detect_shell(preferred: str | None = None) -> ShellSpec:
125
+ """Prefer pwsh, then the native fallback for the current platform.
126
+
127
+ Candidates are verified with a trivial command before they are accepted, so
128
+ an installed-but-unusable shell (missing ICU, no memory, wrong architecture)
129
+ falls through to the next one instead of failing every command later.
130
+ """
131
+ if preferred:
132
+ shell = _resolve_override(preferred)
133
+ reason = probe_shell(shell)
134
+ if reason is not None:
135
+ raise ShellError(
136
+ f"configured shell failed to start: {shell.executable}: {reason}"
137
+ )
138
+ return shell
139
+ return select_shell(os.name == "nt", shutil.which, probe=probe_shell)
140
+
141
+
142
+ def probe_shell(shell: ShellSpec) -> str | None:
143
+ """Return None when ``shell`` can start, otherwise a short failure reason.
144
+
145
+ The result is cached per executable for the lifetime of the process: the
146
+ probe costs one shell startup (~0.2 s for pwsh) and is therefore paid once.
147
+ """
148
+ key = (shell.executable, shell.kind)
149
+ if key in _PROBE_CACHE:
150
+ return _PROBE_CACHE[key]
151
+ reason = _probe_once(shell)
152
+ _PROBE_CACHE[key] = reason
153
+ return reason
154
+
155
+
156
+ def _probe_once(shell: ShellSpec) -> str | None:
157
+ try:
158
+ completed = subprocess.run(
159
+ prefix_command(shell, "exit 0"),
160
+ stdin=subprocess.DEVNULL,
161
+ capture_output=True,
162
+ timeout=_PROBE_TIMEOUT_SECONDS,
163
+ check=False,
164
+ )
165
+ except subprocess.TimeoutExpired:
166
+ return f"did not answer within {_PROBE_TIMEOUT_SECONDS:g} seconds"
167
+ except (OSError, ValueError) as exc:
168
+ return str(exc)
169
+ if completed.returncode == 0:
170
+ return None
171
+ detail = (completed.stderr or b"").decode("utf-8", errors="replace").strip()
172
+ if not detail:
173
+ detail = (completed.stdout or b"").decode("utf-8", errors="replace").strip()
174
+ first_line = detail.splitlines()[0] if detail else ""
175
+ if not first_line:
176
+ return f"exited with code {completed.returncode}"
177
+ return first_line[:200]
178
+
179
+
180
+ def select_shell(
181
+ is_windows: bool,
182
+ finder: Callable[[str], str | None],
183
+ *,
184
+ probe: Callable[[ShellSpec], str | None] | None = None,
185
+ ) -> ShellSpec:
186
+ """Resolve the shell order without depending on the host platform."""
187
+ candidates = (
188
+ ("pwsh", "powershell", "bash") if is_windows else ("pwsh", "bash", "zsh", "sh")
189
+ )
190
+ unusable: list[str] = []
191
+ for name in candidates:
192
+ executable = finder(name)
193
+ if executable:
194
+ kind, label = _kind_for_executable(executable)
195
+ if kind == "bash" and _is_wsl_bash_stub(executable):
196
+ unusable.append(f"{name}: WSL bash launcher is not a native shell")
197
+ continue
198
+ shell = ShellSpec(
199
+ kind=kind,
200
+ executable=executable,
201
+ label=label,
202
+ is_windows=is_windows,
203
+ )
204
+ if probe is not None:
205
+ reason = probe(shell)
206
+ if reason is not None:
207
+ unusable.append(f"{name}: {reason}")
208
+ continue
209
+ return shell
210
+ message = f"no supported shell found (tried {'/'.join(candidates)})"
211
+ if unusable:
212
+ message = f"{message}: {'; '.join(unusable)}"
213
+ raise ShellError(message)
214
+
215
+
216
+ def _unquoted_heredoc_marks(
217
+ line: str, quote: str | None = None
218
+ ) -> tuple[list[int], str | None]:
219
+ """Return ``<<`` offsets outside string literals, and the open quote.
220
+
221
+ ``quote`` carries an unterminated literal in from the previous line, because
222
+ a PowerShell string may span lines: scanning line by line reported a
223
+ ``<<WORD`` inside a multi-line string as a bash heredoc and refused the
224
+ command. ``#`` starts a comment and ends the scan of its line.
225
+ """
226
+ positions: list[int] = []
227
+ index = 0
228
+ while index < len(line):
229
+ char = line[index]
230
+ if quote is not None:
231
+ if char == "`" and quote == '"':
232
+ index += 2
233
+ continue
234
+ if char == quote:
235
+ quote = None
236
+ index += 1
237
+ continue
238
+ if char == "#":
239
+ break
240
+ if char in {"'", '"'}:
241
+ quote = char
242
+ index += 1
243
+ continue
244
+ if (
245
+ line.startswith("<<", index)
246
+ and not line.startswith("<<<", index)
247
+ and (index == 0 or line[index - 1] != "<")
248
+ ):
249
+ positions.append(index)
250
+ index += 2
251
+ continue
252
+ index += 1
253
+ return positions, quote
254
+
255
+
256
+ def powershell_heredoc_delimiter(command: str) -> str | None:
257
+ """Return the bash heredoc delimiter that PowerShell cannot execute.
258
+
259
+ ``<<`` inside a quoted string (including a PowerShell here-string) and a
260
+ shift such as ``$x << 2`` are ignored. The caller decides whether the
261
+ active shell is PowerShell; bash/zsh heredocs remain valid there.
262
+ """
263
+ command = _HERE_STRING_RE.sub("", command)
264
+ quote: str | None = None
265
+ for line in command.splitlines():
266
+ positions, quote = _unquoted_heredoc_marks(line, quote)
267
+ for index in positions:
268
+ match = _HEREDOC_RE.match(line, index)
269
+ if match is not None:
270
+ return match.group("delimiter")
271
+ return None
272
+
273
+
274
+ def prefix_command(shell: ShellSpec, command: str) -> list[str]:
275
+ """Build the shell invocation for ``command``."""
276
+ if shell.kind in {"pwsh7", "windows_powershell"}:
277
+ bootstrap = (
278
+ "$ProgressPreference='SilentlyContinue';"
279
+ "$ErrorActionPreference='Stop';"
280
+ "[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false);"
281
+ "$OutputEncoding=[Console]::OutputEncoding;"
282
+ )
283
+ return [
284
+ shell.executable,
285
+ "-NoLogo",
286
+ "-NoProfile",
287
+ "-NonInteractive",
288
+ "-Command",
289
+ bootstrap + command,
290
+ ]
291
+ if shell.kind == "bash":
292
+ return [shell.executable, "--noprofile", "--norc", "-c", command]
293
+ if shell.kind == "zsh":
294
+ return [shell.executable, "-f", "-c", command]
295
+ return [shell.executable, "-c", command]
296
+
297
+
298
+ def run_shell(
299
+ command: str,
300
+ *,
301
+ shell: ShellSpec,
302
+ cwd: Path,
303
+ timeout: float,
304
+ max_output_chars: int,
305
+ env: Mapping[str, str] | None = None,
306
+ stdin_text: str | None = None,
307
+ sandbox: SandboxRunner | None = None,
308
+ ) -> dict[str, object]:
309
+ """Run ``command`` through the unified sandbox execution entry point."""
310
+ if not isinstance(command, str):
311
+ return {"status": "error", "message": "command must be a string"}
312
+ if not command.strip():
313
+ return {"status": "error", "message": "command is empty"}
314
+ try:
315
+ requested_timeout = float(timeout)
316
+ except (TypeError, ValueError, OverflowError):
317
+ return {"status": "error", "message": "timeout must be a number"}
318
+ if math.isnan(requested_timeout):
319
+ return {"status": "error", "message": "timeout must be a number"}
320
+ timeout = max(1.0, min(requested_timeout, MAX_TIMEOUT_SECONDS))
321
+
322
+ runner = sandbox if sandbox is not None else SandboxRunner.disabled(cwd)
323
+ return runner.run_argv(
324
+ prefix_command(shell, command),
325
+ command=command,
326
+ shell_kind=shell.kind,
327
+ shell_label=shell.label,
328
+ cwd=cwd,
329
+ timeout=timeout,
330
+ max_output_chars=max_output_chars,
331
+ env=env,
332
+ stdin_text=stdin_text,
333
+ )
@@ -0,0 +1,120 @@
1
+ """Helpers for dispatching a child MinimalCode process.
2
+
3
+ The design is deliberately blunt: a subagent is one more shell command that
4
+ runs ``<python> -m minimalcode -p`` in the workspace. The task text travels on
5
+ stdin so no prompt text ever reaches the command line, and the nesting depth
6
+ travels through the environment so grandchildren can be disabled.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import shlex
13
+ import sys
14
+ from collections.abc import Mapping, Sequence
15
+ from pathlib import Path
16
+
17
+ from .shell import ShellSpec
18
+
19
+ DEPTH_ENV = "MINIMALCODE_SUBAGENT_DEPTH"
20
+
21
+ PREAMBLE = (
22
+ "You are a subagent dispatched by a parent MinimalCode agent. "
23
+ "Work only on the task below and answer with the result itself; "
24
+ "do not ask questions back.\n\n"
25
+ )
26
+
27
+ _POWERSHELL_KINDS = {"pwsh7", "windows_powershell"}
28
+
29
+
30
+ def quote_argument(value: str, shell: ShellSpec) -> str:
31
+ """Quote one command-line argument for the shell that runs the child."""
32
+ text = str(value)
33
+ if shell.kind in _POWERSHELL_KINDS:
34
+ return "'" + text.replace("'", "''") + "'"
35
+ return shlex.quote(text)
36
+
37
+
38
+ def build_argv(
39
+ *,
40
+ cwd: Path,
41
+ python: str | None = None,
42
+ home: Path | None = None,
43
+ provider: str | None = None,
44
+ model: str | None = None,
45
+ thinking: str | None = None,
46
+ max_turns: int | None = None,
47
+ extra_flags: Sequence[str] | None = None,
48
+ extra_options: Sequence[tuple[str, str]] | None = None,
49
+ ) -> list[str]:
50
+ """Raw argv for the child process, independent of any shell."""
51
+ argv = [
52
+ str(python or sys.executable),
53
+ "-m",
54
+ "minimalcode",
55
+ "-p",
56
+ "--no-session",
57
+ "--quiet",
58
+ ]
59
+ argv.extend(extra_flags or ())
60
+ options = [
61
+ ("--cwd", str(cwd)),
62
+ ("--home", None if home is None else str(home)),
63
+ ("--provider", provider),
64
+ ("--thinking", thinking),
65
+ ("--max-turns", None if max_turns is None else str(max_turns)),
66
+ ("-m", model),
67
+ *(extra_options or ()),
68
+ ]
69
+ for flag, value in options:
70
+ if value:
71
+ argv.extend([flag, value])
72
+ return argv
73
+
74
+
75
+ def build_command(shell: ShellSpec, **kwargs) -> str:
76
+ argv = build_argv(**kwargs)
77
+ quoted = [quote_argument(a, shell) for a in argv]
78
+ if shell.kind in _POWERSHELL_KINDS:
79
+ # If the interpreter cannot be launched at all, PowerShell leaves
80
+ # $LASTEXITCODE null and a bare `exit` reports success. The parent would
81
+ # then treat a child that never started as a completed empty answer.
82
+ return (
83
+ f"& {quoted[0]} "
84
+ + " ".join(quoted[1:])
85
+ + "; if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 }"
86
+ )
87
+ return " ".join(quoted)
88
+
89
+
90
+ def package_root() -> Path:
91
+ return Path(__file__).resolve().parent.parent
92
+
93
+
94
+ def child_env(depth: int, *, python_path: Path | None = None) -> dict[str, str]:
95
+ """Environment overrides so the child runs this checkout and knows its depth."""
96
+ root = str(python_path or package_root())
97
+ existing = os.environ.get("PYTHONPATH", "")
98
+ entries = [entry for entry in existing.split(os.pathsep) if entry]
99
+ if root not in entries:
100
+ entries.insert(0, root)
101
+ return {
102
+ DEPTH_ENV: str(max(0, int(depth))),
103
+ "PYTHONPATH": os.pathsep.join(entries),
104
+ # The child is driven through pipes: the task arrives on stdin and the
105
+ # answer leaves on stdout. A Windows pipe defaults to the ANSI code page,
106
+ # so a Chinese task or answer was decoded as cp936/cp1252 on one side and
107
+ # corrupted (or replaced with `?`). UTF-8 mode pins both directions.
108
+ "PYTHONUTF8": "1",
109
+ }
110
+
111
+
112
+ def depth_from_env(env: Mapping[str, str] | None = None) -> int:
113
+ source = os.environ if env is None else env
114
+ raw = str(source.get(DEPTH_ENV, "") or "").strip()
115
+ if not raw:
116
+ return 0
117
+ try:
118
+ return max(0, int(raw))
119
+ except ValueError:
120
+ return 0
@@ -0,0 +1,46 @@
1
+ """Workspace-bound tools exposed to the agent loop.
2
+
3
+ The implementation is split by concern:
4
+
5
+ * :mod:`minimalcode.tools.schemas` - provider-facing tool declarations
6
+ * :mod:`minimalcode.tools.files` - read/read_range/write/append_file/edit
7
+ * :mod:`minimalcode.tools.search` - low-level grep process handling
8
+ * :mod:`minimalcode.tools.search_tools` - grep/find runtime methods
9
+ * :mod:`minimalcode.tools.process` - shell/subagent execution
10
+ * :mod:`minimalcode.tools.remote` - provider web and vision clients
11
+ * :mod:`minimalcode.tools.validation` - model-argument checks
12
+ * :mod:`minimalcode.tools.runtime` - shared state and dispatch
13
+
14
+ ``__all__`` is the complete public surface. Names that lived on the pre-0.7
15
+ single ``tools`` module are no longer re-exported; import the helper from the
16
+ module that defines it.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from typing import Any
23
+
24
+ from ..errors import ToolError
25
+ from .runtime import ToolRuntime
26
+ from .schemas import (
27
+ READ_IMAGE_SCHEMA,
28
+ SUBAGENT_SCHEMA,
29
+ TOOL_SCHEMAS,
30
+ available_tool_schemas,
31
+ )
32
+
33
+ __all__ = [
34
+ "READ_IMAGE_SCHEMA",
35
+ "SUBAGENT_SCHEMA",
36
+ "TOOL_SCHEMAS",
37
+ "ToolError",
38
+ "ToolRuntime",
39
+ "available_tool_schemas",
40
+ "tool_result_text",
41
+ ]
42
+
43
+
44
+ def tool_result_text(result: dict[str, Any]) -> str:
45
+ """Serialize a tool result compactly for the next model message."""
46
+ return json.dumps(result, ensure_ascii=False, separators=(",", ":"))