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.
Files changed (58) hide show
  1. {multi_claude_cli-0.5.0/src/multi_claude_cli.egg-info → multi_claude_cli-0.7.0}/PKG-INFO +46 -4
  2. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/README.md +45 -3
  3. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/__init__.py +1 -1
  4. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/cli.py +176 -13
  5. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/completion.py +1 -1
  6. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/doctor.py +2 -1
  7. multi_claude_cli-0.7.0/src/multi_claude/history.py +178 -0
  8. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0/src/multi_claude_cli.egg-info}/PKG-INFO +46 -4
  9. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude_cli.egg-info/SOURCES.txt +4 -1
  10. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_help.py +2 -2
  11. multi_claude_cli-0.7.0/tests/test_history.py +194 -0
  12. multi_claude_cli-0.7.0/tests/test_vscode.py +127 -0
  13. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/LICENSE +0 -0
  14. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/pyproject.toml +0 -0
  15. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/setup.cfg +0 -0
  16. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/__main__.py +0 -0
  17. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/accounts.py +0 -0
  18. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/actions.py +0 -0
  19. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/config.py +0 -0
  20. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/env.py +0 -0
  21. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/fsutil.py +0 -0
  22. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/hook.py +0 -0
  23. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/identity.py +0 -0
  24. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/launcher.py +0 -0
  25. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/lock.py +0 -0
  26. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/migrate.py +0 -0
  27. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/platform.py +0 -0
  28. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/restore.py +0 -0
  29. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/routes.py +0 -0
  30. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/sessions.py +0 -0
  31. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/shared.py +0 -0
  32. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/shellpath.py +0 -0
  33. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/statusline.py +0 -0
  34. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude/usage.py +0 -0
  35. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude_cli.egg-info/dependency_links.txt +0 -0
  36. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude_cli.egg-info/entry_points.txt +0 -0
  37. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/src/multi_claude_cli.egg-info/top_level.txt +0 -0
  38. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_accounts.py +0 -0
  39. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_completion.py +0 -0
  40. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_config.py +0 -0
  41. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_doctor.py +0 -0
  42. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_everyday.py +0 -0
  43. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_fsutil.py +0 -0
  44. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_handoff.py +0 -0
  45. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_identity.py +0 -0
  46. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_install.py +0 -0
  47. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_mcp.py +0 -0
  48. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_migrate.py +0 -0
  49. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_onboarding.py +0 -0
  50. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_packaging.py +0 -0
  51. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_platform.py +0 -0
  52. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_rename.py +0 -0
  53. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_routes.py +0 -0
  54. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_set.py +0 -0
  55. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_shared_exclude.py +0 -0
  56. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_statusline.py +0 -0
  57. {multi_claude_cli-0.5.0 → multi_claude_cli-0.7.0}/tests/test_usability.py +0 -0
  58. {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.5.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
- No. It never reads, copies or deletes credentials; its only keychain call checks whether an item exists. On macOS a login is tied to the account directory's path, which is why `rename` keeps the directory.
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
- No. It never reads, copies or deletes credentials; its only keychain call checks whether an item exists. On macOS a login is tied to the account directory's path, which is why `rename` keeps the directory.
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
 
@@ -1,3 +1,3 @@
1
1
  """multi-claude:管理多个 Claude Code 账号的配置目录、启动命令、代理与启动参数。"""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.7.0"
@@ -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, restore,
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] == "run":
311
- # run 的命令从第一个 `--` 之后原样取出;没有 `--` 与 `--` 后为空都表示“没有命令”(运行 claude)。
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), status))
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