multi-claude-cli 0.5.0__tar.gz → 0.7.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.5.0/src/multi_claude_cli.egg-info → multi_claude_cli-0.7.0}/PKG-INFO +46 -4
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/README.md +45 -3
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/__init__.py +1 -1
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/cli.py +176 -13
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/completion.py +1 -1
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/doctor.py +2 -1
- multi_claude_cli-0.7.0/src/multi_claude/history.py +178 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0/src/multi_claude_cli.egg-info}/PKG-INFO +46 -4
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude_cli.egg-info/SOURCES.txt +4 -1
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_help.py +2 -2
- multi_claude_cli-0.7.0/tests/test_history.py +194 -0
- multi_claude_cli-0.7.0/tests/test_vscode.py +127 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/LICENSE +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/pyproject.toml +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/setup.cfg +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/__main__.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/accounts.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/actions.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/config.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/env.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/fsutil.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/hook.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/identity.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/launcher.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/lock.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/migrate.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/platform.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/restore.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/routes.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/sessions.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/shared.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/shellpath.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/statusline.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/usage.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude_cli.egg-info/dependency_links.txt +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude_cli.egg-info/entry_points.txt +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude_cli.egg-info/top_level.txt +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_accounts.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_completion.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_config.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_doctor.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_everyday.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_fsutil.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_handoff.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_identity.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_install.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_mcp.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_migrate.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_onboarding.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_packaging.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_platform.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_rename.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_routes.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_set.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_shared_exclude.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_statusline.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_usability.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_usage.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.7.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,6 +37,8 @@ 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.
|
|
41
|
+
|
|
40
42
|
```sh
|
|
41
43
|
claude-work # Claude Code with your work account
|
|
42
44
|
claude-personal # Claude Code with your personal account, in another terminal
|
|
@@ -62,6 +64,10 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
|
|
|
62
64
|
|
|
63
65
|
## Features
|
|
64
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.
|
|
69
|
+
- **Leaves your shell alone** — when `PATH` needs a change, it prints the line to add instead of editing `~/.zshrc` or `~/.bashrc`.
|
|
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`).
|
|
65
71
|
- **Migrate the default account** — move your existing `~/.claude` into the account root without logging out. `claude` keeps working exactly as before.
|
|
66
72
|
- **Add accounts** — create an account directory and a `claude-<name>` launcher, or adopt a directory you already have.
|
|
67
73
|
- **Per-account proxy** — a local port, an HTTP(S) proxy URL, `off`, or `inherit`.
|
|
@@ -69,7 +75,6 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
|
|
|
69
75
|
- **One-step deployment** — `install.sh --config accounts.json` installs the tool and creates every account in the file.
|
|
70
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.
|
|
71
77
|
- **Optional shared resources** — link `CLAUDE.md`, `skills`, `agents` and `commands` from one shared directory into selected accounts.
|
|
72
|
-
- **Hands off your credentials** — multi-claude never reads, copies or deletes logins. Its only keychain call checks whether an item exists.
|
|
73
78
|
|
|
74
79
|
## Requirements
|
|
75
80
|
|
|
@@ -245,6 +250,23 @@ The command becomes `<path of multi-claude> statusline-hook '<original command>'
|
|
|
245
250
|
- If several settings files define `statusLine`, wrap the one that wins (for example a file passed with `--settings` in the launcher arguments).
|
|
246
251
|
- Run `multi-claude statusline uninstall FILE` before uninstalling multi-claude; otherwise the status line stays empty because the wrapped command cannot be found. Running `install` again after multi-claude moves updates the path.
|
|
247
252
|
|
|
253
|
+
#### Token history
|
|
254
|
+
|
|
255
|
+
```sh
|
|
256
|
+
multi-claude usage --history # tokens per day, last 7 days, every account
|
|
257
|
+
multi-claude usage --history work --days 30 --by model
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
```text
|
|
261
|
+
work:
|
|
262
|
+
DATE INPUT OUTPUT CACHE READ CACHE WRITE REPLIES
|
|
263
|
+
2026-10-01 1,246 499,681 273,254,361 5,926,527 612
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`--history` counts the replies in the account's session files on this machine (`projects/**/*.jsonl`, including subagents): input, output, cache-read and cache-write tokens per day (local time) or per model. Only these counters are read; nothing else from the conversations is printed or stored, and nothing is sent anywhere. Sessions on other machines and deleted sessions are not counted. A session copied with `handoff` appears in both accounts; the `TOTAL` block counts each reply once. `--json` prints the same numbers for scripts.
|
|
267
|
+
|
|
268
|
+
`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.
|
|
269
|
+
|
|
248
270
|
### Handing a session over to another account
|
|
249
271
|
|
|
250
272
|
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:
|
|
@@ -286,6 +308,22 @@ multi-claude add client-b --config-from work # start client-b with a copy of wor
|
|
|
286
308
|
|
|
287
309
|
`--config-from` copies `settings.json` once; afterwards the two files are independent. A different existing `settings.json` in the new account, or a `settings.json` that the account shares, is a conflict and nothing is changed. Use [shared resources](#shared-resources) for settings that should stay the same in every account.
|
|
288
310
|
|
|
311
|
+
### Opening VS Code for an account (experimental)
|
|
312
|
+
|
|
313
|
+
```sh
|
|
314
|
+
multi-claude code work ~/projects/app # a separate VS Code window for work
|
|
315
|
+
multi-claude code personal . -- --disable-gpu # arguments after -- go to VS Code
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The Claude Code extension uses the `CLAUDE_CONFIG_DIR` of the environment VS Code was started with, so a VS Code opened from the Dock always uses `~/.claude`. `code` starts VS Code with the account's environment (the same as `run`) and a separate user data directory, `<root>/.apps/<account directory>/vscode`. A separate user data directory is required: otherwise VS Code hands the request to the instance that is already running, with its own environment. Each account's instance starts with default VS Code settings; extensions are shared because they live in `~/.vscode/extensions`.
|
|
319
|
+
|
|
320
|
+
Notes:
|
|
321
|
+
|
|
322
|
+
- If an instance for the account is already running, the request goes to it; changes to the proxy or environment variables apply after that instance quits.
|
|
323
|
+
- Setting `CLAUDE_CONFIG_DIR` in the extension's `claudeCode.environmentVariables` inside that instance overrides the account.
|
|
324
|
+
- On macOS the `code` command passes the whole environment to `open --env`, so the values are briefly visible in the process list.
|
|
325
|
+
- Verified with VS Code 1.139.1 and Claude Code extension 2.1.286 on macOS; `code` prints a warning because it relies on undocumented behaviour. It needs the `code` command on `PATH` (in VS Code: "Shell Command: Install 'code' command in PATH").
|
|
326
|
+
|
|
289
327
|
### Diagnostics and completion
|
|
290
328
|
|
|
291
329
|
`multi-claude doctor` checks, without changing anything: the configuration and any unfinished migration, whether `claude` and the launcher directory are on `PATH`, whether each launcher is up to date and not hidden by another file of the same name, whether each account directory exists and is private (`0700`), the default account's link, broken shared links, whether a login exists, and variables such as `ANTHROPIC_API_KEY` that override the accounts' logins. Every problem comes with the command that fixes it. It exits with 1 when there is an error; `--json` prints the results for scripts.
|
|
@@ -347,6 +385,8 @@ With pipx, run `pipx upgrade multi-claude-cli`; with Homebrew, `brew upgrade mul
|
|
|
347
385
|
|
|
348
386
|
The one in `~/.claude`, as before: multi-claude does not change it. After [migrating `~/.claude`](#migrating-claude) it is the migrated account. `claude-auto` picks an account by directory instead; see [Choosing an account by directory](#choosing-an-account-by-directory).
|
|
349
387
|
|
|
388
|
+
There is no command that switches which account plain `claude` uses. Besides `~/.claude`, plain `claude` reads the login in the keychain (macOS) and `~/.claude.json`, and neither would follow such a switch, so two accounts would get mixed up. Use the launchers (`claude-work`), or `multi-claude route --default work` and start `claude-auto`.
|
|
389
|
+
|
|
350
390
|
### How do I continue a conversation with another account?
|
|
351
391
|
|
|
352
392
|
Inside the Claude session, run `! multi-claude handoff OTHER`, then run the command it prints in a new terminal. See [Handing a session over to another account](#handing-a-session-over-to-another-account).
|
|
@@ -361,7 +401,7 @@ By default the numbers come from the cache that Claude Code writes in `.claude.j
|
|
|
361
401
|
|
|
362
402
|
### Does multi-claude read or copy my login?
|
|
363
403
|
|
|
364
|
-
|
|
404
|
+
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.
|
|
365
405
|
|
|
366
406
|
## Reference
|
|
367
407
|
|
|
@@ -381,6 +421,7 @@ No. It never reads, copies or deletes credentials; its only keychain call checks
|
|
|
381
421
|
| `multi-claude apply [-f FILE]` | Converge everything to the configuration (or to `FILE`). |
|
|
382
422
|
| `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. |
|
|
383
423
|
| `multi-claude usage [NAME] [--json]` | Show the last known 5-hour and 7-day usage of each account. |
|
|
424
|
+
| `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). |
|
|
384
425
|
| `multi-claude doctor [--json] [--verbose]` | Check the configuration, launchers, account directories, shared links, logins and environment. |
|
|
385
426
|
| `multi-claude route DIR NAME`, `route DIR --remove`, `route --default NAME`, `route --no-default` | Choose an account by directory for `claude-auto`. |
|
|
386
427
|
| `multi-claude which [DIR]` | Show which account `claude-auto` would use in `DIR` (default: the current directory). |
|
|
@@ -389,6 +430,7 @@ No. It never reads, copies or deletes credentials; its only keychain call checks
|
|
|
389
430
|
| `multi-claude mcp NAME [ARG ...]` | Run `claude mcp ARG ...` as that account. |
|
|
390
431
|
| `multi-claude run [NAME] [-- COMMAND ...]` | Run a command (default: `claude` with the fixed arguments) with the environment of the account's launcher. Without NAME, the account `claude-auto` would use. |
|
|
391
432
|
| `multi-claude path NAME` | Print the account's directory. |
|
|
433
|
+
| `multi-claude code NAME [PATH] [-- ARGS ...]` | Open a separate VS Code instance with the account's environment (experimental). |
|
|
392
434
|
| `multi-claude handoff TARGET [--from NAME] [--session ID] [--force]` | Copy a session to another account and print the command that resumes it there. |
|
|
393
435
|
| `multi-claude statusline install\|uninstall FILE` | Wrap the statusLine command in a settings file so that `usage` gets fresh numbers, or restore it. |
|
|
394
436
|
| `multi-claude completion bash\|zsh\|fish` | Print a shell completion script. |
|
|
@@ -464,7 +506,7 @@ If you move an account directory yourself, its launcher reports that the directo
|
|
|
464
506
|
./install.sh --uninstall
|
|
465
507
|
```
|
|
466
508
|
|
|
467
|
-
This removes the tool only. Your configuration, account directories, logins and `claude-<name>` launchers stay; the launchers keep working because they do not depend on multi-claude. Run `multi-claude statusline uninstall FILE` first if you wrapped a status line.
|
|
509
|
+
This removes the tool only. Your configuration, account directories (including the VS Code data in `<root>/.apps` created by `code`), logins and `claude-<name>` launchers stay; the launchers keep working because they do not depend on multi-claude. Run `multi-claude statusline uninstall FILE` first if you wrapped a status line.
|
|
468
510
|
|
|
469
511
|
## Contributing
|
|
470
512
|
|
|
@@ -10,6 +10,8 @@
|
|
|
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.
|
|
14
|
+
|
|
13
15
|
```sh
|
|
14
16
|
claude-work # Claude Code with your work account
|
|
15
17
|
claude-personal # Claude Code with your personal account, in another terminal
|
|
@@ -35,6 +37,10 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
|
|
|
35
37
|
|
|
36
38
|
## Features
|
|
37
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.
|
|
42
|
+
- **Leaves your shell alone** — when `PATH` needs a change, it prints the line to add instead of editing `~/.zshrc` or `~/.bashrc`.
|
|
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`).
|
|
38
44
|
- **Migrate the default account** — move your existing `~/.claude` into the account root without logging out. `claude` keeps working exactly as before.
|
|
39
45
|
- **Add accounts** — create an account directory and a `claude-<name>` launcher, or adopt a directory you already have.
|
|
40
46
|
- **Per-account proxy** — a local port, an HTTP(S) proxy URL, `off`, or `inherit`.
|
|
@@ -42,7 +48,6 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
|
|
|
42
48
|
- **One-step deployment** — `install.sh --config accounts.json` installs the tool and creates every account in the file.
|
|
43
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.
|
|
44
50
|
- **Optional shared resources** — link `CLAUDE.md`, `skills`, `agents` and `commands` from one shared directory into selected accounts.
|
|
45
|
-
- **Hands off your credentials** — multi-claude never reads, copies or deletes logins. Its only keychain call checks whether an item exists.
|
|
46
51
|
|
|
47
52
|
## Requirements
|
|
48
53
|
|
|
@@ -218,6 +223,23 @@ The command becomes `<path of multi-claude> statusline-hook '<original command>'
|
|
|
218
223
|
- If several settings files define `statusLine`, wrap the one that wins (for example a file passed with `--settings` in the launcher arguments).
|
|
219
224
|
- Run `multi-claude statusline uninstall FILE` before uninstalling multi-claude; otherwise the status line stays empty because the wrapped command cannot be found. Running `install` again after multi-claude moves updates the path.
|
|
220
225
|
|
|
226
|
+
#### Token history
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
multi-claude usage --history # tokens per day, last 7 days, every account
|
|
230
|
+
multi-claude usage --history work --days 30 --by model
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
work:
|
|
235
|
+
DATE INPUT OUTPUT CACHE READ CACHE WRITE REPLIES
|
|
236
|
+
2026-10-01 1,246 499,681 273,254,361 5,926,527 612
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`--history` counts the replies in the account's session files on this machine (`projects/**/*.jsonl`, including subagents): input, output, cache-read and cache-write tokens per day (local time) or per model. Only these counters are read; nothing else from the conversations is printed or stored, and nothing is sent anywhere. Sessions on other machines and deleted sessions are not counted. A session copied with `handoff` appears in both accounts; the `TOTAL` block counts each reply once. `--json` prints the same numbers for scripts.
|
|
240
|
+
|
|
241
|
+
`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.
|
|
242
|
+
|
|
221
243
|
### Handing a session over to another account
|
|
222
244
|
|
|
223
245
|
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:
|
|
@@ -259,6 +281,22 @@ multi-claude add client-b --config-from work # start client-b with a copy of wor
|
|
|
259
281
|
|
|
260
282
|
`--config-from` copies `settings.json` once; afterwards the two files are independent. A different existing `settings.json` in the new account, or a `settings.json` that the account shares, is a conflict and nothing is changed. Use [shared resources](#shared-resources) for settings that should stay the same in every account.
|
|
261
283
|
|
|
284
|
+
### Opening VS Code for an account (experimental)
|
|
285
|
+
|
|
286
|
+
```sh
|
|
287
|
+
multi-claude code work ~/projects/app # a separate VS Code window for work
|
|
288
|
+
multi-claude code personal . -- --disable-gpu # arguments after -- go to VS Code
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The Claude Code extension uses the `CLAUDE_CONFIG_DIR` of the environment VS Code was started with, so a VS Code opened from the Dock always uses `~/.claude`. `code` starts VS Code with the account's environment (the same as `run`) and a separate user data directory, `<root>/.apps/<account directory>/vscode`. A separate user data directory is required: otherwise VS Code hands the request to the instance that is already running, with its own environment. Each account's instance starts with default VS Code settings; extensions are shared because they live in `~/.vscode/extensions`.
|
|
292
|
+
|
|
293
|
+
Notes:
|
|
294
|
+
|
|
295
|
+
- If an instance for the account is already running, the request goes to it; changes to the proxy or environment variables apply after that instance quits.
|
|
296
|
+
- Setting `CLAUDE_CONFIG_DIR` in the extension's `claudeCode.environmentVariables` inside that instance overrides the account.
|
|
297
|
+
- On macOS the `code` command passes the whole environment to `open --env`, so the values are briefly visible in the process list.
|
|
298
|
+
- Verified with VS Code 1.139.1 and Claude Code extension 2.1.286 on macOS; `code` prints a warning because it relies on undocumented behaviour. It needs the `code` command on `PATH` (in VS Code: "Shell Command: Install 'code' command in PATH").
|
|
299
|
+
|
|
262
300
|
### Diagnostics and completion
|
|
263
301
|
|
|
264
302
|
`multi-claude doctor` checks, without changing anything: the configuration and any unfinished migration, whether `claude` and the launcher directory are on `PATH`, whether each launcher is up to date and not hidden by another file of the same name, whether each account directory exists and is private (`0700`), the default account's link, broken shared links, whether a login exists, and variables such as `ANTHROPIC_API_KEY` that override the accounts' logins. Every problem comes with the command that fixes it. It exits with 1 when there is an error; `--json` prints the results for scripts.
|
|
@@ -320,6 +358,8 @@ With pipx, run `pipx upgrade multi-claude-cli`; with Homebrew, `brew upgrade mul
|
|
|
320
358
|
|
|
321
359
|
The one in `~/.claude`, as before: multi-claude does not change it. After [migrating `~/.claude`](#migrating-claude) it is the migrated account. `claude-auto` picks an account by directory instead; see [Choosing an account by directory](#choosing-an-account-by-directory).
|
|
322
360
|
|
|
361
|
+
There is no command that switches which account plain `claude` uses. Besides `~/.claude`, plain `claude` reads the login in the keychain (macOS) and `~/.claude.json`, and neither would follow such a switch, so two accounts would get mixed up. Use the launchers (`claude-work`), or `multi-claude route --default work` and start `claude-auto`.
|
|
362
|
+
|
|
323
363
|
### How do I continue a conversation with another account?
|
|
324
364
|
|
|
325
365
|
Inside the Claude session, run `! multi-claude handoff OTHER`, then run the command it prints in a new terminal. See [Handing a session over to another account](#handing-a-session-over-to-another-account).
|
|
@@ -334,7 +374,7 @@ By default the numbers come from the cache that Claude Code writes in `.claude.j
|
|
|
334
374
|
|
|
335
375
|
### Does multi-claude read or copy my login?
|
|
336
376
|
|
|
337
|
-
|
|
377
|
+
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.
|
|
338
378
|
|
|
339
379
|
## Reference
|
|
340
380
|
|
|
@@ -354,6 +394,7 @@ No. It never reads, copies or deletes credentials; its only keychain call checks
|
|
|
354
394
|
| `multi-claude apply [-f FILE]` | Converge everything to the configuration (or to `FILE`). |
|
|
355
395
|
| `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. |
|
|
356
396
|
| `multi-claude usage [NAME] [--json]` | Show the last known 5-hour and 7-day usage of each account. |
|
|
397
|
+
| `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). |
|
|
357
398
|
| `multi-claude doctor [--json] [--verbose]` | Check the configuration, launchers, account directories, shared links, logins and environment. |
|
|
358
399
|
| `multi-claude route DIR NAME`, `route DIR --remove`, `route --default NAME`, `route --no-default` | Choose an account by directory for `claude-auto`. |
|
|
359
400
|
| `multi-claude which [DIR]` | Show which account `claude-auto` would use in `DIR` (default: the current directory). |
|
|
@@ -362,6 +403,7 @@ No. It never reads, copies or deletes credentials; its only keychain call checks
|
|
|
362
403
|
| `multi-claude mcp NAME [ARG ...]` | Run `claude mcp ARG ...` as that account. |
|
|
363
404
|
| `multi-claude run [NAME] [-- COMMAND ...]` | Run a command (default: `claude` with the fixed arguments) with the environment of the account's launcher. Without NAME, the account `claude-auto` would use. |
|
|
364
405
|
| `multi-claude path NAME` | Print the account's directory. |
|
|
406
|
+
| `multi-claude code NAME [PATH] [-- ARGS ...]` | Open a separate VS Code instance with the account's environment (experimental). |
|
|
365
407
|
| `multi-claude handoff TARGET [--from NAME] [--session ID] [--force]` | Copy a session to another account and print the command that resumes it there. |
|
|
366
408
|
| `multi-claude statusline install\|uninstall FILE` | Wrap the statusLine command in a settings file so that `usage` gets fresh numbers, or restore it. |
|
|
367
409
|
| `multi-claude completion bash\|zsh\|fish` | Print a shell completion script. |
|
|
@@ -437,7 +479,7 @@ If you move an account directory yourself, its launcher reports that the directo
|
|
|
437
479
|
./install.sh --uninstall
|
|
438
480
|
```
|
|
439
481
|
|
|
440
|
-
This removes the tool only. Your configuration, account directories, logins and `claude-<name>` launchers stay; the launchers keep working because they do not depend on multi-claude. Run `multi-claude statusline uninstall FILE` first if you wrapped a status line.
|
|
482
|
+
This removes the tool only. Your configuration, account directories (including the VS Code data in `<root>/.apps` created by `code`), logins and `claude-<name>` launchers stay; the launchers keep working because they do not depend on multi-claude. Run `multi-claude statusline uninstall FILE` first if you wrapped a status line.
|
|
441
483
|
|
|
442
484
|
## Contributing
|
|
443
485
|
|
|
@@ -18,8 +18,8 @@ import sys
|
|
|
18
18
|
from datetime import datetime, timedelta, timezone
|
|
19
19
|
from typing import List, Optional, Tuple
|
|
20
20
|
|
|
21
|
-
from . import (__version__, accounts, completion, doctor, hook, identity, launcher, migrate, platform,
|
|
22
|
-
routes, sessions, shellpath, statusline, usage)
|
|
21
|
+
from . import (__version__, accounts, completion, doctor, history, hook, identity, launcher, migrate, platform,
|
|
22
|
+
restore, routes, sessions, shellpath, statusline, usage)
|
|
23
23
|
from .actions import 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,
|
|
@@ -56,6 +56,7 @@ COMMAND_GROUPS = [
|
|
|
56
56
|
("handoff", "copy a session to another account"),
|
|
57
57
|
("mcp", "run `claude mcp` as an account"),
|
|
58
58
|
("run, path", "run any command as an account; print its directory"),
|
|
59
|
+
("code", "open VS Code for an account (experimental)"),
|
|
59
60
|
]),
|
|
60
61
|
("Setup and checks", [
|
|
61
62
|
("init, apply", "global settings; converge everything to config.json"),
|
|
@@ -175,6 +176,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
175
176
|
"(from Claude Code's own cache; never reads credentials)")
|
|
176
177
|
p_usage.add_argument("name", nargs="?")
|
|
177
178
|
p_usage.add_argument("--json", action="store_true", help="print machine-readable JSON")
|
|
179
|
+
p_usage.add_argument("--history", action="store_true",
|
|
180
|
+
help="show token usage counted from the session files on this machine instead")
|
|
181
|
+
p_usage.add_argument("--days", type=int, metavar="N", help="with --history: the last N days (default 7)")
|
|
182
|
+
p_usage.add_argument("--by", choices=("day", "model"), help="with --history: group by day (default) or model")
|
|
178
183
|
|
|
179
184
|
p_doctor = sub.add_parser("doctor", description="check the accounts, launchers and environment (read-only)")
|
|
180
185
|
p_doctor.add_argument("--json", action="store_true", help="print machine-readable JSON")
|
|
@@ -217,6 +222,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
217
222
|
p_path = sub.add_parser("path", description="print the directory of an account")
|
|
218
223
|
p_path.add_argument("name")
|
|
219
224
|
|
|
225
|
+
# code 的 VS Code 参数与 run 一样,在 argparse 之前从第一个 `--` 处切走。
|
|
226
|
+
p_code = sub.add_parser("code", description="open a separate VS Code instance with the environment of an "
|
|
227
|
+
"account (experimental): code NAME [PATH] [-- CODE_ARGS ...]")
|
|
228
|
+
p_code.add_argument("name")
|
|
229
|
+
p_code.add_argument("path", nargs="?", metavar="PATH")
|
|
230
|
+
|
|
220
231
|
p_restore = sub.add_parser("restore", description="undo migrate-default: move the account directory back to "
|
|
221
232
|
"~/.claude and unregister the account; the login is kept")
|
|
222
233
|
p_restore.add_argument("name")
|
|
@@ -285,7 +296,7 @@ def _add_write_options(parser: argparse.ArgumentParser) -> None:
|
|
|
285
296
|
def _split_args_command(argv: List[str]) -> Tuple[List[str], Optional[List[str]]]:
|
|
286
297
|
"""子命令是 `args` 时,把第一个 `--` 之后的全部内容取出来作为参数列表;
|
|
287
298
|
子命令是 `mcp` 或 `login` 时,账号名之后的全部内容原样作为 `claude mcp` / `claude auth login` 的参数(不经 argparse);
|
|
288
|
-
子命令是 `run` 时,第一个 `--`
|
|
299
|
+
子命令是 `run` / `code` 时,第一个 `--` 之后的内容是要运行的命令 / 传给 VS Code 的参数。
|
|
289
300
|
|
|
290
301
|
不能用 argparse.REMAINDER:可选位置参数加 --defaults 加 REMAINDER 时,
|
|
291
302
|
`args --defaults -- --settings s` 会被解析成账号名 `--settings`(方案 §5.1.2,实测 3.10/3.11/3.13)。
|
|
@@ -307,8 +318,8 @@ def _split_args_command(argv: List[str]) -> Tuple[List[str], Optional[List[str]]
|
|
|
307
318
|
example = "multi-claude mcp NAME list" if command == "mcp" else "multi-claude login NAME"
|
|
308
319
|
raise UsageError("{} needs an account name first, e.g. `{}`".format(command, example))
|
|
309
320
|
return argv[:command_index + 2], rest[1:]
|
|
310
|
-
if command_index is not None and argv[command_index]
|
|
311
|
-
# run
|
|
321
|
+
if command_index is not None and argv[command_index] in ("run", "code"):
|
|
322
|
+
# run 的命令(code 的 VS Code 参数)从第一个 `--` 之后原样取出;没有 `--` 与 `--` 后为空都表示“没有”。
|
|
312
323
|
if "--" not in argv[command_index + 1:]:
|
|
313
324
|
return argv, None
|
|
314
325
|
separator = argv.index("--", command_index + 1)
|
|
@@ -374,6 +385,10 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
374
385
|
if args.command == "list":
|
|
375
386
|
return cmd_list(verbose=args.verbose, as_json=args.json, names_only=args.names)
|
|
376
387
|
if args.command == "usage":
|
|
388
|
+
if args.history:
|
|
389
|
+
return cmd_usage_history(args.name, args.days, args.by or "day", args.json)
|
|
390
|
+
if args.days is not None or args.by is not None:
|
|
391
|
+
raise UsageError("--days and --by need --history")
|
|
377
392
|
return cmd_usage(args.name, args.json)
|
|
378
393
|
if args.command == "which":
|
|
379
394
|
return cmd_which(args.path)
|
|
@@ -394,7 +409,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
394
409
|
return accounts.EXIT_ERROR
|
|
395
410
|
# 不加写锁:凭据由 Claude 自己写入,登录过程需要浏览器交互,可能持续很久。
|
|
396
411
|
return cmd_login(args.name, args.launch_args or [])
|
|
397
|
-
if args.command in ("run", "path"):
|
|
412
|
+
if args.command in ("run", "path", "code"):
|
|
398
413
|
if notice:
|
|
399
414
|
# 与 mcp 相同:迁移进行到一半时账号目录可能正在搬动。
|
|
400
415
|
error("finish the unfinished migration before running `{}`".format(args.command))
|
|
@@ -402,6 +417,8 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
402
417
|
# 只读配置、不加写锁:run 会把进程换成目标命令,可能运行很久。
|
|
403
418
|
if args.command == "path":
|
|
404
419
|
return cmd_path(args.name)
|
|
420
|
+
if args.command == "code":
|
|
421
|
+
return cmd_code(args.name, args.path, args.launch_args or [])
|
|
405
422
|
return cmd_run(args.name, args.launch_args or [])
|
|
406
423
|
if args.command == "mcp":
|
|
407
424
|
if notice:
|
|
@@ -788,7 +805,7 @@ def _account_process_env(name: str) -> Tuple[Optional[dict], int]:
|
|
|
788
805
|
if process_env is None:
|
|
789
806
|
return None, code
|
|
790
807
|
if shutil.which("claude", path=process_env.get("PATH")) is None:
|
|
791
|
-
error("claude not found in PATH")
|
|
808
|
+
error("claude not found in PATH" + _CLAUDE_NOT_FOUND_HINT)
|
|
792
809
|
return None, 127
|
|
793
810
|
return process_env, accounts.EXIT_OK
|
|
794
811
|
|
|
@@ -831,14 +848,29 @@ def cmd_run(name: Optional[str], command: List[str]) -> int:
|
|
|
831
848
|
# 提示写 stderr:stdout 留给目标命令,`run -- cmd | …` 才不会混进这一行。
|
|
832
849
|
print("[multi-claude] run: using account {} ({})".format(
|
|
833
850
|
account.name, "route " + rule.path if rule is not None else "default"), file=sys.stderr)
|
|
834
|
-
process_env, code = _usable_account_env(config, account)
|
|
835
|
-
if process_env is None:
|
|
836
|
-
return code
|
|
837
851
|
if not command:
|
|
838
852
|
# 固定参数按启动命令的规则展开 `~`(launcher.render),两种启动方式收到的参数才逐项相同。
|
|
839
853
|
command = ["claude"] + [launcher.expand_value(arg) for arg in config.effective_args(account)]
|
|
854
|
+
return _exec_as(config, account, command,
|
|
855
|
+
not_found_hint=_CLAUDE_NOT_FOUND_HINT if command[0] == "claude" else "")
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
# 找不到 claude 时附上的官方安装方法(https://code.claude.com/docs/en/setup,2026-10-03 核对)。
|
|
859
|
+
# 只用于 multi-claude 自己的报错:启动命令里的文字不改,改了所有启动命令都会变成过期。
|
|
860
|
+
_CLAUDE_NOT_FOUND_HINT = ("; install Claude Code: curl -fsSL https://claude.ai/install.sh | bash "
|
|
861
|
+
"(other ways: https://code.claude.com/docs/en/setup)")
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _exec_as(config: Config, account: Account, command: List[str], not_found_hint: str = "") -> int:
|
|
865
|
+
"""run 与 code 共用:账号可用时以它的启动命令环境 exec 目标命令,成功则不返回。
|
|
866
|
+
|
|
867
|
+
返回值只出现在失败时:账号不可用 1,PATH 中找不到命令 127。
|
|
868
|
+
"""
|
|
869
|
+
process_env, code = _usable_account_env(config, account)
|
|
870
|
+
if process_env is None:
|
|
871
|
+
return code
|
|
840
872
|
if shutil.which(command[0], path=process_env.get("PATH")) is None:
|
|
841
|
-
error("{} not found in PATH".format(command[0]))
|
|
873
|
+
error("{} not found in PATH{}".format(command[0], not_found_hint))
|
|
842
874
|
return 127
|
|
843
875
|
sys.stdout.flush()
|
|
844
876
|
sys.stderr.flush()
|
|
@@ -847,6 +879,54 @@ def cmd_run(name: Optional[str], command: List[str]) -> int:
|
|
|
847
879
|
return accounts.EXIT_ERROR # 不会执行到这里:execvpe 成功后不返回
|
|
848
880
|
|
|
849
881
|
|
|
882
|
+
# code 依赖的未公开行为由 2026-10-02 实测确认的版本(方案 feature-vscode-launch §1.1)。
|
|
883
|
+
VSCODE_VERIFIED_WITH = "VS Code 1.139.1, extension 2.1.286"
|
|
884
|
+
# macOS 的 unix 套接字路径上限是 104 字节,VS Code 在 macOS 上把主套接字放在用户数据目录下(形如 1.13-main.sock,
|
|
885
|
+
# 约 15 字节,见 VS Code out/main.js 构造套接字路径的函数);Linux 设了 XDG_RUNTIME_DIR 时不放在这里。
|
|
886
|
+
_SOCKET_DIR_WARN_LENGTH = 80
|
|
887
|
+
_CODE_NOT_FOUND_HINT = "; in VS Code run \"Shell Command: Install 'code' command in PATH\""
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def cmd_code(name: str, path: Optional[str], code_args: List[str]) -> int:
|
|
891
|
+
"""以账号环境打开一个独立的 VS Code 实例(实验功能,方案 feature-vscode-launch §5.1)。
|
|
892
|
+
|
|
893
|
+
必须给每个账号单独的 --user-data-dir:VS Code 已在运行时,同一用户数据目录的请求会转给已运行的实例,
|
|
894
|
+
新窗口沿用那个实例的环境,账号就不生效了。目录按账号目录名存放,rename 后数据仍在原处。
|
|
895
|
+
"""
|
|
896
|
+
warn("experimental: code relies on undocumented behaviour of VS Code and the Claude Code extension "
|
|
897
|
+
"(verified with {})".format(VSCODE_VERIFIED_WITH))
|
|
898
|
+
config, _ = load_config()
|
|
899
|
+
account = config.find(_checked_name(name))
|
|
900
|
+
if account is None:
|
|
901
|
+
error("account {!r} is not registered".format(name))
|
|
902
|
+
return accounts.EXIT_ERROR
|
|
903
|
+
# 先确认账号可用、找得到 code 命令,再建目录:任何失败都不留下空的用户数据目录。
|
|
904
|
+
process_env, code = _usable_account_env(config, account)
|
|
905
|
+
if process_env is None:
|
|
906
|
+
return code
|
|
907
|
+
if shutil.which("code", path=process_env.get("PATH")) is None:
|
|
908
|
+
error("code not found in PATH{}".format(_CODE_NOT_FOUND_HINT))
|
|
909
|
+
return 127
|
|
910
|
+
data_dir = _private_dirs(expand(config.root), [".apps", account.dir, "vscode"])
|
|
911
|
+
if sys.platform == "darwin":
|
|
912
|
+
if len(data_dir) > _SOCKET_DIR_WARN_LENGTH:
|
|
913
|
+
warn("{} is long; VS Code's socket path inside it may exceed the 104-byte limit".format(data_dir))
|
|
914
|
+
warn("on macOS, code passes the whole environment (including this account's variables) to "
|
|
915
|
+
"`open --env`, so the values are briefly visible in the process list")
|
|
916
|
+
command = ["code", "--user-data-dir", data_dir] + ([path] if path else []) + list(code_args)
|
|
917
|
+
return _exec_as(config, account, command, not_found_hint=_CODE_NOT_FOUND_HINT)
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def _private_dirs(root: str, parts: List[str]) -> str:
|
|
921
|
+
"""在 root 下逐级创建 parts,每一级都只允许本人访问(makedirs 的 mode 受 umask 影响,所以再 chmod)。"""
|
|
922
|
+
path = root
|
|
923
|
+
for part in parts:
|
|
924
|
+
path = os.path.join(path, part)
|
|
925
|
+
os.makedirs(path, mode=0o700, exist_ok=True)
|
|
926
|
+
os.chmod(path, 0o700)
|
|
927
|
+
return path
|
|
928
|
+
|
|
929
|
+
|
|
850
930
|
def cmd_path(name: str) -> int:
|
|
851
931
|
"""打印账号目录;目录不存在时照样打印,但以 1 退出,脚本里 `cd "$(multi-claude path X)"` 不会静默进错目录。"""
|
|
852
932
|
config, _ = load_config()
|
|
@@ -1220,14 +1300,15 @@ def _print_brief_list(config: Config) -> int:
|
|
|
1220
1300
|
# 与完整输出相同:LOGIN 与由它得出的 STATUS 都按正式服务探测,这时可能不准。
|
|
1221
1301
|
warn("CLAUDE_CODE_CUSTOM_OAUTH_URL is set; LOGIN assumes the production service and may be wrong")
|
|
1222
1302
|
now = datetime.now(timezone.utc)
|
|
1223
|
-
rows = [("NAME", "LOGIN", "PROXY", "SHARED", "5H", "7D", "STATUS")]
|
|
1303
|
+
rows = [("NAME", "LOGIN", "PROXY", "SHARED", "5H", "7D", "LAST USED", "STATUS")]
|
|
1224
1304
|
has_problem = False
|
|
1225
1305
|
for entry in _account_entries(config):
|
|
1226
1306
|
report = usage.read_account_usage(config, config.find(entry["name"]), now)
|
|
1227
1307
|
status = _status_cell(entry)
|
|
1228
1308
|
has_problem = has_problem or status != "ok"
|
|
1229
1309
|
rows.append((entry["name"], entry["login"], entry["proxy"], _shared_cell(entry),
|
|
1230
|
-
_window_cell(report.five_hour, now), _window_cell(report.seven_day, now),
|
|
1310
|
+
_window_cell(report.five_hour, now), _window_cell(report.seven_day, now),
|
|
1311
|
+
_last_used_cell(entry["last_used"], now), status))
|
|
1231
1312
|
_print_table(rows)
|
|
1232
1313
|
_print_routes(config)
|
|
1233
1314
|
if has_problem:
|
|
@@ -1294,6 +1375,8 @@ def _account_entries(config: Config, with_usage: bool = False) -> List[dict]:
|
|
|
1294
1375
|
"link": accounts.default_link_status(config, account) if is_default else None,
|
|
1295
1376
|
"keychain_service": identity.service_name(account.identity, directory),
|
|
1296
1377
|
"credentials_file": identity.credentials_file(directory),
|
|
1378
|
+
# history.jsonl 与会话文件的最大 mtime;只 stat 不读内容(方案 feature-usage-history §5.1.3)
|
|
1379
|
+
"last_used": _iso(history.last_used(directory)),
|
|
1297
1380
|
}
|
|
1298
1381
|
if with_usage:
|
|
1299
1382
|
entry["usage"] = usage.report_to_dict(usage.read_account_usage(config, account, now), now)
|
|
@@ -1388,6 +1471,86 @@ def _updated_cell(name: str, report: usage.UsageReport, now: datetime) -> str:
|
|
|
1388
1471
|
return age + (" (statusline)" if report.source == usage.SOURCE_STATUSLINE else "")
|
|
1389
1472
|
|
|
1390
1473
|
|
|
1474
|
+
def _iso(moment: Optional[datetime]) -> Optional[str]:
|
|
1475
|
+
return None if moment is None else moment.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
def _last_used_cell(value: Optional[str], now: datetime) -> str:
|
|
1479
|
+
if value is None:
|
|
1480
|
+
return "-"
|
|
1481
|
+
moment = datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
|
1482
|
+
return _format_age(now - moment)
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
_HISTORY_COLUMNS = ("INPUT", "OUTPUT", "CACHE READ", "CACHE WRITE", "REPLIES")
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
def cmd_usage_history(name: Optional[str], days: Optional[int], by: str, as_json: bool) -> int:
|
|
1489
|
+
"""按天或按模型汇总本机会话记录里的 token 用量(方案 feature-usage-history §5.1.2)。
|
|
1490
|
+
|
|
1491
|
+
只读计数字段,不输出对话内容、不联网、不读凭据。各账号分别计数;TOTAL 跨账号按回复 ID 去重,
|
|
1492
|
+
因为 handoff 复制过的会话在两个账号里都有同样的回复。
|
|
1493
|
+
"""
|
|
1494
|
+
days = 7 if days is None else days
|
|
1495
|
+
if not 1 <= days <= 366:
|
|
1496
|
+
raise UsageError("--days must be between 1 and 366")
|
|
1497
|
+
config, exists = load_config()
|
|
1498
|
+
selected = list(config.accounts.values()) if exists else []
|
|
1499
|
+
if name is not None:
|
|
1500
|
+
account = config.find(_checked_name(name)) if exists else None
|
|
1501
|
+
if account is None:
|
|
1502
|
+
error("account {!r} is not registered".format(name))
|
|
1503
|
+
return accounts.EXIT_ERROR
|
|
1504
|
+
selected = [account]
|
|
1505
|
+
local_now = datetime.now().astimezone()
|
|
1506
|
+
start_local = (local_now - timedelta(days=days - 1)).replace(hour=0, minute=0, second=0, microsecond=0)
|
|
1507
|
+
since = start_local.astimezone(timezone.utc)
|
|
1508
|
+
per_account = []
|
|
1509
|
+
combined: dict = {}
|
|
1510
|
+
skipped = 0
|
|
1511
|
+
for account in selected:
|
|
1512
|
+
replies, unreadable = history.scan(accounts.account_dir(config, account.name), since)
|
|
1513
|
+
skipped += unreadable
|
|
1514
|
+
per_account.append((account.name, history.summarize(replies.values(), by)))
|
|
1515
|
+
for reply_id, reply in replies.items():
|
|
1516
|
+
# 同一回复出现在多个账号(handoff)时只算一次,计数仍按各项取最大值合并
|
|
1517
|
+
combined[reply_id] = reply if reply_id not in combined else history.merge(combined[reply_id], reply)
|
|
1518
|
+
if skipped:
|
|
1519
|
+
warn("skipped {} session file(s) that could not be read".format(skipped))
|
|
1520
|
+
total = history.summarize(combined.values(), by)
|
|
1521
|
+
if as_json:
|
|
1522
|
+
_print_json({"schema_version": 1, "days": days, "by": by,
|
|
1523
|
+
"accounts": [{"name": account_name, "rows": [row._asdict() for row in rows]}
|
|
1524
|
+
for account_name, rows in per_account],
|
|
1525
|
+
"total": [row._asdict() for row in total]})
|
|
1526
|
+
return accounts.EXIT_OK
|
|
1527
|
+
if not exists:
|
|
1528
|
+
info("no configuration yet at {}; run `multi-claude add NAME` to start".format(
|
|
1529
|
+
os.path.join(platform.state_dir(), "config.json")))
|
|
1530
|
+
return accounts.EXIT_OK
|
|
1531
|
+
for account_name, rows in per_account:
|
|
1532
|
+
_print_history_block(account_name, rows, by, days)
|
|
1533
|
+
if len(per_account) > 1:
|
|
1534
|
+
_print_history_block("TOTAL (each reply counted once)", total, by, days)
|
|
1535
|
+
print("Counted from the session files on this machine (not other machines, not deleted sessions); "
|
|
1536
|
+
"multi-claude reads only the token counts, never connects to the network and never reads credentials.")
|
|
1537
|
+
return accounts.EXIT_OK
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def _print_history_block(title: str, rows, by: str, days: int) -> None:
|
|
1541
|
+
print("{}:".format(title))
|
|
1542
|
+
if not rows:
|
|
1543
|
+
print(" no replies in the last {} day{}".format(days, "" if days == 1 else "s"))
|
|
1544
|
+
return
|
|
1545
|
+
table = [(" " + ("DATE" if by == "day" else "MODEL"),) + _HISTORY_COLUMNS]
|
|
1546
|
+
for row in rows:
|
|
1547
|
+
table.append((" " + row.key,) + tuple("{:,}".format(value) for value in row[1:]))
|
|
1548
|
+
widths = [max(len(line[index]) for line in table) for index in range(len(table[0]))]
|
|
1549
|
+
for line in table:
|
|
1550
|
+
cells = [line[0].ljust(widths[0])] + [cell.rjust(width) for cell, width in zip(line[1:], widths[1:])]
|
|
1551
|
+
print(" ".join(cells).rstrip())
|
|
1552
|
+
|
|
1553
|
+
|
|
1391
1554
|
def _format_age(delta) -> str:
|
|
1392
1555
|
minutes = max(0, int(delta.total_seconds() // 60))
|
|
1393
1556
|
if minutes < 60:
|
|
@@ -19,7 +19,7 @@ POSITIONAL_KINDS = {
|
|
|
19
19
|
"args": ["name"], "remove": ["name"], "usage": ["name"], "completion": ["shell"],
|
|
20
20
|
"route": ["dir", "name"], "which": ["dir"], "rename": ["name"], "mcp": ["name"],
|
|
21
21
|
"statusline": ["statusline", "file"], "handoff": ["name"], "login": ["name"], "set": ["name"],
|
|
22
|
-
"run": ["name"], "path": ["name"], "restore": ["name"],
|
|
22
|
+
"run": ["name"], "path": ["name"], "restore": ["name"], "code": ["name", "file"],
|
|
23
23
|
}
|
|
24
24
|
# 选项值是路径、应按文件补全的选项。
|
|
25
25
|
PATH_OPTIONS = ("-f", "--file", "--root", "--bin-dir", "--shared-dir")
|
|
@@ -100,7 +100,8 @@ def _load() -> Tuple[Optional[Config], Check]:
|
|
|
100
100
|
def _check_claude_on_path() -> Check:
|
|
101
101
|
found = shutil.which("claude")
|
|
102
102
|
if found is None:
|
|
103
|
-
return Check("claude-on-path", LEVEL_ERROR, "claude", "`claude` is not on PATH; install Claude Code"
|
|
103
|
+
return Check("claude-on-path", LEVEL_ERROR, "claude", "`claude` is not on PATH; install Claude Code: curl -fsSL https://claude.ai/install.sh "
|
|
104
|
+
"| bash (other ways: https://code.claude.com/docs/en/setup)")
|
|
104
105
|
return Check("claude-on-path", LEVEL_OK, "claude", found)
|
|
105
106
|
|
|
106
107
|
|