@goodandready/dsh-clinebot 0.5.32 → 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 +31 -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 +187 -140
- package/lib/routes/accounts.js +523 -403
- package/lib/routes/models.js +2 -1
- package/lib/routes/proxy.js +8 -27
- 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/lib/updater.js +5 -1
- package/package.json +113 -77
package/lib/index.js
CHANGED
|
@@ -1,502 +1,569 @@
|
|
|
1
|
-
import { Config, publicConfig, plainConfig, volatileConfig, isVolatileRef, NS, LLM_PI_AI_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 {
|
|
11
|
-
rotateToNextAccount,
|
|
12
|
-
isQuotaExceededError,
|
|
13
|
-
} from './account-pool.js'
|
|
14
|
-
import { PROVIDER_ID } from './models.js'
|
|
15
|
-
import {
|
|
16
|
-
upsertPiAiProvider,
|
|
17
|
-
removePiAiProvider,
|
|
18
|
-
autoDiscoverPlanModels,
|
|
19
|
-
resolveActiveAccountKey,
|
|
20
|
-
} from './provider-sync.js'
|
|
21
|
-
import { registerSettingsRoutes } from './routes/settings.js'
|
|
22
|
-
import { registerAccountsRoutes } from './routes/accounts.js'
|
|
23
|
-
import { registerModelsRoutes } from './routes/models.js'
|
|
24
|
-
import { registerAuthRoutes } from './routes/auth.js'
|
|
25
|
-
import { registerProxyRoutes } from './routes/proxy.js'
|
|
26
|
-
import { startLoopbackProxyServer, stopLoopbackProxyServer } from './proxy-server.js'
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
return
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
if (typeof svc.replace
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
if (typeof svc.update
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
1
|
+
import { Config, publicConfig, plainConfig, volatileConfig, isVolatileRef, NS, LLM_PI_AI_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 {
|
|
11
|
+
rotateToNextAccount,
|
|
12
|
+
isQuotaExceededError,
|
|
13
|
+
} from './account-pool.js'
|
|
14
|
+
import { PROVIDER_ID } from './models.js'
|
|
15
|
+
import {
|
|
16
|
+
upsertPiAiProvider,
|
|
17
|
+
removePiAiProvider,
|
|
18
|
+
autoDiscoverPlanModels,
|
|
19
|
+
resolveActiveAccountKey,
|
|
20
|
+
} from './provider-sync.js'
|
|
21
|
+
import { registerSettingsRoutes } from './routes/settings.js'
|
|
22
|
+
import { registerAccountsRoutes } from './routes/accounts.js'
|
|
23
|
+
import { registerModelsRoutes } from './routes/models.js'
|
|
24
|
+
import { registerAuthRoutes } from './routes/auth.js'
|
|
25
|
+
import { registerProxyRoutes } from './routes/proxy.js'
|
|
26
|
+
import { startLoopbackProxyServer, stopLoopbackProxyServer } from './proxy-server.js'
|
|
27
|
+
import { ClineBotAdapter } from './adapter.js'
|
|
28
|
+
import { registerSlashCommand } from './slash-command.js'
|
|
29
|
+
import { ClineBotSearchProvider, CLINEBOT_SEARCH_PROVIDER_ID } from './web-search.js'
|
|
30
|
+
import { resolveAccountPool } from './account-pool.js'
|
|
31
|
+
import { recordUsage, loadStats, flushStats, resolvePath, switchStatsStorage } from './stats-storage.js'
|
|
32
|
+
import { service, migrateLegacyPiAiProvider } from './services.js'
|
|
33
|
+
import { applyTuiSettings } from './tui-settings.js'
|
|
34
|
+
|
|
35
|
+
export const name = '@goodandready/dsh-clinebot'
|
|
36
|
+
export const inject = []
|
|
37
|
+
|
|
38
|
+
export function configReader(config) {
|
|
39
|
+
return () => config
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { NS, LLM_PI_AI_NS, Config, sessionStats, recordSessionRequest, resetSessionStats, rotateToNextAccount }
|
|
43
|
+
|
|
44
|
+
export function apply(ctx, config) {
|
|
45
|
+
let currentConfig = config || {}
|
|
46
|
+
const overrides = {}
|
|
47
|
+
const getConfig = () => {
|
|
48
|
+
if (!currentConfig) return currentConfig
|
|
49
|
+
if (Object.isFrozen(currentConfig)) {
|
|
50
|
+
return new Proxy({}, {
|
|
51
|
+
get(_target, prop) {
|
|
52
|
+
const orig = Reflect.get(currentConfig, prop)
|
|
53
|
+
if (isVolatileRef(orig) || Object.getOwnPropertyDescriptor(currentConfig, prop)?.get) {
|
|
54
|
+
return orig
|
|
55
|
+
}
|
|
56
|
+
if (prop in overrides) {
|
|
57
|
+
return overrides[prop]
|
|
58
|
+
}
|
|
59
|
+
return orig
|
|
60
|
+
},
|
|
61
|
+
has(_target, prop) {
|
|
62
|
+
return prop in overrides || Reflect.has(currentConfig, prop)
|
|
63
|
+
},
|
|
64
|
+
ownKeys() {
|
|
65
|
+
return Array.from(new Set([...Reflect.ownKeys(currentConfig), ...Object.keys(overrides)]))
|
|
66
|
+
},
|
|
67
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
68
|
+
if (prop in overrides) {
|
|
69
|
+
return { configurable: true, enumerable: true, value: overrides[prop], writable: true }
|
|
70
|
+
}
|
|
71
|
+
const desc = Object.getOwnPropertyDescriptor(currentConfig, prop)
|
|
72
|
+
if (desc) {
|
|
73
|
+
return { configurable: true, enumerable: desc.enumerable ?? true, value: Reflect.get(currentConfig, prop) }
|
|
74
|
+
}
|
|
75
|
+
return undefined
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
return currentConfig
|
|
80
|
+
}
|
|
81
|
+
const live = () => plainConfig(Config(plainConfig(getConfig() || config || {})))
|
|
82
|
+
let settingsApi
|
|
83
|
+
let adapterRegistration = null
|
|
84
|
+
let adapterDirectory = null
|
|
85
|
+
|
|
86
|
+
const syncProviderState = async (cfg) => {
|
|
87
|
+
try {
|
|
88
|
+
const pub = publicConfig(cfg)
|
|
89
|
+
if (pub.transport !== 'pi-ai') {
|
|
90
|
+
// Native ctx.llm adapter: the route set follows `enabled`; no
|
|
91
|
+
// llm-pi-ai mutation and no loopback proxy are involved.
|
|
92
|
+
if (adapterRegistration) {
|
|
93
|
+
adapterRegistration.replace(pub.enabled ? [PROVIDER_ID] : [])
|
|
94
|
+
}
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (!pub.enabled) {
|
|
98
|
+
await removePiAiProvider(ctx)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
if (pub.proxyMode !== false && (service(ctx, 'webServer') || typeof ctx?.get === 'function')) {
|
|
102
|
+
await startLoopbackProxyServer(ctx, routeEnv)
|
|
103
|
+
} else {
|
|
104
|
+
await stopLoopbackProxyServer()
|
|
105
|
+
}
|
|
106
|
+
const activeKey = await resolveActiveAccountKey(ctx, cfg)
|
|
107
|
+
if (activeKey.value) {
|
|
108
|
+
await upsertPiAiProvider(ctx, cfg, pub.enabledModels)
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
/* ignore transient settings unavailable */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const triggerAutoDiscover = () => {
|
|
116
|
+
autoDiscoverPlanModels(ctx, { live, getSettingsApi: () => settingsApi, syncProviderState })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let activeStatsPath = resolvePath(publicConfig(live()).statsPath)
|
|
120
|
+
loadStats(activeStatsPath)
|
|
121
|
+
|
|
122
|
+
const syncStatsStorage = (cfg) => {
|
|
123
|
+
const nextPath = resolvePath(publicConfig(cfg).statsPath)
|
|
124
|
+
if (nextPath !== activeStatsPath) {
|
|
125
|
+
switchStatsStorage(nextPath)
|
|
126
|
+
activeStatsPath = nextPath
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const normalizeEndpoint = (url) => String(url || '').trim().replace(/\/+$/, '')
|
|
131
|
+
const getCatalogIdentity = (cfg) => {
|
|
132
|
+
const pub = publicConfig(cfg)
|
|
133
|
+
return `${normalizeEndpoint(pub.baseUrl)}::${String(pub.activeAccount || '').trim()}::${String(pub.apiKeyEnv || '').trim()}`
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let lastCatalogIdentity = getCatalogIdentity(live())
|
|
137
|
+
const resetCatalogSnapshot = () => {
|
|
138
|
+
try {
|
|
139
|
+
if (currentConfig && typeof currentConfig === 'object') {
|
|
140
|
+
currentConfig.dynamicModels = []
|
|
141
|
+
currentConfig.planSyncedAt = 0
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
/* ignore read-only or frozen */
|
|
145
|
+
}
|
|
146
|
+
overrides.dynamicModels = []
|
|
147
|
+
overrides.planSyncedAt = 0
|
|
148
|
+
clearUsageCache()
|
|
149
|
+
clearProbeCache()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof ctx?.on === 'function') {
|
|
153
|
+
ctx.on('loader/volatile-update', () => {
|
|
154
|
+
const liveCfg = live()
|
|
155
|
+
const currentIdentity = getCatalogIdentity(liveCfg)
|
|
156
|
+
if (currentIdentity !== lastCatalogIdentity) {
|
|
157
|
+
lastCatalogIdentity = currentIdentity
|
|
158
|
+
resetCatalogSnapshot()
|
|
159
|
+
}
|
|
160
|
+
syncStatsStorage(liveCfg)
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const getRevision = (svc) => {
|
|
165
|
+
try {
|
|
166
|
+
return svc?.describe?.().find((row) => row.ns === NS)?.revision
|
|
167
|
+
} catch {
|
|
168
|
+
return undefined
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const applyConfigMutations = (target, source) => {
|
|
173
|
+
const targetConfig = typeof target === 'function' ? target() : target
|
|
174
|
+
if (targetConfig && typeof targetConfig === 'object') {
|
|
175
|
+
if (Object.isFrozen(targetConfig)) {
|
|
176
|
+
for (const [key, value] of Object.entries(source)) {
|
|
177
|
+
const orig = targetConfig[key]
|
|
178
|
+
if (isVolatileRef(orig) || Object.getOwnPropertyDescriptor(targetConfig, key)?.get) {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
overrides[key] = value
|
|
182
|
+
}
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
for (const [key, value] of Object.entries(source)) {
|
|
186
|
+
try {
|
|
187
|
+
if (isVolatileRef(targetConfig[key])) {
|
|
188
|
+
// Cosmokit native Volatile references are reactive and authoritative;
|
|
189
|
+
// preserve the reference without overwriting with plain value.
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
const desc = Object.getOwnPropertyDescriptor(targetConfig, key)
|
|
193
|
+
if (desc?.get && !desc.set) {
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
if (desc?.set) {
|
|
197
|
+
targetConfig[key] = value
|
|
198
|
+
} else {
|
|
199
|
+
targetConfig[key] = value
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
/* ignore read-only */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
settingsApi = {
|
|
209
|
+
get: () => live(),
|
|
210
|
+
replace: async (next) => {
|
|
211
|
+
const svc = ctx?.get?.('settings')
|
|
212
|
+
if (!svc || (typeof svc.replace !== 'function' && typeof svc.update !== 'function')) {
|
|
213
|
+
throw new Error('DSH settings service is unavailable or non-writable')
|
|
214
|
+
}
|
|
215
|
+
const prevIdentity = getCatalogIdentity(live())
|
|
216
|
+
const parsed = Config(structuredClone(plainConfig(next)))
|
|
217
|
+
const nextIdentity = getCatalogIdentity(parsed)
|
|
218
|
+
const identityChanged = nextIdentity !== prevIdentity
|
|
219
|
+
if (identityChanged) {
|
|
220
|
+
if (!('dynamicModels' in next)) parsed.dynamicModels = []
|
|
221
|
+
if (!('planSyncedAt' in next)) parsed.planSyncedAt = 0
|
|
222
|
+
}
|
|
223
|
+
const payload = volatileConfig(parsed)
|
|
224
|
+
const rev = getRevision(svc)
|
|
225
|
+
if (typeof svc.replace === 'function') {
|
|
226
|
+
await svc.replace(NS, payload, rev)
|
|
227
|
+
} else {
|
|
228
|
+
await svc.update(NS, payload, rev)
|
|
229
|
+
}
|
|
230
|
+
if (identityChanged) {
|
|
231
|
+
lastCatalogIdentity = nextIdentity
|
|
232
|
+
resetCatalogSnapshot()
|
|
233
|
+
}
|
|
234
|
+
applyConfigMutations(currentConfig, parsed)
|
|
235
|
+
syncStatsStorage(live())
|
|
236
|
+
await syncProviderState(live())
|
|
237
|
+
return plainConfig(parsed)
|
|
238
|
+
},
|
|
239
|
+
update: async (patch) => {
|
|
240
|
+
const svc = ctx?.get?.('settings')
|
|
241
|
+
if (!svc || (typeof svc.update !== 'function' && typeof svc.replace !== 'function')) {
|
|
242
|
+
throw new Error('DSH settings service is unavailable or non-writable')
|
|
243
|
+
}
|
|
244
|
+
const prevIdentity = getCatalogIdentity(live())
|
|
245
|
+
const merged = Config({ ...publicConfig(live()), ...plainConfig(patch) })
|
|
246
|
+
const nextIdentity = getCatalogIdentity(merged)
|
|
247
|
+
const identityChanged = nextIdentity !== prevIdentity
|
|
248
|
+
if (identityChanged) {
|
|
249
|
+
if (!('dynamicModels' in patch)) merged.dynamicModels = []
|
|
250
|
+
if (!('planSyncedAt' in patch)) merged.planSyncedAt = 0
|
|
251
|
+
}
|
|
252
|
+
const payload = volatileConfig(merged)
|
|
253
|
+
const rev = getRevision(svc)
|
|
254
|
+
if (typeof svc.update === 'function') {
|
|
255
|
+
await svc.update(NS, payload, rev)
|
|
256
|
+
} else {
|
|
257
|
+
await svc.replace(NS, payload, rev)
|
|
258
|
+
}
|
|
259
|
+
if (identityChanged) {
|
|
260
|
+
lastCatalogIdentity = nextIdentity
|
|
261
|
+
resetCatalogSnapshot()
|
|
262
|
+
}
|
|
263
|
+
applyConfigMutations(currentConfig, merged)
|
|
264
|
+
syncStatsStorage(live())
|
|
265
|
+
await syncProviderState(live())
|
|
266
|
+
return plainConfig(merged)
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (typeof ctx.effect === 'function') {
|
|
272
|
+
ctx.effect(() => {
|
|
273
|
+
return () => {
|
|
274
|
+
flushStats(activeStatsPath)
|
|
275
|
+
}
|
|
276
|
+
}, 'dsh-clinebot: stats-storage')
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const routeEnv = {
|
|
280
|
+
live,
|
|
281
|
+
getSettingsApi: () => settingsApi,
|
|
282
|
+
syncProviderState,
|
|
283
|
+
triggerAutoDiscover,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const ensureAdapter = () => {
|
|
287
|
+
if (publicConfig(live()).transport === 'pi-ai') return
|
|
288
|
+
if (typeof ctx.inject !== 'function') return
|
|
289
|
+
ctx.inject(['llm'], (llmCtx) => {
|
|
290
|
+
const llm = llmCtx?.llm || llmCtx?.get?.('llm')
|
|
291
|
+
if (!llm || typeof llm.registerAdapter !== 'function') return
|
|
292
|
+
try {
|
|
293
|
+
const adapter = new ClineBotAdapter({
|
|
294
|
+
ctx,
|
|
295
|
+
live,
|
|
296
|
+
resolveAttachments: () => ctx?.get?.('attachments') || ctx?.attachments,
|
|
297
|
+
logger: ctx?.logger,
|
|
298
|
+
})
|
|
299
|
+
adapterRegistration = llm.registerAdapter([PROVIDER_ID], adapter)
|
|
300
|
+
if (typeof llm.registerConfigurableProviders === 'function') {
|
|
301
|
+
adapterDirectory = llm.registerConfigurableProviders([{
|
|
302
|
+
provider: PROVIDER_ID,
|
|
303
|
+
displayName: 'ClineBot (ClinePass)',
|
|
304
|
+
settingsNs: NS,
|
|
305
|
+
settingsPath: [],
|
|
306
|
+
defaultBaseURL: 'https://api.cline.bot/api/v1',
|
|
307
|
+
}])
|
|
308
|
+
}
|
|
309
|
+
ctx.effect(() => () => {
|
|
310
|
+
try { adapterDirectory?.() } catch {}
|
|
311
|
+
try { adapterRegistration?.() } catch {}
|
|
312
|
+
adapterDirectory = null
|
|
313
|
+
adapterRegistration = null
|
|
314
|
+
}, 'dsh-clinebot: llm adapter')
|
|
315
|
+
void syncProviderState(live())
|
|
316
|
+
void migrateLegacyPiAiProvider(ctx)
|
|
317
|
+
} catch (error) {
|
|
318
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Failed to register llm adapter: ' + (error?.message || error))
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
ensureAdapter()
|
|
323
|
+
applyTuiSettings(ctx, { live })
|
|
324
|
+
|
|
325
|
+
if (typeof ctx.inject === 'function' && publicConfig(live()).transport !== 'pi-ai') {
|
|
326
|
+
ctx.inject(['settings'], () => {
|
|
327
|
+
void migrateLegacyPiAiProvider(ctx)
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (typeof ctx.effect === 'function') {
|
|
332
|
+
ctx.effect(() => {
|
|
333
|
+
return () => {
|
|
334
|
+
stopLoopbackProxyServer()
|
|
335
|
+
}
|
|
336
|
+
}, 'dsh-clinebot: loopback-proxy-server')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
syncProviderState(live())
|
|
340
|
+
if (typeof ctx.effect === 'function') {
|
|
341
|
+
ctx.effect(() => {
|
|
342
|
+
const timer = setTimeout(triggerAutoDiscover, 500)
|
|
343
|
+
const retryTimer = setTimeout(triggerAutoDiscover, 3000)
|
|
344
|
+
return () => {
|
|
345
|
+
clearTimeout(timer)
|
|
346
|
+
clearTimeout(retryTimer)
|
|
347
|
+
}
|
|
348
|
+
}, 'dsh-clinebot: auto-discover')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const registerWebSurfaces = (webCtx) => {
|
|
352
|
+
if (!service(webCtx, 'webServer')?.register) return
|
|
353
|
+
const unregisterUpdater = registerPluginUpdater(webCtx, {
|
|
354
|
+
endpoint: '/dsh-clinebot/update',
|
|
355
|
+
packageName: name,
|
|
356
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
357
|
+
})
|
|
358
|
+
if (typeof webCtx.effect === 'function') {
|
|
359
|
+
webCtx.effect(() => () => unregisterUpdater?.(), 'dsh-clinebot: updater')
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
registerSettingsRoutes(webCtx, routeEnv)
|
|
363
|
+
registerAccountsRoutes(webCtx, routeEnv)
|
|
364
|
+
registerModelsRoutes(webCtx, routeEnv)
|
|
365
|
+
registerAuthRoutes(webCtx, routeEnv)
|
|
366
|
+
registerProxyRoutes(webCtx, routeEnv)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (service(ctx, 'webServer')?.register) {
|
|
370
|
+
registerWebSurfaces(ctx)
|
|
371
|
+
} else if (typeof ctx.inject === 'function') {
|
|
372
|
+
// Headless/TUI profiles mount no web server; the adapter and slash command
|
|
373
|
+
// still work, and web routes appear if a server mounts later.
|
|
374
|
+
ctx.inject(['webServer'], registerWebSurfaces)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
registerSlashCommand(ctx, {
|
|
378
|
+
live,
|
|
379
|
+
getSettingsApi: () => settingsApi,
|
|
380
|
+
syncProviderState,
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
let lastFailoverAt = 0
|
|
384
|
+
|
|
385
|
+
const handleStream429 = async () => {
|
|
386
|
+
const now = Date.now()
|
|
387
|
+
if (now - lastFailoverAt < 30000) {
|
|
388
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Suppressing rapid failover rotation within 30s storm window')
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
lastFailoverAt = now
|
|
392
|
+
try {
|
|
393
|
+
const failover = await rotateToNextAccount(ctx, live(), 'stream_429', settingsApi)
|
|
394
|
+
if (failover?.rotated) {
|
|
395
|
+
await syncProviderState(live())
|
|
396
|
+
}
|
|
397
|
+
} catch (err) {
|
|
398
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Auto-failover rotation failed: ' + (err?.message || err))
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (typeof ctx.on === 'function' && publicConfig(live()).transport === 'pi-ai') {
|
|
403
|
+
const unlisten = ctx.on('llm/stream', (options, next) => {
|
|
404
|
+
const isCline = options?.provider === PROVIDER_ID
|
|
405
|
+
const stream = next()
|
|
406
|
+
const startTime = Date.now()
|
|
407
|
+
let promptTokens = 0
|
|
408
|
+
let completionTokens = 0
|
|
409
|
+
let finished = false
|
|
410
|
+
|
|
411
|
+
return (async function* () {
|
|
412
|
+
try {
|
|
413
|
+
for await (const chunk of stream) {
|
|
414
|
+
if (isCline && chunk?.type === 'usage' && chunk?.usage) {
|
|
415
|
+
const u = chunk.usage
|
|
416
|
+
promptTokens += (Number(u.inputTokens) || 0) + (Number(u.cacheReadTokens) || 0) + (Number(u.cacheWriteTokens) || 0)
|
|
417
|
+
completionTokens += Number(u.outputTokens) || 0
|
|
418
|
+
}
|
|
419
|
+
if (isCline && chunk?.type === 'finish' && chunk?.reason) {
|
|
420
|
+
finished = true
|
|
421
|
+
const latencyMs = Date.now() - startTime
|
|
422
|
+
const reason = chunk.reason
|
|
423
|
+
const failure = reason.failure || {}
|
|
424
|
+
const isError = reason.kind === 'error'
|
|
425
|
+
const isAborted = reason.kind === 'aborted'
|
|
426
|
+
const is429 = failure.status === 429 || failure.code === 429 || failure.code === 'rate_limit_exceeded'
|
|
427
|
+
const isExceeded = isQuotaExceededError(`${failure.message || ''} ${failure.code || ''}`)
|
|
428
|
+
|
|
429
|
+
const isProxy = publicConfig(live()).proxyMode !== false
|
|
430
|
+
if (isError) {
|
|
431
|
+
recordSessionRequest({
|
|
432
|
+
latencyMs,
|
|
433
|
+
ok: false,
|
|
434
|
+
error: failure.message || 'Stream error',
|
|
435
|
+
promptTokens,
|
|
436
|
+
completionTokens,
|
|
437
|
+
model: options?.model,
|
|
438
|
+
})
|
|
439
|
+
if (!isProxy) {
|
|
440
|
+
recordUsage({
|
|
441
|
+
model: options?.model,
|
|
442
|
+
promptTokens,
|
|
443
|
+
completionTokens,
|
|
444
|
+
totalTokens: promptTokens + completionTokens,
|
|
445
|
+
isError: true,
|
|
446
|
+
is429,
|
|
447
|
+
statsPath: publicConfig(live()).statsPath,
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
if (is429 || isExceeded) {
|
|
451
|
+
handleStream429()
|
|
452
|
+
}
|
|
453
|
+
} else if (isAborted) {
|
|
454
|
+
recordSessionRequest({
|
|
455
|
+
latencyMs,
|
|
456
|
+
ok: true,
|
|
457
|
+
aborted: true,
|
|
458
|
+
promptTokens,
|
|
459
|
+
completionTokens,
|
|
460
|
+
model: options?.model,
|
|
461
|
+
})
|
|
462
|
+
} else {
|
|
463
|
+
recordSessionRequest({
|
|
464
|
+
latencyMs,
|
|
465
|
+
ok: true,
|
|
466
|
+
promptTokens,
|
|
467
|
+
completionTokens,
|
|
468
|
+
model: options?.model,
|
|
469
|
+
})
|
|
470
|
+
if (!isProxy) {
|
|
471
|
+
recordUsage({
|
|
472
|
+
model: options?.model,
|
|
473
|
+
promptTokens,
|
|
474
|
+
completionTokens,
|
|
475
|
+
totalTokens: promptTokens + completionTokens,
|
|
476
|
+
statsPath: publicConfig(live()).statsPath,
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
yield chunk
|
|
482
|
+
}
|
|
483
|
+
if (isCline && !finished) {
|
|
484
|
+
recordSessionRequest({
|
|
485
|
+
latencyMs: Date.now() - startTime,
|
|
486
|
+
ok: true,
|
|
487
|
+
promptTokens,
|
|
488
|
+
completionTokens,
|
|
489
|
+
model: options?.model,
|
|
490
|
+
})
|
|
491
|
+
const isProxy = publicConfig(live()).proxyMode !== false
|
|
492
|
+
if (!isProxy) {
|
|
493
|
+
recordUsage({
|
|
494
|
+
model: options?.model,
|
|
495
|
+
promptTokens,
|
|
496
|
+
completionTokens,
|
|
497
|
+
totalTokens: promptTokens + completionTokens,
|
|
498
|
+
statsPath: publicConfig(live()).statsPath,
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} catch (err) {
|
|
503
|
+
if (isCline) {
|
|
504
|
+
const latencyMs = Date.now() - startTime
|
|
505
|
+
const status = err?.status || err?.statusCode
|
|
506
|
+
const msg = String(err?.message || err)
|
|
507
|
+
recordSessionRequest({
|
|
508
|
+
latencyMs,
|
|
509
|
+
ok: false,
|
|
510
|
+
error: msg,
|
|
511
|
+
promptTokens,
|
|
512
|
+
completionTokens,
|
|
513
|
+
model: options?.model,
|
|
514
|
+
})
|
|
515
|
+
const isProxy = publicConfig(live()).proxyMode !== false
|
|
516
|
+
if (!isProxy) {
|
|
517
|
+
recordUsage({
|
|
518
|
+
model: options?.model,
|
|
519
|
+
promptTokens,
|
|
520
|
+
completionTokens,
|
|
521
|
+
totalTokens: promptTokens + completionTokens,
|
|
522
|
+
isError: true,
|
|
523
|
+
is429: status === 429,
|
|
524
|
+
statsPath: publicConfig(live()).statsPath,
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
if (status === 429 || isQuotaExceededError(msg)) {
|
|
528
|
+
handleStream429()
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
throw err
|
|
532
|
+
}
|
|
533
|
+
})()
|
|
534
|
+
}, { global: true, prepend: true })
|
|
535
|
+
|
|
536
|
+
if (typeof ctx.effect === 'function') {
|
|
537
|
+
ctx.effect(() => () => unlisten?.(), 'dsh-clinebot: llm/stream failover')
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Native DSH WebSearchProvider over ctx.web (#233)
|
|
542
|
+
if (typeof ctx.inject === 'function') {
|
|
543
|
+
ctx.inject(['web'], (webCtx) => {
|
|
544
|
+
const cfg = publicConfig(live())
|
|
545
|
+
if (cfg.webSearch === false) return
|
|
546
|
+
const web = webCtx?.get?.('web')
|
|
547
|
+
if (web && typeof web.registerSearchProvider === 'function') {
|
|
548
|
+
const searchProvider = new ClineBotSearchProvider({
|
|
549
|
+
getBaseUrl: () => publicConfig(live()).baseUrl,
|
|
550
|
+
getApiKey: async () => {
|
|
551
|
+
const pool = await resolveAccountPool(ctx, live())
|
|
552
|
+
const active = pool.find((a) => a.present && a.value)
|
|
553
|
+
return active?.value || ''
|
|
554
|
+
},
|
|
555
|
+
})
|
|
556
|
+
try {
|
|
557
|
+
const unreg = web.registerSearchProvider(CLINEBOT_SEARCH_PROVIDER_ID, searchProvider)
|
|
558
|
+
if (typeof ctx.effect === 'function') {
|
|
559
|
+
ctx.effect(() => () => {
|
|
560
|
+
try { if (typeof unreg === 'function') unreg() } catch {}
|
|
561
|
+
}, 'dsh-clinebot: web search provider')
|
|
562
|
+
}
|
|
563
|
+
} catch (e) {
|
|
564
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Failed to register web search provider: ' + (e?.message || e))
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
}
|