cswap-pin 0.1.0__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.
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .venv/
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: cswap-pin
3
+ Version: 0.1.0
4
+ Summary: Keep Claude Code's Remote Control and Artifacts on one account while inference follows cswap's account swap
5
+ Project-URL: Homepage, https://github.com/codeslake/cswap-pin
6
+ Project-URL: Repository, https://github.com/codeslake/cswap-pin
7
+ Project-URL: Issues, https://github.com/codeslake/cswap-pin/issues
8
+ Project-URL: Upstream discussion, https://github.com/realiti4/claude-swap/issues/198
9
+ Author: Junyong Lee
10
+ License: MIT
11
+ Keywords: claude,claude-code,claude-swap,cswap,proxy,remote-control
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: cryptography>=41.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # cswap-pin
25
+
26
+ Keep Claude Code's **Remote Control** and **Artifacts** on one account while
27
+ inference keeps following [`cswap`](https://github.com/realiti4/claude-swap)'s
28
+ account swap.
29
+
30
+ ## The problem
31
+
32
+ cswap swaps the on-disk credential, so *everything* follows the swap —
33
+ including two things that are not inference and that you usually want to stay
34
+ put:
35
+
36
+ - **Remote Control** — a session's owner is fixed at creation by whichever
37
+ bearer created it. Swap accounts and the phone/web loses the session; stale
38
+ "ghost" sessions pile up on the old account.
39
+ - **Artifacts** — owned by the publishing bearer. After a swap a republish
40
+ 403s and the artifact "disappears" from the account you are logged into.
41
+
42
+ Claude Code resolves all of these through one credential accessor and has no
43
+ per-operation token selector, so splitting auth *per operation inside one
44
+ session* means intercepting the requests.
45
+
46
+ ## How it works
47
+
48
+ A local MITM forward proxy that swaps the `Authorization` bearer on exactly
49
+ the routes whose server-side ownership is decided by it, and passes everything
50
+ else — `/v1/messages` above all — through untouched.
51
+
52
+ ```
53
+ claude session
54
+ HTTPS_PROXY ─► cswap pin proxy ──► (whatever HTTPS_PROXY was already set) ──► api.anthropic.com
55
+ swaps bearer on: /v1/code/sessions*, /v1/sessions/*,
56
+ /api/frame/*, /v1/ultrareview/*
57
+ passes through: /v1/messages, /api/oauth/usage, everything else
58
+ ```
59
+
60
+ Inference keeps billing whichever account cswap has swapped onto. Only the
61
+ claude.ai-side assets are pinned.
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ uv tool install 'claude-swap[pin]' # or: pipx install 'claude-swap[pin]'
67
+ ```
68
+
69
+ The pin is an optional extra of claude-swap, not a standalone tool: it reads
70
+ cswap's account store and rewrites the config cswap already manages. Installing
71
+ `cswap-pin` on its own does nothing useful.
72
+
73
+ ## Use
74
+
75
+ ```bash
76
+ cswap pin 2 # RC / artifacts / ultrareview → account 2
77
+ cswap pin # show the current pin
78
+ cswap pin --clear # remove it
79
+ ```
80
+
81
+ The pinned account is re-read per request, so `cswap pin <other>` takes effect
82
+ under a live daemon — no session restart. The one thing a re-pin cannot move is
83
+ a Remote Control session that is **already open**: the server fixed its owner
84
+ when it was created, so reconnecting inside it is what mints a new one under
85
+ the new pin.
86
+
87
+ ## Requirements
88
+
89
+ - Python 3.10+
90
+ - [`claude-swap`](https://github.com/realiti4/claude-swap) — a peer, not a
91
+ dependency: this package is loaded *by* it (see `src/cswap_pin/_host.py` for
92
+ the exact surface it borrows)
93
+ - `cryptography` (installed automatically) for the MITM CA
94
+
95
+ ## Why a separate package
96
+
97
+ Upstream did not want a MITM proxy shipped inside claude-swap itself and asked
98
+ for a companion distribution exposed through an optional extra. See
99
+ [realiti4/claude-swap#198](https://github.com/realiti4/claude-swap/issues/198).
100
+
101
+ ## Trust
102
+
103
+ The proxy generates its own CA to re-sign `api.anthropic.com` and names it in
104
+ `NODE_EXTRA_CA_CERTS`. Node accepts exactly one file there, so an existing CA
105
+ (a corporate MITM, another local proxy) is **merged**, never replaced —
106
+ otherwise the session silently loses trust in every host the other proxy
107
+ re-signs.
108
+
109
+ The proxy also requires a per-daemon credential on `CONNECT`: it listens on
110
+ loopback, which carries no identity, and without one any local process could
111
+ have a junk bearer replaced with the pinned account's real token.
112
+
113
+ ## License
114
+
115
+ MIT
@@ -0,0 +1,92 @@
1
+ # cswap-pin
2
+
3
+ Keep Claude Code's **Remote Control** and **Artifacts** on one account while
4
+ inference keeps following [`cswap`](https://github.com/realiti4/claude-swap)'s
5
+ account swap.
6
+
7
+ ## The problem
8
+
9
+ cswap swaps the on-disk credential, so *everything* follows the swap —
10
+ including two things that are not inference and that you usually want to stay
11
+ put:
12
+
13
+ - **Remote Control** — a session's owner is fixed at creation by whichever
14
+ bearer created it. Swap accounts and the phone/web loses the session; stale
15
+ "ghost" sessions pile up on the old account.
16
+ - **Artifacts** — owned by the publishing bearer. After a swap a republish
17
+ 403s and the artifact "disappears" from the account you are logged into.
18
+
19
+ Claude Code resolves all of these through one credential accessor and has no
20
+ per-operation token selector, so splitting auth *per operation inside one
21
+ session* means intercepting the requests.
22
+
23
+ ## How it works
24
+
25
+ A local MITM forward proxy that swaps the `Authorization` bearer on exactly
26
+ the routes whose server-side ownership is decided by it, and passes everything
27
+ else — `/v1/messages` above all — through untouched.
28
+
29
+ ```
30
+ claude session
31
+ HTTPS_PROXY ─► cswap pin proxy ──► (whatever HTTPS_PROXY was already set) ──► api.anthropic.com
32
+ swaps bearer on: /v1/code/sessions*, /v1/sessions/*,
33
+ /api/frame/*, /v1/ultrareview/*
34
+ passes through: /v1/messages, /api/oauth/usage, everything else
35
+ ```
36
+
37
+ Inference keeps billing whichever account cswap has swapped onto. Only the
38
+ claude.ai-side assets are pinned.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ uv tool install 'claude-swap[pin]' # or: pipx install 'claude-swap[pin]'
44
+ ```
45
+
46
+ The pin is an optional extra of claude-swap, not a standalone tool: it reads
47
+ cswap's account store and rewrites the config cswap already manages. Installing
48
+ `cswap-pin` on its own does nothing useful.
49
+
50
+ ## Use
51
+
52
+ ```bash
53
+ cswap pin 2 # RC / artifacts / ultrareview → account 2
54
+ cswap pin # show the current pin
55
+ cswap pin --clear # remove it
56
+ ```
57
+
58
+ The pinned account is re-read per request, so `cswap pin <other>` takes effect
59
+ under a live daemon — no session restart. The one thing a re-pin cannot move is
60
+ a Remote Control session that is **already open**: the server fixed its owner
61
+ when it was created, so reconnecting inside it is what mints a new one under
62
+ the new pin.
63
+
64
+ ## Requirements
65
+
66
+ - Python 3.10+
67
+ - [`claude-swap`](https://github.com/realiti4/claude-swap) — a peer, not a
68
+ dependency: this package is loaded *by* it (see `src/cswap_pin/_host.py` for
69
+ the exact surface it borrows)
70
+ - `cryptography` (installed automatically) for the MITM CA
71
+
72
+ ## Why a separate package
73
+
74
+ Upstream did not want a MITM proxy shipped inside claude-swap itself and asked
75
+ for a companion distribution exposed through an optional extra. See
76
+ [realiti4/claude-swap#198](https://github.com/realiti4/claude-swap/issues/198).
77
+
78
+ ## Trust
79
+
80
+ The proxy generates its own CA to re-sign `api.anthropic.com` and names it in
81
+ `NODE_EXTRA_CA_CERTS`. Node accepts exactly one file there, so an existing CA
82
+ (a corporate MITM, another local proxy) is **merged**, never replaced —
83
+ otherwise the session silently loses trust in every host the other proxy
84
+ re-signs.
85
+
86
+ The proxy also requires a per-daemon credential on `CONNECT`: it listens on
87
+ loopback, which carries no identity, and without one any local process could
88
+ have a junk bearer replaced with the pinned account's real token.
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "cswap-pin"
3
+ version = "0.1.0"
4
+ description = "Keep Claude Code's Remote Control and Artifacts on one account while inference follows cswap's account swap"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Junyong Lee" }]
9
+ keywords = ["claude", "claude-code", "claude-swap", "cswap", "proxy", "remote-control"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Operating System :: MacOS",
16
+ "Operating System :: POSIX :: Linux",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Utilities",
19
+ ]
20
+
21
+ # claude-swap is a PEER, not a dependency. cswap-pin is loaded BY claude-swap
22
+ # (via `claude-swap[pin]`), so depending on it here would make the pair
23
+ # circular and let `pip install cswap-pin` drag in the whole switcher for
24
+ # someone who already has it. The import surface it needs is small and
25
+ # declared in _host.py.
26
+ dependencies = ["cryptography>=41.0"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/codeslake/cswap-pin"
30
+ Repository = "https://github.com/codeslake/cswap-pin"
31
+ Issues = "https://github.com/codeslake/cswap-pin/issues"
32
+ "Upstream discussion" = "https://github.com/realiti4/claude-swap/issues/198"
33
+
34
+ [build-system]
35
+ requires = ["hatchling"]
36
+ build-backend = "hatchling.build"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/cswap_pin"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
@@ -0,0 +1,35 @@
1
+ """Keep Claude Code's Remote Control and Artifacts on one account.
2
+
3
+ cswap swaps the on-disk credential, so everything follows the swap — including
4
+ two things that are not inference and usually should not move: Remote Control
5
+ (a session's owner is fixed at creation by the bearer that created it) and
6
+ Artifacts (owned by the publishing bearer). This package keeps those on one
7
+ account while inference keeps following ``cswap switch`` / ``cswap auto``.
8
+
9
+ Companion to claude-swap. Installed as ``claude-swap[pin]``; see
10
+ https://github.com/realiti4/claude-swap/issues/198 for why it lives apart.
11
+
12
+ **Nothing is imported from :mod:`cswap_pin.proxy` here.** That module reaches
13
+ into claude-swap at import time, so re-exporting its symbols made
14
+ ``import cswap_pin`` itself raise ``HostMissing`` when the host was absent —
15
+ from proxy.py line 43, naming neither package, and before any caller could
16
+ catch it. Importing a package must never require the thing the package is
17
+ meant to report on. Reach for ``cswap_pin.proxy`` directly; use
18
+ :func:`host_available` first if you want to check rather than catch.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ __version__ = "0.1.0"
24
+
25
+ __all__ = ["__version__", "host_available"]
26
+
27
+
28
+ def host_available() -> bool:
29
+ """Whether claude-swap is importable, i.e. whether the pin can do anything.
30
+
31
+ Safe to call with the host missing — that is the case it exists for.
32
+ """
33
+ from cswap_pin._host import available
34
+
35
+ return available()
@@ -0,0 +1,68 @@
1
+ """Everything cswap-pin borrows from claude-swap, in one place.
2
+
3
+ The pin runs INSIDE claude-swap: it is loaded by `claude-swap[pin]`, reads the
4
+ account store cswap owns, and rewrites the config file cswap already rewrites.
5
+ So it needs part of cswap's internals — but "needs part of" is exactly the
6
+ coupling that made splitting the two hard in the first place, and an import
7
+ scattered across 2800 lines is a dependency nobody can see the shape of.
8
+
9
+ This module IS that shape. Seven symbols, six modules, listed once:
10
+
11
+ paths.get_global_config_path where the env block lives
12
+ paths.get_claude_config_home the config dir root
13
+ settings read/write the pin record
14
+ exceptions.AccountNotFoundError pin resolution failures
15
+ exceptions.ConfigError ...
16
+ switcher.ClaudeAccountSwitcher the account store
17
+ oauth token extraction and refresh
18
+ claude_locks.claude_config_lock serialize config rewrites
19
+
20
+ Two rules follow from putting it here:
21
+
22
+ 1. **Nothing else in cswap_pin imports claude_swap directly.** A new import
23
+ elsewhere grows the contract silently; a new name in this file is a
24
+ deliberate change to it, visible in one diff.
25
+ 2. **claude-swap is a PEER, not a dependency.** It is not in our
26
+ ``dependencies``: cswap-pin is loaded by claude-swap, so declaring it here
27
+ would make the pair circular and pull the whole switcher into anyone who
28
+ installs cswap-pin alone. The import therefore has to be able to fail, and
29
+ ``require()`` is where that failure gets a readable message instead of a
30
+ traceback from line 2000 of the proxy.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ from types import ModuleType
36
+
37
+ _HINT = (
38
+ "cswap-pin runs inside claude-swap and cannot find it. "
39
+ "Install the pair together: pip install 'claude-swap[pin]'"
40
+ )
41
+
42
+
43
+ class HostMissing(RuntimeError):
44
+ """claude-swap is not importable, so the pin has nothing to pin."""
45
+
46
+
47
+ def require(name: str) -> ModuleType:
48
+ """Import a claude_swap submodule, or fail with something readable.
49
+
50
+ ``name`` is the part after ``claude_swap.`` — ``require("paths")`` gets
51
+ ``claude_swap.paths``.
52
+ """
53
+ import importlib
54
+
55
+ try:
56
+ return importlib.import_module(f"claude_swap.{name}")
57
+ except ImportError as exc: # noqa: TRY003 — the message IS the point
58
+ raise HostMissing(_HINT) from exc
59
+
60
+
61
+ def available() -> bool:
62
+ """Whether the host is importable at all. For callers that want to check
63
+ rather than catch."""
64
+ try:
65
+ require("paths")
66
+ except HostMissing:
67
+ return False
68
+ return True