spawnllm 0.5.2__tar.gz → 0.5.4__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.2 → spawnllm-0.5.4}/PKG-INFO +1 -1
- {spawnllm-0.5.2 → spawnllm-0.5.4}/pyproject.toml +1 -1
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/backends/claude.py +36 -11
- {spawnllm-0.5.2 → spawnllm-0.5.4}/LICENSE +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/README.md +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/__init__.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/__main__.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/backends/__init__.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/backends/base.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/backends/codex.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/backends/gemini.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/backends/mlx.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/backends/registry.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/call.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/cli.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/extract.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/mlx/__init__.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/mlx/codec.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/mlx/engine.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/mlx/fuse.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/mlx/patches.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/proc.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/py.typed +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/response.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/run.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/spec.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/spawnllm/structured.py +0 -0
- {spawnllm-0.5.2 → spawnllm-0.5.4}/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.4"
|
|
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"
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import atexit
|
|
6
|
+
import hashlib
|
|
6
7
|
import json
|
|
8
|
+
import os
|
|
7
9
|
import shutil
|
|
8
10
|
import subprocess
|
|
11
|
+
import sys
|
|
9
12
|
import tempfile
|
|
10
13
|
from pathlib import Path
|
|
11
14
|
from typing import TYPE_CHECKING, ClassVar
|
|
@@ -38,6 +41,21 @@ def result_event(raw: str) -> dict[str, object] | None:
|
|
|
38
41
|
return None
|
|
39
42
|
|
|
40
43
|
|
|
44
|
+
def keychain_credentials(home: Path) -> str | None:
|
|
45
|
+
"""Return the claude.ai OAuth credentials for `home` from the macOS Keychain, or `None` off darwin / on a miss."""
|
|
46
|
+
if sys.platform != "darwin":
|
|
47
|
+
return None
|
|
48
|
+
service = f"Claude Code-credentials-{hashlib.sha256(str(home).encode()).hexdigest()[:8]}"
|
|
49
|
+
proc = subprocess.run(
|
|
50
|
+
["security", "find-generic-password", "-s", service, "-w"],
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
timeout=10,
|
|
54
|
+
check=False,
|
|
55
|
+
)
|
|
56
|
+
return proc.stdout if proc.returncode == 0 else None
|
|
57
|
+
|
|
58
|
+
|
|
41
59
|
class ClaudeCliBackend(CliBackend):
|
|
42
60
|
"""`CliBackend` for the Anthropic `claude` CLI.
|
|
43
61
|
|
|
@@ -178,26 +196,33 @@ class ClaudeCliBackend(CliBackend):
|
|
|
178
196
|
def _isolated_dir(self) -> str:
|
|
179
197
|
"""Return the process-lifetime isolated config home, creating and seeding it once.
|
|
180
198
|
|
|
181
|
-
The home is a fresh temp dir seeded with only the two auth-bearing files
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
the
|
|
199
|
+
The home is a fresh temp dir seeded with only the two auth-bearing files,
|
|
200
|
+
sourced from the caller's effective config home (`$CLAUDE_CONFIG_DIR` when
|
|
201
|
+
set, else `~/.claude`): the active-account pointer
|
|
202
|
+
(`$CLAUDE_CONFIG_DIR/.claude.json`, or `~/.claude.json` under the default
|
|
203
|
+
home — the CLI's own lookup rule) minus its `mcpServers` block, so no host
|
|
204
|
+
MCP servers leak even absent `--strict-mcp-config`, and the claude.ai
|
|
205
|
+
OAuth token — the home's `.credentials.json` when present, else on darwin
|
|
206
|
+
the `Claude Code-credentials-<sha256(home)[:8]>` Keychain item, written
|
|
207
|
+
out with mode 0600. When neither exists nothing is seeded and the run
|
|
208
|
+
fails with the CLI's own not-logged-in error. Host `settings.json`,
|
|
209
|
+
plugins, and hooks are never copied. The dir is cached on the backend and
|
|
210
|
+
removed at interpreter exit.
|
|
188
211
|
"""
|
|
189
212
|
if self._isolated_config_dir is not None:
|
|
190
213
|
return self._isolated_config_dir
|
|
191
214
|
config_dir = Path(tempfile.mkdtemp(prefix="spawnllm-claude-config-"))
|
|
192
|
-
home = Path.home()
|
|
193
|
-
account_path = home / ".claude.json"
|
|
215
|
+
home = Path(env_home) if (env_home := os.environ.get("CLAUDE_CONFIG_DIR")) else Path.home() / ".claude"
|
|
216
|
+
account_path = home / ".claude.json" if env_home else Path.home() / ".claude.json"
|
|
194
217
|
if account_path.exists():
|
|
195
218
|
account = json.loads(account_path.read_text())
|
|
196
219
|
account.pop("mcpServers", None)
|
|
197
220
|
(config_dir / ".claude.json").write_text(json.dumps(account))
|
|
198
|
-
credentials_path
|
|
199
|
-
if credentials_path.exists():
|
|
221
|
+
if (credentials_path := home / ".credentials.json").exists():
|
|
200
222
|
shutil.copyfile(credentials_path, config_dir / ".credentials.json")
|
|
223
|
+
elif (token := keychain_credentials(home)) is not None:
|
|
224
|
+
(seeded := config_dir / ".credentials.json").write_text(token)
|
|
225
|
+
seeded.chmod(0o600)
|
|
201
226
|
atexit.register(shutil.rmtree, config_dir, ignore_errors=True)
|
|
202
227
|
self._isolated_config_dir = str(config_dir)
|
|
203
228
|
return self._isolated_config_dir
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|