@chenmiao8563/dsh-token-ledger 0.1.1 → 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.
- package/CHANGELOG.md +27 -1
- package/README.md +38 -1
- package/README.zh.md +31 -1
- package/docs/VERIFICATION.md +77 -0
- package/lib/client.js +678 -0
- package/lib/index.js +24 -0
- package/lib/overview.js +228 -0
- package/lib/route.js +141 -0
- package/package.json +16 -1
package/lib/index.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { UsageLedger, inheritedCut, isForkSession } from './ledger.js'
|
|
26
|
+
import { createOverviewRoute, OVERVIEW_PATH } from './route.js'
|
|
26
27
|
import { ledgerPaths, loadLedger, saveLedger, writeFileAtomic } from './store.js'
|
|
27
28
|
import { join } from 'node:path'
|
|
28
29
|
|
|
@@ -248,6 +249,29 @@ export function apply(ctx, config = {}) {
|
|
|
248
249
|
})
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
// The browser half reads this route, which is a read-only view of the live
|
|
253
|
+
// ledger. Without a web server the plugin is still fully usable through
|
|
254
|
+
// /tokens and the CLI, so the service is optional rather than injected.
|
|
255
|
+
//
|
|
256
|
+
// Registration is logged because the settings page can only report that it
|
|
257
|
+
// could not read the route. The log is what distinguishes "this profile has no
|
|
258
|
+
// web server" from "the route failed to register"; from the browser the two
|
|
259
|
+
// look identical.
|
|
260
|
+
if (typeof ctx.inject === 'function') {
|
|
261
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
262
|
+
ctx.effect(() => {
|
|
263
|
+
const dispose = webCtx.webServer.register(
|
|
264
|
+
createOverviewRoute({
|
|
265
|
+
ledger,
|
|
266
|
+
options: { onError: (error) => log.warn('[token-ledger] overview route failed: %o', error) },
|
|
267
|
+
}),
|
|
268
|
+
)
|
|
269
|
+
log.info('[token-ledger] settings page route ready at %s', OVERVIEW_PATH)
|
|
270
|
+
return dispose
|
|
271
|
+
}, 'token-ledger: overview route')
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
251
275
|
if (typeof ctx.effect === 'function') {
|
|
252
276
|
ctx.effect(
|
|
253
277
|
() => () => {
|
package/lib/overview.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The overview payload the browser half renders.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the plugin so it is a pure function of a ledger snapshot
|
|
5
|
+
* and a clock, and therefore testable without a host, a browser or a socket.
|
|
6
|
+
* The host route is then a thin wrapper: take the live snapshot, call this,
|
|
7
|
+
* serialize.
|
|
8
|
+
*
|
|
9
|
+
* ## Why the payload is shaped like this
|
|
10
|
+
*
|
|
11
|
+
* The page the client draws has three parts — range totals, today, and a
|
|
12
|
+
* calendar. Each is answered here rather than in the browser so the client
|
|
13
|
+
* stays dumb and two clients cannot disagree:
|
|
14
|
+
*
|
|
15
|
+
* - `ranges` carries the totals for the three selectable ranges, plus the
|
|
16
|
+
* derived cache-hit rate, so switching a tab is a re-render and not a
|
|
17
|
+
* re-fetch.
|
|
18
|
+
* - `today` carries the current local day's numbers, which is what "live"
|
|
19
|
+
* means for a ledger: a value that moves as steps complete.
|
|
20
|
+
* - `daily` is a **contiguous** day series including zero-token days, because
|
|
21
|
+
* a calendar grid needs every cell, and gap-filling in one place is better
|
|
22
|
+
* than in every view.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-token-ledger/overview
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** How many days of daily series to publish, bounding the payload. */
|
|
28
|
+
export const SERIES_DAYS = 366
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Render a local calendar day as `YYYY-MM-DD`.
|
|
32
|
+
*
|
|
33
|
+
* @param {Date} date - the day.
|
|
34
|
+
* @returns {string} the key.
|
|
35
|
+
*/
|
|
36
|
+
export function dayKey(date) {
|
|
37
|
+
const pad = (value) => (value < 10 ? `0${value}` : String(value))
|
|
38
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse a `YYYY-MM-DD` key back into a local midnight `Date`.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} key - the day key.
|
|
45
|
+
* @returns {Date} local midnight of that day.
|
|
46
|
+
*/
|
|
47
|
+
export function parseDayKey(key) {
|
|
48
|
+
const [year, month, day] = String(key).split('-').map((part) => Number.parseInt(part, 10))
|
|
49
|
+
return new Date(year, (month ?? 1) - 1, day ?? 1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The local date a range starts on.
|
|
54
|
+
*
|
|
55
|
+
* `week` is the trailing seven days including today, because "7 天" reads as a
|
|
56
|
+
* rolling window; `month` and `year` are calendar periods, because "本月" and
|
|
57
|
+
* "本年" read as the period you are in.
|
|
58
|
+
*
|
|
59
|
+
* @param {'week'|'month'|'year'} kind - the range.
|
|
60
|
+
* @param {Date} now - the current time.
|
|
61
|
+
* @returns {Date} local midnight of the first day in range.
|
|
62
|
+
*/
|
|
63
|
+
export function rangeStart(kind, now) {
|
|
64
|
+
if (kind === 'week') {
|
|
65
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
66
|
+
start.setDate(start.getDate() - 6)
|
|
67
|
+
return start
|
|
68
|
+
}
|
|
69
|
+
if (kind === 'month') return new Date(now.getFullYear(), now.getMonth(), 1)
|
|
70
|
+
if (kind === 'year') return new Date(now.getFullYear(), 0, 1)
|
|
71
|
+
throw new Error(`unknown range "${kind}" (expected week, month, or year)`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* An empty counter set in the shape the ledger emits.
|
|
76
|
+
*
|
|
77
|
+
* @returns {{ inputTokens: number, outputTokens: number, cacheReadTokens: number, cacheWriteTokens: number, totalTokens: number, reasoningTokens: number }} zeroed counters.
|
|
78
|
+
*/
|
|
79
|
+
function emptyTotals() {
|
|
80
|
+
return {
|
|
81
|
+
inputTokens: 0,
|
|
82
|
+
outputTokens: 0,
|
|
83
|
+
cacheReadTokens: 0,
|
|
84
|
+
cacheWriteTokens: 0,
|
|
85
|
+
totalTokens: 0,
|
|
86
|
+
reasoningTokens: 0,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Add one day row into an accumulator.
|
|
92
|
+
*
|
|
93
|
+
* @param {ReturnType<typeof emptyTotals>} target - mutated accumulator.
|
|
94
|
+
* @param {object} row - a ledger daily row.
|
|
95
|
+
* @returns {void}
|
|
96
|
+
*/
|
|
97
|
+
function accumulate(target, row) {
|
|
98
|
+
for (const key of ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'totalTokens', 'reasoningTokens']) {
|
|
99
|
+
const value = row?.[key]
|
|
100
|
+
if (typeof value === 'number' && Number.isFinite(value)) target[key] += value
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The share of input tokens served from the prompt cache.
|
|
106
|
+
*
|
|
107
|
+
* Defined over input only: a cache read is an input token that did not have to
|
|
108
|
+
* be re-sent, so `cacheRead / (cacheRead + uncachedInput)` is the fraction of
|
|
109
|
+
* input the cache absorbed. Returns `null` when there was no input at all,
|
|
110
|
+
* which the client renders as a dash rather than as a misleading 0%.
|
|
111
|
+
*
|
|
112
|
+
* @param {{ inputTokens: number, cacheReadTokens: number }} totals - the counters.
|
|
113
|
+
* @returns {number|null} a fraction between 0 and 1, or null when undefined.
|
|
114
|
+
*/
|
|
115
|
+
export function cacheHitRate(totals) {
|
|
116
|
+
const denominator = (totals.cacheReadTokens ?? 0) + (totals.inputTokens ?? 0)
|
|
117
|
+
if (denominator <= 0) return null
|
|
118
|
+
return totals.cacheReadTokens / denominator
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fold a set of day rows into one range summary.
|
|
123
|
+
*
|
|
124
|
+
* @param {object[]} rows - day rows inside the range.
|
|
125
|
+
* @returns {{ totals: ReturnType<typeof emptyTotals>, calls: number, activeDays: number, cacheHitRate: number|null }} the summary.
|
|
126
|
+
*/
|
|
127
|
+
export function summarize(rows) {
|
|
128
|
+
const totals = emptyTotals()
|
|
129
|
+
let calls = 0
|
|
130
|
+
let activeDays = 0
|
|
131
|
+
for (const row of rows) {
|
|
132
|
+
accumulate(totals, row)
|
|
133
|
+
if (typeof row?.calls === 'number' && Number.isFinite(row.calls)) calls += row.calls
|
|
134
|
+
if ((row?.totalTokens ?? 0) > 0) activeDays += 1
|
|
135
|
+
}
|
|
136
|
+
return { totals, calls, activeDays, cacheHitRate: cacheHitRate(totals) }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A contiguous day series ending today, gaps filled with zeroes.
|
|
141
|
+
*
|
|
142
|
+
* @param {Map<string, object>} byDay - ledger daily rows keyed by day.
|
|
143
|
+
* @param {Date} now - the current time.
|
|
144
|
+
* @param {number} [limit] - maximum number of days emitted.
|
|
145
|
+
* @returns {object[]} ascending day rows, one per calendar day.
|
|
146
|
+
*/
|
|
147
|
+
export function buildSeries(byDay, now, limit = SERIES_DAYS) {
|
|
148
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
149
|
+
const keys = [...byDay.keys()].sort()
|
|
150
|
+
const earliest = keys.length > 0 ? parseDayKey(keys[0]) : today
|
|
151
|
+
const span = Math.min(limit, Math.max(1, Math.round((today - earliest) / 86400000) + 1))
|
|
152
|
+
|
|
153
|
+
const series = []
|
|
154
|
+
for (let offset = span - 1; offset >= 0; offset -= 1) {
|
|
155
|
+
const date = new Date(today.getFullYear(), today.getMonth(), today.getDate() - offset)
|
|
156
|
+
const key = dayKey(date)
|
|
157
|
+
const row = byDay.get(key)
|
|
158
|
+
series.push({
|
|
159
|
+
date: key,
|
|
160
|
+
calls: typeof row?.calls === 'number' ? row.calls : 0,
|
|
161
|
+
inputTokens: row?.inputTokens ?? 0,
|
|
162
|
+
outputTokens: row?.outputTokens ?? 0,
|
|
163
|
+
cacheReadTokens: row?.cacheReadTokens ?? 0,
|
|
164
|
+
cacheWriteTokens: row?.cacheWriteTokens ?? 0,
|
|
165
|
+
totalTokens: row?.totalTokens ?? 0,
|
|
166
|
+
reasoningTokens: row?.reasoningTokens ?? 0,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
return series
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build the whole overview payload.
|
|
174
|
+
*
|
|
175
|
+
* @param {object} snapshot - a `UsageLedger#snapshot()` value.
|
|
176
|
+
* @param {{ now?: Date }} [options] - clock override for tests.
|
|
177
|
+
* @returns {object} the JSON payload the client renders. Every field is a
|
|
178
|
+
* primitive, a plain object or an array, so it serializes losslessly.
|
|
179
|
+
*/
|
|
180
|
+
export function buildOverview(snapshot, { now = new Date() } = {}) {
|
|
181
|
+
const dailyRows = Array.isArray(snapshot?.daily) ? snapshot.daily : []
|
|
182
|
+
const byDay = new Map()
|
|
183
|
+
for (const row of dailyRows) {
|
|
184
|
+
if (typeof row?.date === 'string') byDay.set(row.date, row)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const series = buildSeries(byDay, now)
|
|
188
|
+
const todayKey = dayKey(now)
|
|
189
|
+
|
|
190
|
+
const ranges = {}
|
|
191
|
+
for (const kind of ['week', 'month', 'year']) {
|
|
192
|
+
const startKey = dayKey(rangeStart(kind, now))
|
|
193
|
+
const inRange = [...byDay.entries()]
|
|
194
|
+
.filter(([key]) => key >= startKey && key <= todayKey)
|
|
195
|
+
.map(([, row]) => row)
|
|
196
|
+
ranges[kind] = { kind, from: startKey, to: todayKey, ...summarize(inRange) }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const todayRow = byDay.get(todayKey)
|
|
200
|
+
const todaySummary = summarize(todayRow === undefined ? [] : [todayRow])
|
|
201
|
+
|
|
202
|
+
const models = (Array.isArray(snapshot?.models) ? snapshot.models : [])
|
|
203
|
+
.slice(0, 12)
|
|
204
|
+
.map((model) => ({
|
|
205
|
+
model: String(model?.model ?? 'unknown'),
|
|
206
|
+
calls: typeof model?.calls === 'number' ? model.calls : 0,
|
|
207
|
+
totalTokens: model?.totalTokens ?? 0,
|
|
208
|
+
cacheReadTokens: model?.cacheReadTokens ?? 0,
|
|
209
|
+
inputTokens: model?.inputTokens ?? 0,
|
|
210
|
+
cacheHitRate: cacheHitRate({ cacheReadTokens: model?.cacheReadTokens ?? 0, inputTokens: model?.inputTokens ?? 0 }),
|
|
211
|
+
}))
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
plugin: 'token-ledger',
|
|
215
|
+
generatedAt: now.getTime(),
|
|
216
|
+
ledgerUpdatedAt: typeof snapshot?.updatedAt === 'number' ? snapshot.updatedAt : null,
|
|
217
|
+
totals: {
|
|
218
|
+
...emptyTotals(),
|
|
219
|
+
...(snapshot?.totals ?? {}),
|
|
220
|
+
cacheHitRate: cacheHitRate(snapshot?.totals ?? {}),
|
|
221
|
+
},
|
|
222
|
+
ranges,
|
|
223
|
+
today: { date: todayKey, lastEventAt: typeof snapshot?.updatedAt === 'number' ? snapshot.updatedAt : null, ...todaySummary },
|
|
224
|
+
series,
|
|
225
|
+
models,
|
|
226
|
+
sessionCount: Array.isArray(snapshot?.sessions) ? snapshot.sessions.length : 0,
|
|
227
|
+
}
|
|
228
|
+
}
|
package/lib/route.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host route that feeds the browser half.
|
|
3
|
+
*
|
|
4
|
+
* ## Why a route instead of the settings namespace
|
|
5
|
+
*
|
|
6
|
+
* The obvious channel for plugin data is a settings namespace, and the reference
|
|
7
|
+
* UI plugins use one. This plugin deliberately does not, for two reasons:
|
|
8
|
+
*
|
|
9
|
+
* 1. A settings namespace needs a schema, and the schema library is a real
|
|
10
|
+
* dependency. This package's whole installability claim is that it has none.
|
|
11
|
+
* 2. Settings are persisted. Publishing token aggregates through them would
|
|
12
|
+
* rewrite `settings.yaml` on every debounce and grow it with data that is
|
|
13
|
+
* derived and reproducible, which is exactly the mistake that left a 185 kB
|
|
14
|
+
* settings backup on the machine this was developed against.
|
|
15
|
+
*
|
|
16
|
+
* The ledger file stays the store of record; this route is a read-only view of
|
|
17
|
+
* the live in-memory ledger.
|
|
18
|
+
*
|
|
19
|
+
* ## The guard
|
|
20
|
+
*
|
|
21
|
+
* The route exposes local usage counts, which are not secrets but are also
|
|
22
|
+
* nobody else's business. The web server binds loopback by default, but an
|
|
23
|
+
* operator may bind `0.0.0.0`, so the handler checks the peer itself: an
|
|
24
|
+
* address is accepted only when it is loopback, and an `Origin` header is
|
|
25
|
+
* accepted only when it is loopback or absent.
|
|
26
|
+
*
|
|
27
|
+
* Absent is allowed because the desktop build loads the front end over
|
|
28
|
+
* `file://` and carries `fetch` over an IPC bridge, where a socket address may
|
|
29
|
+
* be unavailable. Refusing that case would break the desktop app to protect
|
|
30
|
+
* against a request that never left the machine.
|
|
31
|
+
*
|
|
32
|
+
* @module dsh-token-ledger/route
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { buildOverview } from './overview.js'
|
|
36
|
+
|
|
37
|
+
/** The path the browser half fetches. */
|
|
38
|
+
export const OVERVIEW_PATH = '/api/token-ledger/summary'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether a peer address is loopback.
|
|
42
|
+
*
|
|
43
|
+
* @param {string|undefined} address - `req.socket.remoteAddress`.
|
|
44
|
+
* @returns {boolean} true for IPv4 or IPv6 loopback.
|
|
45
|
+
*/
|
|
46
|
+
export function isLoopbackAddress(address) {
|
|
47
|
+
if (typeof address !== 'string') return false
|
|
48
|
+
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether an `Origin` header belongs to a local page.
|
|
53
|
+
*
|
|
54
|
+
* @param {string|undefined} origin - the header value.
|
|
55
|
+
* @returns {boolean} true when absent, `null`, `file://`, or a loopback host.
|
|
56
|
+
*/
|
|
57
|
+
export function isLocalOrigin(origin) {
|
|
58
|
+
if (origin === undefined || origin === '' || origin === 'null') return true
|
|
59
|
+
if (origin.startsWith('file://')) return true
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(origin)
|
|
62
|
+
// WHATWG URL keeps the brackets on an IPv6 host: `new URL('http://[::1]/').hostname`
|
|
63
|
+
// is `'[::1]'`, so they have to come off before comparing.
|
|
64
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, '')
|
|
65
|
+
return hostname === '127.0.0.1' || hostname === '::1' || hostname === 'localhost'
|
|
66
|
+
} catch {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Decide whether a request may read the overview.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} req - a node `IncomingMessage`-shaped object.
|
|
75
|
+
* @returns {boolean} whether to serve it.
|
|
76
|
+
*/
|
|
77
|
+
export function isAllowedPeer(req) {
|
|
78
|
+
const address = req?.socket?.remoteAddress
|
|
79
|
+
// An unrecognized address means the request did not arrive over a socket we
|
|
80
|
+
// can judge (the desktop IPC bridge), not that it came from a stranger.
|
|
81
|
+
if (typeof address === 'string' && address !== '' && !isLoopbackAddress(address)) return false
|
|
82
|
+
return isLocalOrigin(req?.headers?.origin)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Write a JSON response.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} res - a node `ServerResponse`-shaped object.
|
|
89
|
+
* @param {number} status - the HTTP status.
|
|
90
|
+
* @param {unknown} body - the JSON value.
|
|
91
|
+
* @returns {void}
|
|
92
|
+
*/
|
|
93
|
+
function sendJson(res, status, body) {
|
|
94
|
+
const text = JSON.stringify(body)
|
|
95
|
+
res.writeHead(status, {
|
|
96
|
+
'content-type': 'application/json; charset=utf-8',
|
|
97
|
+
'content-length': Buffer.byteLength(text),
|
|
98
|
+
// The numbers move as steps complete, so a cached copy is always wrong.
|
|
99
|
+
'cache-control': 'no-store',
|
|
100
|
+
})
|
|
101
|
+
res.end(text)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build the route definition to hand to `ctx.webServer.register`.
|
|
106
|
+
*
|
|
107
|
+
* The handler never throws: a fault becomes a 500 with a JSON body, because the
|
|
108
|
+
* web server logs a warning and destroys the socket when a handler throws, and
|
|
109
|
+
* a client that cannot reach the plugin should be able to say why.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} input - the route's dependencies.
|
|
112
|
+
* @param {{ snapshot: () => object }} input.ledger - the live ledger.
|
|
113
|
+
* @param {{ now?: () => Date, onError?: (error: unknown) => void }} [input.options] - clock and diagnostics.
|
|
114
|
+
* @returns {{ kind: 'exact', path: string, handler: (req: object, res: object) => void }} the route.
|
|
115
|
+
*/
|
|
116
|
+
export function createOverviewRoute({ ledger, options = {} }) {
|
|
117
|
+
const now = options.now ?? (() => new Date())
|
|
118
|
+
const onError = options.onError ?? (() => {})
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
kind: 'exact',
|
|
122
|
+
path: OVERVIEW_PATH,
|
|
123
|
+
handler: (req, res) => {
|
|
124
|
+
if (!isAllowedPeer(req)) {
|
|
125
|
+
sendJson(res, 403, { error: 'forbidden' })
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
const method = req?.method ?? 'GET'
|
|
129
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
130
|
+
sendJson(res, 405, { error: 'method not allowed' })
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
sendJson(res, 200, buildOverview(ledger.snapshot(), { now: now() }))
|
|
135
|
+
} catch (error) {
|
|
136
|
+
onError(error)
|
|
137
|
+
sendJson(res, 500, { error: 'overview unavailable' })
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chenmiao8563/dsh-token-ledger",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Transparent, auditable token accounting for DeepSeek Harness: a restart-safe ledger over the durable session log, plus a CLI that recomputes it from raw logs and diffs the result.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"./ledger": "./lib/ledger.js",
|
|
27
27
|
"./session-log": "./lib/session-log.js",
|
|
28
28
|
"./store": "./lib/store.js",
|
|
29
|
+
"./overview": "./lib/overview.js",
|
|
30
|
+
"./route": "./lib/route.js",
|
|
31
|
+
"./client": "./lib/client.js",
|
|
29
32
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
30
33
|
"./package.json": "./package.json"
|
|
31
34
|
},
|
|
@@ -52,11 +55,23 @@
|
|
|
52
55
|
"verify": "node scripts/verify-package.mjs",
|
|
53
56
|
"prepublishOnly": "npm run verify && npm test"
|
|
54
57
|
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1",
|
|
60
|
+
"react": "18.3.1",
|
|
61
|
+
"react-dom": "18.3.1"
|
|
62
|
+
},
|
|
55
63
|
"dsh": {
|
|
56
64
|
"pluginType": "feature",
|
|
57
65
|
"bundle": {
|
|
58
66
|
"patch": "./cordis.patch.yml"
|
|
59
67
|
},
|
|
68
|
+
"client": {
|
|
69
|
+
"platform": "web",
|
|
70
|
+
"inject": [
|
|
71
|
+
"@deepseek-ai/dsh-client-locale",
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-settings-general"
|
|
73
|
+
]
|
|
74
|
+
},
|
|
60
75
|
"compatibility": {
|
|
61
76
|
"dsh": ">=0.1.2-alpha.1"
|
|
62
77
|
}
|