multi-codex 0.8.0__tar.gz → 0.10.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 (48) hide show
  1. {multi_codex-0.8.0/src/multi_codex.egg-info → multi_codex-0.10.0}/PKG-INFO +33 -9
  2. {multi_codex-0.8.0 → multi_codex-0.10.0}/README.md +32 -8
  3. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/__init__.py +1 -1
  4. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/accounts.py +72 -5
  5. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/apps.py +8 -5
  6. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/cli.py +156 -8
  7. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/completion.py +2 -2
  8. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/config.py +70 -3
  9. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/doctor.py +3 -2
  10. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/launcher.py +7 -1
  11. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/migrate.py +2 -2
  12. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/platform.py +2 -1
  13. multi_codex-0.10.0/src/multi_codex/router.py +121 -0
  14. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/shared.py +4 -0
  15. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/switch.py +30 -6
  16. {multi_codex-0.8.0 → multi_codex-0.10.0/src/multi_codex.egg-info}/PKG-INFO +33 -9
  17. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex.egg-info/SOURCES.txt +3 -0
  18. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_ergonomics.py +11 -8
  19. multi_codex-0.10.0/tests/test_isolation.py +428 -0
  20. multi_codex-0.10.0/tests/test_router.py +370 -0
  21. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_switch.py +20 -3
  22. {multi_codex-0.8.0 → multi_codex-0.10.0}/LICENSE +0 -0
  23. {multi_codex-0.8.0 → multi_codex-0.10.0}/pyproject.toml +0 -0
  24. {multi_codex-0.8.0 → multi_codex-0.10.0}/setup.cfg +0 -0
  25. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/__main__.py +0 -0
  26. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/actions.py +0 -0
  27. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/binding.py +0 -0
  28. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/fsutil.py +0 -0
  29. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/identity.py +0 -0
  30. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/lock.py +0 -0
  31. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/shellpath.py +0 -0
  32. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex/usage.py +0 -0
  33. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex.egg-info/dependency_links.txt +0 -0
  34. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex.egg-info/entry_points.txt +0 -0
  35. {multi_codex-0.8.0 → multi_codex-0.10.0}/src/multi_codex.egg-info/top_level.txt +0 -0
  36. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_accounts.py +0 -0
  37. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_apps.py +0 -0
  38. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_binding.py +0 -0
  39. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_config.py +0 -0
  40. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_config_copy.py +0 -0
  41. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_everyday.py +0 -0
  42. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_fsutil.py +0 -0
  43. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_insight.py +0 -0
  44. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_install.py +0 -0
  45. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_migrate.py +0 -0
  46. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_onboarding.py +0 -0
  47. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_onboarding_commands.py +0 -0
  48. {multi_codex-0.8.0 → multi_codex-0.10.0}/tests/test_platform.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-codex
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: Run several Codex CLI accounts side by side: separate CODEX_HOME directories, launchers and proxies.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/jakoes-wu/multi-codex
@@ -134,7 +134,7 @@ Without a name, the account is named after the e-mail address in `~/.codex/auth.
134
134
  | Open VS Code with an account | `multi-codex code work ~/src/project` | [VS Code and the desktop app](#vs-code-and-the-desktop-app-experimental) |
135
135
  | Open the desktop app with an account (macOS) | `multi-codex app work` | [VS Code and the desktop app](#vs-code-and-the-desktop-app-experimental) |
136
136
  | Change the account that plain `codex` and the Dock apps use | `multi-codex use work` (after `migrate-default`) | [Default account](#default-account) |
137
- | Always use one account inside a project | In the project directory: `multi-codex bind work`, then `multi-codex run` | [Directory bindings](#directory-bindings) |
137
+ | Always use one account inside a project | In the project directory: `multi-codex bind work`, then `codex-auto` | [Directory bindings](#directory-bindings) |
138
138
  | Set or change an account's proxy | `multi-codex set work --proxy 7901` | [Proxy values](#proxy-values) |
139
139
  | Share `AGENTS.md`, skills and rules between accounts | Put them in `~/.codex-shared`, then run `multi-codex set work --shared` | [Shared resources](#shared-resources) |
140
140
  | Start a new account with another account's settings | `multi-codex add new --config-from work` | [Copying settings](#copying-settings-from-another-account) |
@@ -142,6 +142,7 @@ Without a name, the account is named after the e-mail address in `~/.codex/auth.
142
142
  | Set up all accounts on a new machine | `curl -fsSL https://raw.githubusercontent.com/jakoes-wu/multi-codex/main/install.sh \| sh -s -- --config accounts.json` | [Declarative setup](#declarative-setup-with-apply) |
143
143
  | Get tab completion | `eval "$(multi-codex completion zsh)"` | [Shell completion](#shell-completion) |
144
144
  | See what a command would change | add `--dry-run` | [Commands](#commands) |
145
+ | Rename an account | `multi-codex rename work client-a` (the directory and login stay) | [Renaming accounts](#renaming-accounts) |
145
146
  | Remove an account | `multi-codex remove work` (the directory is kept) | [Commands](#commands) |
146
147
 
147
148
  More questions are answered in the [FAQ](#faq).
@@ -159,8 +160,9 @@ More questions are answered in the [FAQ](#faq).
159
160
  | ---- | ---- |
160
161
  | `multi-codex init [--root DIR] [--bin-dir DIR] [--shared-dir DIR] [--shared-items A,B]` | Create or change global settings. |
161
162
  | `multi-codex migrate-default [NAME] [--source DIR] [--copy] [--keep-backup] [--proxy P] [--skip-process-check] [--accept-relogin]` | Turn the default directory into an account. Without NAME, the e-mail address in its `auth.json` is used. |
162
- | `multi-codex add NAME [--proxy P] [--shared [DIR] \| --no-shared] [--adopt] [--config-from OTHER]` | Add an account, adopt an existing directory, or change its options. `--config-from` copies `config.toml` from another account once. |
163
- | `multi-codex set NAME [--proxy P] [--shared [DIR] \| --no-shared] [--adopt] [--config-from OTHER]` | Change an existing account; same options as `add`, but never creates one. |
163
+ | `multi-codex add NAME [--proxy P] [--shared [DIR] \| --no-shared] [--shared-exclude ITEM] [--shared-include ITEM] [--adopt] [--config-from OTHER]` | Add an account, adopt an existing directory, or change its options. `--config-from` copies `config.toml` from another account once. |
164
+ | `multi-codex set NAME [same options as add]` | Change an existing account; same options as `add`, but never creates one. |
165
+ | `multi-codex rename OLD NEW` | Rename an account and its launcher; the directory, login and links stay. |
164
166
  | `multi-codex login NAME [-- ARGS]` | Run `codex login` with an account's environment; arguments after `--` go to `codex login`. Does not need `~/.local/bin` on `PATH`. |
165
167
  | `multi-codex proxy NAME PORT\|URL\|off\|inherit` | Set an account's proxy. |
166
168
  | `multi-codex remove NAME` | Unregister an account and delete its launcher. **The account directory is kept.** |
@@ -170,6 +172,7 @@ More questions are answered in the [FAQ](#faq).
170
172
  | `multi-codex doctor [--json]` | Check the installation, configuration and accounts. Read-only. |
171
173
  | `multi-codex run [NAME] [-- COMMAND ...]` | Run a command (default: `codex`) with an account's environment. Without NAME, the account bound to the current directory is used. |
172
174
  | `multi-codex bind [NAME [DIR]]` / `unbind [DIR]` | Bind a directory to an account, list bindings, or remove one. |
175
+ | `multi-codex which [DIR]` | Show which account a directory uses (the same one `codex-auto` and `run` pick). |
173
176
  | `multi-codex code NAME [PATH] [-- ARGS]` | Open VS Code for an account (experimental). |
174
177
  | `multi-codex app NAME` | Open the Codex desktop app for an account (macOS, experimental). |
175
178
  | `multi-codex path NAME` | Print an account's directory. |
@@ -178,7 +181,7 @@ More questions are answered in the [FAQ](#faq).
178
181
  | `multi-codex restore NAME [--skip-process-check] [--accept-relogin]` | Undo `migrate-default`: move the account back to `~/.codex`. |
179
182
  | `multi-codex completion bash\|zsh\|fish` | Print a shell completion script. |
180
183
 
181
- Every write command accepts `--dry-run`. `init`, `add`, `set`, `proxy`, `remove`, `apply`, `bind`, `unbind` and `env` print only the items they change (or `already up to date`); `-v` / `--verbose` also prints unchanged items. For `list`, `-v` means the full table instead. `list`, `usage` and `doctor` never change anything; with `--json` they print a single JSON object on stdout (with a `"version": 1` field) and keep warnings on stderr. Use `--json` in scripts: the table layout is not guaranteed to stay the same.
184
+ Every write command accepts `--dry-run`. `init`, `add`, `set`, `rename`, `proxy`, `remove`, `apply`, `bind`, `unbind` and `env` print only the items they change (or `already up to date`); `-v` / `--verbose` also prints unchanged items. For `list`, `-v` means the full table instead. `list`, `usage` and `doctor` never change anything; with `--json` they print a single JSON object on stdout (with a `"version": 1` field) and keep warnings on stderr. Use `--json` in scripts: the table layout is not guaranteed to stay the same.
182
185
 
183
186
  ### Login and usage
184
187
 
@@ -216,7 +219,7 @@ Subcommands, options and registered account names (including e-mail addresses) a
216
219
 
217
220
  ### Running other commands
218
221
 
219
- `multi-codex run NAME -- COMMAND ...` runs any command with exactly the environment of `codex-NAME` (`CODEX_HOME`, proxy, extra variables). Without a command it runs `codex`. Everything after the first `--` is passed through unchanged; the exit code is the command's. `multi-codex path NAME` prints the account directory.
222
+ `multi-codex run NAME -- COMMAND ...` runs any command with exactly the environment of `codex-NAME` (`CODEX_HOME`, proxy, extra variables). Without a command it runs `codex`. Everything after the first `--` is passed through unchanged; the exit code is the command's. `multi-codex path NAME` prints the account directory. For example, to manage an account's MCP servers: `multi-codex run work -- codex mcp list`.
220
223
 
221
224
  ### Directory bindings
222
225
 
@@ -225,14 +228,26 @@ cd ~/work/project && multi-codex bind work # this directory and everything b
225
228
  multi-codex run -- codex resume # no account name needed here
226
229
  multi-codex bind # list bindings; * marks the one in effect here
227
230
  multi-codex unbind # remove the binding of the current directory
231
+ codex-auto # runs codex with the bound account
232
+ multi-codex which # prints the account this directory uses
228
233
  ```
229
234
 
235
+ While at least one binding exists, multi-codex generates `codex-auto` next to the other launchers. It looks up the nearest bound directory exactly like `run` and starts that account's launcher; outside any bound directory it runs plain `codex` (the default account, see [Default account](#default-account)) and says so on stderr. It is updated whenever bindings or accounts change and removed with the last binding. An account named `auto` cannot coexist with bindings: rename it first (`multi-codex rename auto NEW`).
236
+
230
237
  `run` without an account name walks up from the current directory and uses the nearest bound directory. Bindings are stored in `config.json` (not in your project), keyed by the real path of the directory (links resolved, the on-disk letter case used on case-insensitive file systems). `apply -f` keeps the current bindings; removing an account (`remove`, `restore`, `apply -f`) also removes its bindings. Older versions of multi-codex drop the `bindings` field on their next write.
231
238
 
232
239
  ### Copying settings from another account
233
240
 
234
241
  `multi-codex add new --config-from work` copies `config.toml` from `work` into `new` once; afterwards the two files are independent. An existing `config.toml` with different content is a conflict (nothing is written), and so is copying into an account that shares `config.toml`. The copy includes everything in the file, such as `cli_auth_credentials_store` or absolute paths that point into the other account.
235
242
 
243
+ ### Renaming accounts
244
+
245
+ ```sh
246
+ multi-codex rename work client-a
247
+ ```
248
+
249
+ The account and its launcher get the new name (`codex-client-a`; `codex-work` is deleted), and directory bindings, `codex-auto` and the default account follow. The directory stays where it is, so the login, sessions and shared links are kept; `config.json` records it as `"dir": "work"` (in `list --json`, `dir` is the full path instead). A name that is still used as another account's directory, such as `work` after this rename, cannot be given to a new account. Renaming only the letter case is not supported. Before downgrading to a version older than 0.9, rename the account back: older versions ignore `dir` and would use `<root>/client-a`.
250
+
236
251
  ### Per-account environment variables
237
252
 
238
253
  ```sh
@@ -242,6 +257,8 @@ multi-codex env work --unset TERM_PROGRAM
242
257
  multi-codex env work --clear
243
258
  ```
244
259
 
260
+ Launchers (and `run`, `login`, `code` and `usage --live`, which use the same environment) clear `CODEX_API_KEY`, `CODEX_ACCESS_TOKEN` and `CODEX_SQLITE_HOME` inherited from your shell, so one exported API key or database location does not leak into every account. An account that needs an API key sets it here, for example `multi-codex env work CODEX_API_KEY=sk-…`; per-account variables are applied after the clearing.
261
+
245
262
  The variables are stored in `config.json` (`accounts.<name>.env`) and written into the launcher. Values are used literally (no `$VAR` expansion). `CODEX_HOME` and the proxy variables are reserved: use `multi-codex proxy` for proxies. A launcher with environment variables is readable only by you (mode 0700), and `list --json` shows only the variable names; still, the values are stored in plain text, so do not put secrets there that need stronger protection. Older versions of multi-codex ignore the `env` field and drop it on their next write; run `multi-codex env NAME --clear` before downgrading.
246
263
 
247
264
  ### VS Code and the desktop app (experimental)
@@ -327,7 +344,7 @@ Progress is recorded in `~/.config/multi-codex/migrate-journal.json`. If the mig
327
344
 
328
345
  Sockets and FIFOs (runtime files such as `ipc.sock`) are not copied in copy mode. On macOS, copy mode does not preserve extended attributes.
329
346
 
330
- If `CODEX_HOME`, `CODEX_SQLITE_HOME`, `CODEX_API_KEY` or `CODEX_ACCESS_TOKEN` is set in your environment, multi-codex warns you: these variables override or bypass per-account isolation.
347
+ If `CODEX_HOME`, `CODEX_SQLITE_HOME`, `CODEX_API_KEY` or `CODEX_ACCESS_TOKEN` is set in your environment, multi-codex warns you: launchers set or clear them, but plain `codex` still uses them.
331
348
 
332
349
  ### Undoing a migration
333
350
 
@@ -361,6 +378,10 @@ multi-codex creates the missing links and remembers which links it created. Turn
361
378
 
362
379
  If you already linked an account to the shared directory by hand, `multi-codex set NAME --shared --adopt` takes those links over without recreating them: from then on, turning sharing off removes them as well. Only links that already point to the matching shared item are adopted.
363
380
 
381
+ One account can opt out of some shared items: `multi-codex set work --shared-exclude skills` removes the `skills` link multi-codex created in `work` (as if sharing were off for that item) and no longer creates it; `--shared-include skills` undoes that. Both options can be repeated, names match `shared.items` exactly, and `list` shows the exclusions as `yes (not: skills)`. To exclude `config.toml` and then copy another account's settings into it, run `--shared-exclude config.toml` and `--config-from` as two separate commands.
382
+
383
+ Items that hold one account's own state can never be shared: `shared.items` must not contain the entries marked "No" in the table below. A configuration that lists one fails to load and names the item; remove it from `config.json`.
384
+
364
385
  ### What can be shared
365
386
 
366
387
  Based on the Codex source code (openai/codex at `6b4daafd`):
@@ -375,7 +396,7 @@ Based on the Codex source code (openai/codex at `6b4daafd`):
375
396
  | `history.jsonl` | Prompt history | Yes | Reads and writes are file-locked; the histories of the accounts are merged. |
376
397
  | `auth.json`, `secrets/`, `.credentials.json`, `.env` | Credentials | **No** | They are the account. |
377
398
  | `installation_id` | Installation identifier | No | Sent with requests; sharing makes several accounts look like one installation. |
378
- | `*.sqlite` (`state_5.sqlite`, …) | Threads, logs, memories | **No** | `state_5.sqlite` records account IDs. |
399
+ | `*.sqlite`, `*.sqlite-wal`, `*.sqlite-shm` (`state_5.sqlite`, …), `sqlite/` | Threads, logs, memories | **No** | `state_5.sqlite` records account IDs. |
379
400
  | `sessions/`, `archived_sessions/`, `session_index.jsonl` | Session logs | No | The index has only an in-process lock; sessions record the account that created them. |
380
401
  | `models_cache.json`, `cache/` | Caches | Not needed | Keyed by the account; a mismatch is a cache miss. |
381
402
  | `app-server-control/`, `app-server-daemon/`, `packages/`, `tmp/`, `.tmp/`, `log/`, `shell_snapshots/` | Runtime state | No | Per process or per session. |
@@ -438,8 +459,11 @@ Run the installer again; configuration, accounts and launchers are not touched:
438
459
  ```sh
439
460
  curl -fsSL https://raw.githubusercontent.com/jakoes-wu/multi-codex/main/install.sh | sh
440
461
  multi-codex --version
462
+ multi-codex apply # after upgrading from 0.8 or older: rewrites the launchers
441
463
  ```
442
464
 
465
+ 0.9 changed what the launchers contain. Until `multi-codex apply` (or any other write command) rewrites them, `list` shows `launcher stale` and `usage --live` refuses to run; the old launchers keep working.
466
+
443
467
  ## Exit codes
444
468
 
445
469
  | Code | Meaning |
@@ -473,7 +497,7 @@ The tool goes to `~/.local/share/multi-codex` and the `multi-codex` command to `
473
497
  curl -fsSL https://raw.githubusercontent.com/jakoes-wu/multi-codex/main/install.sh | sh -s -- --uninstall # without a clone
474
498
  ```
475
499
 
476
- This removes the tool only. Your configuration, account directories and `codex-<name>` launchers stay; the launchers keep working because they do not depend on multi-codex.
500
+ This removes the tool only. Your configuration, account directories, `codex-<name>` launchers and `codex-auto` stay; they keep working because they do not depend on multi-codex.
477
501
 
478
502
  ## Contributing
479
503
 
@@ -110,7 +110,7 @@ Without a name, the account is named after the e-mail address in `~/.codex/auth.
110
110
  | Open VS Code with an account | `multi-codex code work ~/src/project` | [VS Code and the desktop app](#vs-code-and-the-desktop-app-experimental) |
111
111
  | Open the desktop app with an account (macOS) | `multi-codex app work` | [VS Code and the desktop app](#vs-code-and-the-desktop-app-experimental) |
112
112
  | Change the account that plain `codex` and the Dock apps use | `multi-codex use work` (after `migrate-default`) | [Default account](#default-account) |
113
- | Always use one account inside a project | In the project directory: `multi-codex bind work`, then `multi-codex run` | [Directory bindings](#directory-bindings) |
113
+ | Always use one account inside a project | In the project directory: `multi-codex bind work`, then `codex-auto` | [Directory bindings](#directory-bindings) |
114
114
  | Set or change an account's proxy | `multi-codex set work --proxy 7901` | [Proxy values](#proxy-values) |
115
115
  | Share `AGENTS.md`, skills and rules between accounts | Put them in `~/.codex-shared`, then run `multi-codex set work --shared` | [Shared resources](#shared-resources) |
116
116
  | Start a new account with another account's settings | `multi-codex add new --config-from work` | [Copying settings](#copying-settings-from-another-account) |
@@ -118,6 +118,7 @@ Without a name, the account is named after the e-mail address in `~/.codex/auth.
118
118
  | Set up all accounts on a new machine | `curl -fsSL https://raw.githubusercontent.com/jakoes-wu/multi-codex/main/install.sh \| sh -s -- --config accounts.json` | [Declarative setup](#declarative-setup-with-apply) |
119
119
  | Get tab completion | `eval "$(multi-codex completion zsh)"` | [Shell completion](#shell-completion) |
120
120
  | See what a command would change | add `--dry-run` | [Commands](#commands) |
121
+ | Rename an account | `multi-codex rename work client-a` (the directory and login stay) | [Renaming accounts](#renaming-accounts) |
121
122
  | Remove an account | `multi-codex remove work` (the directory is kept) | [Commands](#commands) |
122
123
 
123
124
  More questions are answered in the [FAQ](#faq).
@@ -135,8 +136,9 @@ More questions are answered in the [FAQ](#faq).
135
136
  | ---- | ---- |
136
137
  | `multi-codex init [--root DIR] [--bin-dir DIR] [--shared-dir DIR] [--shared-items A,B]` | Create or change global settings. |
137
138
  | `multi-codex migrate-default [NAME] [--source DIR] [--copy] [--keep-backup] [--proxy P] [--skip-process-check] [--accept-relogin]` | Turn the default directory into an account. Without NAME, the e-mail address in its `auth.json` is used. |
138
- | `multi-codex add NAME [--proxy P] [--shared [DIR] \| --no-shared] [--adopt] [--config-from OTHER]` | Add an account, adopt an existing directory, or change its options. `--config-from` copies `config.toml` from another account once. |
139
- | `multi-codex set NAME [--proxy P] [--shared [DIR] \| --no-shared] [--adopt] [--config-from OTHER]` | Change an existing account; same options as `add`, but never creates one. |
139
+ | `multi-codex add NAME [--proxy P] [--shared [DIR] \| --no-shared] [--shared-exclude ITEM] [--shared-include ITEM] [--adopt] [--config-from OTHER]` | Add an account, adopt an existing directory, or change its options. `--config-from` copies `config.toml` from another account once. |
140
+ | `multi-codex set NAME [same options as add]` | Change an existing account; same options as `add`, but never creates one. |
141
+ | `multi-codex rename OLD NEW` | Rename an account and its launcher; the directory, login and links stay. |
140
142
  | `multi-codex login NAME [-- ARGS]` | Run `codex login` with an account's environment; arguments after `--` go to `codex login`. Does not need `~/.local/bin` on `PATH`. |
141
143
  | `multi-codex proxy NAME PORT\|URL\|off\|inherit` | Set an account's proxy. |
142
144
  | `multi-codex remove NAME` | Unregister an account and delete its launcher. **The account directory is kept.** |
@@ -146,6 +148,7 @@ More questions are answered in the [FAQ](#faq).
146
148
  | `multi-codex doctor [--json]` | Check the installation, configuration and accounts. Read-only. |
147
149
  | `multi-codex run [NAME] [-- COMMAND ...]` | Run a command (default: `codex`) with an account's environment. Without NAME, the account bound to the current directory is used. |
148
150
  | `multi-codex bind [NAME [DIR]]` / `unbind [DIR]` | Bind a directory to an account, list bindings, or remove one. |
151
+ | `multi-codex which [DIR]` | Show which account a directory uses (the same one `codex-auto` and `run` pick). |
149
152
  | `multi-codex code NAME [PATH] [-- ARGS]` | Open VS Code for an account (experimental). |
150
153
  | `multi-codex app NAME` | Open the Codex desktop app for an account (macOS, experimental). |
151
154
  | `multi-codex path NAME` | Print an account's directory. |
@@ -154,7 +157,7 @@ More questions are answered in the [FAQ](#faq).
154
157
  | `multi-codex restore NAME [--skip-process-check] [--accept-relogin]` | Undo `migrate-default`: move the account back to `~/.codex`. |
155
158
  | `multi-codex completion bash\|zsh\|fish` | Print a shell completion script. |
156
159
 
157
- Every write command accepts `--dry-run`. `init`, `add`, `set`, `proxy`, `remove`, `apply`, `bind`, `unbind` and `env` print only the items they change (or `already up to date`); `-v` / `--verbose` also prints unchanged items. For `list`, `-v` means the full table instead. `list`, `usage` and `doctor` never change anything; with `--json` they print a single JSON object on stdout (with a `"version": 1` field) and keep warnings on stderr. Use `--json` in scripts: the table layout is not guaranteed to stay the same.
160
+ Every write command accepts `--dry-run`. `init`, `add`, `set`, `rename`, `proxy`, `remove`, `apply`, `bind`, `unbind` and `env` print only the items they change (or `already up to date`); `-v` / `--verbose` also prints unchanged items. For `list`, `-v` means the full table instead. `list`, `usage` and `doctor` never change anything; with `--json` they print a single JSON object on stdout (with a `"version": 1` field) and keep warnings on stderr. Use `--json` in scripts: the table layout is not guaranteed to stay the same.
158
161
 
159
162
  ### Login and usage
160
163
 
@@ -192,7 +195,7 @@ Subcommands, options and registered account names (including e-mail addresses) a
192
195
 
193
196
  ### Running other commands
194
197
 
195
- `multi-codex run NAME -- COMMAND ...` runs any command with exactly the environment of `codex-NAME` (`CODEX_HOME`, proxy, extra variables). Without a command it runs `codex`. Everything after the first `--` is passed through unchanged; the exit code is the command's. `multi-codex path NAME` prints the account directory.
198
+ `multi-codex run NAME -- COMMAND ...` runs any command with exactly the environment of `codex-NAME` (`CODEX_HOME`, proxy, extra variables). Without a command it runs `codex`. Everything after the first `--` is passed through unchanged; the exit code is the command's. `multi-codex path NAME` prints the account directory. For example, to manage an account's MCP servers: `multi-codex run work -- codex mcp list`.
196
199
 
197
200
  ### Directory bindings
198
201
 
@@ -201,14 +204,26 @@ cd ~/work/project && multi-codex bind work # this directory and everything b
201
204
  multi-codex run -- codex resume # no account name needed here
202
205
  multi-codex bind # list bindings; * marks the one in effect here
203
206
  multi-codex unbind # remove the binding of the current directory
207
+ codex-auto # runs codex with the bound account
208
+ multi-codex which # prints the account this directory uses
204
209
  ```
205
210
 
211
+ While at least one binding exists, multi-codex generates `codex-auto` next to the other launchers. It looks up the nearest bound directory exactly like `run` and starts that account's launcher; outside any bound directory it runs plain `codex` (the default account, see [Default account](#default-account)) and says so on stderr. It is updated whenever bindings or accounts change and removed with the last binding. An account named `auto` cannot coexist with bindings: rename it first (`multi-codex rename auto NEW`).
212
+
206
213
  `run` without an account name walks up from the current directory and uses the nearest bound directory. Bindings are stored in `config.json` (not in your project), keyed by the real path of the directory (links resolved, the on-disk letter case used on case-insensitive file systems). `apply -f` keeps the current bindings; removing an account (`remove`, `restore`, `apply -f`) also removes its bindings. Older versions of multi-codex drop the `bindings` field on their next write.
207
214
 
208
215
  ### Copying settings from another account
209
216
 
210
217
  `multi-codex add new --config-from work` copies `config.toml` from `work` into `new` once; afterwards the two files are independent. An existing `config.toml` with different content is a conflict (nothing is written), and so is copying into an account that shares `config.toml`. The copy includes everything in the file, such as `cli_auth_credentials_store` or absolute paths that point into the other account.
211
218
 
219
+ ### Renaming accounts
220
+
221
+ ```sh
222
+ multi-codex rename work client-a
223
+ ```
224
+
225
+ The account and its launcher get the new name (`codex-client-a`; `codex-work` is deleted), and directory bindings, `codex-auto` and the default account follow. The directory stays where it is, so the login, sessions and shared links are kept; `config.json` records it as `"dir": "work"` (in `list --json`, `dir` is the full path instead). A name that is still used as another account's directory, such as `work` after this rename, cannot be given to a new account. Renaming only the letter case is not supported. Before downgrading to a version older than 0.9, rename the account back: older versions ignore `dir` and would use `<root>/client-a`.
226
+
212
227
  ### Per-account environment variables
213
228
 
214
229
  ```sh
@@ -218,6 +233,8 @@ multi-codex env work --unset TERM_PROGRAM
218
233
  multi-codex env work --clear
219
234
  ```
220
235
 
236
+ Launchers (and `run`, `login`, `code` and `usage --live`, which use the same environment) clear `CODEX_API_KEY`, `CODEX_ACCESS_TOKEN` and `CODEX_SQLITE_HOME` inherited from your shell, so one exported API key or database location does not leak into every account. An account that needs an API key sets it here, for example `multi-codex env work CODEX_API_KEY=sk-…`; per-account variables are applied after the clearing.
237
+
221
238
  The variables are stored in `config.json` (`accounts.<name>.env`) and written into the launcher. Values are used literally (no `$VAR` expansion). `CODEX_HOME` and the proxy variables are reserved: use `multi-codex proxy` for proxies. A launcher with environment variables is readable only by you (mode 0700), and `list --json` shows only the variable names; still, the values are stored in plain text, so do not put secrets there that need stronger protection. Older versions of multi-codex ignore the `env` field and drop it on their next write; run `multi-codex env NAME --clear` before downgrading.
222
239
 
223
240
  ### VS Code and the desktop app (experimental)
@@ -303,7 +320,7 @@ Progress is recorded in `~/.config/multi-codex/migrate-journal.json`. If the mig
303
320
 
304
321
  Sockets and FIFOs (runtime files such as `ipc.sock`) are not copied in copy mode. On macOS, copy mode does not preserve extended attributes.
305
322
 
306
- If `CODEX_HOME`, `CODEX_SQLITE_HOME`, `CODEX_API_KEY` or `CODEX_ACCESS_TOKEN` is set in your environment, multi-codex warns you: these variables override or bypass per-account isolation.
323
+ If `CODEX_HOME`, `CODEX_SQLITE_HOME`, `CODEX_API_KEY` or `CODEX_ACCESS_TOKEN` is set in your environment, multi-codex warns you: launchers set or clear them, but plain `codex` still uses them.
307
324
 
308
325
  ### Undoing a migration
309
326
 
@@ -337,6 +354,10 @@ multi-codex creates the missing links and remembers which links it created. Turn
337
354
 
338
355
  If you already linked an account to the shared directory by hand, `multi-codex set NAME --shared --adopt` takes those links over without recreating them: from then on, turning sharing off removes them as well. Only links that already point to the matching shared item are adopted.
339
356
 
357
+ One account can opt out of some shared items: `multi-codex set work --shared-exclude skills` removes the `skills` link multi-codex created in `work` (as if sharing were off for that item) and no longer creates it; `--shared-include skills` undoes that. Both options can be repeated, names match `shared.items` exactly, and `list` shows the exclusions as `yes (not: skills)`. To exclude `config.toml` and then copy another account's settings into it, run `--shared-exclude config.toml` and `--config-from` as two separate commands.
358
+
359
+ Items that hold one account's own state can never be shared: `shared.items` must not contain the entries marked "No" in the table below. A configuration that lists one fails to load and names the item; remove it from `config.json`.
360
+
340
361
  ### What can be shared
341
362
 
342
363
  Based on the Codex source code (openai/codex at `6b4daafd`):
@@ -351,7 +372,7 @@ Based on the Codex source code (openai/codex at `6b4daafd`):
351
372
  | `history.jsonl` | Prompt history | Yes | Reads and writes are file-locked; the histories of the accounts are merged. |
352
373
  | `auth.json`, `secrets/`, `.credentials.json`, `.env` | Credentials | **No** | They are the account. |
353
374
  | `installation_id` | Installation identifier | No | Sent with requests; sharing makes several accounts look like one installation. |
354
- | `*.sqlite` (`state_5.sqlite`, …) | Threads, logs, memories | **No** | `state_5.sqlite` records account IDs. |
375
+ | `*.sqlite`, `*.sqlite-wal`, `*.sqlite-shm` (`state_5.sqlite`, …), `sqlite/` | Threads, logs, memories | **No** | `state_5.sqlite` records account IDs. |
355
376
  | `sessions/`, `archived_sessions/`, `session_index.jsonl` | Session logs | No | The index has only an in-process lock; sessions record the account that created them. |
356
377
  | `models_cache.json`, `cache/` | Caches | Not needed | Keyed by the account; a mismatch is a cache miss. |
357
378
  | `app-server-control/`, `app-server-daemon/`, `packages/`, `tmp/`, `.tmp/`, `log/`, `shell_snapshots/` | Runtime state | No | Per process or per session. |
@@ -414,8 +435,11 @@ Run the installer again; configuration, accounts and launchers are not touched:
414
435
  ```sh
415
436
  curl -fsSL https://raw.githubusercontent.com/jakoes-wu/multi-codex/main/install.sh | sh
416
437
  multi-codex --version
438
+ multi-codex apply # after upgrading from 0.8 or older: rewrites the launchers
417
439
  ```
418
440
 
441
+ 0.9 changed what the launchers contain. Until `multi-codex apply` (or any other write command) rewrites them, `list` shows `launcher stale` and `usage --live` refuses to run; the old launchers keep working.
442
+
419
443
  ## Exit codes
420
444
 
421
445
  | Code | Meaning |
@@ -449,7 +473,7 @@ The tool goes to `~/.local/share/multi-codex` and the `multi-codex` command to `
449
473
  curl -fsSL https://raw.githubusercontent.com/jakoes-wu/multi-codex/main/install.sh | sh -s -- --uninstall # without a clone
450
474
  ```
451
475
 
452
- This removes the tool only. Your configuration, account directories and `codex-<name>` launchers stay; the launchers keep working because they do not depend on multi-codex.
476
+ This removes the tool only. Your configuration, account directories, `codex-<name>` launchers and `codex-auto` stay; they keep working because they do not depend on multi-codex.
453
477
 
454
478
  ## Contributing
455
479
 
@@ -1,3 +1,3 @@
1
1
  """multi-codex:管理多个 Codex CLI 账号目录、启动命令与代理设置。"""
2
2
 
3
- __version__ = "0.8.0"
3
+ __version__ = "0.10.0"
@@ -14,7 +14,7 @@
14
14
  import os
15
15
  from typing import Dict, FrozenSet, Iterable, List, Optional, Sequence, Set, Union
16
16
 
17
- from . import launcher, shared
17
+ from . import launcher, router, shared
18
18
  from .actions import (CONFLICT, CREATE, DELETE, SKIP, UNCHANGED, UPDATE, Action, error,
19
19
  has_conflict, info, print_action)
20
20
  from .config import Account, Config, config_path, dump_config, save_config
@@ -32,7 +32,27 @@ NO_ORPHANS: FrozenSet[str] = frozenset()
32
32
 
33
33
 
34
34
  def account_dir(config: Config, name: str) -> str:
35
- return os.path.join(expand(config.root), name)
35
+ """账号目录。用账号记录的目录名(rename 后与账号名不同);未登记的名字按名字本身拼。"""
36
+ account = config.find(name)
37
+ return os.path.join(expand(config.root), account.dir_name if account is not None else name)
38
+
39
+
40
+ def dir_conflicts(config: Config) -> List[str]:
41
+ """两个账号会落到同一个目录时的说明文字(规则同 config._check_dir_names)。
42
+
43
+ parse_config 只检查读入的文件;这里检查命令算出的新配置,典型是 rename work job 之后 add work。
44
+ """
45
+ problems = []
46
+ for account in config.accounts.values():
47
+ for other in config.accounts.values():
48
+ if other is account:
49
+ continue
50
+ if account.dir_name.casefold() == other.dir_name.casefold() or \
51
+ account.name.casefold() == other.dir_name.casefold():
52
+ problems.append("account {!r} would use the directory of account {!r}".format(
53
+ account.name, other.name))
54
+ break
55
+ return problems
36
56
 
37
57
 
38
58
  def plan(old: Config, new: Config, *, config_exists: bool = True,
@@ -60,6 +80,9 @@ def plan(old: Config, new: Config, *, config_exists: bool = True,
60
80
  "cannot change root from {} to {} while accounts are registered".format(
61
81
  old_root, new_root)))
62
82
 
83
+ for problem in dir_conflicts(new):
84
+ actions.append(Action(CONFLICT, "config", config_path(), problem))
85
+
63
86
  planned_deletes: Set[str] = set()
64
87
  for account in new.accounts.values():
65
88
  old_account = old.find(account.name)
@@ -70,7 +93,7 @@ def plan(old: Config, new: Config, *, config_exists: bool = True,
70
93
  "account {!r} is registered as {!r}; renaming is not supported".format(
71
94
  account.name, old_account.name)))
72
95
  continue
73
- directory = os.path.join(new_root, account.name)
96
+ directory = os.path.join(new_root, account.dir_name)
74
97
  actions.extend(_plan_account_dir(directory, assumed))
75
98
  actions.extend(_plan_launcher(new_bin, account.name, directory, account.proxy, account.env))
76
99
  if old_account is not None and old_bin != new_bin:
@@ -79,11 +102,15 @@ def plan(old: Config, new: Config, *, config_exists: bool = True,
79
102
  actions.extend(shared.plan_shared(new, account, directory, old.shared_dir,
80
103
  adopt=account.name.casefold() in adopt_accounts))
81
104
 
105
+ new_dirs = {account.dir_name.casefold() for account in new.accounts.values()}
82
106
  for old_account in old.accounts.values():
83
107
  if new.find(old_account.name) is None:
84
108
  actions.extend(_plan_launcher_delete(old_bin, old_account.name, planned_deletes,
85
109
  "account removed"))
86
- actions.extend(shared.plan_remove_links(old_account, os.path.join(old_root, old_account.name),
110
+ # rename:旧名字从配置里消失,但目录由新名字接着用,共享链接必须留着。
111
+ if old_account.dir_name.casefold() in new_dirs:
112
+ continue
113
+ actions.extend(shared.plan_remove_links(old_account, os.path.join(old_root, old_account.dir_name),
87
114
  old.shared_dir))
88
115
 
89
116
  if orphan_scope:
@@ -94,6 +121,8 @@ def plan(old: Config, new: Config, *, config_exists: bool = True,
94
121
  actions.extend(_plan_launcher_delete(new_bin, name, planned_deletes,
95
122
  "orphan launcher", path=path))
96
123
 
124
+ actions.extend(_plan_router(old_bin, new_bin, new, planned_deletes))
125
+
97
126
  config_changed = not config_exists or dump_config(old) != dump_config(new)
98
127
  config_action = Action(CREATE if not config_exists else (UPDATE if config_changed else UNCHANGED),
99
128
  "config", config_path())
@@ -121,7 +150,7 @@ def plan_config_copy(config: Config, account: Account, content: str, source_name
121
150
  已有不同内容时不覆盖:那可能是用户已经改过的配置。
122
151
  """
123
152
  target = os.path.join(account_dir(config, account.name), "config.toml")
124
- if account.shared and "config.toml" in config.shared_items:
153
+ if account.shared and "config.toml" in config.shared_items and "config.toml" not in account.shared_exclude:
125
154
  return [Action(CONFLICT, "config-file", target,
126
155
  "config.toml is shared for this account and does not need copying")]
127
156
  kind = entry_kind(target)
@@ -164,6 +193,44 @@ def _plan_launcher(bin_dir: str, name: str, directory: str, proxy: str,
164
193
  return [Action(UPDATE, "launcher", path, run=_write_launcher(path, content, mode))]
165
194
 
166
195
 
196
+ def _plan_router(old_bin: str, new_bin: str, new: Config, planned_deletes: Set[str]) -> List[Action]:
197
+ """codex-auto 入口(feature-auto-launcher §5.1.2):有目录绑定时生成或更新,没有时删除。
198
+
199
+ 必须放在账号启动命令的删除(含孤儿清理)之后计算:rename / remove / restore 名为 auto 的账号时,
200
+ codex-auto 原本是它的启动命令,正被删除,这里要把它当作不存在、重建成入口。
201
+ """
202
+ actions: List[Action] = []
203
+ path = router.router_path(new_bin)
204
+ if old_bin != new_bin:
205
+ old_path = router.router_path(old_bin)
206
+ if router.is_router(old_path):
207
+ actions.append(Action(DELETE, "launcher", old_path, "bin_dir changed", _unlink_file(old_path)))
208
+ if not new.bindings:
209
+ if router.is_router(path):
210
+ actions.append(Action(DELETE, "launcher", path, "no directory bindings", _unlink_file(path)))
211
+ return actions
212
+ owner = new.find(router.ROUTER_NAME)
213
+ if owner is not None:
214
+ return actions + [Action(CONFLICT, "launcher", path,
215
+ "codex-auto is generated from directory bindings; rename the account named {!r} "
216
+ "(multi-codex rename {} NEW) or remove the bindings".format(owner.name, owner.name))]
217
+ content = router.render(new)
218
+ write = _write_launcher(path, content, router.ROUTER_MODE)
219
+ if entry_kind(path) == KIND_MISSING or router.same_file(path, planned_deletes):
220
+ return actions + [Action(CREATE, "launcher", path, run=write)]
221
+ if not router.is_router(path):
222
+ return actions + [Action(CONFLICT, "launcher", path, "already exists and is not the multi-codex router")]
223
+ if launcher.launcher_file_ok(path, content, None):
224
+ return actions + [Action(UNCHANGED, "launcher", path)]
225
+ return actions + [Action(UPDATE, "launcher", path, run=write)]
226
+
227
+
228
+ def _unlink_file(path: str):
229
+ def run() -> None:
230
+ os.unlink(path)
231
+ return run
232
+
233
+
167
234
  def _write_launcher(path: str, content: str, mode: int):
168
235
  def run() -> None:
169
236
  atomic_write(path, content, mode=mode)
@@ -33,15 +33,18 @@ def _private_dir(path: str) -> str:
33
33
  return path
34
34
 
35
35
 
36
- def gui_data_dir(config: Config, name: str, kind: str) -> str:
37
- """返回并创建 <root>/.apps/<名>/<kind>(kind 为 vscode 或 desktop),各级目录都只允许本人访问。"""
36
+ def gui_data_dir(config: Config, dir_name: str, kind: str) -> str:
37
+ """返回并创建 <root>/.apps/<账号目录名>/<kind>(kind 为 vscode 或 desktop),各级目录都只允许本人访问。
38
+
39
+ 按目录名而不是账号名存放:rename 后 VS Code 与桌面端的登录状态、窗口与扩展数据都还在原处。
40
+ """
38
41
  apps_root = _private_dir(os.path.join(expand(config.root), ".apps"))
39
- account_root = _private_dir(os.path.join(apps_root, name))
42
+ account_root = _private_dir(os.path.join(apps_root, dir_name))
40
43
  return _private_dir(os.path.join(account_root, kind))
41
44
 
42
45
 
43
- def desktop_log(config: Config, name: str) -> str:
44
- path = os.path.join(expand(config.root), ".apps", name, "desktop.log")
46
+ def desktop_log(config: Config, dir_name: str) -> str:
47
+ path = os.path.join(expand(config.root), ".apps", dir_name, "desktop.log")
45
48
  fd = os.open(path, os.O_CREAT | os.O_APPEND | os.O_WRONLY, 0o600)
46
49
  os.close(fd)
47
50
  os.chmod(path, 0o600)