ai-share-usage-cli
v0.1.13
Published
Shared Codex account usage in the terminal: collect, upload, watch and dashboard
Downloads
1,671
Maintainers
Readme
ai-share-usage-cli
Shared Codex (ChatGPT) account usage in the terminal. Same product as the AI Share Usage VS Code extension, same team server, same numbers — for people who work in a terminal.
npm i -g ai-share-usage-cli
ais join <invite>ais needs Node 20 or newer. It ships as one bundled file, so the install has
nothing to build.
No editor? Use the browser dashboard
If Codex is the desktop app for you — no VS Code, no herdr, no terminal habit — three commands are the whole setup:
npm i -g github:DongHyunnn/ai-share-usage-herdr # the `ais` CLI
ais join <invite>
ais open # starts the daemon if it is not running, opens the dashboardais open prints the URL and opens it (open / wslview / xdg-open);
ais web-url only prints it. The daemon binds 127.0.0.1 on
settings.webPort (0 = an OS-assigned port) and every request must carry the
random token from daemon.json, so nothing on the network can reach it.
Threshold alerts arrive as native OS notifications — macOS Notification Center
via osascript, notify-send on Linux, a toast through PowerShell on Windows.
ais daemon --no-notify or ais config set notifications false turns them off,
--no-web leaves the dashboard unserved.
Collection needs nothing from you. By default the collector reads OMO sessions
and their discovered Senpi child sessions, Codex rollout logs from
~/.codex/sessions (or $CODEX_HOME/sessions), and Gajae Code sessions from
~/.gjc/agent/sessions.
Commands
| Command | What it does |
| --- | --- |
| ais join <invite\|link> | Applies the invite (server, key and group), signs in with GitHub, joins |
| ais login | GitHub sign-in through a loopback callback; the URL is printed for headless shells |
| ais logout | Forgets the stored session |
| ais status | One line: ~48% / 61% ⚡1.3×, plus daemon health |
| ais status --json | The full snapshot and a dashboard summary, for scripts |
| ais report | One quota window as a summary to paste into a chat (Markdown) |
| ais report --window last --format json | The previous window, as the report object |
| ais dashboard | Full-screen dashboard: r refresh, 1-4 range, ? help, q quit |
| ais daemon | Collects, uploads, polls the account quota, refits weights, raises alerts |
| ais daemon --once | One cycle and exit — for cron, a shell hook or a CI step |
| ais open | Opens the local web dashboard, starting the daemon when it is not running |
| ais web-url | Prints the dashboard URL (token included) and nothing else |
| ais autostart install\|uninstall | Writes (or removes) the launchd agent / systemd --user unit |
| ais guide [--admin] | The setup guide as text (--lang ko\|en) |
| ais join --prompt | Asks for the invite on the terminal (what the herdr popup pane runs) |
| ais herdr status-line | Prints the token the herdr sidebar shows as $ais |
| ais invite | Prints the invite token and the one-click link |
| ais create-group <name> | Creates a team (admin; needs the server settings) |
| ais config get [key] | Prints settings |
| ais config set <key> <value> | Changes one setting |
| ais --version | Prints the version |
The weekly report
ais report turns one quota window into the paragraph somebody would
otherwise write by hand: the range, where the account opened and closed, every
member with their tokens, quota, API-equivalent cost and share of their own
fair share, the models that did the work, what the window is missing, the
calibration the percentages rest on, and where the current pace lands.
It carries the marks the dashboard uses, and they mean the same thing here:
≥ is a floor, ≈ is partly estimated, and — is withheld — nothing behind
that figure could be priced, so no percent is claimed. --format json prints
the same object with those qualifications as fields (coverage.isLowerBound,
coverage.unknown, cost.hasEstimate) rather than as marks.
The same summary is a Copy report button on the dashboard's admin tab, in the editor and in the browser page alike, and it is there for every member: it is a summary of your own group.
Where state lives
$AIS_HOME, by default ~/.config/ai-share-usage/. Every file is written
0600.
| File | Contents |
| --- | --- |
| settings.json | supabaseUrl, supabaseAnonKey, groupInviteCode, displayName, pollIntervalSeconds, language, sources, extraHomes, discoverContainers, containerNamePatterns, targetSharePercent, notifications, webPort |
| session.json | The Supabase session and its PKCE verifier |
| cursors.json | Per-file resume cursors for the collector |
| alerts.json | Which alert levels already fired in this quota window |
| daemon.json | Heartbeat: pid, startedAt, lastCycleAt, lastStatus, and web (port, token, url) |
language is auto (follows LANG/LC_ALL), ko or en. sources picks
which session logs to collect: omo (including discovered Senpi children),
codex, and gjc. All three are enabled by default.
Collecting from a container
When omo or Codex run inside a container, their sessions are written into the
container's home. Bind-mount that home and name it in extraHomes:
docker run -v $HOME/dev-box:/root -it my-image
ais config set extraHomes '~/dev-box'
# or, to choose the label: ais config set extraHomes '~/dev-box = box'settings.json takes the same list as strings or objects, and the daemon's
browser dashboard edits it in the Extra home directories card:
{ "extraHomes": ["~/dev-box", { "path": "/srv/ci", "label": "ci" }] }Each entry is an absolute path (~ and $VAR are expanded) and is read like
your own home — <home>/.omo/agent/sessions, <home>/.codex/sessions,
<home>/.gjc/agent/sessions for the enabled sources, Senpi child sessions
included. Usage found there is filed under the machine <hostname>:<label>,
the label defaulting to the directory's name, so a container and its host stay
separate rows in the per-machine breakdown. A home that is missing or
unreadable is not watched and says so once in the log; it is picked up on a
later cycle. Adding or removing an entry takes effect on the next cycle — no
daemon restart.
Your Codex sign-in stays the one in your own home: an extra home's
.codex/auth.json is only compared with it, and a different account there is
logged as a warning while collection continues. What may be uploaded is decided
by the account-provenance rules, not by which directory a log sat in.
Finding those homes automatically
If the containers already run on this host, the daemon can ask Docker where their homes are instead of you writing the paths down:
ais config set discoverContainers true
ais config set containerNamePatterns 'dev-*, ci' # empty = every running containerEvery running container that matches is inspected, and each bind or volume
mount whose destination is a home (/root, /home/<user>) or one of the agent
directories (.omo, .codex, .gjc) becomes an extra home on its host-side
path, filed under the machine <hostname>:<container name>. A container that
stops loses its watchers on the next cycle, one that starts gains them; no
restart either way. An extraHomes entry you wrote yourself always wins for the
same path, label included, and your own home is never collected twice.
Volume mounts resolve to /var/lib/docker/volumes/<name>/_data, which usually
only root can read. That is detected, not hit: the home is skipped and the log
says which container and which path, so you can bind-mount the home instead or
run the daemon as root. No Docker socket, or a DOCKER_HOST that is not a local
unix socket, means the feature stays inactive and says so once.
The trust boundary. Being able to talk to /var/run/docker.sock is
root-equivalent on most hosts — whoever reaches it can start a privileged
container. That is why this is opt-in and off by default. What the collector
itself does with the socket is two read-only calls, GET /containers/json and
GET /containers/{id}/json; it never execs, never attaches, never creates, and
of the answer it reads only the container name, whether it is running, and the
mount table. Environment variables, labels and secrets are not read.
What is collected
Model names and token counts, session ids, message timestamps and the machine hostname — never prompts, responses, file names or file contents. The Codex access token never leaves the machine.
Running it as a service
ais daemon runs until SIGINT/SIGTERM and can be supervised by systemd, tmux
or a herdr session. ais daemon --once fits a cron entry or a shell hook.
ais status exits 0 even before you have joined, so a prompt can call it
unguarded.
ais autostart install writes the file your platform wants and prints the one
command left to run:
| OS | File | Then run |
| --- | --- | --- |
| macOS | ~/Library/LaunchAgents/com.github.donghyunnn.ais.plist | launchctl load -w <file> |
| Linux | ~/.config/systemd/user/ais.service | systemctl --user daemon-reload && systemctl --user enable --now ais |
| Windows | none — one Scheduled Task | schtasks /create /tn "AI Share Usage" /sc onlogon /tr "node C:\path\to\ais.mjs daemon" |
ais autostart uninstall removes the file and prints the matching disable
command. Both files are also in autostart/ to copy by hand.
MIT licensed.
