@goodandready/dsh-clinebot 0.5.33 → 0.6.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 +22 -0
- package/README.md +63 -36
- package/README.ru.md +3 -1
- package/README.zh.md +3 -1
- package/lib/adapter-messages.js +237 -0
- package/lib/adapter-stream.js +292 -0
- package/lib/adapter.js +616 -0
- package/lib/client.js +2564 -924
- package/lib/cline-client.js +15 -6
- package/lib/config.js +266 -233
- package/lib/credential-refs.js +104 -84
- package/lib/index.js +569 -502
- package/lib/model-prices.js +117 -0
- package/lib/provider-sync.js +404 -387
- package/lib/proxy-token.js +186 -186
- package/lib/routes/accounts.js +523 -403
- package/lib/routes/models.js +2 -1
- package/lib/routes/settings.js +63 -2
- package/lib/services.js +55 -0
- package/lib/stats-storage.js +364 -166
- package/lib/transport/cline-transport.js +314 -0
- package/lib/tui-settings.js +158 -0
- package/package.json +113 -77
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClinePass OpenAI-compatible transport primitives.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the wire boundary: request construction helpers, SSE framing,
|
|
5
|
+
* usage extraction, timeout/abort handling, and provider error classification.
|
|
6
|
+
* It intentionally contains no account routing or DSH adapter policy.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createRequire } from 'node:module'
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url)
|
|
12
|
+
let packageVersion = '0.6.0'
|
|
13
|
+
try {
|
|
14
|
+
packageVersion = require('../../package.json').version || packageVersion
|
|
15
|
+
} catch {
|
|
16
|
+
/* fallback when package.json is unavailable */
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const USER_AGENT = `@goodandready/dsh-clinebot/${packageVersion}`
|
|
20
|
+
export const DEFAULT_CONNECT_TIMEOUT_MS = 15000
|
|
21
|
+
export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 30000
|
|
22
|
+
export const DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the abort-shaped error every transport caller understands.
|
|
26
|
+
* @param {string} [message]
|
|
27
|
+
* @returns {Error}
|
|
28
|
+
*/
|
|
29
|
+
export function abortError(message = 'Request aborted') {
|
|
30
|
+
const error = new Error(message)
|
|
31
|
+
error.name = 'AbortError'
|
|
32
|
+
error.code = 'ABORTED'
|
|
33
|
+
return error
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {unknown} error
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
export function isAbortError(error) {
|
|
41
|
+
return Boolean(error && (error.name === 'AbortError' || error.code === 'ABORTED'))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Normalize an OpenAI-compatible usage payload into DSH's disjoint token buckets.
|
|
46
|
+
*
|
|
47
|
+
* `prompt_tokens` includes cached input, while DSH expects `inputTokens` to be
|
|
48
|
+
* uncached input only. Cache read/write buckets are reported separately.
|
|
49
|
+
*
|
|
50
|
+
* @param {Record<string, unknown>} usage
|
|
51
|
+
* @returns {{ inputTokens: number, outputTokens: number, totalTokens?: number, cacheReadTokens?: number, cacheWriteTokens?: number, reasoningTokens?: number, promptTokens: number, completionTokens: number }}
|
|
52
|
+
*/
|
|
53
|
+
export function extractUsage(usage) {
|
|
54
|
+
if (!usage || typeof usage !== 'object') {
|
|
55
|
+
return { inputTokens: 0, outputTokens: 0, promptTokens: 0, completionTokens: 0 }
|
|
56
|
+
}
|
|
57
|
+
const promptTokens = Math.max(0, Number(usage.prompt_tokens) || 0)
|
|
58
|
+
const completionTokens = Math.max(0, Number(usage.completion_tokens) || 0)
|
|
59
|
+
const cacheReadTokens = Math.max(0, Number(
|
|
60
|
+
usage.prompt_tokens_details?.cached_tokens ||
|
|
61
|
+
usage.cache_read_input_tokens ||
|
|
62
|
+
usage.cached_tokens ||
|
|
63
|
+
0
|
|
64
|
+
))
|
|
65
|
+
const cacheWriteTokens = Math.max(0, Number(
|
|
66
|
+
usage.prompt_tokens_details?.cache_write_tokens ||
|
|
67
|
+
usage.cache_creation_input_tokens ||
|
|
68
|
+
0
|
|
69
|
+
))
|
|
70
|
+
const reasoningTokens = Math.max(0, Number(
|
|
71
|
+
usage.completion_tokens_details?.reasoning_tokens ||
|
|
72
|
+
usage.reasoning_tokens ||
|
|
73
|
+
usage.prompt_tokens_details?.reasoning_tokens ||
|
|
74
|
+
0
|
|
75
|
+
))
|
|
76
|
+
const inputTokens = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens)
|
|
77
|
+
const totalTokens = Number.isFinite(Number(usage.total_tokens))
|
|
78
|
+
? Math.max(0, Number(usage.total_tokens))
|
|
79
|
+
: promptTokens + completionTokens
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
inputTokens,
|
|
83
|
+
outputTokens: completionTokens,
|
|
84
|
+
...(totalTokens > 0 ? { totalTokens } : {}),
|
|
85
|
+
...(cacheReadTokens > 0 ? { cacheReadTokens } : {}),
|
|
86
|
+
...(cacheWriteTokens > 0 ? { cacheWriteTokens } : {}),
|
|
87
|
+
...(reasoningTokens > 0 ? { reasoningTokens } : {}),
|
|
88
|
+
promptTokens,
|
|
89
|
+
completionTokens,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Classify a non-2xx upstream response into an adapter error code.
|
|
95
|
+
*
|
|
96
|
+
* @param {number} status
|
|
97
|
+
* @param {string} [bodyText]
|
|
98
|
+
* @returns {{ code: string, retryable: boolean, message: string }}
|
|
99
|
+
*/
|
|
100
|
+
export function classifyUpstreamStatus(status, bodyText = '') {
|
|
101
|
+
const detail = String(bodyText || '').slice(0, 500)
|
|
102
|
+
const lower = detail.toLowerCase()
|
|
103
|
+
if (status === 401 || status === 403) {
|
|
104
|
+
return { code: 'AUTH', retryable: false, message: `ClinePass authentication failed (HTTP ${status})${detail ? `: ${detail}` : ''}` }
|
|
105
|
+
}
|
|
106
|
+
if (status === 429) {
|
|
107
|
+
return { code: 'RATE_LIMIT', retryable: true, message: `ClinePass rate limit exceeded (HTTP 429)${detail ? `: ${detail}` : ''}` }
|
|
108
|
+
}
|
|
109
|
+
if (status === 402 || /quota|credit|usage limit|insufficient/i.test(lower)) {
|
|
110
|
+
return { code: 'QUOTA_EXCEEDED', retryable: true, message: `ClinePass quota exhausted (HTTP ${status})${detail ? `: ${detail}` : ''}` }
|
|
111
|
+
}
|
|
112
|
+
if (status === 400 && /context|too long|maximum context|token limit/i.test(lower)) {
|
|
113
|
+
return { code: 'CONTEXT_WINDOW_EXCEEDED', retryable: false, message: `Context window exceeded (HTTP 400)${detail ? `: ${detail}` : ''}` }
|
|
114
|
+
}
|
|
115
|
+
if (status >= 500) {
|
|
116
|
+
return { code: 'PROVIDER_ERROR', retryable: true, message: `ClinePass upstream error (HTTP ${status})${detail ? `: ${detail}` : ''}` }
|
|
117
|
+
}
|
|
118
|
+
return { code: 'UPSTREAM_ERROR', retryable: false, message: `ClinePass request failed (HTTP ${status})${detail ? `: ${detail}` : ''}` }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read a response body with a hard size bound.
|
|
123
|
+
*
|
|
124
|
+
* @param {Response} response
|
|
125
|
+
* @param {{ maxBytes?: number }} [options]
|
|
126
|
+
* @returns {Promise<string>}
|
|
127
|
+
*/
|
|
128
|
+
export async function readResponseText(response, { maxBytes = DEFAULT_MAX_BODY_BYTES } = {}) {
|
|
129
|
+
if (!response?.body) return ''
|
|
130
|
+
const reader = response.body.getReader()
|
|
131
|
+
const decoder = new TextDecoder('utf8')
|
|
132
|
+
let size = 0
|
|
133
|
+
let out = ''
|
|
134
|
+
try {
|
|
135
|
+
while (true) {
|
|
136
|
+
const { done, value } = await reader.read()
|
|
137
|
+
if (done) break
|
|
138
|
+
size += value.byteLength
|
|
139
|
+
if (size > maxBytes) {
|
|
140
|
+
throw new Error(`Upstream response exceeded ${maxBytes} bytes`)
|
|
141
|
+
}
|
|
142
|
+
out += decoder.decode(value, { stream: true })
|
|
143
|
+
}
|
|
144
|
+
out += decoder.decode()
|
|
145
|
+
return out
|
|
146
|
+
} finally {
|
|
147
|
+
try { reader.releaseLock() } catch {}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Whether a response is an SSE stream.
|
|
153
|
+
* @param {Response} response
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
export function isEventStream(response) {
|
|
157
|
+
return String(response?.headers?.get?.('content-type') || '').toLowerCase().includes('text/event-stream')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Send one OpenAI-compatible chat completion request.
|
|
162
|
+
*
|
|
163
|
+
* The connect timeout covers headers only; once headers arrive the caller owns
|
|
164
|
+
* the stream and applies its own idle watchdog.
|
|
165
|
+
*
|
|
166
|
+
* @param {{
|
|
167
|
+
* baseUrl: string,
|
|
168
|
+
* apiKey: string,
|
|
169
|
+
* body: Record<string, unknown>,
|
|
170
|
+
* signal?: AbortSignal,
|
|
171
|
+
* connectTimeoutMs?: number,
|
|
172
|
+
* extraHeaders?: Record<string, string>,
|
|
173
|
+
* }} options
|
|
174
|
+
* @returns {Promise<Response>}
|
|
175
|
+
*/
|
|
176
|
+
export async function requestChatCompletion({
|
|
177
|
+
baseUrl,
|
|
178
|
+
apiKey,
|
|
179
|
+
body,
|
|
180
|
+
signal,
|
|
181
|
+
connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS,
|
|
182
|
+
extraHeaders = {},
|
|
183
|
+
}) {
|
|
184
|
+
const connectCtrl = new AbortController()
|
|
185
|
+
let timedOut = false
|
|
186
|
+
const timer = setTimeout(() => {
|
|
187
|
+
timedOut = true
|
|
188
|
+
connectCtrl.abort(abortError(`Upstream connect timeout after ${connectTimeoutMs}ms`))
|
|
189
|
+
}, Math.max(1, Number(connectTimeoutMs) || DEFAULT_CONNECT_TIMEOUT_MS))
|
|
190
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
191
|
+
const combined = signal
|
|
192
|
+
? AbortSignal.any([signal, connectCtrl.signal])
|
|
193
|
+
: connectCtrl.signal
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
return await fetch(`${String(baseUrl).replace(/\/+$/, '')}/chat/completions`, {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: {
|
|
199
|
+
'Content-Type': 'application/json',
|
|
200
|
+
Authorization: `Bearer ${apiKey}`,
|
|
201
|
+
Accept: 'text/event-stream, application/json',
|
|
202
|
+
'User-Agent': USER_AGENT,
|
|
203
|
+
...extraHeaders,
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify(body),
|
|
206
|
+
signal: combined,
|
|
207
|
+
keepalive: true,
|
|
208
|
+
})
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (timedOut) {
|
|
211
|
+
const timeoutError = new Error(`Upstream connect timeout after ${connectTimeoutMs}ms`)
|
|
212
|
+
timeoutError.code = 'CONNECT_TIMEOUT'
|
|
213
|
+
timeoutError.cause = error
|
|
214
|
+
throw timeoutError
|
|
215
|
+
}
|
|
216
|
+
throw error
|
|
217
|
+
} finally {
|
|
218
|
+
clearTimeout(timer)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Parse one SSE block's `data:` payload.
|
|
224
|
+
* @param {string} raw
|
|
225
|
+
* @returns {Record<string, unknown> | { done: true } | { raw: string } | undefined}
|
|
226
|
+
*/
|
|
227
|
+
function parseSseBlock(raw) {
|
|
228
|
+
const lines = String(raw || '').replace(/\r\n/g, '\n').split('\n')
|
|
229
|
+
const dataLines = []
|
|
230
|
+
for (const line of lines) {
|
|
231
|
+
if (!line.startsWith('data:')) continue
|
|
232
|
+
dataLines.push(line.slice(5).replace(/^ /, ''))
|
|
233
|
+
}
|
|
234
|
+
if (dataLines.length === 0) return undefined
|
|
235
|
+
const data = dataLines.join('\n').trim()
|
|
236
|
+
if (data === '') return undefined
|
|
237
|
+
if (data === '[DONE]') return { done: true }
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(data)
|
|
240
|
+
} catch {
|
|
241
|
+
return { raw: data }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Read SSE events from an OpenAI-compatible response as parsed objects.
|
|
247
|
+
*
|
|
248
|
+
* Yields `{ done: true }` for `[DONE]`, parsed JSON objects otherwise, and
|
|
249
|
+
* `{ raw }` for unrecognized non-JSON payloads. Throws an abort-shaped error
|
|
250
|
+
* when the caller signal aborts, and a `STREAM_IDLE_TIMEOUT` error when no
|
|
251
|
+
* bytes arrive for `idleTimeoutMs`.
|
|
252
|
+
*
|
|
253
|
+
* @param {Response} response
|
|
254
|
+
* @param {{ signal?: AbortSignal, idleTimeoutMs?: number }} [options]
|
|
255
|
+
* @returns {AsyncGenerator<Record<string, unknown> | { done: true } | { raw: string }>}
|
|
256
|
+
*/
|
|
257
|
+
export async function* readSseEvents(response, {
|
|
258
|
+
signal,
|
|
259
|
+
idleTimeoutMs = DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
|
260
|
+
} = {}) {
|
|
261
|
+
if (!response?.body) return
|
|
262
|
+
const reader = response.body.getReader()
|
|
263
|
+
const decoder = new TextDecoder('utf8')
|
|
264
|
+
let buffer = ''
|
|
265
|
+
|
|
266
|
+
async function readWithIdle() {
|
|
267
|
+
let timer = null
|
|
268
|
+
try {
|
|
269
|
+
return await Promise.race([
|
|
270
|
+
reader.read(),
|
|
271
|
+
new Promise((_, reject) => {
|
|
272
|
+
timer = setTimeout(() => {
|
|
273
|
+
const error = new Error(`Stream idle timeout after ${idleTimeoutMs}ms`)
|
|
274
|
+
error.code = 'STREAM_IDLE_TIMEOUT'
|
|
275
|
+
reject(error)
|
|
276
|
+
}, Math.max(1, Number(idleTimeoutMs) || DEFAULT_STREAM_IDLE_TIMEOUT_MS))
|
|
277
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
278
|
+
}),
|
|
279
|
+
])
|
|
280
|
+
} finally {
|
|
281
|
+
if (timer) clearTimeout(timer)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
while (true) {
|
|
287
|
+
if (signal?.aborted) {
|
|
288
|
+
try { await reader.cancel('aborted') } catch {}
|
|
289
|
+
throw abortError()
|
|
290
|
+
}
|
|
291
|
+
const { done, value } = await readWithIdle()
|
|
292
|
+
if (done) break
|
|
293
|
+
buffer += decoder.decode(value, { stream: true })
|
|
294
|
+
buffer = buffer.replace(/\r\n/g, '\n')
|
|
295
|
+
let boundary = buffer.indexOf('\n\n')
|
|
296
|
+
while (boundary >= 0) {
|
|
297
|
+
const block = buffer.slice(0, boundary)
|
|
298
|
+
buffer = buffer.slice(boundary + 2)
|
|
299
|
+
const parsed = parseSseBlock(block)
|
|
300
|
+
if (parsed !== undefined) yield parsed
|
|
301
|
+
boundary = buffer.indexOf('\n\n')
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
buffer += decoder.decode()
|
|
305
|
+
const tail = parseSseBlock(buffer)
|
|
306
|
+
if (tail !== undefined) yield tail
|
|
307
|
+
} catch (error) {
|
|
308
|
+
try { await reader.cancel(error?.message || 'stream error') } catch {}
|
|
309
|
+
if (signal?.aborted) throw abortError()
|
|
310
|
+
throw error
|
|
311
|
+
} finally {
|
|
312
|
+
try { reader.releaseLock() } catch {}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-TUI settings section.
|
|
3
|
+
*
|
|
4
|
+
* The TUI owns its settings screen and only accepts declarations. The API key
|
|
5
|
+
* is a secret field written through the credentials seam; every other field is
|
|
6
|
+
* a normal settings path in the `dsh-clinebot` namespace.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { publicConfig } from './config.js'
|
|
10
|
+
import { DEFAULT_API_KEY_ENV } from './credential-refs.js'
|
|
11
|
+
|
|
12
|
+
function boolFormat(value) {
|
|
13
|
+
return value === false ? 'false' : 'true'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function boolParse(text) {
|
|
17
|
+
return { kind: 'set', value: String(text).trim() === 'true' }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the section descriptor from live configuration.
|
|
22
|
+
* @param {{ live: () => Record<string, unknown> }} deps
|
|
23
|
+
*/
|
|
24
|
+
export function buildTuiSection({ live }) {
|
|
25
|
+
const pub = publicConfig(live())
|
|
26
|
+
const apiKeyRef = pub.apiKeyEnv || DEFAULT_API_KEY_ENV
|
|
27
|
+
const accounts = Array.isArray(pub.accounts) ? pub.accounts : []
|
|
28
|
+
const fields = [
|
|
29
|
+
{
|
|
30
|
+
path: ['apiKey'],
|
|
31
|
+
group: 'connection',
|
|
32
|
+
kind: 'text',
|
|
33
|
+
label: 'API key',
|
|
34
|
+
secret: { ref: apiKeyRef },
|
|
35
|
+
hint: `Stored in the credential store as ${apiKeyRef}, never in configuration files.`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
path: ['baseUrl'],
|
|
39
|
+
group: 'connection',
|
|
40
|
+
kind: 'text',
|
|
41
|
+
label: 'API base',
|
|
42
|
+
placeholder: 'https://api.cline.bot/api/v1',
|
|
43
|
+
format: (value) => String(value || ''),
|
|
44
|
+
parse: (text) => {
|
|
45
|
+
const value = String(text).trim()
|
|
46
|
+
return value === '' ? { kind: 'clear' } : { kind: 'set', value }
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
path: ['filterModelsByPlan'],
|
|
51
|
+
group: 'models',
|
|
52
|
+
kind: 'boolean',
|
|
53
|
+
label: 'Hide out-of-plan models',
|
|
54
|
+
hint: 'Fails open when the plan cannot be resolved.',
|
|
55
|
+
format: boolFormat,
|
|
56
|
+
parse: boolParse,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
path: ['webSearch'],
|
|
60
|
+
group: 'models',
|
|
61
|
+
kind: 'boolean',
|
|
62
|
+
label: 'Serve web search with ClineBot',
|
|
63
|
+
format: boolFormat,
|
|
64
|
+
parse: boolParse,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
path: ['showSidebarQuota'],
|
|
68
|
+
group: 'models',
|
|
69
|
+
kind: 'boolean',
|
|
70
|
+
label: 'Show sidebar quota card',
|
|
71
|
+
format: boolFormat,
|
|
72
|
+
parse: boolParse,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
path: ['activeAccount'],
|
|
76
|
+
group: 'advanced',
|
|
77
|
+
kind: 'text',
|
|
78
|
+
label: 'Active account',
|
|
79
|
+
hint: 'An account id, or auto to follow the rotation order.',
|
|
80
|
+
options: [
|
|
81
|
+
{ value: 'auto', label: 'Automatic (rotation order)' },
|
|
82
|
+
...accounts.map((account) => ({
|
|
83
|
+
value: String(account.apiKeyEnv || ''),
|
|
84
|
+
label: String(account.label || account.apiKeyEnv || ''),
|
|
85
|
+
})).filter((option) => option.value !== ''),
|
|
86
|
+
],
|
|
87
|
+
format: (value) => (typeof value === 'string' && value.trim() !== '' ? value.trim() : 'auto'),
|
|
88
|
+
parse: (text) => {
|
|
89
|
+
const value = String(text).trim()
|
|
90
|
+
return value === '' || value === 'auto' ? { kind: 'clear' } : { kind: 'set', value }
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
path: ['contextWindow'],
|
|
95
|
+
group: 'advanced',
|
|
96
|
+
kind: 'number',
|
|
97
|
+
label: 'Global context window',
|
|
98
|
+
hint: '0 follows the catalog; per-model overrides win.',
|
|
99
|
+
format: (value) => (Number(value) > 0 ? String(Number(value)) : ''),
|
|
100
|
+
parse: (text) => {
|
|
101
|
+
const value = Number(text)
|
|
102
|
+
return Number.isFinite(value) && value > 0 ? { kind: 'set', value: Math.trunc(value) } : { kind: 'clear' }
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
path: ['maxOutputTokens'],
|
|
107
|
+
group: 'advanced',
|
|
108
|
+
kind: 'number',
|
|
109
|
+
label: 'Output cap (-1 derived, 0 omit)',
|
|
110
|
+
hint: 'A positive value tightens the model output cap; 0 sends no max_tokens.',
|
|
111
|
+
format: (value) => String(Number.isFinite(Number(value)) ? Number(value) : -1),
|
|
112
|
+
parse: (text) => {
|
|
113
|
+
const value = Number(text)
|
|
114
|
+
return Number.isInteger(value) && value >= -1 ? { kind: 'set', value } : undefined
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
ns: 'dsh-clinebot',
|
|
121
|
+
title: 'ClineBot',
|
|
122
|
+
descriptions: { en: 'ClineBot (ClinePass) connection and model controls.', zh: 'ClineBot(ClinePass)连接与模型设置。' },
|
|
123
|
+
groups: [
|
|
124
|
+
{ id: 'connection', title: 'Connection', descriptions: { zh: '连接' } },
|
|
125
|
+
{ id: 'models', title: 'Models', descriptions: { zh: '模型' } },
|
|
126
|
+
{ id: 'advanced', title: 'Advanced', descriptions: { zh: '高级' } },
|
|
127
|
+
],
|
|
128
|
+
fields,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Register the TUI section when the host provides the seam.
|
|
134
|
+
* @param {object} ctx
|
|
135
|
+
* @param {{ live: () => Record<string, unknown> }} deps
|
|
136
|
+
*/
|
|
137
|
+
export function applyTuiSettings(ctx, { live }) {
|
|
138
|
+
if (typeof ctx?.inject !== 'function') return
|
|
139
|
+
ctx.inject(['tuiSettingsSections'], (tuiCtx) => {
|
|
140
|
+
let service
|
|
141
|
+
try {
|
|
142
|
+
service = tuiCtx?.tuiSettingsSections || tuiCtx?.get?.('tuiSettingsSections')
|
|
143
|
+
} catch {
|
|
144
|
+
service = undefined
|
|
145
|
+
}
|
|
146
|
+
if (!service || typeof service.register !== 'function') return
|
|
147
|
+
try {
|
|
148
|
+
const dispose = service.register(buildTuiSection({ live }))
|
|
149
|
+
if (typeof ctx.effect === 'function') {
|
|
150
|
+
ctx.effect(() => () => {
|
|
151
|
+
try { dispose() } catch {}
|
|
152
|
+
}, 'dsh-clinebot: tui settings')
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Could not register TUI settings: ' + (error?.message || error))
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
}
|
package/package.json
CHANGED
|
@@ -1,77 +1,113 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@goodandready/dsh-clinebot",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "./lib/index.js",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": "./lib/index.js",
|
|
10
|
-
"./client": "./lib/client.js",
|
|
11
|
-
"./package.json": "./package.json",
|
|
12
|
-
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"lib/",
|
|
16
|
-
"cordis.patch.yml",
|
|
17
|
-
"README.md",
|
|
18
|
-
"README.ru.md",
|
|
19
|
-
"README.zh.md",
|
|
20
|
-
"CHANGELOG.md",
|
|
21
|
-
"LICENSE",
|
|
22
|
-
"media/visual-verification.png"
|
|
23
|
-
],
|
|
24
|
-
"keywords": [
|
|
25
|
-
"dsh",
|
|
26
|
-
"dsh-plugin",
|
|
27
|
-
"deepseek-harness",
|
|
28
|
-
"cline",
|
|
29
|
-
"clinebot",
|
|
30
|
-
"clinepass",
|
|
31
|
-
"openai-compatible",
|
|
32
|
-
"llm-provider",
|
|
33
|
-
"usage-dashboard",
|
|
34
|
-
"quota-limits"
|
|
35
|
-
],
|
|
36
|
-
"repository": {
|
|
37
|
-
"type": "git",
|
|
38
|
-
"url": "git+https://github.com/GooDAnDReaDY/dsh-clinebot.git"
|
|
39
|
-
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"build:client": "node scripts/build-client.js",
|
|
42
|
-
"test": "node scripts/build-client.js && node --import ./test/_setup.mjs --test test/*.test.js"
|
|
43
|
-
},
|
|
44
|
-
"engines": {
|
|
45
|
-
"node": ">=
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@goodandready/dsh-clinebot",
|
|
3
|
+
"version": "0.6.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib/",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md",
|
|
18
|
+
"README.ru.md",
|
|
19
|
+
"README.zh.md",
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"media/visual-verification.png"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"dsh",
|
|
26
|
+
"dsh-plugin",
|
|
27
|
+
"deepseek-harness",
|
|
28
|
+
"cline",
|
|
29
|
+
"clinebot",
|
|
30
|
+
"clinepass",
|
|
31
|
+
"openai-compatible",
|
|
32
|
+
"llm-provider",
|
|
33
|
+
"usage-dashboard",
|
|
34
|
+
"quota-limits"
|
|
35
|
+
],
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/GooDAnDReaDY/dsh-clinebot.git"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build:client": "node scripts/build-client.js",
|
|
42
|
+
"test": "node scripts/build-client.js && node --import ./test/_setup.mjs --test test/*.test.js"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=22",
|
|
46
|
+
"dsh": "0.2.1-rc.2"
|
|
47
|
+
},
|
|
48
|
+
"dsh": {
|
|
49
|
+
"bundle": {
|
|
50
|
+
"patch": "./cordis.patch.yml"
|
|
51
|
+
},
|
|
52
|
+
"compatibility": {
|
|
53
|
+
"dsh": "0.2.1-rc.2",
|
|
54
|
+
"dshReleases": {
|
|
55
|
+
"0.2.1-rc.2": "compatible"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"client": {
|
|
59
|
+
"platform": "web",
|
|
60
|
+
"inject": [
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
62
|
+
"@deepseek-ai/dsh-client-locale",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"@deepseek-ai/cordis": "~4.0.5",
|
|
69
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-commands": "0.2.1-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-credentials": "0.2.1-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.1-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-settings": "0.2.1-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-web": "0.2.1-rc.2",
|
|
76
|
+
"@deepseek-ai/schemastery": "~3.18.5",
|
|
77
|
+
"react": "^18.2.0"
|
|
78
|
+
},
|
|
79
|
+
"peerDependenciesMeta": {
|
|
80
|
+
"@deepseek-ai/dsh-attachment": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"@deepseek-ai/dsh-commands": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"@deepseek-ai/dsh-settings": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"@deepseek-ai/dsh-web": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"react": {
|
|
93
|
+
"optional": true
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"devDependencies": {
|
|
97
|
+
"@deepseek-ai/cordis": "~4.0.5",
|
|
98
|
+
"@deepseek-ai/dsh-attachment": "^0.2.1-rc.2",
|
|
99
|
+
"@deepseek-ai/dsh-brand": "0.2.1-rc.2",
|
|
100
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.2",
|
|
101
|
+
"@deepseek-ai/schemastery": "~3.18.5",
|
|
102
|
+
"@types/node": "^24.0.0",
|
|
103
|
+
"eslint": "^9.39.5",
|
|
104
|
+
"typescript": "^6.0.3"
|
|
105
|
+
},
|
|
106
|
+
"homepage": "https://github.com/GooDAnDReaDY/dsh-clinebot#readme",
|
|
107
|
+
"bugs": {
|
|
108
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-clinebot/issues"
|
|
109
|
+
},
|
|
110
|
+
"publishConfig": {
|
|
111
|
+
"access": "public"
|
|
112
|
+
}
|
|
113
|
+
}
|