multi-claude-cli 0.13.0__tar.gz → 0.15.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.13.0 → multi_claude_cli-0.15.0}/PKG-INFO +55 -9
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/README.md +53 -8
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/pyproject.toml +1 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/__init__.py +1 -1
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/__main__.py +7 -0
- multi_claude_cli-0.15.0/src/multi_claude/_entry.py +43 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/accounts.py +21 -1
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/artifacts.py +4 -5
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/bundle.py +11 -7
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/cli.py +268 -17
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/completion.py +97 -2
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/config.py +19 -1
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/doctor.py +11 -13
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/env.py +24 -1
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/fsutil.py +31 -6
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/history.py +5 -1
- multi_claude_cli-0.15.0/src/multi_claude/hook.py +185 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/identity.py +27 -1
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/launcher.py +127 -5
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/lock.py +5 -6
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/migrate.py +62 -8
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/plan.py +4 -1
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/platform.py +469 -15
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/restore.py +24 -3
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/routes.py +33 -5
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/sessions.py +5 -5
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/shared.py +27 -2
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/shellpath.py +20 -2
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/statusline.py +34 -5
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude_cli.egg-info/PKG-INFO +55 -9
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude_cli.egg-info/SOURCES.txt +4 -1
- multi_claude_cli-0.15.0/tests/test_help_examples.py +224 -0
- multi_claude_cli-0.15.0/tests/test_windows.py +502 -0
- multi_claude_cli-0.13.0/src/multi_claude/hook.py +0 -90
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/LICENSE +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/setup.cfg +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/actions.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/refresh.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/usage.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude_cli.egg-info/dependency_links.txt +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude_cli.egg-info/entry_points.txt +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude_cli.egg-info/top_level.txt +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_accounts.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_artifacts.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_bundle.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_completion.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_config.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_doctor.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_everyday.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_fsutil.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_handoff.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_help.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_history.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_identity.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_install.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_mcp.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_migrate.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_onboarding.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_packaging.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_plan.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_platform.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_refresh.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_rename.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_renewal.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_routes.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_sessions.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_set.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_shared_exclude.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_statusline.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_usability.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_usage.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.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.15.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,13 +28,14 @@ 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
|
|
|
@@ -84,7 +86,7 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
|
|
|
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,38 @@ 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 with `pipx install multi-claude-cli`; the installer script is for macOS and Linux
|
|
147
|
+
only. Everything above works the same way, with these differences:
|
|
148
|
+
|
|
149
|
+
- Launchers are `claude-<name>.cmd` and `claude-auto.cmd` in `%USERPROFILE%\.local\bin` (the directory
|
|
150
|
+
`pipx ensurepath` adds to `PATH`), so they run from PowerShell and cmd alike. They read the proxy, variables
|
|
151
|
+
and arguments from the configuration each time they start, so a change takes effect without `apply`.
|
|
152
|
+
- Account directories are in `%USERPROFILE%\.cc` and the configuration in `%APPDATA%\multi-claude`.
|
|
153
|
+
- Shared directories are linked with directory junctions, which need no special rights. Sharing a single file
|
|
154
|
+
(such as `CLAUDE.md`) needs a file symbolic link, which needs Developer Mode (Settings > System > For
|
|
155
|
+
developers) or an administrator terminal; without it, `add` and `apply` stop and say so. Exclude the file
|
|
156
|
+
with `--shared-exclude` to share the rest.
|
|
157
|
+
- `migrate-default` refuses while any Claude process is running: Windows does not let it see which account
|
|
158
|
+
another process uses. Close every Claude window, terminal and IDE first.
|
|
159
|
+
- `statusline` needs Git for Windows, whose Git Bash is what Claude Code uses to run the status line. A
|
|
160
|
+
`statusLine` setting with `"shell": "powershell"` cannot be wrapped.
|
|
161
|
+
- Paths containing `"`, `%` or `!` cannot be written into a `.cmd` launcher; `apply` reports such an account as
|
|
162
|
+
a conflict.
|
|
163
|
+
- cmd reads a `.cmd` file in the console code page, so a launcher never contains non-ASCII characters. When the
|
|
164
|
+
Python or package path has them (for example a Chinese user name), the launcher writes it as
|
|
165
|
+
`%USERPROFILE%\...` (or `%LOCALAPPDATA%`, `%APPDATA%`), or else as its 8.3 short name. A non-ASCII path
|
|
166
|
+
that cannot be written with a variable and has no short name is reported as a conflict.
|
|
167
|
+
- When Claude exits (Ctrl+C or `/exit`), cmd may flash "Terminate batch job (Y/N)?" for a moment: the launcher is
|
|
168
|
+
a batch file. It closes by itself; Claude has already exited and nothing is lost.
|
|
169
|
+
- `multi-claude completion powershell | Add-Content $PROFILE` adds tab completion to PowerShell. Do not use `>>`:
|
|
170
|
+
Windows PowerShell 5.1 writes it as UTF-16 and breaks an existing profile. If the profile's directory does not
|
|
171
|
+
exist yet, create it first with `New-Item -ItemType Directory -Force (Split-Path $PROFILE)`.
|
|
172
|
+
|
|
173
|
+
Continuous integration runs the Windows-specific tests on Windows (`tests/test_windows.py`); the rest of the test
|
|
174
|
+
suite runs on macOS and Linux only.
|
|
175
|
+
|
|
133
176
|
## Quick start
|
|
134
177
|
|
|
135
178
|
### 1. Create one account per login
|
|
@@ -456,6 +499,7 @@ Notes:
|
|
|
456
499
|
multi-claude completion bash > ~/.local/share/bash-completion/completions/multi-claude
|
|
457
500
|
multi-claude completion zsh > "${fpath[1]}/_multi-claude" # or any directory on your fpath
|
|
458
501
|
multi-claude completion fish > ~/.config/fish/completions/multi-claude.fish
|
|
502
|
+
multi-claude completion powershell | Add-Content $PROFILE # Windows PowerShell
|
|
459
503
|
```
|
|
460
504
|
|
|
461
505
|
## Migrating `~/.claude`
|
|
@@ -535,6 +579,8 @@ It never copies, exports, uploads or backs up a login. One command touches one:
|
|
|
535
579
|
|
|
536
580
|
### Commands
|
|
537
581
|
|
|
582
|
+
Every command's `--help` ends with examples you can copy, covering its options, e.g. `multi-claude route --help`.
|
|
583
|
+
|
|
538
584
|
| Command | What it does |
|
|
539
585
|
| ---- | ---- |
|
|
540
586
|
| `multi-claude init [--root DIR] [--bin-dir DIR] [--shared-dir DIR] [--shared-items A,B]` | Create or change global settings. |
|
|
@@ -575,13 +621,13 @@ Every write command accepts `--dry-run`, and prints only what changes; add `--ve
|
|
|
575
621
|
| ---- | ---- |
|
|
576
622
|
| Account directories | `~/.cc/<name>` |
|
|
577
623
|
| Launchers | `~/.local/bin/claude-<name>` |
|
|
578
|
-
| Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`) |
|
|
624
|
+
| Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`); on Windows `%APPDATA%\multi-claude\config.json` |
|
|
579
625
|
|
|
580
626
|
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.
|
|
581
627
|
|
|
582
628
|
### Declarative setup with `apply`
|
|
583
629
|
|
|
584
|
-
See [`examples/config.example.json`](examples/config.example.json):
|
|
630
|
+
See [`examples/config.example.json`](https://github.com/jakoes-wu/multi-claude/blob/main/examples/config.example.json):
|
|
585
631
|
|
|
586
632
|
```json
|
|
587
633
|
{
|
|
@@ -642,7 +688,7 @@ This removes the tool only. Your configuration, account directories (including t
|
|
|
642
688
|
|
|
643
689
|
## Contributing
|
|
644
690
|
|
|
645
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md). Run the tests with:
|
|
691
|
+
See [CONTRIBUTING.md](https://github.com/jakoes-wu/multi-claude/blob/main/CONTRIBUTING.md). Run the tests with:
|
|
646
692
|
|
|
647
693
|
```sh
|
|
648
694
|
python3 -m unittest discover -s tests -t tests
|
|
@@ -650,4 +696,4 @@ python3 -m unittest discover -s tests -t tests
|
|
|
650
696
|
|
|
651
697
|
## License
|
|
652
698
|
|
|
653
|
-
[MIT](LICENSE)
|
|
699
|
+
[MIT](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
|
|
@@ -1,12 +1,13 @@
|
|
|
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
|
|
|
@@ -57,7 +58,7 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
|
|
|
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,38 @@ 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 with `pipx install multi-claude-cli`; the installer script is for macOS and Linux
|
|
119
|
+
only. Everything above works the same way, with these differences:
|
|
120
|
+
|
|
121
|
+
- Launchers are `claude-<name>.cmd` and `claude-auto.cmd` in `%USERPROFILE%\.local\bin` (the directory
|
|
122
|
+
`pipx ensurepath` adds to `PATH`), so they run from PowerShell and cmd alike. They read the proxy, variables
|
|
123
|
+
and arguments from the configuration each time they start, so a change takes effect without `apply`.
|
|
124
|
+
- Account directories are in `%USERPROFILE%\.cc` and the configuration in `%APPDATA%\multi-claude`.
|
|
125
|
+
- Shared directories are linked with directory junctions, which need no special rights. Sharing a single file
|
|
126
|
+
(such as `CLAUDE.md`) needs a file symbolic link, which needs Developer Mode (Settings > System > For
|
|
127
|
+
developers) or an administrator terminal; without it, `add` and `apply` stop and say so. Exclude the file
|
|
128
|
+
with `--shared-exclude` to share the rest.
|
|
129
|
+
- `migrate-default` refuses while any Claude process is running: Windows does not let it see which account
|
|
130
|
+
another process uses. Close every Claude window, terminal and IDE first.
|
|
131
|
+
- `statusline` needs Git for Windows, whose Git Bash is what Claude Code uses to run the status line. A
|
|
132
|
+
`statusLine` setting with `"shell": "powershell"` cannot be wrapped.
|
|
133
|
+
- Paths containing `"`, `%` or `!` cannot be written into a `.cmd` launcher; `apply` reports such an account as
|
|
134
|
+
a conflict.
|
|
135
|
+
- cmd reads a `.cmd` file in the console code page, so a launcher never contains non-ASCII characters. When the
|
|
136
|
+
Python or package path has them (for example a Chinese user name), the launcher writes it as
|
|
137
|
+
`%USERPROFILE%\...` (or `%LOCALAPPDATA%`, `%APPDATA%`), or else as its 8.3 short name. A non-ASCII path
|
|
138
|
+
that cannot be written with a variable and has no short name is reported as a conflict.
|
|
139
|
+
- When Claude exits (Ctrl+C or `/exit`), cmd may flash "Terminate batch job (Y/N)?" for a moment: the launcher is
|
|
140
|
+
a batch file. It closes by itself; Claude has already exited and nothing is lost.
|
|
141
|
+
- `multi-claude completion powershell | Add-Content $PROFILE` adds tab completion to PowerShell. Do not use `>>`:
|
|
142
|
+
Windows PowerShell 5.1 writes it as UTF-16 and breaks an existing profile. If the profile's directory does not
|
|
143
|
+
exist yet, create it first with `New-Item -ItemType Directory -Force (Split-Path $PROFILE)`.
|
|
144
|
+
|
|
145
|
+
Continuous integration runs the Windows-specific tests on Windows (`tests/test_windows.py`); the rest of the test
|
|
146
|
+
suite runs on macOS and Linux only.
|
|
147
|
+
|
|
106
148
|
## Quick start
|
|
107
149
|
|
|
108
150
|
### 1. Create one account per login
|
|
@@ -429,6 +471,7 @@ Notes:
|
|
|
429
471
|
multi-claude completion bash > ~/.local/share/bash-completion/completions/multi-claude
|
|
430
472
|
multi-claude completion zsh > "${fpath[1]}/_multi-claude" # or any directory on your fpath
|
|
431
473
|
multi-claude completion fish > ~/.config/fish/completions/multi-claude.fish
|
|
474
|
+
multi-claude completion powershell | Add-Content $PROFILE # Windows PowerShell
|
|
432
475
|
```
|
|
433
476
|
|
|
434
477
|
## Migrating `~/.claude`
|
|
@@ -508,6 +551,8 @@ It never copies, exports, uploads or backs up a login. One command touches one:
|
|
|
508
551
|
|
|
509
552
|
### Commands
|
|
510
553
|
|
|
554
|
+
Every command's `--help` ends with examples you can copy, covering its options, e.g. `multi-claude route --help`.
|
|
555
|
+
|
|
511
556
|
| Command | What it does |
|
|
512
557
|
| ---- | ---- |
|
|
513
558
|
| `multi-claude init [--root DIR] [--bin-dir DIR] [--shared-dir DIR] [--shared-items A,B]` | Create or change global settings. |
|
|
@@ -548,13 +593,13 @@ Every write command accepts `--dry-run`, and prints only what changes; add `--ve
|
|
|
548
593
|
| ---- | ---- |
|
|
549
594
|
| Account directories | `~/.cc/<name>` |
|
|
550
595
|
| Launchers | `~/.local/bin/claude-<name>` |
|
|
551
|
-
| Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`) |
|
|
596
|
+
| Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`); on Windows `%APPDATA%\multi-claude\config.json` |
|
|
552
597
|
|
|
553
598
|
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.
|
|
554
599
|
|
|
555
600
|
### Declarative setup with `apply`
|
|
556
601
|
|
|
557
|
-
See [`examples/config.example.json`](examples/config.example.json):
|
|
602
|
+
See [`examples/config.example.json`](https://github.com/jakoes-wu/multi-claude/blob/main/examples/config.example.json):
|
|
558
603
|
|
|
559
604
|
```json
|
|
560
605
|
{
|
|
@@ -615,7 +660,7 @@ This removes the tool only. Your configuration, account directories (including t
|
|
|
615
660
|
|
|
616
661
|
## Contributing
|
|
617
662
|
|
|
618
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md). Run the tests with:
|
|
663
|
+
See [CONTRIBUTING.md](https://github.com/jakoes-wu/multi-claude/blob/main/CONTRIBUTING.md). Run the tests with:
|
|
619
664
|
|
|
620
665
|
```sh
|
|
621
666
|
python3 -m unittest discover -s tests -t tests
|
|
@@ -623,4 +668,4 @@ python3 -m unittest discover -s tests -t tests
|
|
|
623
668
|
|
|
624
669
|
## License
|
|
625
670
|
|
|
626
|
-
[MIT](LICENSE)
|
|
671
|
+
[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 "
|