claude-dev-env 8.34.0 → 8.35.1
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.
- package/docs/codex-accounts.md +65 -12
- package/package.json +1 -1
- package/scripts/claude_account_profile.py +74 -27
- package/scripts/codex_account_choice.py +385 -24
- package/scripts/dev_env_scripts_constants/claude_account_constants.py +31 -11
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +1 -2
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +57 -2
- package/scripts/dev_env_scripts_constants/test_claude_account_constants.py +0 -6
- package/scripts/test_claude_account_profile.py +115 -3
- package/scripts/test_codex_account_choice.py +334 -1
package/docs/codex-accounts.md
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
# Codex accounts
|
|
2
2
|
|
|
3
|
-
The codex_account_choice picker spreads agent work across
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The codex_account_choice picker spreads agent work across the Codex accounts
|
|
4
|
+
on one machine. Each account signs in under its own Codex home, and every home
|
|
5
|
+
shares the same Codex setup: config, rules, skills, plugins, prompts and agents.
|
|
6
|
+
A job asks the picker which account to use.
|
|
7
7
|
|
|
8
8
|
## Pieces
|
|
9
9
|
|
|
10
10
|
| File | What it does |
|
|
11
11
|
|---|---|
|
|
12
|
-
| `scripts/codex_account_choice.py` | `choose` names the account and tier a job runs on, `check` tells a running job whether its account is still above a floor, `sync` links the shared setup into every account's home |
|
|
12
|
+
| `scripts/codex_account_choice.py` | `choose` names the account and tier a job runs on, `check` tells a running job whether its account is still above a floor, `sync` links the shared setup into every account's home, `setup` and `install` write one launcher per account |
|
|
13
13
|
| `scripts/codex_account_meters.py` | Reads one account's rate-limit windows through `codex app-server` with `CODEX_HOME` set to that account's home |
|
|
14
|
-
| `scripts/dev_env_scripts_constants/codex_account_constants.py` | The account names
|
|
14
|
+
| `scripts/dev_env_scripts_constants/codex_account_constants.py` | The fallback account names, the roster variable and file, the launcher template, the shared entry names, the 10% bar, the 1% Luna stop, and the 20% 5-hour floor for Luna |
|
|
15
15
|
|
|
16
16
|
## Names and order
|
|
17
17
|
|
|
18
|
-
The
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
The account roster names the accounts and their order. It comes from the
|
|
19
|
+
`CODEX_ACCOUNT_PROFILES` environment variable, a comma-separated list such as
|
|
20
|
+
`alpha,beta`. When that variable is unset or empty, the roster is the saved list
|
|
21
|
+
in `account-launchers.json` under the profiles root. When neither exists, the
|
|
22
|
+
accounts are `codex-1`, `codex-2`, `codex-3` and `codex-4`.
|
|
23
|
+
|
|
24
|
+
`choose`, `sync` and `check` all work on that list, in that order. `check`
|
|
25
|
+
accepts only a name on it. Each account's home is `~/.codex-profiles/<name>`, or
|
|
26
|
+
`<name>` under `CODEX_PROFILES_ROOT` when set. `~/.codex` holds the shared setup
|
|
27
|
+
and is never an account. A name uses letters, digits, hyphens or underscores.
|
|
28
|
+
`main`, `wait` and the Windows device names are refused.
|
|
22
29
|
|
|
23
30
|
## Sign in once
|
|
24
31
|
|
|
@@ -33,8 +40,54 @@ $env:CODEX_HOME = "$HOME\.codex-profiles\codex-1"; codex login
|
|
|
33
40
|
```
|
|
34
41
|
|
|
35
42
|
The sign-in lives in that folder's `auth.json`. Only the entries in
|
|
36
|
-
`ALL_SHARED_CODEX_HOME_NAMES` link to `~/.codex`.
|
|
37
|
-
|
|
43
|
+
`ALL_SHARED_CODEX_HOME_NAMES` link to `~/.codex`. They are `AGENTS.md`, `agents`,
|
|
44
|
+
`config.toml`, `hooks`, `hooks.json`, `plugins`, `prompts`, `rules` and `skills`.
|
|
45
|
+
Sign-in, sessions, history, logs and state files stay per account.
|
|
46
|
+
|
|
47
|
+
## Named launchers
|
|
48
|
+
|
|
49
|
+
Each roster account gets a launcher, `codex-<name>.cmd`, in `~/.local/bin`. It
|
|
50
|
+
sets `CODEX_HOME` to that account's home and runs Codex with every argument, so
|
|
51
|
+
`codex-alpha exec "fix the test"` runs on the `alpha` account.
|
|
52
|
+
|
|
53
|
+
Run the setup once:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
python packages/claude-dev-env/scripts/codex_account_choice.py setup
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It prints the saved names, then asks for one name per line. A blank line or the
|
|
60
|
+
end of input finishes. An invalid name prints the reason and asks again. A
|
|
61
|
+
repeated name counts once. The setup saves the list to
|
|
62
|
+
`~/.codex-profiles/account-launchers.json`, links each account's home to
|
|
63
|
+
`~/.codex`, and writes each launcher.
|
|
64
|
+
|
|
65
|
+
Rerun the installer after the shared setup or this package changes:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
python packages/claude-dev-env/scripts/codex_account_choice.py install
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
It reads the roster, links every account's home, and rewrites any launcher that
|
|
72
|
+
differs. A second run changes nothing. With no roster it prints `{}`.
|
|
73
|
+
`CODEX_ACCOUNT_PROFILES` overrides the saved file here too. Both commands take
|
|
74
|
+
`--main-home` and `--launcher-directory`.
|
|
75
|
+
|
|
76
|
+
The launcher runs `call codex %*`. `codex` is npm's `codex.cmd`, and its last
|
|
77
|
+
line ends the batch scope. Without `call`, that line also drops the launcher's
|
|
78
|
+
`CODEX_HOME`, and Codex runs on the main home.
|
|
79
|
+
|
|
80
|
+
The names typed in one setup run become the whole roster. A saved name
|
|
81
|
+
left out has its launcher renamed to `codex-<name>.cmd.replaced-<time>`. Its
|
|
82
|
+
folder under the profiles root stays, with its `auth.json`, so typing the name
|
|
83
|
+
again restores it. A blank first line empties the roster and moves every launcher
|
|
84
|
+
aside.
|
|
85
|
+
|
|
86
|
+
`check <name>` reads a saved name the same way:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
python packages/claude-dev-env/scripts/codex_account_choice.py check alpha --floor 1
|
|
90
|
+
```
|
|
38
91
|
|
|
39
92
|
## Which account a job uses
|
|
40
93
|
|
package/package.json
CHANGED
|
@@ -28,14 +28,15 @@ from dev_env_scripts_constants.claude_account_constants import (
|
|
|
28
28
|
ALL_WINDOWS_JUNCTION_COMMAND_PREFIX,
|
|
29
29
|
CHOICE_MAIN,
|
|
30
30
|
CHOICE_WAIT,
|
|
31
|
+
CLAUDE_LAUNCHER_PROGRAM,
|
|
31
32
|
JSON_LAUNCHER_KEY,
|
|
32
33
|
JSON_LINKED_KEY,
|
|
33
34
|
JSON_MOVED_ASIDE_KEY,
|
|
34
35
|
JSON_UNLINKED_KEY,
|
|
35
36
|
ALL_LAUNCHER_DIRECTORY_RELATIVE_PARTS,
|
|
36
|
-
|
|
37
|
+
LAUNCHER_BODY_TEMPLATE,
|
|
37
38
|
LAUNCHER_REPLACED_SUFFIX,
|
|
38
|
-
|
|
39
|
+
LauncherProgram,
|
|
39
40
|
MAIN_CLAUDE_HOME_DIRECTORY_NAME,
|
|
40
41
|
PROFILES_ROOT_DIRECTORY_NAME,
|
|
41
42
|
PROFILES_ROOT_ENVIRONMENT_VARIABLE,
|
|
@@ -309,8 +310,58 @@ def sync_profile(
|
|
|
309
310
|
)
|
|
310
311
|
|
|
311
312
|
|
|
312
|
-
def
|
|
313
|
-
|
|
313
|
+
def sync_report_payload(report: ProfileSyncReport) -> dict[str, object]:
|
|
314
|
+
"""Turn a sync report into its JSON payload.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
report: The entries one sync linked, moved aside, and unlinked.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
The linked, moved-aside, and unlinked entries as JSON lists.
|
|
321
|
+
"""
|
|
322
|
+
return {
|
|
323
|
+
JSON_LINKED_KEY: list(report.all_linked),
|
|
324
|
+
JSON_MOVED_ASIDE_KEY: list(report.all_moved_aside),
|
|
325
|
+
JSON_UNLINKED_KEY: list(report.all_unlinked),
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def move_launcher_aside(launcher_path: Path, now: datetime) -> Path:
|
|
330
|
+
"""Rename a launcher to ``<name>.replaced-<time>`` beside it.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
launcher_path: The launcher to move.
|
|
334
|
+
now: The run time that names the moved launcher.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
The moved launcher's path.
|
|
338
|
+
"""
|
|
339
|
+
moved_path = launcher_path.with_name(
|
|
340
|
+
f"{launcher_path.name}{LAUNCHER_REPLACED_SUFFIX}{_stamp(now)}"
|
|
341
|
+
)
|
|
342
|
+
os.replace(launcher_path, moved_path)
|
|
343
|
+
return moved_path
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def launcher_path(
|
|
347
|
+
launcher_directory: Path, profile_name: str, launcher_program: LauncherProgram
|
|
348
|
+
) -> Path:
|
|
349
|
+
"""Name the launcher file for a profile.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
launcher_directory: The directory on PATH that holds the launcher.
|
|
353
|
+
profile_name: The name used in the launcher file name.
|
|
354
|
+
launcher_program: The program the launcher runs, which names the file.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
The launcher path.
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
ValueError: When the profile name is not a valid profile name.
|
|
361
|
+
"""
|
|
362
|
+
return launcher_directory / launcher_program.file_name_template.format(
|
|
363
|
+
profile_name=validate_profile_name(profile_name)
|
|
364
|
+
)
|
|
314
365
|
|
|
315
366
|
|
|
316
367
|
def write_launcher(
|
|
@@ -318,41 +369,41 @@ def write_launcher(
|
|
|
318
369
|
launcher_directory: Path,
|
|
319
370
|
profile_home: Path,
|
|
320
371
|
now: datetime,
|
|
321
|
-
profile_name: str
|
|
372
|
+
profile_name: str,
|
|
373
|
+
launcher_program: LauncherProgram,
|
|
322
374
|
) -> Path:
|
|
323
|
-
"""Write the launcher that runs
|
|
375
|
+
"""Write the launcher that runs a program under a named profile.
|
|
324
376
|
|
|
325
377
|
::
|
|
326
378
|
|
|
327
379
|
claude-NAME -p "fix the test"
|
|
328
|
-
-> CLAUDE_CONFIG_DIR=<profile home>, then claude -p "fix the test"
|
|
380
|
+
-> CLAUDE_CONFIG_DIR=<profile home>, then call claude -p "fix the test"
|
|
329
381
|
an older claude-NAME.cmd -> claude-NAME.cmd.replaced-<time>
|
|
330
382
|
|
|
331
383
|
Args:
|
|
332
384
|
launcher_directory: The directory on PATH that holds the launcher.
|
|
333
|
-
profile_home: The named account's
|
|
385
|
+
profile_home: The named account's home for the program.
|
|
334
386
|
now: The run time that names a moved older launcher.
|
|
335
387
|
profile_name: The name used in the launcher file name.
|
|
388
|
+
launcher_program: The program to run and the variable naming its home.
|
|
336
389
|
|
|
337
390
|
Returns:
|
|
338
391
|
The launcher path.
|
|
339
392
|
"""
|
|
340
|
-
|
|
341
|
-
|
|
393
|
+
target_path = launcher_path(launcher_directory, profile_name, launcher_program)
|
|
394
|
+
launcher_text = LAUNCHER_BODY_TEMPLATE.format(
|
|
395
|
+
environment_variable=launcher_program.environment_variable,
|
|
396
|
+
profile_home=profile_home,
|
|
397
|
+
program=launcher_program.program,
|
|
342
398
|
)
|
|
343
|
-
launcher_path = launcher_directory / launcher_file_name
|
|
344
|
-
launcher_text = LAUNCHER_TEXT_TEMPLATE.format(profile_home=profile_home)
|
|
345
399
|
launcher_bytes = launcher_text.encode(TEXT_ENCODING)
|
|
346
|
-
if
|
|
347
|
-
return
|
|
348
|
-
if
|
|
349
|
-
|
|
350
|
-
launcher_path,
|
|
351
|
-
launcher_path.with_name(_moved_launcher_name(launcher_file_name, now)),
|
|
352
|
-
)
|
|
400
|
+
if target_path.is_file() and target_path.read_bytes() == launcher_bytes:
|
|
401
|
+
return target_path
|
|
402
|
+
if target_path.is_file():
|
|
403
|
+
move_launcher_aside(target_path, now)
|
|
353
404
|
launcher_directory.mkdir(parents=True, exist_ok=True)
|
|
354
|
-
|
|
355
|
-
return
|
|
405
|
+
target_path.write_bytes(launcher_bytes)
|
|
406
|
+
return target_path
|
|
356
407
|
|
|
357
408
|
|
|
358
409
|
def _build_argument_parser() -> argparse.ArgumentParser:
|
|
@@ -402,13 +453,9 @@ def _sync_named_profile(arguments: argparse.Namespace) -> dict[str, object]:
|
|
|
402
453
|
profile_home=profile_home,
|
|
403
454
|
now=now,
|
|
404
455
|
profile_name=arguments.profile_name,
|
|
456
|
+
launcher_program=CLAUDE_LAUNCHER_PROGRAM,
|
|
405
457
|
)
|
|
406
|
-
return {
|
|
407
|
-
JSON_LINKED_KEY: list(report.all_linked),
|
|
408
|
-
JSON_MOVED_ASIDE_KEY: list(report.all_moved_aside),
|
|
409
|
-
JSON_UNLINKED_KEY: list(report.all_unlinked),
|
|
410
|
-
JSON_LAUNCHER_KEY: str(launcher_path),
|
|
411
|
-
}
|
|
458
|
+
return {**sync_report_payload(report), JSON_LAUNCHER_KEY: str(launcher_path)}
|
|
412
459
|
|
|
413
460
|
|
|
414
461
|
if __name__ == "__main__":
|