@nexus-aethra/dshell-usage 0.1.6

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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/lib/client/heatmap.js +96 -0
  3. package/lib/client/heatmap.js.map +1 -0
  4. package/lib/client/index.js +36 -0
  5. package/lib/client/index.js.map +1 -0
  6. package/lib/client/locales.js +95 -0
  7. package/lib/client/locales.js.map +1 -0
  8. package/lib/client/nav-icon.js +88 -0
  9. package/lib/client/nav-icon.js.map +1 -0
  10. package/lib/client/page.js +289 -0
  11. package/lib/client/page.js.map +1 -0
  12. package/lib/client.js +1146 -0
  13. package/lib/client.js.map +1 -0
  14. package/lib/index-store.js +250 -0
  15. package/lib/index-store.js.map +1 -0
  16. package/lib/index.js +41 -0
  17. package/lib/index.js.map +1 -0
  18. package/lib/protocol.js +9 -0
  19. package/lib/protocol.js.map +1 -0
  20. package/lib/route.js +63 -0
  21. package/lib/route.js.map +1 -0
  22. package/lib/scan.js +187 -0
  23. package/lib/scan.js.map +1 -0
  24. package/lib/types/client/heatmap.d.ts +69 -0
  25. package/lib/types/client/heatmap.d.ts.map +1 -0
  26. package/lib/types/client/index.d.ts +14 -0
  27. package/lib/types/client/index.d.ts.map +1 -0
  28. package/lib/types/client/locales.d.ts +62 -0
  29. package/lib/types/client/locales.d.ts.map +1 -0
  30. package/lib/types/client/nav-icon.d.ts +28 -0
  31. package/lib/types/client/nav-icon.d.ts.map +1 -0
  32. package/lib/types/client/page.d.ts +18 -0
  33. package/lib/types/client/page.d.ts.map +1 -0
  34. package/lib/types/index-store.d.ts +87 -0
  35. package/lib/types/index-store.d.ts.map +1 -0
  36. package/lib/types/index.d.ts +19 -0
  37. package/lib/types/index.d.ts.map +1 -0
  38. package/lib/types/protocol.d.ts +115 -0
  39. package/lib/types/protocol.d.ts.map +1 -0
  40. package/lib/types/route.d.ts +19 -0
  41. package/lib/types/route.d.ts.map +1 -0
  42. package/lib/types/scan.d.ts +94 -0
  43. package/lib/types/scan.d.ts.map +1 -0
  44. package/package.json +73 -0
package/lib/scan.js ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * The scan: walk every session's durable log, count the token usage each model
3
+ * turn reported, and add it to the index.
4
+ *
5
+ * Where the numbers come from. dsh attaches provider-reported accounting to the
6
+ * `assistant/message` event itself — `data.usage` — and the same event's
7
+ * `data.message.source` names the route that produced it. Usage and route
8
+ * therefore travel together on one event and nothing has to be paired up or
9
+ * inferred, which is what makes a scan over history as reliable as one over a
10
+ * live session.
11
+ *
12
+ * Incremental by session. The index keeps the highest seq counted for each
13
+ * session, and a session whose log has not grown is skipped after a cheap
14
+ * metadata listing rather than a full read. A session that has grown is read
15
+ * whole and filtered down to the events past its cursor, so a rescan costs one
16
+ * decode per touched session and nothing for the rest.
17
+ *
18
+ * Writes are batched, never per event and never per session. A scan accumulates
19
+ * its rows in memory and hands them to the store once, and {@link UsageScanner.schedule}
20
+ * coalesces concurrent callers behind a single flight plus a minimum interval,
21
+ * so a page opened repeatedly still produces at most one transaction per
22
+ * interval rather than one per open.
23
+ */
24
+ import { localDay } from './index-store.js';
25
+ /** Defaults: a page can be reopened freely without a second disk write. */
26
+ export const DEFAULT_SCAN_POLICY = { minIntervalMs: 30_000, debounceMs: 400 };
27
+ /** The route and usage one `assistant/message` carries, or undefined. */
28
+ export function usageOf(event) {
29
+ const data = event.data;
30
+ const usage = data?.usage;
31
+ if (usage === undefined)
32
+ return undefined;
33
+ const source = data?.message?.source;
34
+ // A turn with no route is not attributable, and an unattributed turn would
35
+ // silently land in a bucket named after nothing.
36
+ if (typeof source?.provider !== 'string' || typeof source.model !== 'string')
37
+ return undefined;
38
+ const time = event.time;
39
+ return {
40
+ day: localDay(new Date(typeof time === 'number' ? time : Date.now())),
41
+ provider: source.provider,
42
+ model: source.model,
43
+ uncachedInputTokens: usage.inputTokens ?? 0,
44
+ outputTokens: usage.outputTokens ?? 0,
45
+ cacheReadTokens: usage.cacheReadTokens ?? 0,
46
+ cacheWriteTokens: usage.cacheWriteTokens ?? 0,
47
+ turns: 1,
48
+ };
49
+ }
50
+ /** Fold counted turns into one row per (day, route). */
51
+ export function foldTurns(turns) {
52
+ const folded = new Map();
53
+ for (const turn of turns) {
54
+ const key = `${turn.day}\u0000${turn.provider}\u0000${turn.model}`;
55
+ const running = folded.get(key);
56
+ if (running === undefined) {
57
+ folded.set(key, {
58
+ day: turn.day,
59
+ provider: turn.provider,
60
+ model: turn.model,
61
+ uncachedInputTokens: turn.uncachedInputTokens,
62
+ outputTokens: turn.outputTokens,
63
+ cacheReadTokens: turn.cacheReadTokens,
64
+ cacheWriteTokens: turn.cacheWriteTokens,
65
+ turns: turn.turns,
66
+ });
67
+ continue;
68
+ }
69
+ running.uncachedInputTokens += turn.uncachedInputTokens;
70
+ running.outputTokens += turn.outputTokens;
71
+ running.cacheReadTokens += turn.cacheReadTokens;
72
+ running.cacheWriteTokens += turn.cacheWriteTokens;
73
+ running.turns += turn.turns;
74
+ }
75
+ return [...folded.values()];
76
+ }
77
+ /** Serialises scans and holds the write-behind window. */
78
+ export class UsageScanner {
79
+ store;
80
+ query;
81
+ policy;
82
+ #inFlight;
83
+ #lastFinishedAt = 0;
84
+ #timer;
85
+ constructor(store, query, policy = DEFAULT_SCAN_POLICY) {
86
+ this.store = store;
87
+ this.query = query;
88
+ this.policy = policy;
89
+ }
90
+ /**
91
+ * Ask for a scan, without waiting for one that is already running.
92
+ *
93
+ * A caller inside the minimum interval is answered with what the last scan
94
+ * did, so the page never blocks on a second pass it does not need.
95
+ */
96
+ async schedule() {
97
+ if (this.#inFlight !== undefined)
98
+ return await this.#inFlight;
99
+ const since = Date.now() - this.#lastFinishedAt;
100
+ if (since < this.policy.minIntervalMs) {
101
+ return { sessions: 0, read: 0, turns: 0, skipped: 0, skipReason: undefined, written: false };
102
+ }
103
+ if (this.policy.debounceMs > 0) {
104
+ await new Promise((settle) => {
105
+ if (this.#timer !== undefined)
106
+ clearTimeout(this.#timer);
107
+ this.#timer = setTimeout(() => { this.#timer = undefined; settle(); }, this.policy.debounceMs);
108
+ });
109
+ }
110
+ if (this.#inFlight !== undefined)
111
+ return await this.#inFlight;
112
+ this.#inFlight = this.#run().finally(() => {
113
+ this.#inFlight = undefined;
114
+ this.#lastFinishedAt = Date.now();
115
+ });
116
+ return await this.#inFlight;
117
+ }
118
+ /** Scan now, ignoring pacing. The page's explicit rebuild action. */
119
+ async rescan() {
120
+ return await this.#run();
121
+ }
122
+ /** Stop a waiting trigger; the plugin's teardown path. */
123
+ dispose() {
124
+ if (this.#timer !== undefined)
125
+ clearTimeout(this.#timer);
126
+ this.#timer = undefined;
127
+ }
128
+ async #run() {
129
+ const cursors = this.store.cursors();
130
+ const sessions = await this.query.listSessions();
131
+ const counted = [];
132
+ const advanced = [];
133
+ let read = 0;
134
+ let skipped = 0;
135
+ let skipReason;
136
+ for (const session of sessions) {
137
+ const id = String(session.header.id);
138
+ const cursor = cursors.get(id);
139
+ // One unreadable log must not cost the reader every other session's
140
+ // usage. A session whose event list or log cannot be decoded is counted
141
+ // as skipped and left WITHOUT advancing its cursor, so a later scan
142
+ // visits it again — the log is not ours to repair, and a refusal is
143
+ // usually a fact about that one artifact (a log dsh cannot migrate, a
144
+ // half-written file) rather than about the index. The first reason is
145
+ // carried out so the page can say why the totals are partial.
146
+ let maxSeq;
147
+ let full;
148
+ try {
149
+ const events = await this.query.listEvents(id);
150
+ maxSeq = events.reduce((highest, event) => Math.max(highest, event.seq), 0);
151
+ if (cursor !== undefined && maxSeq <= cursor)
152
+ continue;
153
+ // A log that shrank means the persisted session was replaced rather than
154
+ // appended to; its cursor no longer describes it, so it is read afresh
155
+ // and its earlier contribution is left where it is — an index of what was
156
+ // spent cannot be un-spent by a rewrite.
157
+ full = await this.query.readSession(id);
158
+ }
159
+ catch (error) {
160
+ skipped += 1;
161
+ skipReason ??= error instanceof Error ? error.message : String(error);
162
+ continue;
163
+ }
164
+ read += 1;
165
+ for (const event of full.events) {
166
+ if (cursor !== undefined && event.seq <= cursor)
167
+ continue;
168
+ const turn = usageOf(event);
169
+ if (turn !== undefined)
170
+ counted.push(turn);
171
+ }
172
+ advanced.push({ sessionId: id, seq: maxSeq });
173
+ }
174
+ const rows = foldTurns(counted);
175
+ const scannedAt = localDay(new Date());
176
+ // The one write: this scan's rows, its cursors, and the freshness stamp in
177
+ // a single transaction. An empty scan still advances nothing.
178
+ if (rows.length > 0 || advanced.length > 0) {
179
+ this.store.applyBatch({ rows, cursors: advanced, scannedAt });
180
+ }
181
+ return {
182
+ sessions: sessions.length, read, turns: counted.length, skipped, skipReason,
183
+ written: rows.length > 0 || advanced.length > 0,
184
+ };
185
+ }
186
+ }
187
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The heatmap's layout: a fixed calendar rectangle of day cells.
3
+ *
4
+ * The grid is filled the way a page is read — top to bottom, then the next
5
+ * column — so the newest day always sits in the BOTTOM-RIGHT corner. That is
6
+ * the shape a reader expects from a "last 26 weeks" block, and it is why the
7
+ * cells are not weekday-aligned: aligning columns to weeks would leave the most
8
+ * recent day wherever its weekday happened to fall, which reads as a hole in the
9
+ * corner rather than as "this is now".
10
+ *
11
+ * Pure and separate from the page for the same reason the fold is separate from
12
+ * the store: the interesting part is date arithmetic (which day a cell holds,
13
+ * which cells the history actually covers, how a token count becomes a shade),
14
+ * and that is testable without a browser while a chart is not.
15
+ *
16
+ * Days are the host's own `YYYY-MM-DD` local days, and the anchor is the host's
17
+ * today — the index bucketed by LOCAL day when it scanned, so the grid reads
18
+ * them as local dates too, or a bucket would slide across a cell boundary near
19
+ * midnight.
20
+ */
21
+ /** How many shades a non-empty day can take, above "nothing recorded". */
22
+ export declare const HEAT_LEVELS = 4;
23
+ /** The shape of the grid: columns of a week, however many weeks fit. */
24
+ export declare const HEAT_ROWS = 7;
25
+ /** One day's sum, as the host returns it. */
26
+ export interface HeatDay {
27
+ /** `YYYY-MM-DD`, the host's local day. */
28
+ readonly day: string;
29
+ /** Every bucket summed, the same measure the charts use. */
30
+ readonly total: number;
31
+ readonly turns: number;
32
+ }
33
+ /** One day in the grid, with the shade it is drawn in. */
34
+ export interface HeatCell extends HeatDay {
35
+ /** 0 for a day with nothing recorded, else 1…{@link HEAT_LEVELS}. */
36
+ readonly level: number;
37
+ }
38
+ /**
39
+ * One column of the grid, top to bottom.
40
+ *
41
+ * A cell is `undefined` when the day is out of reach: before the history, or
42
+ * after the anchor (a grid ending today has no future in it).
43
+ */
44
+ export type HeatColumn = readonly (HeatCell | undefined)[];
45
+ /**
46
+ * Lay the recorded days out in a full rectangle of `weeks` columns.
47
+ *
48
+ * The newest cell is the anchor: the last column's bottom cell holds the anchor
49
+ * day, and every earlier cell steps back one day, so the grid reads oldest at
50
+ * the top-left and newest at the bottom-right. The rectangle is always full —
51
+ * cells the history does not reach are present but empty — because a grid that
52
+ * grew and shrank with the data would move the corner as the history changed.
53
+ *
54
+ * @param days - one entry per recorded day, any order.
55
+ * @param weeks - how many columns to draw.
56
+ * @param anchor - the host's today, `YYYY-MM-DD`; the bottom-right cell.
57
+ * @returns the columns, left to right.
58
+ */
59
+ export declare function heatmapColumns(days: readonly HeatDay[], weeks: number, anchor: string): readonly HeatColumn[];
60
+ /**
61
+ * Whether the history reaches further back than the grid draws.
62
+ *
63
+ * @param days - the same days handed to {@link heatmapColumns}.
64
+ * @param weeks - the same width.
65
+ * @param anchor - the same anchor.
66
+ * @returns whether any recorded day fell off the left edge.
67
+ */
68
+ export declare function heatmapTruncated(days: readonly HeatDay[], weeks: number, anchor: string): boolean;
69
+ //# sourceMappingURL=heatmap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heatmap.d.ts","sourceRoot":"","sources":["../../../src/client/heatmap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,WAAW,IAAI,CAAA;AAE5B,wEAAwE;AACxE,eAAO,MAAM,SAAS,IAAI,CAAA;AAE1B,6CAA6C;AAC7C,MAAM,WAAW,OAAO;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,0DAA0D;AAC1D,MAAM,WAAW,QAAS,SAAQ,OAAO;IACvC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAA;AAa1D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,UAAU,EAAE,CA4B7G;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAMjG"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The usage page's client face: the locale dictionaries and the Settings
3
+ * navigation row that mounts the page.
4
+ *
5
+ * `settings.section` is the seat dsh's own Plugins page uses, so this page is a
6
+ * peer of it rather than a tab inside it — the charts want the full settings
7
+ * column, and a reader looking for "what did this cost" looks in the
8
+ * navigation, not under a plugin list.
9
+ */
10
+ import { type Context } from '@deepseek-ai/cordis';
11
+ export declare const name = "@nexus-aethra/dshell-usage/client";
12
+ export declare const inject: readonly ["slots", "locale"];
13
+ export declare function apply(ctx: Context): void;
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAelD,eAAO,MAAM,IAAI,sCAAsC,CAAA;AAEvD,eAAO,MAAM,MAAM,8BAA+B,CAAA;AAElD,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAkBxC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `dshellUsage` namespace dictionaries, and the namespace's declaration.
3
+ *
4
+ * The namespace merge lives with its key set so that any module naming
5
+ * `TranslateNS<'dshellUsage'>` or `PropsLocale<'dshellUsage'>` needs only this
6
+ * file. Keys are lowercase and dotted: `nav.*` (the settings navigation row),
7
+ * `range.*` (the window selector), `chart.*` (the two charts' captions),
8
+ * `table.*` (the route breakdown), and `state.*` (empty, busy, freshness, and
9
+ * the load failure).
10
+ */
11
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
12
+ interface LocaleNamespaceMap {
13
+ /** The usage settings page: navigation label, charts, table, and states. */
14
+ dshellUsage: DshellUsageKey;
15
+ }
16
+ }
17
+ /** Simplified Chinese dictionary and key-set source of truth. */
18
+ export declare const zh: {
19
+ readonly nav: "用量";
20
+ readonly title: "模型 token 消耗";
21
+ readonly subtitle: "按模型汇总每个会话上报的用量。索引建在 dshell 自己的数据目录里,首次打开会扫一遍全部历史会话,之后只补新增的部分。";
22
+ readonly 'range.7': "近 7 天";
23
+ readonly 'range.30': "近 30 天";
24
+ readonly 'range.all': "全部";
25
+ readonly 'range.label': "时间范围";
26
+ readonly rebuild: "重新聚合";
27
+ readonly 'rebuild.busy': "正在聚合…";
28
+ readonly 'chart.heatmap': "每日热力(近 26 周内)";
29
+ readonly 'chart.heatmapDay': "{day} · {total} tok · {turns} 次调用";
30
+ readonly 'chart.heatmapLess': "少";
31
+ readonly 'chart.heatmapMore': "多";
32
+ readonly 'chart.heatmapPeak': "最忙一天 {total} tok";
33
+ readonly 'chart.heatmapTruncated': "只画最近 26 周";
34
+ readonly 'chart.curve': "每日消耗(按模型)";
35
+ readonly 'chart.pie': "模型占比";
36
+ readonly 'summary.total': "合计";
37
+ readonly 'summary.uncached': "未缓存输入";
38
+ readonly 'summary.output': "输出";
39
+ readonly 'summary.cacheRead': "缓存读";
40
+ readonly 'summary.turns': "调用";
41
+ readonly 'summary.hitRate': "缓存命中率";
42
+ readonly 'table.title': "按模型明细";
43
+ readonly 'table.model': "模型";
44
+ readonly 'table.input': "输入";
45
+ readonly 'table.output': "输出";
46
+ readonly 'table.cacheRead': "缓存读";
47
+ readonly 'table.cacheWrite': "缓存写";
48
+ readonly 'table.turns': "调用";
49
+ readonly 'table.total': "合计";
50
+ readonly 'state.empty.title': "还没有可统计的用量";
51
+ readonly 'state.empty.body': "索引里没有任何一条带用量上报的模型调用。跑一次 agent 会话后再点「重新聚合」。";
52
+ readonly 'state.freshness': "索引更新于 {day}";
53
+ readonly 'state.skipped': "有 {count} 个会话的日志读不了,未计入(详情见悬停提示)。";
54
+ readonly 'state.scanned': "本次扫描列出 {sessions} 个会话,读取了 {read} 个,命中 {turns} 次调用。";
55
+ readonly 'state.error': "读取用量失败:{reason}";
56
+ readonly 'state.loading': "正在读取索引…";
57
+ };
58
+ /** The key set, taken from the dictionary that defines it. */
59
+ export type DshellUsageKey = keyof typeof zh;
60
+ /** English dictionary, keyed by the same set. */
61
+ export declare const en: Record<DshellUsageKey, string>;
62
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,4EAA4E;QAC5E,WAAW,EAAE,cAAc,CAAA;KAC5B;CACF;AAED,iEAAiE;AACjE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgDL,CAAA;AAEV,8DAA8D;AAC9D,MAAM,MAAM,cAAc,GAAG,MAAM,OAAO,EAAE,CAAA;AAE5C,iDAAiD;AACjD,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CA8C7C,CAAA"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The usage entry's own icon in the settings navigation.
3
+ *
4
+ * dsh's settings shell picks a nav icon from a map of the section ids IT ships
5
+ * (`account`, `models`, `plugins`, …) and draws a generic gear for anything
6
+ * else, and rc.2's `settings.section` seat carries no icon of its own — so a
7
+ * contributed section cannot ask for one and ours read as "some settings".
8
+ *
9
+ * Hence the one DOM patch in this package: find our own row by the label we
10
+ * registered, and put a gauge where the gear was. It is the same technique
11
+ * dshell's sidebar section uses (and the artwork is copied verbatim from
12
+ * `@deepseek-ai/dsh-client-ui-primitives`, medium weight, 16 viewBox, 1.3px
13
+ * stroke) because a surface mounted into a node dsh owns cannot render React
14
+ * components into it.
15
+ *
16
+ * The row is re-created whenever the dialog opens, so a MutationObserver
17
+ * watches for it; the patch is idempotent (it re-checks the label and a marker
18
+ * attribute) and only touches the svg node React will not write to again.
19
+ * Delete this file the day `settings.section` accepts an icon.
20
+ */
21
+ /**
22
+ * Keep the settings navigation's usage row drawn with the gauge.
23
+ *
24
+ * @param label - the section label as the shell renders it, in the live locale.
25
+ * @returns the disposer that stops watching.
26
+ */
27
+ export declare function watchUsageNavIcon(label: () => string): () => void;
28
+ //# sourceMappingURL=nav-icon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav-icon.d.ts","sourceRoot":"","sources":["../../../src/client/nav-icon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAqCH;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,MAAM,GAAG,MAAM,IAAI,CA0BjE"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The usage page: one settings entry that charts what every model has cost.
3
+ *
4
+ * Everything on it comes from one host endpoint. `summary` reads the index,
5
+ * `scan` extends it first — the host owns both the session events and the
6
+ * index, so the browser asks for a rebuild rather than performing one.
7
+ *
8
+ * The two charts answer different questions and neither is derivable from the
9
+ * other: the stacked area shows how the total moved over time and which route
10
+ * moved it, and the pie shows what share each route holds across the whole
11
+ * window. Both are hand-rolled SVG — the client bundle inlines its imports, and
12
+ * a charting library would be the largest thing in it by an order of magnitude.
13
+ */
14
+ import { type ReactElement } from 'react';
15
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
16
+ /** The page body. */
17
+ export declare function UsageSection({ t }: PropsLocale<'dshellUsage'>): ReactElement;
18
+ //# sourceMappingURL=page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../../src/client/page.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAqD,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAE5F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAyEnE,qBAAqB;AACrB,wBAAgB,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,WAAW,CAAC,aAAa,CAAC,GAAG,YAAY,CA4Z5E"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The usage index: one SQLite database holding per-day, per-route token
3
+ * buckets, plus the per-session cursor that makes a rescan incremental.
4
+ *
5
+ * Node's built-in `node:sqlite` is the engine, the same choice
6
+ * `@nexus-aethra/dshell-storage` makes for command history and for the same
7
+ * reasons: no native dependency and no install script.
8
+ *
9
+ * Why a table and not the JSON document this could have been: the page asks
10
+ * aggregate questions ("this route over these days", "every day for this
11
+ * route") and a document would have to be parsed whole to answer any of them.
12
+ * The buckets are stored UNSHIFTED, one row per (day, route), and the row is
13
+ * added to on rescan rather than rewritten, which is what lets a rescan touch
14
+ * only the sessions that grew.
15
+ *
16
+ * Writes are deliberately coarse. A scan accumulates its rows in memory and
17
+ * calls {@link UsageStore.applyBatch} once, inside a single transaction: the
18
+ * host must not write per model turn, per event, or per session — see
19
+ * `scan.ts` for the policy that decides when a batch is worth flushing.
20
+ *
21
+ * Format — `PRAGMA user_version = 1`.
22
+ *
23
+ * usage_bucket(day, provider, model, uncached_input, output, cache_read,
24
+ * cache_write, turns)
25
+ * PRIMARY KEY (day, provider, model)
26
+ * `day` is `YYYY-MM-DD` in the HOST's local time, decided at scan time so
27
+ * a query never has to guess a zone. Every deployment here has the
28
+ * browser and the host on one machine, and the response states the days
29
+ * it covers, so a reader can tell what the buckets mean.
30
+ * usage_cursor(session_id, seq)
31
+ * PRIMARY KEY (session_id)
32
+ * the highest event seq already counted for that session; absent means
33
+ * never scanned.
34
+ * usage_meta(key, value)
35
+ * `scannedAt`, the local day the index was last extended.
36
+ */
37
+ import type { UsageBuckets, UsageKey, UsageSummary } from './protocol.js';
38
+ /** A bucket to add, and the session cursor it arrived with. */
39
+ export interface UsageBatch {
40
+ readonly rows: readonly (UsageKey & UsageBuckets)[];
41
+ readonly cursors: readonly {
42
+ readonly sessionId: string;
43
+ readonly seq: number;
44
+ }[];
45
+ /** Local day the batch was taken; becomes the index's freshness stamp. */
46
+ readonly scannedAt: string;
47
+ }
48
+ /** One open index. */
49
+ export declare class UsageStore {
50
+ #private;
51
+ private readonly path;
52
+ constructor(path: string);
53
+ /** The highest seq already counted for one session, or undefined. */
54
+ cursorFor(sessionId: string): number | undefined;
55
+ /** Every cursor, so a scan can decide what to read without a query per session. */
56
+ cursors(): Map<string, number>;
57
+ /**
58
+ * Add one scan's rows and advance its cursors, in one transaction.
59
+ *
60
+ * Additive on purpose: a rescan hands over only what it newly counted, so the
61
+ * statement can never overwrite a bucket it did not read.
62
+ */
63
+ applyBatch(batch: UsageBatch): void;
64
+ /**
65
+ * The aggregate the page renders, over one window.
66
+ *
67
+ * @param days - Window length in days, or null for everything on record.
68
+ * @returns Day rows for the curve, route totals for the pie and table, and
69
+ * the two axis lists both need so the page never derives its own.
70
+ */
71
+ summary(days: number | null): UsageSummary;
72
+ close(): void;
73
+ }
74
+ /** Every bucket summed — the pie's measure and the table's sort key. */
75
+ export declare function total(buckets: UsageBuckets): number;
76
+ /** How a route is named in the UI and in the response's `models` list. */
77
+ export declare function routeName(route: {
78
+ provider: string;
79
+ model: string;
80
+ }): string;
81
+ /** `YYYY-MM-DD` in the host's local time. */
82
+ export declare function localDay(at: Date): string;
83
+ /** Open (or reuse) the index at one path. */
84
+ export declare function openUsageStore(path: string): UsageStore;
85
+ /** Close and forget one path's index; the plugin's teardown path. */
86
+ export declare function closeUsageStore(path: string): void;
87
+ //# sourceMappingURL=index-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-store.d.ts","sourceRoot":"","sources":["../../src/index-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAe,QAAQ,EAAE,YAAY,EAAiB,MAAM,eAAe,CAAA;AAIrG,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,GAAG,YAAY,CAAC,EAAE,CAAA;IACnD,QAAQ,CAAC,OAAO,EAAE,SAAS;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACjF,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAaD,sBAAsB;AACtB,qBAAa,UAAU;;IAGT,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,MAAM;IAyCzC,qEAAqE;IACrE,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAKhD,mFAAmF;IACnF,OAAO,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAK9B;;;;;OAKG;IACH,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAsCnC;;;;;;OAMG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,YAAY;IA6D1C,KAAK,IAAI,IAAI;CAGd;AAED,wEAAwE;AACxE,wBAAgB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAEnD;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAE5E;AAED,6CAA6C;AAC7C,wBAAgB,QAAQ,CAAC,EAAE,EAAE,IAAI,GAAG,MAAM,CAIzC;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAQvD;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAIlD"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * dshell usage: the durable per-model token index and the settings page that
3
+ * charts it.
4
+ *
5
+ * The host half is small on purpose. It owns one SQLite index under dshell's
6
+ * data root, one scanner over `ctx.sessionQuery`, and one route; everything the
7
+ * reader sees is folded from the same three. The page lives in `./client`.
8
+ *
9
+ * Nothing here subscribes to turns. A scan is requested — by the page on open,
10
+ * or by the reader's rebuild action — and the scanner coalesces repeated
11
+ * requests behind a minimum interval, so a host with a busy agent still writes
12
+ * to disk at most once per window instead of once per model turn.
13
+ */
14
+ import type { Context } from '@deepseek-ai/cordis';
15
+ export type * from './protocol.js';
16
+ /** The one service this package cannot compose without. */
17
+ export declare const inject: readonly ["dshellDataRoot"];
18
+ export declare function apply(ctx: Context): void;
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAMlD,mBAAmB,eAAe,CAAA;AAElC,2DAA2D;AAC3D,eAAO,MAAM,MAAM,6BAAsC,CAAA;AAKzD,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAqBxC"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The usage index's wire and storage shapes.
3
+ *
4
+ * One request type and one response type, because the page both reads the
5
+ * aggregate and asks for a scan through the same endpoint — the scan belongs to
6
+ * the host, which is where the session events already are.
7
+ */
8
+ /** What the browser can ask of the usage endpoint. */
9
+ export interface UsageRequest {
10
+ /** `scan` extends the index first, then answers with the same summary. */
11
+ readonly action: 'summary' | 'scan';
12
+ /** Window length in days, or null for everything on record. */
13
+ readonly days: number | null;
14
+ }
15
+ /**
16
+ * The four buckets a provider reports, kept apart rather than summed.
17
+ *
18
+ * They are not interchangeable: a cache read is what makes a long session
19
+ * affordable and a cache write is what made it expensive, so a single "total"
20
+ * hides the only number a reader can act on.
21
+ */
22
+ export interface UsageBuckets {
23
+ readonly uncachedInputTokens: number;
24
+ readonly outputTokens: number;
25
+ readonly cacheReadTokens: number;
26
+ readonly cacheWriteTokens: number;
27
+ /** Model turns that reported these buckets — one per assistant step. */
28
+ readonly turns: number;
29
+ }
30
+ /** One day's usage for one route, as the curve reads it. */
31
+ export interface UsageDayRow extends UsageBuckets {
32
+ /** `YYYY-MM-DD` in the host's own local time. */
33
+ readonly day: string;
34
+ readonly provider: string;
35
+ readonly model: string;
36
+ }
37
+ /** One route's total across the requested window, as the pie and table read it. */
38
+ export interface UsageTotalRow extends UsageBuckets {
39
+ readonly provider: string;
40
+ readonly model: string;
41
+ }
42
+ /** One day's total, for the calendar. One entry per recorded day, oldest first. */
43
+ export interface UsageHeatDay {
44
+ readonly day: string;
45
+ /** Every bucket summed across every route that day. */
46
+ readonly total: number;
47
+ readonly turns: number;
48
+ }
49
+ /** What the page renders, in one response. */
50
+ export interface UsageSummary {
51
+ /** Whether a scan has ever completed. False renders the empty state. */
52
+ readonly built: boolean;
53
+ /** The window the rows cover, or null for everything on record. */
54
+ readonly days: number | null;
55
+ /** The local day the index was last extended, for the page's freshness line. */
56
+ readonly scannedAt: string | null;
57
+ /**
58
+ * The host's own today, `YYYY-MM-DD`.
59
+ *
60
+ * The calendar anchors on it (its newest cell is this day), and the browser
61
+ * does not compute it: the days in the index are HOST-local, and a reader on
62
+ * another machine or in another timezone would otherwise draw the last day in
63
+ * the wrong corner.
64
+ */
65
+ readonly today: string;
66
+ /** Distinct local days present, oldest first, so the curve has its x-axis. */
67
+ readonly daysList: readonly string[];
68
+ /** Routes present in the window, so the pie and the table agree. */
69
+ readonly models: readonly string[];
70
+ readonly byDay: readonly UsageDayRow[];
71
+ readonly totals: readonly UsageTotalRow[];
72
+ /**
73
+ * Day totals over ALL time, whatever `days` the charts were asked for.
74
+ *
75
+ * The calendar is a rhythm rather than a window: it answers "which days do I
76
+ * burn tokens on", so it keeps its own span while the range chips move the
77
+ * charts, and it does not shrink to two squares when the reader picks 7 days.
78
+ */
79
+ readonly heat: readonly UsageHeatDay[];
80
+ /**
81
+ * What the scan that produced this answer did, present only on a `scan`
82
+ * action. The page reports these numbers, so they come from the scan itself
83
+ * and not from anything the page can count on the summary — the two are
84
+ * different quantities and only one of them is "how many sessions".
85
+ */
86
+ readonly scanned?: UsageScanReport;
87
+ }
88
+ /** One scan's own counts. */
89
+ export interface UsageScanReport {
90
+ /** Sessions the query engine listed, whether or not they were read. */
91
+ readonly sessions: number;
92
+ /** Sessions whose logs were decoded, because they had grown. */
93
+ readonly read: number;
94
+ /** Model turns counted from those logs. */
95
+ readonly turns: number;
96
+ /**
97
+ * Sessions whose log could not be decoded — a refusal by dsh's own migration
98
+ * or a damaged file. Their usage is missing from the totals, so the page says
99
+ * so instead of presenting a partial number as the whole.
100
+ */
101
+ readonly skipped: number;
102
+ /** Why the first skipped session was skipped; absent when nothing was. */
103
+ readonly skipReason?: string;
104
+ }
105
+ /** A summary response, or a refusal the page renders verbatim. */
106
+ export type UsageResponse = UsageSummary | {
107
+ readonly error: string;
108
+ };
109
+ /** The composite key a bucket is stored under. */
110
+ export interface UsageKey {
111
+ readonly day: string;
112
+ readonly provider: string;
113
+ readonly model: string;
114
+ }
115
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAAA;IACnC,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAA;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,4DAA4D;AAC5D,MAAM,WAAW,WAAY,SAAQ,YAAY;IAC/C,iDAAiD;IACjD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,mFAAmF;AACnF,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,mFAAmF;AACnF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,8CAA8C;AAC9C,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IACtC,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAA;IACzC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,YAAY,EAAE,CAAA;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAA;CACnC;AAED,6BAA6B;AAC7B,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED,kEAAkE;AAClE,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAErE,kDAAkD;AAClD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB"}