spawnllm 0.5.1__tar.gz → 0.5.2__tar.gz
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.
- {spawnllm-0.5.1 → spawnllm-0.5.2}/PKG-INFO +1 -1
- {spawnllm-0.5.1 → spawnllm-0.5.2}/pyproject.toml +1 -1
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/backends/base.py +9 -4
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/backends/claude.py +48 -7
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/backends/codex.py +1 -1
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/backends/gemini.py +1 -1
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/backends/mlx.py +1 -1
- {spawnllm-0.5.1 → spawnllm-0.5.2}/LICENSE +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/README.md +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/__init__.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/__main__.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/backends/__init__.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/backends/registry.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/call.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/cli.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/extract.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/mlx/__init__.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/mlx/codec.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/mlx/engine.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/mlx/fuse.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/mlx/patches.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/proc.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/py.typed +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/response.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/run.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/spec.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/structured.py +0 -0
- {spawnllm-0.5.1 → spawnllm-0.5.2}/spawnllm/types.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "spawnllm"
|
|
3
3
|
# Inert sentinel: the real version is set from the release tag (uv version --frozen).
|
|
4
|
-
version = "0.5.
|
|
4
|
+
version = "0.5.2"
|
|
5
5
|
description = "Subshell + MLX LLM-calling backends (Claude/Codex CLI, local MLX) shared across tools."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
@@ -129,8 +129,13 @@ class LlmBackend(ABC):
|
|
|
129
129
|
"""
|
|
130
130
|
|
|
131
131
|
@abstractmethod
|
|
132
|
-
def env(self) -> dict[str, str]:
|
|
133
|
-
"""Return extra environment variables for the invocation, merged over the inherited environment.
|
|
132
|
+
def env(self, spec: RunSpec) -> dict[str, str]:
|
|
133
|
+
"""Return extra environment variables for the invocation, merged over the inherited environment.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
spec: The configured run, so a backend can scope env overrides to
|
|
137
|
+
`spec.isolated` (e.g. a fresh config home only when isolating).
|
|
138
|
+
"""
|
|
134
139
|
|
|
135
140
|
@abstractmethod
|
|
136
141
|
def is_authenticated(self, *, timeout: int) -> bool:
|
|
@@ -292,7 +297,7 @@ class CliBackend(LlmBackend):
|
|
|
292
297
|
rr = await acapture_cli(
|
|
293
298
|
inv.argv,
|
|
294
299
|
input=inv.stdin,
|
|
295
|
-
env=os.environ | self.env() | (spec.env or {}),
|
|
300
|
+
env=os.environ | self.env(spec) | (spec.env or {}),
|
|
296
301
|
cwd=spec.cwd,
|
|
297
302
|
timeout=spec.timeout,
|
|
298
303
|
)
|
|
@@ -311,7 +316,7 @@ class CliBackend(LlmBackend):
|
|
|
311
316
|
rr = capture_cli(
|
|
312
317
|
inv.argv,
|
|
313
318
|
input=inv.stdin,
|
|
314
|
-
env=os.environ | self.env() | (spec.env or {}),
|
|
319
|
+
env=os.environ | self.env(spec) | (spec.env or {}),
|
|
315
320
|
cwd=spec.cwd,
|
|
316
321
|
timeout=spec.timeout,
|
|
317
322
|
)
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import atexit
|
|
5
6
|
import json
|
|
7
|
+
import shutil
|
|
6
8
|
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
7
11
|
from typing import TYPE_CHECKING, ClassVar
|
|
8
12
|
|
|
9
13
|
from spawnllm.backends.base import CliBackend
|
|
@@ -58,6 +62,8 @@ class ClaudeCliBackend(CliBackend):
|
|
|
58
62
|
binary: ClassVar[str] = "claude"
|
|
59
63
|
install_hint: ClassVar[str] = "curl -fsSL https://claude.ai/install.sh | bash"
|
|
60
64
|
|
|
65
|
+
_isolated_config_dir: str | None = None
|
|
66
|
+
|
|
61
67
|
def build_command(self, spec: RunSpec) -> list[str]:
|
|
62
68
|
"""Build the `claude -p` argv for one stdin-prompted invocation.
|
|
63
69
|
|
|
@@ -151,15 +157,50 @@ class ClaudeCliBackend(CliBackend):
|
|
|
151
157
|
return event["result"] if isinstance(event.get("result"), str) else "claude reported an error"
|
|
152
158
|
return None
|
|
153
159
|
|
|
154
|
-
def env(self) -> dict[str, str]:
|
|
155
|
-
"""
|
|
160
|
+
def env(self, spec: RunSpec) -> dict[str, str]:
|
|
161
|
+
"""Point an isolated run at a fresh, host-free `CLAUDE_CONFIG_DIR`; otherwise add nothing.
|
|
162
|
+
|
|
163
|
+
Defense in depth behind the argv flags: a config home seeded with nothing
|
|
164
|
+
but the account pointer and OAuth token means plugin and
|
|
165
|
+
`~/.claude.json`-driven loading finds no host settings, plugins, or hooks
|
|
166
|
+
even if a flag is ever dropped.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
spec: The configured run; `spec.isolated` gates the override.
|
|
156
170
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
171
|
+
Returns:
|
|
172
|
+
`{"CLAUDE_CONFIG_DIR": <isolated dir>}` for an isolated run, else `{}`.
|
|
173
|
+
"""
|
|
174
|
+
if not spec.isolated:
|
|
175
|
+
return {}
|
|
176
|
+
return {"CLAUDE_CONFIG_DIR": self._isolated_dir()}
|
|
177
|
+
|
|
178
|
+
def _isolated_dir(self) -> str:
|
|
179
|
+
"""Return the process-lifetime isolated config home, creating and seeding it once.
|
|
180
|
+
|
|
181
|
+
The home is a fresh temp dir seeded with only the two auth-bearing files:
|
|
182
|
+
`~/.claude.json` minus its `mcpServers` block (the active-account pointer,
|
|
183
|
+
with no host MCP servers leaking even absent `--strict-mcp-config`) and
|
|
184
|
+
`~/.claude/.credentials.json` (the claude.ai OAuth token, which lives in
|
|
185
|
+
the config home — relocating it without this copy logs the run out). Host
|
|
186
|
+
`settings.json`, plugins, and hooks are never copied. The dir is cached on
|
|
187
|
+
the backend and removed at interpreter exit.
|
|
161
188
|
"""
|
|
162
|
-
|
|
189
|
+
if self._isolated_config_dir is not None:
|
|
190
|
+
return self._isolated_config_dir
|
|
191
|
+
config_dir = Path(tempfile.mkdtemp(prefix="spawnllm-claude-config-"))
|
|
192
|
+
home = Path.home()
|
|
193
|
+
account_path = home / ".claude.json"
|
|
194
|
+
if account_path.exists():
|
|
195
|
+
account = json.loads(account_path.read_text())
|
|
196
|
+
account.pop("mcpServers", None)
|
|
197
|
+
(config_dir / ".claude.json").write_text(json.dumps(account))
|
|
198
|
+
credentials_path = home / ".claude" / ".credentials.json"
|
|
199
|
+
if credentials_path.exists():
|
|
200
|
+
shutil.copyfile(credentials_path, config_dir / ".credentials.json")
|
|
201
|
+
atexit.register(shutil.rmtree, config_dir, ignore_errors=True)
|
|
202
|
+
self._isolated_config_dir = str(config_dir)
|
|
203
|
+
return self._isolated_config_dir
|
|
163
204
|
|
|
164
205
|
def is_authenticated(self, *, timeout: int) -> bool:
|
|
165
206
|
"""Report whether `claude auth status` exits cleanly, i.e. a claude.ai login is stored.
|
|
@@ -128,7 +128,7 @@ class CodexCliBackend(CliBackend):
|
|
|
128
128
|
|
|
129
129
|
return json.dumps(to_strict_json_schema(model))
|
|
130
130
|
|
|
131
|
-
def env(self) -> dict[str, str]:
|
|
131
|
+
def env(self, _spec: RunSpec) -> dict[str, str]:
|
|
132
132
|
"""Return no extra environment variables; `--ignore-user-config` isolates config while `CODEX_HOME` keeps auth.
|
|
133
133
|
|
|
134
134
|
`codex` keeps `auth.json` in `CODEX_HOME`, so relocating it would strand a
|
|
@@ -30,7 +30,7 @@ class GeminiFamilyBackend(CliBackend, ABC):
|
|
|
30
30
|
|
|
31
31
|
api_key_envs: ClassVar[tuple[str, ...]]
|
|
32
32
|
|
|
33
|
-
def env(self) -> dict[str, str]:
|
|
33
|
+
def env(self, _spec: RunSpec) -> dict[str, str]:
|
|
34
34
|
"""Return no extra environment variables.
|
|
35
35
|
|
|
36
36
|
Gemini-family CLIs read settings and OAuth from the same config home (`~/.gemini`,
|
|
@@ -52,7 +52,7 @@ class MlxBackend(LlmBackend):
|
|
|
52
52
|
"""Return the `structured_output` from a stream-json result event, else `raw` parsed as JSON."""
|
|
53
53
|
return structured_value(raw)
|
|
54
54
|
|
|
55
|
-
def env(self) -> dict[str, str]:
|
|
55
|
+
def env(self, _spec: RunSpec) -> dict[str, str]:
|
|
56
56
|
"""Return no extra environment variables; MLX runs in-process with nothing to isolate."""
|
|
57
57
|
return {}
|
|
58
58
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|