multi-claude-cli 0.8.0__tar.gz → 0.9.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.
Files changed (63) hide show
  1. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/PKG-INFO +47 -11
  2. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/README.md +46 -10
  3. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/__init__.py +1 -1
  4. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/cli.py +65 -21
  5. multi_claude_cli-0.9.0/src/multi_claude/identity.py +189 -0
  6. multi_claude_cli-0.9.0/src/multi_claude/plan.py +317 -0
  7. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude_cli.egg-info/PKG-INFO +47 -11
  8. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude_cli.egg-info/SOURCES.txt +2 -0
  9. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_help.py +3 -3
  10. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_migrate.py +1 -1
  11. multi_claude_cli-0.9.0/tests/test_plan.py +478 -0
  12. multi_claude_cli-0.8.0/src/multi_claude/identity.py +0 -94
  13. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/LICENSE +0 -0
  14. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/pyproject.toml +0 -0
  15. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/setup.cfg +0 -0
  16. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/__main__.py +0 -0
  17. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/accounts.py +0 -0
  18. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/actions.py +0 -0
  19. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/bundle.py +0 -0
  20. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/completion.py +0 -0
  21. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/config.py +0 -0
  22. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/doctor.py +0 -0
  23. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/env.py +0 -0
  24. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/fsutil.py +0 -0
  25. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/history.py +0 -0
  26. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/hook.py +0 -0
  27. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/launcher.py +0 -0
  28. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/lock.py +0 -0
  29. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/migrate.py +0 -0
  30. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/platform.py +0 -0
  31. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/restore.py +0 -0
  32. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/routes.py +0 -0
  33. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/sessions.py +0 -0
  34. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/shared.py +0 -0
  35. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/shellpath.py +0 -0
  36. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/statusline.py +0 -0
  37. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude/usage.py +0 -0
  38. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude_cli.egg-info/dependency_links.txt +0 -0
  39. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude_cli.egg-info/entry_points.txt +0 -0
  40. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/src/multi_claude_cli.egg-info/top_level.txt +0 -0
  41. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_accounts.py +0 -0
  42. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_bundle.py +0 -0
  43. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_completion.py +0 -0
  44. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_config.py +0 -0
  45. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_doctor.py +0 -0
  46. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_everyday.py +0 -0
  47. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_fsutil.py +0 -0
  48. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_handoff.py +0 -0
  49. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_history.py +0 -0
  50. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_identity.py +0 -0
  51. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_install.py +0 -0
  52. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_mcp.py +0 -0
  53. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_onboarding.py +0 -0
  54. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_packaging.py +0 -0
  55. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_platform.py +0 -0
  56. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_rename.py +0 -0
  57. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_routes.py +0 -0
  58. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_set.py +0 -0
  59. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_shared_exclude.py +0 -0
  60. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_statusline.py +0 -0
  61. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_usability.py +0 -0
  62. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.0}/tests/test_usage.py +0 -0
  63. {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.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.8.0
3
+ Version: 0.9.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
@@ -37,7 +37,7 @@ Dynamic: license-file
37
37
 
38
38
  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
39
 
40
- It never reads your login, never connects to the network and never edits your shell startup files.
40
+ It never changes or copies 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; `list --offline` skips that.
41
41
 
42
42
  ```sh
43
43
  claude-work # Claude Code with your work account
@@ -64,8 +64,8 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
64
64
 
65
65
  ## Features
66
66
 
67
- - **Never reads your login** — the only keychain call checks whether an item exists; the contents of keychain items and `.credentials.json` are never opened. `migrate-default` and `restore` move whole account directories, and on Linux the credentials file moves with its directory without being opened.
68
- - **No network** — multi-claude itself makes no network connections: no telemetry, no usage reporting, no update checks. Only the installer, pipx or Homebrew download the package.
67
+ - **Your login stays yours** — logins are never written, copied, exported or refreshed. One command reads one: `list` takes each account's access token (from the keychain, or `.credentials.json` on Linux) to ask Anthropic which plan the account is on. The token stays in memory and goes only to `api.anthropic.com`, through that account's proxy. `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
+ - **One kind of network request, nothing else** — the plan lookup of `list` (one request per account) is the only connection 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
69
  - **Leaves your shell alone** — when `PATH` needs a change, it prints the line to add instead of editing `~/.zshrc` or `~/.bashrc`.
70
70
  - **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
71
  - **Migrate the default account** — move your existing `~/.claude` into the account root without logging out. `claude` keeps working exactly as before.
@@ -76,11 +76,31 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
76
76
  - **Idempotent** — every command can be re-run safely. Write commands print only what changes (`--verbose` lists everything); conflicts with files multi-claude does not own are reported without changing anything; an interrupted migration resumes where it stopped.
77
77
  - **Optional shared resources** — link `CLAUDE.md`, `skills`, `agents` and `commands` from one shared directory into selected accounts.
78
78
 
79
- ## Requirements
79
+ ## Prerequisites
80
80
 
81
- - macOS or Linux (Windows is planned; inside WSL, use the Linux instructions)
82
- - Python 3.8 or newer (standard library only)
83
- - Claude Code (`claude`) on your `PATH`
81
+ **Servers.** None. multi-claude runs only on your own computer; its only network requests are the plan lookups of `list`, sent to Anthropic.
82
+
83
+ **Your computer.**
84
+
85
+ | | Needed | Optional |
86
+ | ---- | ---- | ---- |
87
+ | Operating system | macOS or Linux (WSL counts as Linux) | — |
88
+ | 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
+
90
+ Install what is missing:
91
+
92
+ ```sh
93
+ # macOS (with Homebrew, https://brew.sh); curl, tar and lsof come with macOS
94
+ brew install python
95
+ curl -fsSL https://claude.ai/install.sh | bash # Claude Code, if you do not have it yet
96
+
97
+ # Debian / Ubuntu / WSL
98
+ sudo apt update
99
+ sudo apt install -y python3 curl tar lsof
100
+ curl -fsSL https://claude.ai/install.sh | bash # Claude Code, if you do not have it yet
101
+ ```
102
+
103
+ Windows itself is not supported yet; use WSL. Other ways to install Claude Code: https://code.claude.com/docs/en/setup
84
104
 
85
105
  ## Installation
86
106
 
@@ -261,7 +281,7 @@ work 23% 14:00 41% Fri 12m ago
261
281
  home - - no data (start claude-home once)
262
282
  ```
263
283
 
264
- The numbers are the last values Claude Code itself cached in the account's `.claude.json` (for the default identity, `~/.claude.json`). multi-claude never reads credentials and never calls the network, so the data can be old: `UPDATED` shows its age, and values older than an hour are marked `stale`. Claude Code writes this cache itself; when it refreshes it is not documented. A window whose reset time has passed shows `reset`.
284
+ The numbers are the last values Claude Code itself cached in the account's `.claude.json` (for the default identity, `~/.claude.json`). `usage` never reads credentials and never calls the network, so the data can be old: `UPDATED` shows its age, and values older than an hour are marked `stale`. Claude Code writes this cache itself; when it refreshes it is not documented. A window whose reset time has passed shows `reset`.
265
285
 
266
286
  For fresher numbers, let your status line record them. Claude Code passes the 5-hour and 7-day usage to the `statusLine` command each time it redraws the status line (Pro and Max plans, after the first response of a session). Wrap the `statusLine` command of the settings file that is actually in effect:
267
287
 
@@ -293,6 +313,22 @@ work:
293
313
 
294
314
  `list` shows when each account was last used (`LAST USED`), from the newest session file or prompt history in its directory; a `handoff` into an account counts as use.
295
315
 
316
+ #### Plan and billing
317
+
318
+ ```text
319
+ NAME LOGIN PLAN BILLING PROXY SHARED 5H 7D LAST USED STATUS
320
+ work keychain max (5x) stripe http://127.0.0.1:7890 no 23% 14:00 41% Fri 5m ago ok
321
+ home keychain pro apple inherit no - - 2d ago ok
322
+ old keychain expired - inherit no - - 9d ago ok
323
+ ```
324
+
325
+ `PLAN` and `BILLING` are not cached values: every `list` asks Anthropic (`GET https://api.anthropic.com/api/oauth/profile`, the request Claude Code itself makes) with the account's own access token. `PLAN` is the subscription — `pro`, `max (5x)`, `max (20x)`, `team`, `enterprise`. `BILLING` is where the subscription is paid: `stripe` (claude.ai), `apple` (App Store), `google-play`, `aws-marketplace` and so on.
326
+
327
+ - The request uses the account's proxy setting, exactly like its launcher: the configured proxy, the proxy variables of your shell for `inherit`, none for `off`. If a proxy is configured and fails, the lookup fails; it never falls back to a direct connection. Only `http://` proxies work for the lookup.
328
+ - `expired` means the access token has run out. Tokens last a few hours and multi-claude never refreshes them (refreshing would compete with running sessions): start `claude-NAME` once and run `list` again. `-` means the account has no OAuth login, and `rejected` that the server refused the token.
329
+ - `error` comes with a reason on stderr, for example `plan lookup failed for work: timeout (via 127.0.0.1:7890, 10.01s)`. `tls_failed` usually means the Python that runs multi-claude has no root certificates (python.org installers ship an `Install Certificates.command`).
330
+ - `list --offline` skips the lookup — no token is read, nothing is sent — and prints the table without the two columns. `list --json` carries the same data in each account's `plan` object; scripts should use it instead of parsing the table. The lookup adds about a second, at most 15.
331
+
296
332
  ### Handing a session over to another account
297
333
 
298
334
  Each account keeps its own sessions, so `claude --resume` in one account cannot see the sessions of another. `handoff` copies one session (its `.jsonl` file and the folder next to it with subagent transcripts and saved tool output) into the same project folder of another account:
@@ -427,7 +463,7 @@ By default the numbers come from the cache that Claude Code writes in `.claude.j
427
463
 
428
464
  ### Does multi-claude read or copy my login?
429
465
 
430
- It never reads it: its only keychain call checks whether an item exists, and it never opens `.credentials.json`. It does not export, upload or back up logins. `migrate-default` and `restore` move a whole account directory; on Linux the credentials file is inside it and moves with it (in copy mode it is copied as part of the directory, verified, and the original directory is then removed unless you pass `--keep-backup`), still without being opened. On macOS a login is tied to the account directory's path, which is why `rename` keeps the directory.
466
+ It never writes, copies, exports, uploads, refreshes or backs up a login. One command reads one: `list` takes each account's access token to ask Anthropic for the account's plan (see [Plan and billing](#plan-and-billing)); the token stays in memory and is sent only to `api.anthropic.com`. `list --offline` does not read it. All other commands only check whether a login exists. `migrate-default` and `restore` move a whole account directory; on Linux the credentials file is inside it and moves with it (in copy mode it is copied as part of the directory, verified, and the original directory is then removed unless you pass `--keep-backup`), still without being opened. On macOS a login is tied to the account directory's path, which is why `rename` keeps the directory.
431
467
 
432
468
  ## Reference
433
469
 
@@ -447,7 +483,7 @@ It never reads it: its only keychain call checks whether an item exists, and it
447
483
  | `multi-claude args (NAME \| --defaults) -- [ARG ...]` | Replace the fixed arguments; `--` with nothing after it clears them. |
448
484
  | `multi-claude remove NAME` | Unregister an account and delete its launcher. **The directory and the login are kept.** |
449
485
  | `multi-claude apply [-f FILE]` | Converge everything to the configuration (or to `FILE`). |
450
- | `multi-claude list [--verbose \| --json \| --names]` | Show accounts, launchers and login state. `--json` adds usage and is meant for scripts; `--names` prints only the names. |
486
+ | `multi-claude list [--verbose \| --json \| --names] [--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; `--offline` skips the plan lookup (no token read, no network). |
451
487
  | `multi-claude usage [NAME] [--json]` | Show the last known 5-hour and 7-day usage of each account. |
452
488
  | `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). |
453
489
  | `multi-claude doctor [--json] [--verbose]` | Check the configuration, launchers, account directories, shared links, logins and environment. |
@@ -10,7 +10,7 @@
10
10
 
11
11
  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
12
 
13
- It never reads your login, never connects to the network and never edits your shell startup files.
13
+ It never changes or copies 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; `list --offline` skips that.
14
14
 
15
15
  ```sh
16
16
  claude-work # Claude Code with your work account
@@ -37,8 +37,8 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
37
37
 
38
38
  ## Features
39
39
 
40
- - **Never reads your login** — the only keychain call checks whether an item exists; the contents of keychain items and `.credentials.json` are never opened. `migrate-default` and `restore` move whole account directories, and on Linux the credentials file moves with its directory without being opened.
41
- - **No network** — multi-claude itself makes no network connections: no telemetry, no usage reporting, no update checks. Only the installer, pipx or Homebrew download the package.
40
+ - **Your login stays yours** — logins are never written, copied, exported or refreshed. One command reads one: `list` takes each account's access token (from the keychain, or `.credentials.json` on Linux) to ask Anthropic which plan the account is on. The token stays in memory and goes only to `api.anthropic.com`, through that account's proxy. `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
+ - **One kind of network request, nothing else** — the plan lookup of `list` (one request per account) is the only connection 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
42
  - **Leaves your shell alone** — when `PATH` needs a change, it prints the line to add instead of editing `~/.zshrc` or `~/.bashrc`.
43
43
  - **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
44
  - **Migrate the default account** — move your existing `~/.claude` into the account root without logging out. `claude` keeps working exactly as before.
@@ -49,11 +49,31 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
49
49
  - **Idempotent** — every command can be re-run safely. Write commands print only what changes (`--verbose` lists everything); conflicts with files multi-claude does not own are reported without changing anything; an interrupted migration resumes where it stopped.
50
50
  - **Optional shared resources** — link `CLAUDE.md`, `skills`, `agents` and `commands` from one shared directory into selected accounts.
51
51
 
52
- ## Requirements
52
+ ## Prerequisites
53
53
 
54
- - macOS or Linux (Windows is planned; inside WSL, use the Linux instructions)
55
- - Python 3.8 or newer (standard library only)
56
- - Claude Code (`claude`) on your `PATH`
54
+ **Servers.** None. multi-claude runs only on your own computer; its only network requests are the plan lookups of `list`, sent to Anthropic.
55
+
56
+ **Your computer.**
57
+
58
+ | | Needed | Optional |
59
+ | ---- | ---- | ---- |
60
+ | Operating system | macOS or Linux (WSL counts as Linux) | — |
61
+ | 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
+ Install what is missing:
64
+
65
+ ```sh
66
+ # macOS (with Homebrew, https://brew.sh); curl, tar and lsof come with macOS
67
+ brew install python
68
+ curl -fsSL https://claude.ai/install.sh | bash # Claude Code, if you do not have it yet
69
+
70
+ # Debian / Ubuntu / WSL
71
+ sudo apt update
72
+ sudo apt install -y python3 curl tar lsof
73
+ curl -fsSL https://claude.ai/install.sh | bash # Claude Code, if you do not have it yet
74
+ ```
75
+
76
+ Windows itself is not supported yet; use WSL. Other ways to install Claude Code: https://code.claude.com/docs/en/setup
57
77
 
58
78
  ## Installation
59
79
 
@@ -234,7 +254,7 @@ work 23% 14:00 41% Fri 12m ago
234
254
  home - - no data (start claude-home once)
235
255
  ```
236
256
 
237
- The numbers are the last values Claude Code itself cached in the account's `.claude.json` (for the default identity, `~/.claude.json`). multi-claude never reads credentials and never calls the network, so the data can be old: `UPDATED` shows its age, and values older than an hour are marked `stale`. Claude Code writes this cache itself; when it refreshes it is not documented. A window whose reset time has passed shows `reset`.
257
+ The numbers are the last values Claude Code itself cached in the account's `.claude.json` (for the default identity, `~/.claude.json`). `usage` never reads credentials and never calls the network, so the data can be old: `UPDATED` shows its age, and values older than an hour are marked `stale`. Claude Code writes this cache itself; when it refreshes it is not documented. A window whose reset time has passed shows `reset`.
238
258
 
239
259
  For fresher numbers, let your status line record them. Claude Code passes the 5-hour and 7-day usage to the `statusLine` command each time it redraws the status line (Pro and Max plans, after the first response of a session). Wrap the `statusLine` command of the settings file that is actually in effect:
240
260
 
@@ -266,6 +286,22 @@ work:
266
286
 
267
287
  `list` shows when each account was last used (`LAST USED`), from the newest session file or prompt history in its directory; a `handoff` into an account counts as use.
268
288
 
289
+ #### Plan and billing
290
+
291
+ ```text
292
+ NAME LOGIN PLAN BILLING PROXY SHARED 5H 7D LAST USED STATUS
293
+ work keychain max (5x) stripe http://127.0.0.1:7890 no 23% 14:00 41% Fri 5m ago ok
294
+ home keychain pro apple inherit no - - 2d ago ok
295
+ old keychain expired - inherit no - - 9d ago ok
296
+ ```
297
+
298
+ `PLAN` and `BILLING` are not cached values: every `list` asks Anthropic (`GET https://api.anthropic.com/api/oauth/profile`, the request Claude Code itself makes) with the account's own access token. `PLAN` is the subscription — `pro`, `max (5x)`, `max (20x)`, `team`, `enterprise`. `BILLING` is where the subscription is paid: `stripe` (claude.ai), `apple` (App Store), `google-play`, `aws-marketplace` and so on.
299
+
300
+ - The request uses the account's proxy setting, exactly like its launcher: the configured proxy, the proxy variables of your shell for `inherit`, none for `off`. If a proxy is configured and fails, the lookup fails; it never falls back to a direct connection. Only `http://` proxies work for the lookup.
301
+ - `expired` means the access token has run out. Tokens last a few hours and multi-claude never refreshes them (refreshing would compete with running sessions): start `claude-NAME` once and run `list` again. `-` means the account has no OAuth login, and `rejected` that the server refused the token.
302
+ - `error` comes with a reason on stderr, for example `plan lookup failed for work: timeout (via 127.0.0.1:7890, 10.01s)`. `tls_failed` usually means the Python that runs multi-claude has no root certificates (python.org installers ship an `Install Certificates.command`).
303
+ - `list --offline` skips the lookup — no token is read, nothing is sent — and prints the table without the two columns. `list --json` carries the same data in each account's `plan` object; scripts should use it instead of parsing the table. The lookup adds about a second, at most 15.
304
+
269
305
  ### Handing a session over to another account
270
306
 
271
307
  Each account keeps its own sessions, so `claude --resume` in one account cannot see the sessions of another. `handoff` copies one session (its `.jsonl` file and the folder next to it with subagent transcripts and saved tool output) into the same project folder of another account:
@@ -400,7 +436,7 @@ By default the numbers come from the cache that Claude Code writes in `.claude.j
400
436
 
401
437
  ### Does multi-claude read or copy my login?
402
438
 
403
- It never reads it: its only keychain call checks whether an item exists, and it never opens `.credentials.json`. It does not export, upload or back up logins. `migrate-default` and `restore` move a whole account directory; on Linux the credentials file is inside it and moves with it (in copy mode it is copied as part of the directory, verified, and the original directory is then removed unless you pass `--keep-backup`), still without being opened. On macOS a login is tied to the account directory's path, which is why `rename` keeps the directory.
439
+ It never writes, copies, exports, uploads, refreshes or backs up a login. One command reads one: `list` takes each account's access token to ask Anthropic for the account's plan (see [Plan and billing](#plan-and-billing)); the token stays in memory and is sent only to `api.anthropic.com`. `list --offline` does not read it. All other commands only check whether a login exists. `migrate-default` and `restore` move a whole account directory; on Linux the credentials file is inside it and moves with it (in copy mode it is copied as part of the directory, verified, and the original directory is then removed unless you pass `--keep-backup`), still without being opened. On macOS a login is tied to the account directory's path, which is why `rename` keeps the directory.
404
440
 
405
441
  ## Reference
406
442
 
@@ -420,7 +456,7 @@ It never reads it: its only keychain call checks whether an item exists, and it
420
456
  | `multi-claude args (NAME \| --defaults) -- [ARG ...]` | Replace the fixed arguments; `--` with nothing after it clears them. |
421
457
  | `multi-claude remove NAME` | Unregister an account and delete its launcher. **The directory and the login are kept.** |
422
458
  | `multi-claude apply [-f FILE]` | Converge everything to the configuration (or to `FILE`). |
423
- | `multi-claude list [--verbose \| --json \| --names]` | Show accounts, launchers and login state. `--json` adds usage and is meant for scripts; `--names` prints only the names. |
459
+ | `multi-claude list [--verbose \| --json \| --names] [--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; `--offline` skips the plan lookup (no token read, no network). |
424
460
  | `multi-claude usage [NAME] [--json]` | Show the last known 5-hour and 7-day usage of each account. |
425
461
  | `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). |
426
462
  | `multi-claude doctor [--json] [--verbose]` | Check the configuration, launchers, account directories, shared links, logins and environment. |
@@ -1,3 +1,3 @@
1
1
  """multi-claude:管理多个 Claude Code 账号的配置目录、启动命令、代理与启动参数。"""
2
2
 
3
- __version__ = "0.8.0"
3
+ __version__ = "0.9.0"
@@ -19,8 +19,8 @@ from datetime import datetime, timedelta, timezone
19
19
  from typing import List, Optional, Tuple
20
20
 
21
21
  from . import (__version__, accounts, bundle, completion, doctor, history, hook, identity, launcher, migrate,
22
- platform, restore, routes, sessions, shellpath, statusline, usage)
23
- from .actions import error, info, warn
22
+ plan, platform, restore, routes, sessions, shellpath, statusline, usage)
23
+ from .actions import LOG_PREFIX, error, info, warn
24
24
  from .config import (DEFAULT_SHARED_DIR, DEFAULT_SHARED_ITEMS, IDENTITY_DEFAULT, IDENTITY_DIR, Account, Config, ConfigError, RouteRule,
25
25
  is_unshareable, load_config, normalize_proxy, parse_config, validate_env_key, validate_name,
26
26
  validate_route_path, validate_value, config_path)
@@ -166,12 +166,15 @@ def build_parser() -> argparse.ArgumentParser:
166
166
  p_apply.add_argument("-f", "--file", help="use this file as the new configuration")
167
167
  _add_write_options(p_apply)
168
168
 
169
- p_list = sub.add_parser("list", description="show accounts and their status")
169
+ p_list = sub.add_parser("list", description="show accounts and their status; looks up each account's plan "
170
+ "and billing channel from Anthropic unless --offline is given")
170
171
  list_mode = p_list.add_mutually_exclusive_group()
171
172
  list_mode.add_argument("--verbose", action="store_true",
172
173
  help="show every field, the global settings and the keychain details")
173
174
  list_mode.add_argument("--json", action="store_true", help="print machine-readable JSON (includes usage)")
174
175
  list_mode.add_argument("--names", action="store_true", help="print only the account names, one per line")
176
+ p_list.add_argument("--offline", action="store_true",
177
+ help="skip the plan lookup: do not read access tokens and do not connect to the network")
175
178
 
176
179
  p_usage = sub.add_parser("usage", description="show the last known 5-hour and 7-day usage of each account "
177
180
  "(from Claude Code's own cache; never reads credentials)")
@@ -397,7 +400,7 @@ def main(argv: Optional[List[str]] = None) -> int:
397
400
  warn(notice)
398
401
  # list 与 usage 只读:不加写锁,迁移未完成时也放行。
399
402
  if args.command == "list":
400
- return cmd_list(verbose=args.verbose, as_json=args.json, names_only=args.names)
403
+ return cmd_list(verbose=args.verbose, as_json=args.json, names_only=args.names, offline=args.offline)
401
404
  if args.command == "usage":
402
405
  if args.history:
403
406
  return cmd_usage_history(args.name, args.days, args.by or "day", args.json)
@@ -979,7 +982,7 @@ _EMAIL_LIKE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
979
982
  def cmd_login(name: str, claude_args: List[str]) -> int:
980
983
  """以账号的身份环境运行 `claude auth login …`(方案 feature-easier-onboarding §5.1.4)。
981
984
 
982
- 凭据由 Claude 自己写入该账号的钥匙串条目或凭据文件,本工具不读写凭据。
985
+ 凭据由 Claude 自己写入该账号的钥匙串条目或凭据文件,本命令不读写凭据。
983
986
  不带账号的固定参数:取多个值的选项会把 `auth login` 当成自己的值吞掉(同 mcp)。
984
987
  """
985
988
  process_env, code = _account_process_env(name)
@@ -1260,7 +1263,8 @@ def _checked_proxy(value: str) -> str:
1260
1263
  raise UsageError(str(exc))
1261
1264
 
1262
1265
 
1263
- def cmd_list(verbose: bool = False, as_json: bool = False, names_only: bool = False) -> int:
1266
+ def cmd_list(verbose: bool = False, as_json: bool = False, names_only: bool = False, offline: bool = False) -> int:
1267
+ """列出账号。除 --names 与 --offline 外,会读各账号的访问令牌并向服务端查询套餐(方案 feature-live-plan)。"""
1264
1268
  config, exists = load_config()
1265
1269
  if names_only:
1266
1270
  # 给补全脚本用:只输出名称,不输出任何告警。
@@ -1271,9 +1275,12 @@ def cmd_list(verbose: bool = False, as_json: bool = False, names_only: bool = Fa
1271
1275
  for variable in _UNMANAGED_PROXY_VARS:
1272
1276
  if variable in os.environ:
1273
1277
  warn("{} is set; launchers do not change it, even with proxy off".format(variable))
1278
+ # 套餐只查一次,表格与 JSON 共用;None 表示本次不查(--offline 或还没有配置)。
1279
+ plans = None if offline or not exists else plan.query_all(config, os.environ, datetime.now(timezone.utc))
1274
1280
  if as_json:
1275
- _print_json(_list_data(config, exists))
1281
+ _print_json(_list_data(config, exists, plans))
1276
1282
  if exists:
1283
+ _report_plan_problems(plans)
1277
1284
  accounts.warn_launch_settings(config, None, note_invalid=True)
1278
1285
  return accounts.EXIT_OK
1279
1286
  if not exists:
@@ -1281,7 +1288,7 @@ def cmd_list(verbose: bool = False, as_json: bool = False, names_only: bool = Fa
1281
1288
  os.path.join(platform.state_dir(), "config.json")))
1282
1289
  return accounts.EXIT_OK
1283
1290
  if not verbose:
1284
- return _print_brief_list(config)
1291
+ return _print_brief_list(config, plans)
1285
1292
  print("root: {}".format(expand(config.root)))
1286
1293
  print("bin_dir: {}".format(expand(config.bin_dir)))
1287
1294
  print("shared.dir: {}".format(expand(config.shared_dir) if config.shared_dir else "(not set)"))
@@ -1290,15 +1297,15 @@ def cmd_list(verbose: bool = False, as_json: bool = False, names_only: bool = Fa
1290
1297
  return accounts.EXIT_OK
1291
1298
  if "CLAUDE_CODE_CUSTOM_OAUTH_URL" in os.environ:
1292
1299
  warn("CLAUDE_CODE_CUSTOM_OAUTH_URL is set; LOGIN assumes the production service and may be wrong")
1293
- rows = [("NAME", "IDENTITY", "DIR", "PROXY", "SHARED", "LAUNCHER", "LOGIN", "LINK")]
1300
+ rows = [("NAME", "IDENTITY", "DIR", "PROXY", "SHARED", "LAUNCHER", "LOGIN") + _plan_header(plans) + ("LINK",)]
1294
1301
  details = []
1295
1302
  default_account = None
1296
1303
  for entry in _account_entries(config):
1297
1304
  if entry["identity"] == IDENTITY_DEFAULT:
1298
1305
  default_account = entry["name"]
1299
1306
  rows.append((entry["name"], entry["identity"], "ok" if entry["dir_exists"] else "missing-dir",
1300
- entry["proxy"], _shared_cell(entry), entry["launcher"], entry["login"],
1301
- entry["link"] or "-"))
1307
+ entry["proxy"], _shared_cell(entry), entry["launcher"], entry["login"])
1308
+ + _plan_cells(plans, entry["name"]) + (entry["link"] or "-",))
1302
1309
  if verbose:
1303
1310
  details.append(" {}: keychain service {!r}, credentials file {}".format(
1304
1311
  entry["name"], entry["keychain_service"], entry["credentials_file"]))
@@ -1309,11 +1316,43 @@ def cmd_list(verbose: bool = False, as_json: bool = False, names_only: bool = Fa
1309
1316
  default_account, os.path.join(os.path.expanduser("~"), ".claude.json")))
1310
1317
  for line in details:
1311
1318
  print(line)
1319
+ _report_plan_problems(plans)
1312
1320
  accounts.warn_launch_settings(config, None, note_invalid=True)
1313
1321
  return accounts.EXIT_OK
1314
1322
 
1315
1323
 
1316
- def _print_brief_list(config: Config) -> int:
1324
+ _PLAN_HEADER = ("PLAN", "BILLING")
1325
+
1326
+
1327
+ def _plan_header(plans: Optional[dict]) -> tuple:
1328
+ """--offline 时不加这两列,表格与改动前完全相同。"""
1329
+ return () if plans is None else _PLAN_HEADER
1330
+
1331
+
1332
+ def _plan_cells(plans: Optional[dict], name: str) -> tuple:
1333
+ if plans is None:
1334
+ return ()
1335
+ return (plan.plan_cell(plans[name]), plan.billing_cell(plans[name]))
1336
+
1337
+
1338
+ def _report_plan_problems(plans: Optional[dict]) -> None:
1339
+ """套餐查询的失败与过期提示,打到 stderr;不影响表格、STATUS 列与退出码。"""
1340
+ if plans is None:
1341
+ return
1342
+ expired = []
1343
+ for name, result in plans.items():
1344
+ if result.status == plan.STATUS_ERROR:
1345
+ warn(plan.failure_text(name, result))
1346
+ elif result.status == plan.STATUS_EXPIRED:
1347
+ expired.append(name)
1348
+ if expired:
1349
+ # multi-claude 不刷新令牌:让 Claude Code 自己续期,再查一次即可。
1350
+ # 走 stderr 而不是 info():info() 写 stdout,会混进 --json 的输出。
1351
+ print("{} expired: start {} once to renew the login, then run list again".format(
1352
+ LOG_PREFIX, ", ".join("claude-" + name for name in expired)), file=sys.stderr)
1353
+
1354
+
1355
+ def _print_brief_list(config: Config, plans: Optional[dict] = None) -> int:
1317
1356
  """list 的默认输出:只放新用户关心的列(方案 feature-clearer-help §5.1.2);完整信息见 --verbose。"""
1318
1357
  if not config.accounts:
1319
1358
  print("no accounts registered; run `multi-claude add NAME` to start")
@@ -1322,19 +1361,21 @@ def _print_brief_list(config: Config) -> int:
1322
1361
  # 与完整输出相同:LOGIN 与由它得出的 STATUS 都按正式服务探测,这时可能不准。
1323
1362
  warn("CLAUDE_CODE_CUSTOM_OAUTH_URL is set; LOGIN assumes the production service and may be wrong")
1324
1363
  now = datetime.now(timezone.utc)
1325
- rows = [("NAME", "LOGIN", "PROXY", "SHARED", "5H", "7D", "LAST USED", "STATUS")]
1364
+ rows = [("NAME", "LOGIN") + _plan_header(plans) + ("PROXY", "SHARED", "5H", "7D", "LAST USED", "STATUS")]
1326
1365
  has_problem = False
1327
1366
  for entry in _account_entries(config):
1328
1367
  report = usage.read_account_usage(config, config.find(entry["name"]), now)
1329
1368
  status = _status_cell(entry)
1330
1369
  has_problem = has_problem or status != "ok"
1331
- rows.append((entry["name"], entry["login"], entry["proxy"], _shared_cell(entry),
1332
- _window_cell(report.five_hour, now), _window_cell(report.seven_day, now),
1333
- _last_used_cell(entry["last_used"], now), status))
1370
+ rows.append((entry["name"], entry["login"]) + _plan_cells(plans, entry["name"])
1371
+ + (entry["proxy"], _shared_cell(entry),
1372
+ _window_cell(report.five_hour, now), _window_cell(report.seven_day, now),
1373
+ _last_used_cell(entry["last_used"], now), status))
1334
1374
  _print_table(rows)
1335
1375
  _print_routes(config)
1336
1376
  if has_problem:
1337
1377
  print("run multi-claude doctor for details")
1378
+ _report_plan_problems(plans)
1338
1379
  accounts.warn_launch_settings(config, None, note_invalid=True)
1339
1380
  return accounts.EXIT_OK
1340
1381
 
@@ -1377,7 +1418,7 @@ def _shared_cell(entry: dict) -> str:
1377
1418
  return "yes"
1378
1419
 
1379
1420
 
1380
- def _account_entries(config: Config, with_usage: bool = False) -> List[dict]:
1421
+ def _account_entries(config: Config, with_usage: bool = False, plans: Optional[dict] = None) -> List[dict]:
1381
1422
  """list 的表格与 JSON 共用的每账号数据;表格的列取自这里,保证两种输出口径一致。"""
1382
1423
  now = datetime.now(timezone.utc)
1383
1424
  entries = []
@@ -1402,11 +1443,14 @@ def _account_entries(config: Config, with_usage: bool = False) -> List[dict]:
1402
1443
  }
1403
1444
  if with_usage:
1404
1445
  entry["usage"] = usage.report_to_dict(usage.read_account_usage(config, account, now), now)
1446
+ if plans is not None:
1447
+ # 本次没查套餐(--offline)时不加这个键,JSON 与改动前相同
1448
+ entry["plan"] = plan.to_dict(plans[name])
1405
1449
  entries.append(entry)
1406
1450
  return entries
1407
1451
 
1408
1452
 
1409
- def _list_data(config: Config, exists: bool) -> dict:
1453
+ def _list_data(config: Config, exists: bool, plans: Optional[dict] = None) -> dict:
1410
1454
  if not exists:
1411
1455
  return {"schema_version": 1, "configured": False, "accounts": [],
1412
1456
  "routes": {"default": None, "rules": []}}
@@ -1416,7 +1460,7 @@ def _list_data(config: Config, exists: bool) -> dict:
1416
1460
  "root": expand(config.root),
1417
1461
  "bin_dir": expand(config.bin_dir),
1418
1462
  "shared_dir": expand(config.shared_dir) if config.shared_dir else None,
1419
- "accounts": _account_entries(config, with_usage=True),
1463
+ "accounts": _account_entries(config, with_usage=True, plans=plans),
1420
1464
  "routes": {"default": config.route_default,
1421
1465
  "rules": [{"path": rule.path, "account": rule.account, "exists": os.path.isdir(expand(rule.path))}
1422
1466
  for rule in config.route_rules]},
@@ -1463,7 +1507,7 @@ def cmd_usage(name: Optional[str], as_json: bool) -> int:
1463
1507
  for row in rows:
1464
1508
  print(" ".join(cell.ljust(width) for cell, width in zip(row, widths)).rstrip())
1465
1509
  print("Values come from Claude Code's own cache (.claude.json) or the statusline hook, and may be out of date; "
1466
- "multi-claude never reads credentials.")
1510
+ "this command never reads credentials.")
1467
1511
  return accounts.EXIT_OK
1468
1512
 
1469
1513
 
@@ -1555,7 +1599,7 @@ def cmd_usage_history(name: Optional[str], days: Optional[int], by: str, as_json
1555
1599
  if len(per_account) > 1:
1556
1600
  _print_history_block("TOTAL (each reply counted once)", total, by, days)
1557
1601
  print("Counted from the session files on this machine (not other machines, not deleted sessions); "
1558
- "multi-claude reads only the token counts, never connects to the network and never reads credentials.")
1602
+ "this command reads only the token counts, never connects to the network and never reads credentials.")
1559
1603
  return accounts.EXIT_OK
1560
1604
 
1561
1605