@goodandready/dsh-clinebot 0.5.28 → 0.5.29
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 +7 -0
- package/lib/client.js +252 -0
- package/lib/key-discovery.js +109 -0
- package/lib/proxy-token.js +2 -2
- package/lib/routes/accounts.js +17 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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.5.29] - 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Composer Session Cost Widget (`conversation.composer.dock` slot) (Issue #231)**: Added live token & cost indicator pill inside the chat composer dock. Displays real-time estimated dollar cost (`≈ $0.00X`) and token volume (prompt, completion, reasoning) with an interactive popover showing full pricing breakdown.
|
|
12
|
+
- **Local Key Auto-Discovery (`lib/key-discovery.js`) (Issue #232)**: Automatically scans local Cline CLI configuration (`~/.cline/`, `~/.config/cline/`, `~/.cline.json`), VS Code and Cursor extension storage (`saoudrizwan.claude-dev`), and environment variables (`CLINE_API_KEY`, `CLINEPASS_API_KEY`) to discover existing keys.
|
|
13
|
+
- **Discovered Keys API & Import UI (Issue #232)**: Added endpoint `GET /dsh-clinebot/keys/discovered` with secure secret masking and one-click import banner in `AccountsSection` to seamlessly add discovered keys into the DSH credentials and accounts pool.
|
|
14
|
+
|
|
8
15
|
## [0.5.28] - 2026-10-17
|
|
9
16
|
|
|
10
17
|
### Added
|
package/lib/client.js
CHANGED
|
@@ -241,6 +241,16 @@ const en = {
|
|
|
241
241
|
'card.save_key': 'Save Key',
|
|
242
242
|
'card.key_saved': 'Key saved',
|
|
243
243
|
'card.models_active': '{count} active models',
|
|
244
|
+
'composer.cost_title': 'Estimated Session Cost',
|
|
245
|
+
'composer.cost_breakdown': 'Token & Cost Breakdown',
|
|
246
|
+
'composer.prompt_tok': 'Prompt Tokens',
|
|
247
|
+
'composer.completion_tok': 'Completion Tokens',
|
|
248
|
+
'composer.reasoning_tok': 'Reasoning Tokens',
|
|
249
|
+
'composer.est_cost': 'Estimated Cost',
|
|
250
|
+
'discovery.title': 'Discovered Local Keys',
|
|
251
|
+
'discovery.import_btn': 'Import from {source}',
|
|
252
|
+
'discovery.imported': 'Imported to accounts pool',
|
|
253
|
+
'discovery.scanning': 'Scanning for local Cline keys…',
|
|
244
254
|
}
|
|
245
255
|
|
|
246
256
|
const zh = {
|
|
@@ -446,6 +456,16 @@ const en = {
|
|
|
446
456
|
'card.save_key': '保存密钥',
|
|
447
457
|
'card.key_saved': '密钥已保存',
|
|
448
458
|
'card.models_active': '{count} 个可用模型',
|
|
459
|
+
'composer.cost_title': '预估会话成本',
|
|
460
|
+
'composer.cost_breakdown': 'Token 与费用明细',
|
|
461
|
+
'composer.prompt_tok': '提示词 Token',
|
|
462
|
+
'composer.completion_tok': '补全 Token',
|
|
463
|
+
'composer.reasoning_tok': '思考推理 Token',
|
|
464
|
+
'composer.est_cost': '预估花费',
|
|
465
|
+
'discovery.title': '发现本地密钥',
|
|
466
|
+
'discovery.import_btn': '从 {source} 导入',
|
|
467
|
+
'discovery.imported': '已成功导入账户池',
|
|
468
|
+
'discovery.scanning': '正在扫描本地 Cline 密钥…',
|
|
449
469
|
}
|
|
450
470
|
|
|
451
471
|
function makeT(dict, fallback) {
|
|
@@ -950,6 +970,40 @@ function AccountsSection({
|
|
|
950
970
|
const [deleteSecret, setDeleteSecret] = React.useState(true)
|
|
951
971
|
const [routeModel, setRouteModel] = React.useState('')
|
|
952
972
|
const [routeAccount, setRouteAccount] = React.useState('')
|
|
973
|
+
const [discoveredKeys, setDiscoveredKeys] = React.useState([])
|
|
974
|
+
|
|
975
|
+
React.useEffect(() => {
|
|
976
|
+
let active = true
|
|
977
|
+
fetchWithTimeout(`${ROUTE_PREFIX}/keys/discovered`, { cache: 'no-store' })
|
|
978
|
+
.then((r) => r.json())
|
|
979
|
+
.then((data) => {
|
|
980
|
+
if (active && data?.ok && Array.isArray(data.discovered)) {
|
|
981
|
+
setDiscoveredKeys(data.discovered)
|
|
982
|
+
}
|
|
983
|
+
})
|
|
984
|
+
.catch(() => {})
|
|
985
|
+
return () => { active = false }
|
|
986
|
+
}, [])
|
|
987
|
+
|
|
988
|
+
async function handleImportDiscovered(item) {
|
|
989
|
+
let max = 1
|
|
990
|
+
for (const a of (status.accounts || [])) {
|
|
991
|
+
const m = String(a.apiKeyEnv || '').match(/^CLINEBOT_API_KEY_(\d+)$/)
|
|
992
|
+
if (m) {
|
|
993
|
+
const n = parseInt(m[1], 10)
|
|
994
|
+
if (n >= max) max = n + 1
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const targetEnv = `CLINEBOT_API_KEY_${max}`
|
|
998
|
+
if (typeof handleAddAccount === 'function') {
|
|
999
|
+
await handleAddAccount({
|
|
1000
|
+
label: item.source || targetEnv,
|
|
1001
|
+
apiKeyEnv: targetEnv,
|
|
1002
|
+
apiKey: item.key,
|
|
1003
|
+
})
|
|
1004
|
+
setDiscoveredKeys((prev) => prev.filter((k) => k.key !== item.key))
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
953
1007
|
|
|
954
1008
|
function handleOpenAdd() {
|
|
955
1009
|
let max = 1
|
|
@@ -1011,6 +1065,31 @@ function AccountsSection({
|
|
|
1011
1065
|
: null
|
|
1012
1066
|
),
|
|
1013
1067
|
React.createElement('div', { className: 'cb-section-desc' }, t('accounts.desc')),
|
|
1068
|
+
discoveredKeys.length > 0
|
|
1069
|
+
? React.createElement(
|
|
1070
|
+
'div',
|
|
1071
|
+
{ className: 'cb-alert-ok', style: { display: 'flex', flexDirection: 'column', gap: '6px', margin: '6px 0' } },
|
|
1072
|
+
React.createElement('div', { style: { fontWeight: 600 } }, `🔍 ${t('discovery.title')}`),
|
|
1073
|
+
discoveredKeys.map((item, idx) =>
|
|
1074
|
+
React.createElement(
|
|
1075
|
+
'div',
|
|
1076
|
+
{ key: idx, style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
1077
|
+
React.createElement('span', null, `${item.maskedKey} (${item.source})`),
|
|
1078
|
+
React.createElement(
|
|
1079
|
+
'button',
|
|
1080
|
+
{
|
|
1081
|
+
type: 'button',
|
|
1082
|
+
className: 'cb-btn',
|
|
1083
|
+
style: { fontSize: '11px', padding: '2px 8px' },
|
|
1084
|
+
disabled: !!busy,
|
|
1085
|
+
onClick: () => handleImportDiscovered(item),
|
|
1086
|
+
},
|
|
1087
|
+
t('discovery.import_btn', { source: item.source })
|
|
1088
|
+
)
|
|
1089
|
+
)
|
|
1090
|
+
)
|
|
1091
|
+
)
|
|
1092
|
+
: null,
|
|
1014
1093
|
React.createElement(
|
|
1015
1094
|
'div',
|
|
1016
1095
|
{
|
|
@@ -3137,6 +3216,165 @@ function ModelsProviderCard(props) {
|
|
|
3137
3216
|
)
|
|
3138
3217
|
}
|
|
3139
3218
|
|
|
3219
|
+
// --- src/client/components/composer-cost.js ---
|
|
3220
|
+
function formatTokens(count) {
|
|
3221
|
+
const n = Number(count) || 0
|
|
3222
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'
|
|
3223
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'k'
|
|
3224
|
+
return String(n)
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
function calculateCost(tokens, modelId = '') {
|
|
3228
|
+
const isPremium = String(modelId).includes('claude') || String(modelId).includes('pro')
|
|
3229
|
+
// Rates per 1M tokens
|
|
3230
|
+
const promptRate = isPremium ? 3.0 : 0.15
|
|
3231
|
+
const completionRate = isPremium ? 15.0 : 0.60
|
|
3232
|
+
const reasoningRate = isPremium ? 15.0 : 0.60
|
|
3233
|
+
|
|
3234
|
+
const pTok = Number(tokens?.promptTokens || tokens?.inputTokens || 0)
|
|
3235
|
+
const cTok = Number(tokens?.completionTokens || tokens?.outputTokens || 0)
|
|
3236
|
+
const rTok = Number(tokens?.reasoningTokens || 0)
|
|
3237
|
+
|
|
3238
|
+
const cost = (pTok * promptRate + cTok * completionRate + rTok * reasoningRate) / 1000000
|
|
3239
|
+
return { cost, pTok, cTok, rTok, total: pTok + cTok + rTok }
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
function ComposerCostDock(props) {
|
|
3243
|
+
const t = props?.t || makeT(en, en)
|
|
3244
|
+
const [stats, setStats] = React.useState(null)
|
|
3245
|
+
const [showPopover, setShowPopover] = React.useState(false)
|
|
3246
|
+
|
|
3247
|
+
// Use projection hook if passed by DSH composer dock, or poll session stats
|
|
3248
|
+
const projectedTokens = typeof props?.useProjection === 'function' ? props.useProjection('tokenUsage') : null
|
|
3249
|
+
const projectedModel = typeof props?.useProjection === 'function' ? props.useProjection('modelSelection') : null
|
|
3250
|
+
|
|
3251
|
+
React.useEffect(() => {
|
|
3252
|
+
if (projectedTokens) return
|
|
3253
|
+
let active = true
|
|
3254
|
+
const loadStats = async () => {
|
|
3255
|
+
try {
|
|
3256
|
+
const res = await fetchWithTimeout(`${ROUTE_PREFIX}/stats`, { cache: 'no-store' })
|
|
3257
|
+
if (!res.ok) return
|
|
3258
|
+
const data = await res.json().catch(() => null)
|
|
3259
|
+
if (active && data?.stats?.summary) {
|
|
3260
|
+
setStats(data.stats.summary)
|
|
3261
|
+
}
|
|
3262
|
+
} catch {
|
|
3263
|
+
// quiet
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
loadStats()
|
|
3267
|
+
const timer = setTimeout(loadStats, 30000)
|
|
3268
|
+
return () => { active = false; clearTimeout(timer) }
|
|
3269
|
+
}, [projectedTokens])
|
|
3270
|
+
|
|
3271
|
+
const tokens = projectedTokens || {
|
|
3272
|
+
promptTokens: stats?.promptTokens || 0,
|
|
3273
|
+
completionTokens: stats?.completionTokens || 0,
|
|
3274
|
+
reasoningTokens: stats?.reasoningTokens || 0,
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
const modelId = projectedModel?.id || ''
|
|
3278
|
+
const { cost, pTok, cTok, rTok, total } = calculateCost(tokens, modelId)
|
|
3279
|
+
|
|
3280
|
+
if (total === 0 && !stats) {
|
|
3281
|
+
return null
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
const costDisplay = cost < 0.001 && cost > 0 ? '< $0.001' : `$${cost.toFixed(3)}`
|
|
3285
|
+
|
|
3286
|
+
return React.createElement(
|
|
3287
|
+
'div',
|
|
3288
|
+
{
|
|
3289
|
+
className: 'cb-composer-cost-dock',
|
|
3290
|
+
'data-composer-session-cost': 'true',
|
|
3291
|
+
style: { position: 'relative', display: 'inline-flex', alignItems: 'center' },
|
|
3292
|
+
},
|
|
3293
|
+
React.createElement(
|
|
3294
|
+
'button',
|
|
3295
|
+
{
|
|
3296
|
+
type: 'button',
|
|
3297
|
+
className: 'cb-btn',
|
|
3298
|
+
style: {
|
|
3299
|
+
padding: '2px 8px',
|
|
3300
|
+
fontSize: '11px',
|
|
3301
|
+
height: '24px',
|
|
3302
|
+
borderRadius: '999px',
|
|
3303
|
+
display: 'inline-flex',
|
|
3304
|
+
alignItems: 'center',
|
|
3305
|
+
gap: '4px',
|
|
3306
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
3307
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
3308
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
3309
|
+
cursor: 'pointer',
|
|
3310
|
+
},
|
|
3311
|
+
title: `${t('composer.cost_title')}: ${costDisplay} (${formatTokens(total)} tokens)`,
|
|
3312
|
+
onClick: () => setShowPopover(!showPopover),
|
|
3313
|
+
},
|
|
3314
|
+
React.createElement('span', { style: { fontSize: '12px' } }, '🪙'),
|
|
3315
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, `≈ ${costDisplay}`),
|
|
3316
|
+
React.createElement('span', { style: { opacity: 0.7 } }, `· ${formatTokens(total)} tok`)
|
|
3317
|
+
),
|
|
3318
|
+
showPopover
|
|
3319
|
+
? React.createElement(
|
|
3320
|
+
'div',
|
|
3321
|
+
{
|
|
3322
|
+
className: 'cb-popover',
|
|
3323
|
+
style: {
|
|
3324
|
+
position: 'absolute',
|
|
3325
|
+
bottom: '100%',
|
|
3326
|
+
left: 0,
|
|
3327
|
+
marginBottom: '6px',
|
|
3328
|
+
minWidth: '200px',
|
|
3329
|
+
padding: '10px 12px',
|
|
3330
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
3331
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
3332
|
+
borderRadius: '8px',
|
|
3333
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
3334
|
+
zIndex: 100,
|
|
3335
|
+
fontSize: '12px',
|
|
3336
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
3337
|
+
display: 'flex',
|
|
3338
|
+
flexDirection: 'column',
|
|
3339
|
+
gap: '6px',
|
|
3340
|
+
},
|
|
3341
|
+
},
|
|
3342
|
+
React.createElement(
|
|
3343
|
+
'div',
|
|
3344
|
+
{ style: { fontWeight: 600, borderBottom: '1px solid var(--dsw-alias-border-l2)', paddingBottom: '4px' } },
|
|
3345
|
+
t('composer.cost_breakdown')
|
|
3346
|
+
),
|
|
3347
|
+
React.createElement(
|
|
3348
|
+
'div',
|
|
3349
|
+
{ style: { display: 'flex', justifyContent: 'space-between' } },
|
|
3350
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('composer.prompt_tok')),
|
|
3351
|
+
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, formatTokens(pTok))
|
|
3352
|
+
),
|
|
3353
|
+
React.createElement(
|
|
3354
|
+
'div',
|
|
3355
|
+
{ style: { display: 'flex', justifyContent: 'space-between' } },
|
|
3356
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('composer.completion_tok')),
|
|
3357
|
+
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, formatTokens(cTok))
|
|
3358
|
+
),
|
|
3359
|
+
rTok > 0
|
|
3360
|
+
? React.createElement(
|
|
3361
|
+
'div',
|
|
3362
|
+
{ style: { display: 'flex', justifyContent: 'space-between' } },
|
|
3363
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('composer.reasoning_tok')),
|
|
3364
|
+
React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, formatTokens(rTok))
|
|
3365
|
+
)
|
|
3366
|
+
: null,
|
|
3367
|
+
React.createElement(
|
|
3368
|
+
'div',
|
|
3369
|
+
{ style: { display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '4px', fontWeight: 600 } },
|
|
3370
|
+
React.createElement('span', null, t('composer.est_cost')),
|
|
3371
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-brand-primary)' } }, `≈ ${costDisplay}`)
|
|
3372
|
+
)
|
|
3373
|
+
)
|
|
3374
|
+
: null
|
|
3375
|
+
)
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3140
3378
|
// --- src/client/settings-page.js ---
|
|
3141
3379
|
function SettingsPage(props) {
|
|
3142
3380
|
const ctx = props?.ctx
|
|
@@ -3843,6 +4081,20 @@ function apply(ctx) {
|
|
|
3843
4081
|
)
|
|
3844
4082
|
)
|
|
3845
4083
|
}
|
|
4084
|
+
|
|
4085
|
+
// Composer Session Cost Dock (conversation.composer.dock slot)
|
|
4086
|
+
registerSlotWhenReady('conversation.composer.dock', () =>
|
|
4087
|
+
ctx.slots.register(
|
|
4088
|
+
{
|
|
4089
|
+
name: 'conversation.composer.dock',
|
|
4090
|
+
id: 'clinebot-cost',
|
|
4091
|
+
order: 10,
|
|
4092
|
+
locale: NS,
|
|
4093
|
+
inject: () => ({ ctx }),
|
|
4094
|
+
},
|
|
4095
|
+
(props) => React.createElement(ErrorBoundary, { t: props && props.t }, React.createElement(ComposerCostDock, { ...props, ctx: (props && props.ctx) || ctx }))
|
|
4096
|
+
)
|
|
4097
|
+
)
|
|
3846
4098
|
}
|
|
3847
4099
|
|
|
3848
4100
|
module.exports = { apply, inject: ['slots', 'locale', 'configForms'] }
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
|
|
5
|
+
const CLINE_KEY_REGEX = /cline_[A-Za-z0-9_\-]{16,}/g
|
|
6
|
+
|
|
7
|
+
function maskApiKey(key) {
|
|
8
|
+
if (!key || typeof key !== 'string') return ''
|
|
9
|
+
if (key.length <= 12) return '••••••••'
|
|
10
|
+
return key.slice(0, 8) + '••••' + key.slice(-4)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function scanFileForKeys(filePath, sourceName) {
|
|
14
|
+
const results = []
|
|
15
|
+
try {
|
|
16
|
+
if (!fs.existsSync(filePath)) return results
|
|
17
|
+
const stat = fs.statSync(filePath)
|
|
18
|
+
if (!stat.isFile() || stat.size > 5 * 1024 * 1024) return results
|
|
19
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
20
|
+
const matches = content.match(CLINE_KEY_REGEX)
|
|
21
|
+
if (matches) {
|
|
22
|
+
for (const m of matches) {
|
|
23
|
+
results.push({
|
|
24
|
+
source: sourceName,
|
|
25
|
+
key: m,
|
|
26
|
+
maskedKey: maskApiKey(m),
|
|
27
|
+
path: filePath,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// quiet on unreadable files
|
|
33
|
+
}
|
|
34
|
+
return results
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function discoverLocalClineKeys() {
|
|
38
|
+
const discovered = []
|
|
39
|
+
const seenKeys = new Set()
|
|
40
|
+
|
|
41
|
+
const addKey = (item) => {
|
|
42
|
+
if (!item?.key || seenKeys.has(item.key)) return
|
|
43
|
+
seenKeys.add(item.key)
|
|
44
|
+
discovered.push(item)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 1. Check environment variables
|
|
48
|
+
for (const envName of ['CLINE_API_KEY', 'CLINEPASS_API_KEY']) {
|
|
49
|
+
const val = process.env[envName]
|
|
50
|
+
if (val && typeof val === 'string' && val.startsWith('cline_')) {
|
|
51
|
+
addKey({
|
|
52
|
+
source: `Environment (${envName})`,
|
|
53
|
+
key: val,
|
|
54
|
+
maskedKey: maskApiKey(val),
|
|
55
|
+
path: `$${envName}`,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. Candidate directories & file paths across OSes
|
|
61
|
+
const home = os.homedir()
|
|
62
|
+
const candidateFiles = [
|
|
63
|
+
// CLI configs
|
|
64
|
+
{ path: path.join(home, '.cline', 'config.json'), source: 'Cline CLI' },
|
|
65
|
+
{ path: path.join(home, '.cline', 'data', 'settings.json'), source: 'Cline CLI Data' },
|
|
66
|
+
{ path: path.join(home, '.cline', 'settings.json'), source: 'Cline CLI Settings' },
|
|
67
|
+
{ path: path.join(home, '.config', 'cline', 'config.json'), source: 'Cline Config' },
|
|
68
|
+
{ path: path.join(home, '.cline.json'), source: 'Cline Dotfile' },
|
|
69
|
+
|
|
70
|
+
// VS Code Extension Global Storage (Linux, macOS, Windows)
|
|
71
|
+
{ path: path.join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_custom_modes.json'), source: 'VS Code Extension' },
|
|
72
|
+
{ path: path.join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'state.vscdb'), source: 'VS Code Extension Storage' },
|
|
73
|
+
{ path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_custom_modes.json'), source: 'VS Code Extension (macOS)' },
|
|
74
|
+
{ path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_custom_modes.json'), source: 'VS Code Extension (Windows)' },
|
|
75
|
+
|
|
76
|
+
// Cursor Extension Global Storage
|
|
77
|
+
{ path: path.join(home, '.config', 'Cursor', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_custom_modes.json'), source: 'Cursor Extension' },
|
|
78
|
+
{ path: path.join(home, '.config', 'Cursor', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'state.vscdb'), source: 'Cursor Extension Storage' },
|
|
79
|
+
{ path: path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_custom_modes.json'), source: 'Cursor Extension (macOS)' },
|
|
80
|
+
{ path: path.join(home, 'AppData', 'Roaming', 'Cursor', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_custom_modes.json'), source: 'Cursor Extension (Windows)' },
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
for (const candidate of candidateFiles) {
|
|
84
|
+
const hits = scanFileForKeys(candidate.path, candidate.source)
|
|
85
|
+
for (const h of hits) {
|
|
86
|
+
addKey(h)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Also check if any file in ~/.cline/ contains cline_ keys
|
|
91
|
+
const clineDir = path.join(home, '.cline')
|
|
92
|
+
try {
|
|
93
|
+
if (fs.existsSync(clineDir)) {
|
|
94
|
+
const entries = fs.readdirSync(clineDir)
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const full = path.join(clineDir, entry)
|
|
97
|
+
try {
|
|
98
|
+
const st = fs.statSync(full)
|
|
99
|
+
if (st.isFile()) {
|
|
100
|
+
const hits = scanFileForKeys(full, 'Cline CLI (~/.cline)')
|
|
101
|
+
for (const h of hits) addKey(h)
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {}
|
|
107
|
+
|
|
108
|
+
return discovered
|
|
109
|
+
}
|
package/lib/proxy-token.js
CHANGED
|
@@ -14,9 +14,9 @@ export const LOCAL_PROXY_KEY_ENV = 'CLINEBOT_LOCAL_PROXY_TOKEN'
|
|
|
14
14
|
let cachedToken = null
|
|
15
15
|
|
|
16
16
|
const require = createRequire(import.meta.url)
|
|
17
|
-
let packageVersion = '0.5.
|
|
17
|
+
let packageVersion = '0.5.29'
|
|
18
18
|
try {
|
|
19
|
-
packageVersion = require('../package.json').version || '0.5.
|
|
19
|
+
packageVersion = require('../package.json').version || '0.5.29'
|
|
20
20
|
} catch (err) {
|
|
21
21
|
/* fallback to default version if manifest is unreadable */
|
|
22
22
|
}
|
package/lib/routes/accounts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { writeJson, readBody } from '../http.js'
|
|
2
|
+
import { discoverLocalClineKeys } from '../key-discovery.js'
|
|
2
3
|
import { assertTrustedSettingsRequest } from '../access.js'
|
|
3
4
|
import { publicConfig, plainConfig, Config } from '../config.js'
|
|
4
5
|
import { upsertPiAiProvider, removePiAiProvider } from '../provider-sync.js'
|
|
@@ -336,4 +337,20 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
336
337
|
}
|
|
337
338
|
},
|
|
338
339
|
}), 'dsh-clinebot: /unregister')
|
|
340
|
+
|
|
341
|
+
// GET /dsh-clinebot/keys/discovered — scan local Cline CLI and VS Code extension keys (#232)
|
|
342
|
+
ctx.effect(() => ctx.webServer.register({
|
|
343
|
+
kind: 'exact',
|
|
344
|
+
path: '/dsh-clinebot/keys/discovered',
|
|
345
|
+
handler: async (req, res) => {
|
|
346
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
347
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
348
|
+
try {
|
|
349
|
+
const discovered = discoverLocalClineKeys()
|
|
350
|
+
writeJson(res, 200, { ok: true, discovered })
|
|
351
|
+
} catch (err) {
|
|
352
|
+
writeJson(res, 500, { ok: false, error: String(err?.message || err) })
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
}), 'dsh-clinebot: /keys/discovered')
|
|
339
356
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.29",
|
|
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",
|