@nightlab-agent/subagent-spend 0.7.2 → 0.8.0
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/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +10 -3
- package/CHANGELOG.md +24 -0
- package/README.md +39 -18
- package/hooks/register.tsx +60 -5
- package/hooks/spend.ts +4 -1
- package/hooks/usage.ts +4 -2
- package/package.json +6 -3
- package/subagent_spend.py +1 -1
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
{
|
|
8
8
|
"name": "subagent-spend",
|
|
9
9
|
"source": "./",
|
|
10
|
-
"description": "
|
|
11
|
-
"version": "0.
|
|
10
|
+
"description": "A spend cap for interactive sessions, and per-subagent tokens, rate-limit windows, context fill, cost, steps, tool calls and tool errors of a Claude Code session, in a notice under the prompt, local files and panes (the usage pane not yet seen on a screen).",
|
|
11
|
+
"version": "0.8.0",
|
|
12
12
|
"category": "productivity"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "subagent-spend",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "A spend cap for interactive sessions, and per-subagent tokens, rate-limit windows, context fill, cost, steps, tool calls and tool errors of a Claude Code session, in a notice under the prompt, local files and panes (the usage pane not yet seen on a screen).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Nightlab"
|
|
7
7
|
},
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"usage",
|
|
21
21
|
"headless",
|
|
22
22
|
"max-turns",
|
|
23
|
-
"budget"
|
|
23
|
+
"budget",
|
|
24
|
+
"spend-cap"
|
|
24
25
|
],
|
|
25
26
|
"userConfig": {
|
|
26
27
|
"dir": {
|
|
@@ -52,6 +53,12 @@
|
|
|
52
53
|
"title": "Turn counts folder",
|
|
53
54
|
"description": "Folder for the day's counts file. A relative path is under the project the session runs in; give an absolute path to keep every project in one place.",
|
|
54
55
|
"default": ".turn-tally"
|
|
56
|
+
},
|
|
57
|
+
"capUsd": {
|
|
58
|
+
"type": "number",
|
|
59
|
+
"title": "Spend cap (dollars)",
|
|
60
|
+
"description": "Stop sending requests once the session reaches this cost, by Claude Code's own estimate: at 80 percent a notice, at the cap the next model step is answered by the mod with no request made, until /spend-cap raises or lifts it. 0 is off.",
|
|
61
|
+
"default": 0
|
|
55
62
|
}
|
|
56
63
|
}
|
|
57
64
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
The form is Keep a Changelog; versions follow SemVer.
|
|
4
4
|
|
|
5
|
+
## 0.8.0 - 2026-11-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- A spend cap for interactive sessions (`capUsd`, off by default). Claude Code's own `--max-budget-usd` works only
|
|
9
|
+
in print mode (`claude -p`). At 80 percent of the cap a notice says so, once. At the cap, the next model step
|
|
10
|
+
(the main conversation's or a subagent's) is answered by the mod and no request goes out, until `/spend-cap`
|
|
11
|
+
raises the cap (`/spend-cap 5`) or lifts it (`/spend-cap off`). A step already running finishes, so its cost can
|
|
12
|
+
pass the cap. The figure is Claude Code's own client-side estimate, which can differ from your bill. Eight tests;
|
|
13
|
+
with the hold taken out, five of them fail. The cost is read before every step, not only after a turn, so a
|
|
14
|
+
tool loop or a subagent inside one turn is held too (two more tests, each beside a comparison case).
|
|
15
|
+
- With the cap on, the status line shows the cost the cap holds on, so it moves inside a turn too. When two
|
|
16
|
+
subagents step at once and a cost reading comes back late, the highest reading in the session is kept: a late,
|
|
17
|
+
lower one cannot set the count back under the cap, and a read still out when a session ends does not land in
|
|
18
|
+
the next one.
|
|
19
|
+
|
|
20
|
+
## 0.7.3 - 2026-10-31
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- The pane read "1 subagents" and "1 turns" when there was one; it now reads "1 subagent" and "1 turn". Three tests
|
|
24
|
+
had the old wording written into them and now expect the singular; one new test checks one and two of each.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- The README says the pane was seen updating step by step on a screen (a preview recording on 2026-10-31).
|
|
28
|
+
|
|
5
29
|
## 0.7.2 - 2026-10-30
|
|
6
30
|
|
|
7
31
|
### Added
|
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@ Claude Code meters: subagent tokens, usage limits and turn counts.
|
|
|
5
5
|
This mod (a plugin that adds behaviour to Claude Code) shows three things in
|
|
6
6
|
one line under the prompt and writes each to a small local file a script can
|
|
7
7
|
read: the tokens each subagent used, summed per subagent; the usage limits;
|
|
8
|
-
and a running count of steps, tool calls and tool errors.
|
|
8
|
+
and a running count of steps, tool calls and tool errors. If you set one, a
|
|
9
|
+
spend cap stops sending requests once an interactive session's cost reaches it. It also has side
|
|
9
10
|
panes: the subagent pane is seen below; the Usage pane is not yet seen on a
|
|
10
11
|
real screen. Nothing leaves your machine.
|
|
11
12
|
|
|
@@ -21,7 +22,7 @@ was cut with "…".
|
|
|
21
22
|
## Install
|
|
22
23
|
|
|
23
24
|
One line at the Claude Code prompt (Claude Code 2.1.275 or later; the tests
|
|
24
|
-
|
|
25
|
+
and previews run on 2.1.312):
|
|
25
26
|
|
|
26
27
|
```
|
|
27
28
|
/plugin install subagent-spend --marketplace nightlab-agent/subagent-spend
|
|
@@ -116,12 +117,34 @@ preview showed, after one turn with one subagent:
|
|
|
116
117
|
⚠ subagent-spend: subagents 1 | tok 18k (out 1068) | 5h 49% | 7d 26% | ctx 4% | $0.17 | steps 3 | tools 1 | errors …
|
|
117
118
|
```
|
|
118
119
|
|
|
120
|
+
## Spend cap
|
|
121
|
+
|
|
122
|
+
Claude Code's own cap, `--max-budget-usd`, works only in print mode
|
|
123
|
+
(`claude -p`, per its [CLI reference](https://code.claude.com/docs/en/cli-reference)).
|
|
124
|
+
For an interactive session, set `capUsd` in `/config` to a number of dollars.
|
|
125
|
+
At 80 percent of it a notice says so, once. At the cap, the next model step,
|
|
126
|
+
the main conversation's or a subagent's, is answered by the mod and no request
|
|
127
|
+
goes out, until you raise it (`/spend-cap 5`) or lift it (`/spend-cap off`)
|
|
128
|
+
for the session. `/spend-cap` alone shows the cap and the cost so far.
|
|
129
|
+
|
|
130
|
+

|
|
131
|
+
|
|
132
|
+
Seen in a Claude Code 2.1.312 terminal on 2026-10-31 with the cap set to
|
|
133
|
+
$0.10: one prompt asked Claude to read every file and describe each; after
|
|
134
|
+
it had read four files the cost was $0.11, and the next step, inside the
|
|
135
|
+
same turn, got the line above instead of a request. In an earlier run with
|
|
136
|
+
a $0.05 cap, the first prompt cost $0.06 and the second got the same line in
|
|
137
|
+
0 seconds, with no tool call. The figure is Claude Code's own client-side
|
|
138
|
+
estimate, which can differ from your bill. A step already running finishes,
|
|
139
|
+
so the cost can pass the cap: leave headroom. The 80 percent notice is
|
|
140
|
+
tested, not yet seen on a screen.
|
|
141
|
+
|
|
119
142
|
## What each part shows
|
|
120
143
|
|
|
121
144
|
**Subagent spend.** For each subagent: model turns (one request each), and
|
|
122
145
|
the input, output, cache-read and cache-creation tokens the API reported for
|
|
123
|
-
each of its steps, summed as the steps happen (
|
|
124
|
-
|
|
146
|
+
each of its steps, summed as the steps happen (seen in a terminal preview recording on
|
|
147
|
+
2026-10-31: three subagents' total climbing from 66k to 209k tokens as they ran). `context` is the last step's
|
|
125
148
|
prompt (input, cache read and cache creation) plus its reply: how full the
|
|
126
149
|
subagent's context was when it ended. The pane opens by itself at the first subagent of a session and lists them, biggest spender first, as many as its height holds;
|
|
127
150
|
`/subagent-spend` opens it again. An agent id is shown by its first six
|
|
@@ -162,6 +185,7 @@ Set them in `/config`.
|
|
|
162
185
|
| `usageFileDir` | `.usage-file` | Folder for `latest.json`. |
|
|
163
186
|
| `turnTally` | on | The turn counts. Off: nothing is counted, no counts in the status line, no day file. |
|
|
164
187
|
| `turnTallyDir` | `.turn-tally` | Folder for the day files. |
|
|
188
|
+
| `capUsd` | 0 (off) | The spend cap in dollars (see Spend cap). |
|
|
165
189
|
|
|
166
190
|
A relative folder is under the project the session runs in; an absolute
|
|
167
191
|
path keeps every project's files in one place. An option that is off
|
|
@@ -269,19 +293,20 @@ environment variable. Everything stays on your machine.
|
|
|
269
293
|
|
|
270
294
|
| Hook | What it does | What it changes |
|
|
271
295
|
|---|---|---|
|
|
272
|
-
| `session.start` | Registers `/subagent-spend`, and `/usage-file` when the usage part is on. | Nothing. |
|
|
273
|
-
| `command.run` | Opens the pane the command names. | Nothing. |
|
|
296
|
+
| `session.start` | Registers `/subagent-spend` and `/spend-cap`, and `/usage-file` when the usage part is on. | Nothing. |
|
|
297
|
+
| `command.run` | Opens the pane the command names; `/spend-cap` shows, sets or lifts the cap for the session. | Nothing. |
|
|
274
298
|
| `ui.render` (its own panes) | Draws the subagent rows, or the usage bars, from memory. | Nothing. |
|
|
275
299
|
| `agent.spawn` | Notes the subagent's type; opens the spend pane at the first one. | Nothing: the spawn goes on as asked, even if the mod fails. |
|
|
276
300
|
| `tool.call` | Counts the call, and whether it came back as an error (turn counts). | Nothing: the call runs as asked and its result passes through. |
|
|
277
|
-
| `session.measure` | Updates the usage figures, the status line and `latest.json`; opens the Usage pane once at 80 percent. | Nothing: the event passes through untouched. |
|
|
301
|
+
| `session.measure` | Updates the usage figures, the status line and `latest.json`; opens the Usage pane once at 80 percent; notes the cost for the spend cap and shows its 80 percent notice. | Nothing: the event passes through untouched. |
|
|
278
302
|
| `session.end` | At a `/clear` or a resume: the subagent rows, the panes' one-time opening and `/subagent-spend` start over for the next session. | Nothing: the event passes through untouched. |
|
|
279
|
-
| `turn.step` | Main conversation: counts the step and rewrites the day file. Subagent: adds the step's tokens to its row and rewrites the rows file. | Nothing: the
|
|
303
|
+
| `turn.step` | Main conversation: counts the step and rewrites the day file. Subagent: adds the step's tokens to its row and rewrites the rows file. | Nothing, unless a spend cap you set is reached: then the step is answered with the cap's line and no request goes out. |
|
|
280
304
|
|
|
281
305
|
## What it does not do
|
|
282
306
|
|
|
283
307
|
No dollars per subagent (plans have no price per token; use the counts).
|
|
284
|
-
No cloud or remote sessions.
|
|
308
|
+
No cloud or remote sessions. Apart from a spend cap you set, it never denies or
|
|
309
|
+
rewrites a request, and it
|
|
285
310
|
does not poll: with no event from Claude Code there is no write. It keeps no
|
|
286
311
|
history beyond the files it leaves, and it does not say which model a usage
|
|
287
312
|
window is for; Claude Code reports the windows as the account's.
|
|
@@ -328,13 +353,14 @@ sh tests/release.sh
|
|
|
328
353
|
```
|
|
329
354
|
|
|
330
355
|
It needs `claude` on PATH, Python 3, TypeScript (`tsc` on PATH, or set
|
|
331
|
-
`TSC` to a `tsc
|
|
356
|
+
`TSC` to a `tsc`; the check has passed on 5.9.3, 6.0.3 and 7.0.2, and no other
|
|
357
|
+
version has been tried) and Claude Code's type declarations
|
|
332
358
|
(written to `.claude-plugin/types/claude-code/index.d.ts` when you start one
|
|
333
359
|
session with `claude --plugin-dir .` from the clone, or set `CC_TYPES` to
|
|
334
360
|
that file). A missing compiler or missing declarations fails the check with
|
|
335
361
|
what to install; it never skips.
|
|
336
362
|
|
|
337
|
-
That runs `claude plugin validate .`; `claude plugin test .`,
|
|
363
|
+
That runs `claude plugin validate .`; `claude plugin test .`, 78 tests
|
|
338
364
|
through the test runner Claude Code ships for plugins, with a mocked file
|
|
339
365
|
system and mocked Claude Code events (the tests of the three former mods, one per
|
|
340
366
|
option turned off, and tests where events finish out of order, arrive two at
|
|
@@ -350,13 +376,8 @@ fixture test fails on each (`tests/planted_bug_test.py`); and a check that the v
|
|
|
350
376
|
plugin.json, marketplace.json, subagent_spend.py and CHANGELOG.md. It exits
|
|
351
377
|
non-zero if any step fails. Changes are listed in [CHANGELOG.md](CHANGELOG.md).
|
|
352
378
|
|
|
353
|
-
It needs the `claude` command, Python 3, and for the type check two more
|
|
354
|
-
things, each failing with an install hint when missing: TypeScript
|
|
355
|
-
(`tsc` on PATH, or set `TSC`; the check has passed on
|
|
356
|
-
5.9.3, 6.0.3 and 7.0.2, and no other version has been tried), and the
|
|
357
|
-
declarations Claude Code writes beside a mod it loads from a folder (run
|
|
358
|
-
`claude --plugin-dir .` once in the clone, or set `CC_TYPES` to that file).
|
|
359
|
-
|
|
360
379
|
Built and maintained by Nightlab, an AI agent created by xnfinite ([@xnfinite](https://github.com/xnfinite)).
|
|
361
380
|
|
|
381
|
+
Nightlab on YouTube: [@NightlabBC](https://www.youtube.com/@NightlabBC).
|
|
382
|
+
|
|
362
383
|
MIT license.
|
package/hooks/register.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { atom, read, update } from 'claude-code'
|
|
2
|
-
import type { EngineInterface, Register } from 'claude-code'
|
|
2
|
+
import type { EngineInterface, Register, SessionMeasureInput } from 'claude-code'
|
|
3
3
|
|
|
4
4
|
import type { Row, Shown } from '../types'
|
|
5
|
-
import { addStep, fmt, latest, nextStep, rowsText, paneLines, rows, spendStatus, types } from './spend'
|
|
5
|
+
import { addStep, count, fmt, latest, nextStep, rowsText, paneLines, rows, spendStatus, types } from './spend'
|
|
6
6
|
import { countCall, localDay, tally, tallyText, turnsStatus } from './turns'
|
|
7
|
+
import { cap, capCommand, capOf, heldText, reached, warnNow } from './cap'
|
|
7
8
|
import { OPEN_AT, bar, fileText, shownOf, shownStatus, usageLines, usageStatus } from './usage'
|
|
8
9
|
|
|
9
10
|
// subagent-spend, the hooks module: a session's meters in one mod.
|
|
@@ -24,8 +25,11 @@ import { OPEN_AT, bar, fileText, shownOf, shownStatus, usageLines, usageStatus }
|
|
|
24
25
|
// each part keeps its own piece and the line shows the pieces that have a reading, in the
|
|
25
26
|
// order spend | usage | turns.
|
|
26
27
|
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
28
|
+
// - cap (option `capUsd`, off by default): at the cap the next model step is answered by the
|
|
29
|
+
// mod with no request made (./cap.ts).
|
|
30
|
+
//
|
|
31
|
+
// DESIGN NOTE, deliberately narrow: apart from the cap the user sets, it never denies, never rewrites a
|
|
32
|
+
// request or an event, reads only the events the engine hands it and sends nothing off the machine. A failed
|
|
29
33
|
// write is swallowed (the spend part names it once in the status line): the files are
|
|
30
34
|
// advisory, and a gauge that stops the engine would be worse than no gauge.
|
|
31
35
|
|
|
@@ -130,6 +134,9 @@ function showStatus($: EngineInterface, parts: Parts) {
|
|
|
130
134
|
|
|
131
135
|
export const register: Register = (on, options) => {
|
|
132
136
|
const usageOn = options.usageFile !== false
|
|
137
|
+
cap.usd = capOf(options.capUsd)
|
|
138
|
+
cap.spent = null
|
|
139
|
+
cap.warned = false
|
|
133
140
|
const turnsOn = options.turnTally !== false
|
|
134
141
|
const spendDir = String(options.dir || '.subagent-spend')
|
|
135
142
|
const usageDir = String(options.usageFileDir || '.usage-file')
|
|
@@ -142,10 +149,17 @@ export const register: Register = (on, options) => {
|
|
|
142
149
|
let spendWarned = false
|
|
143
150
|
// Numbers each measurement in the order the engine raised them; latest.json is written from the newest.
|
|
144
151
|
let seq = 0
|
|
152
|
+
/** The last measurement, whose windows and context the status line keeps between measurements. */
|
|
153
|
+
let measured: SessionMeasureInput | undefined
|
|
154
|
+
// A session's cost only grows (SessionCost: summed over every priced response), so the highest
|
|
155
|
+
// reading is the newest: a read that answers late with a lower figure never brings it back. Each
|
|
156
|
+
// session end starts a new count, and a read still out for the session left is dropped.
|
|
157
|
+
let costSession = 0
|
|
145
158
|
|
|
146
159
|
on('session.start', async ($, e, next) => {
|
|
147
160
|
try {
|
|
148
161
|
await $.command.register({ name: 'subagent-spend', description: 'Show what each subagent of this session costs, in a pane' })
|
|
162
|
+
await $.command.register({ name: 'spend-cap', description: 'Show, set (/spend-cap 5) or lift (/spend-cap off) the spend cap of this session' })
|
|
149
163
|
if (usageOn) await $.command.register({ name: 'usage-file', description: 'Show the rate-limit windows, context and cost of this session, in a pane' })
|
|
150
164
|
} catch {
|
|
151
165
|
// a surface with no commands; the panes still open by themselves
|
|
@@ -153,6 +167,8 @@ export const register: Register = (on, options) => {
|
|
|
153
167
|
return next(e)
|
|
154
168
|
})
|
|
155
169
|
|
|
170
|
+
on('command.run', { command: 'spend-cap' }, async (_, e) => ({ text: capCommand(e.args ?? '') }))
|
|
171
|
+
|
|
156
172
|
on('command.run', { command: 'subagent-spend' }, async $ => {
|
|
157
173
|
await $.ui.open({ id: SPEND_PANE, title: SPEND_TITLE })
|
|
158
174
|
return { text: `Subagent spend pane opened. ${spendStatus()}` }
|
|
@@ -174,7 +190,7 @@ export const register: Register = (on, options) => {
|
|
|
174
190
|
return (
|
|
175
191
|
<Box flexDirection="column">
|
|
176
192
|
<Text bold>
|
|
177
|
-
{list.length}
|
|
193
|
+
{count(list.length, 'subagent')}, {fmt(tok)} tokens
|
|
178
194
|
</Text>
|
|
179
195
|
{list.length === 0 && <Text dimColor>No subagent has run in this session yet.</Text>}
|
|
180
196
|
{paneLines(list)
|
|
@@ -231,9 +247,14 @@ export const register: Register = (on, options) => {
|
|
|
231
247
|
|
|
232
248
|
on('session.end', async ($, e, next) => {
|
|
233
249
|
startOver(e.sessionId)
|
|
250
|
+
// the next session's first measurement brings its own cost; the cap the user set stays
|
|
251
|
+
cap.spent = null
|
|
252
|
+
cap.warned = false
|
|
253
|
+
costSession++
|
|
234
254
|
parts.spend = undefined
|
|
235
255
|
// cost and context belong to the session left: the panes and the line wait for the new one's first reading
|
|
236
256
|
parts.usage = undefined
|
|
257
|
+
measured = undefined
|
|
237
258
|
showStatus($, parts)
|
|
238
259
|
try {
|
|
239
260
|
await update($, shownRows, () => [])
|
|
@@ -245,8 +266,18 @@ export const register: Register = (on, options) => {
|
|
|
245
266
|
})
|
|
246
267
|
|
|
247
268
|
on('session.measure', async ($, e, next) => {
|
|
269
|
+
if (e.cost) cap.spent = Math.max(cap.spent ?? 0, e.cost.usd)
|
|
270
|
+
const warning = warnNow()
|
|
271
|
+
if (warning) {
|
|
272
|
+
try {
|
|
273
|
+
$.ui.toast(warning)
|
|
274
|
+
} catch {
|
|
275
|
+
// a surface with no toasts: the cap still holds
|
|
276
|
+
}
|
|
277
|
+
}
|
|
248
278
|
if (!usageOn) return next(e)
|
|
249
279
|
const mine = ++seq
|
|
280
|
+
measured = e
|
|
250
281
|
parts.usage = usageStatus(e)
|
|
251
282
|
showStatus($, parts)
|
|
252
283
|
try {
|
|
@@ -277,6 +308,30 @@ export const register: Register = (on, options) => {
|
|
|
277
308
|
})
|
|
278
309
|
|
|
279
310
|
on('turn.step', async function* ($, e, next) {
|
|
311
|
+
if (cap.usd !== null) {
|
|
312
|
+
// session.measure comes after a turn, not after each step: a tool loop or a subagent inside one
|
|
313
|
+
// turn is held only if the cost is read here, before each step
|
|
314
|
+
const session = costSession
|
|
315
|
+
try {
|
|
316
|
+
const usage = await $.session.usage()
|
|
317
|
+
const usd = session === costSession ? usage.cost?.usd : undefined
|
|
318
|
+
if (usd !== undefined && usd >= (cap.spent ?? 0)) cap.spent = usd
|
|
319
|
+
if (usd !== undefined && usd === cap.spent && usageOn && measured) {
|
|
320
|
+
// the line's dollar figure is the one the cap holds on, not the last turn's
|
|
321
|
+
parts.usage = usageStatus(measured, usd)
|
|
322
|
+
showStatus($, parts)
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
// no cost here: the last measurement stands
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (reached()) {
|
|
329
|
+
// answered here, no request made: the turn (or the subagent) ends on this text
|
|
330
|
+
const text = heldText()
|
|
331
|
+
yield { kind: 'text' as const, index: 0, text }
|
|
332
|
+
yield { kind: 'stop' as const, stopReason: 'end_turn' as const, usage: null }
|
|
333
|
+
return { turnId: e.turnId, index: e.index, answer: text, toolUses: [], stopReason: 'end_turn' as const, usage: null }
|
|
334
|
+
}
|
|
280
335
|
if (!e.agentId) {
|
|
281
336
|
if (!turnsOn) return yield* next(e)
|
|
282
337
|
// A main-loop step: counted as it starts, its file named and filled before any wait, so an
|
package/hooks/spend.ts
CHANGED
|
@@ -89,10 +89,13 @@ export const label = (id: string, all: readonly string[]) => {
|
|
|
89
89
|
return id
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/** `1 turn`, `2 turns`: a count with its word, singular for one. */
|
|
93
|
+
export const count = (n: number, word: string) => `${n} ${word}${n === 1 ? '' : 's'}`
|
|
94
|
+
|
|
92
95
|
/** The pane's lines, biggest spender first: `Explore a1b2c3 8 turns 41k (out 2104, ctx 24k) claude-opus-5-5`. */
|
|
93
96
|
export const paneLines = (list: Row[]) => {
|
|
94
97
|
const ids = list.map(r => r.agent_id)
|
|
95
98
|
return [...list]
|
|
96
99
|
.sort((a, b) => b.total - a.total)
|
|
97
|
-
.map(r => `${r.agent_type || 'agent'} ${label(r.agent_id, ids)} ${r.turns}
|
|
100
|
+
.map(r => `${r.agent_type || 'agent'} ${label(r.agent_id, ids)} ${count(r.turns, 'turn')} ${fmt(r.total)} (out ${fmt(r.output)}, ctx ${fmt(r.context ?? 0)}) ${r.model}`)
|
|
98
101
|
}
|
package/hooks/usage.ts
CHANGED
|
@@ -61,10 +61,12 @@ export const fileText = (e: SessionMeasureInput, session: string, now: Date) =>
|
|
|
61
61
|
) + '\n'
|
|
62
62
|
|
|
63
63
|
/** The status line part: only the figures that have a reading; undefined when none has. */
|
|
64
|
-
export const usageStatus = (e: SessionMeasureInput) => {
|
|
64
|
+
export const usageStatus = (e: SessionMeasureInput, usd?: number) => {
|
|
65
65
|
const parts = e.rateLimits.map(w => `${LABEL[w.kind] ?? w.kind} ${w.percentUsed}%`)
|
|
66
66
|
if (e.context.percent !== undefined) parts.push(`ctx ${e.context.percent}%`)
|
|
67
|
-
|
|
67
|
+
// `usd`: a cost read since that measurement (before a step, for the cap) replaces its figure
|
|
68
|
+
const shown = usd ?? e.cost?.usd
|
|
69
|
+
if (shown !== undefined) parts.push(`$${shown.toFixed(2)}`)
|
|
68
70
|
return parts.length ? parts.join(' | ') : undefined
|
|
69
71
|
}
|
|
70
72
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nightlab-agent/subagent-spend",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Claude Code mod: per-subagent tokens, rate-limit windows, context fill, cost, steps, tool calls and tool errors of a session, in a notice under the prompt, local files and panes.",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Claude Code mod: a spend cap for interactive sessions, and per-subagent tokens, rate-limit windows, context fill, cost, steps, tool calls and tool errors of a session, in a notice under the prompt, local files and panes (the usage pane not yet seen on a screen).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"claude-code-mod",
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
"subagents",
|
|
10
10
|
"token-usage",
|
|
11
11
|
"rate-limits",
|
|
12
|
-
"statusline"
|
|
12
|
+
"statusline",
|
|
13
|
+
"spend-cap",
|
|
14
|
+
"budget",
|
|
15
|
+
"max-budget-usd"
|
|
13
16
|
],
|
|
14
17
|
"homepage": "https://github.com/nightlab-agent/subagent-spend#readme",
|
|
15
18
|
"bugs": "https://github.com/nightlab-agent/subagent-spend/issues",
|
package/subagent_spend.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""subagent-spend 0.
|
|
2
|
+
"""subagent-spend 0.8.0: attribute Claude Code token usage to each subagent.
|
|
3
3
|
|
|
4
4
|
subagent_spend.py hook SubagentStop hook: reads the hook JSON on stdin, sums the usage
|
|
5
5
|
in the subagent transcript, appends one line to the session's rows file.
|