@goodandready/dsh-clinebot 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/lib/client.js +90 -18
- package/lib/proxy-token.js +2 -2
- package/lib/routes/settings.js +19 -1
- package/lib/stats-storage.js +16 -1
- package/lib/transport/cline-transport.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.1] - 2026-10-21
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **ClineBot-only session cost**: the composer now reads the plugin's per-session `bySession` bucket instead of the host's session-wide `tokenUsage`. A conversation that also used OpenCode can no longer inflate the ClineBot estimate.
|
|
12
|
+
- **Last request in the tooltip**: each session bucket retains the latest ClineBot request; the native token pill title now shows the ClineBot session cost and the last request cost.
|
|
13
|
+
- **Free estimates**: priced requests that cost zero render as `Free` instead of `$0.0000`.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `GET /dsh-clinebot/stats/session?sessionId=...` returns the ClineBot-only aggregate for one DSH session, including the last request.
|
|
17
|
+
|
|
8
18
|
## [0.6.0] - 2026-10-20
|
|
9
19
|
|
|
10
20
|
### Added
|
package/lib/client.js
CHANGED
|
@@ -241,7 +241,9 @@ const en = {
|
|
|
241
241
|
'dashboard.account_pool': 'Active Routing Account',
|
|
242
242
|
'dashboard.credits': 'Credit Balance',
|
|
243
243
|
'dashboard.updated_at': 'Updated at',
|
|
244
|
-
'composer.cost_title': '
|
|
244
|
+
'composer.cost_title': 'ClineBot Session Cost',
|
|
245
|
+
'composer.last_request': 'Last request',
|
|
246
|
+
'composer.free': 'Free',
|
|
245
247
|
'composer.cost_breakdown': 'Token & Cost Breakdown',
|
|
246
248
|
'composer.prompt_tok': 'Prompt Tokens',
|
|
247
249
|
'composer.completion_tok': 'Completion Tokens',
|
|
@@ -528,7 +530,9 @@ const zh = {
|
|
|
528
530
|
'dashboard.account_pool': '当前承载账户',
|
|
529
531
|
'dashboard.credits': '点数余额',
|
|
530
532
|
'dashboard.updated_at': '更新于',
|
|
531
|
-
'composer.cost_title': '
|
|
533
|
+
'composer.cost_title': 'ClineBot 会话成本',
|
|
534
|
+
'composer.last_request': '最近一次请求',
|
|
535
|
+
'composer.free': '免费',
|
|
532
536
|
'composer.cost_breakdown': 'Token 与费用明细',
|
|
533
537
|
'composer.prompt_tok': '提示词 Token',
|
|
534
538
|
'composer.completion_tok': '补全 Token',
|
|
@@ -4710,10 +4714,12 @@ function ComposerCostEntry(props) {
|
|
|
4710
4714
|
const configSnapshot = useConfigFormsSnapshot(props?.ctx)
|
|
4711
4715
|
const [stats, setStats] = React.useState(null)
|
|
4712
4716
|
const [prices, setPrices] = React.useState([])
|
|
4717
|
+
const [sessionStats, setSessionStats] = React.useState(null)
|
|
4713
4718
|
const markerRef = React.useRef(null)
|
|
4714
4719
|
|
|
4715
4720
|
const projectedTokens = props.useProjection('tokenUsage')
|
|
4716
4721
|
const selection = props.useProjection('modelSelection')
|
|
4722
|
+
const sessionId = String(props?.sessionId || '')
|
|
4717
4723
|
const priceOverridesKey = JSON.stringify(configSnapshot?.value?.priceOverrides || {})
|
|
4718
4724
|
|
|
4719
4725
|
React.useEffect(() => {
|
|
@@ -4728,7 +4734,40 @@ function ComposerCostEntry(props) {
|
|
|
4728
4734
|
}, [priceOverridesKey])
|
|
4729
4735
|
|
|
4730
4736
|
React.useEffect(() => {
|
|
4731
|
-
if (
|
|
4737
|
+
if (!sessionId) {
|
|
4738
|
+
setSessionStats(null)
|
|
4739
|
+
return undefined
|
|
4740
|
+
}
|
|
4741
|
+
let active = true
|
|
4742
|
+
const loadSession = async () => {
|
|
4743
|
+
try {
|
|
4744
|
+
const res = await fetchWithTimeout(
|
|
4745
|
+
`${ROUTE_PREFIX}/stats/session?sessionId=${encodeURIComponent(sessionId)}`,
|
|
4746
|
+
{ cache: 'no-store' },
|
|
4747
|
+
)
|
|
4748
|
+
const data = await res.json().catch(() => null)
|
|
4749
|
+
if (active && data?.ok) setSessionStats(data.session || null)
|
|
4750
|
+
} catch {
|
|
4751
|
+
/* quiet */
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
loadSession()
|
|
4755
|
+
let timer = null
|
|
4756
|
+
const schedule = () => {
|
|
4757
|
+
timer = setTimeout(async () => {
|
|
4758
|
+
await loadSession()
|
|
4759
|
+
if (active) schedule()
|
|
4760
|
+
}, 10000)
|
|
4761
|
+
}
|
|
4762
|
+
schedule()
|
|
4763
|
+
return () => {
|
|
4764
|
+
active = false
|
|
4765
|
+
if (timer) clearTimeout(timer)
|
|
4766
|
+
}
|
|
4767
|
+
}, [sessionId, projectedTokens])
|
|
4768
|
+
|
|
4769
|
+
React.useEffect(() => {
|
|
4770
|
+
if (projectedTokens || sessionId) return undefined
|
|
4732
4771
|
let active = true
|
|
4733
4772
|
const loadStats = async () => {
|
|
4734
4773
|
try {
|
|
@@ -4744,7 +4783,7 @@ function ComposerCostEntry(props) {
|
|
|
4744
4783
|
loadStats()
|
|
4745
4784
|
const timer = setTimeout(loadStats, 30000)
|
|
4746
4785
|
return () => { active = false; clearTimeout(timer) }
|
|
4747
|
-
}, [projectedTokens])
|
|
4786
|
+
}, [projectedTokens, sessionId])
|
|
4748
4787
|
|
|
4749
4788
|
const selected = selection?.lastUsed
|
|
4750
4789
|
?? selection?.next
|
|
@@ -4754,32 +4793,65 @@ function ComposerCostEntry(props) {
|
|
|
4754
4793
|
const isClineBot = (provider === '' || provider === 'clinebot' || provider === 'llm-clinebot')
|
|
4755
4794
|
&& (provider === 'clinebot' || provider === 'llm-clinebot' || modelId.startsWith('cline-pass/'))
|
|
4756
4795
|
|
|
4757
|
-
|
|
4796
|
+
// ClineBot requests are recorded in the plugin's own per-session bucket. That
|
|
4797
|
+
// bucket is provider-scoped, unlike the host's session-wide tokenUsage, so a
|
|
4798
|
+
// session that also used OpenCode cannot inflate the ClineBot read-out.
|
|
4799
|
+
const session = sessionStats && Number(sessionStats.requests || 0) > 0 ? sessionStats : null
|
|
4800
|
+
const sessionTokens = session
|
|
4801
|
+
? {
|
|
4802
|
+
promptTokens: Number(session.promptTokens || 0),
|
|
4803
|
+
completionTokens: Number(session.completionTokens || 0),
|
|
4804
|
+
reasoningTokens: Number(session.reasoningTokens || 0),
|
|
4805
|
+
cacheReadTokens: Number(session.cacheReadTokens || 0),
|
|
4806
|
+
cacheWriteTokens: Number(session.cacheWriteTokens || 0),
|
|
4807
|
+
}
|
|
4808
|
+
: null
|
|
4809
|
+
const rawTokens = sessionTokens || (sessionId ? null : (projectedTokens || {
|
|
4758
4810
|
promptTokens: stats?.promptTokens || 0,
|
|
4759
4811
|
completionTokens: stats?.completionTokens || 0,
|
|
4760
4812
|
reasoningTokens: stats?.reasoningTokens || 0,
|
|
4761
4813
|
cacheReadTokens: stats?.cacheReadTokens || 0,
|
|
4762
4814
|
cacheWriteTokens: stats?.cacheWriteTokens || 0,
|
|
4763
|
-
}
|
|
4764
|
-
const cacheRead = Number(rawTokens
|
|
4765
|
-
const cacheWrite = Number(rawTokens
|
|
4766
|
-
const uncachedInput = Number(rawTokens
|
|
4767
|
-
const promptTokens =
|
|
4768
|
-
? uncachedInput
|
|
4769
|
-
|
|
4815
|
+
}))
|
|
4816
|
+
const cacheRead = Number(rawTokens?.cacheReadTokens || 0)
|
|
4817
|
+
const cacheWrite = Number(rawTokens?.cacheWriteTokens || 0)
|
|
4818
|
+
const uncachedInput = Number(rawTokens?.uncachedInputTokens)
|
|
4819
|
+
const promptTokens = rawTokens
|
|
4820
|
+
? (Number.isFinite(uncachedInput)
|
|
4821
|
+
? uncachedInput + cacheRead + cacheWrite
|
|
4822
|
+
: Number(rawTokens.promptTokens || rawTokens.inputTokens || 0))
|
|
4823
|
+
: 0
|
|
4770
4824
|
const tokens = {
|
|
4771
4825
|
promptTokens,
|
|
4772
|
-
completionTokens: Number(rawTokens
|
|
4773
|
-
reasoningTokens: Number(rawTokens
|
|
4826
|
+
completionTokens: Number(rawTokens?.completionTokens || rawTokens?.outputTokens || 0),
|
|
4827
|
+
reasoningTokens: Number(rawTokens?.reasoningTokens || 0),
|
|
4774
4828
|
cacheReadTokens: cacheRead,
|
|
4775
4829
|
cacheWriteTokens: cacheWrite,
|
|
4776
4830
|
}
|
|
4777
|
-
const
|
|
4778
|
-
|
|
4779
|
-
const
|
|
4780
|
-
const
|
|
4831
|
+
const calculated = calculateCost(tokens, session?.model || modelId, prices)
|
|
4832
|
+
const cost = session ? Number(session.costUsd || 0) : calculated.cost
|
|
4833
|
+
const priced = session ? Number(session.pricedRequests || 0) > 0 : calculated.priced
|
|
4834
|
+
const source = session
|
|
4835
|
+
? (findPrice(session.model || modelId, prices)?.source || 'estimate')
|
|
4836
|
+
: calculated.source
|
|
4837
|
+
const { pTok, cTok, rTok } = calculated
|
|
4838
|
+
const total = session ? Number(session.totalTokens || pTok + cTok) : calculated.total
|
|
4839
|
+
const last = session?.last || null
|
|
4840
|
+
const lastCost = last ? Number(last.costUsd || 0) : 0
|
|
4841
|
+
const lastPriced = Boolean(last?.priced)
|
|
4842
|
+
|
|
4843
|
+
const costValue = priced
|
|
4844
|
+
? (cost === 0 ? t('composer.free') : (cost < 0.0001 ? '< $0.0001' : `$${cost.toFixed(4)}`))
|
|
4845
|
+
: t('composer.unpriced')
|
|
4846
|
+
const costDisplay = priced && source === 'estimate' && cost > 0 ? `≈ ${costValue}` : costValue
|
|
4847
|
+
const lastCostValue = last
|
|
4848
|
+
? (lastPriced
|
|
4849
|
+
? (lastCost === 0 ? t('composer.free') : (lastCost < 0.0001 ? '< $0.0001' : `$${lastCost.toFixed(4)}`))
|
|
4850
|
+
: t('composer.unpriced'))
|
|
4851
|
+
: ''
|
|
4781
4852
|
const title = [
|
|
4782
4853
|
`${t('composer.cost_title')} ${costDisplay}`,
|
|
4854
|
+
...(last ? [`${t('composer.last_request')} ${lastCostValue}`] : []),
|
|
4783
4855
|
`${t('composer.prompt_tok')} ${formatTokens(pTok)}`,
|
|
4784
4856
|
`${t('composer.completion_tok')} ${formatTokens(cTok)}`,
|
|
4785
4857
|
...(rTok > 0 ? [`${t('composer.reasoning_tok')} ${formatTokens(rTok)}`] : []),
|
package/lib/proxy-token.js
CHANGED
|
@@ -22,9 +22,9 @@ if (process.env[LOCAL_PROXY_KEY_ENV]) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const require = createRequire(import.meta.url)
|
|
25
|
-
let packageVersion = '0.6.
|
|
25
|
+
let packageVersion = '0.6.1'
|
|
26
26
|
try {
|
|
27
|
-
packageVersion = require('../package.json').version || '0.6.
|
|
27
|
+
packageVersion = require('../package.json').version || '0.6.1'
|
|
28
28
|
} catch (err) {
|
|
29
29
|
/* fallback to default version if manifest is unreadable */
|
|
30
30
|
}
|
package/lib/routes/settings.js
CHANGED
|
@@ -3,7 +3,7 @@ import { assertTrustedSettingsRequest } from '../access.js'
|
|
|
3
3
|
import { publicConfig, plainConfig, Config } from '../config.js'
|
|
4
4
|
import { buildStatus } from '../provider-sync.js'
|
|
5
5
|
import { saveCredentialKey, smokeChat, DEFAULT_API_KEY_ENV } from '../cline-client.js'
|
|
6
|
-
import { getStatsSummary, resetStats, exportStatsAsCsv, getAnalytics } from '../stats-storage.js'
|
|
6
|
+
import { getStatsSummary, getSessionStats, resetStats, exportStatsAsCsv, getAnalytics } from '../stats-storage.js'
|
|
7
7
|
import { priceTable } from '../model-prices.js'
|
|
8
8
|
|
|
9
9
|
export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProviderState, triggerAutoDiscover }) {
|
|
@@ -197,6 +197,24 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
197
197
|
},
|
|
198
198
|
}), 'dsh-clinebot: /stats')
|
|
199
199
|
|
|
200
|
+
// 4b. GET /dsh-clinebot/stats/session — ClineBot-only aggregate for one DSH session
|
|
201
|
+
ctx.effect(() => ctx.webServer.register({
|
|
202
|
+
kind: 'exact',
|
|
203
|
+
path: '/dsh-clinebot/stats/session',
|
|
204
|
+
handler: async (req, res) => {
|
|
205
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
206
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
207
|
+
try {
|
|
208
|
+
const url = new URL(req.url || '/', 'http://localhost')
|
|
209
|
+
const sessionId = String(url.searchParams.get('sessionId') || '').trim().slice(0, 200)
|
|
210
|
+
if (!sessionId) return writeJson(res, 400, { ok: false, error: 'sessionId required' })
|
|
211
|
+
writeJson(res, 200, { ok: true, session: getSessionStats(sessionId) })
|
|
212
|
+
} catch (err) {
|
|
213
|
+
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
}), 'dsh-clinebot: /stats/session')
|
|
217
|
+
|
|
200
218
|
// 5. POST /dsh-clinebot/stats/reset — reset persistent stats (Issue #129)
|
|
201
219
|
ctx.effect(() => ctx.webServer.register({
|
|
202
220
|
kind: 'exact',
|
package/lib/stats-storage.js
CHANGED
|
@@ -336,7 +336,10 @@ export function recordUsage({
|
|
|
336
336
|
inMemoryStats.byHourModel[hourKey][model] = Number(inMemoryStats.byHourModel[hourKey][model] || 0) + 1
|
|
337
337
|
pruneHours(now)
|
|
338
338
|
if (sessionId) {
|
|
339
|
-
|
|
339
|
+
const key = String(sessionId)
|
|
340
|
+
incrementBucket(inMemoryStats.bySession, key, entry)
|
|
341
|
+
const bucket = inMemoryStats.bySession[key]
|
|
342
|
+
if (bucket) bucket.last = { ...entry, sessionId: key }
|
|
340
343
|
pruneSessions(now)
|
|
341
344
|
}
|
|
342
345
|
queueSaveStats(statsPath)
|
|
@@ -346,6 +349,18 @@ export function getStatsSummary() {
|
|
|
346
349
|
return JSON.parse(JSON.stringify(inMemoryStats))
|
|
347
350
|
}
|
|
348
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Return the ClineBot-only aggregate for one DSH session.
|
|
354
|
+
* Returns null when the session has no recorded ClineBot traffic.
|
|
355
|
+
* @param {string} sessionId
|
|
356
|
+
*/
|
|
357
|
+
export function getSessionStats(sessionId) {
|
|
358
|
+
const key = String(sessionId || '').trim()
|
|
359
|
+
if (!key) return null
|
|
360
|
+
const bucket = inMemoryStats.bySession?.[key]
|
|
361
|
+
return bucket ? JSON.parse(JSON.stringify(bucket)) : null
|
|
362
|
+
}
|
|
363
|
+
|
|
349
364
|
export function resetStats(customPath) {
|
|
350
365
|
inMemoryStats = createEmptyStats()
|
|
351
366
|
saveStatsSync(customPath)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "DeepSeek Harness companion for ClineBot / ClinePass: dynamic subscription models sync, quota exhaustion warnings, session metrics, dedicated settings page, live usage limits, and /cline slash-command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|