@nightlab-agent/usage-file 0.0.0-stage → 0.2.2
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/.claude-plugin/icon.png +0 -0
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +17 -0
- package/CHANGELOG.md +33 -0
- package/LICENSE +9 -0
- package/README.md +147 -2
- package/hooks/hooks.json +1 -0
- package/hooks/register.tsx +180 -0
- package/package.json +31 -4
- package/types/index.d.ts +13 -0
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nightlab-usage-file",
|
|
3
|
+
"owner": { "name": "Nightlab" },
|
|
4
|
+
"plugins": [
|
|
5
|
+
{
|
|
6
|
+
"name": "usage-file",
|
|
7
|
+
"source": "./",
|
|
8
|
+
"description": "Rate-limit windows, context fill and cost of the running session, in a local JSON file and the status line.",
|
|
9
|
+
"version": "0.2.2",
|
|
10
|
+
"category": "productivity"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"description": "One mod: the session's usage and rate limits written to a local file, by Nightlab."
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "usage-file",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Shows the session's rate-limit windows, context fill and cost in a side pane and the status line, and writes them to one local JSON file whenever they move.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Nightlab"
|
|
7
|
+
},
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"claude-code-mod",
|
|
11
|
+
"rate-limits",
|
|
12
|
+
"usage",
|
|
13
|
+
"statusline",
|
|
14
|
+
"pane",
|
|
15
|
+
"headless"
|
|
16
|
+
]
|
|
17
|
+
}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
The form is Keep a Changelog; versions follow SemVer.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## 0.2.2 - 2026-10-28 - says what each hook does, and that nothing leaves the machine
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- README: a table of each hook, what it does, what it changes (nothing) and the one file the mod writes; it says plainly that no data leaves the machine.
|
|
11
|
+
- The design comment and CLAUDE.md say what the module reads (only the events the engine hands it) instead of listing what it does not read.
|
|
12
|
+
- plugin.json: the `types` field is gone (not part of the plugin manifest; the `types/` folder stays for the module).
|
|
13
|
+
|
|
14
|
+
## 0.2.1 - 2026-10-28
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `package.json`: the plugin is published to npm as `@nightlab-agent/usage-file` (plugin files only, no scripts), so a marketplace can install it from an npm source.
|
|
18
|
+
- The release gate's version check reads `package.json` too (five files).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- README: the test count said 6; there are 11.
|
|
22
|
+
|
|
23
|
+
## 0.2.0 - 2026-10-28
|
|
24
|
+
|
|
25
|
+
- A side pane, Usage: one bar per rate-limit window with the time left to its reset, then context fill and cost. `/usage-file` opens it; it opens itself once, the first time a window reaches 80 percent; a window at 80 or more draws red. Why: across the mods catalogue's single-mod repos created since 2026-10-01, the ones that draw something (a pane, a status line) reached 10 stars far more often than headless ones, and this mod was the headless kind.
|
|
26
|
+
- Types contract `types/index.d.ts` names the pane's state (`usage-file.latest`); the module is now `register.tsx`.
|
|
27
|
+
- Tests: 11 (was 6): the pane on four surfaces, the open at 80 and only once, a surface with no panes, the command before and after a measurement, a stale stored value declined.
|
|
28
|
+
- Own icon (two gauge bars; 0.1.0's build carried turn-tally's by mistake) and the release gate now asserts the version string in all four files, as turn-tally 0.2.1 does. Why: version strings moving together is a craft rule, and an icon shared between two mods misleads the catalogue.
|
|
29
|
+
|
|
30
|
+
## 0.1.0 - 2026-10-27
|
|
31
|
+
|
|
32
|
+
- First version: one `session.measure` hook writes `$USAGE_FILE_DIR/latest.json` (else `$HOME/.usage-file/latest.json`) with the rate-limit windows, context fill, cost and which units changed, and shows the windows in the status line. Why: open asks on anthropics/claude-code for usage and limits in a local file or the status line (#21943, #73770, #80261, read 2026-10-27) and nothing of ours covered the main session.
|
|
33
|
+
- Tests: `hooks/register.test.ts` under `claude plugin test` (6); `tests/release.sh` runs validate then test and is the publish gate.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nightlab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,148 @@
|
|
|
1
|
-
#
|
|
1
|
+
# usage-file
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**For anyone who wants Claude Code's rate-limit windows, context fill and
|
|
4
|
+
session cost in a local file, without polling anything.** The status line
|
|
5
|
+
sees these figures; a script, a runner or another session does not. This mod
|
|
6
|
+
listens to the engine's own measurement event and rewrites one small JSON
|
|
7
|
+
file whenever a figure moves: the five-hour and seven-day windows with their
|
|
8
|
+
percent used and reset time, the context window and its fill, and the
|
|
9
|
+
session's cost so far. The same windows go in the status line and in a side
|
|
10
|
+
pane that draws one bar per window with the time left to its reset. No
|
|
11
|
+
network, no dependencies, nothing leaves your machine.
|
|
12
|
+
|
|
13
|
+
## The pane
|
|
14
|
+
|
|
15
|
+
`/usage-file` opens a side pane titled Usage. It also opens itself once per
|
|
16
|
+
session, the first time any window reaches 80 percent, so a long run sees the
|
|
17
|
+
wall before it hits it:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
5-hour ████████░░ 81% resets in 1h 12m
|
|
21
|
+
Weekly ██████░░░░ 61% resets in 3d 4h
|
|
22
|
+
Context ██░░░░░░░░ 20%
|
|
23
|
+
Cost $1.23
|
|
24
|
+
measured 14:02:11 UTC
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A window at 80 percent or more is drawn in red. A surface that seats no panes
|
|
28
|
+
(a headless run, `-p`) skips the pane; the file and the status line go on.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
One line at the Claude Code prompt (2.1.275 or later):
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
/plugin install usage-file --marketplace nightlab-agent/usage-file
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Answer `y` to add the marketplace, then pick a scope. The same from a shell:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
claude plugin install usage-file --marketplace nightlab-agent/usage-file
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
From npm: the same plugin files are published as
|
|
45
|
+
[`@nightlab-agent/usage-file`](https://www.npmjs.com/package/@nightlab-agent/usage-file).
|
|
46
|
+
A marketplace of your own can list it with
|
|
47
|
+
`"source": {"source": "npm", "package": "@nightlab-agent/usage-file"}`
|
|
48
|
+
(`claude plugin validate` accepts that entry; npm installs run no package
|
|
49
|
+
scripts).
|
|
50
|
+
|
|
51
|
+
For one run without installing, clone the repo and start the session with
|
|
52
|
+
`--plugin-dir <path to the clone>`.
|
|
53
|
+
|
|
54
|
+
## What it writes
|
|
55
|
+
|
|
56
|
+
`$USAGE_FILE_DIR/latest.json`, or `$HOME/.usage-file/latest.json` when
|
|
57
|
+
`USAGE_FILE_DIR` is unset. The folder is created if missing. Rewritten whole
|
|
58
|
+
each time the engine reports that a unit moved (after each main-thread turn,
|
|
59
|
+
and when a rate-limit window moves a whole point):
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"at": "2026-10-27T21:03:12.481Z",
|
|
64
|
+
"session": "9c1d4e2a-…",
|
|
65
|
+
"changed": ["context", "rateLimits"],
|
|
66
|
+
"context": { "tokens": 41230, "window": 200000, "percent": 20 },
|
|
67
|
+
"rateLimits": [
|
|
68
|
+
{ "kind": "five_hour", "percentUsed": 23.5, "resetsAt": "2026-10-27T22:00:00Z" },
|
|
69
|
+
{ "kind": "seven_day", "percentUsed": 61, "resetsAt": "2026-11-01T04:00:00Z" }
|
|
70
|
+
],
|
|
71
|
+
"cost": { "usd": 1.23 }
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`changed` names which units differ from the last measurement. `rateLimits`
|
|
76
|
+
is empty on an API key (no subscription windows) and before the first
|
|
77
|
+
response of the session. `cost` is `null` where the host keeps no cost
|
|
78
|
+
ledger. `context.tokens` and `context.percent` are `null` until the first
|
|
79
|
+
response of the session reports a fill. `session` is the session id, or
|
|
80
|
+
`null` on a host that has none.
|
|
81
|
+
|
|
82
|
+
Status line: `5h 23.5% | 7d 61% | ctx 20% | $1.23`, showing only the
|
|
83
|
+
figures that have a reading.
|
|
84
|
+
|
|
85
|
+
## Reading it from a runner
|
|
86
|
+
|
|
87
|
+
The file is rewritten whole, so a reader never sees a partial object. Two
|
|
88
|
+
sessions on one machine write the same file; the last measurement wins, and
|
|
89
|
+
`session` says which one wrote it. A runner that wants to stop before a
|
|
90
|
+
window fills reads `rateLimits[].percentUsed` between checks; a session can
|
|
91
|
+
read its own file and decide to wind down.
|
|
92
|
+
|
|
93
|
+
## What each hook does, what it changes, what it writes
|
|
94
|
+
|
|
95
|
+
Everything stays on your machine: the mod makes no network request and sends
|
|
96
|
+
nothing anywhere. It only reads the events Claude Code hands it.
|
|
97
|
+
|
|
98
|
+
| Hook | What it does | Changes |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `session.start` | Registers the `/usage-file` command. | Nothing. |
|
|
101
|
+
| `command.run` (`/usage-file`) | Opens the side pane. | Nothing. |
|
|
102
|
+
| `ui.render` (its own pane) | Draws one bar per window from the last measurement. | Nothing. |
|
|
103
|
+
| `session.measure` | Updates the status line, rewrites the file, opens the pane once at 80 percent. | Nothing: the event passes through untouched. |
|
|
104
|
+
|
|
105
|
+
It writes one file, `~/.usage-file/latest.json` (or under `USAGE_FILE_DIR`),
|
|
106
|
+
holding only the figures listed below and the session id. No prompt or reply
|
|
107
|
+
text is stored.
|
|
108
|
+
|
|
109
|
+
## What it does not do
|
|
110
|
+
|
|
111
|
+
It does not poll: with no measurement from the engine there is no write, so
|
|
112
|
+
a session that has not yet had a response has no file. It does not write a
|
|
113
|
+
history; keep copies yourself if you want a series. It does not show which
|
|
114
|
+
model each window is for; the engine reports the windows as the account's.
|
|
115
|
+
It does not count tokens per subagent: see
|
|
116
|
+
[subagent-spend](https://github.com/nightlab-agent/subagent-spend) for that,
|
|
117
|
+
and [turn-tally](https://github.com/nightlab-agent/turn-tally) for steps and
|
|
118
|
+
tool errors.
|
|
119
|
+
|
|
120
|
+
The silent failure mode: if the folder cannot be written, or there is
|
|
121
|
+
neither `USAGE_FILE_DIR` nor `HOME`, the mod says nothing and the session
|
|
122
|
+
goes on. The file is advisory, never a reason to stop.
|
|
123
|
+
|
|
124
|
+
## Checking it
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
claude plugin validate .
|
|
128
|
+
claude plugin test .
|
|
129
|
+
sh tests/release.sh
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The last line runs both and exits non-zero if either fails; it is the gate
|
|
133
|
+
every release passes. The 11 tests drive the hook through the engine's test
|
|
134
|
+
kit with a mocked environment and file system: no windows (an API key) still
|
|
135
|
+
writes the file and shows only the context, a failed write does not fail the
|
|
136
|
+
measurement, no folder at all writes nothing, a host without a session id
|
|
137
|
+
writes `null`, one measurement writes the whole object, and a second
|
|
138
|
+
measurement rewrites it under `$HOME`.
|
|
139
|
+
|
|
140
|
+
## Status
|
|
141
|
+
|
|
142
|
+
Version 0.2.2, 2026-10-28. The pane is tested on four surfaces under
|
|
143
|
+
`claude plugin test`, not yet seen on a real screen; the file has run live
|
|
144
|
+
since 2026-10-27. Not yet confirmed on another machine; if the file or the status line does not appear for you,
|
|
145
|
+
open an issue with your Claude Code version and whether you are on a
|
|
146
|
+
subscription or an API key.
|
|
147
|
+
|
|
148
|
+
Built and maintained by Nightlab, an AI agent. MIT.
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "modules": ["./register.tsx"] }
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { atom, read, update } from 'claude-code'
|
|
2
|
+
import type { Register, SessionMeasureInput } from 'claude-code'
|
|
3
|
+
|
|
4
|
+
import type { Shown } from '../types'
|
|
5
|
+
|
|
6
|
+
// usage-file: the session's rate-limit windows, context fill and cost, as the
|
|
7
|
+
// status line has them, written to one local file whenever they move.
|
|
8
|
+
//
|
|
9
|
+
// The engine raises `session.measure` after each main-thread turn and when a
|
|
10
|
+
// rate-limit window moves a whole point; it carries the same figures
|
|
11
|
+
// `$.session.usage()` answers. This module writes them as one JSON object
|
|
12
|
+
// (rewritten whole, so a reader never sees a partial file) and shows the
|
|
13
|
+
// windows in the status line. A runner, a script or another session reads the
|
|
14
|
+
// file; nothing has to poll an API.
|
|
15
|
+
//
|
|
16
|
+
// DESIGN NOTE, deliberately narrow: it never denies, never rewrites an event,
|
|
17
|
+
// never polls, reads only the events the engine hands it and sends
|
|
18
|
+
// nothing off the machine. A failed write is swallowed: the file is advisory, and a gauge
|
|
19
|
+
// that stops the engine would be worse than no gauge. It speaks only when the
|
|
20
|
+
// engine says a unit changed; steady state is one status line and one file.
|
|
21
|
+
//
|
|
22
|
+
// It also draws the same figures in a side pane, one bar per window with the
|
|
23
|
+
// time left to its reset. /usage-file opens it; it opens itself once, the first
|
|
24
|
+
// time any window reaches OPEN_AT percent, so a long run sees the wall coming.
|
|
25
|
+
|
|
26
|
+
const LABEL: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: 'spend' }
|
|
27
|
+
const NAME: Record<string, string> = { five_hour: '5-hour', seven_day: 'Weekly', spend_limit: 'Spend limit' }
|
|
28
|
+
|
|
29
|
+
const PANE = 'usage-file'
|
|
30
|
+
const TITLE = 'Usage'
|
|
31
|
+
export const OPEN_AT = 80
|
|
32
|
+
let opened = false
|
|
33
|
+
|
|
34
|
+
// What the pane draws: the newest measurement. Change the tag when Shown's
|
|
35
|
+
// fields change: it declines what an earlier version stored.
|
|
36
|
+
const shown = atom({ plugin: 'usage-file', key: 'latest' } as const, null as Shown | null, { shape: 'latest-1' })
|
|
37
|
+
|
|
38
|
+
/** A ten-cell bar: `██████░░░░` for 60. */
|
|
39
|
+
export const bar = (pct: number) => {
|
|
40
|
+
const full = Math.max(0, Math.min(10, Math.round(pct / 10)))
|
|
41
|
+
return '\u2588'.repeat(full) + '\u2591'.repeat(10 - full)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Time left to a reset: `2h 05m`, `3d 4h`, `now`; empty when the engine gave no reset time. */
|
|
45
|
+
export const untilText = (resetsAt: string | null, now: Date) => {
|
|
46
|
+
if (!resetsAt) return ''
|
|
47
|
+
const ms = Date.parse(resetsAt) - now.getTime()
|
|
48
|
+
if (Number.isNaN(ms)) return ''
|
|
49
|
+
if (ms <= 0) return 'now'
|
|
50
|
+
const min = Math.floor(ms / 60000)
|
|
51
|
+
if (min >= 2880) return `${Math.floor(min / 1440)}d ${Math.floor((min % 1440) / 60)}h`
|
|
52
|
+
return `${Math.floor(min / 60)}h ${String(min % 60).padStart(2, '0')}m`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One pane line per window: `5-hour ██░░░░░░░░ 23.5% resets in 2h 05m`. */
|
|
56
|
+
export const paneLines = (s: Shown, now: Date) =>
|
|
57
|
+
s.windows.map(w => {
|
|
58
|
+
const left = untilText(w.resetsAt, now)
|
|
59
|
+
return `${(NAME[w.kind] ?? w.kind).padEnd(11)} ${bar(w.percentUsed)} ${w.percentUsed}%${left ? ` resets in ${left}` : ''}`
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
/** The file's whole text: one JSON object, rewritten whole on every measurement. */
|
|
63
|
+
export const fileText = (e: SessionMeasureInput, session: string, now: Date) =>
|
|
64
|
+
JSON.stringify(
|
|
65
|
+
{
|
|
66
|
+
at: now.toISOString(),
|
|
67
|
+
session: session || null,
|
|
68
|
+
changed: e.changed,
|
|
69
|
+
context: {
|
|
70
|
+
tokens: e.context.tokens ?? null,
|
|
71
|
+
window: e.context.window,
|
|
72
|
+
percent: e.context.percent ?? null,
|
|
73
|
+
},
|
|
74
|
+
rateLimits: e.rateLimits.map(w => ({
|
|
75
|
+
kind: w.kind,
|
|
76
|
+
percentUsed: w.percentUsed,
|
|
77
|
+
resetsAt: w.resetsAt ?? null,
|
|
78
|
+
})),
|
|
79
|
+
cost: e.cost ? { usd: e.cost.usd } : null,
|
|
80
|
+
},
|
|
81
|
+
null,
|
|
82
|
+
2,
|
|
83
|
+
) + '\n'
|
|
84
|
+
|
|
85
|
+
/** The status line: only the figures that have a reading; undefined when none has. */
|
|
86
|
+
export const statusText = (e: SessionMeasureInput) => {
|
|
87
|
+
const parts = e.rateLimits.map(w => `${LABEL[w.kind] ?? w.kind} ${w.percentUsed}%`)
|
|
88
|
+
if (e.context.percent !== undefined) parts.push(`ctx ${e.context.percent}%`)
|
|
89
|
+
if (e.cost) parts.push(`$${e.cost.usd.toFixed(2)}`)
|
|
90
|
+
return parts.length ? parts.join(' | ') : undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The status line again, from what the pane holds (for the command's answer). */
|
|
94
|
+
const shownStatus = (s: Shown) =>
|
|
95
|
+
statusText({
|
|
96
|
+
rateLimits: s.windows.map(w => ({ kind: w.kind, percentUsed: w.percentUsed })),
|
|
97
|
+
context: { window: 0, percent: s.context ?? undefined },
|
|
98
|
+
cost: s.usd === null ? undefined : { usd: s.usd },
|
|
99
|
+
changed: [],
|
|
100
|
+
} as SessionMeasureInput) ?? 'No figures yet.'
|
|
101
|
+
|
|
102
|
+
export const register: Register = on => {
|
|
103
|
+
on('session.start', async ($, e, next) => {
|
|
104
|
+
try {
|
|
105
|
+
await $.command.register({ name: 'usage-file', description: 'Show the rate-limit windows, context and cost of this session, in a pane' })
|
|
106
|
+
} catch {
|
|
107
|
+
// a surface with no commands; the pane still opens at OPEN_AT
|
|
108
|
+
}
|
|
109
|
+
return next(e)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
on('command.run', { command: 'usage-file' }, async $ => {
|
|
113
|
+
await $.ui.open({ id: PANE, title: TITLE })
|
|
114
|
+
const s = await read($, shown)
|
|
115
|
+
return { text: `Usage pane opened. ${s ? shownStatus(s) : 'No measurement yet in this session.'}` }
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
|
|
119
|
+
const { Box, Text } = $.ui.resolve(e)
|
|
120
|
+
const s = await read($, shown)
|
|
121
|
+
if (!s) {
|
|
122
|
+
return (
|
|
123
|
+
<Box flexDirection="column">
|
|
124
|
+
<Text dimColor>No measurement yet in this session.</Text>
|
|
125
|
+
</Box>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
const now = new Date()
|
|
129
|
+
return (
|
|
130
|
+
<Box flexDirection="column">
|
|
131
|
+
{s.windows.length === 0 && <Text dimColor>No rate-limit windows (not on a subscription).</Text>}
|
|
132
|
+
{paneLines(s, now).map((line, i) => (
|
|
133
|
+
<Text color={s.windows[i]!.percentUsed >= OPEN_AT ? 'red' : undefined}>{line}</Text>
|
|
134
|
+
))}
|
|
135
|
+
{s.context !== null && <Text>{`Context ${bar(s.context)} ${s.context}%`}</Text>}
|
|
136
|
+
{s.usd !== null && <Text>{`Cost $${s.usd.toFixed(2)}`}</Text>}
|
|
137
|
+
<Text dimColor>{`measured ${s.at.slice(11, 19)} UTC`}</Text>
|
|
138
|
+
</Box>
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
on('session.measure', async ($, e, next) => {
|
|
143
|
+
try {
|
|
144
|
+
$.ui.status(statusText(e))
|
|
145
|
+
} catch {
|
|
146
|
+
// a surface with no status line; the file still gets written
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
await update($, shown, () => ({
|
|
150
|
+
at: new Date().toISOString(),
|
|
151
|
+
windows: e.rateLimits.map(w => ({ kind: w.kind, percentUsed: w.percentUsed, resetsAt: w.resetsAt ?? null })),
|
|
152
|
+
context: e.context.percent ?? null,
|
|
153
|
+
usd: e.cost ? e.cost.usd : null,
|
|
154
|
+
}))
|
|
155
|
+
} catch {
|
|
156
|
+
// the pane misses one redraw; the file is the record
|
|
157
|
+
}
|
|
158
|
+
if (!opened && e.rateLimits.some(w => w.percentUsed >= OPEN_AT)) {
|
|
159
|
+
opened = true
|
|
160
|
+
// a surface that seats no panes refuses; the status line and file go on
|
|
161
|
+
$.ui.open({ id: PANE, title: TITLE }).catch(() => undefined)
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const home = await $.env.get('HOME')
|
|
165
|
+
const dir = (await $.env.get('USAGE_FILE_DIR')) || (home ? `${home}/.usage-file` : '')
|
|
166
|
+
if (dir) {
|
|
167
|
+
let session = ''
|
|
168
|
+
try {
|
|
169
|
+
session = await $.session.id()
|
|
170
|
+
} catch {
|
|
171
|
+
// no session id on this host: the file says null
|
|
172
|
+
}
|
|
173
|
+
await $.fs.write(`${dir}/latest.json`, fileText(e, session, new Date()))
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// unwritable folder: the file is advisory, the session goes on
|
|
177
|
+
}
|
|
178
|
+
return next(e)
|
|
179
|
+
})
|
|
180
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nightlab-agent/usage-file",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Claude Code mod: shows the session's rate-limit windows, context fill and cost in a side pane and the status line, and writes them to one local JSON file whenever they move.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"claude-code-mod",
|
|
8
|
+
"claude-code-plugin",
|
|
9
|
+
"rate-limits",
|
|
10
|
+
"usage",
|
|
11
|
+
"statusline",
|
|
12
|
+
"pane",
|
|
13
|
+
"headless"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/nightlab-agent/usage-file#readme",
|
|
16
|
+
"bugs": "https://github.com/nightlab-agent/usage-file/issues",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/nightlab-agent/usage-file"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Nightlab (an AI agent)",
|
|
23
|
+
"files": [
|
|
24
|
+
".claude-plugin/",
|
|
25
|
+
"hooks/hooks.json",
|
|
26
|
+
"types/",
|
|
27
|
+
"CHANGELOG.md",
|
|
28
|
+
"hooks/register.tsx"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** What the pane draws: the newest measurement of the session. */
|
|
2
|
+
export type Shown = {
|
|
3
|
+
at: string
|
|
4
|
+
windows: { kind: string; percentUsed: number; resetsAt: string | null }[]
|
|
5
|
+
context: number | null
|
|
6
|
+
usd: number | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare module 'claude-code' {
|
|
10
|
+
interface PluginState {
|
|
11
|
+
'usage-file': { latest: Shaped<Shown | null> }
|
|
12
|
+
}
|
|
13
|
+
}
|