@goodandready/dsh-clinebot 0.6.29 → 0.6.31
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 +14 -0
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +5 -5
- package/lib/config.js +283 -283
- package/lib/http.js +2 -2
- package/lib/index.js +427 -426
- package/lib/path-safety.js +57 -4
- package/lib/price-sync.js +2 -5
- package/lib/provider-sync.js +258 -275
- package/lib/stats-storage.js +2 -27
- package/lib/transport/cline-transport.js +342 -343
- package/lib/updater.js +4 -9
- package/package.json +117 -117
package/lib/index.js
CHANGED
|
@@ -1,426 +1,427 @@
|
|
|
1
|
-
import { Config, publicConfig, plainConfig, volatileConfig, isVolatileRef, NS } from './config.js'
|
|
2
|
-
import { registerPluginUpdater } from './updater.js'
|
|
3
|
-
import {
|
|
4
|
-
sessionStats,
|
|
5
|
-
recordSessionRequest,
|
|
6
|
-
resetSessionStats,
|
|
7
|
-
clearUsageCache,
|
|
8
|
-
clearProbeCache,
|
|
9
|
-
} from './cline-client.js'
|
|
10
|
-
import { rotateToNextAccount } from './account-pool.js'
|
|
11
|
-
import { PROVIDER_ID } from './models.js'
|
|
12
|
-
import {
|
|
13
|
-
autoDiscoverPlanModels,
|
|
14
|
-
} from './provider-sync.js'
|
|
15
|
-
import { registerSettingsRoutes } from './routes/settings.js'
|
|
16
|
-
import { registerAccountsRoutes } from './routes/accounts.js'
|
|
17
|
-
import { registerModelsRoutes } from './routes/models.js'
|
|
18
|
-
import { registerAuthRoutes } from './routes/auth.js'
|
|
19
|
-
import { ClineBotAdapter } from './adapter.js'
|
|
20
|
-
import { registerSlashCommand } from './slash-command.js'
|
|
21
|
-
import { ClineBotSearchProvider, CLINEBOT_SEARCH_PROVIDER_ID } from './web-search.js'
|
|
22
|
-
import { resolveAccountPool } from './account-pool.js'
|
|
23
|
-
import { loadStats, flushStats, resolvePath, switchStatsStorage, setStatsRetentionDays } from './stats-storage.js'
|
|
24
|
-
import { loadPublicPriceCache } from './price-sync.js'
|
|
25
|
-
import { setPublicPriceCatalog } from './model-prices.js'
|
|
26
|
-
import { loadAccountEvents, flushAccountEvents, recordAccountEvent } from './account-events.js'
|
|
27
|
-
import { setAccountEventSink } from './session-router.js'
|
|
28
|
-
import { service, migrateLegacyPiAiProvider } from './services.js'
|
|
29
|
-
import { applyTuiSettings } from './tui-settings.js'
|
|
30
|
-
import { isSafeDataPath } from './path-safety.js'
|
|
31
|
-
import { safeDict, setOwn } from './safe-dict.js'
|
|
32
|
-
|
|
33
|
-
export const name = '@goodandready/dsh-clinebot'
|
|
34
|
-
export const inject = []
|
|
35
|
-
|
|
36
|
-
const CATALOG_REFRESH_MS = 6 * 60 * 60 * 1000
|
|
37
|
-
const CATALOG_STALE_MS = 5 * 60 * 1000
|
|
38
|
-
|
|
39
|
-
export function configReader(config) {
|
|
40
|
-
return () => config
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export { NS, Config, sessionStats, recordSessionRequest, resetSessionStats, rotateToNextAccount }
|
|
44
|
-
|
|
45
|
-
export function apply(ctx, config) {
|
|
46
|
-
let currentConfig = configReader(config)() || {}
|
|
47
|
-
const overrides = safeDict()
|
|
48
|
-
const getConfig = () => {
|
|
49
|
-
if (!currentConfig) return currentConfig
|
|
50
|
-
if (Object.isFrozen(currentConfig)) {
|
|
51
|
-
return new Proxy({}, {
|
|
52
|
-
get(_target, prop) {
|
|
53
|
-
const orig = Reflect.get(currentConfig, prop)
|
|
54
|
-
if (isVolatileRef(orig) || Object.getOwnPropertyDescriptor(currentConfig, prop)?.get) {
|
|
55
|
-
return orig
|
|
56
|
-
}
|
|
57
|
-
if (prop in overrides) {
|
|
58
|
-
return overrides[prop]
|
|
59
|
-
}
|
|
60
|
-
return orig
|
|
61
|
-
},
|
|
62
|
-
has(_target, prop) {
|
|
63
|
-
return prop in overrides || Reflect.has(currentConfig, prop)
|
|
64
|
-
},
|
|
65
|
-
ownKeys() {
|
|
66
|
-
return Array.from(new Set([...Reflect.ownKeys(currentConfig), ...Object.keys(overrides)]))
|
|
67
|
-
},
|
|
68
|
-
getOwnPropertyDescriptor(_target, prop) {
|
|
69
|
-
if (prop in overrides) {
|
|
70
|
-
return { configurable: true, enumerable: true, value: overrides[prop], writable: true }
|
|
71
|
-
}
|
|
72
|
-
const desc = Object.getOwnPropertyDescriptor(currentConfig, prop)
|
|
73
|
-
if (desc) {
|
|
74
|
-
return { configurable: true, enumerable: desc.enumerable ?? true, value: Reflect.get(currentConfig, prop) }
|
|
75
|
-
}
|
|
76
|
-
return undefined
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
return currentConfig
|
|
81
|
-
}
|
|
82
|
-
const live = () => plainConfig(Config(plainConfig(getConfig() || config || {})))
|
|
83
|
-
let settingsApi
|
|
84
|
-
let adapterRegistration = null
|
|
85
|
-
let adapterDirectory = null
|
|
86
|
-
|
|
87
|
-
const syncProviderState = async (cfg) => {
|
|
88
|
-
try {
|
|
89
|
-
const pub = publicConfig(cfg)
|
|
90
|
-
if (adapterRegistration) {
|
|
91
|
-
adapterRegistration.replace(pub.enabled ? [PROVIDER_ID] : [])
|
|
92
|
-
}
|
|
93
|
-
} catch {
|
|
94
|
-
/* ignore transient settings unavailable */
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let autoDiscoverInFlight = false
|
|
99
|
-
const triggerAutoDiscover = () => {
|
|
100
|
-
if (autoDiscoverInFlight) return
|
|
101
|
-
autoDiscoverInFlight = true
|
|
102
|
-
Promise.resolve(autoDiscoverPlanModels(ctx, {
|
|
103
|
-
live,
|
|
104
|
-
getSettingsApi: () => settingsApi,
|
|
105
|
-
syncProviderState,
|
|
106
|
-
}))
|
|
107
|
-
.catch(() => {})
|
|
108
|
-
.finally(() => {
|
|
109
|
-
autoDiscoverInFlight = false
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const initial = publicConfig(live())
|
|
114
|
-
let activeStatsPath = resolvePath(isSafeDataPath(initial.statsPath) ? initial.statsPath :
|
|
115
|
-
setStatsRetentionDays(initial.statsRetentionDays)
|
|
116
|
-
setPublicPriceCatalog(loadPublicPriceCache(isSafeDataPath(initial.priceCatalogPath) ? initial.priceCatalogPath :
|
|
117
|
-
let activeEventPath = resolvePath(isSafeDataPath(initial.eventLogPath) ? initial.eventLogPath :
|
|
118
|
-
loadAccountEvents(activeEventPath)
|
|
119
|
-
setAccountEventSink(recordAccountEvent)
|
|
120
|
-
loadStats(activeStatsPath)
|
|
121
|
-
|
|
122
|
-
const syncStatsStorage = (cfg) => {
|
|
123
|
-
const pub = publicConfig(cfg)
|
|
124
|
-
setStatsRetentionDays(pub.statsRetentionDays)
|
|
125
|
-
const priceCatalogPath = isSafeDataPath(pub.priceCatalogPath) ? pub.priceCatalogPath :
|
|
126
|
-
setPublicPriceCatalog(loadPublicPriceCache(priceCatalogPath))
|
|
127
|
-
const nextEventPath = resolvePath(isSafeDataPath(pub.eventLogPath) ? pub.eventLogPath :
|
|
128
|
-
if (nextEventPath !== activeEventPath) {
|
|
129
|
-
flushAccountEvents()
|
|
130
|
-
activeEventPath = nextEventPath
|
|
131
|
-
loadAccountEvents(activeEventPath)
|
|
132
|
-
}
|
|
133
|
-
const nextPath = resolvePath(isSafeDataPath(pub.statsPath) ? pub.statsPath :
|
|
134
|
-
if (nextPath !== activeStatsPath) {
|
|
135
|
-
switchStatsStorage(nextPath)
|
|
136
|
-
activeStatsPath = nextPath
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const normalizeEndpoint = (url) => String(url || '').trim().replace(/\/+$/, '')
|
|
141
|
-
const getCatalogIdentity = (cfg) => {
|
|
142
|
-
const pub = publicConfig(cfg)
|
|
143
|
-
return `${normalizeEndpoint(pub.baseUrl)}::${String(pub.activeAccount || '').trim()}::${String(pub.apiKeyEnv || '').trim()}`
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
let lastCatalogIdentity = getCatalogIdentity(live())
|
|
147
|
-
const resetCatalogSnapshot = () => {
|
|
148
|
-
try {
|
|
149
|
-
if (currentConfig && typeof currentConfig === 'object') {
|
|
150
|
-
currentConfig.dynamicModels = []
|
|
151
|
-
currentConfig.planSyncedAt = 0
|
|
152
|
-
}
|
|
153
|
-
} catch {
|
|
154
|
-
/* ignore read-only or frozen */
|
|
155
|
-
}
|
|
156
|
-
overrides.dynamicModels = []
|
|
157
|
-
overrides.planSyncedAt = 0
|
|
158
|
-
clearUsageCache()
|
|
159
|
-
clearProbeCache()
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (typeof ctx?.on === 'function') {
|
|
163
|
-
ctx.on('loader/volatile-update', () => {
|
|
164
|
-
const liveCfg = live()
|
|
165
|
-
const currentIdentity = getCatalogIdentity(liveCfg)
|
|
166
|
-
if (currentIdentity !== lastCatalogIdentity) {
|
|
167
|
-
lastCatalogIdentity = currentIdentity
|
|
168
|
-
resetCatalogSnapshot()
|
|
169
|
-
}
|
|
170
|
-
syncStatsStorage(liveCfg)
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const getRevision = (svc) => {
|
|
175
|
-
try {
|
|
176
|
-
return svc?.describe?.().find((row) => row.ns === NS)?.revision
|
|
177
|
-
} catch {
|
|
178
|
-
return undefined
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const applyConfigMutations = (target, source) => {
|
|
183
|
-
const targetConfig = typeof target === 'function' ? target() : target
|
|
184
|
-
if (targetConfig && typeof targetConfig === 'object') {
|
|
185
|
-
if (Object.isFrozen(targetConfig)) {
|
|
186
|
-
for (const [key, value] of Object.entries(source)) {
|
|
187
|
-
const orig = targetConfig[key]
|
|
188
|
-
if (isVolatileRef(orig) || Object.getOwnPropertyDescriptor(targetConfig, key)?.get) {
|
|
189
|
-
continue
|
|
190
|
-
}
|
|
191
|
-
setOwn(overrides, key, value)
|
|
192
|
-
}
|
|
193
|
-
return
|
|
194
|
-
}
|
|
195
|
-
for (const [key, value] of Object.entries(source)) {
|
|
196
|
-
try {
|
|
197
|
-
if (isVolatileRef(targetConfig[key])) {
|
|
198
|
-
// Cosmokit native Volatile references are reactive and authoritative;
|
|
199
|
-
// preserve the reference without overwriting with plain value.
|
|
200
|
-
continue
|
|
201
|
-
}
|
|
202
|
-
const desc = Object.getOwnPropertyDescriptor(targetConfig, key)
|
|
203
|
-
if (desc?.get && !desc.set) {
|
|
204
|
-
continue
|
|
205
|
-
}
|
|
206
|
-
if (desc?.set) {
|
|
207
|
-
targetConfig[key] = value
|
|
208
|
-
} else {
|
|
209
|
-
setOwn(targetConfig, key, value)
|
|
210
|
-
}
|
|
211
|
-
} catch {
|
|
212
|
-
/* ignore read-only */
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
settingsApi = {
|
|
219
|
-
get: () => live(),
|
|
220
|
-
replace: async (next) => {
|
|
221
|
-
const svc = ctx?.get?.('settings')
|
|
222
|
-
if (!svc || (typeof svc.replace !== 'function' && typeof svc.update !== 'function')) {
|
|
223
|
-
throw new Error('DSH settings service is unavailable or non-writable')
|
|
224
|
-
}
|
|
225
|
-
const prevIdentity = getCatalogIdentity(live())
|
|
226
|
-
const parsed = Config(structuredClone(plainConfig(next)))
|
|
227
|
-
const nextIdentity = getCatalogIdentity(parsed)
|
|
228
|
-
const identityChanged = nextIdentity !== prevIdentity
|
|
229
|
-
if (identityChanged) {
|
|
230
|
-
if (!('dynamicModels' in next)) parsed.dynamicModels = []
|
|
231
|
-
if (!('planSyncedAt' in next)) parsed.planSyncedAt = 0
|
|
232
|
-
}
|
|
233
|
-
const payload = volatileConfig(parsed)
|
|
234
|
-
const rev = getRevision(svc)
|
|
235
|
-
if (typeof svc.replace === 'function') {
|
|
236
|
-
await svc.replace(NS, payload, rev)
|
|
237
|
-
} else {
|
|
238
|
-
await svc.update(NS, payload, rev)
|
|
239
|
-
}
|
|
240
|
-
if (identityChanged) {
|
|
241
|
-
lastCatalogIdentity = nextIdentity
|
|
242
|
-
resetCatalogSnapshot()
|
|
243
|
-
}
|
|
244
|
-
applyConfigMutations(currentConfig, parsed)
|
|
245
|
-
syncStatsStorage(live())
|
|
246
|
-
await syncProviderState(live())
|
|
247
|
-
return plainConfig(parsed)
|
|
248
|
-
},
|
|
249
|
-
update: async (patch) => {
|
|
250
|
-
const svc = ctx?.get?.('settings')
|
|
251
|
-
if (!svc || (typeof svc.update !== 'function' && typeof svc.replace !== 'function')) {
|
|
252
|
-
throw new Error('DSH settings service is unavailable or non-writable')
|
|
253
|
-
}
|
|
254
|
-
const prevIdentity = getCatalogIdentity(live())
|
|
255
|
-
const merged = Config({ ...publicConfig(live()), ...plainConfig(patch) })
|
|
256
|
-
const nextIdentity = getCatalogIdentity(merged)
|
|
257
|
-
const identityChanged = nextIdentity !== prevIdentity
|
|
258
|
-
if (identityChanged) {
|
|
259
|
-
if (!('dynamicModels' in patch)) merged.dynamicModels = []
|
|
260
|
-
if (!('planSyncedAt' in patch)) merged.planSyncedAt = 0
|
|
261
|
-
}
|
|
262
|
-
const payload = volatileConfig(merged)
|
|
263
|
-
const rev = getRevision(svc)
|
|
264
|
-
if (typeof svc.update === 'function') {
|
|
265
|
-
await svc.update(NS, payload, rev)
|
|
266
|
-
} else {
|
|
267
|
-
await svc.replace(NS, payload, rev)
|
|
268
|
-
}
|
|
269
|
-
if (identityChanged) {
|
|
270
|
-
lastCatalogIdentity = nextIdentity
|
|
271
|
-
resetCatalogSnapshot()
|
|
272
|
-
}
|
|
273
|
-
applyConfigMutations(currentConfig, merged)
|
|
274
|
-
syncStatsStorage(live())
|
|
275
|
-
await syncProviderState(live())
|
|
276
|
-
return plainConfig(merged)
|
|
277
|
-
},
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (typeof ctx.effect === 'function') {
|
|
282
|
-
ctx.effect(() => {
|
|
283
|
-
return () => {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
ctx.inject
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
try {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
void
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
clearTimeout(
|
|
361
|
-
clearTimeout(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
1
|
+
import { Config, publicConfig, plainConfig, volatileConfig, isVolatileRef, NS } from './config.js'
|
|
2
|
+
import { registerPluginUpdater } from './updater.js'
|
|
3
|
+
import {
|
|
4
|
+
sessionStats,
|
|
5
|
+
recordSessionRequest,
|
|
6
|
+
resetSessionStats,
|
|
7
|
+
clearUsageCache,
|
|
8
|
+
clearProbeCache,
|
|
9
|
+
} from './cline-client.js'
|
|
10
|
+
import { rotateToNextAccount } from './account-pool.js'
|
|
11
|
+
import { PROVIDER_ID } from './models.js'
|
|
12
|
+
import {
|
|
13
|
+
autoDiscoverPlanModels,
|
|
14
|
+
} from './provider-sync.js'
|
|
15
|
+
import { registerSettingsRoutes } from './routes/settings.js'
|
|
16
|
+
import { registerAccountsRoutes } from './routes/accounts.js'
|
|
17
|
+
import { registerModelsRoutes } from './routes/models.js'
|
|
18
|
+
import { registerAuthRoutes } from './routes/auth.js'
|
|
19
|
+
import { ClineBotAdapter } from './adapter.js'
|
|
20
|
+
import { registerSlashCommand } from './slash-command.js'
|
|
21
|
+
import { ClineBotSearchProvider, CLINEBOT_SEARCH_PROVIDER_ID } from './web-search.js'
|
|
22
|
+
import { resolveAccountPool } from './account-pool.js'
|
|
23
|
+
import { loadStats, flushStats, resolvePath, switchStatsStorage, setStatsRetentionDays } from './stats-storage.js'
|
|
24
|
+
import { loadPublicPriceCache } from './price-sync.js'
|
|
25
|
+
import { setPublicPriceCatalog } from './model-prices.js'
|
|
26
|
+
import { loadAccountEvents, flushAccountEvents, recordAccountEvent } from './account-events.js'
|
|
27
|
+
import { setAccountEventSink } from './session-router.js'
|
|
28
|
+
import { service, migrateLegacyPiAiProvider } from './services.js'
|
|
29
|
+
import { applyTuiSettings } from './tui-settings.js'
|
|
30
|
+
import { isSafeDataPath, defaultStatsPath, defaultEventLogPath, defaultPriceCatalogPath } from './path-safety.js'
|
|
31
|
+
import { safeDict, setOwn } from './safe-dict.js'
|
|
32
|
+
|
|
33
|
+
export const name = '@goodandready/dsh-clinebot'
|
|
34
|
+
export const inject = []
|
|
35
|
+
|
|
36
|
+
const CATALOG_REFRESH_MS = 6 * 60 * 60 * 1000
|
|
37
|
+
const CATALOG_STALE_MS = 5 * 60 * 1000
|
|
38
|
+
|
|
39
|
+
export function configReader(config) {
|
|
40
|
+
return () => config
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { NS, Config, sessionStats, recordSessionRequest, resetSessionStats, rotateToNextAccount }
|
|
44
|
+
|
|
45
|
+
export function apply(ctx, config) {
|
|
46
|
+
let currentConfig = configReader(config)() || {}
|
|
47
|
+
const overrides = safeDict()
|
|
48
|
+
const getConfig = () => {
|
|
49
|
+
if (!currentConfig) return currentConfig
|
|
50
|
+
if (Object.isFrozen(currentConfig)) {
|
|
51
|
+
return new Proxy({}, {
|
|
52
|
+
get(_target, prop) {
|
|
53
|
+
const orig = Reflect.get(currentConfig, prop)
|
|
54
|
+
if (isVolatileRef(orig) || Object.getOwnPropertyDescriptor(currentConfig, prop)?.get) {
|
|
55
|
+
return orig
|
|
56
|
+
}
|
|
57
|
+
if (prop in overrides) {
|
|
58
|
+
return overrides[prop]
|
|
59
|
+
}
|
|
60
|
+
return orig
|
|
61
|
+
},
|
|
62
|
+
has(_target, prop) {
|
|
63
|
+
return prop in overrides || Reflect.has(currentConfig, prop)
|
|
64
|
+
},
|
|
65
|
+
ownKeys() {
|
|
66
|
+
return Array.from(new Set([...Reflect.ownKeys(currentConfig), ...Object.keys(overrides)]))
|
|
67
|
+
},
|
|
68
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
69
|
+
if (prop in overrides) {
|
|
70
|
+
return { configurable: true, enumerable: true, value: overrides[prop], writable: true }
|
|
71
|
+
}
|
|
72
|
+
const desc = Object.getOwnPropertyDescriptor(currentConfig, prop)
|
|
73
|
+
if (desc) {
|
|
74
|
+
return { configurable: true, enumerable: desc.enumerable ?? true, value: Reflect.get(currentConfig, prop) }
|
|
75
|
+
}
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
return currentConfig
|
|
81
|
+
}
|
|
82
|
+
const live = () => plainConfig(Config(plainConfig(getConfig() || config || {})))
|
|
83
|
+
let settingsApi
|
|
84
|
+
let adapterRegistration = null
|
|
85
|
+
let adapterDirectory = null
|
|
86
|
+
|
|
87
|
+
const syncProviderState = async (cfg) => {
|
|
88
|
+
try {
|
|
89
|
+
const pub = publicConfig(cfg)
|
|
90
|
+
if (adapterRegistration) {
|
|
91
|
+
adapterRegistration.replace(pub.enabled ? [PROVIDER_ID] : [])
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
/* ignore transient settings unavailable */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let autoDiscoverInFlight = false
|
|
99
|
+
const triggerAutoDiscover = () => {
|
|
100
|
+
if (autoDiscoverInFlight) return
|
|
101
|
+
autoDiscoverInFlight = true
|
|
102
|
+
Promise.resolve(autoDiscoverPlanModels(ctx, {
|
|
103
|
+
live,
|
|
104
|
+
getSettingsApi: () => settingsApi,
|
|
105
|
+
syncProviderState,
|
|
106
|
+
}))
|
|
107
|
+
.catch(() => {})
|
|
108
|
+
.finally(() => {
|
|
109
|
+
autoDiscoverInFlight = false
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const initial = publicConfig(live())
|
|
114
|
+
let activeStatsPath = resolvePath(isSafeDataPath(initial.statsPath) ? initial.statsPath : defaultStatsPath())
|
|
115
|
+
setStatsRetentionDays(initial.statsRetentionDays)
|
|
116
|
+
setPublicPriceCatalog(loadPublicPriceCache(isSafeDataPath(initial.priceCatalogPath) ? initial.priceCatalogPath : defaultPriceCatalogPath()))
|
|
117
|
+
let activeEventPath = resolvePath(isSafeDataPath(initial.eventLogPath) ? initial.eventLogPath : defaultEventLogPath())
|
|
118
|
+
loadAccountEvents(activeEventPath)
|
|
119
|
+
setAccountEventSink(recordAccountEvent)
|
|
120
|
+
loadStats(activeStatsPath)
|
|
121
|
+
|
|
122
|
+
const syncStatsStorage = (cfg) => {
|
|
123
|
+
const pub = publicConfig(cfg)
|
|
124
|
+
setStatsRetentionDays(pub.statsRetentionDays)
|
|
125
|
+
const priceCatalogPath = isSafeDataPath(pub.priceCatalogPath) ? pub.priceCatalogPath : defaultPriceCatalogPath()
|
|
126
|
+
setPublicPriceCatalog(loadPublicPriceCache(priceCatalogPath))
|
|
127
|
+
const nextEventPath = resolvePath(isSafeDataPath(pub.eventLogPath) ? pub.eventLogPath : defaultEventLogPath())
|
|
128
|
+
if (nextEventPath !== activeEventPath) {
|
|
129
|
+
flushAccountEvents()
|
|
130
|
+
activeEventPath = nextEventPath
|
|
131
|
+
loadAccountEvents(activeEventPath)
|
|
132
|
+
}
|
|
133
|
+
const nextPath = resolvePath(isSafeDataPath(pub.statsPath) ? pub.statsPath : defaultStatsPath())
|
|
134
|
+
if (nextPath !== activeStatsPath) {
|
|
135
|
+
switchStatsStorage(nextPath)
|
|
136
|
+
activeStatsPath = nextPath
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const normalizeEndpoint = (url) => String(url || '').trim().replace(/\/+$/, '')
|
|
141
|
+
const getCatalogIdentity = (cfg) => {
|
|
142
|
+
const pub = publicConfig(cfg)
|
|
143
|
+
return `${normalizeEndpoint(pub.baseUrl)}::${String(pub.activeAccount || '').trim()}::${String(pub.apiKeyEnv || '').trim()}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let lastCatalogIdentity = getCatalogIdentity(live())
|
|
147
|
+
const resetCatalogSnapshot = () => {
|
|
148
|
+
try {
|
|
149
|
+
if (currentConfig && typeof currentConfig === 'object') {
|
|
150
|
+
currentConfig.dynamicModels = []
|
|
151
|
+
currentConfig.planSyncedAt = 0
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
/* ignore read-only or frozen */
|
|
155
|
+
}
|
|
156
|
+
overrides.dynamicModels = []
|
|
157
|
+
overrides.planSyncedAt = 0
|
|
158
|
+
clearUsageCache()
|
|
159
|
+
clearProbeCache()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (typeof ctx?.on === 'function') {
|
|
163
|
+
ctx.on('loader/volatile-update', () => {
|
|
164
|
+
const liveCfg = live()
|
|
165
|
+
const currentIdentity = getCatalogIdentity(liveCfg)
|
|
166
|
+
if (currentIdentity !== lastCatalogIdentity) {
|
|
167
|
+
lastCatalogIdentity = currentIdentity
|
|
168
|
+
resetCatalogSnapshot()
|
|
169
|
+
}
|
|
170
|
+
syncStatsStorage(liveCfg)
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const getRevision = (svc) => {
|
|
175
|
+
try {
|
|
176
|
+
return svc?.describe?.().find((row) => row.ns === NS)?.revision
|
|
177
|
+
} catch {
|
|
178
|
+
return undefined
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const applyConfigMutations = (target, source) => {
|
|
183
|
+
const targetConfig = typeof target === 'function' ? target() : target
|
|
184
|
+
if (targetConfig && typeof targetConfig === 'object') {
|
|
185
|
+
if (Object.isFrozen(targetConfig)) {
|
|
186
|
+
for (const [key, value] of Object.entries(source)) {
|
|
187
|
+
const orig = targetConfig[key]
|
|
188
|
+
if (isVolatileRef(orig) || Object.getOwnPropertyDescriptor(targetConfig, key)?.get) {
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
setOwn(overrides, key, value)
|
|
192
|
+
}
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
for (const [key, value] of Object.entries(source)) {
|
|
196
|
+
try {
|
|
197
|
+
if (isVolatileRef(targetConfig[key])) {
|
|
198
|
+
// Cosmokit native Volatile references are reactive and authoritative;
|
|
199
|
+
// preserve the reference without overwriting with plain value.
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
const desc = Object.getOwnPropertyDescriptor(targetConfig, key)
|
|
203
|
+
if (desc?.get && !desc.set) {
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
if (desc?.set) {
|
|
207
|
+
targetConfig[key] = value
|
|
208
|
+
} else {
|
|
209
|
+
setOwn(targetConfig, key, value)
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
/* ignore read-only */
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
settingsApi = {
|
|
219
|
+
get: () => live(),
|
|
220
|
+
replace: async (next) => {
|
|
221
|
+
const svc = ctx?.get?.('settings')
|
|
222
|
+
if (!svc || (typeof svc.replace !== 'function' && typeof svc.update !== 'function')) {
|
|
223
|
+
throw new Error('DSH settings service is unavailable or non-writable')
|
|
224
|
+
}
|
|
225
|
+
const prevIdentity = getCatalogIdentity(live())
|
|
226
|
+
const parsed = Config(structuredClone(plainConfig(next)))
|
|
227
|
+
const nextIdentity = getCatalogIdentity(parsed)
|
|
228
|
+
const identityChanged = nextIdentity !== prevIdentity
|
|
229
|
+
if (identityChanged) {
|
|
230
|
+
if (!('dynamicModels' in next)) parsed.dynamicModels = []
|
|
231
|
+
if (!('planSyncedAt' in next)) parsed.planSyncedAt = 0
|
|
232
|
+
}
|
|
233
|
+
const payload = volatileConfig(parsed)
|
|
234
|
+
const rev = getRevision(svc)
|
|
235
|
+
if (typeof svc.replace === 'function') {
|
|
236
|
+
await svc.replace(NS, payload, rev)
|
|
237
|
+
} else {
|
|
238
|
+
await svc.update(NS, payload, rev)
|
|
239
|
+
}
|
|
240
|
+
if (identityChanged) {
|
|
241
|
+
lastCatalogIdentity = nextIdentity
|
|
242
|
+
resetCatalogSnapshot()
|
|
243
|
+
}
|
|
244
|
+
applyConfigMutations(currentConfig, parsed)
|
|
245
|
+
syncStatsStorage(live())
|
|
246
|
+
await syncProviderState(live())
|
|
247
|
+
return plainConfig(parsed)
|
|
248
|
+
},
|
|
249
|
+
update: async (patch) => {
|
|
250
|
+
const svc = ctx?.get?.('settings')
|
|
251
|
+
if (!svc || (typeof svc.update !== 'function' && typeof svc.replace !== 'function')) {
|
|
252
|
+
throw new Error('DSH settings service is unavailable or non-writable')
|
|
253
|
+
}
|
|
254
|
+
const prevIdentity = getCatalogIdentity(live())
|
|
255
|
+
const merged = Config({ ...publicConfig(live()), ...plainConfig(patch) })
|
|
256
|
+
const nextIdentity = getCatalogIdentity(merged)
|
|
257
|
+
const identityChanged = nextIdentity !== prevIdentity
|
|
258
|
+
if (identityChanged) {
|
|
259
|
+
if (!('dynamicModels' in patch)) merged.dynamicModels = []
|
|
260
|
+
if (!('planSyncedAt' in patch)) merged.planSyncedAt = 0
|
|
261
|
+
}
|
|
262
|
+
const payload = volatileConfig(merged)
|
|
263
|
+
const rev = getRevision(svc)
|
|
264
|
+
if (typeof svc.update === 'function') {
|
|
265
|
+
await svc.update(NS, payload, rev)
|
|
266
|
+
} else {
|
|
267
|
+
await svc.replace(NS, payload, rev)
|
|
268
|
+
}
|
|
269
|
+
if (identityChanged) {
|
|
270
|
+
lastCatalogIdentity = nextIdentity
|
|
271
|
+
resetCatalogSnapshot()
|
|
272
|
+
}
|
|
273
|
+
applyConfigMutations(currentConfig, merged)
|
|
274
|
+
syncStatsStorage(live())
|
|
275
|
+
await syncProviderState(live())
|
|
276
|
+
return plainConfig(merged)
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (typeof ctx.effect === 'function') {
|
|
282
|
+
ctx.effect(() => {
|
|
283
|
+
return () => {
|
|
284
|
+
const safeStatsPath = isSafeDataPath(activeStatsPath) ? resolvePath(activeStatsPath) : defaultStatsPath()
|
|
285
|
+
flushStats(safeStatsPath)
|
|
286
|
+
flushAccountEvents()
|
|
287
|
+
setAccountEventSink(null)
|
|
288
|
+
}
|
|
289
|
+
}, 'dsh-clinebot: stats-storage')
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const routeEnv = {
|
|
293
|
+
live,
|
|
294
|
+
getSettingsApi: () => settingsApi,
|
|
295
|
+
syncProviderState,
|
|
296
|
+
triggerAutoDiscover,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const ensureAdapter = () => {
|
|
300
|
+
if (typeof ctx.inject !== 'function') return
|
|
301
|
+
ctx.inject(['llm'], (llmCtx) => {
|
|
302
|
+
const llm = llmCtx?.llm || llmCtx?.get?.('llm')
|
|
303
|
+
if (!llm || typeof llm.registerAdapter !== 'function') return
|
|
304
|
+
try {
|
|
305
|
+
const adapter = new ClineBotAdapter({
|
|
306
|
+
ctx,
|
|
307
|
+
live,
|
|
308
|
+
resolveAttachments: () => ctx?.get?.('attachments') || ctx?.attachments,
|
|
309
|
+
logger: ctx?.logger,
|
|
310
|
+
})
|
|
311
|
+
adapterRegistration = llm.registerAdapter([PROVIDER_ID], adapter)
|
|
312
|
+
if (typeof llm.registerConfigurableProviders === 'function') {
|
|
313
|
+
adapterDirectory = llm.registerConfigurableProviders([{
|
|
314
|
+
provider: PROVIDER_ID,
|
|
315
|
+
displayName: 'ClineBot (ClinePass)',
|
|
316
|
+
settingsNs: NS,
|
|
317
|
+
settingsPath: [],
|
|
318
|
+
defaultBaseURL: 'https://api.cline.bot/api/v1',
|
|
319
|
+
}])
|
|
320
|
+
}
|
|
321
|
+
ctx.effect(() => () => {
|
|
322
|
+
try { adapterDirectory?.() } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
323
|
+
try { adapterRegistration?.() } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
324
|
+
adapterDirectory = null
|
|
325
|
+
adapterRegistration = null
|
|
326
|
+
}, 'dsh-clinebot: llm adapter')
|
|
327
|
+
void syncProviderState(live())
|
|
328
|
+
void migrateLegacyPiAiProvider(ctx)
|
|
329
|
+
} catch (error) {
|
|
330
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Failed to register llm adapter: ' + (error?.message || error))
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
ensureAdapter()
|
|
335
|
+
applyTuiSettings(ctx, { live })
|
|
336
|
+
|
|
337
|
+
if (typeof ctx.inject === 'function') {
|
|
338
|
+
ctx.inject(['settings'], () => {
|
|
339
|
+
void migrateLegacyPiAiProvider(ctx)
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
syncProviderState(live())
|
|
344
|
+
if (typeof ctx.effect === 'function') {
|
|
345
|
+
ctx.effect(() => {
|
|
346
|
+
const timer = setTimeout(triggerAutoDiscover, 500)
|
|
347
|
+
const retryTimer = setTimeout(triggerAutoDiscover, 3000)
|
|
348
|
+
const lateRetry = setTimeout(() => {
|
|
349
|
+
const syncedAt = Number(publicConfig(live()).planSyncedAt || 0)
|
|
350
|
+
if (Date.now() - syncedAt >= CATALOG_STALE_MS) triggerAutoDiscover()
|
|
351
|
+
}, 15000)
|
|
352
|
+
const refreshTimer = setInterval(() => {
|
|
353
|
+
const syncedAt = Number(publicConfig(live()).planSyncedAt || 0)
|
|
354
|
+
if (Date.now() - syncedAt >= CATALOG_REFRESH_MS) triggerAutoDiscover()
|
|
355
|
+
}, 60 * 60 * 1000)
|
|
356
|
+
for (const handle of [timer, retryTimer, lateRetry, refreshTimer]) {
|
|
357
|
+
if (typeof handle.unref === 'function') handle.unref()
|
|
358
|
+
}
|
|
359
|
+
return () => {
|
|
360
|
+
clearTimeout(timer)
|
|
361
|
+
clearTimeout(retryTimer)
|
|
362
|
+
clearTimeout(lateRetry)
|
|
363
|
+
clearInterval(refreshTimer)
|
|
364
|
+
}
|
|
365
|
+
}, 'dsh-clinebot: auto-discover')
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const registerWebSurfaces = (webCtx) => {
|
|
369
|
+
if (!service(webCtx, 'webServer')?.register) return
|
|
370
|
+
const unregisterUpdater = registerPluginUpdater(webCtx, {
|
|
371
|
+
endpoint: '/dsh-clinebot/update',
|
|
372
|
+
packageName: name,
|
|
373
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
374
|
+
})
|
|
375
|
+
if (typeof webCtx.effect === 'function') {
|
|
376
|
+
webCtx.effect(() => () => unregisterUpdater?.(), 'dsh-clinebot: updater')
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
registerSettingsRoutes(webCtx, routeEnv)
|
|
380
|
+
registerAccountsRoutes(webCtx, routeEnv)
|
|
381
|
+
registerModelsRoutes(webCtx, routeEnv)
|
|
382
|
+
registerAuthRoutes(webCtx, routeEnv)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (service(ctx, 'webServer')?.register) {
|
|
386
|
+
registerWebSurfaces(ctx)
|
|
387
|
+
} else if (typeof ctx.inject === 'function') {
|
|
388
|
+
// Headless/TUI profiles mount no web server; the adapter and slash command
|
|
389
|
+
// still work, and web routes appear if a server mounts later.
|
|
390
|
+
ctx.inject(['webServer'], registerWebSurfaces)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
registerSlashCommand(ctx, {
|
|
394
|
+
live,
|
|
395
|
+
getSettingsApi: () => settingsApi,
|
|
396
|
+
syncProviderState,
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
// Native DSH WebSearchProvider over ctx.web (#233)
|
|
400
|
+
if (typeof ctx.inject === 'function') {
|
|
401
|
+
ctx.inject(['web'], (webCtx) => {
|
|
402
|
+
const cfg = publicConfig(live())
|
|
403
|
+
if (cfg.webSearch === false) return
|
|
404
|
+
const web = webCtx?.get?.('web')
|
|
405
|
+
if (web && typeof web.registerSearchProvider === 'function') {
|
|
406
|
+
const searchProvider = new ClineBotSearchProvider({
|
|
407
|
+
getBaseUrl: () => publicConfig(live()).baseUrl,
|
|
408
|
+
getApiKey: async () => {
|
|
409
|
+
const pool = await resolveAccountPool(ctx, live())
|
|
410
|
+
const active = pool.find((a) => a.present && a.value)
|
|
411
|
+
return active?.value || ''
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
try {
|
|
415
|
+
const unreg = web.registerSearchProvider(CLINEBOT_SEARCH_PROVIDER_ID, searchProvider)
|
|
416
|
+
if (typeof ctx.effect === 'function') {
|
|
417
|
+
ctx.effect(() => () => {
|
|
418
|
+
try { if (typeof unreg === 'function') unreg() } catch { /* explicit best-effort fallback: non-fatal by design */ }
|
|
419
|
+
}, 'dsh-clinebot: web search provider')
|
|
420
|
+
}
|
|
421
|
+
} catch (e) {
|
|
422
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Failed to register web search provider: ' + (e?.message || e))
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
}
|