@knpkv/agent-usage 0.1.0 → 0.2.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/README.md +129 -20
- package/dist/client/assets/index-BWajfpNv.js +55 -0
- package/dist/client/assets/{index-CUuLXODk.css → index-B_xl-_rU.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/core/ClaudeLimitSamples.d.ts +30 -0
- package/dist/core/ClaudeLimitSamples.js +69 -0
- package/dist/core/ClaudeLimitSamples.js.map +1 -0
- package/dist/core/ClaudeLimits.d.ts +38 -4
- package/dist/core/ClaudeLimits.js +66 -19
- package/dist/core/ClaudeLimits.js.map +1 -1
- package/dist/core/ClaudeLimitsLive.d.ts +33 -7
- package/dist/core/ClaudeLimitsLive.js +54 -10
- package/dist/core/ClaudeLimitsLive.js.map +1 -1
- package/dist/core/Database.d.ts +9 -1
- package/dist/core/Database.js +6 -2
- package/dist/core/Database.js.map +1 -1
- package/dist/core/Ingest.d.ts +7 -1
- package/dist/core/Ingest.js +36 -6
- package/dist/core/Ingest.js.map +1 -1
- package/dist/core/Model.d.ts +19 -7
- package/dist/core/Model.js +15 -3
- package/dist/core/Model.js.map +1 -1
- package/dist/core/Report.js +21 -7
- package/dist/core/Report.js.map +1 -1
- package/dist/core/Store.js +14 -12
- package/dist/core/Store.js.map +1 -1
- package/dist/main.js +30 -20
- package/dist/main.js.map +1 -1
- package/dist/server/Api.d.ts +130 -94
- package/dist/server/Config.d.ts +22 -3
- package/dist/server/Config.js +66 -4
- package/dist/server/Config.js.map +1 -1
- package/dist/server/ControlSocket.d.ts +73 -0
- package/dist/server/ControlSocket.js +270 -0
- package/dist/server/ControlSocket.js.map +1 -0
- package/dist/server/HttpApplication.d.ts +1 -1
- package/dist/server/HttpApplication.js +2 -1
- package/dist/server/HttpApplication.js.map +1 -1
- package/dist/server/IngestSummary.d.ts +8 -0
- package/dist/server/IngestSummary.js +32 -0
- package/dist/server/IngestSummary.js.map +1 -0
- package/dist/server/Live.d.ts +4 -0
- package/dist/server/Live.js +49 -0
- package/dist/server/Live.js.map +1 -0
- package/dist/server/Login.d.ts +34 -0
- package/dist/server/Login.js +74 -0
- package/dist/server/Login.js.map +1 -0
- package/dist/server/OwnerSession.d.ts +15 -8
- package/dist/server/OwnerSession.js +33 -25
- package/dist/server/OwnerSession.js.map +1 -1
- package/dist/server/Runtime.d.ts +3 -1
- package/dist/server/Runtime.js +32 -8
- package/dist/server/Runtime.js.map +1 -1
- package/dist/server/Server.d.ts +2 -2
- package/dist/server/Server.js +19 -6
- package/dist/server/Server.js.map +1 -1
- package/dist/shared/contracts.d.ts +51 -6
- package/dist/shared/contracts.js +11 -1
- package/dist/shared/contracts.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/dist/client/assets/index-DB-VXWqy.js +0 -55
package/README.md
CHANGED
|
@@ -17,19 +17,112 @@ every model request in a private SQLite store, polls Claude's limits, and serves
|
|
|
17
17
|
|
|
18
18
|
## Running it
|
|
19
19
|
|
|
20
|
-
Installed from npm, the binary is `agent-usage` (`agent-usage serve`, `agent-usage
|
|
21
|
-
repository:
|
|
20
|
+
Installed from npm, the binary is `agent-usage` (`agent-usage serve`, `agent-usage login`,
|
|
21
|
+
`agent-usage ingest`). From this repository:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
pnpm build # builds the workspace, this package included
|
|
25
25
|
pnpm --filter @knpkv/agent-usage start serve # prints the URL that gets you in
|
|
26
|
+
pnpm --filter @knpkv/agent-usage start login # a fresh URL from the running server; --open opens it
|
|
26
27
|
pnpm --filter @knpkv/agent-usage start ingest # one pass, then a summary; --json for one JSON value
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
`serve` ingests every minute and polls Claude's limits every five. The
|
|
30
|
+
`serve` ingests every minute and polls Claude's limits every five. The page stays current by
|
|
31
|
+
itself: after each ingest pass and limit poll is stored, the server tells the page over a WebSocket
|
|
32
|
+
(`/api/live`, admitted by the same session cookie and origin checks as every read) which reads
|
|
33
|
+
changed, and the page refetches only those. A dropped socket reconnects with backoff and refetches
|
|
34
|
+
everything once; the header says how long ago the page was updated, or that it is reconnecting.
|
|
35
|
+
The printed URL carries a
|
|
30
36
|
one-time code in its fragment; opening it exchanges the code for a session cookie and strips it from
|
|
31
|
-
the address bar. The code expires a minute after
|
|
32
|
-
|
|
37
|
+
the address bar. The code works once and expires a minute after it was printed. The server only
|
|
38
|
+
listens on loopback and only answers reads.
|
|
39
|
+
|
|
40
|
+
### Getting back in
|
|
41
|
+
|
|
42
|
+
`agent-usage login` asks the running server for a fresh link and prints it; `agent-usage login
|
|
43
|
+
--open` also opens it in the browser (`xdg-open` on Linux, `open` on macOS). Each link follows the
|
|
44
|
+
startup link's rules: one use, one minute, and a newer link replaces one not yet used. Use it when
|
|
45
|
+
the startup link has expired, the session cookie is gone, or the server runs as a service whose
|
|
46
|
+
output you do not watch.
|
|
47
|
+
|
|
48
|
+
`login` reaches the server over a Unix socket, `serve.sock` in the store directory. The directory
|
|
49
|
+
is owner-only and the socket `0600`, so only the store's owner can ask; the server refuses to bind,
|
|
50
|
+
and `login` to connect, when the path is a symlink, not a socket, or owned by another user. Either
|
|
51
|
+
side gives up on the other after five seconds, and no link is minted before the server is listening.
|
|
52
|
+
|
|
53
|
+
One server runs per store. `serve` first takes an exclusive lock on `serve.lock` in the store
|
|
54
|
+
directory and holds it while it runs; the operating system releases it when the process ends,
|
|
55
|
+
however it ends. A second `serve` on the same store exits with an error before binding a port, and
|
|
56
|
+
the next start after a crash replaces the socket the dead server left. A Unix socket path holds
|
|
57
|
+
about a hundred bytes (103 here): a store directory deeper than that still runs, one server at a
|
|
58
|
+
time, but logs that `login` is unavailable, and `login` says to choose a shorter `AGENT_USAGE_HOME`.
|
|
59
|
+
|
|
60
|
+
| `login` says | Meaning |
|
|
61
|
+
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
62
|
+
| `agent-usage is not running on this store` | Nothing listens on the socket: start `serve` or its service, or restart a server older than `login`, which has no socket |
|
|
63
|
+
| `refused its control socket … : <reason>` | Something other than the server's own socket is at the path |
|
|
64
|
+
| `control socket … could not be used (<why>)` | The socket exists but this process may not connect to it |
|
|
65
|
+
| `did not answer with a link` | Something answered on the socket with a reply that is not a sign-in link |
|
|
66
|
+
|
|
67
|
+
Every failure exits nonzero and prints nothing on stdout, so `agent-usage login | xargs …` is safe.
|
|
68
|
+
|
|
69
|
+
### Running as a service
|
|
70
|
+
|
|
71
|
+
`serve` needs no terminal: it reads nothing from stdin and prints the startup link to stdout once.
|
|
72
|
+
Run it under the service manager and get in with `agent-usage login --open`. It reads its
|
|
73
|
+
configuration from the environment ([Configuration](#configuration)); set `AGENT_USAGE_HOME` and
|
|
74
|
+
`PORT` there when the defaults do not suit. Point `ExecStart` / `ProgramArguments` at the installed
|
|
75
|
+
binary (`command -v agent-usage`).
|
|
76
|
+
|
|
77
|
+
systemd user unit, `~/.config/systemd/user/agent-usage.service`:
|
|
78
|
+
|
|
79
|
+
```ini
|
|
80
|
+
[Unit]
|
|
81
|
+
Description=agent-usage: Claude and Codex usage over time
|
|
82
|
+
|
|
83
|
+
[Service]
|
|
84
|
+
ExecStart=%h/.local/bin/agent-usage serve
|
|
85
|
+
Restart=on-failure
|
|
86
|
+
# Environment=AGENT_USAGE_HOME=%h/.local/share/agent-usage
|
|
87
|
+
# Environment=PORT=3112
|
|
88
|
+
|
|
89
|
+
[Install]
|
|
90
|
+
WantedBy=default.target
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
systemctl --user daemon-reload && systemctl --user enable --now agent-usage
|
|
95
|
+
journalctl --user -u agent-usage # its output
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Under a service manager stdout goes to the journal or a log file, so the startup link lands there.
|
|
99
|
+
It is spent on first use and dead a minute after startup either way; `login` links never leave the
|
|
100
|
+
`login` process.
|
|
101
|
+
|
|
102
|
+
launchd agent, `~/Library/LaunchAgents/dev.knpkv.agent-usage.plist`:
|
|
103
|
+
|
|
104
|
+
```xml
|
|
105
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
106
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
107
|
+
<plist version="1.0">
|
|
108
|
+
<dict>
|
|
109
|
+
<key>Label</key><string>dev.knpkv.agent-usage</string>
|
|
110
|
+
<key>ProgramArguments</key>
|
|
111
|
+
<array><string>/usr/local/bin/agent-usage</string><string>serve</string></array>
|
|
112
|
+
<key>RunAtLoad</key><true/>
|
|
113
|
+
<key>KeepAlive</key><true/>
|
|
114
|
+
</dict>
|
|
115
|
+
</plist>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.knpkv.agent-usage.plist
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A service's environment is not your shell's: if Claude Code or Codex keep their files somewhere
|
|
123
|
+
other than the defaults (`CLAUDE_CONFIG_DIR`, `CODEX_HOME`), set the same variables for the service.
|
|
124
|
+
On macOS, the Keychain lookup for Claude's token needs the service to run in your login session (a
|
|
125
|
+
LaunchAgent does; a LaunchDaemon does not).
|
|
33
126
|
|
|
34
127
|
The first pass backfills everything on disk (on a machine with ~9 GB of Codex rollouts, under a
|
|
35
128
|
minute); later passes read only what was appended. A line longer than 32 MiB (a huge pasted tool
|
|
@@ -39,14 +132,16 @@ For development, `pnpm --filter @knpkv/agent-usage dev` runs the server and Vite
|
|
|
39
132
|
|
|
40
133
|
## Configuration
|
|
41
134
|
|
|
42
|
-
| Variable
|
|
43
|
-
|
|
|
44
|
-
| `PORT`
|
|
45
|
-
| `AGENT_USAGE_HOME`
|
|
46
|
-
| `AGENT_USAGE_MACHINE`
|
|
47
|
-
| `AGENT_USAGE_PROJECTS`
|
|
48
|
-
| `CLAUDE_CONFIG_DIR`
|
|
49
|
-
| `CODEX_HOME`
|
|
135
|
+
| Variable | Default | Meaning |
|
|
136
|
+
| --------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
137
|
+
| `PORT` | `3112` | Port to bind on `127.0.0.1` |
|
|
138
|
+
| `AGENT_USAGE_HOME` | `~/.local/share/agent-usage` | Store directory; must be `0700`, created so when missing |
|
|
139
|
+
| `AGENT_USAGE_MACHINE` | short, lower-cased hostname | Machine name stamped on every row |
|
|
140
|
+
| `AGENT_USAGE_PROJECTS` | none | Extra Known Projects, comma-separated (`RPS,ABC`) |
|
|
141
|
+
| `CLAUDE_CONFIG_DIR` | `~/.claude` | Claude Code's config; transcripts are read from `projects/` below it |
|
|
142
|
+
| `CODEX_HOME` | `~/.codex` | Codex's home; rollouts are read from `sessions/` and `archived_sessions/` |
|
|
143
|
+
| `CLAUDE_SECURESTORAGE_CONFIG_DIR` | unset | Where Claude Code keeps credentials when not in its config directory |
|
|
144
|
+
| `AGENT_USAGE_CLAUDE_LIMITS` | `${XDG_STATE_HOME:-~/.local/state}/agent-usage/claude-limits.jsonl` | The claude-statusline limit log, read like a transcript |
|
|
50
145
|
|
|
51
146
|
Ticket titles come from `acli jira workitem search` when `acli` is installed, cached for a day in
|
|
52
147
|
the store. Without it, tickets show their key and the status line says why.
|
|
@@ -80,11 +175,23 @@ with no price are shown as unpriced, never as $0.
|
|
|
80
175
|
|
|
81
176
|
- **Claude** limits appear in no transcript. They are polled from `GET
|
|
82
177
|
https://api.anthropic.com/api/oauth/usage`, the endpoint Claude Code's `/usage` dialog reads, with
|
|
83
|
-
the OAuth access token Claude Code already holds
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
178
|
+
the OAuth access token Claude Code already holds: `.credentials.json` in its secure-storage
|
|
179
|
+
directory (`CLAUDE_SECURESTORAGE_CONFIG_DIR`, else `CLAUDE_CONFIG_DIR`, else `~/.claude`), or on
|
|
180
|
+
macOS the login Keychain item Claude Code itself reads, `Claude Code-credentials` under your user
|
|
181
|
+
name, suffixed with `-` and the first eight hex digits of the directory's SHA-256 whenever a
|
|
182
|
+
non-default directory is set. The token is server-private: it is read per poll, sent only in that
|
|
183
|
+
request's `Authorization` header to `api.anthropic.com` with redirects refused, and never stored,
|
|
184
|
+
logged, refreshed or put in an error. A failed poll is stored as an Unknown reading with its
|
|
185
|
+
reason and what exactly went wrong (no credentials file, no Keychain item, the Keychain refusing
|
|
186
|
+
access with `security`'s exit code, an expired token, an HTTP status, an unreadable reply), shown
|
|
187
|
+
on hover or focus of the hatched gap and in the readings table.
|
|
188
|
+
- **claude-statusline** appends a line to its limit log whenever a Claude window's reading moves
|
|
189
|
+
(`{"v":1,"observedAt":<ms>,"machine":"<hostname>","window":"five_hour"|"seven_day"|"spend",
|
|
190
|
+
"usedPercentage":<number>,"resetsAt":<ms|null>}`). The log is read incrementally like a
|
|
191
|
+
transcript and recorded as Limit Snapshots of this Machine next to the polls, so a window is one
|
|
192
|
+
series whichever observed it. A repeated reading (same machine, window, reset and percentage) is
|
|
193
|
+
kept once; a line that does not decode, or another format version, is skipped and counted in the
|
|
194
|
+
status line. `spend` is the apps gateway's spend limit, shown as the Spend row.
|
|
88
195
|
- **Codex** writes its account limits and credit balance into every rollout, so its limit history
|
|
89
196
|
is backfilled from old sessions. Only the account-level `codex` limit is read; model-scoped limits
|
|
90
197
|
are left out. A forked subagent rollout begins with a copy of its parent's history; that copy is
|
|
@@ -104,8 +211,10 @@ do not publish, so any split would be invented.
|
|
|
104
211
|
`0600`, and a symlinked store directory or database file is refused.
|
|
105
212
|
- Nothing leaves the machine except the Claude limit poll to Anthropic and, when installed, `acli`
|
|
106
213
|
lookups of ticket keys against your Jira.
|
|
107
|
-
- The session cookie (`agent_usage_owner`, `HttpOnly`, `SameSite=Strict`, path `/api`)
|
|
108
|
-
|
|
214
|
+
- The session cookie (`agent_usage_owner`, `HttpOnly`, `SameSite=Strict`, path `/api`) is minted per
|
|
215
|
+
process. One-time codes are minted at startup and on each `agent-usage login`; they reach only
|
|
216
|
+
stdout of `serve` or `login`, are never logged, and with `--open` are on the opener's command line
|
|
217
|
+
for the moment it runs.
|
|
109
218
|
|
|
110
219
|
Each machine keeps its own store. Every row names its machine, so a combined view across machines
|
|
111
220
|
can come later without migrating anything.
|