@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.
Files changed (62) hide show
  1. package/README.md +129 -20
  2. package/dist/client/assets/index-BWajfpNv.js +55 -0
  3. package/dist/client/assets/{index-CUuLXODk.css → index-B_xl-_rU.css} +1 -1
  4. package/dist/client/index.html +2 -2
  5. package/dist/core/ClaudeLimitSamples.d.ts +30 -0
  6. package/dist/core/ClaudeLimitSamples.js +69 -0
  7. package/dist/core/ClaudeLimitSamples.js.map +1 -0
  8. package/dist/core/ClaudeLimits.d.ts +38 -4
  9. package/dist/core/ClaudeLimits.js +66 -19
  10. package/dist/core/ClaudeLimits.js.map +1 -1
  11. package/dist/core/ClaudeLimitsLive.d.ts +33 -7
  12. package/dist/core/ClaudeLimitsLive.js +54 -10
  13. package/dist/core/ClaudeLimitsLive.js.map +1 -1
  14. package/dist/core/Database.d.ts +9 -1
  15. package/dist/core/Database.js +6 -2
  16. package/dist/core/Database.js.map +1 -1
  17. package/dist/core/Ingest.d.ts +7 -1
  18. package/dist/core/Ingest.js +36 -6
  19. package/dist/core/Ingest.js.map +1 -1
  20. package/dist/core/Model.d.ts +19 -7
  21. package/dist/core/Model.js +15 -3
  22. package/dist/core/Model.js.map +1 -1
  23. package/dist/core/Report.js +21 -7
  24. package/dist/core/Report.js.map +1 -1
  25. package/dist/core/Store.js +14 -12
  26. package/dist/core/Store.js.map +1 -1
  27. package/dist/main.js +30 -20
  28. package/dist/main.js.map +1 -1
  29. package/dist/server/Api.d.ts +130 -94
  30. package/dist/server/Config.d.ts +22 -3
  31. package/dist/server/Config.js +66 -4
  32. package/dist/server/Config.js.map +1 -1
  33. package/dist/server/ControlSocket.d.ts +73 -0
  34. package/dist/server/ControlSocket.js +270 -0
  35. package/dist/server/ControlSocket.js.map +1 -0
  36. package/dist/server/HttpApplication.d.ts +1 -1
  37. package/dist/server/HttpApplication.js +2 -1
  38. package/dist/server/HttpApplication.js.map +1 -1
  39. package/dist/server/IngestSummary.d.ts +8 -0
  40. package/dist/server/IngestSummary.js +32 -0
  41. package/dist/server/IngestSummary.js.map +1 -0
  42. package/dist/server/Live.d.ts +4 -0
  43. package/dist/server/Live.js +49 -0
  44. package/dist/server/Live.js.map +1 -0
  45. package/dist/server/Login.d.ts +34 -0
  46. package/dist/server/Login.js +74 -0
  47. package/dist/server/Login.js.map +1 -0
  48. package/dist/server/OwnerSession.d.ts +15 -8
  49. package/dist/server/OwnerSession.js +33 -25
  50. package/dist/server/OwnerSession.js.map +1 -1
  51. package/dist/server/Runtime.d.ts +3 -1
  52. package/dist/server/Runtime.js +32 -8
  53. package/dist/server/Runtime.js.map +1 -1
  54. package/dist/server/Server.d.ts +2 -2
  55. package/dist/server/Server.js +19 -6
  56. package/dist/server/Server.js.map +1 -1
  57. package/dist/shared/contracts.d.ts +51 -6
  58. package/dist/shared/contracts.js +11 -1
  59. package/dist/shared/contracts.js.map +1 -1
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +3 -3
  62. 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 ingest`). From this
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 printed URL carries a
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 the server binds, so restart for a fresh one. The
32
- server only listens on loopback and only answers reads.
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 | Default | Meaning |
43
- | ---------------------- | ---------------------------- | ------------------------------------------------------------------------- |
44
- | `PORT` | `3112` | Port to bind on `127.0.0.1` |
45
- | `AGENT_USAGE_HOME` | `~/.local/share/agent-usage` | Store directory; must be `0700`, created so when missing |
46
- | `AGENT_USAGE_MACHINE` | short, lower-cased hostname | Machine name stamped on every row |
47
- | `AGENT_USAGE_PROJECTS` | none | Extra Known Projects, comma-separated (`RPS,ABC`) |
48
- | `CLAUDE_CONFIG_DIR` | `~/.claude` | Claude Code's config; transcripts are read from `projects/` below it |
49
- | `CODEX_HOME` | `~/.codex` | Codex's home; rollouts are read from `sessions/` and `archived_sessions/` |
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 (`<CLAUDE_CONFIG_DIR>/.credentials.json`, or the
84
- macOS login Keychain item `Claude Code-credentials`). The token is server-private: it is read per
85
- poll, sent only in that request's `Authorization` header to `api.anthropic.com` with redirects
86
- refused, and never stored, logged, refreshed or put in an error. A failed poll is stored as an
87
- Unknown reading with its reason, so the gap shows.
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`) and the
108
- one-time bootstrap code are minted per process; the code is printed to stdout only, never logged.
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.