spawnllm 0.5.3__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.
Files changed (28) hide show
  1. {spawnllm-0.5.3 → spawnllm-0.5.4}/PKG-INFO +1 -1
  2. {spawnllm-0.5.3 → spawnllm-0.5.4}/pyproject.toml +1 -1
  3. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/backends/claude.py +36 -11
  4. {spawnllm-0.5.3 → spawnllm-0.5.4}/LICENSE +0 -0
  5. {spawnllm-0.5.3 → spawnllm-0.5.4}/README.md +0 -0
  6. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/__init__.py +0 -0
  7. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/__main__.py +0 -0
  8. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/backends/__init__.py +0 -0
  9. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/backends/base.py +0 -0
  10. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/backends/codex.py +0 -0
  11. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/backends/gemini.py +0 -0
  12. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/backends/mlx.py +0 -0
  13. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/backends/registry.py +0 -0
  14. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/call.py +0 -0
  15. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/cli.py +0 -0
  16. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/extract.py +0 -0
  17. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/mlx/__init__.py +0 -0
  18. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/mlx/codec.py +0 -0
  19. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/mlx/engine.py +0 -0
  20. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/mlx/fuse.py +0 -0
  21. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/mlx/patches.py +0 -0
  22. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/proc.py +0 -0
  23. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/py.typed +0 -0
  24. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/response.py +0 -0
  25. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/run.py +0 -0
  26. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/spec.py +0 -0
  27. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/structured.py +0 -0
  28. {spawnllm-0.5.3 → spawnllm-0.5.4}/spawnllm/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawnllm
3
- Version: 0.5.3
3
+ Version: 0.5.4
4
4
  Summary: Subshell + MLX LLM-calling backends (Claude/Codex CLI, local MLX) shared across tools.
5
5
  Keywords:
6
6
  Author: Yasyf Mohamedali
@@ -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.3"
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
- `~/.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.
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 = home / ".claude" / ".credentials.json"
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