@goodandready/dsh-clinebot 0.5.30 → 0.5.32
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 +16 -0
- package/lib/client.js +9 -7
- package/lib/key-discovery.js +65 -3
- package/lib/proxy-token.js +2 -2
- package/lib/routes/accounts.js +123 -75
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ 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.32] - 2026-10-19
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
- **API Key Sanitization in Key Auto-Discovery & Import Flow (Issue #246)**:
|
|
12
|
+
- Eliminated raw secret key exposure from `GET /dsh-clinebot/keys/discovered`. Discovered keys are now cached exclusively on the server with a 15-minute TTL under an opaque UUID `id`.
|
|
13
|
+
- Added dedicated server-side import route `POST /dsh-clinebot/keys/discovered/import` that consumes the discovered secret by `id` directly on the server and persists it into DSH Credentials (`saveCredentialKey`) without ever passing secret tokens through client memory, devtools, or network logs.
|
|
14
|
+
- Updated client settings UI to import discovered keys strictly by opaque identifier.
|
|
15
|
+
|
|
16
|
+
## [0.5.31] - 2026-10-18
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **CI / Host Native Adapter Test (#241)**: Mocked `process.env.CLINEBOT_API_KEY` in `test/settings-service.test.js:638` with guaranteed cleanup, ensuring compatibility with newer `@deepseek-ai/dsh-llm-pi-ai` adapter authentication requirements.
|
|
20
|
+
- **DSH Design Token Compliance (#242)**: Replaced hardcoded `#888` fallback with `var(--dsw-alias-label-secondary)` and `rgba(0,0,0,0.15)` with `var(--dsw-elevation-prominent)` in composer cost dock and accounts section.
|
|
21
|
+
- **Design Contract Synchronization (#243)**: Updated `docs/design/DESIGN.md` with specifications for `DashboardDialog` modal, `settings.section`, native web search `ClineBotSearchProvider`, and multimodal KV-cache prompt budgeting.
|
|
22
|
+
- **Repository Hygiene (#244)**: Pruned obsolete `.tgz` release archives from repository root.
|
|
23
|
+
|
|
8
24
|
## [0.5.30] - 2026-10-18
|
|
9
25
|
|
|
10
26
|
### Added
|
package/lib/client.js
CHANGED
|
@@ -1019,9 +1019,9 @@ function AccountsSection({
|
|
|
1019
1019
|
await handleAddAccount({
|
|
1020
1020
|
label: item.source || targetEnv,
|
|
1021
1021
|
apiKeyEnv: targetEnv,
|
|
1022
|
-
|
|
1022
|
+
id: item.id,
|
|
1023
1023
|
})
|
|
1024
|
-
setDiscoveredKeys((prev) => prev.filter((k) => k.
|
|
1024
|
+
setDiscoveredKeys((prev) => prev.filter((k) => k.id !== item.id))
|
|
1025
1025
|
}
|
|
1026
1026
|
}
|
|
1027
1027
|
|
|
@@ -1119,7 +1119,7 @@ function AccountsSection({
|
|
|
1119
1119
|
gap: '8px',
|
|
1120
1120
|
margin: '8px 0 12px 0',
|
|
1121
1121
|
fontSize: '12px',
|
|
1122
|
-
color: 'var(--dsw-alias-label-secondary
|
|
1122
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
1123
1123
|
},
|
|
1124
1124
|
},
|
|
1125
1125
|
React.createElement('span', { style: { fontWeight: 500 } }, t('accounts.mode_label') + ':'),
|
|
@@ -3208,7 +3208,7 @@ function ComposerCostDock(props) {
|
|
|
3208
3208
|
background: 'var(--dsw-alias-bg-layer-3)',
|
|
3209
3209
|
border: '1px solid var(--dsw-alias-border-l2)',
|
|
3210
3210
|
borderRadius: '8px',
|
|
3211
|
-
boxShadow: '
|
|
3211
|
+
boxShadow: 'var(--dsw-elevation-prominent)',
|
|
3212
3212
|
zIndex: 100,
|
|
3213
3213
|
fontSize: '12px',
|
|
3214
3214
|
color: 'var(--dsw-alias-label-primary)',
|
|
@@ -3510,11 +3510,13 @@ function SettingsPage(props) {
|
|
|
3510
3510
|
})
|
|
3511
3511
|
}
|
|
3512
3512
|
|
|
3513
|
-
async function handleAddAccount({ label, apiKeyEnv, apiKey }) {
|
|
3513
|
+
async function handleAddAccount({ label, apiKeyEnv, apiKey, id }) {
|
|
3514
3514
|
await performAction('add-account', async () => {
|
|
3515
|
-
const
|
|
3515
|
+
const endpoint = id ? `${ROUTE_PREFIX}/keys/discovered/import` : `${ROUTE_PREFIX}/accounts`
|
|
3516
|
+
const payload = id ? { id, label, apiKeyEnv } : { label, apiKeyEnv, apiKey }
|
|
3517
|
+
const res = await fetchWithTimeout(endpoint, {
|
|
3516
3518
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
3517
|
-
body: JSON.stringify(
|
|
3519
|
+
body: JSON.stringify(payload),
|
|
3518
3520
|
})
|
|
3519
3521
|
const data = await res.json().catch(() => ({}))
|
|
3520
3522
|
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
|
package/lib/key-discovery.js
CHANGED
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import os from 'node:os'
|
|
4
|
+
import crypto from 'node:crypto'
|
|
4
5
|
|
|
5
6
|
const CLINE_KEY_REGEX = /cline_[A-Za-z0-9_\-]{16,}/g
|
|
7
|
+
const DISCOVERY_CACHE_TTL_MS = 15 * 60 * 1000 // 15 minutes TTL
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
// Server-side cache for discovered keys: id -> { id, key, source, maskedKey, path, expiresAt }
|
|
10
|
+
const discoveredKeysCache = new Map()
|
|
11
|
+
|
|
12
|
+
function cleanupExpired() {
|
|
13
|
+
const now = Date.now()
|
|
14
|
+
for (const [id, item] of discoveredKeysCache.entries()) {
|
|
15
|
+
if (item.expiresAt && item.expiresAt <= now) {
|
|
16
|
+
discoveredKeysCache.delete(id)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getDiscoveredKey(id) {
|
|
22
|
+
if (!id || typeof id !== 'string') return null
|
|
23
|
+
cleanupExpired()
|
|
24
|
+
const item = discoveredKeysCache.get(id)
|
|
25
|
+
return item?.key || null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function consumeDiscoveredKey(id) {
|
|
29
|
+
if (!id || typeof id !== 'string') return null
|
|
30
|
+
cleanupExpired()
|
|
31
|
+
const item = discoveredKeysCache.get(id)
|
|
32
|
+
if (!item) return null
|
|
33
|
+
discoveredKeysCache.delete(id)
|
|
34
|
+
return item.key
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function clearDiscoveredKeysCache() {
|
|
38
|
+
discoveredKeysCache.clear()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function maskApiKey(key) {
|
|
8
42
|
if (!key || typeof key !== 'string') return ''
|
|
9
43
|
if (key.length <= 12) return '••••••••'
|
|
10
44
|
return key.slice(0, 8) + '••••' + key.slice(-4)
|
|
@@ -35,13 +69,41 @@ function scanFileForKeys(filePath, sourceName) {
|
|
|
35
69
|
}
|
|
36
70
|
|
|
37
71
|
export function discoverLocalClineKeys() {
|
|
72
|
+
cleanupExpired()
|
|
38
73
|
const discovered = []
|
|
39
74
|
const seenKeys = new Set()
|
|
40
75
|
|
|
41
76
|
const addKey = (item) => {
|
|
42
77
|
if (!item?.key || seenKeys.has(item.key)) return
|
|
43
78
|
seenKeys.add(item.key)
|
|
44
|
-
|
|
79
|
+
|
|
80
|
+
// Reuse existing ID if already cached
|
|
81
|
+
let existingId = null
|
|
82
|
+
for (const [id, cached] of discoveredKeysCache.entries()) {
|
|
83
|
+
if (cached.key === item.key) {
|
|
84
|
+
existingId = id
|
|
85
|
+
cached.expiresAt = Date.now() + DISCOVERY_CACHE_TTL_MS
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const id = existingId || crypto.randomUUID()
|
|
91
|
+
discoveredKeysCache.set(id, {
|
|
92
|
+
id,
|
|
93
|
+
key: item.key,
|
|
94
|
+
source: item.source,
|
|
95
|
+
maskedKey: item.maskedKey,
|
|
96
|
+
path: item.path,
|
|
97
|
+
expiresAt: Date.now() + DISCOVERY_CACHE_TTL_MS,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Public view: NEVER expose the raw secret key (#246)
|
|
101
|
+
discovered.push({
|
|
102
|
+
id,
|
|
103
|
+
source: item.source,
|
|
104
|
+
maskedKey: item.maskedKey,
|
|
105
|
+
path: item.path,
|
|
106
|
+
})
|
|
45
107
|
}
|
|
46
108
|
|
|
47
109
|
// 1. Check environment variables
|
|
@@ -106,4 +168,4 @@ export function discoverLocalClineKeys() {
|
|
|
106
168
|
} catch {}
|
|
107
169
|
|
|
108
170
|
return discovered
|
|
109
|
-
}
|
|
171
|
+
}
|
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.32'
|
|
18
18
|
try {
|
|
19
|
-
packageVersion = require('../package.json').version || '0.5.
|
|
19
|
+
packageVersion = require('../package.json').version || '0.5.32'
|
|
20
20
|
} catch (err) {
|
|
21
21
|
/* fallback to default version if manifest is unreadable */
|
|
22
22
|
}
|
package/lib/routes/accounts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeJson, readBody } from '../http.js'
|
|
2
|
-
import { discoverLocalClineKeys } from '../key-discovery.js'
|
|
2
|
+
import { discoverLocalClineKeys, consumeDiscoveredKey } from '../key-discovery.js'
|
|
3
3
|
import { assertTrustedSettingsRequest } from '../access.js'
|
|
4
4
|
import { publicConfig, plainConfig, Config } from '../config.js'
|
|
5
5
|
import { upsertPiAiProvider, removePiAiProvider } from '../provider-sync.js'
|
|
@@ -12,10 +12,81 @@ import {
|
|
|
12
12
|
DEFAULT_API_KEY_ENV,
|
|
13
13
|
} from '../cline-client.js'
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
export function registerAccountsRoutes(ctx, { live, syncProviderState, getSettingsApi }) {
|
|
16
|
+
const pendingSecretCleanupAccounts = new Set()
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
async function addAccountInternal({ apiKey, label, apiKeyEnv }) {
|
|
19
|
+
const curr = plainConfig(live())
|
|
20
|
+
const existingAccounts = Array.isArray(curr.accounts) ? [...curr.accounts] : []
|
|
21
|
+
|
|
22
|
+
let targetEnv = String(apiKeyEnv || '').trim()
|
|
23
|
+
if (!targetEnv) {
|
|
24
|
+
let maxN = 1
|
|
25
|
+
for (const acc of existingAccounts) {
|
|
26
|
+
const m = String(acc?.apiKeyEnv || '').match(/^CLINEBOT_API_KEY_(\d+)$/)
|
|
27
|
+
if (m) {
|
|
28
|
+
const num = parseInt(m[1], 10)
|
|
29
|
+
if (num >= maxN) maxN = num + 1
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (curr.apiKeyEnv && curr.apiKeyEnv.startsWith('CLINEBOT_API_KEY_')) {
|
|
33
|
+
const m = curr.apiKeyEnv.match(/^CLINEBOT_API_KEY_(\d+)$/)
|
|
34
|
+
if (m) {
|
|
35
|
+
const num = parseInt(m[1], 10)
|
|
36
|
+
if (num >= maxN) maxN = num + 1
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
targetEnv = `CLINEBOT_API_KEY_${maxN}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!/^CLINEBOT_API_KEY(_[A-Z0-9]+)?$/.test(targetEnv)) {
|
|
43
|
+
const err = new Error(`Disallowed apiKeyEnv: "${targetEnv}". Must match ^CLINEBOT_API_KEY(_[A-Z0-9]+)?$`)
|
|
44
|
+
err.status = 400
|
|
45
|
+
throw err
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const accountLabel = String(label || '').trim() || targetEnv
|
|
49
|
+
|
|
50
|
+
await saveCredentialKey(ctx, targetEnv, apiKey)
|
|
51
|
+
|
|
52
|
+
const idx = existingAccounts.findIndex((a) => a && a.apiKeyEnv === targetEnv)
|
|
53
|
+
const accountEntry = { label: accountLabel, apiKeyEnv: targetEnv }
|
|
54
|
+
if (idx >= 0) {
|
|
55
|
+
existingAccounts[idx] = accountEntry
|
|
56
|
+
} else {
|
|
57
|
+
existingAccounts.push(accountEntry)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const next = plainConfig(Config({ ...curr, accounts: existingAccounts }))
|
|
61
|
+
const settingsApi = getSettingsApi()
|
|
62
|
+
await settingsApi.replace(next)
|
|
63
|
+
await syncProviderState(next)
|
|
64
|
+
|
|
65
|
+
let validation = { ok: false, error: 'Skipped validation: baseUrl is not https' }
|
|
66
|
+
try {
|
|
67
|
+
const parsed = new URL(curr.baseUrl)
|
|
68
|
+
if (parsed.protocol === 'https:') {
|
|
69
|
+
validation = await smokeChat(curr.baseUrl, apiKey, {
|
|
70
|
+
model: curr.defaultModel,
|
|
71
|
+
timeoutMs: 15000,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
validation = { ok: false, error: 'Invalid baseUrl' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
clearUsageCache()
|
|
79
|
+
clearProbeCache()
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
account: accountEntry,
|
|
83
|
+
validated: validation.ok,
|
|
84
|
+
latencyMs: validation.latencyMs,
|
|
85
|
+
validationError: validation.ok ? null : validation.error,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// POST /dsh-clinebot/accounts — add account endpoint (supports DELETE for backward compat)
|
|
19
90
|
ctx.effect(() => ctx.webServer.register({
|
|
20
91
|
kind: 'exact',
|
|
21
92
|
path: '/dsh-clinebot/accounts',
|
|
@@ -39,77 +110,15 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
39
110
|
return writeJson(res, 400, { ok: false, error: 'API key is required' })
|
|
40
111
|
}
|
|
41
112
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!targetEnv) {
|
|
47
|
-
let maxN = 1
|
|
48
|
-
for (const acc of existingAccounts) {
|
|
49
|
-
const m = String(acc?.apiKeyEnv || '').match(/^CLINEBOT_API_KEY_(\d+)$/)
|
|
50
|
-
if (m) {
|
|
51
|
-
const num = parseInt(m[1], 10)
|
|
52
|
-
if (num >= maxN) maxN = num + 1
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (curr.apiKeyEnv && curr.apiKeyEnv.startsWith('CLINEBOT_API_KEY_')) {
|
|
56
|
-
const m = curr.apiKeyEnv.match(/^CLINEBOT_API_KEY_(\d+)$/)
|
|
57
|
-
if (m) {
|
|
58
|
-
const num = parseInt(m[1], 10)
|
|
59
|
-
if (num >= maxN) maxN = num + 1
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
targetEnv = `CLINEBOT_API_KEY_${maxN}`
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!/^CLINEBOT_API_KEY(_[A-Z0-9]+)?$/.test(targetEnv)) {
|
|
66
|
-
return writeJson(res, 400, {
|
|
67
|
-
ok: false,
|
|
68
|
-
error: `Disallowed apiKeyEnv: "${targetEnv}". Must match ^CLINEBOT_API_KEY(_[A-Z0-9]+)?$`,
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const label = String(body.label || '').trim() || targetEnv
|
|
73
|
-
|
|
74
|
-
await saveCredentialKey(ctx, targetEnv, apiKey)
|
|
75
|
-
|
|
76
|
-
const idx = existingAccounts.findIndex((a) => a && a.apiKeyEnv === targetEnv)
|
|
77
|
-
const accountEntry = { label, apiKeyEnv: targetEnv }
|
|
78
|
-
if (idx >= 0) {
|
|
79
|
-
existingAccounts[idx] = accountEntry
|
|
80
|
-
} else {
|
|
81
|
-
existingAccounts.push(accountEntry)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const next = plainConfig(Config({ ...curr, accounts: existingAccounts }))
|
|
85
|
-
await settingsApi.replace(next)
|
|
86
|
-
await syncProviderState(next)
|
|
87
|
-
|
|
88
|
-
let validation = { ok: false, error: 'Skipped validation: baseUrl is not https' }
|
|
89
|
-
try {
|
|
90
|
-
const parsed = new URL(curr.baseUrl)
|
|
91
|
-
if (parsed.protocol === 'https:') {
|
|
92
|
-
validation = await smokeChat(curr.baseUrl, apiKey, {
|
|
93
|
-
model: curr.defaultModel,
|
|
94
|
-
timeoutMs: 15000,
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
|
-
} catch {
|
|
98
|
-
validation = { ok: false, error: 'Invalid baseUrl' }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
clearUsageCache()
|
|
102
|
-
clearProbeCache()
|
|
103
|
-
|
|
104
|
-
writeJson(res, 200, {
|
|
105
|
-
ok: true,
|
|
106
|
-
account: accountEntry,
|
|
107
|
-
validated: validation.ok,
|
|
108
|
-
latencyMs: validation.latencyMs,
|
|
109
|
-
validationError: validation.ok ? null : validation.error,
|
|
113
|
+
const result = await addAccountInternal({
|
|
114
|
+
apiKey,
|
|
115
|
+
label: body.label,
|
|
116
|
+
apiKeyEnv: body.apiKeyEnv,
|
|
110
117
|
})
|
|
118
|
+
writeJson(res, 200, { ok: true, ...result })
|
|
111
119
|
} catch (err) {
|
|
112
|
-
|
|
120
|
+
const status = typeof err?.status === 'number' ? err.status : 500
|
|
121
|
+
writeJson(res, status, { ok: false, error: String(err?.message || err) })
|
|
113
122
|
}
|
|
114
123
|
},
|
|
115
124
|
}), 'dsh-clinebot: /accounts')
|
|
@@ -338,7 +347,7 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
338
347
|
},
|
|
339
348
|
}), 'dsh-clinebot: /unregister')
|
|
340
349
|
|
|
341
|
-
// GET /dsh-clinebot/keys/discovered — scan local Cline CLI and VS Code extension keys (#232)
|
|
350
|
+
// GET /dsh-clinebot/keys/discovered — scan local Cline CLI and VS Code extension keys (#232, #246)
|
|
342
351
|
ctx.effect(() => ctx.webServer.register({
|
|
343
352
|
kind: 'exact',
|
|
344
353
|
path: '/dsh-clinebot/keys/discovered',
|
|
@@ -353,4 +362,43 @@ export function registerAccountsRoutes(ctx, { live, getSettingsApi, syncProvider
|
|
|
353
362
|
}
|
|
354
363
|
},
|
|
355
364
|
}), 'dsh-clinebot: /keys/discovered')
|
|
356
|
-
|
|
365
|
+
|
|
366
|
+
// POST /dsh-clinebot/keys/discovered/import — safely import discovered key without exposing secret (#246)
|
|
367
|
+
ctx.effect(() => ctx.webServer.register({
|
|
368
|
+
kind: 'exact',
|
|
369
|
+
path: '/dsh-clinebot/keys/discovered/import',
|
|
370
|
+
handler: async (req, res) => {
|
|
371
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
372
|
+
if (!assertTrustedSettingsRequest(req, res, ctx)) return
|
|
373
|
+
const settingsApi = getSettingsApi()
|
|
374
|
+
if (!settingsApi || typeof settingsApi.replace !== 'function') {
|
|
375
|
+
return writeJson(res, 503, { ok: false, error: 'Settings service unavailable or read-only' })
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const bodyBuf = await readBody(req)
|
|
379
|
+
let body = {}
|
|
380
|
+
try { body = JSON.parse(bodyBuf.toString('utf8')) } catch { body = {} }
|
|
381
|
+
|
|
382
|
+
const id = String(body.id || '').trim()
|
|
383
|
+
if (!id) {
|
|
384
|
+
return writeJson(res, 400, { ok: false, error: 'Discovered key id is required' })
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const apiKey = consumeDiscoveredKey(id)
|
|
388
|
+
if (!apiKey) {
|
|
389
|
+
return writeJson(res, 404, { ok: false, error: 'Discovered key not found or expired' })
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const result = await addAccountInternal({
|
|
393
|
+
apiKey,
|
|
394
|
+
label: body.label,
|
|
395
|
+
apiKeyEnv: body.apiKeyEnv,
|
|
396
|
+
})
|
|
397
|
+
writeJson(res, 200, { ok: true, ...result })
|
|
398
|
+
} catch (err) {
|
|
399
|
+
const status = typeof err?.status === 'number' ? err.status : 500
|
|
400
|
+
writeJson(res, status, { ok: false, error: String(err?.message || err) })
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
}), 'dsh-clinebot: /keys/discovered/import')
|
|
404
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.32",
|
|
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",
|