@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.
@@ -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.5.33",
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": ">=20"
46
- },
47
- "dsh": {
48
- "bundle": {
49
- "patch": "./cordis.patch.yml"
50
- },
51
- "client": {
52
- "platform": "web",
53
- "inject": [
54
- "@deepseek-ai/dsh-client-ui-slots",
55
- "@deepseek-ai/dsh-client-locale",
56
- "@deepseek-ai/dsh-client-ui-settings"
57
- ]
58
- }
59
- },
60
- "peerDependencies": {
61
- "@deepseek-ai/cordis": "^4.0.1",
62
- "@deepseek-ai/dsh-credentials": "^0.1.7-rc.2 || ^0.2.0-rc.1",
63
- "@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2 || ^0.2.0-rc.1",
64
- "@deepseek-ai/schemastery": "^3.18.1"
65
- },
66
- "devDependencies": {
67
- "@deepseek-ai/schemastery": "^3.18.1",
68
- "eslint": "^9.39.5"
69
- },
70
- "homepage": "https://github.com/GooDAnDReaDY/dsh-clinebot#readme",
71
- "bugs": {
72
- "url": "https://github.com/GooDAnDReaDY/dsh-clinebot/issues"
73
- },
74
- "publishConfig": {
75
- "access": "public"
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
+ }