multi-claude-cli 0.14.0__tar.gz → 0.16.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.
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/PKG-INFO +70 -13
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/README.md +68 -12
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/pyproject.toml +1 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/__init__.py +1 -1
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/__main__.py +7 -0
- multi_claude_cli-0.16.0/src/multi_claude/_entry.py +43 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/accounts.py +21 -1
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/artifacts.py +4 -5
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/bundle.py +11 -7
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/cli.py +129 -20
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/completion.py +97 -2
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/config.py +19 -1
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/doctor.py +18 -14
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/env.py +24 -1
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/fsutil.py +31 -6
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/history.py +5 -1
- multi_claude_cli-0.16.0/src/multi_claude/hook.py +185 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/identity.py +27 -1
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/launcher.py +127 -5
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/lock.py +5 -6
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/migrate.py +62 -8
- multi_claude_cli-0.16.0/src/multi_claude/netcheck.py +642 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/plan.py +6 -2
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/platform.py +469 -15
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/restore.py +24 -3
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/routes.py +33 -5
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/sessions.py +5 -5
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/shared.py +27 -2
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/shellpath.py +20 -2
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/statusline.py +34 -5
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude_cli.egg-info/PKG-INFO +70 -13
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude_cli.egg-info/SOURCES.txt +5 -1
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_help_examples.py +4 -1
- multi_claude_cli-0.16.0/tests/test_netcheck.py +486 -0
- multi_claude_cli-0.16.0/tests/test_windows.py +502 -0
- multi_claude_cli-0.14.0/src/multi_claude/hook.py +0 -90
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/LICENSE +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/setup.cfg +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/actions.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/refresh.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/usage.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude_cli.egg-info/dependency_links.txt +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude_cli.egg-info/entry_points.txt +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude_cli.egg-info/top_level.txt +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_accounts.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_artifacts.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_bundle.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_completion.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_config.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_doctor.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_everyday.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_fsutil.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_handoff.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_help.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_history.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_identity.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_install.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_mcp.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_migrate.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_onboarding.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_packaging.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_plan.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_platform.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_refresh.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_rename.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_renewal.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_routes.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_sessions.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_set.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_shared_exclude.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_statusline.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_usability.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_usage.py +0 -0
- {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_vscode.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multi-claude-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: Run several Claude Code accounts side by side: separate CLAUDE_CONFIG_DIR directories, launchers, proxies, environment variables and arguments.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/jakoes-wu/multi-claude
|
|
@@ -11,6 +11,7 @@ Classifier: Environment :: Console
|
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
13
|
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
15
|
Classifier: Operating System :: POSIX :: Linux
|
|
15
16
|
Classifier: Programming Language :: Python :: 3
|
|
16
17
|
Classifier: Programming Language :: Python :: 3.8
|
|
@@ -27,17 +28,18 @@ Dynamic: license-file
|
|
|
27
28
|
|
|
28
29
|
# multi-claude
|
|
29
30
|
|
|
30
|
-
**English** | [简体中文](README.zh-CN.md)
|
|
31
|
+
**English** | [简体中文](https://github.com/jakoes-wu/multi-claude/blob/main/README.zh-CN.md)
|
|
31
32
|
|
|
32
33
|
[](https://github.com/jakoes-wu/multi-claude/releases)
|
|
33
34
|
[](https://github.com/jakoes-wu/multi-claude/actions/workflows/ci.yml)
|
|
34
35
|
[](https://pypi.org/project/multi-claude-cli/)
|
|
35
36
|

|
|
36
|
-
[](LICENSE)
|
|
37
|
+
[](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
|
|
38
|
+

|
|
37
39
|
|
|
38
40
|
Use several [Claude Code](https://code.claude.com/docs) accounts on one machine, at the same time. Each account keeps its own login, settings and history and, if you like, its own proxy. No more logging out and in again.
|
|
39
41
|
|
|
40
|
-
It never copies or exports your login and never edits your shell startup files. Only `list` reads an account's access token and goes online, to ask Anthropic for the current plan (renewing the token first if it has expired); `list --offline` skips all of that.
|
|
42
|
+
It never copies or exports your login and never edits your shell startup files. Only `list` reads an account's access token and goes online, to ask Anthropic for the current plan (renewing the token first if it has expired); `list --offline` skips all of that. `doctor --network` also goes online, but only when you ask for it and without any login.
|
|
41
43
|
|
|
42
44
|
```sh
|
|
43
45
|
claude-work # Claude Code with your work account
|
|
@@ -65,7 +67,7 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
|
|
|
65
67
|
## Features
|
|
66
68
|
|
|
67
69
|
- **Your login stays yours** — logins are never copied or exported. One command touches them: `list` takes each account's access token (from the keychain, or `.credentials.json` on Linux) to ask Anthropic which plan the account is on, and when that token has expired it renews it the way Claude Code does and saves the new one in the same place. Tokens stay in memory and go only to Anthropic (`api.anthropic.com`, and `platform.claude.com` for a renewal), through that account's proxy. `list --no-refresh` never writes, `list --offline` reads nothing. Every other command only checks whether a login exists. `migrate-default` and `restore` move whole account directories, and on Linux the credentials file moves with its directory without being opened.
|
|
68
|
-
- **Only `list`
|
|
70
|
+
- **Only `list` and `doctor --network` go online** — the plan lookup of `list` (one request per account, plus one to renew an expired token) and the connection check of `doctor --network` (one `HEAD /` without credentials to `api.anthropic.com` and `platform.claude.com` per proxy, repeated once if it fails) are the only connections multi-claude makes: no telemetry, no usage reporting, no update checks. `list --offline` makes none. Only the installer, pipx or Homebrew download the package.
|
|
69
71
|
- **Leaves your shell alone** — when `PATH` needs a change, it prints the line to add instead of editing `~/.zshrc` or `~/.bashrc`.
|
|
70
72
|
- **Changes only what you ask** — it writes its own configuration and state, the launchers, account directories (including the VS Code data that `code` keeps in `<root>/.apps`) and shared links. A Claude Code settings file is changed only by `statusline install` (the file you name, with a backup) and `add --config-from` (the new account's `settings.json`).
|
|
71
73
|
- **Migrate the default account** — move your existing `~/.claude` into the account root without logging out. `claude` keeps working exactly as before.
|
|
@@ -78,13 +80,13 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
|
|
|
78
80
|
|
|
79
81
|
## Prerequisites
|
|
80
82
|
|
|
81
|
-
**Servers.** None. multi-claude runs only on your own computer; its only network requests are the plan lookups and token renewals of `list`, sent to Anthropic.
|
|
83
|
+
**Servers.** None. multi-claude runs only on your own computer; its only network requests are the plan lookups and token renewals of `list` and the connection check of `doctor --network`, all sent to Anthropic.
|
|
82
84
|
|
|
83
85
|
**Your computer.**
|
|
84
86
|
|
|
85
87
|
| | Needed | Optional |
|
|
86
88
|
| ---- | ---- | ---- |
|
|
87
|
-
| Operating system | macOS
|
|
89
|
+
| Operating system | macOS, Linux (WSL counts as Linux) or Windows | — |
|
|
88
90
|
| Tools | Python 3.8+ (`python3`, standard library only); Claude Code (`claude`); `curl` or `wget`, and `tar`, for the installer | `pipx` or Homebrew, to install from PyPI or the tap; `lsof`, so that `migrate-default` and `restore` can find processes using `~/.claude` (Linux falls back to `/proc`); VS Code's `code` command, for `multi-claude code` |
|
|
89
91
|
|
|
90
92
|
Install what is missing:
|
|
@@ -100,7 +102,16 @@ sudo apt install -y python3 curl tar lsof
|
|
|
100
102
|
curl -fsSL https://claude.ai/install.sh | bash # Claude Code, if you do not have it yet
|
|
101
103
|
```
|
|
102
104
|
|
|
103
|
-
Windows
|
|
105
|
+
On Windows, install Python 3.8+ from https://www.python.org/downloads/windows/ or the Microsoft Store, then in
|
|
106
|
+
PowerShell:
|
|
107
|
+
|
|
108
|
+
```powershell
|
|
109
|
+
irm https://claude.ai/install.ps1 | iex # Claude Code, if you do not have it yet
|
|
110
|
+
python -m pip install --user pipx
|
|
111
|
+
python -m pipx ensurepath # then open a new terminal
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Other ways to install Claude Code: https://code.claude.com/docs/en/setup
|
|
104
115
|
|
|
105
116
|
## Installation
|
|
106
117
|
|
|
@@ -130,6 +141,49 @@ With Homebrew: `brew install jakoes-wu/tap/multi-claude` (upgrade with `brew upg
|
|
|
130
141
|
|
|
131
142
|
Run `./install.sh --help` for all options.
|
|
132
143
|
|
|
144
|
+
### Windows
|
|
145
|
+
|
|
146
|
+
Windows support is new. Install it with pipx; the installer script is for macOS and Linux only. If `pipx` is not
|
|
147
|
+
installed yet, run these in cmd or PowerShell (with Python 3.8 or later; use `python` instead of `py` if you do not
|
|
148
|
+
have the Python launcher):
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
py -m pip install --user pipx
|
|
152
|
+
py -m pipx ensurepath
|
|
153
|
+
py -m pipx install multi-claude-cli
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Then open a new terminal so the `PATH` change takes effect, and check with `multi-claude --version`. With pipx
|
|
157
|
+
already on `PATH`, `pipx install multi-claude-cli` is enough. Everything above works the same way, with these
|
|
158
|
+
differences:
|
|
159
|
+
|
|
160
|
+
- Launchers are `claude-<name>.cmd` and `claude-auto.cmd` in `%USERPROFILE%\.local\bin` (the directory
|
|
161
|
+
`pipx ensurepath` adds to `PATH`), so they run from PowerShell and cmd alike. They read the proxy, variables
|
|
162
|
+
and arguments from the configuration each time they start, so a change takes effect without `apply`.
|
|
163
|
+
- Account directories are in `%USERPROFILE%\.cc` and the configuration in `%APPDATA%\multi-claude`.
|
|
164
|
+
- Shared directories are linked with directory junctions, which need no special rights. Sharing a single file
|
|
165
|
+
(such as `CLAUDE.md`) needs a file symbolic link, which needs Developer Mode (Settings > System > For
|
|
166
|
+
developers) or an administrator terminal; without it, `add` and `apply` stop and say so. Exclude the file
|
|
167
|
+
with `--shared-exclude` to share the rest.
|
|
168
|
+
- `migrate-default` refuses while any Claude process is running: Windows does not let it see which account
|
|
169
|
+
another process uses. Close every Claude window, terminal and IDE first.
|
|
170
|
+
- `statusline` needs Git for Windows, whose Git Bash is what Claude Code uses to run the status line. A
|
|
171
|
+
`statusLine` setting with `"shell": "powershell"` cannot be wrapped.
|
|
172
|
+
- Paths containing `"`, `%` or `!` cannot be written into a `.cmd` launcher; `apply` reports such an account as
|
|
173
|
+
a conflict.
|
|
174
|
+
- cmd reads a `.cmd` file in the console code page, so a launcher never contains non-ASCII characters. When the
|
|
175
|
+
Python or package path has them (for example a Chinese user name), the launcher writes it as
|
|
176
|
+
`%USERPROFILE%\...` (or `%LOCALAPPDATA%`, `%APPDATA%`), or else as its 8.3 short name. A non-ASCII path
|
|
177
|
+
that cannot be written with a variable and has no short name is reported as a conflict.
|
|
178
|
+
- When Claude exits (Ctrl+C or `/exit`), cmd may flash "Terminate batch job (Y/N)?" for a moment: the launcher is
|
|
179
|
+
a batch file. It closes by itself; Claude has already exited and nothing is lost.
|
|
180
|
+
- `multi-claude completion powershell | Add-Content $PROFILE` adds tab completion to PowerShell. Do not use `>>`:
|
|
181
|
+
Windows PowerShell 5.1 writes it as UTF-16 and breaks an existing profile. If the profile's directory does not
|
|
182
|
+
exist yet, create it first with `New-Item -ItemType Directory -Force (Split-Path $PROFILE)`.
|
|
183
|
+
|
|
184
|
+
Continuous integration runs the Windows-specific tests on Windows (`tests/test_windows.py`); the rest of the test
|
|
185
|
+
suite runs on macOS and Linux only.
|
|
186
|
+
|
|
133
187
|
## Quick start
|
|
134
188
|
|
|
135
189
|
### 1. Create one account per login
|
|
@@ -452,10 +506,13 @@ Notes:
|
|
|
452
506
|
|
|
453
507
|
`multi-claude doctor` checks, without changing anything: the configuration and any unfinished migration, whether `claude` and the launcher directory are on `PATH`, whether each launcher is up to date and not hidden by another file of the same name, whether each account directory exists and is private (`0700`), the default account's link, broken shared links, whether a login exists, and variables such as `ANTHROPIC_API_KEY` that override the accounts' logins. Every problem comes with the command that fixes it. It exits with 1 when there is an error; `--json` prints the results for scripts.
|
|
454
508
|
|
|
509
|
+
`multi-claude doctor --network` also checks that each account can reach `api.anthropic.com` and `platform.claude.com` through the proxy it starts with (including the `env` of its `settings.json`). It connects step by step and says where it stopped: the proxy is not running, the proxy cannot get through, a name does not resolve, the secure connection was cut, the certificate could not be verified, a timeout, or `NO_PROXY` sends the account around its proxy. SOCKS and malformed proxy URLs, which Claude Code cannot use, are reported without connecting; https proxies are not checked. It sends one `HEAD /` per address and proxy (a second one if the first fails), with no login or token; addresses covered by `NO_PROXY` are checked directly.
|
|
510
|
+
|
|
455
511
|
```sh
|
|
456
512
|
multi-claude completion bash > ~/.local/share/bash-completion/completions/multi-claude
|
|
457
513
|
multi-claude completion zsh > "${fpath[1]}/_multi-claude" # or any directory on your fpath
|
|
458
514
|
multi-claude completion fish > ~/.config/fish/completions/multi-claude.fish
|
|
515
|
+
multi-claude completion powershell | Add-Content $PROFILE # Windows PowerShell
|
|
459
516
|
```
|
|
460
517
|
|
|
461
518
|
## Migrating `~/.claude`
|
|
@@ -554,7 +611,7 @@ Every command's `--help` ends with examples you can copy, covering its options,
|
|
|
554
611
|
| `multi-claude list [--verbose \| --json \| --names] [--no-refresh] [--offline]` | Show accounts, launchers, login state and the plan and billing channel looked up from Anthropic. `--json` adds usage and is meant for scripts; `--names` prints only the names; `--no-refresh` does not renew expired access tokens (never writes to a login); `--offline` skips the plan lookup (no token read, no network). A `RENEWS` column (and a `renews` key in `--json`) shows the renewal dates you recorded. |
|
|
555
612
|
| `multi-claude usage [NAME] [--json]` | Show the last known 5-hour and 7-day usage of each account. |
|
|
556
613
|
| `multi-claude usage --history [NAME] [--days N] [--by day\|model] [--json]` | Token usage per day or model, counted from the session files on this machine (default: 7 days). |
|
|
557
|
-
| `multi-claude doctor [--json] [--verbose]` | Check the configuration, launchers, account directories, shared links, logins and environment. |
|
|
614
|
+
| `multi-claude doctor [--json] [--verbose] [--network]` | Check the configuration, launchers, account directories, shared links, logins and environment; `--network` also checks each account's connection to Anthropic. |
|
|
558
615
|
| `multi-claude route DIR NAME`, `route DIR --remove`, `route --default NAME`, `route --no-default` | Choose an account by directory for `claude-auto`. |
|
|
559
616
|
| `multi-claude which [DIR]` | Show which account `claude-auto` would use in `DIR` (default: the current directory). |
|
|
560
617
|
| `multi-claude rename OLD NEW` | Rename an account and its launcher. The directory and the login stay. |
|
|
@@ -577,13 +634,13 @@ Every write command accepts `--dry-run`, and prints only what changes; add `--ve
|
|
|
577
634
|
| ---- | ---- |
|
|
578
635
|
| Account directories | `~/.cc/<name>` |
|
|
579
636
|
| Launchers | `~/.local/bin/claude-<name>` |
|
|
580
|
-
| Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`) |
|
|
637
|
+
| Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`); on Windows `%APPDATA%\multi-claude\config.json` |
|
|
581
638
|
|
|
582
639
|
Account names may contain letters, digits and `._@+-`, must start with a letter or digit, and are case-insensitive (`Work` and `work` are the same account). An email address works as a name. Entries starting with `.` in the root directory are never treated as accounts.
|
|
583
640
|
|
|
584
641
|
### Declarative setup with `apply`
|
|
585
642
|
|
|
586
|
-
See [`examples/config.example.json`](examples/config.example.json):
|
|
643
|
+
See [`examples/config.example.json`](https://github.com/jakoes-wu/multi-claude/blob/main/examples/config.example.json):
|
|
587
644
|
|
|
588
645
|
```json
|
|
589
646
|
{
|
|
@@ -644,7 +701,7 @@ This removes the tool only. Your configuration, account directories (including t
|
|
|
644
701
|
|
|
645
702
|
## Contributing
|
|
646
703
|
|
|
647
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md). Run the tests with:
|
|
704
|
+
See [CONTRIBUTING.md](https://github.com/jakoes-wu/multi-claude/blob/main/CONTRIBUTING.md). Run the tests with:
|
|
648
705
|
|
|
649
706
|
```sh
|
|
650
707
|
python3 -m unittest discover -s tests -t tests
|
|
@@ -652,4 +709,4 @@ python3 -m unittest discover -s tests -t tests
|
|
|
652
709
|
|
|
653
710
|
## License
|
|
654
711
|
|
|
655
|
-
[MIT](LICENSE)
|
|
712
|
+
[MIT](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# multi-claude
|
|
2
2
|
|
|
3
|
-
**English** | [简体中文](README.zh-CN.md)
|
|
3
|
+
**English** | [简体中文](https://github.com/jakoes-wu/multi-claude/blob/main/README.zh-CN.md)
|
|
4
4
|
|
|
5
5
|
[](https://github.com/jakoes-wu/multi-claude/releases)
|
|
6
6
|
[](https://github.com/jakoes-wu/multi-claude/actions/workflows/ci.yml)
|
|
7
7
|
[](https://pypi.org/project/multi-claude-cli/)
|
|
8
8
|

|
|
9
|
-
[](LICENSE)
|
|
9
|
+
[](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
|
|
10
|
+

|
|
10
11
|
|
|
11
12
|
Use several [Claude Code](https://code.claude.com/docs) accounts on one machine, at the same time. Each account keeps its own login, settings and history and, if you like, its own proxy. No more logging out and in again.
|
|
12
13
|
|
|
13
|
-
It never copies or exports your login and never edits your shell startup files. Only `list` reads an account's access token and goes online, to ask Anthropic for the current plan (renewing the token first if it has expired); `list --offline` skips all of that.
|
|
14
|
+
It never copies or exports your login and never edits your shell startup files. Only `list` reads an account's access token and goes online, to ask Anthropic for the current plan (renewing the token first if it has expired); `list --offline` skips all of that. `doctor --network` also goes online, but only when you ask for it and without any login.
|
|
14
15
|
|
|
15
16
|
```sh
|
|
16
17
|
claude-work # Claude Code with your work account
|
|
@@ -38,7 +39,7 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
|
|
|
38
39
|
## Features
|
|
39
40
|
|
|
40
41
|
- **Your login stays yours** — logins are never copied or exported. One command touches them: `list` takes each account's access token (from the keychain, or `.credentials.json` on Linux) to ask Anthropic which plan the account is on, and when that token has expired it renews it the way Claude Code does and saves the new one in the same place. Tokens stay in memory and go only to Anthropic (`api.anthropic.com`, and `platform.claude.com` for a renewal), through that account's proxy. `list --no-refresh` never writes, `list --offline` reads nothing. Every other command only checks whether a login exists. `migrate-default` and `restore` move whole account directories, and on Linux the credentials file moves with its directory without being opened.
|
|
41
|
-
- **Only `list`
|
|
42
|
+
- **Only `list` and `doctor --network` go online** — the plan lookup of `list` (one request per account, plus one to renew an expired token) and the connection check of `doctor --network` (one `HEAD /` without credentials to `api.anthropic.com` and `platform.claude.com` per proxy, repeated once if it fails) are the only connections multi-claude makes: no telemetry, no usage reporting, no update checks. `list --offline` makes none. Only the installer, pipx or Homebrew download the package.
|
|
42
43
|
- **Leaves your shell alone** — when `PATH` needs a change, it prints the line to add instead of editing `~/.zshrc` or `~/.bashrc`.
|
|
43
44
|
- **Changes only what you ask** — it writes its own configuration and state, the launchers, account directories (including the VS Code data that `code` keeps in `<root>/.apps`) and shared links. A Claude Code settings file is changed only by `statusline install` (the file you name, with a backup) and `add --config-from` (the new account's `settings.json`).
|
|
44
45
|
- **Migrate the default account** — move your existing `~/.claude` into the account root without logging out. `claude` keeps working exactly as before.
|
|
@@ -51,13 +52,13 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
|
|
|
51
52
|
|
|
52
53
|
## Prerequisites
|
|
53
54
|
|
|
54
|
-
**Servers.** None. multi-claude runs only on your own computer; its only network requests are the plan lookups and token renewals of `list`, sent to Anthropic.
|
|
55
|
+
**Servers.** None. multi-claude runs only on your own computer; its only network requests are the plan lookups and token renewals of `list` and the connection check of `doctor --network`, all sent to Anthropic.
|
|
55
56
|
|
|
56
57
|
**Your computer.**
|
|
57
58
|
|
|
58
59
|
| | Needed | Optional |
|
|
59
60
|
| ---- | ---- | ---- |
|
|
60
|
-
| Operating system | macOS
|
|
61
|
+
| Operating system | macOS, Linux (WSL counts as Linux) or Windows | — |
|
|
61
62
|
| Tools | Python 3.8+ (`python3`, standard library only); Claude Code (`claude`); `curl` or `wget`, and `tar`, for the installer | `pipx` or Homebrew, to install from PyPI or the tap; `lsof`, so that `migrate-default` and `restore` can find processes using `~/.claude` (Linux falls back to `/proc`); VS Code's `code` command, for `multi-claude code` |
|
|
62
63
|
|
|
63
64
|
Install what is missing:
|
|
@@ -73,7 +74,16 @@ sudo apt install -y python3 curl tar lsof
|
|
|
73
74
|
curl -fsSL https://claude.ai/install.sh | bash # Claude Code, if you do not have it yet
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
Windows
|
|
77
|
+
On Windows, install Python 3.8+ from https://www.python.org/downloads/windows/ or the Microsoft Store, then in
|
|
78
|
+
PowerShell:
|
|
79
|
+
|
|
80
|
+
```powershell
|
|
81
|
+
irm https://claude.ai/install.ps1 | iex # Claude Code, if you do not have it yet
|
|
82
|
+
python -m pip install --user pipx
|
|
83
|
+
python -m pipx ensurepath # then open a new terminal
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Other ways to install Claude Code: https://code.claude.com/docs/en/setup
|
|
77
87
|
|
|
78
88
|
## Installation
|
|
79
89
|
|
|
@@ -103,6 +113,49 @@ With Homebrew: `brew install jakoes-wu/tap/multi-claude` (upgrade with `brew upg
|
|
|
103
113
|
|
|
104
114
|
Run `./install.sh --help` for all options.
|
|
105
115
|
|
|
116
|
+
### Windows
|
|
117
|
+
|
|
118
|
+
Windows support is new. Install it with pipx; the installer script is for macOS and Linux only. If `pipx` is not
|
|
119
|
+
installed yet, run these in cmd or PowerShell (with Python 3.8 or later; use `python` instead of `py` if you do not
|
|
120
|
+
have the Python launcher):
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
py -m pip install --user pipx
|
|
124
|
+
py -m pipx ensurepath
|
|
125
|
+
py -m pipx install multi-claude-cli
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Then open a new terminal so the `PATH` change takes effect, and check with `multi-claude --version`. With pipx
|
|
129
|
+
already on `PATH`, `pipx install multi-claude-cli` is enough. Everything above works the same way, with these
|
|
130
|
+
differences:
|
|
131
|
+
|
|
132
|
+
- Launchers are `claude-<name>.cmd` and `claude-auto.cmd` in `%USERPROFILE%\.local\bin` (the directory
|
|
133
|
+
`pipx ensurepath` adds to `PATH`), so they run from PowerShell and cmd alike. They read the proxy, variables
|
|
134
|
+
and arguments from the configuration each time they start, so a change takes effect without `apply`.
|
|
135
|
+
- Account directories are in `%USERPROFILE%\.cc` and the configuration in `%APPDATA%\multi-claude`.
|
|
136
|
+
- Shared directories are linked with directory junctions, which need no special rights. Sharing a single file
|
|
137
|
+
(such as `CLAUDE.md`) needs a file symbolic link, which needs Developer Mode (Settings > System > For
|
|
138
|
+
developers) or an administrator terminal; without it, `add` and `apply` stop and say so. Exclude the file
|
|
139
|
+
with `--shared-exclude` to share the rest.
|
|
140
|
+
- `migrate-default` refuses while any Claude process is running: Windows does not let it see which account
|
|
141
|
+
another process uses. Close every Claude window, terminal and IDE first.
|
|
142
|
+
- `statusline` needs Git for Windows, whose Git Bash is what Claude Code uses to run the status line. A
|
|
143
|
+
`statusLine` setting with `"shell": "powershell"` cannot be wrapped.
|
|
144
|
+
- Paths containing `"`, `%` or `!` cannot be written into a `.cmd` launcher; `apply` reports such an account as
|
|
145
|
+
a conflict.
|
|
146
|
+
- cmd reads a `.cmd` file in the console code page, so a launcher never contains non-ASCII characters. When the
|
|
147
|
+
Python or package path has them (for example a Chinese user name), the launcher writes it as
|
|
148
|
+
`%USERPROFILE%\...` (or `%LOCALAPPDATA%`, `%APPDATA%`), or else as its 8.3 short name. A non-ASCII path
|
|
149
|
+
that cannot be written with a variable and has no short name is reported as a conflict.
|
|
150
|
+
- When Claude exits (Ctrl+C or `/exit`), cmd may flash "Terminate batch job (Y/N)?" for a moment: the launcher is
|
|
151
|
+
a batch file. It closes by itself; Claude has already exited and nothing is lost.
|
|
152
|
+
- `multi-claude completion powershell | Add-Content $PROFILE` adds tab completion to PowerShell. Do not use `>>`:
|
|
153
|
+
Windows PowerShell 5.1 writes it as UTF-16 and breaks an existing profile. If the profile's directory does not
|
|
154
|
+
exist yet, create it first with `New-Item -ItemType Directory -Force (Split-Path $PROFILE)`.
|
|
155
|
+
|
|
156
|
+
Continuous integration runs the Windows-specific tests on Windows (`tests/test_windows.py`); the rest of the test
|
|
157
|
+
suite runs on macOS and Linux only.
|
|
158
|
+
|
|
106
159
|
## Quick start
|
|
107
160
|
|
|
108
161
|
### 1. Create one account per login
|
|
@@ -425,10 +478,13 @@ Notes:
|
|
|
425
478
|
|
|
426
479
|
`multi-claude doctor` checks, without changing anything: the configuration and any unfinished migration, whether `claude` and the launcher directory are on `PATH`, whether each launcher is up to date and not hidden by another file of the same name, whether each account directory exists and is private (`0700`), the default account's link, broken shared links, whether a login exists, and variables such as `ANTHROPIC_API_KEY` that override the accounts' logins. Every problem comes with the command that fixes it. It exits with 1 when there is an error; `--json` prints the results for scripts.
|
|
427
480
|
|
|
481
|
+
`multi-claude doctor --network` also checks that each account can reach `api.anthropic.com` and `platform.claude.com` through the proxy it starts with (including the `env` of its `settings.json`). It connects step by step and says where it stopped: the proxy is not running, the proxy cannot get through, a name does not resolve, the secure connection was cut, the certificate could not be verified, a timeout, or `NO_PROXY` sends the account around its proxy. SOCKS and malformed proxy URLs, which Claude Code cannot use, are reported without connecting; https proxies are not checked. It sends one `HEAD /` per address and proxy (a second one if the first fails), with no login or token; addresses covered by `NO_PROXY` are checked directly.
|
|
482
|
+
|
|
428
483
|
```sh
|
|
429
484
|
multi-claude completion bash > ~/.local/share/bash-completion/completions/multi-claude
|
|
430
485
|
multi-claude completion zsh > "${fpath[1]}/_multi-claude" # or any directory on your fpath
|
|
431
486
|
multi-claude completion fish > ~/.config/fish/completions/multi-claude.fish
|
|
487
|
+
multi-claude completion powershell | Add-Content $PROFILE # Windows PowerShell
|
|
432
488
|
```
|
|
433
489
|
|
|
434
490
|
## Migrating `~/.claude`
|
|
@@ -527,7 +583,7 @@ Every command's `--help` ends with examples you can copy, covering its options,
|
|
|
527
583
|
| `multi-claude list [--verbose \| --json \| --names] [--no-refresh] [--offline]` | Show accounts, launchers, login state and the plan and billing channel looked up from Anthropic. `--json` adds usage and is meant for scripts; `--names` prints only the names; `--no-refresh` does not renew expired access tokens (never writes to a login); `--offline` skips the plan lookup (no token read, no network). A `RENEWS` column (and a `renews` key in `--json`) shows the renewal dates you recorded. |
|
|
528
584
|
| `multi-claude usage [NAME] [--json]` | Show the last known 5-hour and 7-day usage of each account. |
|
|
529
585
|
| `multi-claude usage --history [NAME] [--days N] [--by day\|model] [--json]` | Token usage per day or model, counted from the session files on this machine (default: 7 days). |
|
|
530
|
-
| `multi-claude doctor [--json] [--verbose]` | Check the configuration, launchers, account directories, shared links, logins and environment. |
|
|
586
|
+
| `multi-claude doctor [--json] [--verbose] [--network]` | Check the configuration, launchers, account directories, shared links, logins and environment; `--network` also checks each account's connection to Anthropic. |
|
|
531
587
|
| `multi-claude route DIR NAME`, `route DIR --remove`, `route --default NAME`, `route --no-default` | Choose an account by directory for `claude-auto`. |
|
|
532
588
|
| `multi-claude which [DIR]` | Show which account `claude-auto` would use in `DIR` (default: the current directory). |
|
|
533
589
|
| `multi-claude rename OLD NEW` | Rename an account and its launcher. The directory and the login stay. |
|
|
@@ -550,13 +606,13 @@ Every write command accepts `--dry-run`, and prints only what changes; add `--ve
|
|
|
550
606
|
| ---- | ---- |
|
|
551
607
|
| Account directories | `~/.cc/<name>` |
|
|
552
608
|
| Launchers | `~/.local/bin/claude-<name>` |
|
|
553
|
-
| Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`) |
|
|
609
|
+
| Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`); on Windows `%APPDATA%\multi-claude\config.json` |
|
|
554
610
|
|
|
555
611
|
Account names may contain letters, digits and `._@+-`, must start with a letter or digit, and are case-insensitive (`Work` and `work` are the same account). An email address works as a name. Entries starting with `.` in the root directory are never treated as accounts.
|
|
556
612
|
|
|
557
613
|
### Declarative setup with `apply`
|
|
558
614
|
|
|
559
|
-
See [`examples/config.example.json`](examples/config.example.json):
|
|
615
|
+
See [`examples/config.example.json`](https://github.com/jakoes-wu/multi-claude/blob/main/examples/config.example.json):
|
|
560
616
|
|
|
561
617
|
```json
|
|
562
618
|
{
|
|
@@ -617,7 +673,7 @@ This removes the tool only. Your configuration, account directories (including t
|
|
|
617
673
|
|
|
618
674
|
## Contributing
|
|
619
675
|
|
|
620
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md). Run the tests with:
|
|
676
|
+
See [CONTRIBUTING.md](https://github.com/jakoes-wu/multi-claude/blob/main/CONTRIBUTING.md). Run the tests with:
|
|
621
677
|
|
|
622
678
|
```sh
|
|
623
679
|
python3 -m unittest discover -s tests -t tests
|
|
@@ -625,4 +681,4 @@ python3 -m unittest discover -s tests -t tests
|
|
|
625
681
|
|
|
626
682
|
## License
|
|
627
683
|
|
|
628
|
-
[MIT](LICENSE)
|
|
684
|
+
[MIT](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
|
|
@@ -16,6 +16,7 @@ classifiers = [
|
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
17
|
"License :: OSI Approved :: MIT License",
|
|
18
18
|
"Operating System :: MacOS",
|
|
19
|
+
"Operating System :: Microsoft :: Windows",
|
|
19
20
|
"Operating System :: POSIX :: Linux",
|
|
20
21
|
"Programming Language :: Python :: 3",
|
|
21
22
|
"Programming Language :: Python :: 3.8",
|
|
@@ -7,6 +7,13 @@ if sys.argv[1:2] == ["statusline-hook"]:
|
|
|
7
7
|
from .hook import main as hook_main
|
|
8
8
|
sys.exit(hook_main(sys.argv[2:]))
|
|
9
9
|
|
|
10
|
+
from . import platform # noqa: E402
|
|
11
|
+
|
|
12
|
+
# 闸门放在导入 cli 之前:不支持的平台上 cli 依赖的模块可能根本加载不了,要先给出一句能看懂的报错
|
|
13
|
+
if not platform.is_supported():
|
|
14
|
+
sys.stderr.write("multi-claude: unsupported platform {}\n".format(sys.platform))
|
|
15
|
+
sys.exit(1)
|
|
16
|
+
|
|
10
17
|
from .cli import main # noqa: E402
|
|
11
18
|
|
|
12
19
|
sys.exit(main())
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Windows 启动命令(claude-<名称>.cmd、claude-auto.cmd)与状态栏后台进程的入口脚本。
|
|
2
|
+
|
|
3
|
+
调用方式固定为 `"<python>" -I "<包目录>\\_entry.py" ...`(方案 feature-windows-support §5.1 D2):
|
|
4
|
+
- 用 `-I` 而不用 `-m`:`-m` 会把当前目录放进模块搜索路径,用户项目里的 json.py 之类文件会顶替标准库;
|
|
5
|
+
`-I` 也让脚本所在的包目录不进搜索路径,包里的 platform.py 等就不会顶替同名标准库模块。
|
|
6
|
+
- `-I` 同时忽略 PYTHONPATH,源码检出里运行时解释器找不到本包,所以这里把包的上一级目录追加到搜索路径末尾。
|
|
7
|
+
|
|
8
|
+
本文件只能用标准库,且必须在导入本包之前完成搜索路径的设置。
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import runpy
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _same_dir(a: str, b: str) -> bool:
|
|
17
|
+
# 启动命令里的路径可能是 8.3 短名(MULTI_~1),而包从解释器原有的长路径加载:
|
|
18
|
+
# 只比 normcase 会把同一目录判成两个。realpath 在 Windows 上会展开短名与链接。
|
|
19
|
+
if os.path.normcase(os.path.realpath(a)) == os.path.normcase(os.path.realpath(b)):
|
|
20
|
+
return True
|
|
21
|
+
try:
|
|
22
|
+
return os.path.samefile(a, b)
|
|
23
|
+
except OSError:
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _main() -> None:
|
|
28
|
+
package_dir = os.path.dirname(os.path.abspath(__file__))
|
|
29
|
+
parent = os.path.dirname(package_dir)
|
|
30
|
+
# 追加到末尾而不是插到最前:已安装的同名标准库模块仍优先于包目录之外的任何东西
|
|
31
|
+
if parent not in sys.path:
|
|
32
|
+
sys.path.append(parent)
|
|
33
|
+
import multi_claude
|
|
34
|
+
loaded = os.path.dirname(os.path.abspath(multi_claude.__file__))
|
|
35
|
+
if not _same_dir(loaded, package_dir):
|
|
36
|
+
# 解释器的 site-packages 里另装了一份 multi_claude:继续运行就是在用另一个版本
|
|
37
|
+
sys.stderr.write("multi-claude: loaded the package from {} instead of {}; reinstall multi-claude\n".format(
|
|
38
|
+
loaded, package_dir))
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
runpy.run_module("multi_claude", run_name="__main__", alter_sys=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_main()
|
|
@@ -53,6 +53,9 @@ def default_dir() -> str:
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def render_launcher(config: Config, account: Account) -> str:
|
|
56
|
+
if platform.IS_WINDOWS:
|
|
57
|
+
python, entry = launcher.windows_command_paths()
|
|
58
|
+
return launcher.render_windows(account.name, account_dir(config, account.name), python, entry)
|
|
56
59
|
return launcher.render(account.name, account.identity, account_dir(config, account.name), default_dir(),
|
|
57
60
|
account.proxy, config.effective_env(account), config.effective_args(account))
|
|
58
61
|
|
|
@@ -169,6 +172,11 @@ def _plan_router(old: Config, new: Config) -> List[Action]:
|
|
|
169
172
|
actions.append(Action(CONFLICT, "router", new_path, "claude-auto is the directory router; an account "
|
|
170
173
|
"named 'auto' cannot coexist with routes"))
|
|
171
174
|
|
|
175
|
+
if platform.IS_WINDOWS:
|
|
176
|
+
unsafe = launcher.cmd_unsafe_reason([], launcher.windows_command_paths())
|
|
177
|
+
if unsafe is not None:
|
|
178
|
+
actions.append(Action(CONFLICT, "router", new_path, unsafe))
|
|
179
|
+
return actions
|
|
172
180
|
content = routes.render_router(new)
|
|
173
181
|
kind = entry_kind(new_path)
|
|
174
182
|
if kind == KIND_MISSING:
|
|
@@ -230,12 +238,18 @@ def _make_private_dir(directory: str) -> None:
|
|
|
230
238
|
if not os.path.isdir(parent):
|
|
231
239
|
os.makedirs(parent, mode=0o700)
|
|
232
240
|
os.mkdir(directory, 0o700)
|
|
233
|
-
|
|
241
|
+
# Windows 上 chmod 只能改只读属性,访问控制靠用户目录的 ACL
|
|
242
|
+
if not platform.IS_WINDOWS:
|
|
243
|
+
os.chmod(directory, 0o700)
|
|
234
244
|
|
|
235
245
|
|
|
236
246
|
def _plan_launcher(bin_dir: str, config: Config, account: Account) -> List[Action]:
|
|
237
247
|
name = account.name
|
|
238
248
|
path = launcher.launcher_path(bin_dir, name)
|
|
249
|
+
if platform.IS_WINDOWS:
|
|
250
|
+
unsafe = launcher.cmd_unsafe_reason([account_dir(config, name)], launcher.windows_command_paths())
|
|
251
|
+
if unsafe is not None:
|
|
252
|
+
return [Action(CONFLICT, "launcher", path, unsafe)]
|
|
239
253
|
content = render_launcher(config, account)
|
|
240
254
|
kind = entry_kind(path)
|
|
241
255
|
if kind == KIND_MISSING:
|
|
@@ -296,6 +310,12 @@ def execute(old: Config, new: Config, actions: List[Action], *, dry_run: bool, v
|
|
|
296
310
|
return EXIT_CONFLICT
|
|
297
311
|
if dry_run:
|
|
298
312
|
return EXIT_OK
|
|
313
|
+
# Windows 上没有权限建文件链接时,在写配置之前就停下,免得配置已写、链接只建了一半(方案 C4)
|
|
314
|
+
file_links = shared.file_links_needed(actions) if platform.IS_WINDOWS else []
|
|
315
|
+
if file_links and not platform.can_create_file_link():
|
|
316
|
+
error(shared.file_link_message("the file links " + ", ".join(file_links),
|
|
317
|
+
sorted({os.path.basename(path) for path in file_links})), phase="shared-link")
|
|
318
|
+
return EXIT_ERROR
|
|
299
319
|
config_action = actions[0]
|
|
300
320
|
if config_action.changes:
|
|
301
321
|
try:
|
|
@@ -10,7 +10,6 @@ assistant 记录,结果写一行带 `tool_result` 块的 user 记录,发布
|
|
|
10
10
|
这段字节的 SHA-256),查询时据此把副本里交接前的记录归回来源账号(§5.1.3)。
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
import fcntl
|
|
14
13
|
import hashlib
|
|
15
14
|
import json
|
|
16
15
|
import os
|
|
@@ -244,13 +243,13 @@ def record_handoff(handoff: "sessions.Plan", source_account_dir: str, target_acc
|
|
|
244
243
|
data = (json.dumps(entry, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8")
|
|
245
244
|
path = log_path()
|
|
246
245
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
247
|
-
fd = os.open(path, os.O_RDWR | os.O_APPEND | os.O_CREAT, 0o600)
|
|
246
|
+
fd = os.open(path, os.O_RDWR | os.O_APPEND | os.O_CREAT | getattr(os, "O_BINARY", 0), 0o600)
|
|
248
247
|
try:
|
|
249
248
|
# 阻塞等待:两个 handoff 同时追加时排队,不交错。锁的是本文件,不是工具的写锁(handoff 不拿写锁)
|
|
250
|
-
|
|
249
|
+
platform.lock_fd(fd, blocking=True)
|
|
251
250
|
# 只写描述符读不了末字节,所以上面用 O_RDWR 打开
|
|
252
251
|
size = os.fstat(fd).st_size
|
|
253
|
-
if size and
|
|
252
|
+
if size and platform.read_at(fd, 1, size - 1) != b"\n":
|
|
254
253
|
data = b"\n" + data
|
|
255
254
|
view = memoryview(data)
|
|
256
255
|
while view:
|
|
@@ -258,7 +257,7 @@ def record_handoff(handoff: "sessions.Plan", source_account_dir: str, target_acc
|
|
|
258
257
|
view = view[written:]
|
|
259
258
|
os.fsync(fd)
|
|
260
259
|
finally:
|
|
261
|
-
#
|
|
260
|
+
# 关闭即释放锁(flock 与 msvcrt 字节锁都如此)
|
|
262
261
|
os.close(fd)
|
|
263
262
|
|
|
264
263
|
|
|
@@ -22,7 +22,7 @@ import time
|
|
|
22
22
|
from datetime import datetime, timezone
|
|
23
23
|
from typing import Dict, List, Optional, Tuple
|
|
24
24
|
|
|
25
|
-
from . import __version__, accounts, usage
|
|
25
|
+
from . import __version__, accounts, platform, usage
|
|
26
26
|
from .actions import error, info, warn
|
|
27
27
|
from .config import Account, Config
|
|
28
28
|
from .fsutil import KIND_DIR, KIND_FILE, KIND_LINK, KIND_MISSING, atomic_write, entry_kind
|
|
@@ -114,10 +114,12 @@ def _create_temp(parent: str):
|
|
|
114
114
|
for _attempt in range(100):
|
|
115
115
|
path = os.path.join(parent, ".multi-claude-export.{}.tar.gz".format(os.urandom(6).hex()))
|
|
116
116
|
try:
|
|
117
|
-
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
117
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0), 0o600)
|
|
118
118
|
except FileExistsError:
|
|
119
119
|
continue
|
|
120
|
-
|
|
120
|
+
# 不受 umask 影响:包里可能有密钥,只给本人读。Windows 没有 fchmod,权限由用户目录的 ACL 保证
|
|
121
|
+
if hasattr(os, "fchmod"):
|
|
122
|
+
os.fchmod(fd, 0o600)
|
|
121
123
|
return path, fd
|
|
122
124
|
raise OSError("cannot create a temporary file in {}".format(parent))
|
|
123
125
|
|
|
@@ -132,7 +134,7 @@ def _publish(tmp_path: str, out_path: str) -> None:
|
|
|
132
134
|
# 不支持硬链接的文件系统(如 exFAT):再检查一次后改名
|
|
133
135
|
if os.path.lexists(out_path):
|
|
134
136
|
raise OSError("{} appeared while exporting; nothing was written".format(out_path))
|
|
135
|
-
os.replace
|
|
137
|
+
platform.retry_on_sharing_violation(os.replace, tmp_path, out_path)
|
|
136
138
|
return
|
|
137
139
|
os.unlink(tmp_path)
|
|
138
140
|
|
|
@@ -350,7 +352,9 @@ def _check_item_types(members: List[tarfile.TarInfo]) -> None:
|
|
|
350
352
|
def _check_member(member: tarfile.TarInfo) -> None:
|
|
351
353
|
name = member.name
|
|
352
354
|
parts = name.split("/")
|
|
353
|
-
|
|
355
|
+
# Windows 上成员名里的冒号会被解析成盘符(C:x)或 NTFS 备用数据流(a:b),一律拒绝;POSIX 上冒号是普通字符
|
|
356
|
+
if (name.startswith("/") or "\\" in name or "\x00" in name or (platform.IS_WINDOWS and ":" in name)
|
|
357
|
+
or any(part in ("", ".", "..") for part in parts)):
|
|
354
358
|
raise BundleError("unsafe path in bundle: {!r}".format(name))
|
|
355
359
|
if not (member.isfile() or member.isdir()):
|
|
356
360
|
raise BundleError("unsupported entry in bundle (only files and directories): {!r}".format(name))
|
|
@@ -453,9 +457,9 @@ def _apply(plan: _Plan, directory: str, staging: str, state_path: str) -> None:
|
|
|
453
457
|
target = os.path.join(directory, item)
|
|
454
458
|
if action == "replace":
|
|
455
459
|
backup = _backup_name(target, stamp)
|
|
456
|
-
os.rename
|
|
460
|
+
platform.retry_on_sharing_violation(os.rename, target, backup)
|
|
457
461
|
info("backup: {}".format(backup))
|
|
458
|
-
os.replace
|
|
462
|
+
platform.retry_on_sharing_violation(os.replace, os.path.join(staging, ITEMS_DIR, item), target)
|
|
459
463
|
if data is None:
|
|
460
464
|
return
|
|
461
465
|
warn("Claude sessions of this account that are running keep their own copy of {} and may overwrite "
|