@dudousxd/nestjs-agent-dashboard 0.1.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/dist/client/agent-client.d.ts +78 -0
  4. package/dist/client/agent-client.d.ts.map +1 -0
  5. package/dist/client/agent-client.js +47 -0
  6. package/dist/client/agent-client.js.map +1 -0
  7. package/dist/client/budget-usage.d.ts +23 -0
  8. package/dist/client/budget-usage.d.ts.map +1 -0
  9. package/dist/client/budget-usage.js +24 -0
  10. package/dist/client/budget-usage.js.map +1 -0
  11. package/dist/client/default-range.d.ts +13 -0
  12. package/dist/client/default-range.d.ts.map +1 -0
  13. package/dist/client/default-range.js +26 -0
  14. package/dist/client/default-range.js.map +1 -0
  15. package/dist/client/format-usd.d.ts +8 -0
  16. package/dist/client/format-usd.d.ts.map +1 -0
  17. package/dist/client/format-usd.js +34 -0
  18. package/dist/client/format-usd.js.map +1 -0
  19. package/dist/client/merge-live-events.d.ts +17 -0
  20. package/dist/client/merge-live-events.d.ts.map +1 -0
  21. package/dist/client/merge-live-events.js +19 -0
  22. package/dist/client/merge-live-events.js.map +1 -0
  23. package/dist/client/spend-summary.d.ts +37 -0
  24. package/dist/client/spend-summary.d.ts.map +1 -0
  25. package/dist/client/spend-summary.js +57 -0
  26. package/dist/client/spend-summary.js.map +1 -0
  27. package/dist/client/trend-path.d.ts +27 -0
  28. package/dist/client/trend-path.d.ts.map +1 -0
  29. package/dist/client/trend-path.js +25 -0
  30. package/dist/client/trend-path.js.map +1 -0
  31. package/dist/server/index.cjs +449 -0
  32. package/dist/server/index.cjs.map +1 -0
  33. package/dist/server/index.d.cts +129 -0
  34. package/dist/server/index.d.ts +129 -0
  35. package/dist/server/index.js +416 -0
  36. package/dist/server/index.js.map +1 -0
  37. package/dist/spa/assets/index-DAwWrHBV.css +1 -0
  38. package/dist/spa/assets/index-DUf1WOrt.js +49 -0
  39. package/dist/spa/assets/index-DkSjPfKA.js +1 -0
  40. package/dist/spa/assets/preview-DlXVLsfh.js +1 -0
  41. package/dist/spa/index.html +20 -0
  42. package/dist/spa/preview.html +20 -0
  43. package/package.json +65 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Davide Carvalho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @dudousxd/nestjs-agent-dashboard
2
+
3
+ An embedded **AI-gateway governance console** for [`@dudousxd/nestjs-agent`](https://www.npmjs.com/package/@dudousxd/nestjs-agent) — the in-process analog of the Vercel AI Gateway dashboard, with governance already coupled (cost per model/actor, budgets, live tool-call/quota signals).
4
+
5
+ A bundled **React SPA** served by a **NestJS module**, mounted at its own route (default `/ai-gateway`). It reads historical spend/usage from the shared governance read-model and tails live activity off the `aviary:agent:*` diagnostics channel over SSE. Mirrors [`@dudousxd/nestjs-durable-dashboard`](https://www.npmjs.com/package/@dudousxd/nestjs-durable-dashboard).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @dudousxd/nestjs-agent-dashboard
11
+ ```
12
+
13
+ Peer deps: `@dudousxd/nestjs-agent-core`, `@nestjs/common`, `@nestjs/core`, `rxjs`.
14
+
15
+ ## Mount it
16
+
17
+ Import `AgentDashboardModule.forRoot(...)` alongside your `@dudousxd/nestjs-agent` module (global), which must provide the `AGENT_GOVERNANCE_QUERIES` read-model (bound by a store adapter — `store-mikro-orm`, `store-drizzle`, or the in-memory testing adapter).
18
+
19
+ ```ts
20
+ import { Module } from '@nestjs/common';
21
+ import { AgentModule } from '@dudousxd/nestjs-agent';
22
+ import { AgentDashboardModule } from '@dudousxd/nestjs-agent-dashboard';
23
+
24
+ @Module({
25
+ imports: [
26
+ AgentModule.forRoot({ /* ... */ }), // provides AGENT_GOVERNANCE_QUERIES (global)
27
+ AgentDashboardModule.forRoot({
28
+ basePath: '/ai-gateway', // UI route (default)
29
+ apiBasePath: '/api/ai-gateway', // JSON + SSE API (default: `<basePath>/api`)
30
+ }),
31
+ ],
32
+ })
33
+ export class AppModule {}
34
+ ```
35
+
36
+ The controllers are **path-relative and guard-frontable** — front `basePath`/`apiBasePath` with your own auth guard/middleware.
37
+
38
+ ### `forRoot` options
39
+
40
+ ```ts
41
+ AgentDashboardModule.forRoot({
42
+ basePath?: string; // where the SPA is served. Default '/ai-gateway'
43
+ apiBasePath?: string; // where the JSON/SSE API is mounted. Default '<basePath>/api'
44
+ });
45
+ ```
46
+
47
+ ## JSON + SSE API
48
+
49
+ Mounted at `apiBasePath`:
50
+
51
+ | Method | Path | Response |
52
+ | ------ | ---- | -------- |
53
+ | `GET` | `/spend?from=YYYY-MM-DD&to=YYYY-MM-DD` | `{ byModel: ModelSpendRow[], byActor: ActorSpendRow[], trend: UsageTrendPoint[] }` (defaults to the last 30 days) |
54
+ | `GET` | `/tool-calls?limit=` | `ToolCallActivityRow[]` (default 50, max 200) |
55
+ | `GET` | `/threads?limit=` | `ThreadActivityRow[]` (default 50, max 200) |
56
+ | `GET` | `/stream` | SSE of live `aviary:agent:*` events (`{ event, ts, payload }`) |
57
+
58
+ ## SPA sections
59
+
60
+ - **Spend & usage** — headline $ + tokens, by-model donut + legend, daily trend (cost/tokens toggle).
61
+ - **Models** — per-model requests / in+out tokens / cost / share.
62
+ - **Actors & budgets** — spend per acting ref; usage-vs-budget bar when a daily limit is known.
63
+ - **Runs & tools** — recent tool calls (with a denied/forbidden banner) and recent threads.
64
+ - **Live** — the diagnostics SSE feed, newest-first, with quota/denied events flagged.
65
+
66
+ ## Typed client
67
+
68
+ For your own front-end, `@dudousxd/nestjs-agent-dashboard/client` exports `agentClient` (`spend`, `toolCalls`, `threads`, `streamEvents`) with the response types — dependency-free.
69
+
70
+ ```ts
71
+ import { agentClient } from '@dudousxd/nestjs-agent-dashboard/client';
72
+
73
+ const overview = await agentClient.spend({ fromDay: '2026-06-01', toDay: '2026-06-30' });
74
+ ```
75
+
76
+ ## Build
77
+
78
+ `vite build && tsup && tsc -p tsconfig.client.json` — the SPA compiles to `dist/spa`, the NestJS server to `dist/server` (dual ESM + CJS with decorator metadata + `import.meta.url` shim), and the client types to `dist/client`. The bundled SPA ships in the package, so the UI controller serves it with no extra assets.
79
+
80
+ `preview.html` renders every section against mock data (no backend) for visual verification.
81
+
82
+ ## License
83
+
84
+ MIT © Davide Carvalho
@@ -0,0 +1,78 @@
1
+ /** Inclusive UTC day range, each `YYYY-MM-DD`. */
2
+ export interface GovernanceRange {
3
+ fromDay: string;
4
+ toDay: string;
5
+ }
6
+ /** Spend + token totals for one model over a range. */
7
+ export interface ModelSpendRow {
8
+ modelId: string;
9
+ requests: number;
10
+ inputTokens: number;
11
+ outputTokens: number;
12
+ costUsd: number;
13
+ }
14
+ /** Spend + token totals for one acting ref (user/tenant) over a range. */
15
+ export interface ActorSpendRow {
16
+ actorRef: string;
17
+ requests: number;
18
+ totalTokens: number;
19
+ costUsd: number;
20
+ }
21
+ /** One point on the daily usage/cost trend. */
22
+ export interface UsageTrendPoint {
23
+ day: string;
24
+ totalTokens: number;
25
+ costUsd: number;
26
+ }
27
+ /** A recent tool-call for the activity feed. */
28
+ export interface ToolCallActivityRow {
29
+ toolCallId: string;
30
+ toolName: string;
31
+ toolType: string;
32
+ status: string;
33
+ threadId: string;
34
+ createdAt: string;
35
+ }
36
+ /** A recent thread with rolled-up activity. */
37
+ export interface ThreadActivityRow {
38
+ threadId: string;
39
+ title: string;
40
+ actorRef: string;
41
+ messageCount: number;
42
+ totalTokens: number;
43
+ lastActivityAt: string;
44
+ }
45
+ /** The `GET <api>/spend` response. */
46
+ export interface SpendOverview {
47
+ byModel: ModelSpendRow[];
48
+ byActor: ActorSpendRow[];
49
+ trend: UsageTrendPoint[];
50
+ }
51
+ /** One live agent event forwarded over SSE. */
52
+ export interface LiveAgentEvent {
53
+ event: string;
54
+ ts: number;
55
+ payload: Record<string, unknown>;
56
+ }
57
+ declare global {
58
+ interface Window {
59
+ /** UI mount base (e.g. `/ai-gateway`) injected by the UI controller; falls back to `/ai-gateway`. */
60
+ __AGENT_BASE__?: string;
61
+ /** JSON API base (e.g. `/ai-gateway/api`) injected by the UI controller; falls back to `<base>/api`. */
62
+ __AGENT_API__?: string;
63
+ }
64
+ }
65
+ export declare const agentClient: {
66
+ /** Spend/usage overview for a day range: `{ byModel, byActor, trend }`. */
67
+ spend(range: GovernanceRange): Promise<SpendOverview>;
68
+ /** Most recent tool calls (default 50). */
69
+ toolCalls(limit?: number): Promise<ToolCallActivityRow[]>;
70
+ /** Most recent threads (default 50). */
71
+ threads(limit?: number): Promise<ThreadActivityRow[]>;
72
+ /**
73
+ * Live-tail `aviary:agent:*` events over SSE. Calls `onEvent` per event; returns a function that
74
+ * closes the stream.
75
+ */
76
+ streamEvents(onEvent: (event: LiveAgentEvent) => void): () => void;
77
+ };
78
+ //# sourceMappingURL=agent-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-client.d.ts","sourceRoot":"","sources":["../../src/client/agent-client.ts"],"names":[],"mappings":"AAIA,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,uDAAuD;AACvD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,+CAA+C;AAC/C,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,gDAAgD;AAChD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,+CAA+C;AAC/C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,sCAAsC;AACtC,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,KAAK,EAAE,eAAe,EAAE,CAAC;CAC1B;AAED,+CAA+C;AAC/C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,qGAAqG;QACrG,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,wGAAwG;QACxG,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB;CACF;AAcD,eAAO,MAAM,WAAW;IACtB,2EAA2E;iBAC9D,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC;IAIrD,2CAA2C;+BACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAGrD,wCAAwC;6BACnB,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAGjD;;;OAGG;0BACmB,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI;CAWnE,CAAC"}
@@ -0,0 +1,47 @@
1
+ // Typed API client for the AI-gateway console. Self-contained: the row shapes are re-declared here
2
+ // (identical to core's `AgentGovernanceQueries` contract) so `@dudousxd/nestjs-agent-dashboard/client`
3
+ // is dependency-free for an external front-end that only wants to call the API.
4
+ function apiBase() {
5
+ if (typeof window !== 'undefined' && window.__AGENT_API__)
6
+ return window.__AGENT_API__;
7
+ const base = (typeof window !== 'undefined' && window.__AGENT_BASE__) || '/ai-gateway';
8
+ return `${base}/api`;
9
+ }
10
+ async function http(path, init) {
11
+ const res = await fetch(apiBase() + path, init);
12
+ if (!res.ok)
13
+ throw new Error(`${res.status} ${res.statusText}`);
14
+ return (await res.json());
15
+ }
16
+ export const agentClient = {
17
+ /** Spend/usage overview for a day range: `{ byModel, byActor, trend }`. */
18
+ spend(range) {
19
+ const q = new URLSearchParams({ from: range.fromDay, to: range.toDay });
20
+ return http(`/spend?${q.toString()}`);
21
+ },
22
+ /** Most recent tool calls (default 50). */
23
+ toolCalls(limit = 50) {
24
+ return http(`/tool-calls?limit=${limit}`);
25
+ },
26
+ /** Most recent threads (default 50). */
27
+ threads(limit = 50) {
28
+ return http(`/threads?limit=${limit}`);
29
+ },
30
+ /**
31
+ * Live-tail `aviary:agent:*` events over SSE. Calls `onEvent` per event; returns a function that
32
+ * closes the stream.
33
+ */
34
+ streamEvents(onEvent) {
35
+ const source = new EventSource(`${apiBase()}/stream`);
36
+ source.onmessage = (msg) => {
37
+ try {
38
+ onEvent(JSON.parse(msg.data));
39
+ }
40
+ catch {
41
+ /* ignore malformed event */
42
+ }
43
+ };
44
+ return () => source.close();
45
+ },
46
+ };
47
+ //# sourceMappingURL=agent-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-client.js","sourceRoot":"","sources":["../../src/client/agent-client.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,uGAAuG;AACvG,gFAAgF;AA2EhF,SAAS,OAAO;IACd,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,aAAa;QAAE,OAAO,MAAM,CAAC,aAAa,CAAC;IACvF,MAAM,IAAI,GAAG,CAAC,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,cAAc,CAAC,IAAI,aAAa,CAAC;IACvF,OAAO,GAAG,IAAI,MAAM,CAAC;AACvB,CAAC;AAED,KAAK,UAAU,IAAI,CAAI,IAAY,EAAE,IAAkB;IACrD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,OAAO,EAAE,GAAG,IAAI,EAAE,IAAI,CAAC,CAAC;IAChD,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;IAChE,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAM,CAAC;AACjC,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,2EAA2E;IAC3E,KAAK,CAAC,KAAsB;QAC1B,MAAM,CAAC,GAAG,IAAI,eAAe,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;QACxE,OAAO,IAAI,CAAgB,UAAU,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,2CAA2C;IAC3C,SAAS,CAAC,KAAK,GAAG,EAAE;QAClB,OAAO,IAAI,CAAwB,qBAAqB,KAAK,EAAE,CAAC,CAAC;IACnE,CAAC;IACD,wCAAwC;IACxC,OAAO,CAAC,KAAK,GAAG,EAAE;QAChB,OAAO,IAAI,CAAsB,kBAAkB,KAAK,EAAE,CAAC,CAAC;IAC9D,CAAC;IACD;;;OAGG;IACH,YAAY,CAAC,OAAwC;QACnD,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,GAAG,OAAO,EAAE,SAAS,CAAC,CAAC;QACtD,MAAM,CAAC,SAAS,GAAG,CAAC,GAAG,EAAE,EAAE;YACzB,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAmB,CAAC,CAAC;YAClD,CAAC;YAAC,MAAM,CAAC;gBACP,4BAA4B;YAC9B,CAAC;QACH,CAAC,CAAC;QACF,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAC9B,CAAC;CACF,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { ActorSpendRow } from './agent-client';
2
+ /** A configured per-actor daily token budget, keyed by `actorRef`. Empty when no budgets are known. */
3
+ export type BudgetMap = Record<string, number>;
4
+ /** An actor's spend joined with its budget (when one is configured). */
5
+ export interface ActorBudget {
6
+ actorRef: string;
7
+ requests: number;
8
+ totalTokens: number;
9
+ costUsd: number;
10
+ /** Configured daily token limit, when known. */
11
+ limitTokens?: number;
12
+ /** `totalTokens / limitTokens` (0..N; can exceed 1). Present only when a limit is known. */
13
+ usageRatio?: number;
14
+ /** True when a known limit is met or exceeded. */
15
+ overBudget: boolean;
16
+ }
17
+ /**
18
+ * Join by-actor spend with the configured daily budgets. An actor with no configured limit keeps its
19
+ * spend but carries no `limitTokens`/`usageRatio` (the UI hides the bar — "usage vs limit when
20
+ * known"). Sorted by cost descending. Pure.
21
+ */
22
+ export declare function joinBudgets(rows: ActorSpendRow[], budgets?: BudgetMap): ActorBudget[];
23
+ //# sourceMappingURL=budget-usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget-usage.d.ts","sourceRoot":"","sources":["../../src/client/budget-usage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,uGAAuG;AACvG,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE/C,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,OAAO,GAAE,SAAc,GAAG,WAAW,EAAE,CAiBzF"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Join by-actor spend with the configured daily budgets. An actor with no configured limit keeps its
3
+ * spend but carries no `limitTokens`/`usageRatio` (the UI hides the bar — "usage vs limit when
4
+ * known"). Sorted by cost descending. Pure.
5
+ */
6
+ export function joinBudgets(rows, budgets = {}) {
7
+ return rows
8
+ .map((row) => {
9
+ const limit = budgets[row.actorRef];
10
+ const hasLimit = typeof limit === 'number' && limit > 0;
11
+ const usageRatio = hasLimit ? row.totalTokens / limit : undefined;
12
+ return {
13
+ actorRef: row.actorRef,
14
+ requests: row.requests,
15
+ totalTokens: row.totalTokens,
16
+ costUsd: row.costUsd,
17
+ overBudget: usageRatio !== undefined && usageRatio >= 1,
18
+ ...(hasLimit ? { limitTokens: limit } : {}),
19
+ ...(usageRatio !== undefined ? { usageRatio } : {}),
20
+ };
21
+ })
22
+ .sort((a, b) => b.costUsd - a.costUsd || b.totalTokens - a.totalTokens);
23
+ }
24
+ //# sourceMappingURL=budget-usage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget-usage.js","sourceRoot":"","sources":["../../src/client/budget-usage.ts"],"names":[],"mappings":"AAmBA;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,IAAqB,EAAE,UAAqB,EAAE;IACxE,OAAO,IAAI;SACR,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACX,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,QAAQ,GAAG,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,GAAG,CAAC,CAAC;QACxD,MAAM,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;QAClE,OAAO;YACL,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,WAAW,EAAE,GAAG,CAAC,WAAW;YAC5B,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,UAAU,EAAE,UAAU,KAAK,SAAS,IAAI,UAAU,IAAI,CAAC;YACvD,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3C,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpD,CAAC;IACJ,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC;AAC5E,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { GovernanceRange } from './agent-client';
2
+ /** A `YYYY-MM-DD` UTC day string `daysAgo` days before `now` (0 = the `now` day). */
3
+ export declare function utcDay(daysAgo: number, now?: number): string;
4
+ /** The default range the console opens on: an inclusive `spanDays`-day window ending today (UTC). */
5
+ export declare function defaultRange(spanDays?: number, now?: number): GovernanceRange;
6
+ /** True for a well-formed `YYYY-MM-DD` day string. */
7
+ export declare function isIsoDay(value: string): boolean;
8
+ /**
9
+ * Number of inclusive days a range spans (`from` and `to` both counted). Returns 1 for a same-day
10
+ * range and clamps a reversed range to 1. Pure — assumes valid `YYYY-MM-DD` inputs.
11
+ */
12
+ export declare function rangeDays(range: GovernanceRange): number;
13
+ //# sourceMappingURL=default-range.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-range.d.ts","sourceRoot":"","sources":["../../src/client/default-range.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAKtD,qFAAqF;AACrF,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CAExE;AAED,qGAAqG;AACrG,wBAAgB,YAAY,CAAC,QAAQ,SAAK,EAAE,GAAG,GAAE,MAAmB,GAAG,eAAe,CAErF;AAED,sDAAsD;AACtD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAKxD"}
@@ -0,0 +1,26 @@
1
+ const DAY_MS = 86_400_000;
2
+ const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
3
+ /** A `YYYY-MM-DD` UTC day string `daysAgo` days before `now` (0 = the `now` day). */
4
+ export function utcDay(daysAgo, now = Date.now()) {
5
+ return new Date(now - daysAgo * DAY_MS).toISOString().slice(0, 10);
6
+ }
7
+ /** The default range the console opens on: an inclusive `spanDays`-day window ending today (UTC). */
8
+ export function defaultRange(spanDays = 30, now = Date.now()) {
9
+ return { fromDay: utcDay(spanDays - 1, now), toDay: utcDay(0, now) };
10
+ }
11
+ /** True for a well-formed `YYYY-MM-DD` day string. */
12
+ export function isIsoDay(value) {
13
+ return ISO_DAY.test(value);
14
+ }
15
+ /**
16
+ * Number of inclusive days a range spans (`from` and `to` both counted). Returns 1 for a same-day
17
+ * range and clamps a reversed range to 1. Pure — assumes valid `YYYY-MM-DD` inputs.
18
+ */
19
+ export function rangeDays(range) {
20
+ const from = Date.parse(`${range.fromDay}T00:00:00Z`);
21
+ const to = Date.parse(`${range.toDay}T00:00:00Z`);
22
+ if (!Number.isFinite(from) || !Number.isFinite(to))
23
+ return 1;
24
+ return Math.max(1, Math.round((to - from) / DAY_MS) + 1);
25
+ }
26
+ //# sourceMappingURL=default-range.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-range.js","sourceRoot":"","sources":["../../src/client/default-range.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,UAAU,CAAC;AAC1B,MAAM,OAAO,GAAG,qBAAqB,CAAC;AAEtC,qFAAqF;AACrF,MAAM,UAAU,MAAM,CAAC,OAAe,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IAC9D,OAAO,IAAI,IAAI,CAAC,GAAG,GAAG,OAAO,GAAG,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACrE,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,YAAY,CAAC,QAAQ,GAAG,EAAE,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IAClE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AACvE,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,KAAsB;IAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;IACtD,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,YAAY,CAAC,CAAC;IAClD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,OAAO,CAAC,CAAC;IAC7D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,8 @@
1
+ /** Compact-number + currency formatting for the console. Pure; unit-tested. */
2
+ /** Format a USD amount. Small amounts keep more precision; large ones read as `$1.2k` / `$3.4M`. */
3
+ export declare function formatUsd(amount: number): string;
4
+ /** Format a token/count with compact suffixes (`1.2k`, `3.4M`, `1.1B`). */
5
+ export declare function formatCount(value: number): string;
6
+ /** Format a 0..1 ratio as an integer percent (`0.1234` -> `12%`). */
7
+ export declare function formatPercent(ratio: number): string;
8
+ //# sourceMappingURL=format-usd.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-usd.d.ts","sourceRoot":"","sources":["../../src/client/format-usd.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,oGAAoG;AACpG,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGnD"}
@@ -0,0 +1,34 @@
1
+ /** Compact-number + currency formatting for the console. Pure; unit-tested. */
2
+ /** Format a USD amount. Small amounts keep more precision; large ones read as `$1.2k` / `$3.4M`. */
3
+ export function formatUsd(amount) {
4
+ if (!Number.isFinite(amount))
5
+ return '$0.00';
6
+ const abs = Math.abs(amount);
7
+ if (abs >= 1_000_000)
8
+ return `$${(amount / 1_000_000).toFixed(2)}M`;
9
+ if (abs >= 1_000)
10
+ return `$${(amount / 1_000).toFixed(2)}k`;
11
+ if (abs > 0 && abs < 0.01)
12
+ return '<$0.01';
13
+ return `$${amount.toFixed(2)}`;
14
+ }
15
+ /** Format a token/count with compact suffixes (`1.2k`, `3.4M`, `1.1B`). */
16
+ export function formatCount(value) {
17
+ if (!Number.isFinite(value))
18
+ return '0';
19
+ const abs = Math.abs(value);
20
+ if (abs >= 1_000_000_000)
21
+ return `${(value / 1_000_000_000).toFixed(1)}B`;
22
+ if (abs >= 1_000_000)
23
+ return `${(value / 1_000_000).toFixed(1)}M`;
24
+ if (abs >= 1_000)
25
+ return `${(value / 1_000).toFixed(1)}k`;
26
+ return `${Math.round(value)}`;
27
+ }
28
+ /** Format a 0..1 ratio as an integer percent (`0.1234` -> `12%`). */
29
+ export function formatPercent(ratio) {
30
+ if (!Number.isFinite(ratio))
31
+ return '0%';
32
+ return `${Math.round(ratio * 100)}%`;
33
+ }
34
+ //# sourceMappingURL=format-usd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-usd.js","sourceRoot":"","sources":["../../src/client/format-usd.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,oGAAoG;AACpG,MAAM,UAAU,SAAS,CAAC,MAAc;IACtC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,OAAO,CAAC;IAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC7B,IAAI,GAAG,IAAI,SAAS;QAAE,OAAO,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IACpE,IAAI,GAAG,IAAI,KAAK;QAAE,OAAO,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAC5D,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,IAAI;QAAE,OAAO,QAAQ,CAAC;IAC3C,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;AACjC,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,GAAG,IAAI,aAAa;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,aAAa,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAC1E,IAAI,GAAG,IAAI,SAAS;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAClE,IAAI,GAAG,IAAI,KAAK;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAC1D,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;AAChC,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC;AACvC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { LiveAgentEvent } from './agent-client';
2
+ /** A live event with a stable client-assigned id, so React can key the feed without server ids. */
3
+ export interface FeedEvent extends LiveAgentEvent {
4
+ /** Monotonic client id (`ts`-prefixed) — stable across re-renders, unique within a session. */
5
+ id: string;
6
+ }
7
+ /** Default number of live events the feed retains (a bounded ring so a long session can't grow unbounded). */
8
+ export declare const DEFAULT_FEED_CAPACITY = 200;
9
+ /**
10
+ * Prepend a freshly-received live event onto the feed (newest first) and cap the list at `capacity`.
11
+ * `seq` disambiguates two events sharing the same `ts` so the generated `id` stays unique. Pure —
12
+ * returns a new array, never mutates `prev`.
13
+ */
14
+ export declare function pushLiveEvent(prev: FeedEvent[], event: LiveAgentEvent, seq: number, capacity?: number): FeedEvent[];
15
+ /** True when a live event denotes a governance breach the Live feed should surface loudly. */
16
+ export declare function isAlertEvent(event: LiveAgentEvent): boolean;
17
+ //# sourceMappingURL=merge-live-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-live-events.d.ts","sourceRoot":"","sources":["../../src/client/merge-live-events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,mGAAmG;AACnG,MAAM,WAAW,SAAU,SAAQ,cAAc;IAC/C,+FAA+F;IAC/F,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,8GAA8G;AAC9G,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,EAAE,EACjB,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAA8B,GACvC,SAAS,EAAE,CAGb;AAED,8FAA8F;AAC9F,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAI3D"}
@@ -0,0 +1,19 @@
1
+ /** Default number of live events the feed retains (a bounded ring so a long session can't grow unbounded). */
2
+ export const DEFAULT_FEED_CAPACITY = 200;
3
+ /**
4
+ * Prepend a freshly-received live event onto the feed (newest first) and cap the list at `capacity`.
5
+ * `seq` disambiguates two events sharing the same `ts` so the generated `id` stays unique. Pure —
6
+ * returns a new array, never mutates `prev`.
7
+ */
8
+ export function pushLiveEvent(prev, event, seq, capacity = DEFAULT_FEED_CAPACITY) {
9
+ const entry = { ...event, id: `${event.ts}-${seq}` };
10
+ return [entry, ...prev].slice(0, capacity);
11
+ }
12
+ /** True when a live event denotes a governance breach the Live feed should surface loudly. */
13
+ export function isAlertEvent(event) {
14
+ if (event.event === 'quota.exceeded')
15
+ return true;
16
+ const status = event.payload.status;
17
+ return typeof status === 'string' && /forbidden|denied|error|failed/i.test(status);
18
+ }
19
+ //# sourceMappingURL=merge-live-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-live-events.js","sourceRoot":"","sources":["../../src/client/merge-live-events.ts"],"names":[],"mappings":"AAQA,8GAA8G;AAC9G,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAEzC;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAiB,EACjB,KAAqB,EACrB,GAAW,EACX,WAAmB,qBAAqB;IAExC,MAAM,KAAK,GAAc,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;IAChE,OAAO,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAC7C,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,YAAY,CAAC,KAAqB;IAChD,IAAI,KAAK,CAAC,KAAK,KAAK,gBAAgB;QAAE,OAAO,IAAI,CAAC;IAClD,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;IACpC,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,gCAAgC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACrF,CAAC"}
@@ -0,0 +1,37 @@
1
+ import type { ModelSpendRow } from './agent-client';
2
+ /** Headline totals for a range, summed across every model row. */
3
+ export interface SpendTotals {
4
+ costUsd: number;
5
+ inputTokens: number;
6
+ outputTokens: number;
7
+ totalTokens: number;
8
+ requests: number;
9
+ }
10
+ /** A model row enriched with its share (0..1) of total cost — for share bars / donut segments. */
11
+ export interface ModelSpendShare extends ModelSpendRow {
12
+ totalTokens: number;
13
+ /** Fraction of total cost (0..1). 0 when there is no cost anywhere (falls back to token share). */
14
+ costShare: number;
15
+ }
16
+ /** One donut arc segment: a normalized 0..1 slice with its cumulative offset (also 0..1). */
17
+ export interface DonutSegment {
18
+ modelId: string;
19
+ value: number;
20
+ fraction: number;
21
+ offset: number;
22
+ }
23
+ /** Sum the by-model rows into the headline totals. */
24
+ export declare function summarizeSpend(rows: ModelSpendRow[]): SpendTotals;
25
+ /**
26
+ * Enrich each model row with its cost share, sorted by cost descending. When nothing has a cost
27
+ * (every model unpriced) the share falls back to token share so the bars/donut still convey usage
28
+ * mix instead of collapsing to zero.
29
+ */
30
+ export declare function withShares(rows: ModelSpendRow[]): ModelSpendShare[];
31
+ /**
32
+ * Build donut segments from the shared rows: each fraction is the row's `costShare`, and `offset` is
33
+ * the running cumulative fraction (so an SVG can lay arcs end-to-end). Zero-share rows are dropped so
34
+ * the donut has no invisible slices.
35
+ */
36
+ export declare function donutSegments(rows: ModelSpendShare[]): DonutSegment[];
37
+ //# sourceMappingURL=spend-summary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spend-summary.d.ts","sourceRoot":"","sources":["../../src/client/spend-summary.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,kGAAkG;AAClG,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,mGAAmG;IACnG,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,6FAA6F;AAC7F,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,WAAW,CAgBjE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,eAAe,EAAE,CAYnE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,YAAY,EAAE,CAcrE"}
@@ -0,0 +1,57 @@
1
+ /** Sum the by-model rows into the headline totals. */
2
+ export function summarizeSpend(rows) {
3
+ const totals = {
4
+ costUsd: 0,
5
+ inputTokens: 0,
6
+ outputTokens: 0,
7
+ totalTokens: 0,
8
+ requests: 0,
9
+ };
10
+ for (const row of rows) {
11
+ totals.costUsd += row.costUsd;
12
+ totals.inputTokens += row.inputTokens;
13
+ totals.outputTokens += row.outputTokens;
14
+ totals.requests += row.requests;
15
+ }
16
+ totals.totalTokens = totals.inputTokens + totals.outputTokens;
17
+ return totals;
18
+ }
19
+ /**
20
+ * Enrich each model row with its cost share, sorted by cost descending. When nothing has a cost
21
+ * (every model unpriced) the share falls back to token share so the bars/donut still convey usage
22
+ * mix instead of collapsing to zero.
23
+ */
24
+ export function withShares(rows) {
25
+ const totalCost = rows.reduce((sum, row) => sum + row.costUsd, 0);
26
+ const totalTokens = rows.reduce((sum, row) => sum + row.inputTokens + row.outputTokens, 0);
27
+ const denomCost = totalCost > 0 ? totalCost : 0;
28
+ return rows
29
+ .map((row) => {
30
+ const rowTokens = row.inputTokens + row.outputTokens;
31
+ const costShare = denomCost > 0 ? row.costUsd / denomCost : totalTokens > 0 ? rowTokens / totalTokens : 0;
32
+ return { ...row, totalTokens: rowTokens, costShare };
33
+ })
34
+ .sort((a, b) => b.costUsd - a.costUsd || b.totalTokens - a.totalTokens);
35
+ }
36
+ /**
37
+ * Build donut segments from the shared rows: each fraction is the row's `costShare`, and `offset` is
38
+ * the running cumulative fraction (so an SVG can lay arcs end-to-end). Zero-share rows are dropped so
39
+ * the donut has no invisible slices.
40
+ */
41
+ export function donutSegments(rows) {
42
+ const segments = [];
43
+ let offset = 0;
44
+ for (const row of rows) {
45
+ if (row.costShare <= 0)
46
+ continue;
47
+ segments.push({
48
+ modelId: row.modelId,
49
+ value: row.costUsd,
50
+ fraction: row.costShare,
51
+ offset,
52
+ });
53
+ offset += row.costShare;
54
+ }
55
+ return segments;
56
+ }
57
+ //# sourceMappingURL=spend-summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spend-summary.js","sourceRoot":"","sources":["../../src/client/spend-summary.ts"],"names":[],"mappings":"AA0BA,sDAAsD;AACtD,MAAM,UAAU,cAAc,CAAC,IAAqB;IAClD,MAAM,MAAM,GAAgB;QAC1B,OAAO,EAAE,CAAC;QACV,WAAW,EAAE,CAAC;QACd,YAAY,EAAE,CAAC;QACf,WAAW,EAAE,CAAC;QACd,QAAQ,EAAE,CAAC;KACZ,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,CAAC,OAAO,IAAI,GAAG,CAAC,OAAO,CAAC;QAC9B,MAAM,CAAC,WAAW,IAAI,GAAG,CAAC,WAAW,CAAC;QACtC,MAAM,CAAC,YAAY,IAAI,GAAG,CAAC,YAAY,CAAC;QACxC,MAAM,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC;IAClC,CAAC;IACD,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,YAAY,CAAC;IAC9D,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAqB;IAC9C,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAClE,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,WAAW,GAAG,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC;IAC3F,MAAM,SAAS,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,OAAO,IAAI;SACR,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACX,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,GAAG,GAAG,CAAC,YAAY,CAAC;QACrD,MAAM,SAAS,GACb,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1F,OAAO,EAAE,GAAG,GAAG,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;IACvD,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW,CAAC,CAAC;AAC5E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAuB;IACnD,MAAM,QAAQ,GAAmB,EAAE,CAAC;IACpC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,GAAG,CAAC,SAAS,IAAI,CAAC;YAAE,SAAS;QACjC,QAAQ,CAAC,IAAI,CAAC;YACZ,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,KAAK,EAAE,GAAG,CAAC,OAAO;YAClB,QAAQ,EAAE,GAAG,CAAC,SAAS;YACvB,MAAM;SACP,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,SAAS,CAAC;IAC1B,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { UsageTrendPoint } from './agent-client';
2
+ /** Which series a trend chart plots. */
3
+ export type TrendMetric = 'costUsd' | 'totalTokens';
4
+ /** A plotted point in SVG user space. */
5
+ export interface TrendVertex {
6
+ day: string;
7
+ value: number;
8
+ x: number;
9
+ y: number;
10
+ }
11
+ /** Everything an SVG needs to draw a normalized area/line trend inside `width`x`height`. */
12
+ export interface TrendGeometry {
13
+ /** `M..L..` polyline through every vertex (empty string when there is nothing to plot). */
14
+ line: string;
15
+ /** Closed area path (line + baseline) for a filled gradient; empty string when nothing to plot. */
16
+ area: string;
17
+ vertices: TrendVertex[];
18
+ /** The series max used to normalize the y-axis (never 0 — floored to 1 so a flat/empty series draws). */
19
+ max: number;
20
+ }
21
+ /**
22
+ * Project a daily trend series into SVG geometry. The x-axis spreads points evenly across `width`;
23
+ * the y-axis is normalized against the series max (inverted, since SVG y grows downward). A single
24
+ * point renders centered; an empty series yields empty paths and `max: 1`. Pure — no DOM.
25
+ */
26
+ export declare function buildTrendGeometry(points: UsageTrendPoint[], metric: TrendMetric, width: number, height: number): TrendGeometry;
27
+ //# sourceMappingURL=trend-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trend-path.d.ts","sourceRoot":"","sources":["../../src/client/trend-path.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,wCAAwC;AACxC,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,aAAa,CAAC;AAEpD,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC5B,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb,mGAAmG;IACnG,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,yGAAyG;IACzG,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,eAAe,EAAE,EACzB,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,aAAa,CAsBf"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Project a daily trend series into SVG geometry. The x-axis spreads points evenly across `width`;
3
+ * the y-axis is normalized against the series max (inverted, since SVG y grows downward). A single
4
+ * point renders centered; an empty series yields empty paths and `max: 1`. Pure — no DOM.
5
+ */
6
+ export function buildTrendGeometry(points, metric, width, height) {
7
+ if (points.length === 0)
8
+ return { line: '', area: '', vertices: [], max: 1 };
9
+ const max = Math.max(1, ...points.map((point) => point[metric]));
10
+ const lastIndex = Math.max(1, points.length - 1);
11
+ const vertices = points.map((point, index) => {
12
+ const value = point[metric];
13
+ const x = points.length === 1 ? width / 2 : (index / lastIndex) * width;
14
+ const y = height - (value / max) * height;
15
+ return { day: point.day, value, x, y };
16
+ });
17
+ const line = vertices
18
+ .map((vertex, index) => `${index === 0 ? 'M' : 'L'}${vertex.x.toFixed(2)},${vertex.y.toFixed(2)}`)
19
+ .join(' ');
20
+ const firstX = vertices[0]?.x ?? 0;
21
+ const lastX = vertices[vertices.length - 1]?.x ?? width;
22
+ const area = `${line} L${lastX.toFixed(2)},${height} L${firstX.toFixed(2)},${height} Z`;
23
+ return { line, area, vertices, max };
24
+ }
25
+ //# sourceMappingURL=trend-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trend-path.js","sourceRoot":"","sources":["../../src/client/trend-path.ts"],"names":[],"mappings":"AAwBA;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAAyB,EACzB,MAAmB,EACnB,KAAa,EACb,MAAc;IAEd,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;IAE7E,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACjE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACjD,MAAM,QAAQ,GAAkB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QAC1D,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5B,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,SAAS,CAAC,GAAG,KAAK,CAAC;QACxE,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,MAAM,CAAC;QAC1C,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,QAAQ;SAClB,GAAG,CACF,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAC7F;SACA,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC;IACxD,MAAM,IAAI,GAAG,GAAG,IAAI,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,MAAM,KAAK,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,MAAM,IAAI,CAAC;IAExF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;AACvC,CAAC"}