@goodandready/dsh-clinebot 0.2.1 → 0.3.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/lib/client.js CHANGED
@@ -10,6 +10,32 @@ window.__ModuleLoader__.load({
10
10
  const SUBTITLE = 'ClinePass subscription ($9.99/mo) with curated & custom models.'
11
11
  const ROUTE_PREFIX = '/dsh-clinebot'
12
12
 
13
+ const en = {
14
+ title: 'ClineBot',
15
+ subtitle: 'ClinePass subscription ($9.99/mo) with curated & custom models.',
16
+ 'settings.loading': 'Loading ClineBot settings…',
17
+ 'settings.unavailable': 'ClineBot settings unavailable (host namespace is not ready).',
18
+ }
19
+
20
+ const ru = {
21
+ title: 'ClineBot',
22
+ subtitle: 'Подписка ClinePass ($9.99/мес) с каталогом проверенных моделей.',
23
+ 'settings.loading': 'Загрузка настроек ClineBot…',
24
+ 'settings.unavailable': 'Настройки ClineBot недоступны (пространство хоста ещё не готово).',
25
+ }
26
+
27
+ function makeT(dict, fallback) {
28
+ return function t(key, vars) {
29
+ let val = (dict && dict[key]) || (fallback && fallback[key]) || key
30
+ if (vars && typeof val === 'string') {
31
+ for (const k of Object.keys(vars)) {
32
+ val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]))
33
+ }
34
+ }
35
+ return val
36
+ }
37
+ }
38
+
13
39
  function FallbackChevron() {
14
40
  return React.createElement(
15
41
  'svg',
@@ -52,12 +78,13 @@ window.__ModuleLoader__.load({
52
78
  .cb-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}
53
79
  .cb-input-group{display:flex;gap:8px;align-items:center}
54
80
 
55
- .cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px}
56
- .cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-1)}
81
+ .cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
82
+ .cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
57
83
  .cb-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
58
- .cb-btn-primary:hover:not(:disabled){opacity:0.9}
84
+ .cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88;visibility:visible !important}
59
85
  .cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}
60
- .cb-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.1)}
86
+ .cb-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12) !important;border-color:rgba(239,68,68,0.5)}
87
+ .cb-btn:disabled{opacity:0.5;cursor:not-allowed}
61
88
 
62
89
  .cb-bar-container{display:flex;flex-direction:column;gap:6px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
63
90
  .cb-bar-head{display:flex;justify-content:space-between;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
@@ -72,6 +99,12 @@ window.__ModuleLoader__.load({
72
99
 
73
100
  .cb-alert-ok{padding:10px 14px;border-radius:8px;background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px}
74
101
  .cb-alert-bad{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}
102
+ .cb-alert-err{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}
103
+ .cb-banner-warning{padding:12px 16px;border-radius:8px;background:rgba(245,158,11,0.12);border:1px solid var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:500}
104
+ .cb-banner-exhausted{padding:12px 16px;border-radius:8px;background:rgba(239,68,68,0.12);border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:600}
105
+ .cb-stat-box{padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}
106
+ .cb-stat-val{font-size:18px;font-weight:700;color:var(--dsw-alias-label-primary)}
107
+ .cb-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}
75
108
  .cb-preview{padding:12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);font-family:monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;border:1px solid var(--dsw-alias-border-l2)}
76
109
  `
77
110
  document.head.appendChild(style)
@@ -111,7 +144,22 @@ window.__ModuleLoader__.load({
111
144
  )
112
145
  }
113
146
 
114
- function SettingsPage() {
147
+ function SettingsPage(props) {
148
+ const ctx = props?.ctx
149
+ const t = props?.t || makeT(ru, en)
150
+
151
+ const scope = React.useMemo(
152
+ () => (ctx && ctx.settingsScope ? ctx.settingsScope.bind({ namespace: NS }) : undefined),
153
+ [ctx]
154
+ )
155
+
156
+ const snapshot = React.useSyncExternalStore(
157
+ React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
158
+ React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'ready' }), [scope]),
159
+ React.useCallback(() => ({ status: 'loading' }), [])
160
+ )
161
+ const snapshotStatus = snapshot?.status || 'ready'
162
+
115
163
  const [status, setStatus] = React.useState(null)
116
164
  const [draft, setDraft] = React.useState(null)
117
165
  const [busy, setBusy] = React.useState('')
@@ -123,13 +171,6 @@ window.__ModuleLoader__.load({
123
171
  const [apiKeyInput, setApiKeyInput] = React.useState('')
124
172
  const [showKey, setShowKey] = React.useState(false)
125
173
 
126
- // Custom model form state
127
- const [customId, setCustomId] = React.useState('')
128
- const [customName, setCustomName] = React.useState('')
129
- const [customContext, setCustomContext] = React.useState('200000')
130
- const [customVision, setCustomVision] = React.useState(false)
131
- const [customCategory, setCustomCategory] = React.useState('coding')
132
-
133
174
  React.useEffect(() => {
134
175
  ensureCss()
135
176
  }, [])
@@ -195,6 +236,24 @@ window.__ModuleLoader__.load({
195
236
  }
196
237
  }
197
238
 
239
+ // Sync official models from ClinePass plan
240
+ async function handleSyncPlanModels() {
241
+ setBusy('sync-models')
242
+ setErr('')
243
+ setMsg('')
244
+ try {
245
+ const res = await fetch(`${ROUTE_PREFIX}/models/sync`, { method: 'POST' })
246
+ const data = await res.json().catch(() => ({}))
247
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
248
+ setMsg(`Синхронизировано моделей подписки: ${data.totalModelsCount} (${data.discoveredCount} получено напрямую из плана ClinePass)`)
249
+ await load()
250
+ } catch (e) {
251
+ setErr(String(e.message || e))
252
+ } finally {
253
+ setBusy('')
254
+ }
255
+ }
256
+
198
257
  // Register/Sync Provider in DSH
199
258
  async function handleRegister() {
200
259
  setBusy('register')
@@ -251,6 +310,7 @@ window.__ModuleLoader__.load({
251
310
  if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
252
311
  setSmokeResult(data)
253
312
  setMsg(`Smoke тест пройден! Задержка: ${data.latencyMs} мс`)
313
+ await load()
254
314
  } catch (e) {
255
315
  setErr(String(e.message || e))
256
316
  } finally {
@@ -270,6 +330,9 @@ window.__ModuleLoader__.load({
270
330
  const nextList = Array.from(current)
271
331
  setDraft({ ...draft, enabledModels: nextList })
272
332
 
333
+ if (scope && snapshotStatus === 'ready') {
334
+ try { await scope.set('enabledModels', nextList) } catch (_) {}
335
+ }
273
336
  try {
274
337
  await fetch(`${ROUTE_PREFIX}/models/toggle`, {
275
338
  method: 'POST',
@@ -286,7 +349,7 @@ window.__ModuleLoader__.load({
286
349
  if (type === 'all') {
287
350
  nextList = status.availableModels.map((m) => m.id)
288
351
  } else if (type === 'vision') {
289
- nextList = status.availableModels.filter((m) => m.input?.includes('vision')).map((m) => m.id)
352
+ nextList = status.availableModels.filter((m) => m.input?.includes('image') || m.input?.includes('vision')).map((m) => m.id)
290
353
  } else if (type === 'coding') {
291
354
  nextList = status.availableModels.filter((m) => m.category === 'coding').map((m) => m.id)
292
355
  } else if (type === 'recommended') {
@@ -294,6 +357,9 @@ window.__ModuleLoader__.load({
294
357
  }
295
358
 
296
359
  setDraft({ ...draft, enabledModels: nextList })
360
+ if (scope && snapshotStatus === 'ready') {
361
+ try { await scope.set('enabledModels', nextList) } catch (_) {}
362
+ }
297
363
  try {
298
364
  await fetch(`${ROUTE_PREFIX}/models/toggle`, {
299
365
  method: 'POST',
@@ -303,61 +369,15 @@ window.__ModuleLoader__.load({
303
369
  } catch {}
304
370
  }
305
371
 
306
- // Add custom model
307
- async function handleAddCustomModel() {
308
- if (!customId.trim()) {
309
- setErr('Введите Model ID')
310
- return
311
- }
312
- setBusy('add-model')
313
- setErr('')
314
- try {
315
- const res = await fetch(`${ROUTE_PREFIX}/models/custom`, {
316
- method: 'POST',
317
- headers: { 'Content-Type': 'application/json' },
318
- body: JSON.stringify({
319
- id: customId.trim(),
320
- name: customName.trim() || customId.trim(),
321
- contextLength: Number(customContext) || 200000,
322
- hasVision: customVision,
323
- category: customCategory,
324
- }),
325
- })
326
- const data = await res.json().catch(() => ({}))
327
- if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
328
- setMsg(`Модель ${data.model?.name} успешно добавлена в каталог`)
329
- setCustomId('')
330
- setCustomName('')
331
- await load()
332
- } catch (e) {
333
- setErr(String(e.message || e))
334
- } finally {
335
- setBusy('')
336
- }
337
- }
338
-
339
- // Remove custom model
340
- async function handleRemoveCustomModel(id) {
341
- setBusy(`remove-${id}`)
342
- setErr('')
343
- try {
344
- const res = await fetch(`${ROUTE_PREFIX}/models/custom`, {
345
- method: 'DELETE',
346
- headers: { 'Content-Type': 'application/json' },
347
- body: JSON.stringify({ id }),
348
- })
349
- const data = await res.json().catch(() => ({}))
350
- if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
351
- setMsg(`Модель ${id} удалена`)
352
- await load()
353
- } catch (e) {
354
- setErr(String(e.message || e))
355
- } finally {
356
- setBusy('')
357
- }
372
+ if (snapshotStatus === 'unavailable') {
373
+ return React.createElement(
374
+ 'div',
375
+ { className: 'cb-page' },
376
+ React.createElement('div', { className: 'cb-banner-warning' }, t('settings.unavailable'))
377
+ )
358
378
  }
359
379
 
360
- if (!status || !draft) {
380
+ if (!status || !draft || snapshotStatus === 'loading') {
361
381
  if (err) {
362
382
  return React.createElement(
363
383
  'div',
@@ -490,6 +510,27 @@ window.__ModuleLoader__.load({
490
510
  )
491
511
  ),
492
512
 
513
+ // Quota Warning Banner
514
+ status.quotaWarning
515
+ ? React.createElement(
516
+ 'div',
517
+ { className: status.quotaWarning.level === 'exhausted' ? 'cb-banner-exhausted' : 'cb-banner-warning' },
518
+ React.createElement('span', { style: { fontSize: '16px' } }, status.quotaWarning.level === 'exhausted' ? '🚨' : '⚠️'),
519
+ React.createElement(
520
+ 'div',
521
+ { style: { flex: 1 } },
522
+ React.createElement('div', null, status.quotaWarning.message),
523
+ status.quotaWarning.resetsAt
524
+ ? React.createElement(
525
+ 'div',
526
+ { style: { fontSize: '11px', opacity: 0.9, marginTop: '2px' } },
527
+ `Ожидаемое время сброса лимита: ${new Date(status.quotaWarning.resetsAt).toLocaleTimeString()}`
528
+ )
529
+ : null
530
+ )
531
+ )
532
+ : null,
533
+
493
534
  // Card 2: Subscription Limits & Usage Dashboard
494
535
  keyPresent
495
536
  ? React.createElement(
@@ -536,7 +577,7 @@ window.__ModuleLoader__.load({
536
577
  )
537
578
  : null,
538
579
 
539
- // Card 3: Model Picker Management
580
+ // Card 3: Model Picker Management with Official Plan Sync
540
581
  React.createElement(
541
582
  'div',
542
583
  { className: 'cb-section-card' },
@@ -547,6 +588,16 @@ window.__ModuleLoader__.load({
547
588
  React.createElement(
548
589
  'div',
549
590
  { className: 'cb-row' },
591
+ React.createElement(
592
+ 'button',
593
+ {
594
+ type: 'button',
595
+ className: 'cb-btn',
596
+ disabled: !!busy || !keyPresent,
597
+ onClick: handleSyncPlanModels,
598
+ },
599
+ busy === 'sync-models' ? 'Синхронизация…' : '🔄 Получить модели из плана'
600
+ ),
550
601
  React.createElement(
551
602
  'button',
552
603
  { type: 'button', className: 'cb-btn', onClick: () => handleSetModelsFilter('all') },
@@ -572,7 +623,7 @@ window.__ModuleLoader__.load({
572
623
  React.createElement(
573
624
  'div',
574
625
  { className: 'cb-section-desc' },
575
- 'Отметьте галочками модели, которые должны отображаться в выпадающем списке выбора моделей в чате DeepSeek Harness.'
626
+ 'Список формируется напрямую из официальной подписки ClinePass без ручного ввода. Отметьте модели, которые должны отображаться в выпадающем списке выбора моделей в чате DeepSeek Harness.'
576
627
  ),
577
628
  React.createElement(
578
629
  'table',
@@ -612,9 +663,6 @@ window.__ModuleLoader__.load({
612
663
  React.createElement('strong', null, m.name),
613
664
  m.recommended
614
665
  ? React.createElement('span', { className: 'cb-badge cb-badge-ok', style: { marginLeft: '6px' } }, 'Star')
615
- : null,
616
- m.isCustom
617
- ? React.createElement('span', { className: 'cb-badge cb-badge-warn', style: { marginLeft: '6px' } }, 'Custom')
618
666
  : null
619
667
  ),
620
668
  React.createElement('td', null, React.createElement('code', null, m.id)),
@@ -623,7 +671,7 @@ window.__ModuleLoader__.load({
623
671
  'td',
624
672
  null,
625
673
  React.createElement('span', { className: 'cb-badge' }, m.category || 'general'),
626
- m.input?.includes('vision')
674
+ m.input?.includes('image') || m.input?.includes('vision')
627
675
  ? React.createElement('span', { className: 'cb-badge', style: { marginLeft: '4px' } }, 'Vision')
628
676
  : null
629
677
  )
@@ -633,134 +681,48 @@ window.__ModuleLoader__.load({
633
681
  )
634
682
  ),
635
683
 
636
- // Card 4: Add New Custom Model Form
684
+ // Card 4: Session Metrics & Usage Tracking
637
685
  React.createElement(
638
686
  'div',
639
687
  { className: 'cb-section-card' },
640
688
  React.createElement(
641
689
  'div',
642
690
  { className: 'cb-section-title' },
643
- '➕ Добавить новую модель в подписку'
691
+ '📈 Статистика текущей сессии DSH'
644
692
  ),
645
693
  React.createElement(
646
694
  'div',
647
695
  { className: 'cb-section-desc' },
648
- 'Если в сервисе ClinePass появилась новая модель, добавьте её идентификатор сюда — она мгновенно станет доступна в DSH без обновления плагина.'
696
+ 'Локальный мониторинг запросов, задержек сети и ориентировочного расхода токенов через ClinePass.'
649
697
  ),
650
698
  React.createElement(
651
699
  'div',
652
700
  { className: 'cb-grid-2' },
653
701
  React.createElement(
654
702
  'div',
655
- null,
656
- React.createElement('label', { className: 'cb-section-desc' }, 'Model ID (напр. cline-pass/deepseek-v4-moe)'),
657
- React.createElement('input', {
658
- className: 'cb-input',
659
- placeholder: 'cline-pass/...',
660
- value: customId,
661
- onChange: (e) => setCustomId(e.target.value),
662
- })
703
+ { className: 'cb-stat-box' },
704
+ React.createElement('div', { className: 'cb-stat-val' }, `${status.sessionStats?.successfulRequests || 0} / ${status.sessionStats?.totalRequests || 0}`),
705
+ React.createElement('div', { className: 'cb-stat-lbl' }, 'Успешных запросов / Всего')
663
706
  ),
664
707
  React.createElement(
665
708
  'div',
666
- null,
667
- React.createElement('label', { className: 'cb-section-desc' }, 'Отображаемое имя (Display Name)'),
668
- React.createElement('input', {
669
- className: 'cb-input',
670
- placeholder: 'Например: DeepSeek V4 MoE',
671
- value: customName,
672
- onChange: (e) => setCustomName(e.target.value),
673
- })
709
+ { className: 'cb-stat-box' },
710
+ React.createElement('div', { className: 'cb-stat-val' }, `~${status.sessionStats?.totalTokensEst || 0}`),
711
+ React.createElement('div', { className: 'cb-stat-lbl' }, 'Оценка токенов сессии')
674
712
  ),
675
713
  React.createElement(
676
714
  'div',
677
- null,
678
- React.createElement('label', { className: 'cb-section-desc' }, 'Контекст (токенов)'),
679
- React.createElement('input', {
680
- className: 'cb-input',
681
- type: 'number',
682
- value: customContext,
683
- onChange: (e) => setCustomContext(e.target.value),
684
- })
715
+ { className: 'cb-stat-box' },
716
+ React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastLatencyMs ? `${status.sessionStats.lastLatencyMs} мс` : '—'),
717
+ React.createElement('div', { className: 'cb-stat-lbl' }, 'Последняя задержка (Latency)')
685
718
  ),
686
719
  React.createElement(
687
720
  'div',
688
- null,
689
- React.createElement('label', { className: 'cb-section-desc' }, 'Категория'),
690
- React.createElement(
691
- 'select',
692
- {
693
- className: 'cb-input',
694
- value: customCategory,
695
- onChange: (e) => setCustomCategory(e.target.value),
696
- },
697
- React.createElement('option', { value: 'coding' }, 'Кодинг (Coding)'),
698
- React.createElement('option', { value: 'reasoning' }, 'Рассуждения (Reasoning)'),
699
- React.createElement('option', { value: 'multimodal' }, 'Мультимодальная'),
700
- React.createElement('option', { value: 'general' }, 'Общая (General)')
701
- )
721
+ { className: 'cb-stat-box' },
722
+ React.createElement('div', { className: 'cb-stat-val' }, status.sessionStats?.lastRequestAt ? new Date(status.sessionStats.lastRequestAt).toLocaleTimeString() : '—'),
723
+ React.createElement('div', { className: 'cb-stat-lbl' }, 'Время последнего запроса')
702
724
  )
703
- ),
704
- React.createElement(
705
- 'div',
706
- { className: 'cb-row', style: { marginTop: '4px' } },
707
- React.createElement(
708
- 'label',
709
- { className: 'cb-row', style: { cursor: 'pointer', fontSize: '13px' } },
710
- React.createElement('input', {
711
- type: 'checkbox',
712
- checked: customVision,
713
- onChange: (e) => setCustomVision(e.target.checked),
714
- }),
715
- ' Модель поддерживает изображения (Vision)'
716
- ),
717
- React.createElement(
718
- 'button',
719
- {
720
- type: 'button',
721
- className: 'cb-btn cb-btn-primary',
722
- style: { marginLeft: 'auto' },
723
- disabled: !!busy || !customId.trim(),
724
- onClick: handleAddCustomModel,
725
- },
726
- busy === 'add-model' ? 'Добавление…' : 'Добавить модель'
727
- )
728
- ),
729
- // Custom models list with remove button
730
- draft.customModels?.length > 0
731
- ? React.createElement(
732
- 'div',
733
- { style: { marginTop: '10px' } },
734
- React.createElement('div', { className: 'cb-section-desc' }, 'Добавленные вами модели:'),
735
- React.createElement(
736
- 'div',
737
- { className: 'cb-row', style: { marginTop: '6px' } },
738
- draft.customModels.map((cm) =>
739
- React.createElement(
740
- 'span',
741
- { key: cm.id, className: 'cb-badge' },
742
- `${cm.name} (${cm.id})`,
743
- React.createElement(
744
- 'button',
745
- {
746
- type: 'button',
747
- style: {
748
- background: 'none',
749
- border: 'none',
750
- cursor: 'pointer',
751
- color: 'var(--dsw-alias-state-error-primary)',
752
- padding: 0,
753
- marginLeft: '4px',
754
- },
755
- onClick: () => handleRemoveCustomModel(cm.id),
756
- },
757
- '×'
758
- )
759
- )
760
- )
761
- )
762
- )
763
- : null
725
+ )
764
726
  ),
765
727
 
766
728
  // Card 5: Diagnostics & Sync with DSH
@@ -826,8 +788,9 @@ window.__ModuleLoader__.load({
826
788
  }
827
789
 
828
790
  // Accordion item for Plugins section
829
- function PluginCard() {
791
+ function PluginCard(props) {
830
792
  const [open, setOpen] = React.useState(false)
793
+ const t = props?.t || makeT(ru, en)
831
794
  React.useEffect(() => {
832
795
  ensureCss()
833
796
  }, [])
@@ -855,41 +818,61 @@ window.__ModuleLoader__.load({
855
818
  React.createElement(
856
819
  'div',
857
820
  { style: { flex: 1 } },
858
- React.createElement('div', { style: { fontWeight: 600, fontSize: '15px' } }, TITLE),
859
- React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, SUBTITLE)
821
+ React.createElement('div', { style: { fontWeight: 600, fontSize: '15px' } }, t('title')),
822
+ React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
860
823
  ),
861
824
  React.createElement('span', { style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
862
825
  React.createElement(Chevron)
863
826
  )
864
827
  ),
865
- open ? React.createElement('div', { style: { marginTop: '16px' } }, React.createElement(SettingsPage)) : null
828
+ open ? React.createElement('div', { style: { marginTop: '16px' } }, React.createElement(SettingsPage, { ...props, ctx: props?.ctx })) : null
866
829
  )
867
830
  }
868
831
 
869
832
  function apply(ctx) {
870
- // 1. Register as dedicated Settings Section (Settings -> ClineBot)
871
- ctx.slots.inject('settings.section', () => {
872
- return ctx.slots.register(
873
- {
874
- name: 'settings.section',
875
- id: '@goodandready/dsh-clinebot',
876
- order: 28,
877
- label: () => TITLE,
878
- },
879
- SettingsPage
880
- )
881
- })
833
+ if (ctx.locale && ctx.locale.register) {
834
+ try { ctx.locale.register(NS, { en, ru }) } catch (_) {}
835
+ }
836
+ const t = (ctx.locale && ctx.locale.bind) ? ctx.locale.bind(NS) : makeT(ru, en)
837
+
838
+ let placed = false
839
+ try {
840
+ placed = !!ctx.slots.inject('settings.plugin.item', () => {
841
+ return ctx.slots.register(
842
+ {
843
+ name: 'settings.plugin.item',
844
+ key: NS,
845
+ locale: NS,
846
+ inject: () => ({ ctx }),
847
+ },
848
+ PluginCard
849
+ )
850
+ })
851
+ } catch (_) {
852
+ placed = false
853
+ }
882
854
 
883
- // 2. Also register in settings.plugin.item (Settings -> Plugins)
884
- ctx.slots.inject('settings.plugin.item', () => {
885
- return ctx.slots.register(
886
- { name: 'settings.plugin.item', key: NS },
887
- PluginCard
888
- )
889
- })
855
+ // Fallback to settings.section if settings.plugin.item is not declared
856
+ if (!placed) {
857
+ try {
858
+ ctx.slots.inject('settings.section', () => {
859
+ return ctx.slots.register(
860
+ {
861
+ name: 'settings.section',
862
+ id: '@goodandready/dsh-clinebot',
863
+ order: 28,
864
+ locale: NS,
865
+ label: () => t('title'),
866
+ inject: () => ({ ctx }),
867
+ },
868
+ SettingsPage
869
+ )
870
+ })
871
+ } catch (_) {}
872
+ }
890
873
  }
891
874
 
892
- module.exports = { apply, inject: ['slots'] }
875
+ module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
893
876
  return module.exports
894
877
  },
895
878
  })
@@ -11,6 +11,7 @@ import {
11
11
  PROVIDER_ID,
12
12
  PROVIDER_DISPLAY_NAME,
13
13
  getAllModels,
14
+ parsePlanIncludedModels,
14
15
  } from './models.js'
15
16
 
16
17
  export const DEFAULT_BASE_URL = 'https://api.cline.bot/api/v1'
@@ -48,6 +49,47 @@ export function normalizeBaseUrl(raw) {
48
49
  return s
49
50
  }
50
51
 
52
+ // In-memory runtime session metrics for ClinePass requests
53
+ export const sessionStats = {
54
+ totalRequests: 0,
55
+ successfulRequests: 0,
56
+ failedRequests: 0,
57
+ promptTokensEst: 0,
58
+ completionTokensEst: 0,
59
+ totalTokensEst: 0,
60
+ lastLatencyMs: null,
61
+ lastRequestAt: null,
62
+ lastError: null,
63
+ }
64
+
65
+ export function recordSessionRequest({ latencyMs, ok, error, promptTokens = 0, completionTokens = 0 }) {
66
+ sessionStats.totalRequests += 1
67
+ if (ok) {
68
+ sessionStats.successfulRequests += 1
69
+ sessionStats.lastLatencyMs = typeof latencyMs === 'number' ? latencyMs : null
70
+ sessionStats.lastError = null
71
+ } else {
72
+ sessionStats.failedRequests += 1
73
+ sessionStats.lastError = error || 'Request failed'
74
+ }
75
+ sessionStats.lastRequestAt = Date.now()
76
+ sessionStats.promptTokensEst += Number(promptTokens) || 0
77
+ sessionStats.completionTokensEst += Number(completionTokens) || 0
78
+ sessionStats.totalTokensEst += (Number(promptTokens) || 0) + (Number(completionTokens) || 0)
79
+ }
80
+
81
+ export function resetSessionStats() {
82
+ sessionStats.totalRequests = 0
83
+ sessionStats.successfulRequests = 0
84
+ sessionStats.failedRequests = 0
85
+ sessionStats.promptTokensEst = 0
86
+ sessionStats.completionTokensEst = 0
87
+ sessionStats.totalTokensEst = 0
88
+ sessionStats.lastLatencyMs = null
89
+ sessionStats.lastRequestAt = null
90
+ sessionStats.lastError = null
91
+ }
92
+
51
93
  /**
52
94
  * Resolve API key from environment variables.
53
95
  */
@@ -164,24 +206,46 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
164
206
  const weekly = parseWindow('weekly')
165
207
  const monthly = parseWindow('monthly')
166
208
 
167
- // 2. Fetch user metadata (optional, best-effort)
209
+ // 2. Fetch user metadata and plan details (optional, best-effort)
168
210
  let userEmail = null
169
211
  let createdAt = null
212
+ let planDisplayName = 'ClinePass ($9.99/mo)'
213
+ let dynamicModels = []
214
+
170
215
  try {
171
- const meRes = await fetchImpl(`${base}/users/me`, { method: 'GET', headers, signal })
172
- if (meRes.ok) {
216
+ const [meRes, planRes] = await Promise.all([
217
+ fetchImpl(`${base}/users/me`, { method: 'GET', headers, signal }).catch(() => null),
218
+ fetchImpl(`${base}/users/me/plan`, { method: 'GET', headers, signal }).catch(() => null),
219
+ ])
220
+
221
+ if (meRes && meRes.ok) {
173
222
  const meData = await meRes.json().catch(() => ({}))
174
223
  const me = meData?.data || meData?.user || meData
175
224
  userEmail = me?.email || null
176
225
  createdAt = me?.createdAt || null
177
226
  }
227
+
228
+ if (planRes && planRes.ok) {
229
+ const planData = await planRes.json().catch(() => ({}))
230
+ const planObj = planData?.data?.plan || planData?.plan
231
+ if (planObj?.displayName) {
232
+ planDisplayName = planObj.displayName
233
+ }
234
+ const includedArr = planObj?.features?.included
235
+ if (Array.isArray(includedArr)) {
236
+ const incStr = includedArr.find((s) => typeof s === 'string' && s.toLowerCase().includes('includes'))
237
+ if (incStr) {
238
+ dynamicModels = parsePlanIncludedModels(incStr)
239
+ }
240
+ }
241
+ }
178
242
  } catch {
179
- /* ignore user metadata failure */
243
+ /* ignore user metadata / plan failure */
180
244
  }
181
245
 
182
246
  const result = {
183
247
  ok: true,
184
- plan: 'ClinePass ($9.99/mo)',
248
+ plan: planDisplayName,
185
249
  user: {
186
250
  email: userEmail,
187
251
  createdAt,
@@ -191,6 +255,7 @@ export async function fetchUsageLimits(baseUrl, apiKey, {
191
255
  weekly: weekly || { type: 'weekly', percentUsed: 0, remainingPercent: 100, resetsAt: null },
192
256
  monthly: monthly || { type: 'monthly', percentUsed: 0, remainingPercent: 100, resetsAt: null },
193
257
  },
258
+ dynamicModels,
194
259
  checkedAt: now,
195
260
  }
196
261