multi-claude-cli 0.8.0__tar.gz → 0.9.1__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.8.0 → multi_claude_cli-0.9.1}/PKG-INFO +49 -11
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/README.md +48 -10
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/__init__.py +1 -1
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/cli.py +79 -21
- multi_claude_cli-0.9.1/src/multi_claude/identity.py +285 -0
- multi_claude_cli-0.9.1/src/multi_claude/plan.py +540 -0
- multi_claude_cli-0.9.1/src/multi_claude/refresh.py +326 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude_cli.egg-info/PKG-INFO +49 -11
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude_cli.egg-info/SOURCES.txt +4 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_help.py +3 -3
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_migrate.py +1 -1
- multi_claude_cli-0.9.1/tests/test_plan.py +482 -0
- multi_claude_cli-0.9.1/tests/test_refresh.py +574 -0
- multi_claude_cli-0.8.0/src/multi_claude/identity.py +0 -94
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/LICENSE +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/pyproject.toml +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/setup.cfg +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/__main__.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/accounts.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/actions.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/bundle.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/completion.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/config.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/doctor.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/env.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/fsutil.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/history.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/hook.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/launcher.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/lock.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/migrate.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/platform.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/restore.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/routes.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/sessions.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/shared.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/shellpath.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/statusline.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude/usage.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude_cli.egg-info/dependency_links.txt +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude_cli.egg-info/entry_points.txt +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/src/multi_claude_cli.egg-info/top_level.txt +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_accounts.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_bundle.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_completion.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_config.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_doctor.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_everyday.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_fsutil.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_handoff.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_history.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_identity.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_install.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_mcp.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_onboarding.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_packaging.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_platform.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_rename.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_routes.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_set.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_shared_exclude.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_statusline.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_usability.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/tests/test_usage.py +0 -0
- {multi_claude_cli-0.8.0 → multi_claude_cli-0.9.1}/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.9.1
|
|
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
|
|
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.
|
|
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
|
-
- **
|
|
68
|
-
- **
|
|
67
|
+
- **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` goes online** — its plan lookup (one request per account, plus one to renew an expired token) 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
|
-
##
|
|
79
|
+
## Prerequisites
|
|
80
80
|
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
|
|
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.
|
|
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`).
|
|
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,24 @@ 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 token 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
|
+
- Access tokens last a few hours. When one has expired, `list` renews it first: it takes the same lock as Claude Code (`.oauth_refresh.lock` in the account directory), posts the refresh token to `https://platform.claude.com/v1/oauth/token`, and saves the new tokens where the old ones were, keeping everything else in the login untouched. A running Claude session picks the new token up by itself. If another process has just renewed the token, `list` uses that result and writes nothing; if another process holds the lock, `list` does not wait and shows `token expired` this time. This follows what Claude Code 2.1.295 does; it is not a documented interface.
|
|
329
|
+
- `token expired` means the access token has run out and could not be renewed; the subscription itself is not affected. The reason is on stderr (`token refresh failed for work: lock_busy`). Start `claude-NAME` once and run `list` again; when the reason is `refresh_rejected` or `no_refresh_token`, log in again with `multi-claude login NAME`. `list --no-refresh` never renews and never writes to a login.
|
|
330
|
+
- `-` means the account has no OAuth login, and `rejected` that the server refused the token.
|
|
331
|
+
- `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`).
|
|
332
|
+
- `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 (`refresh` tells whether a token was renewed); scripts should use it instead of parsing the table. The lookup adds about a second, at most 15; a renewal that is under way is allowed to finish first (the first Ctrl-C only prints a notice), so that the new login is not lost.
|
|
333
|
+
|
|
296
334
|
### Handing a session over to another account
|
|
297
335
|
|
|
298
336
|
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 +465,7 @@ By default the numbers come from the cache that Claude Code writes in `.claude.j
|
|
|
427
465
|
|
|
428
466
|
### Does multi-claude read or copy my login?
|
|
429
467
|
|
|
430
|
-
It never
|
|
468
|
+
It never copies, exports, uploads or backs up a login. One command touches one: `list` takes each account's access token to ask Anthropic for the account's plan, and renews the token when it has expired, saving the new one where the old one was (see [Plan and billing](#plan-and-billing)). Tokens stay in memory and are sent only to Anthropic. `list --no-refresh` never writes and `list --offline` does not read. 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
469
|
|
|
432
470
|
## Reference
|
|
433
471
|
|
|
@@ -447,7 +485,7 @@ It never reads it: its only keychain call checks whether an item exists, and it
|
|
|
447
485
|
| `multi-claude args (NAME \| --defaults) -- [ARG ...]` | Replace the fixed arguments; `--` with nothing after it clears them. |
|
|
448
486
|
| `multi-claude remove NAME` | Unregister an account and delete its launcher. **The directory and the login are kept.** |
|
|
449
487
|
| `multi-claude apply [-f FILE]` | Converge everything to the configuration (or to `FILE`). |
|
|
450
|
-
| `multi-claude list [--verbose \| --json \| --names]` | Show accounts, launchers and
|
|
488
|
+
| `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). |
|
|
451
489
|
| `multi-claude usage [NAME] [--json]` | Show the last known 5-hour and 7-day usage of each account. |
|
|
452
490
|
| `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
491
|
| `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
|
|
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
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
|
-
- **
|
|
41
|
-
- **
|
|
40
|
+
- **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` goes online** — its plan lookup (one request per account, plus one to renew an expired token) 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
|
-
##
|
|
52
|
+
## Prerequisites
|
|
53
53
|
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
|
|
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`).
|
|
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,24 @@ 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 token 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
|
+
- Access tokens last a few hours. When one has expired, `list` renews it first: it takes the same lock as Claude Code (`.oauth_refresh.lock` in the account directory), posts the refresh token to `https://platform.claude.com/v1/oauth/token`, and saves the new tokens where the old ones were, keeping everything else in the login untouched. A running Claude session picks the new token up by itself. If another process has just renewed the token, `list` uses that result and writes nothing; if another process holds the lock, `list` does not wait and shows `token expired` this time. This follows what Claude Code 2.1.295 does; it is not a documented interface.
|
|
302
|
+
- `token expired` means the access token has run out and could not be renewed; the subscription itself is not affected. The reason is on stderr (`token refresh failed for work: lock_busy`). Start `claude-NAME` once and run `list` again; when the reason is `refresh_rejected` or `no_refresh_token`, log in again with `multi-claude login NAME`. `list --no-refresh` never renews and never writes to a login.
|
|
303
|
+
- `-` means the account has no OAuth login, and `rejected` that the server refused the token.
|
|
304
|
+
- `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`).
|
|
305
|
+
- `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 (`refresh` tells whether a token was renewed); scripts should use it instead of parsing the table. The lookup adds about a second, at most 15; a renewal that is under way is allowed to finish first (the first Ctrl-C only prints a notice), so that the new login is not lost.
|
|
306
|
+
|
|
269
307
|
### Handing a session over to another account
|
|
270
308
|
|
|
271
309
|
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 +438,7 @@ By default the numbers come from the cache that Claude Code writes in `.claude.j
|
|
|
400
438
|
|
|
401
439
|
### Does multi-claude read or copy my login?
|
|
402
440
|
|
|
403
|
-
It never
|
|
441
|
+
It never copies, exports, uploads or backs up a login. One command touches one: `list` takes each account's access token to ask Anthropic for the account's plan, and renews the token when it has expired, saving the new one where the old one was (see [Plan and billing](#plan-and-billing)). Tokens stay in memory and are sent only to Anthropic. `list --no-refresh` never writes and `list --offline` does not read. 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
442
|
|
|
405
443
|
## Reference
|
|
406
444
|
|
|
@@ -420,7 +458,7 @@ It never reads it: its only keychain call checks whether an item exists, and it
|
|
|
420
458
|
| `multi-claude args (NAME \| --defaults) -- [ARG ...]` | Replace the fixed arguments; `--` with nothing after it clears them. |
|
|
421
459
|
| `multi-claude remove NAME` | Unregister an account and delete its launcher. **The directory and the login are kept.** |
|
|
422
460
|
| `multi-claude apply [-f FILE]` | Converge everything to the configuration (or to `FILE`). |
|
|
423
|
-
| `multi-claude list [--verbose \| --json \| --names]` | Show accounts, launchers and
|
|
461
|
+
| `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). |
|
|
424
462
|
| `multi-claude usage [NAME] [--json]` | Show the last known 5-hour and 7-day usage of each account. |
|
|
425
463
|
| `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
464
|
| `multi-claude doctor [--json] [--verbose]` | Check the configuration, launchers, account directories, shared links, logins and environment. |
|
|
@@ -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, refresh, 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,18 @@ 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("--no-refresh", action="store_true",
|
|
177
|
+
help="do not renew an expired access token (never write to the login); "
|
|
178
|
+
"such accounts show 'token expired'")
|
|
179
|
+
p_list.add_argument("--offline", action="store_true",
|
|
180
|
+
help="skip the plan lookup: do not read access tokens and do not connect to the network")
|
|
175
181
|
|
|
176
182
|
p_usage = sub.add_parser("usage", description="show the last known 5-hour and 7-day usage of each account "
|
|
177
183
|
"(from Claude Code's own cache; never reads credentials)")
|
|
@@ -397,7 +403,8 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
397
403
|
warn(notice)
|
|
398
404
|
# list 与 usage 只读:不加写锁,迁移未完成时也放行。
|
|
399
405
|
if args.command == "list":
|
|
400
|
-
return cmd_list(verbose=args.verbose, as_json=args.json, names_only=args.names
|
|
406
|
+
return cmd_list(verbose=args.verbose, as_json=args.json, names_only=args.names, offline=args.offline,
|
|
407
|
+
refresh_tokens=not args.no_refresh)
|
|
401
408
|
if args.command == "usage":
|
|
402
409
|
if args.history:
|
|
403
410
|
return cmd_usage_history(args.name, args.days, args.by or "day", args.json)
|
|
@@ -979,7 +986,7 @@ _EMAIL_LIKE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
|
979
986
|
def cmd_login(name: str, claude_args: List[str]) -> int:
|
|
980
987
|
"""以账号的身份环境运行 `claude auth login …`(方案 feature-easier-onboarding §5.1.4)。
|
|
981
988
|
|
|
982
|
-
凭据由 Claude
|
|
989
|
+
凭据由 Claude 自己写入该账号的钥匙串条目或凭据文件,本命令不读写凭据。
|
|
983
990
|
不带账号的固定参数:取多个值的选项会把 `auth login` 当成自己的值吞掉(同 mcp)。
|
|
984
991
|
"""
|
|
985
992
|
process_env, code = _account_process_env(name)
|
|
@@ -1260,7 +1267,10 @@ def _checked_proxy(value: str) -> str:
|
|
|
1260
1267
|
raise UsageError(str(exc))
|
|
1261
1268
|
|
|
1262
1269
|
|
|
1263
|
-
def cmd_list(verbose: bool = False, as_json: bool = False, names_only: bool = False
|
|
1270
|
+
def cmd_list(verbose: bool = False, as_json: bool = False, names_only: bool = False, offline: bool = False,
|
|
1271
|
+
refresh_tokens: bool = True) -> int:
|
|
1272
|
+
"""列出账号。除 --names 与 --offline 外,会读各账号的访问令牌并向服务端查询套餐(方案 feature-live-plan);
|
|
1273
|
+
访问令牌过期时先刷新并写回凭据,refresh_tokens 为假时不刷新(方案 feature-token-refresh)。"""
|
|
1264
1274
|
config, exists = load_config()
|
|
1265
1275
|
if names_only:
|
|
1266
1276
|
# 给补全脚本用:只输出名称,不输出任何告警。
|
|
@@ -1271,9 +1281,13 @@ def cmd_list(verbose: bool = False, as_json: bool = False, names_only: bool = Fa
|
|
|
1271
1281
|
for variable in _UNMANAGED_PROXY_VARS:
|
|
1272
1282
|
if variable in os.environ:
|
|
1273
1283
|
warn("{} is set; launchers do not change it, even with proxy off".format(variable))
|
|
1284
|
+
# 套餐只查一次,表格与 JSON 共用;None 表示本次不查(--offline 或还没有配置)。
|
|
1285
|
+
plans = None if offline or not exists else plan.query_all(config, os.environ, datetime.now(timezone.utc),
|
|
1286
|
+
allow_refresh=refresh_tokens)
|
|
1274
1287
|
if as_json:
|
|
1275
|
-
_print_json(_list_data(config, exists))
|
|
1288
|
+
_print_json(_list_data(config, exists, plans))
|
|
1276
1289
|
if exists:
|
|
1290
|
+
_report_plan_problems(plans)
|
|
1277
1291
|
accounts.warn_launch_settings(config, None, note_invalid=True)
|
|
1278
1292
|
return accounts.EXIT_OK
|
|
1279
1293
|
if not exists:
|
|
@@ -1281,7 +1295,7 @@ def cmd_list(verbose: bool = False, as_json: bool = False, names_only: bool = Fa
|
|
|
1281
1295
|
os.path.join(platform.state_dir(), "config.json")))
|
|
1282
1296
|
return accounts.EXIT_OK
|
|
1283
1297
|
if not verbose:
|
|
1284
|
-
return _print_brief_list(config)
|
|
1298
|
+
return _print_brief_list(config, plans)
|
|
1285
1299
|
print("root: {}".format(expand(config.root)))
|
|
1286
1300
|
print("bin_dir: {}".format(expand(config.bin_dir)))
|
|
1287
1301
|
print("shared.dir: {}".format(expand(config.shared_dir) if config.shared_dir else "(not set)"))
|
|
@@ -1290,15 +1304,15 @@ def cmd_list(verbose: bool = False, as_json: bool = False, names_only: bool = Fa
|
|
|
1290
1304
|
return accounts.EXIT_OK
|
|
1291
1305
|
if "CLAUDE_CODE_CUSTOM_OAUTH_URL" in os.environ:
|
|
1292
1306
|
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"
|
|
1307
|
+
rows = [("NAME", "IDENTITY", "DIR", "PROXY", "SHARED", "LAUNCHER", "LOGIN") + _plan_header(plans) + ("LINK",)]
|
|
1294
1308
|
details = []
|
|
1295
1309
|
default_account = None
|
|
1296
1310
|
for entry in _account_entries(config):
|
|
1297
1311
|
if entry["identity"] == IDENTITY_DEFAULT:
|
|
1298
1312
|
default_account = entry["name"]
|
|
1299
1313
|
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
|
-
|
|
1314
|
+
entry["proxy"], _shared_cell(entry), entry["launcher"], entry["login"])
|
|
1315
|
+
+ _plan_cells(plans, entry["name"]) + (entry["link"] or "-",))
|
|
1302
1316
|
if verbose:
|
|
1303
1317
|
details.append(" {}: keychain service {!r}, credentials file {}".format(
|
|
1304
1318
|
entry["name"], entry["keychain_service"], entry["credentials_file"]))
|
|
@@ -1309,11 +1323,50 @@ def cmd_list(verbose: bool = False, as_json: bool = False, names_only: bool = Fa
|
|
|
1309
1323
|
default_account, os.path.join(os.path.expanduser("~"), ".claude.json")))
|
|
1310
1324
|
for line in details:
|
|
1311
1325
|
print(line)
|
|
1326
|
+
_report_plan_problems(plans)
|
|
1312
1327
|
accounts.warn_launch_settings(config, None, note_invalid=True)
|
|
1313
1328
|
return accounts.EXIT_OK
|
|
1314
1329
|
|
|
1315
1330
|
|
|
1316
|
-
|
|
1331
|
+
_PLAN_HEADER = ("PLAN", "BILLING")
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def _plan_header(plans: Optional[dict]) -> tuple:
|
|
1335
|
+
"""--offline 时不加这两列,表格与改动前完全相同。"""
|
|
1336
|
+
return () if plans is None else _PLAN_HEADER
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def _plan_cells(plans: Optional[dict], name: str) -> tuple:
|
|
1340
|
+
if plans is None:
|
|
1341
|
+
return ()
|
|
1342
|
+
return (plan.plan_cell(plans[name]), plan.billing_cell(plans[name]))
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
def _report_plan_problems(plans: Optional[dict]) -> None:
|
|
1346
|
+
"""套餐查询与令牌刷新的失败提示,打到 stderr;不影响表格、STATUS 列与退出码。"""
|
|
1347
|
+
if plans is None:
|
|
1348
|
+
return
|
|
1349
|
+
expired = []
|
|
1350
|
+
for name, result in plans.items():
|
|
1351
|
+
if result.refresh == refresh.SAVE_FAILED:
|
|
1352
|
+
# 新令牌只用了这一次、没存下来:旧的刷新令牌多半已作废,下次启动可能要重新登录。
|
|
1353
|
+
error("the renewed login of {0} could not be saved; if claude-{0} asks you to log in, "
|
|
1354
|
+
"run multi-claude login {0}".format(name))
|
|
1355
|
+
if result.status == plan.STATUS_ERROR:
|
|
1356
|
+
warn(plan.failure_text(name, result))
|
|
1357
|
+
elif result.status == plan.STATUS_EXPIRED:
|
|
1358
|
+
expired.append(name)
|
|
1359
|
+
if result.refresh is not None:
|
|
1360
|
+
hint = "; run multi-claude login {}".format(name) if result.refresh in refresh.NEEDS_LOGIN else ""
|
|
1361
|
+
warn("token refresh failed for {}: {}{}".format(name, result.refresh, hint))
|
|
1362
|
+
if expired:
|
|
1363
|
+
# 走 stderr 而不是 info():info() 写 stdout,会混进 --json 的输出。
|
|
1364
|
+
print("{} token expired: the access token ran out, the subscription is not affected; start {} once, "
|
|
1365
|
+
"then run list again".format(LOG_PREFIX, ", ".join("claude-" + name for name in expired)),
|
|
1366
|
+
file=sys.stderr)
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
def _print_brief_list(config: Config, plans: Optional[dict] = None) -> int:
|
|
1317
1370
|
"""list 的默认输出:只放新用户关心的列(方案 feature-clearer-help §5.1.2);完整信息见 --verbose。"""
|
|
1318
1371
|
if not config.accounts:
|
|
1319
1372
|
print("no accounts registered; run `multi-claude add NAME` to start")
|
|
@@ -1322,19 +1375,21 @@ def _print_brief_list(config: Config) -> int:
|
|
|
1322
1375
|
# 与完整输出相同:LOGIN 与由它得出的 STATUS 都按正式服务探测,这时可能不准。
|
|
1323
1376
|
warn("CLAUDE_CODE_CUSTOM_OAUTH_URL is set; LOGIN assumes the production service and may be wrong")
|
|
1324
1377
|
now = datetime.now(timezone.utc)
|
|
1325
|
-
rows = [("NAME", "LOGIN"
|
|
1378
|
+
rows = [("NAME", "LOGIN") + _plan_header(plans) + ("PROXY", "SHARED", "5H", "7D", "LAST USED", "STATUS")]
|
|
1326
1379
|
has_problem = False
|
|
1327
1380
|
for entry in _account_entries(config):
|
|
1328
1381
|
report = usage.read_account_usage(config, config.find(entry["name"]), now)
|
|
1329
1382
|
status = _status_cell(entry)
|
|
1330
1383
|
has_problem = has_problem or status != "ok"
|
|
1331
|
-
rows.append((entry["name"], entry["login"], entry["
|
|
1332
|
-
|
|
1333
|
-
|
|
1384
|
+
rows.append((entry["name"], entry["login"]) + _plan_cells(plans, entry["name"])
|
|
1385
|
+
+ (entry["proxy"], _shared_cell(entry),
|
|
1386
|
+
_window_cell(report.five_hour, now), _window_cell(report.seven_day, now),
|
|
1387
|
+
_last_used_cell(entry["last_used"], now), status))
|
|
1334
1388
|
_print_table(rows)
|
|
1335
1389
|
_print_routes(config)
|
|
1336
1390
|
if has_problem:
|
|
1337
1391
|
print("run multi-claude doctor for details")
|
|
1392
|
+
_report_plan_problems(plans)
|
|
1338
1393
|
accounts.warn_launch_settings(config, None, note_invalid=True)
|
|
1339
1394
|
return accounts.EXIT_OK
|
|
1340
1395
|
|
|
@@ -1377,7 +1432,7 @@ def _shared_cell(entry: dict) -> str:
|
|
|
1377
1432
|
return "yes"
|
|
1378
1433
|
|
|
1379
1434
|
|
|
1380
|
-
def _account_entries(config: Config, with_usage: bool = False) -> List[dict]:
|
|
1435
|
+
def _account_entries(config: Config, with_usage: bool = False, plans: Optional[dict] = None) -> List[dict]:
|
|
1381
1436
|
"""list 的表格与 JSON 共用的每账号数据;表格的列取自这里,保证两种输出口径一致。"""
|
|
1382
1437
|
now = datetime.now(timezone.utc)
|
|
1383
1438
|
entries = []
|
|
@@ -1402,11 +1457,14 @@ def _account_entries(config: Config, with_usage: bool = False) -> List[dict]:
|
|
|
1402
1457
|
}
|
|
1403
1458
|
if with_usage:
|
|
1404
1459
|
entry["usage"] = usage.report_to_dict(usage.read_account_usage(config, account, now), now)
|
|
1460
|
+
if plans is not None:
|
|
1461
|
+
# 本次没查套餐(--offline)时不加这个键,JSON 与改动前相同
|
|
1462
|
+
entry["plan"] = plan.to_dict(plans[name])
|
|
1405
1463
|
entries.append(entry)
|
|
1406
1464
|
return entries
|
|
1407
1465
|
|
|
1408
1466
|
|
|
1409
|
-
def _list_data(config: Config, exists: bool) -> dict:
|
|
1467
|
+
def _list_data(config: Config, exists: bool, plans: Optional[dict] = None) -> dict:
|
|
1410
1468
|
if not exists:
|
|
1411
1469
|
return {"schema_version": 1, "configured": False, "accounts": [],
|
|
1412
1470
|
"routes": {"default": None, "rules": []}}
|
|
@@ -1416,7 +1474,7 @@ def _list_data(config: Config, exists: bool) -> dict:
|
|
|
1416
1474
|
"root": expand(config.root),
|
|
1417
1475
|
"bin_dir": expand(config.bin_dir),
|
|
1418
1476
|
"shared_dir": expand(config.shared_dir) if config.shared_dir else None,
|
|
1419
|
-
"accounts": _account_entries(config, with_usage=True),
|
|
1477
|
+
"accounts": _account_entries(config, with_usage=True, plans=plans),
|
|
1420
1478
|
"routes": {"default": config.route_default,
|
|
1421
1479
|
"rules": [{"path": rule.path, "account": rule.account, "exists": os.path.isdir(expand(rule.path))}
|
|
1422
1480
|
for rule in config.route_rules]},
|
|
@@ -1463,7 +1521,7 @@ def cmd_usage(name: Optional[str], as_json: bool) -> int:
|
|
|
1463
1521
|
for row in rows:
|
|
1464
1522
|
print(" ".join(cell.ljust(width) for cell, width in zip(row, widths)).rstrip())
|
|
1465
1523
|
print("Values come from Claude Code's own cache (.claude.json) or the statusline hook, and may be out of date; "
|
|
1466
|
-
"
|
|
1524
|
+
"this command never reads credentials.")
|
|
1467
1525
|
return accounts.EXIT_OK
|
|
1468
1526
|
|
|
1469
1527
|
|
|
@@ -1555,7 +1613,7 @@ def cmd_usage_history(name: Optional[str], days: Optional[int], by: str, as_json
|
|
|
1555
1613
|
if len(per_account) > 1:
|
|
1556
1614
|
_print_history_block("TOTAL (each reply counted once)", total, by, days)
|
|
1557
1615
|
print("Counted from the session files on this machine (not other machines, not deleted sessions); "
|
|
1558
|
-
"
|
|
1616
|
+
"this command reads only the token counts, never connects to the network and never reads credentials.")
|
|
1559
1617
|
return accounts.EXIT_OK
|
|
1560
1618
|
|
|
1561
1619
|
|