spawnllm 0.13.3__tar.gz → 0.13.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 (34) hide show
  1. {spawnllm-0.13.3 → spawnllm-0.13.4}/PKG-INFO +1 -1
  2. {spawnllm-0.13.3 → spawnllm-0.13.4}/pyproject.toml +1 -1
  3. {spawnllm-0.13.3 → spawnllm-0.13.4}/pyproject.toml.orig +1 -1
  4. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/_core/spawnllm_core.wasm +0 -0
  5. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/base.py +27 -8
  6. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/claude.py +24 -19
  7. {spawnllm-0.13.3 → spawnllm-0.13.4}/LICENSE +0 -0
  8. {spawnllm-0.13.3 → spawnllm-0.13.4}/README.md +0 -0
  9. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/__init__.py +0 -0
  10. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/__main__.py +0 -0
  11. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/_core/__init__.py +0 -0
  12. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/__init__.py +0 -0
  13. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/apple.py +0 -0
  14. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/claude_sdk.py +0 -0
  15. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/codex.py +0 -0
  16. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/gemini.py +0 -0
  17. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/mlx.py +0 -0
  18. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/openai_endpoint.py +0 -0
  19. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/registry.py +0 -0
  20. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/call.py +0 -0
  21. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/cli.py +0 -0
  22. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/extract.py +0 -0
  23. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/mlx/__init__.py +0 -0
  24. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/mlx/codec.py +0 -0
  25. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/mlx/engine.py +0 -0
  26. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/mlx/fuse.py +0 -0
  27. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/mlx/patches.py +0 -0
  28. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/proc.py +0 -0
  29. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/py.typed +0 -0
  30. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/response.py +0 -0
  31. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/run.py +0 -0
  32. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/spec.py +0 -0
  33. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/structured.py +0 -0
  34. {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawnllm
3
- Version: 0.13.3
3
+ Version: 0.13.4
4
4
  Summary: Delete your subprocess wrappers around claude, codex, and gemini.
5
5
  Keywords:
6
6
  Author: Yasyf Mohamedali
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spawnllm"
3
- version = "0.13.3"
3
+ version = "0.13.4"
4
4
  description = "Delete your subprocess wrappers around claude, codex, and gemini."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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.13.3"
4
+ version = "0.13.4"
5
5
  description = "Delete your subprocess wrappers around claude, codex, and gemini."
6
6
  readme = "README.md"
7
7
  license = "MIT"
@@ -83,6 +83,20 @@ class BackendCallError(RuntimeError):
83
83
  """
84
84
 
85
85
 
86
+ @dataclass(frozen=True)
87
+ class ClaudeIsolation:
88
+ """A seeded throwaway config home and the env a claude run adds beside it.
89
+
90
+ Attributes:
91
+ config_dir: Path substituted for `${isolated_config_dir}` in the plan.
92
+ env: Variables the run sets on the child, such as `CLAUDE_CODE_OAUTH_TOKEN`
93
+ carrying the claude.ai access token; never written to disk.
94
+ """
95
+
96
+ config_dir: str
97
+ env: dict[str, str]
98
+
99
+
86
100
  @dataclass(frozen=True)
87
101
  class Invocation:
88
102
  """A built CLI invocation: argv, optional stdin, and where to read the result.
@@ -382,8 +396,8 @@ class CliBackend(LlmBackend):
382
396
  """
383
397
  return self.binary
384
398
 
385
- def claude_isolation(self) -> str:
386
- """Return the isolated config home a claude run substitutes into `${isolated_config_dir}`."""
399
+ def claude_isolation(self) -> ClaudeIsolation:
400
+ """Return the config home a claude run substitutes into `${isolated_config_dir}` and the env it adds."""
387
401
  raise NotImplementedError
388
402
 
389
403
  def build_command(self, spec: RunSpec) -> list[str]:
@@ -422,9 +436,11 @@ class CliBackend(LlmBackend):
422
436
  argv = [tokens.get(arg, arg) for arg in plan["argv"]]
423
437
  env = plan["env"]
424
438
  if plan["needs_claude_isolation"]:
425
- directory = self.claude_isolation()
426
- argv = [arg.replace("${isolated_config_dir}", directory) for arg in argv]
427
- env = {key: value.replace("${isolated_config_dir}", directory) for key, value in env.items()}
439
+ isolation = self.claude_isolation()
440
+ argv = [arg.replace("${isolated_config_dir}", isolation.config_dir) for arg in argv]
441
+ env = {
442
+ key: value.replace("${isolated_config_dir}", isolation.config_dir) for key, value in env.items()
443
+ } | isolation.env
428
444
  except BaseException:
429
445
  for path in paths.values():
430
446
  Path(path).unlink(missing_ok=True)
@@ -446,13 +462,16 @@ class CliBackend(LlmBackend):
446
462
  spec: The configured run; the plan gates isolation on `spec.isolated`.
447
463
 
448
464
  Returns:
449
- The plan's env map with `${isolated_config_dir}` resolved, or `{}`.
465
+ The plan's env map with `${isolated_config_dir}` resolved plus the
466
+ isolation's own entries, or `{}`.
450
467
  """
451
468
  plan = self.core_plan(spec)
452
469
  if not plan["needs_claude_isolation"]:
453
470
  return plan["env"]
454
- directory = self.claude_isolation()
455
- return {key: value.replace("${isolated_config_dir}", directory) for key, value in plan["env"].items()}
471
+ isolation = self.claude_isolation()
472
+ return {
473
+ key: value.replace("${isolated_config_dir}", isolation.config_dir) for key, value in plan["env"].items()
474
+ } | isolation.env
456
475
 
457
476
  def accounting(self, raw: str) -> tuple[float | None, dict[str, object] | None]:
458
477
  """Return the `(cost_usd, usage)` the core's `resolve` op reads from `raw`."""
@@ -12,7 +12,7 @@ from pathlib import Path
12
12
  from typing import TYPE_CHECKING, ClassVar
13
13
 
14
14
  from spawnllm import _core
15
- from spawnllm.backends.base import CliBackend
15
+ from spawnllm.backends.base import ClaudeIsolation, CliBackend
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from spawnllm.types import ProviderName, TModel
@@ -43,7 +43,8 @@ class ClaudeCliBackend(CliBackend):
43
43
 
44
44
  The core plans the `claude -p` argv (prompt delivered over stdin, result read
45
45
  from a stdout file) and lays out the host-free config home this backend seeds
46
- with only the active-account pointer and claude.ai OAuth token.
46
+ with only the active-account pointer; the claude.ai access token reaches the
47
+ child as `CLAUDE_CODE_OAUTH_TOKEN`, never as a file.
47
48
 
48
49
  Attributes:
49
50
  models: Mapping from abstract model size to a Claude model alias
@@ -63,18 +64,20 @@ class ClaudeCliBackend(CliBackend):
63
64
 
64
65
  _isolated_config_dir: str | None = None
65
66
 
66
- def claude_isolation(self) -> str:
67
- """Return the process-lifetime isolated config home, creating and seeding it once.
67
+ def claude_isolation(self) -> ClaudeIsolation:
68
+ """Return the isolation for one run: the process-lifetime config home and the env resolved now.
68
69
 
69
70
  The core's `claude_isolation_sources` op resolves the account pointer,
70
71
  credentials file, and Keychain service from the caller's effective config
71
- home; this host reads those sources (falling back to the Keychain when the
72
- credentials file is absent), hands them to `claude_isolation_seed` for the
73
- exact files-and-modes to write, and materializes them into a fresh temp
74
- dir removed at interpreter exit. The dir is cached on the backend.
72
+ home, naming no credential source when the process already carries
73
+ `CLAUDE_CODE_OAUTH_TOKEN`; this host reads those sources (falling back to
74
+ the Keychain when the credentials file is absent) and hands them to
75
+ `claude_isolation_seed` for the exact files-and-modes to write and the env
76
+ to set. The files land in a fresh temp dir removed at interpreter exit,
77
+ created on the first call and cached on the backend; the env is resolved
78
+ on every call so a renewed Keychain token reaches the next run, and it
79
+ only ever lives in memory.
75
80
  """
76
- if self._isolated_config_dir is not None:
77
- return self._isolated_config_dir
78
81
  sources = _core.dispatch(
79
82
  "claude_isolation_sources",
80
83
  {
@@ -84,21 +87,23 @@ class ClaudeCliBackend(CliBackend):
84
87
  "claude_config_dir_env": os.environ.get("CLAUDE_CONFIG_DIR") or None,
85
88
  "claude_securestorage_config_dir_env": os.environ.get("CLAUDE_SECURESTORAGE_CONFIG_DIR"),
86
89
  "claude_code_custom_oauth_url_env": os.environ.get("CLAUDE_CODE_CUSTOM_OAUTH_URL"),
90
+ "claude_code_oauth_token_env": os.environ.get("CLAUDE_CODE_OAUTH_TOKEN"),
87
91
  }
88
92
  },
89
93
  )
90
94
  account_json = read_file_opt(sources["account_path"])
91
- credentials_json = read_file_opt(sources["credentials_path"])
95
+ credentials_json = read_file_opt(sources["credentials_path"]) if sources["credentials_path"] else None
92
96
  if credentials_json is None and sources["keychain_service"] is not None:
93
97
  credentials_json = keychain_credentials(sources["keychain_service"])
94
98
  seed = _core.dispatch(
95
99
  "claude_isolation_seed", {"account_json": account_json, "credentials_json": credentials_json}
96
100
  )
97
- config_dir = Path(tempfile.mkdtemp(prefix="spawnllm-claude-config-"))
98
- for file in seed["files"]:
99
- fd = os.open(config_dir / file["name"], os.O_WRONLY | os.O_CREAT | os.O_EXCL, int(file["mode"], 8))
100
- with os.fdopen(fd, "w") as handle:
101
- handle.write(file["content"])
102
- atexit.register(shutil.rmtree, config_dir, ignore_errors=True)
103
- self._isolated_config_dir = str(config_dir)
104
- return self._isolated_config_dir
101
+ if self._isolated_config_dir is None:
102
+ config_dir = Path(tempfile.mkdtemp(prefix="spawnllm-claude-config-"))
103
+ for file in seed["files"]:
104
+ fd = os.open(config_dir / file["name"], os.O_WRONLY | os.O_CREAT | os.O_EXCL, int(file["mode"], 8))
105
+ with os.fdopen(fd, "w") as handle:
106
+ handle.write(file["content"])
107
+ atexit.register(shutil.rmtree, config_dir, ignore_errors=True)
108
+ self._isolated_config_dir = str(config_dir)
109
+ return ClaudeIsolation(self._isolated_config_dir, seed["env"])
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