Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ repos:
language: script
pass_filenames: false
files: ^(bin/.*|lib/.*|completions/.*|docs/account-launchers\.md|tests/(config\.sh|test_config\.py|test_codex_as\.py))$
- id: account-info
name: account metadata, Codex usage scan and queries (isolated HOME and fakes)
entry: tests/account-info.sh
language: script
pass_filenames: false
files: ^(bin/.*|lib/.*|tests/(account-info\.sh|test_account_info\.py))$
- id: test-entrypoints
name: test entry points and kit knob hygiene
entry: tests/test-entrypoints.sh
Expand Down
22 changes: 14 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ consumer, a submodule whose files it links.

```
bin/codex-as, bin/claude-as launchers (Bash, POSIX sh)
lib/codex-as/*.py codex-as modules: launch, check, layout, handoff
lib/codex-as/*.py codex-as modules: launch, check, layout, handoff, usage
lib/claude-as/launch.py claude-as module
lib/coding-agent-kit/config.py the configuration loader both launchers share
lib/coding-agent-kit/accounts.py the account metadata cache both launchers share
completions/{bash,zsh}/ account-name completion for both launchers
helpers/*.cjs Claude Code helpers (Node)
install.sh links bin/ and completions/ into a prefix
Expand All @@ -34,7 +35,7 @@ tests/ suites, fixtures, runners, COVERAGE.md
loads its modules inside the error handling, so a missing file prints
`incomplete install at ROOT`, never a traceback.
- `lib/coding-agent-kit/config.py` is the only launcher or completion code
that reads `HOME` or the configuration file (a test greps for other reads). It loads once per run into
that reads `HOME`, `XDG_STATE_HOME` or the configuration file (a test greps for other reads). It loads once per run into
one frozen object that every consumer takes. Unknown keys warn and never
fail, so a newer configuration file works with an older kit. See
[docs/configuration.md](docs/configuration.md).
Expand All @@ -48,7 +49,7 @@ tests/ suites, fixtures, runners, COVERAGE.md
a pre-commit hook:

- The public paths and their modes (`git ls-files -s`): the launchers, their
`lib/` modules, `config.py`, the completions, the helpers, `install.sh` and
`lib/` modules, `config.py` and `accounts.py`, the completions, the helpers, `install.sh` and
the three docs. Consumers link these paths one by one (dotfiles links each
file from its own tree), copied installs depend on the `bin/` plus `lib/`
shape, and `--help` points at `docs/account-launchers.md`.
Expand All @@ -65,9 +66,14 @@ a pre-commit hook:

The command-line surface is public too, guarded by the suites rather than
`contract.sh`: the help text (dotfiles tests grep its `Concurrent accounts`
line and its `Details and manual repair:` path), `--list-accounts` and
`--show-config` with their output, the configuration file location, keys and
defaults, `install.sh` and its options, and `CODING_AGENT_KIT_COLOR`. Changing any of it, or anything `contract.sh` pins,
line and its `Details and manual repair:` path), `--list-accounts`,
`--account-labels`, `--account-info NAME` (dotfiles' completion preview shows
it), `claude-as --set-label` and `--show-config` with their output, the
configuration file location, keys and defaults, the account metadata files in
`${XDG_STATE_HOME:-~/.local/state}/coding-agent-kit/accounts/` with their
fields, `install.sh` and its options, `CODING_AGENT_KIT_COLOR`, and the
`CODING_AGENT_KIT_USAGE_FILE` and `CODING_AGENT_KIT_TOKEN_ID` that `claude-as`
passes its child. Changing any of it, or anything `contract.sh` pins,
is a breaking change: it needs a minor release and a matching CHANGELOG entry.

## Required conventions
Expand Down Expand Up @@ -108,11 +114,11 @@ After modifying any file, run `pre-commit run --all-files` to ensure changes pas
- Bash: `shfmt -i 4 -ci -ln bash`
- POSIX: `shfmt -i 4 -ci -ln posix`
- Zsh: `shfmt -i 4 -ci -ln zsh` (needs shfmt 3.13+, the first release with a zsh dialect)
- Launcher messages follow one colour policy in shell and Python: `CODING_AGENT_KIT_COLOR=auto|always|never` (unset or invalid means `auto`, styled only when the destination is a terminal), and a nonempty `NO_COLOR` or `TERM=dumb` disables color. Native CLI output, completion lists and the output of `--list-accounts` and `--show-config` stay untouched, and redirected logs stay plain by default.
- Launcher messages follow one colour policy in shell and Python: `CODING_AGENT_KIT_COLOR=auto|always|never` (unset or invalid means `auto`, styled only when the destination is a terminal), and a nonempty `NO_COLOR` or `TERM=dumb` disables color. Native CLI output, completion lists and the output of `--list-accounts`, `--account-labels`, `--account-info` and `--show-config` stay untouched, and redirected logs stay plain by default.

## Testing

- Every test entry point (`tests/*.sh`) starts with the one statement `unset CODING_AGENT_KIT_CONFIG CODING_AGENT_KIT_COLOR XDG_CONFIG_HOME NO_COLOR`, and `tests/run.ps1` removes the same names in the same order. `tests/test-entrypoints.sh` checks each entry point against `tests/run.sh`, and that the statement names every `CODING_AGENT_KIT_*` variable (pattern `_?CODING_AGENT_KIT_[A-Z_]+`) in a tracked file outside `tests/`, `docs/`, `.github/` and Markdown, so a knob a new file reads goes on that list too. Python tests that pass the caller's environment on drop the same names in `setUp`.
- Every test entry point (`tests/*.sh`) starts with the one statement `unset CODING_AGENT_KIT_CONFIG CODING_AGENT_KIT_COLOR CODING_AGENT_KIT_USAGE_FILE CODING_AGENT_KIT_TOKEN_ID XDG_CONFIG_HOME XDG_STATE_HOME NO_COLOR`, and `tests/run.ps1` removes the same names in the same order. `tests/test-entrypoints.sh` checks each entry point against `tests/run.sh`, and that the statement names every `CODING_AGENT_KIT_*` variable (pattern `_?CODING_AGENT_KIT_[A-Z_]+`) in a tracked file outside `tests/`, `docs/`, `.github/` and Markdown, so a knob a new file reads goes on that list too. Python tests that pass the caller's environment on drop the same names in `setUp`.
- Behavior coverage is tracked as a source-to-test matrix in `tests/COVERAGE.md`; update it with the tests.

## Windows line endings
Expand Down
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,70 @@
# Changelog

## 0.3.0

Account details for previews and people: each launcher keeps a small,
offline metadata cache per account, and new queries print it without opening
a credential. A minor release: it adds environment variables, state files,
an enrolment prompt, help lines and queries.

Added:

- `codex-as --account-info NAME` and `claude-as --account-info NAME` print an
account's last-seen details: the Codex email and plan, the Claude label, and
the 5-hour and weekly usage with their reset times and the age of the
snapshot, or `not recorded yet`. `--account-labels` prints `NAME<TAB>LABEL`
lines (the email for Codex, the label for Claude). Both are metadata only:
no directory is created, no preflight or native CLI runs, no credential is
opened, and only allow-listed fields are printed, with control characters
removed.
- The cache lives in `${XDG_STATE_HOME:-~/.local/state}/coding-agent-kit/accounts/`
(0700): `codex-NAME.json` (written by `codex-as`), `claude-NAME.json`
(`claude-as`) and `claude-NAME.usage.json` (`helpers/statusline.cjs`), each
0600, replaced atomically and only when it changes. No token is stored.
`Config.state_dir` in `lib/coding-agent-kit/config.py` names it; it is not a
configuration key.
- `codex-as` records the email and plan from the account's `id_token` at each
launch and, after each run, the last rate-limit snapshot of the run's own
rollouts (the thread it resumes, never a fork's source, or one this account created),
best effort and without changing the exit code. New module
`lib/codex-as/usage.py`.
- `claude-as` asks for an optional label when it creates an account, after
the confirmation and before any token exists, and saves it after the token;
control characters, more than 100 characters and anything containing
`sk-ant-` are refused. `claude-as --set-label NAME [LABEL]` sets, asks for
or (empty) clears the label of an existing account.
- `claude-as` passes its child `CODING_AGENT_KIT_USAGE_FILE` and
`CODING_AGENT_KIT_TOKEN_ID` (the first 16 hex digits of the token's
SHA-256), dropping inherited values. `helpers/statusline.cjs` exports
`recordUsage` and records the session's `rate_limits` there only while
`CLAUDE_CODE_OAUTH_TOKEN` hashes to that identifier, into an owned 0700
directory, never on Windows; its printed status line is unchanged.
- New shared module `lib/coding-agent-kit/accounts.py`, the cache.
- A configuration whose `secrets_dir` is the cache directory is refused,
since the cache's `codex-NAME.json` would replace the credential of that
name; a cache directory that is `secrets_dir` through a link is never read
or written.

Changed:

- The zsh completions run `--account-labels` and show each account's email
or label as its description; Bash still completes bare names.
- Both `--help` texts gain two lines for the new queries.
- `bin/codex-as` accepts `--account-labels` alone and `--account-info NAME`;
other arities stay invalid account names.
- Every test entry point also unsets `CODING_AGENT_KIT_USAGE_FILE`,
`CODING_AGENT_KIT_TOKEN_ID` and `XDG_STATE_HOME`.
- The tests that model a dotfiles host follow its layout from October 2026:
the kit checked out at the root of dotfiles' own `coding-agent-kit` package.

Known limits: Codex usage comes from Codex's internal rollout format, misses
a session switched inside Codex, skips compressed rollouts, can miss this
account's new session past 64 other accounts' rollouts of the run, and can
count a thread this account created that another account resumed during the
run.
Claude usage exists only for Pro and Max subscribers, after the first API
response, in sessions that render a status line.

## 0.2.0

coding-agent-kit becomes a standalone install. The launchers find their own
Expand Down
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ codex-as work resume --last
codex-as work exec --json "Check the tests"
claude-as personal -p "Check the tests"
codex-as --list-accounts # account names, what completion offers
codex-as --account-info work # last-seen email, plan and usage (offline)
claude-as --set-label personal "me@example.com"
claude-as --show-config # configuration in effect
codex-as --help
```
Expand All @@ -158,8 +160,18 @@ codex-as --help
- `claude-as` starts Claude Code with the `claude setup-token` OAuth token from
`<secrets_dir>/claude-NAME` and otherwise inherits the existing
configuration.
- Apart from `--help`, `--list-accounts` and `--show-config`, each accepted
only alone, the launchers have no options of their own.
- Apart from `--help`, `--list-accounts`, `--account-labels` and
`--show-config`, each accepted only alone, `--account-info NAME` and
`claude-as --set-label NAME [LABEL]`, the launchers have no options of their
own.
- Each launcher keeps a small metadata cache per account in
`${XDG_STATE_HOME:-~/.local/state}/coding-agent-kit/accounts/`: the Codex
email and plan from the account's token and the last rate limits a run saw
in its own rollouts, and the Claude label asked at enrolment and the last
rate limits the status line helper saw. `--account-info NAME` prints it
offline, without opening a credential, and the zsh completions show the
email or label as each account's description. Emails show in previews and
transcripts; no token is ever stored there.

[docs/account-launchers.md](docs/account-launchers.md) covers the account
layout, adding accounts, completion, safety boundaries and manual repair;
Expand Down Expand Up @@ -197,8 +209,8 @@ enrolment, and after moving `codex_home` export `CODEX_HOME` for plain

The launchers style only their own messages: headings cyan, questions and
warnings yellow, saves green, errors red. Native Codex and Claude output,
completion lists and `--list-accounts`/`--show-config` output are never
styled.
completion lists and the output of `--list-accounts`, `--account-labels`,
`--account-info` and `--show-config` are never styled.

- `CODING_AGENT_KIT_COLOR=auto` (also when unset or invalid) styles a message
only when its destination is a terminal, so redirected logs stay plain;
Expand All @@ -224,8 +236,9 @@ pre-commit run --all-files
- `run.ps1` runs the Node helper suite; `-CI` requires Windows.
- Every suite runs in a temporary HOME with fake CLIs and fictitious
credentials, reaches no network beyond local fixtures, and first unsets
`CODING_AGENT_KIT_CONFIG`, `CODING_AGENT_KIT_COLOR`, `XDG_CONFIG_HOME` and
`NO_COLOR`.
`CODING_AGENT_KIT_CONFIG`, `CODING_AGENT_KIT_COLOR`,
`CODING_AGENT_KIT_USAGE_FILE`, `CODING_AGENT_KIT_TOKEN_ID`,
`XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `NO_COLOR`.
- `tests/standalone-e2e.sh` installs with `install.sh` under `/bin/sh`, bash and
(when present) dash, checks its refusals and drives both launchers and their
completions through the links;
Expand Down
2 changes: 2 additions & 0 deletions bin/claude-as
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ if [ "$#" -eq 0 ] || [ "$1" = --help ]; then
'login/logout/setup-token, --bare, unsupported flags and auth/provider overrides are refused.' \
'Daily configuration is inherited. No global process killing or concurrency lock.' \
'Requires Python 3.11+ (stdlib).' \
'--account-info NAME: its label and the last usage its status line saw (offline, no token read);' \
'--account-labels: NAME<TAB>label lines for completion; --set-label NAME [LABEL] sets or clears a label.' \
'Configuration: ${XDG_CONFIG_HOME:-~/.config}/coding-agent-kit/config.toml; --show-config prints the effective values.' \
"Details and manual repair: $_claude_as_docs"
[ "$#" -gt 0 ] && exit 0
Expand Down
24 changes: 15 additions & 9 deletions bin/codex-as
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,29 @@ if [ "$#" -eq 0 ] || [ "$1" = --help ]; then
'Same-account OAuth refresh retains native cross-process limitations.' \
'Resume by UUID, --last or picker; explicit session names are refused.' \
'Requires Python 3.11+ (stdlib) and Codex 0.161.0 or newer; no upper version limit.' \
'--account-info NAME: last-seen email, plan and usage of a run of NAME (offline, no credential read);' \
'--account-labels: NAME<TAB>email lines for completion; --list-accounts: the names.' \
'Configuration: ${XDG_CONFIG_HOME:-~/.config}/coding-agent-kit/config.toml; --show-config prints the effective values.' \
"Details and manual repair: $_codex_as_docs"
[ "$#" -gt 0 ] && exit 0
exit 2
fi
_codex_as_locate || exit 1

# --list-accounts and --show-config alone are the launcher's own metadata queries;
# no account name starts with -.
if [ "$#" -ne 1 ] || { [ "$1" != --list-accounts ] && [ "$1" != --show-config ]; }; then
case $1 in
'' | -* | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-]*)
_codex_as_message 31 2 'codex-as: invalid account name (ASCII letters, digits, _ or -; no leading -).'
exit 2
;;
esac
# --list-accounts, --account-labels and --show-config alone, and --account-info
# NAME, are the launcher's own metadata queries; no account name starts with -.
_codex_as_name=${1-}
if [ "$#" -eq 1 ] && { [ "$1" = --list-accounts ] || [ "$1" = --account-labels ] || [ "$1" = --show-config ]; }; then
_codex_as_name=query
elif [ "$#" -eq 2 ] && [ "$1" = --account-info ]; then
_codex_as_name=$2
fi
case $_codex_as_name in
'' | -* | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-]*)
_codex_as_message 31 2 'codex-as: invalid account name (ASCII letters, digits, _ or -; no leading -).'
exit 2
;;
esac
if ! command -v python3 >/dev/null 2>&1; then
_codex_as_message 31 2 'codex-as: Python 3.11+ is required for local JSON and file checks.'
exit 1
Expand Down
6 changes: 4 additions & 2 deletions completions/zsh/_claude-as
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

local -a names suggestions
if ((CURRENT == 2)); then
# Metadata only: claude-as --list-accounts never starts native Claude.
names=("${(@f)$(command claude-as --list-accounts 2>/dev/null)}")
# Metadata only: claude-as --account-labels never starts native Claude or reads
# a credential. NAME<TAB>LABEL lines become NAME:LABEL descriptions.
names=("${(@f)$(command claude-as --account-labels 2>/dev/null)}")
names=("${(@)names:#}")
names=("${(@)names/$'\t'/:}")
((${#names})) && _describe -t accounts 'Claude accounts' names
return
fi
Expand Down
6 changes: 4 additions & 2 deletions completions/zsh/_codex-as
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

local -a names suggestions
if ((CURRENT == 2)); then
# Metadata only: codex-as --list-accounts never starts native Codex.
names=("${(@f)$(command codex-as --list-accounts 2>/dev/null)}")
# Metadata only: codex-as --account-labels never starts native Codex or reads
# a credential. NAME<TAB>LABEL lines become NAME:LABEL descriptions.
names=("${(@f)$(command codex-as --account-labels 2>/dev/null)}")
names=("${(@)names:#}")
names=("${(@)names/$'\t'/:}")
((${#names})) && _describe -t accounts 'Codex accounts' names
return
fi
Expand Down
Loading
Loading