@iowarp/clio-coder 0.5.0 → 0.5.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/CHANGELOG.md +11 -0
- package/README.md +12 -9
- package/dist/{acp-KS7ARGV6.js → acp-X3N7JDGS.js} +2 -2
- package/dist/{agents-YOGJ2YEJ.js → agents-UYGSMAWR.js} +3 -3
- package/dist/assets/codewiki.json +1 -1
- package/dist/{chunk-EOJPDNUP.js → chunk-3OODBJ45.js} +1 -1
- package/dist/{chunk-R7FGRZNF.js → chunk-4D7QM7S5.js} +2 -2
- package/dist/{chunk-X4CEFOSG.js → chunk-6B76U65H.js} +4 -4
- package/dist/{chunk-LTWH2ACR.js → chunk-7FHSA7VB.js} +5 -5
- package/dist/{chunk-GRHOXDKB.js → chunk-7KI4ZSEB.js} +2 -2
- package/dist/{chunk-LYJJ6546.js → chunk-7VNLPEUG.js} +6 -6
- package/dist/{chunk-D65E25JG.js → chunk-A7G7OUND.js} +2 -2
- package/dist/{chunk-3JLTRPN7.js → chunk-AKQ3N3L4.js} +2 -2
- package/dist/{chunk-AVSR2HKI.js → chunk-BIQLF3NZ.js} +3 -3
- package/dist/{chunk-D3KWFOUT.js → chunk-DGMY2DWE.js} +2 -2
- package/dist/{chunk-IN7DGBVS.js → chunk-FBXTHPNZ.js} +2 -2
- package/dist/{chunk-EX57VVWW.js → chunk-GGEEG7CD.js} +2 -2
- package/dist/{chunk-V35KZDUZ.js → chunk-GW2KVAVI.js} +2 -2
- package/dist/{chunk-SJGS3GDI.js → chunk-KIAUZGKF.js} +2 -2
- package/dist/{chunk-CBAQPTDU.js → chunk-LFYA3HYH.js} +2 -2
- package/dist/{chunk-4VDHLCF5.js → chunk-M5FPIZQW.js} +2 -2
- package/dist/{chunk-R2UUNSNQ.js → chunk-O3ZAF7UY.js} +18 -5
- package/dist/{chunk-QNUQ7K7D.js → chunk-OXWRE2QJ.js} +2 -2
- package/dist/{chunk-55RMDDFM.js → chunk-PGQG7ZMW.js} +2 -2
- package/dist/{chunk-HQVN7G4F.js → chunk-QM7HFPQQ.js} +4 -4
- package/dist/{chunk-ZR5YA6ZF.js → chunk-QTYRMBW6.js} +2 -2
- package/dist/{chunk-3LT34CAM.js → chunk-SAL2SBP4.js} +8 -8
- package/dist/{chunk-OJXOA6YU.js → chunk-TL6LNERH.js} +2 -2
- package/dist/{chunk-U6YTUVOX.js → chunk-VMTCZ2GR.js} +2 -2
- package/dist/{chunk-OQ2EOAHA.js → chunk-VWKR2NN5.js} +5 -5
- package/dist/{chunk-QDUUOZ3S.js → chunk-WAISUJNQ.js} +2 -2
- package/dist/{chunk-XDHUDE5K.js → chunk-XOH3SD7H.js} +3 -3
- package/dist/{chunk-62UK7AGW.js → chunk-ZNTGRDFA.js} +281 -14
- package/dist/cli/index.js +17 -17
- package/dist/{clio-K2PBVCEM.js → clio-Z3JTEL2I.js} +2 -2
- package/dist/{config-Q5J7UBAB.js → config-QYIRZHI7.js} +8 -8
- package/dist/{config-graph-P3555574.js → config-graph-TS3SGGQW.js} +8 -8
- package/dist/{configure-KIJ32ZO6.js → configure-OX7KVJTQ.js} +2 -2
- package/dist/{context-LFBQOSXT.js → context-2BNF6HTP.js} +12 -12
- package/dist/{context-WBH3KSVR.js → context-GTMUGAR4.js} +7 -7
- package/dist/{context-clear-YOHMQ6SQ.js → context-clear-FVJQBBL7.js} +7 -7
- package/dist/{context-working-set-MRAEVDNV.js → context-working-set-MOE72SD6.js} +2 -2
- package/dist/{detail-RHJOTGN4.js → detail-QZM3QAAU.js} +8 -8
- package/dist/{dispatch-runner-NC7PYNIO.js → dispatch-runner-2KSQO5FU.js} +8 -8
- package/dist/{doctor-S5KZ2GTN.js → doctor-2U6YR5NH.js} +3 -3
- package/dist/{eval-3D6M36N7.js → eval-ZKK77JWZ.js} +3 -3
- package/dist/{evidence-6IYN52ET.js → evidence-FMP7KZZ2.js} +7 -7
- package/dist/{evidence-JVECPI3F.js → evidence-PJTOE7JA.js} +7 -7
- package/dist/{evidence-XNVPPLVK.js → evidence-POSEQW4Q.js} +9 -9
- package/dist/{evolve-TLPUDJDM.js → evolve-JT6PAMZE.js} +7 -7
- package/dist/{fleet-WKVBF5SZ.js → fleet-4XR2ZEOF.js} +16 -16
- package/dist/{fleet-6RJWIOEO.js → fleet-6JJKNIMV.js} +8 -8
- package/dist/{fleet-inspect-236FZPRB.js → fleet-inspect-SWJMFVDP.js} +9 -9
- package/dist/{fleet-preflight-NZQHABWO.js → fleet-preflight-KSIHWQAU.js} +4 -4
- package/dist/{fleet-verify-E3RO6A5W.js → fleet-verify-QM6XU2KL.js} +7 -7
- package/dist/{fleet-view-KMZQY7YJ.js → fleet-view-RLKFHFCM.js} +8 -8
- package/dist/gui/reads-worker.js +4 -4
- package/dist/{init-DV5B6HQG.js → init-VBUPHABR.js} +11 -11
- package/dist/{interactive-OXGYZYAS.js → interactive-JCVBPVYT.js} +1597 -503
- package/dist/{inventory-OEGIL6PY.js → inventory-54G7FSVY.js} +8 -8
- package/dist/{mcp-2GSPYQ54.js → mcp-EFCWUPUR.js} +3 -3
- package/dist/{memory-J3STUUIK.js → memory-5BPQSSOP.js} +7 -7
- package/dist/{models-Y3GMFJ2V.js → models-R6HILX6X.js} +3 -3
- package/dist/{monitor-LHNH577J.js → monitor-HVRKSFEQ.js} +10 -10
- package/dist/{orchestrator-DTAPXULA.js → orchestrator-OR7V4YWK.js} +26 -26
- package/dist/{preload-OJBMAIXF.js → preload-KLXFOX2F.js} +7 -7
- package/dist/{run-H64MEVTA.js → run-UIDRUYAY.js} +16 -16
- package/dist/{skills-eval-CQ23MYQK.js → skills-eval-JDAXOPWC.js} +7 -7
- package/dist/{slash-commands-ROG3KIFJ.js → slash-commands-MNKQ5QP3.js} +4 -4
- package/dist/{startup-background-KDWCQMWT.js → startup-background-PJMWTKVO.js} +7 -7
- package/dist/{system-ZDW44ZGX.js → system-IVVZRKGI.js} +2 -2
- package/dist/{targets-AJWA6E7Z.js → targets-RFRUMCQ5.js} +4 -4
- package/dist/{terminal-lease-YFUSDPAD.js → terminal-lease-V2N2BIKQ.js} +2 -2
- package/dist/{usage-XSILDHKF.js → usage-LA33SG3R.js} +9 -9
- package/dist/{wiki-generate-RP22WP5K.js → wiki-generate-HL73TICA.js} +10 -10
- package/dist/worker/entry.js +4 -4
- package/docs/architecture/architecture.md +1 -0
- package/docs/architecture/context-engine.md +2 -2
- package/docs/architecture/observability.md +6 -4
- package/docs/architecture/prompt-envelope-and-tools.md +1 -1
- package/docs/architecture/session-lifecycle.md +1 -1
- package/docs/architecture/tui-design.md +12 -9
- package/docs/gui/parity/02-slash-and-surfaces.md +1 -1
- package/docs/guide/commands-and-modes.md +91 -5
- package/docs/guide/configuration-and-targets.md +2 -2
- package/docs/guide/proactive-memory.md +3 -3
- package/package.json +1 -1
- package/src/cli/usage.ts +2 -2
- package/src/domains/gateway/mcp/client.ts +1 -1
- package/src/domains/observability/background-memory-usage.ts +1 -1
- package/src/domains/observability/cost.ts +3 -3
- package/src/domains/observability/extension.ts +1 -1
- package/src/domains/observability/metrics.ts +1 -1
- package/src/domains/quota/anthropic-max-provider.ts +134 -0
- package/src/domains/quota/anthropic-usage.ts +220 -0
- package/src/domains/quota/antigravity-provider.ts +339 -0
- package/src/domains/quota/cache.ts +87 -0
- package/src/domains/quota/claude-code-provider.ts +169 -0
- package/src/domains/quota/codex-provider.ts +237 -0
- package/src/domains/quota/presentation.ts +240 -0
- package/src/domains/quota/registry.ts +23 -0
- package/src/domains/quota/service.ts +93 -0
- package/src/domains/quota/summary-feed.ts +86 -0
- package/src/domains/quota/types.ts +84 -0
- package/src/domains/session/usage.ts +2 -2
- package/src/engine/acp/commands.ts +1 -1
- package/src/entry/orchestrator.ts +2 -2
- package/src/interactive/chat-loop-messages.ts +2 -2
- package/src/interactive/chat-loop.ts +3 -3
- package/src/interactive/dispatch-board.ts +21 -3
- package/src/interactive/footer/dashboard.ts +13 -0
- package/src/interactive/footer/key-hints.ts +2 -2
- package/src/interactive/footer/pages.ts +55 -20
- package/src/interactive/footer-panel.ts +1 -1
- package/src/interactive/interactive-application.ts +5 -2
- package/src/interactive/interactive-input-runtime.ts +2 -2
- package/src/interactive/interactive-presentation.ts +15 -0
- package/src/interactive/interactive-slash-runtime.ts +2 -2
- package/src/interactive/interactive-tickers.ts +7 -1
- package/src/interactive/overlay-general-openers.ts +12 -8
- package/src/interactive/overlay-key-routing.ts +6 -9
- package/src/interactive/overlay-lifecycle.ts +10 -6
- package/src/interactive/overlay-session-lifecycle.ts +3 -3
- package/src/interactive/quota-view.ts +229 -0
- package/src/interactive/session-last-turn.ts +1 -1
- package/src/interactive/session-usage-reseed.ts +2 -2
- package/src/interactive/side-question.ts +2 -2
- package/src/interactive/slash-commands.ts +8 -8
- package/src/interactive/turn-context.ts +1 -1
- package/src/interactive/turn-prewarm.ts +1 -1
- package/src/interactive/{cost-overlay.ts → usage-overlay.ts} +121 -33
- package/src/interactive/welcome-dashboard.ts +26 -3
|
@@ -250,7 +250,7 @@ The registry table below lists the available interactive slash commands. On a ba
|
|
|
250
250
|
| `/oracle` | `/oracle <question>` | Ask a read-only advisor to challenge a question against this session's settled decisions |
|
|
251
251
|
| `/council` | `/council [--roster <name>] [--rounds <n>] [--synthesis <judge\|vote\|none>] <task>` | Ask a roster of read-only members the same task, with an optional vote or judge synthesis |
|
|
252
252
|
| `/agents` | `/agents` | Open the Library on Agents. |
|
|
253
|
-
| `/
|
|
253
|
+
| `/usage` | `/usage` | Show subscription quota, credits, and session token and cost totals |
|
|
254
254
|
| `/doctor` | `/doctor [deep]` | Show a diagnostic report with errors and warnings first and full wrapped check details; `deep` adds live tool probes on the session's targets and a validation-contract dry run at the session's autonomy. See [Doctor](doctor.md). |
|
|
255
255
|
| `/context` | `/context compact [instructions] \| /context recall <ref> \| /context init [--preview] [--heuristic] [--adopt] [--global] [--propose\|--apply\|--rewrite] \| /context refresh \| /context reset` | Context hub: window overlay plus compact, recall, init, refresh, and reset |
|
|
256
256
|
| `/fleet` | `/fleet [run [--var <key=value>] <name>]` | Open Fleet Runs, or run a fleet contract with an approval preview. Configure fleets with `/settings fleet`. |
|
|
@@ -270,6 +270,92 @@ The registry table below lists the available interactive slash commands. On a ba
|
|
|
270
270
|
| `/fork` | `/fork` | Fork from an assistant turn |
|
|
271
271
|
| `/export` | `/export [path]` | Export a self-contained HTML transcript by default; a `.md` path writes Markdown |
|
|
272
272
|
|
|
273
|
+
### Subscription quota and session usage
|
|
274
|
+
|
|
275
|
+
`/usage` replaces `/cost`. There is no alias: `/cost` is no longer a command.
|
|
276
|
+
The overlay keeps the token and cost accounting `/cost` carried and adds what
|
|
277
|
+
the connected subscription accounts report. It is a live view of this session
|
|
278
|
+
and of your accounts right now; `clio-coder usage report` remains the separate
|
|
279
|
+
command for folding token and cost facts across past sessions.
|
|
280
|
+
|
|
281
|
+
| View | What it shows |
|
|
282
|
+
| --- | --- |
|
|
283
|
+
| Accounts | Subscription and credit meters, every reported model group, remaining capacity, reset countdowns and local reset times with an explicit time zone, and stale or expired readings |
|
|
284
|
+
| Session | Recorded token and cost totals, cache traffic, reasoning, and the background-call categories (side questions, handoffs, pre-warms, memory steps) |
|
|
285
|
+
| Models | Each model's share of recorded processed tokens, including cache traffic, plus its detailed token and cost breakdown |
|
|
286
|
+
| Workers | Active and recent runs, recorded tokens and cost, context usage, and the shared account limit when the local credential owner is known |
|
|
287
|
+
|
|
288
|
+
Press 1–4, Tab/Shift+Tab, or ←/→ to change views; ↑/↓ or PgUp/PgDn to scroll;
|
|
289
|
+
Home/End or Ctrl+Home/Ctrl+End to jump to either end; and Esc to close. Each
|
|
290
|
+
view keeps its own scroll position while the overlay stays open.
|
|
291
|
+
|
|
292
|
+
#### Used, left, and what a percentage describes
|
|
293
|
+
|
|
294
|
+
One direction per label, on every surface. A filled meter cell always means
|
|
295
|
+
consumed capacity, and the detailed meters in Accounts and Status label that
|
|
296
|
+
number `used` and print the complement beside it as `remaining`. Compact badges
|
|
297
|
+
carry the other direction and say so: `weekly 9% used` in a detailed meter is
|
|
298
|
+
the same reading as `weekly 91% left` in a footer badge.
|
|
299
|
+
|
|
300
|
+
Subscription percentages describe an account-wide provider window shared across
|
|
301
|
+
your sessions and devices. Session tokens and costs describe only what this
|
|
302
|
+
process recorded, and the two cannot be converted into each other. Context
|
|
303
|
+
occupancy is a third quantity again: it measures one request against a model's
|
|
304
|
+
window, not an account against its plan.
|
|
305
|
+
|
|
306
|
+
#### Where quota appears outside the overlay
|
|
307
|
+
|
|
308
|
+
The welcome header, the compact footer, the expanded dashboard, worker cards,
|
|
309
|
+
and fleet islands read the same cached readings the overlay does.
|
|
310
|
+
|
|
311
|
+
- The **welcome launchpad** carries a `Subscriptions` field beside the wordmark,
|
|
312
|
+
wrapped so every connected account and the free local-inference row stay visible.
|
|
313
|
+
- The **compact footer** shows the selected model's weekly headroom on line 1,
|
|
314
|
+
beside the model identity, and only when the account and model group are
|
|
315
|
+
identifiable. Line 2 always keeps the working directory and Git branch or dirty
|
|
316
|
+
state; notices, tips, and urgent prompts borrow the rotating hint area beside
|
|
317
|
+
them. Quota never takes line 2, and the footer never promotes an unrelated
|
|
318
|
+
account's busier window in place of the selected model's.
|
|
319
|
+
- The expanded dashboard's **Status** page puts the session total above the
|
|
320
|
+
account meters, **Activity** shows shared account headroom on worker cards, and
|
|
321
|
+
**Context** states that request occupancy and account limits are different
|
|
322
|
+
quantities.
|
|
323
|
+
|
|
324
|
+
#### What Clio does not claim
|
|
325
|
+
|
|
326
|
+
Worker badges are shared account headroom, not a measured per-worker share.
|
|
327
|
+
Clio does not infer a worker's subscription consumption from its token counts or
|
|
328
|
+
from account deltas, and a badge appears only for a supported local credential
|
|
329
|
+
owner and the matching model group. Antigravity's Gemini group stays distinct
|
|
330
|
+
from its Claude and GPT group. Remote workers and generic transports stay
|
|
331
|
+
unlinked rather than being attributed to an account that may not be theirs.
|
|
332
|
+
Clio's own `openai-codex` sign-in is separate storage from the Codex CLI's
|
|
333
|
+
`auth.json`, so a Codex CLI reading is never attributed to an `openai-codex`
|
|
334
|
+
worker.
|
|
335
|
+
|
|
336
|
+
Local inference is listed at `$0.00` with no subscription window consumed. No
|
|
337
|
+
saved-quota figure is claimed, because per-target token attribution does not
|
|
338
|
+
exist yet.
|
|
339
|
+
|
|
340
|
+
#### Reads, caching, and failures
|
|
341
|
+
|
|
342
|
+
Quota reads are read-only. They read stored credentials and never refresh a
|
|
343
|
+
token, write a credential, or start a sign-in. An expired credential is reported
|
|
344
|
+
as expired with the provider's own sign-in guidance rather than repaired.
|
|
345
|
+
|
|
346
|
+
A reading is cached for about five minutes and refreshed lazily when a surface
|
|
347
|
+
that shows it is rendered, never on a background timer. A failed refresh keeps
|
|
348
|
+
the last good reading and marks it `STALE` rather than blanking the account. A
|
|
349
|
+
provider that reports no window for a period reports nothing for it: Codex sends
|
|
350
|
+
a weekly window and no 5h window, and Clio prints the rows a provider actually
|
|
351
|
+
sent instead of a fixed shape. Where a provider sends its own severity word,
|
|
352
|
+
that word wins over Clio's thresholds.
|
|
353
|
+
|
|
354
|
+
Two credentials on one Anthropic subscription (an `anthropic-max` sign-in and a
|
|
355
|
+
Claude Code sign-in) report identical windows, so they are folded into one
|
|
356
|
+
account and the apparent budget is not doubled. Accounts that merely happen to
|
|
357
|
+
show equal percentages are never merged.
|
|
358
|
+
|
|
273
359
|
### Notes on individual commands
|
|
274
360
|
|
|
275
361
|
The notes below explain commands whose behavior needs more than a usage line.
|
|
@@ -345,7 +431,7 @@ context every worker inherits. Esc closes the overlay, and cancels the round if
|
|
|
345
431
|
is still streaming. `/btw` during an in-flight turn is refused with a notice
|
|
346
432
|
rather than queued, because a side question answered after the run it was asked
|
|
347
433
|
during has already missed its moment. The round's token usage still shows in
|
|
348
|
-
`/
|
|
434
|
+
`/usage`, labeled as a side question, because it was a real call and cost real
|
|
349
435
|
money; it is deliberately not counted as a turn.
|
|
350
436
|
|
|
351
437
|
`/council [--roster <name>] [--rounds <n>] [--synthesis judge|vote|none] <task>`
|
|
@@ -564,7 +650,7 @@ not a potentially double-counted total. Supported GPU drivers expose utilization
|
|
|
564
650
|
and VRAM through sysfs. Missing counters stay unavailable; WSL measurements are
|
|
565
651
|
labeled as guest measurements and do not describe the Windows host or remote GPU.
|
|
566
652
|
Counter resets wait for a fresh baseline instead of showing negative rates. Memory
|
|
567
|
-
bank and context-engine activity remain visible. `/
|
|
653
|
+
bank and context-engine activity remain visible. `/usage`, `/mcp`, `/library`, `/context`, and
|
|
568
654
|
Fleet Runs retain deeper inspection. Narrow or short terminals explicitly indicate
|
|
569
655
|
when detail does not fit.
|
|
570
656
|
|
|
@@ -1047,9 +1133,9 @@ The footer owns live activity. Transcript actions have static running or outcome
|
|
|
1047
1133
|
|
|
1048
1134
|
The Clio TUI maximizes operational focus, ergonomics, and discoverability:
|
|
1049
1135
|
|
|
1050
|
-
- **Welcome Launchpad & Session Header:** Prior to your first prompt, Clio renders a framed launchpad dashboard (`src/interactive/welcome-dashboard.ts`) with 11 detail rows (model route status, workspace path, permissions/autonomy, guidance, target inventory, and fleet recipes), accompanied by ASCII wordmark art at ≥76 columns and rotating hints at ≥160 columns. A bottom action row indicates the immediate step or blocker (`describe a task · Enter to send · / for commands`, or route blockers such as `/model` or `/settings targets`). On prompt submission, the launchpad collapses to a single live header row (`>C_ Clio Coder v0.5.
|
|
1136
|
+
- **Welcome Launchpad & Session Header:** Prior to your first prompt, Clio renders a framed launchpad dashboard (`src/interactive/welcome-dashboard.ts`) with 11 detail rows (model route status, workspace path, permissions/autonomy, guidance, target inventory, and fleet recipes) plus a wrapping `Subscriptions` field once a connected account reports quota, accompanied by ASCII wordmark art at ≥76 columns and rotating hints at ≥160 columns. A bottom action row indicates the immediate step or blocker (`describe a task · Enter to send · / for commands`, or route blockers such as `/model` or `/settings targets`). On prompt submission, the launchpad collapses to a single live header row (`>C_ Clio Coder v0.5.1 · <target/model> · <workspace> · <branch>*`) tracking mid-session route or branch changes. `/new` restores the launchpad; `/resume`, `/tree`, `/fork`, and `/handoff` collapse it after transition. See [tui-design.md](../architecture/tui-design.md#51-welcome-launchpad--session-header).
|
|
1051
1137
|
- **Quiet, Focused Clio Composer:** Normal composition rails remain clean and uncluttered (`src/interactive/clio-editor.ts`), omitting duplicate model identity or newline hints already provided by the footer. Exceptional operational states elevate to the top rail: `CONFIRM` (`warning` token, or `CONFIRM · FULL-AUTO` in `editorDanger`) when an approval prompt owns input, `PREPARING` during turn dispatch, `COMPACTING` during context compaction, or `FULL-AUTO` (`editorDanger`). Native editor scroll indicators (`↑/↓` line counts) remain on line 0 when drafts scroll. When the draft is empty, line 1 shows context-aware dim placeholder text (`Ask Clio… / for commands`, `A parked call is waiting for your decision`, `Clio has your prompt and is preparing the turn`, or `Clio is compacting the session context`). During parked permission prompts, the bottom rail carries fitted decision keys (`confirmRailHint` via `src/interactive/permission-hint.ts`: `Enter` allow, or `Backspace` clear draft to allow if a draft is present; `Esc` deny; `s` stop turn; `v` inspect mutation; standing approval terms toggle via `?` on the permission card itself).
|
|
1052
|
-
- **Two-Line Compact Footer & Expanded Dashboard:** The compact footer (`src/interactive/footer/pages.ts`) renders an ambient two-line strip: Line 1 displays live activity phase (`accent`) and active worker counts (`agent`), model/target identity, thinking level, context meter bar, and token usage; Line
|
|
1138
|
+
- **Two-Line Compact Footer & Expanded Dashboard:** The compact footer (`src/interactive/footer/pages.ts`) renders an ambient two-line strip: Line 1 displays live activity phase (`accent`) and active worker counts (`agent`), model/target identity, thinking level, context meter bar, and token usage; Line 1 also shows the selected model’s weekly quota remaining when its account and model group are known; Line 2 preserves workspace cwd and Git branch/dirty state on the left, with rotating shortcuts, tips, or urgent notices on the right. Pressing `Alt+U` expands the dashboard into 1/4 of the viewport (minimum 8 rows), cycling through `Activity` (live worker cards and finished history), `Context` (meter bar/grid, category swatches, headroom), `Status` (Cost & Connections, Local Machine metrics, memory bank), and closed.
|
|
1053
1139
|
- **Grouped Slash Command Palette:** Typing `/` opens an autocomplete command palette grouped by operational category (`Run`, `Inspect`, `Configure`, `Sessions`) with compact argument hints. Every suggestion is the command's one canonical spelling.
|
|
1054
1140
|
- **Voice-First Transcript & Receipts:** User (`› `) and assistant (`✦ `) prose are formatted with a two-cell hanging indent, ensuring wrapped continuation lines remain visually tied to their voice prefix. Tool ledgers maintain full terminal width. Completed turn receipts honor Output style: Compact omits the separate receipt, Standard shows a small completion line with available duration, and Detailed includes available call counts, token usage and reasoning provenance.
|
|
1055
1141
|
- **Transactional Settings Center:** Open `/settings` or deep-link to one of `chat`, `fleet`, `targets`, `context`, `safety`, `interface`, or `integrations`. Value edits construct change plans offering `Apply this session`, `Apply and save globally`, or `Cancel`; narrow terminals use a drill-down layout below 72 columns.
|
|
@@ -687,7 +687,7 @@ Every new assistant ledger entry carries `responseModelIdObservation` in one of
|
|
|
687
687
|
|
|
688
688
|
</details>
|
|
689
689
|
|
|
690
|
-
The adapter retains `responseModel` as the differing response id because providers outside the stream tap still supply that fact. `clio-coder usage report` emits `attributedModelId`, `requestedModelIds`, and `responseModelIdObservationCounts`. Its text table and the `/
|
|
690
|
+
The adapter retains `responseModel` as the differing response id because providers outside the stream tap still supply that fact. `clio-coder usage report` emits `attributedModelId`, `requestedModelIds`, and `responseModelIdObservationCounts`. Its text table and the `/usage` overlay use the labels `attributed model`, `requested model ids`, and `response model id observation`; requested ids are printed as ids rather than as `same`.
|
|
691
691
|
|
|
692
692
|
The footer's last-turn line uses `response model id observation <state>`, with the id after `reported` or a historical `legacy difference-only` state. Dispatch receipt `upstreamResponses` entries carry `requestedModelId`, `responseModelIdObservation`, `differingResponseModelId`, and `providerResponseId`. The peer warning is said once per process per distinct fact (target, requested id, resolved instance, peer set), not once per turn.
|
|
693
693
|
|
|
@@ -1235,7 +1235,7 @@ integrations:
|
|
|
1235
1235
|
```
|
|
1236
1236
|
Then invoke it using `/delegate claude-code <task>`.
|
|
1237
1237
|
|
|
1238
|
-
`/delegate [--share] <agent-id> <task>` does not accept `--model` in v0.5.
|
|
1238
|
+
`/delegate [--share] <agent-id> <task>` does not accept `--model` in v0.5.1.
|
|
1239
1239
|
`/run --model <id>` selects a model for a Clio fleet worker; it does not configure
|
|
1240
1240
|
an external ACP session. External model selection depends on the adapter's own
|
|
1241
1241
|
supported configuration. Clio does not currently expose an ACP model selector.
|
|
@@ -19,7 +19,7 @@ writes, model resolution, reminders, handoff offers, and handoff seeding.
|
|
|
19
19
|
| --- | --- |
|
|
20
20
|
| Inspect the bank and recent memory steps | `/memory` |
|
|
21
21
|
| Select a background model or change memory controls | `/settings` → **Context & Memory** |
|
|
22
|
-
| Review recorded cost | `/
|
|
22
|
+
| Review recorded cost | `/usage` or `clio-coder usage report` |
|
|
23
23
|
| Look up the default values and routing example | [Operator setup](#operator-setup) |
|
|
24
24
|
| Understand what survives a handoff | [Handoff continuity](#handoff-continuity) |
|
|
25
25
|
|
|
@@ -234,7 +234,7 @@ The LLM tier costs real tokens, real seconds of model time, and a request slot o
|
|
|
234
234
|
a server that is usually the same machine the operator's own turns run on.
|
|
235
235
|
|
|
236
236
|
Every step is therefore accounted for the way a `/btw` side question is: one cost
|
|
237
|
-
entry under the `background-memory` label, which `/
|
|
237
|
+
entry under the `background-memory` label, which `/usage` shows as its own
|
|
238
238
|
`memory steps` row, and one durable row in `<stateDir>/usage/out-of-turn.jsonl`
|
|
239
239
|
carrying the usage, the call's duration, and the backend's prefill facts.
|
|
240
240
|
`clio-coder usage report` folds those rows after the process exits, and `/memory`
|
|
@@ -582,7 +582,7 @@ Every exact-schema record has:
|
|
|
582
582
|
- count of cited entries, input/output/total memory-model tokens, and latency.
|
|
583
583
|
|
|
584
584
|
The same steps are also billed. See "Cost and the default decision" for the
|
|
585
|
-
`/
|
|
585
|
+
`/usage` row, the durable out-of-turn usage row, and the lifetime figures `/memory`
|
|
586
586
|
folds out of this file.
|
|
587
587
|
|
|
588
588
|
`dropped` is the one outcome that ran no step. It has two causes, separated by
|
package/package.json
CHANGED
package/src/cli/usage.ts
CHANGED
|
@@ -114,7 +114,7 @@ function addResponseModelIdObservation(counts: ResponseModelIdObservationCounts,
|
|
|
114
114
|
* session pre-warm, and a proactive-memory step are real spend and belong in
|
|
115
115
|
* the token and cost totals, but none of them is a turn: they never entered
|
|
116
116
|
* the session. `turns` therefore subtracts them from the folded row count,
|
|
117
|
-
* which is exactly what the `/
|
|
117
|
+
* which is exactly what the `/usage` overlay does.
|
|
118
118
|
*/
|
|
119
119
|
interface CallOrigins {
|
|
120
120
|
rows: number;
|
|
@@ -435,7 +435,7 @@ export async function runUsageCommand(argv: ReadonlyArray<string>): Promise<numb
|
|
|
435
435
|
}
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
-
// Token and cost facts fold the same per-call usage the `/
|
|
438
|
+
// Token and cost facts fold the same per-call usage the `/usage` overlay
|
|
439
439
|
// reseeds from, through the same session-domain function, so the report and
|
|
440
440
|
// the overlay cannot disagree about what a session spent.
|
|
441
441
|
// Keyed by the model id accounting attributes the call to. A reported LM
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
} from "./protocol.js";
|
|
36
36
|
|
|
37
37
|
export const MCP_CLIENT_NAME = "clio-coder";
|
|
38
|
-
export const DEFAULT_MCP_CLIENT_VERSION = "0.5.
|
|
38
|
+
export const DEFAULT_MCP_CLIENT_VERSION = "0.5.1";
|
|
39
39
|
export const DEFAULT_INITIALIZE_TIMEOUT_MS = 15_000;
|
|
40
40
|
export const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
41
41
|
export const DEFAULT_STDERR_TAIL_BYTES = 16 * 1024;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The two writes are the same pair the side-question path makes: the
|
|
12
12
|
* in-process cost tracker under the `background-memory` label, which is what
|
|
13
|
-
* `/
|
|
13
|
+
* `/usage` folds, and one durable row in the out-of-turn usage store, which is
|
|
14
14
|
* what an archive reader folds after the process is gone.
|
|
15
15
|
*/
|
|
16
16
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Per-entry token breakdown (input/output/cacheRead/cacheWrite/reasoning)
|
|
9
9
|
* matches the shape of pi-ai's `Usage` plus provider-specific reasoning detail
|
|
10
|
-
* fields. The /
|
|
10
|
+
* fields. The /usage overlay aggregates it via `aggregateCostEntries`; the TUI
|
|
11
11
|
* footer consumes the session sum through `ObservabilityContract.sessionTokens()`.
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -92,7 +92,7 @@ export function formatCostAggregate(cost: CostAggregate | null | undefined): str
|
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* What a fixed-width surface says when there is no cost claim. The footer and
|
|
95
|
-
* `/
|
|
95
|
+
* `/usage` drop their cost field instead; a table cell that owns a column cannot,
|
|
96
96
|
* so it says the same thing in words rather than inventing `$0.00`.
|
|
97
97
|
*/
|
|
98
98
|
export const COST_NOT_MEASURED = "not measured";
|
|
@@ -134,7 +134,7 @@ export interface UsageBreakdown {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
|
-
* What produced a priced call, when it was not an ordinary turn. `/
|
|
137
|
+
* What produced a priced call, when it was not an ordinary turn. `/usage` and
|
|
138
138
|
* the usage surfaces separate these out so an operator can see that money was
|
|
139
139
|
* spent beside the session rather than inside it.
|
|
140
140
|
*/
|
|
@@ -53,7 +53,7 @@ function recordDispatchCost(
|
|
|
53
53
|
}
|
|
54
54
|
telemetry.record("counter", "tokens.total", payload.tokenCount);
|
|
55
55
|
// Dispatch terminal payloads carry the same full split as receipts. Preserve
|
|
56
|
-
// it so /
|
|
56
|
+
// it so /usage and the footer agree with the fleet board instead of showing
|
|
57
57
|
// zero input/output/cache for worker-only sessions.
|
|
58
58
|
cost.accumulate(
|
|
59
59
|
payload.targetId,
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quota adapter for the `anthropic-max` runtime, the account Clio itself
|
|
3
|
+
* spends when it runs a native turn.
|
|
4
|
+
*
|
|
5
|
+
* Unlike `claude-code`, this credential belongs to Clio: it is the OAuth
|
|
6
|
+
* record her own auth storage persists under the `anthropic` provider id.
|
|
7
|
+
* The adapter reads the stored credential without triggering a refresh,
|
|
8
|
+
* because a quota read must never mutate an authentication record.
|
|
9
|
+
*
|
|
10
|
+
* Both adapters can point at the same Anthropic account. That is a display
|
|
11
|
+
* concern rather than a correctness one: each reports what its own
|
|
12
|
+
* credential sees, and a consumer that shows both should say which
|
|
13
|
+
* credential produced which row.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { openAuthStorage } from "../providers/auth/index.js";
|
|
17
|
+
import { fetchAnthropicUsage } from "./anthropic-usage.js";
|
|
18
|
+
import type { QuotaProvider, UsageSnapshot } from "./types.js";
|
|
19
|
+
|
|
20
|
+
export const ANTHROPIC_MAX_QUOTA_PROVIDER_ID = "anthropic-max";
|
|
21
|
+
|
|
22
|
+
/** The provider id Clio's auth storage files an Anthropic OAuth credential under. */
|
|
23
|
+
const AUTH_PROVIDER_ID = "anthropic";
|
|
24
|
+
const DISPLAY_NAME = "Anthropic Max";
|
|
25
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
26
|
+
|
|
27
|
+
/** The subset of Clio's stored OAuth credential this adapter needs. */
|
|
28
|
+
export interface AnthropicMaxCredentials {
|
|
29
|
+
accessToken: string;
|
|
30
|
+
/** Epoch milliseconds the access token expires, or null when the record omits it. */
|
|
31
|
+
expiresAtMs: number | null;
|
|
32
|
+
hasRefreshToken: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AnthropicMaxQuotaOptions {
|
|
36
|
+
readCredentials?: () => Promise<AnthropicMaxCredentials | null>;
|
|
37
|
+
fetch?: typeof globalThis.fetch;
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
now?: () => Date;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Shape a stored auth credential into the fields a usage read needs. */
|
|
43
|
+
export function parseStoredAnthropicCredential(raw: unknown): AnthropicMaxCredentials | null {
|
|
44
|
+
if (!isRecord(raw) || raw.type !== "oauth") return null;
|
|
45
|
+
const access = raw.access;
|
|
46
|
+
if (typeof access !== "string" || access.length === 0) return null;
|
|
47
|
+
const refresh = raw.refresh;
|
|
48
|
+
return {
|
|
49
|
+
accessToken: access,
|
|
50
|
+
expiresAtMs: asNumber(raw.expires),
|
|
51
|
+
hasRefreshToken: typeof refresh === "string" && refresh.length > 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read Clio's stored Anthropic credential.
|
|
57
|
+
*
|
|
58
|
+
* `AuthStorage.get` is the accessor that does no refresh, unlike
|
|
59
|
+
* `resolveApiKey`, which renews an expiring credential as a side effect.
|
|
60
|
+
*/
|
|
61
|
+
async function readStoredCredential(): Promise<AnthropicMaxCredentials | null> {
|
|
62
|
+
try {
|
|
63
|
+
return parseStoredAnthropicCredential(openAuthStorage().get(AUTH_PROVIDER_ID));
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Build the read-only `anthropic-max` quota adapter. */
|
|
70
|
+
export function createAnthropicMaxQuotaProvider(options: AnthropicMaxQuotaOptions = {}): QuotaProvider {
|
|
71
|
+
const readCredentials = options.readCredentials ?? readStoredCredential;
|
|
72
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
73
|
+
const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
74
|
+
const now = options.now ?? (() => new Date());
|
|
75
|
+
|
|
76
|
+
const snapshot = (
|
|
77
|
+
status: UsageSnapshot["status"],
|
|
78
|
+
message: string | null,
|
|
79
|
+
extra: Partial<UsageSnapshot> = {},
|
|
80
|
+
): UsageSnapshot => ({
|
|
81
|
+
providerId: ANTHROPIC_MAX_QUOTA_PROVIDER_ID,
|
|
82
|
+
displayName: DISPLAY_NAME,
|
|
83
|
+
status,
|
|
84
|
+
windows: [],
|
|
85
|
+
message,
|
|
86
|
+
fetchedAt: now().toISOString(),
|
|
87
|
+
...extra,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id: ANTHROPIC_MAX_QUOTA_PROVIDER_ID,
|
|
92
|
+
displayName: DISPLAY_NAME,
|
|
93
|
+
|
|
94
|
+
async detect(): Promise<boolean> {
|
|
95
|
+
return (await readCredentials()) !== null;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async fetch(): Promise<UsageSnapshot> {
|
|
99
|
+
const credentials = await readCredentials();
|
|
100
|
+
if (credentials === null) {
|
|
101
|
+
return snapshot("no_credentials", "Clio holds no Anthropic OAuth credential");
|
|
102
|
+
}
|
|
103
|
+
if (credentials.expiresAtMs !== null && credentials.expiresAtMs <= now().getTime()) {
|
|
104
|
+
return snapshot(
|
|
105
|
+
"expired",
|
|
106
|
+
credentials.hasRefreshToken
|
|
107
|
+
? "Clio's Anthropic token expired; the next turn refreshes it"
|
|
108
|
+
: "Run clio-coder auth login anthropic again",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const usage = await fetchAnthropicUsage(credentials.accessToken, { fetch: doFetch, timeoutMs });
|
|
113
|
+
if (usage.status !== "ok") {
|
|
114
|
+
return snapshot(usage.status, usage.message ?? "Run clio-coder auth login anthropic again", {
|
|
115
|
+
retryAfterSeconds: usage.retryAfterSeconds,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return snapshot("ok", null, { windows: usage.windows, credits: usage.credits });
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
124
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function asNumber(value: unknown): number | null {
|
|
128
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
129
|
+
if (typeof value === "string") {
|
|
130
|
+
const parsed = Number.parseFloat(value.trim());
|
|
131
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reader for Anthropic's OAuth usage endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Two runtimes spend an Anthropic subscription: the `claude-code` CLI with
|
|
5
|
+
* its own stored login, and `anthropic-max` with the OAuth credential Clio
|
|
6
|
+
* itself holds. The account surface is the same endpoint and the same two
|
|
7
|
+
* response shapes, so only the token source differs. This module owns the
|
|
8
|
+
* request and the parsing; each adapter owns its credential.
|
|
9
|
+
*
|
|
10
|
+
* The endpoint has shipped both a `limits[]` array and flat
|
|
11
|
+
* `five_hour`/`seven_day` buckets, and has been observed serving both at
|
|
12
|
+
* once. The array wins when present because only it carries severity,
|
|
13
|
+
* the active flag, and per-model scope.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { parseRetryAfterSeconds, type UsageCredits, type UsageSnapshot, type UsageWindow } from "./types.js";
|
|
17
|
+
|
|
18
|
+
export const ANTHROPIC_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
19
|
+
export const ANTHROPIC_OAUTH_BETA = "oauth-2025-04-20";
|
|
20
|
+
|
|
21
|
+
const WINDOW_ORDER: Record<string, number> = { session: 0, weekly: 1 };
|
|
22
|
+
|
|
23
|
+
/** What one usage read produced, before an adapter stamps its own identity on it. */
|
|
24
|
+
export interface AnthropicUsageOutcome {
|
|
25
|
+
status: UsageSnapshot["status"];
|
|
26
|
+
message: string | null;
|
|
27
|
+
windows: UsageWindow[];
|
|
28
|
+
credits: UsageCredits | null;
|
|
29
|
+
retryAfterSeconds: number | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AnthropicUsageDependencies {
|
|
33
|
+
fetch: typeof globalThis.fetch;
|
|
34
|
+
timeoutMs: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function outcome(
|
|
38
|
+
status: UsageSnapshot["status"],
|
|
39
|
+
message: string | null,
|
|
40
|
+
extra: Partial<AnthropicUsageOutcome> = {},
|
|
41
|
+
): AnthropicUsageOutcome {
|
|
42
|
+
return { status, message, windows: [], credits: null, retryAfterSeconds: null, ...extra };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read current Anthropic usage with a bearer token, reporting failure as a status. */
|
|
46
|
+
export async function fetchAnthropicUsage(
|
|
47
|
+
token: string,
|
|
48
|
+
dependencies: AnthropicUsageDependencies,
|
|
49
|
+
): Promise<AnthropicUsageOutcome> {
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
const timer = setTimeout(() => controller.abort(), dependencies.timeoutMs);
|
|
52
|
+
let response: Response;
|
|
53
|
+
try {
|
|
54
|
+
response = await dependencies.fetch(ANTHROPIC_USAGE_URL, {
|
|
55
|
+
method: "GET",
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Bearer ${token}`,
|
|
58
|
+
"anthropic-beta": ANTHROPIC_OAUTH_BETA,
|
|
59
|
+
},
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return outcome("error", describeError(error, dependencies.timeoutMs));
|
|
64
|
+
} finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (response.status === 401 || response.status === 403) {
|
|
69
|
+
await discardBody(response);
|
|
70
|
+
return outcome("expired", null);
|
|
71
|
+
}
|
|
72
|
+
if (response.status === 429) {
|
|
73
|
+
await discardBody(response);
|
|
74
|
+
return outcome("error", "HTTP 429", {
|
|
75
|
+
retryAfterSeconds: parseRetryAfterSeconds(response.headers.get("Retry-After")),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (response.status !== 200) {
|
|
79
|
+
await discardBody(response);
|
|
80
|
+
return outcome("error", `HTTP ${response.status}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let body: unknown;
|
|
84
|
+
try {
|
|
85
|
+
body = await response.json();
|
|
86
|
+
} catch {
|
|
87
|
+
return outcome("error", "Unexpected usage response");
|
|
88
|
+
}
|
|
89
|
+
if (!isRecord(body)) {
|
|
90
|
+
return outcome("error", "Unexpected usage response");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return outcome("ok", null, { windows: usageWindows(body), credits: usageCredits(body.spend) });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Windows from `limits[]` when the endpoint sends it, otherwise the flat buckets. */
|
|
97
|
+
function usageWindows(body: Record<string, unknown>): UsageWindow[] {
|
|
98
|
+
const fromLimits = windowsFromLimits(body.limits);
|
|
99
|
+
const windows = fromLimits.length > 0 ? fromLimits : windowsFromFlat(body);
|
|
100
|
+
return windows.sort((left, right) => {
|
|
101
|
+
const order = (WINDOW_ORDER[left.key] ?? 2) - (WINDOW_ORDER[right.key] ?? 2);
|
|
102
|
+
return order !== 0 ? order : left.label.localeCompare(right.label);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function windowsFromLimits(raw: unknown): UsageWindow[] {
|
|
107
|
+
if (!Array.isArray(raw)) return [];
|
|
108
|
+
const windows: UsageWindow[] = [];
|
|
109
|
+
for (const entry of raw) {
|
|
110
|
+
if (!isRecord(entry)) continue;
|
|
111
|
+
const percent = asNumber(entry.percent);
|
|
112
|
+
if (percent === null) continue;
|
|
113
|
+
const common: Pick<UsageWindow, "usedPct" | "resetsAt" | "severity" | "active"> = {
|
|
114
|
+
usedPct: percent,
|
|
115
|
+
resetsAt: asIsoString(entry.resets_at),
|
|
116
|
+
};
|
|
117
|
+
if (typeof entry.severity === "string") common.severity = entry.severity;
|
|
118
|
+
if (entry.is_active === true) common.active = true;
|
|
119
|
+
if (entry.kind === "session") {
|
|
120
|
+
windows.push({ key: "session", label: "5h", short: "5h", ...common });
|
|
121
|
+
} else if (entry.kind === "weekly_all") {
|
|
122
|
+
windows.push({ key: "weekly", label: "Weekly", short: "wk", ...common });
|
|
123
|
+
} else if (entry.kind === "weekly_scoped") {
|
|
124
|
+
const name = scopeName(entry.scope);
|
|
125
|
+
if (name === null) continue;
|
|
126
|
+
windows.push({ key: `weekly_scoped.${slug(name)}`, label: name, scope: name, ...common });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return windows;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function windowsFromFlat(body: Record<string, unknown>): UsageWindow[] {
|
|
133
|
+
const windows: UsageWindow[] = [];
|
|
134
|
+
const fiveHour = bucketWindow(body.five_hour, "session", "5h", "5h");
|
|
135
|
+
if (fiveHour !== null) windows.push(fiveHour);
|
|
136
|
+
const sevenDay = bucketWindow(body.seven_day, "weekly", "Weekly", "wk");
|
|
137
|
+
if (sevenDay !== null) windows.push(sevenDay);
|
|
138
|
+
return windows;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function bucketWindow(raw: unknown, key: string, label: string, short: string): UsageWindow | null {
|
|
142
|
+
if (!isRecord(raw)) return null;
|
|
143
|
+
const usedPct = asNumber(raw.utilization);
|
|
144
|
+
if (usedPct === null) return null;
|
|
145
|
+
return { key, label, short, usedPct, resetsAt: asIsoString(raw.resets_at) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function scopeName(raw: unknown): string | null {
|
|
149
|
+
if (!isRecord(raw)) return null;
|
|
150
|
+
const model = raw.model;
|
|
151
|
+
if (!isRecord(model)) return null;
|
|
152
|
+
const name = typeof model.display_name === "string" ? model.display_name.trim() : "";
|
|
153
|
+
if (name.length === 0) return null;
|
|
154
|
+
return slug(name) === "all-models" || slug(name) === "all" ? null : name;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Credit balance from the `spend` block, present only for credit-billed plans. */
|
|
158
|
+
function usageCredits(raw: unknown): UsageCredits | null {
|
|
159
|
+
if (!isRecord(raw) || raw.enabled !== true) return null;
|
|
160
|
+
const percent = asNumber(raw.percent);
|
|
161
|
+
const limit = majorAmount(raw.limit);
|
|
162
|
+
if (limit === null) {
|
|
163
|
+
return percent === null ? null : { display: `${percent.toFixed(0)}% used`, usedPct: percent };
|
|
164
|
+
}
|
|
165
|
+
const used = majorAmount(raw.used);
|
|
166
|
+
if (used === null) return null;
|
|
167
|
+
const currency = isRecord(raw.limit) && typeof raw.limit.currency === "string" ? raw.limit.currency : "USD";
|
|
168
|
+
const symbol = currency === "USD" ? "$" : "";
|
|
169
|
+
const usedPct = percent ?? (limit > 0 ? (used / limit) * 100 : null);
|
|
170
|
+
return { display: `${symbol}${used.toFixed(2)} / ${symbol}${limit.toFixed(2)}`, usedPct };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function majorAmount(raw: unknown): number | null {
|
|
174
|
+
if (!isRecord(raw)) return null;
|
|
175
|
+
const minor = asNumber(raw.amount_minor);
|
|
176
|
+
if (minor === null) return null;
|
|
177
|
+
const exponent = asNumber(raw.exponent) ?? 0;
|
|
178
|
+
return minor / 10 ** exponent;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
182
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function asNumber(value: unknown): number | null {
|
|
186
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
187
|
+
if (typeof value === "string") {
|
|
188
|
+
const parsed = Number.parseFloat(value.trim());
|
|
189
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function asIsoString(value: unknown): string | null {
|
|
195
|
+
if (typeof value !== "string" || value.trim().length === 0) return null;
|
|
196
|
+
const parsed = new Date(value);
|
|
197
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function slug(value: string): string {
|
|
201
|
+
return value
|
|
202
|
+
.toLowerCase()
|
|
203
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
204
|
+
.replace(/^-+|-+$/g, "");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function describeError(error: unknown, timeoutMs: number): string {
|
|
208
|
+
if (error instanceof Error) {
|
|
209
|
+
return error.name === "AbortError" ? `timeout after ${timeoutMs}ms` : error.message;
|
|
210
|
+
}
|
|
211
|
+
return String(error);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function discardBody(response: Response): Promise<void> {
|
|
215
|
+
try {
|
|
216
|
+
await response.body?.cancel();
|
|
217
|
+
} catch {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|