@shi-yangyang/dsh-token-dashboard 0.0.0-stage → 0.2.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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +166 -0
  3. package/README.md +165 -2
  4. package/THIRD_PARTY_NOTICES.md +25 -0
  5. package/client/client.js +22897 -0
  6. package/cordis.patch.yml +7 -0
  7. package/icon.svg +13 -0
  8. package/lib/host/aggregate.d.ts +61 -0
  9. package/lib/host/aggregate.js +472 -0
  10. package/lib/host/fold.d.ts +103 -0
  11. package/lib/host/fold.js +375 -0
  12. package/lib/host/index.d.ts +24 -0
  13. package/lib/host/index.js +116 -0
  14. package/lib/host/prices.d.ts +42 -0
  15. package/lib/host/prices.js +118 -0
  16. package/lib/host/server.d.ts +30 -0
  17. package/lib/host/server.js +177 -0
  18. package/lib/host/source.d.ts +37 -0
  19. package/lib/host/source.js +179 -0
  20. package/lib/host/types.d.ts +133 -0
  21. package/lib/host/types.js +9 -0
  22. package/lib/host/verify.d.ts +14 -0
  23. package/lib/host/verify.js +79 -0
  24. package/lib/shared/builtin-prices.d.ts +27 -0
  25. package/lib/shared/builtin-prices.generated.d.ts +7 -0
  26. package/lib/shared/builtin-prices.generated.js +1433 -0
  27. package/lib/shared/builtin-prices.js +111 -0
  28. package/lib/shared/dates.d.ts +23 -0
  29. package/lib/shared/dates.js +79 -0
  30. package/lib/shared/export.d.ts +13 -0
  31. package/lib/shared/export.js +113 -0
  32. package/lib/shared/format.d.ts +23 -0
  33. package/lib/shared/format.js +108 -0
  34. package/lib/shared/model.d.ts +37 -0
  35. package/lib/shared/model.js +87 -0
  36. package/lib/shared/pricing.d.ts +70 -0
  37. package/lib/shared/pricing.js +192 -0
  38. package/lib/shared/range.d.ts +34 -0
  39. package/lib/shared/range.js +93 -0
  40. package/lib/shared/types.d.ts +168 -0
  41. package/lib/shared/types.js +10 -0
  42. package/locale/en.json +6 -0
  43. package/locale/zh.json +6 -0
  44. package/package.json +93 -4
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Optional cost estimation. Prices are supplied by the user and stored only in
3
+ * the browser's localStorage; the plugin ships no price table and never infers
4
+ * a rate. All amounts are labelled "Estimated Cost".
5
+ */
6
+ import { builtinPriceFor } from './builtin-prices.js';
7
+ /** Suggested currencies; the editor accepts any short code as well. */
8
+ export const SUGGESTED_CURRENCIES = ['CNY', 'USD', 'EUR', 'JPY'];
9
+ /** Resolve user overrides over online prices over built-in list prices. */
10
+ export function defaultsForModels(models, online) {
11
+ const table = {};
12
+ for (const model of models) {
13
+ const price = online[model.model.trim().toLowerCase()] ?? builtinPriceFor(model.model);
14
+ if (price !== undefined)
15
+ table[model.key] = price;
16
+ }
17
+ return table;
18
+ }
19
+ /** User overrides win per model; the defaults stay for untouched models. */
20
+ export function mergePricing(defaults, overrides) {
21
+ return { ...defaults, ...overrides };
22
+ }
23
+ /** True when two price rows carry identical currency and rates. */
24
+ export function samePrice(left, right) {
25
+ if (left === undefined || right === undefined)
26
+ return left === right;
27
+ return left.currency === right.currency
28
+ && left.input === right.input
29
+ && left.output === right.output
30
+ && left.cacheRead === right.cacheRead
31
+ && left.cacheWrite === right.cacheWrite;
32
+ }
33
+ /** Parse the persisted online table defensively. */
34
+ export function parseOnlinePricing(raw) {
35
+ if (typeof raw !== 'string' || raw.trim() === '')
36
+ return null;
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(raw);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
45
+ return null;
46
+ const record = parsed;
47
+ const source = typeof record.source === 'string' ? record.source.trim().slice(0, 80) : '';
48
+ const fetchedAt = typeof record.fetchedAt === 'number' && Number.isFinite(record.fetchedAt)
49
+ ? record.fetchedAt
50
+ : 0;
51
+ const prices = {};
52
+ if (record.prices !== null && typeof record.prices === 'object' && !Array.isArray(record.prices)) {
53
+ for (const [name, value] of Object.entries(record.prices)) {
54
+ const price = normalizePrice(value);
55
+ if (price !== null)
56
+ prices[name.trim().toLowerCase()] = price;
57
+ }
58
+ }
59
+ return { prices, source, fetchedAt };
60
+ }
61
+ /** Serialize the online table and its provenance. */
62
+ export function serializeOnlinePricing(state) {
63
+ return JSON.stringify(state);
64
+ }
65
+ /** True only when every rate and the currency are present and valid. */
66
+ export function isPriceConfigured(price) {
67
+ if (price === undefined)
68
+ return false;
69
+ if (typeof price.currency !== 'string' || price.currency.trim() === '')
70
+ return false;
71
+ return [price.input, price.output, price.cacheRead, price.cacheWrite].every(validRate);
72
+ }
73
+ /** Estimated cost for one model, or null when no usable price exists. */
74
+ export function costOf(usage, price) {
75
+ if (!isPriceConfigured(price))
76
+ return null;
77
+ const amount = (usage.uncachedInputTokens * price.input
78
+ + usage.cacheReadTokens * price.cacheRead
79
+ + usage.cacheWriteTokens * price.cacheWrite
80
+ + usage.outputTokens * price.output) / 1_000_000;
81
+ return Number.isFinite(amount) ? amount : null;
82
+ }
83
+ /** Build the per-model estimate and currency totals for the current range. */
84
+ export function estimateCost(models, table) {
85
+ const rows = [];
86
+ const totals = new Map();
87
+ let pricedModels = 0;
88
+ let unpricedModels = 0;
89
+ for (const model of models) {
90
+ const price = table[model.key];
91
+ const configured = isPriceConfigured(price);
92
+ const amount = configured ? costOf(model, price) : null;
93
+ if (configured && amount !== null) {
94
+ pricedModels += 1;
95
+ totals.set(price.currency, (totals.get(price.currency) ?? 0) + amount);
96
+ }
97
+ else {
98
+ unpricedModels += 1;
99
+ }
100
+ rows.push({
101
+ key: model.key,
102
+ provider: model.provider,
103
+ model: model.model,
104
+ tokens: {
105
+ uncachedInputTokens: model.uncachedInputTokens,
106
+ cacheReadTokens: model.cacheReadTokens,
107
+ cacheWriteTokens: model.cacheWriteTokens,
108
+ outputTokens: model.outputTokens,
109
+ },
110
+ requests: model.requests,
111
+ priced: configured && amount !== null,
112
+ currency: configured ? price.currency : null,
113
+ amount,
114
+ missing: !configured,
115
+ });
116
+ }
117
+ return {
118
+ rows,
119
+ totals: [...totals.entries()].map(([currency, amount]) => ({ currency, amount })),
120
+ pricedModels,
121
+ unpricedModels,
122
+ };
123
+ }
124
+ /** Parse a persisted pricing table defensively; unknown or invalid rows are dropped. */
125
+ export function parsePricing(raw) {
126
+ if (typeof raw !== 'string' || raw.trim() === '')
127
+ return {};
128
+ let parsed;
129
+ try {
130
+ parsed = JSON.parse(raw);
131
+ }
132
+ catch {
133
+ return {};
134
+ }
135
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
136
+ return {};
137
+ const table = {};
138
+ for (const [key, value] of Object.entries(parsed)) {
139
+ const price = normalizePrice(value);
140
+ if (price !== null)
141
+ table[key] = price;
142
+ }
143
+ return table;
144
+ }
145
+ /** Serialize a table for localStorage. */
146
+ export function serializePricing(table) {
147
+ return JSON.stringify(table);
148
+ }
149
+ /** Normalize one untrusted pricing value. */
150
+ export function normalizePrice(value) {
151
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
152
+ return null;
153
+ const record = value;
154
+ const currency = typeof record.currency === 'string' ? record.currency.trim().toUpperCase() : '';
155
+ if (currency === '' || currency.length > 8)
156
+ return null;
157
+ const input = numberOrNull(record.input);
158
+ const output = numberOrNull(record.output);
159
+ const cacheRead = numberOrNull(record.cacheRead);
160
+ const cacheWrite = numberOrNull(record.cacheWrite);
161
+ if (input === null || output === null || cacheRead === null || cacheWrite === null)
162
+ return null;
163
+ return { currency, input, output, cacheRead, cacheWrite };
164
+ }
165
+ /** Format an amount using its currency, with more precision for tiny values. */
166
+ export function formatAmount(amount, currency) {
167
+ const digits = amount !== 0 && Math.abs(amount) < 0.01 ? 6 : 2;
168
+ try {
169
+ return new Intl.NumberFormat(undefined, {
170
+ style: 'currency',
171
+ currency,
172
+ minimumFractionDigits: digits,
173
+ maximumFractionDigits: digits,
174
+ }).format(amount);
175
+ }
176
+ catch {
177
+ return `${currency} ${amount.toFixed(digits)}`;
178
+ }
179
+ }
180
+ function numberOrNull(value) {
181
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
182
+ return value;
183
+ if (typeof value === 'string' && value.trim() !== '') {
184
+ const parsed = Number(value);
185
+ if (Number.isFinite(parsed) && parsed >= 0)
186
+ return parsed;
187
+ }
188
+ return null;
189
+ }
190
+ function validRate(value) {
191
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
192
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Time-range selection for the dashboard. Range filtering happens on the
3
+ * compact per-day / per-hour buckets the Host already aggregated, so changing
4
+ * the range never triggers a log read.
5
+ */
6
+ import type { ModelUsage, StatsPayload, TokenBuckets, UsageBucket } from './types.js';
7
+ export type RangeId = 'today' | '7d' | '30d' | '90d' | 'all' | 'custom';
8
+ export interface RangeSelection {
9
+ id: RangeId;
10
+ /** Custom range start (`YYYY-MM-DD`); ignored by the preset ranges. */
11
+ from: string;
12
+ /** Custom range end (`YYYY-MM-DD`); ignored by the preset ranges. */
13
+ to: string;
14
+ }
15
+ export interface RangeResult {
16
+ granularity: 'hour' | 'day';
17
+ from: string;
18
+ to: string;
19
+ buckets: UsageBucket[];
20
+ totals: TokenBuckets;
21
+ requests: number;
22
+ models: ModelUsage[];
23
+ cacheHitRate: number;
24
+ totalTokens: number;
25
+ }
26
+ export type RangeSource = Pick<StatsPayload, 'days' | 'hours' | 'today'>;
27
+ /** Preset range lengths in days; 0 means all time and -1 means custom. */
28
+ export declare const RANGE_DAYS: Record<Exclude<RangeId, 'custom' | 'all'>, number>;
29
+ /** Build the chart bucket list and range totals for one selection. */
30
+ export declare function selectRange(source: RangeSource, selection: RangeSelection): RangeResult;
31
+ /** Sum buckets and merge their model rows. */
32
+ export declare function summarize(buckets: UsageBucket[], granularity: 'hour' | 'day', from: string, to: string): RangeResult;
33
+ /** Share of the range total, 0 when the range is empty. */
34
+ export declare function shareOf(value: number, total: number): number;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Time-range selection for the dashboard. Range filtering happens on the
3
+ * compact per-day / per-hour buckets the Host already aggregated, so changing
4
+ * the range never triggers a log read.
5
+ */
6
+ import { addDays, dayHours, enumerateDays, isDayKey, lastDays, spanDays } from './dates.js';
7
+ import { addBuckets, cacheHitRate as hitRateOf, emptyBuckets, sortModels, totalTokens, } from './model.js';
8
+ /** Preset range lengths in days; 0 means all time and -1 means custom. */
9
+ export const RANGE_DAYS = {
10
+ today: 1,
11
+ '7d': 7,
12
+ '30d': 30,
13
+ '90d': 90,
14
+ };
15
+ /** Build the chart bucket list and range totals for one selection. */
16
+ export function selectRange(source, selection) {
17
+ const dayIndex = new Map(source.days.map(bucket => [bucket.bucket, bucket]));
18
+ const hourIndex = new Map(source.hours.map(bucket => [bucket.bucket, bucket]));
19
+ if (selection.id === 'today') {
20
+ const keys = dayHours(source.today);
21
+ const buckets = keys.map(key => hourIndex.get(key) ?? emptyBucket(key));
22
+ return summarize(buckets, 'hour', source.today, source.today);
23
+ }
24
+ let keys;
25
+ let from;
26
+ let to;
27
+ if (selection.id === 'all') {
28
+ const sorted = source.days.map(bucket => bucket.bucket).sort();
29
+ const first = sorted[0];
30
+ const last = sorted[sorted.length - 1];
31
+ if (first === undefined || last === undefined) {
32
+ return summarize([], 'day', source.today, source.today);
33
+ }
34
+ from = first;
35
+ to = last;
36
+ keys = spanDays(from, to) <= 400 ? enumerateDays(from, to) : sorted;
37
+ }
38
+ else if (selection.id === 'custom') {
39
+ const end = isDayKey(selection.to) ? selection.to : source.today;
40
+ const start = isDayKey(selection.from) ? selection.from : addDays(end, -29);
41
+ const [low, high] = start <= end ? [start, end] : [end, start];
42
+ to = high;
43
+ from = spanDays(low, high) > 400 ? addDays(high, -399) : low;
44
+ keys = enumerateDays(from, to, 400);
45
+ }
46
+ else {
47
+ const length = RANGE_DAYS[selection.id];
48
+ keys = lastDays(source.today, length);
49
+ from = keys[0] ?? source.today;
50
+ to = keys[keys.length - 1] ?? source.today;
51
+ }
52
+ const buckets = keys.map(key => dayIndex.get(key) ?? emptyBucket(key));
53
+ return summarize(buckets, 'day', from, to);
54
+ }
55
+ /** Sum buckets and merge their model rows. */
56
+ export function summarize(buckets, granularity, from, to) {
57
+ const totals = emptyBuckets();
58
+ const models = new Map();
59
+ let requests = 0;
60
+ for (const bucket of buckets) {
61
+ addBuckets(totals, bucket);
62
+ requests += bucket.requests;
63
+ for (const usage of bucket.models) {
64
+ const existing = models.get(usage.key);
65
+ if (existing === undefined) {
66
+ // A detached row: bucket model rows are never mutated by range logic.
67
+ models.set(usage.key, { ...usage });
68
+ }
69
+ else {
70
+ addBuckets(existing, usage);
71
+ existing.requests += usage.requests;
72
+ }
73
+ }
74
+ }
75
+ return {
76
+ granularity,
77
+ from,
78
+ to,
79
+ buckets,
80
+ totals,
81
+ requests,
82
+ models: sortModels([...models.values()]),
83
+ cacheHitRate: hitRateOf(totals),
84
+ totalTokens: totalTokens(totals),
85
+ };
86
+ }
87
+ /** Share of the range total, 0 when the range is empty. */
88
+ export function shareOf(value, total) {
89
+ return total <= 0 ? 0 : value / total;
90
+ }
91
+ function emptyBucket(bucket) {
92
+ return { bucket, requests: 0, models: [], ...emptyBuckets() };
93
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Wire contract shared by the Host aggregator and the browser dashboard.
3
+ *
4
+ * These values describe token accounting only. They never include prompt text,
5
+ * assistant text, tool arguments, file contents, credentials, or raw events.
6
+ */
7
+ /** The only wire contract this plugin currently serves. */
8
+ export declare const API_VERSION = 1;
9
+ /** Same-origin route prefix registered on the Harness web server. */
10
+ export declare const API_PREFIX = "/dsh-token-dashboard";
11
+ /** The four disjoint buckets reported by providers and folded by token-meter. */
12
+ export interface TokenBuckets {
13
+ /** Provider `inputTokens`: prompt tokens that were neither read from nor written to cache. */
14
+ uncachedInputTokens: number;
15
+ /** Provider `cacheReadTokens`: prompt tokens served from the provider cache. */
16
+ cacheReadTokens: number;
17
+ /** Provider `cacheWriteTokens`: prompt tokens written to the provider cache. */
18
+ cacheWriteTokens: number;
19
+ /** Provider `outputTokens`: generated tokens, reasoning included as the provider reports it. */
20
+ outputTokens: number;
21
+ }
22
+ /** One model (provider + model identity) over some window. */
23
+ export interface ModelUsage extends TokenBuckets {
24
+ /** Stable key derived from provider and model id; `unknown` when unidentifiable. */
25
+ key: string;
26
+ provider: string;
27
+ model: string;
28
+ /** Number of billed attempts attributed to this model. */
29
+ requests: number;
30
+ }
31
+ /** One time bucket (a local calendar day or a local hour). */
32
+ export interface UsageBucket extends TokenBuckets {
33
+ /** `YYYY-MM-DD` for days, `YYYY-MM-DDTHH` for hours. */
34
+ bucket: string;
35
+ requests: number;
36
+ models: ModelUsage[];
37
+ }
38
+ /** One session row in the dashboard table. */
39
+ export interface SessionSummary {
40
+ id: string;
41
+ /** Latest log-backed `session/title`, or null when the log has none. */
42
+ title: string | null;
43
+ /** Human-readable project label: the workspace title when known, else the cwd basename. */
44
+ project: string;
45
+ /** Absolute working directory, or null when the session has none. */
46
+ cwd: string | null;
47
+ /** Model keys that contributed usage in this session. */
48
+ modelKeys: string[];
49
+ createdAt: number;
50
+ lastActivity: number;
51
+ live: boolean;
52
+ origin: 'root' | 'subagent';
53
+ /** True when this session's log inherited a prefix from another session. */
54
+ seeded: boolean;
55
+ parentSession: string | null;
56
+ agentPreset: string | null;
57
+ totals: TokenBuckets;
58
+ requests: number;
59
+ cacheHitRate: number;
60
+ models: ModelUsage[];
61
+ eventCount: number;
62
+ inheritedEventCount: number;
63
+ /** A newer log revision could not be read; the row shows the last good fold. */
64
+ stale: boolean;
65
+ }
66
+ /** One billed request row for the request log. */
67
+ export interface RequestRecord {
68
+ /** Stable row key for React and paging. */
69
+ id: string;
70
+ sessionId: string;
71
+ sessionTitle: string | null;
72
+ project: string;
73
+ /** Agent preset label, or a generic harness label when absent. */
74
+ app: string;
75
+ time: number;
76
+ provider: string;
77
+ model: string;
78
+ /** Pricing key (`provider::model`). */
79
+ modelKey: string;
80
+ inputTokens: number;
81
+ cacheReadTokens: number;
82
+ cacheWriteTokens: number;
83
+ outputTokens: number;
84
+ speed: number | null;
85
+ firstToken: number | null;
86
+ }
87
+ /** One page of the request log. */
88
+ export interface RequestLogPage {
89
+ revision: number;
90
+ total: number;
91
+ offset: number;
92
+ limit: number;
93
+ generatedAt: number;
94
+ rows: RequestRecord[];
95
+ }
96
+ /** One failed session read. Only an id and a stable reason code are exposed. */
97
+ export interface SessionReadError {
98
+ sessionId: string;
99
+ reason: string;
100
+ }
101
+ /** What the last aggregation pass did. */
102
+ export interface ScanReport {
103
+ /** Sessions currently visible (live + persisted). */
104
+ sessionsDiscovered: number;
105
+ /** Sessions whose log was actually read during the pass. */
106
+ sessionsRead: number;
107
+ /** Sessions served from the in-memory revision cache. */
108
+ sessionsReused: number;
109
+ sessionsFailed: number;
110
+ durationMs: number;
111
+ /** Epoch ms when the pass started. */
112
+ startedAt: number;
113
+ /** Epoch ms when the payload was produced. */
114
+ generatedAt: number;
115
+ /** True when at least one session could not be read. */
116
+ partial: boolean;
117
+ }
118
+ /** The complete dashboard payload. */
119
+ export interface StatsPayload {
120
+ apiVersion: number;
121
+ revision: number;
122
+ generatedAt: number;
123
+ /** IANA time zone used for day/hour bucketing. */
124
+ timezone: string;
125
+ /** Today's local calendar date, `YYYY-MM-DD`. */
126
+ today: string;
127
+ totals: TokenBuckets;
128
+ /** Total billed attempts across all sessions. */
129
+ requests: number;
130
+ cacheHitRate: number;
131
+ sessions: {
132
+ total: number;
133
+ withUsage: number;
134
+ live: number;
135
+ subagents: number;
136
+ seeded: number;
137
+ };
138
+ models: ModelUsage[];
139
+ days: UsageBucket[];
140
+ /** Only the trailing 48 hours, so "today" can render an hourly chart. */
141
+ hours: UsageBucket[];
142
+ sessionRows: SessionSummary[];
143
+ scan: ScanReport;
144
+ errors: SessionReadError[];
145
+ }
146
+ /** Cross-check of this plugin's fold against token-meter's `tokenUsage` projection. */
147
+ export interface VerifyReport {
148
+ apiVersion: number;
149
+ checked: number;
150
+ matched: number;
151
+ mismatched: number;
152
+ unsupported: number;
153
+ /** Fold result that matched the official projection, per checked session group. */
154
+ matchedBasis: {
155
+ own: number;
156
+ allLog: number;
157
+ none: number;
158
+ };
159
+ details: Array<{
160
+ sessionId: string;
161
+ official: TokenBuckets;
162
+ own: TokenBuckets;
163
+ allLog: TokenBuckets;
164
+ inheritedEventCount: number;
165
+ match: 'own' | 'allLog' | 'none';
166
+ }>;
167
+ generatedAt: number;
168
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Wire contract shared by the Host aggregator and the browser dashboard.
3
+ *
4
+ * These values describe token accounting only. They never include prompt text,
5
+ * assistant text, tool arguments, file contents, credentials, or raw events.
6
+ */
7
+ /** The only wire contract this plugin currently serves. */
8
+ export const API_VERSION = 1;
9
+ /** Same-origin route prefix registered on the Harness web server. */
10
+ export const API_PREFIX = '/dsh-token-dashboard';
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Token usage",
4
+ "description": "Global Token usage for every local session: totals, daily trends, model breakdown, session ranking, and optional cost estimates."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Token 用量",
4
+ "description": "本机全部会话的 Token 用量看板:总量、每日趋势、模型分布、会话排行,以及可选费用估算。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,95 @@
1
1
  {
2
2
  "name": "@shi-yangyang/dsh-token-dashboard",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Global Token usage analytics for every DeepSeek Harness session: totals, trends, models, sessions, and optional cost estimates.",
5
+ "keywords": [
6
+ "deepseek",
7
+ "harness",
8
+ "dsh",
9
+ "dsh-plugin",
10
+ "token",
11
+ "usage",
12
+ "dashboard",
13
+ "analytics"
14
+ ],
15
+ "type": "module",
16
+ "main": "./lib/host/index.js",
17
+ "types": "./lib/host/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./lib/host/index.d.ts",
21
+ "default": "./lib/host/index.js"
22
+ },
23
+ "./client": "./client/client.js",
24
+ "./locale/*.json": "./locale/*.json",
25
+ "./icon": "./icon.svg",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib",
30
+ "client",
31
+ "locale",
32
+ "icon.svg",
33
+ "cordis.patch.yml",
34
+ "README.md",
35
+ "THIRD_PARTY_NOTICES.md",
36
+ "LICENSE"
37
+ ],
38
+ "dsh": {
39
+ "bundle": {
40
+ "patch": "./cordis.patch.yml"
41
+ },
42
+ "client": {
43
+ "platform": "web",
44
+ "immediately": true,
45
+ "inject": [
46
+ "@deepseek-ai/dsh-client-locale",
47
+ "@deepseek-ai/dsh-client-ui-theme"
48
+ ]
49
+ }
50
+ },
51
+ "scripts": {
52
+ "build": "npm run build:host && npm run build:client",
53
+ "build:prices": "node scripts/build-builtin-prices.mjs",
54
+ "build:host": "tsc -p tsconfig.host.json",
55
+ "build:css": "tailwindcss -i src/client/beui.css -o src/client/beui.generated.css --minify",
56
+ "build:client": "node scripts/build-client.mjs",
57
+ "pretest": "node scripts/build-client.mjs",
58
+ "typecheck": "tsc -p tsconfig.host.json --noEmit && tsc -p tsconfig.client.json --noEmit && tsc -p tsconfig.test.json --noEmit",
59
+ "test": "node scripts/test.mjs",
60
+ "prepare": "npm run build",
61
+ "prepack": "npm run build",
62
+ "verify:real": "node scripts/verify-real-sessions.mjs"
63
+ },
64
+ "license": "MIT",
65
+ "publishConfig": {
66
+ "access": "public"
67
+ },
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/shi-YangYang/dsh-token-dashboard.git"
71
+ },
72
+ "homepage": "https://github.com/shi-YangYang/dsh-token-dashboard#readme",
73
+ "engines": {
74
+ "node": ">=22.18"
75
+ },
76
+ "devDependencies": {
77
+ "@tailwindcss/cli": "^4.3.5",
78
+ "@types/jsdom": "^21.1.7",
79
+ "@types/node": "^24.10.0",
80
+ "@types/react": "^18.3.12",
81
+ "@types/react-dom": "18.3.7",
82
+ "esbuild": "^0.25.0",
83
+ "jsdom": "^26.1.0",
84
+ "react": "18.3.1",
85
+ "react-dom": "18.3.1",
86
+ "tailwindcss": "^4.3.5",
87
+ "typescript": "^5.9.0"
88
+ },
89
+ "dependencies": {
90
+ "clsx": "^2.1.1",
91
+ "lucide-react": "^1.70.0",
92
+ "motion": "^14.10.1",
93
+ "tailwind-merge": "^3.7.0"
94
+ }
95
+ }