@goodandready/dsh-cost-meter 0.7.8 → 0.7.9

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/README.md CHANGED
@@ -108,14 +108,20 @@ dsh-cost-meter:
108
108
 
109
109
  ### Configuration Parameters
110
110
 
111
- | Parameter | Type | Default | Description |
112
- |:---|:---|:---|:---|
113
- | `currency` | `string` | `"$"` | Display currency symbol or code |
114
- | `usdRate` | `number` | `1.0` | Exchange rate multiplier from USD |
115
- | `displayTimeZone` | `string` | `"UTC"` | Timezone for window schedule formatting (e.g. `Europe/London`, `Asia/Shanghai`) |
116
- | `useOpenRouter` | `boolean` | `true` | Fetch OpenRouter public catalog for fallback models |
117
- | `prices` | `object` | `{}` | Custom override rates per 1M tokens `{ prompt, completion, cacheRead?, cacheWrite? }` |
118
- | `modelMap` | `object` | `{}` | Model alias mappings `{ "alias": "target-model" }` |
111
+ | Parameter | Scope / Location | Type | Default | Description |
112
+ |:---|:---|:---|:---|:---|
113
+ | `currency` | GUI / `settings.yaml` | `string` | `"$"` | Display currency symbol (e.g. `$`, `₽`, `€`) |
114
+ | `usdRate` | GUI / `settings.yaml` | `number` | `1.0` | Exchange rate multiplier: units of currency per 1 USD |
115
+ | `displayTimeZone` | GUI / `settings.yaml` | `string` | `"Europe/Moscow"` | IANA timezone for peak/off-peak windows formatting |
116
+ | `useOpenRouter` | GUI / `settings.yaml` | `boolean` | `true` | Automatically fetch rates & discount windows from OpenRouter |
117
+ | `refreshHours` | GUI / `settings.yaml` | `number` | `24` | Hours between periodic OpenRouter catalog updates |
118
+ | `deepseekPeakPrices` | `settings.yaml` only | `object` | `{}` | Override built-in DeepSeek peak rates by model id |
119
+ | `prices` | `settings.yaml` only | `object` | `{}` | Manual rates per 1M tokens `{ input, output, cacheHit?, cacheWrite? }` |
120
+ | `modelMap` | `settings.yaml` only | `object` | `{}` | Route override: `"provider/model"` -> OpenRouter model id |
121
+ | `manualPeakWindowsUtc` | `settings.yaml` only | `array` | `[]` | Manual peak windows HH:MM-HH:MM UTC used with manual prices |
122
+ | `manualOffPeakMultiplier` | `settings.yaml` only | `number` | `1.0` | Multiplier applied outside manual peak windows |
123
+
124
+ > **Note on GUI vs YAML**: Primary scalar parameters (`currency`, `usdRate`, `displayTimeZone`, `useOpenRouter`, `refreshHours`) are directly editable in the GUI Settings Card under **Settings → Plugins → Plugin Settings → Cost Meter**. Advanced structured rules (`prices`, `modelMap`, `deepseekPeakPrices`, `manualPeakWindowsUtc`, `manualOffPeakMultiplier`) are configured in `settings.yaml` due to their complex dictionary/array schema.
119
125
 
120
126
  ---
121
127
 
package/docs/README.ru.md CHANGED
@@ -108,14 +108,20 @@ dsh-cost-meter:
108
108
 
109
109
  ### Параметры конфигурации
110
110
 
111
- | Параметр | Тип | По умолчанию | Описание |
112
- |:---|:---|:---|:---|
113
- | `currency` | `string` | `"₽"` | Символ или код валюты для отображения |
114
- | `usdRate` | `number` | `90.0` | Множитель пересчёта долларовых цен в валюту отображения |
115
- | `displayTimeZone` | `string` | `"UTC"` | Таймзона для отображения окон в панели |
116
- | `useOpenRouter` | `boolean` | `true` | Загружать ли каталог цен OpenRouter для сторонних моделей |
117
- | `prices` | `object` | `{}` | Ручные ставки за 1M токенов `{ prompt, completion, cacheRead?, cacheWrite? }` |
118
- | `modelMap` | `object` | `{}` | Алиасы моделей `{ "alias": "target-model" }` |
111
+ | Параметр | Доступность | Тип | По умолчанию | Описание |
112
+ |:---|:---|:---|:---|:---|
113
+ | `currency` | GUI / `settings.yaml` | `string` | `"₽"` | Символ валюты в интерфейсе (например, `₽`, `$`, `€`) |
114
+ | `usdRate` | GUI / `settings.yaml` | `number` | `80.0` | Курс обмена: единиц валюты за 1 USD |
115
+ | `displayTimeZone` | GUI / `settings.yaml` | `string` | `"Europe/Moscow"` | IANA часовой пояс для отображения окон пика и не-пика |
116
+ | `useOpenRouter` | GUI / `settings.yaml` | `boolean` | `true` | Загружать ли каталог тарифов и скидочных окон OpenRouter |
117
+ | `refreshHours` | GUI / `settings.yaml` | `number` | `24` | Частота обновления каталога OpenRouter (в часах) |
118
+ | `deepseekPeakPrices` | Только `settings.yaml` | `object` | `{}` | Переопределение встроенных пиковых ставок DeepSeek по id модели |
119
+ | `prices` | Только `settings.yaml` | `object` | `{}` | Ручные ставки за 1M токенов `{ input, output, cacheHit?, cacheWrite? }` |
120
+ | `modelMap` | Только `settings.yaml` | `object` | `{}` | Сопоставление маршрута `"provider/model"` на id в OpenRouter |
121
+ | `manualPeakWindowsUtc` | Только `settings.yaml` | `array` | `[]` | Ручные окна пика HH:MM-HH:MM UTC для ручных ставок |
122
+ | `manualOffPeakMultiplier` | Только `settings.yaml` | `number` | `1.0` | Коэффициент тарифа вне ручных окон пика |
123
+
124
+ > **Интерфейс настроек (GUI) vs settings.yaml**: Основные параметры (`currency`, `usdRate`, `displayTimeZone`, `useOpenRouter`, `refreshHours`) настраиваются прямо в карточке интерфейса DSH (**Настройки → Плагины → Настройки плагинов → Cost Meter**). Расширенные структурированные правила сопоставления и ручных тарифов задаются через `settings.yaml` из-за сложной древовидной структуры.
119
125
 
120
126
  ---
121
127
 
@@ -82,7 +82,7 @@
82
82
  - Не допускать утечек локальных путей и инфраструктурных данных в коде или документации.
83
83
 
84
84
  ## History & Decisions
85
- - 2026-09-05 (v0.7.8):
85
+ - 2026-09-05 (v0.7.9):
86
86
  - Hotfix: исправлен ReferenceError (t is not defined) в объекте словаря ru (lib/client.js) (#15).
87
87
  - Добавлен автоматический тест на чистое исполнение фабрики client loader (`test/public-route.test.mjs`).
88
88
  - 2026-09-05 (v0.7.4):
@@ -1,10 +1,22 @@
1
- # Findings & Architecture Decisions (v0.7.8)
1
+ # Technical Findings (Issue #21)
2
2
 
3
- ## 1. Browser Request Race Conditions
4
- In `CostMeter`, rapid model switches or toggling the popover calls `load(usedKeys)`. Since network latency varies, an older request might resolve after a newer one. By tracking an active request counter or cancellation token per effect invocation, we ensure only the most up-to-date state response mutates the component state.
3
+ ## 1. Config Fields Breakdown
4
+ The plugin `Config` defines 10 fields:
5
+ 1. `currency` (string, default '$') -> Present in UI
6
+ 2. `usdRate` (number, default 1) -> Present in UI
7
+ 3. `displayTimeZone` (string, default 'Europe/Moscow') -> Present in UI
8
+ 4. `useOpenRouter` (boolean, default true) -> Missing in UI -> To be added as a checkbox in `CostMeterCard`
9
+ 5. `refreshHours` (number, default 24) -> Missing in UI -> To be added as a numeric input in `CostMeterCard`
10
+ 6. `modelMap` (dict of string -> string) -> Advanced JSON/YAML route mapping
11
+ 7. `prices` (dict of PriceRow) -> Advanced manual pricing rules
12
+ 8. `deepseekPeakPrices` (dict of PriceRow) -> Advanced DeepSeek peak overrides
13
+ 9. `manualPeakWindowsUtc` (array of string) -> Advanced manual time windows
14
+ 10. `manualOffPeakMultiplier` (number) -> Advanced discount multiplier for manual rates
5
15
 
6
- ## 2. IANA TimeZone Validation
7
- `Intl.DateTimeFormat(undefined, { timeZone })` throws a `RangeError` if the timezone string is invalid. We can safely leverage this in `CostMeterCard` to reject typos (e.g. `Europe/Moskow` instead of `Europe/Moscow`) on the client before writing to `settingsScope`.
16
+ By adding `useOpenRouter` and `refreshHours` to `CostMeterCard`, all 5 primary scalar/control settings are directly configurable in the DSH Settings GUI. The 5 advanced structured override rules (`prices`, `modelMap`, `deepseekPeakPrices`, `manualPeakWindowsUtc`, `manualOffPeakMultiplier`) are explicitly documented in README.md and DESIGN.md as YAML configuration rows.
8
17
 
9
- ## 3. Query Parameter Sanitization
10
- The endpoint `GET /dsh-cost-meter/state?models=...` accepts comma-separated model keys. Adding a length bound (e.g. <= 128 chars) and trimming ensures no malformed payloads cause unexpected overhead.
18
+ ## 2. Service Access Pattern
19
+ In `lib/index.js`, we have:
20
+ `const settings = ctx.get('settings')` (canonical).
21
+ In `lib/client.js`, `ctx.settingsScope` is accessed directly. We should safely check `(ctx.get && ctx.get('settingsScope')) || ctx.settingsScope || (ctx.services && ctx.services.settingsScope)`.
22
+ Same for `ctx.locale` and `ctx.slots`.
@@ -1,17 +1,17 @@
1
- # Implementation Progress (v0.7.8)
1
+ # Progress Log (Issue #21 / v0.7.9)
2
2
 
3
- - [x] Create Gitea Issue #19
4
- - [x] Create isolated worktree `stability-v078`
5
- - [x] Bump version to 0.7.8 in `package.json`
6
- - [x] Add monotonic request sequence guard in `lib/client.js` `CostMeter`
7
- - [x] Add IANA time zone validation in `lib/client.js` `CostMeterCard`
8
- - [x] Sanitize query parameters in `lib/index.js`
9
- - [x] Add automated unit tests (`test/timezone-sanitization.test.mjs`, 11/11 passing)
10
- - [x] Update design documentation in `docs/design/DESIGN.md`
3
+ - [x] Analyze Issue #21
4
+ - [x] Create worktree `.worktrees/issue-21`
5
+ - [x] Initialize plans (`task_plan.md`, `findings.md`, `progress.md`)
6
+ - [x] Bump version to 0.7.9 in `package.json`
7
+ - [x] Implement settings card additions (`useOpenRouter`, `refreshHours`) in `lib/client.js`
8
+ - [x] Safe service resolution with `ctx.get('...')` in `lib/client.js`
9
+ - [x] Update documentation with full 10-field matrix (`README.md`, `docs/README.ru.md`, `docs/design/DESIGN.md`)
10
+ - [x] Add regression test `test/settings-card-fields.test.mjs` (12/12 passing)
11
11
  - [ ] Commit changes with conventional commits (`git-antigravity`)
12
12
  - [ ] Create Pull Request and merge into `main`
13
13
  - [ ] Test package on MiniPC isolated test server (`192.168.1.123:3082`)
14
14
  - [ ] Smoke-test package on production candidate (`192.168.1.111:3080`)
15
- - [ ] Publish v0.7.8 to npm and GitHub Release
16
- - [ ] Install v0.7.8 from npm on MiniAI and verify live health
17
- - [ ] Close Gitea Issue #19 and clean up worktree
15
+ - [ ] Publish v0.7.9 to npm and GitHub Release
16
+ - [ ] Install v0.7.9 from npm on MiniAI and verify live health
17
+ - [ ] Close Gitea Issue #21 and clean up worktree
@@ -1,24 +1,32 @@
1
- # Task Plan: Stability, Race Condition Prevention & TimeZone Validation (v0.7.8)
1
+ # Task Plan: Address Issue #21 (Settings Card Schema Fields & Service Access)
2
2
 
3
3
  ## 1. Goal
4
- Strengthen `@goodandready/dsh-cost-meter` runtime resilience:
5
- - Prevent race condition response overwrites in browser `CostMeter` component.
6
- - Validate IANA TimeZone inputs in `CostMeterCard` settings before saving.
7
- - Sanitize `models` query parameter keys in `lib/index.js` route handler.
8
- - Maintain strict backward compatibility, zero regression in automated tests.
4
+ Fulfill Gitea Issue #21 requirements:
5
+ 1. **Schema Fields Availability in Settings Card**:
6
+ - Audit all 10 fields of `Config`:
7
+ - UI fields: `currency`, `usdRate`, `displayTimeZone`.
8
+ - Boolean toggle: `useOpenRouter` (checkbox / toggle in settings card).
9
+ - Numeric: `refreshHours` (catalog refresh frequency, e.g. 24 hours).
10
+ - Complex structured overrides: `prices` (dict), `modelMap` (dict), `deepseekPeakPrices` (dict), `manualPeakWindowsUtc` (array), `manualOffPeakMultiplier` (number).
11
+ - Add `useOpenRouter` and `refreshHours` directly to the `CostMeterCard` form so all scalar/toggle configuration is editable in GUI.
12
+ - Document structured dict/array fields (`prices`, `modelMap`, `deepseekPeakPrices`, `manualPeakWindowsUtc`, `manualOffPeakMultiplier`) in documentation, READMEs and DESIGN.md as YAML-only/advanced configuration rows.
13
+ 2. **Context Service Access Hygiene**:
14
+ - Inspect all `ctx.settings` / `ctx.settingsScope` / `ctx.locale` / `ctx.webServer` calls.
15
+ - Use `ctx.get('...')` or inject where appropriate, or guard against undefined proxiable properties.
16
+ 3. **Locale & Translations**:
17
+ - Provide clean English locale strings as canonical with ru dictionaries, and hint descriptions for all new fields.
9
18
 
10
19
  ## 2. Checklist
11
- - [ ] Bump version to `0.7.8` in `package.json`
12
- - [ ] Implement cancellation flag / abort protection in `lib/client.js` `CostMeter`
13
- - [ ] Implement `isValidTimeZone` helper and validation check in `CostMeterCard`
14
- - [ ] Implement key sanitization (length <= 128 chars, alphanumeric/dash/slash) in `lib/index.js`
15
- - [ ] Add unit tests for timezone check and query sanitization
16
- - [ ] Run test suite (`npm test`)
17
- - [ ] Update `docs/design/DESIGN.md`
18
- - [ ] Conventional Commit & Push
19
- - [ ] Gitea PR & Merge to `main`
20
+ - [ ] Bump version to 0.7.9 in `package.json`
21
+ - [ ] Add `useOpenRouter` and `refreshHours` fields to `CostMeterCard` in `lib/client.js`
22
+ - [ ] Update `en` and `ru` dictionaries for new fields
23
+ - [ ] Audit and refine service accesses in `lib/index.js` and `lib/client.js`
24
+ - [ ] Update `README.md`, `docs/README.ru.md`, `docs/README.zh.md`, and `docs/design/DESIGN.md`
25
+ - [ ] Add unit test verifying settings card field contract
26
+ - [ ] Run `npm test` (all tests passing)
27
+ - [ ] Conventional commit & push
28
+ - [ ] Gitea PR & merge
20
29
  - [ ] Test on MiniPC test server (`192.168.1.123`)
21
- - [ ] Production Smoke Verification on MiniAI (`192.168.1.111`)
22
- - [ ] Publish Tag `v0.7.8`, GitHub Release & npm
23
- - [ ] Update production profile from npm
24
- - [ ] Close Gitea Issue #19 and clean up worktree
30
+ - [ ] Install on production MiniAI (`192.168.1.111`) & verify
31
+ - [ ] Release v0.7.9 on GitHub & npm
32
+ - [ ] Close Issue #21 & cleanup worktree
package/lib/client.js CHANGED
@@ -49,6 +49,10 @@ window.__ModuleLoader__.load({
49
49
  'hint.currency': 'Currency symbol shown in the widget (e.g. $, ₽, €).',
50
50
  'hint.usdRate': 'Exchange rate: units of currency per 1 USD (e.g. 80 for ₽).',
51
51
  'hint.displayTimeZone': 'IANA timezone used for tariff peak/off-peak schedule.',
52
+ 'field.useOpenRouter': 'Use OpenRouter Catalog',
53
+ 'hint.useOpenRouter': 'Pull rates and discount windows automatically from the OpenRouter catalog.',
54
+ 'field.refreshHours': 'Catalog Refresh Interval (hours)',
55
+ 'hint.refreshHours': 'How often to fetch fresh model rates from OpenRouter (default: 24).',
52
56
  }
53
57
 
54
58
  const ru = {
@@ -79,6 +83,10 @@ window.__ModuleLoader__.load({
79
83
  'hint.currency': 'Символ валюты, отображаемый в виджете (например, ₽, $, €).',
80
84
  'hint.usdRate': 'Сколько единиц валюты в 1 долларе США (например, 80 для ₽).',
81
85
  'hint.displayTimeZone': 'IANA таймзона для расчёта часов пика и не-пика (например, Europe/Moscow).',
86
+ 'field.useOpenRouter': 'Каталог OpenRouter',
87
+ 'hint.useOpenRouter': 'Автоматически загружать тарифы и скидочные окна из каталога OpenRouter.',
88
+ 'field.refreshHours': 'Интервал обновления каталога (часы)',
89
+ 'hint.refreshHours': 'Как часто обновлять ставки из OpenRouter (по умолчанию: 24).',
82
90
  }
83
91
 
84
92
  let ChevronIcon = null
@@ -118,6 +126,8 @@ window.__ModuleLoader__.load({
118
126
  '.dcm-sub { color:var(--dsw-alias-label-secondary); font-size:13px } ' +
119
127
  '.dcm-body { border-top:1px solid var(--dsw-alias-border-l2); margin:0 16px; padding-bottom:8px } ' +
120
128
  '.dcm-field { display:flex; flex-direction:column; gap:6px; padding:12px 0 } ' +
129
+ '.dcm-row-check { display:flex; align-items:center; gap:8px; padding:12px 0 } ' +
130
+ '.dcm-check { width:16px; height:16px; accent-color:var(--dsw-alias-brand-primary, #6366f1); cursor:pointer } ' +
121
131
  '.dcm-label { color:var(--dsw-alias-label-primary); font-size:13px; font-weight:500 } ' +
122
132
  '.dcm-hint { color:var(--dsw-alias-label-secondary); font-size:12px } ' +
123
133
  '.dcm-input { height:34px; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-3); color:var(--dsw-alias-label-primary); border-radius:8px; padding:0 12px; font-size:13px } ' +
@@ -149,9 +159,10 @@ window.__ModuleLoader__.load({
149
159
  }
150
160
 
151
161
  function useActiveLocale(ctx) {
162
+ const localeService = (ctx && typeof ctx.get === 'function' ? ctx.get('locale') : null) || (ctx && ctx.locale) || (ctx && ctx.services && ctx.services.locale)
152
163
  return React.useSyncExternalStore(
153
- React.useMemo(() => (cb) => (ctx && ctx.locale ? ctx.locale.subscribe(cb) : () => {}), [ctx]),
154
- React.useCallback(() => (ctx && ctx.locale && ctx.locale.getSnapshot ? ctx.locale.getSnapshot().active : 'ru'), [ctx]),
164
+ React.useMemo(() => (cb) => (localeService && typeof localeService.subscribe === 'function' ? localeService.subscribe(cb) : () => {}), [localeService]),
165
+ React.useCallback(() => (localeService && typeof localeService.getSnapshot === 'function' ? localeService.getSnapshot().active : 'ru'), [localeService]),
155
166
  React.useCallback(() => 'ru', []),
156
167
  )
157
168
  }
@@ -166,9 +177,10 @@ window.__ModuleLoader__.load({
166
177
  const [err, setErr] = React.useState('')
167
178
  const [saved, setSaved] = React.useState(false)
168
179
 
180
+ const settingsScope = (ctx && typeof ctx.get === 'function' ? ctx.get('settingsScope') : null) || (ctx && ctx.settingsScope) || (ctx && ctx.services && ctx.services.settingsScope)
169
181
  const scope = React.useMemo(
170
- () => (ctx && ctx.settingsScope ? ctx.settingsScope.bind({ namespace: NS }) : undefined),
171
- [ctx],
182
+ () => (settingsScope ? settingsScope.bind({ namespace: NS }) : undefined),
183
+ [settingsScope],
172
184
  )
173
185
 
174
186
  const snapshot = React.useSyncExternalStore(
@@ -188,6 +200,8 @@ window.__ModuleLoader__.load({
188
200
  currency: stored.currency !== undefined ? String(stored.currency) : '$',
189
201
  usdRate: stored.usdRate !== undefined ? String(stored.usdRate) : '1',
190
202
  displayTimeZone: stored.displayTimeZone !== undefined ? String(stored.displayTimeZone) : 'Europe/Moscow',
203
+ useOpenRouter: stored.useOpenRouter !== undefined ? Boolean(stored.useOpenRouter) : true,
204
+ refreshHours: stored.refreshHours !== undefined ? String(stored.refreshHours) : '24',
191
205
  })
192
206
  }
193
207
  }, [status, stored, draft])
@@ -218,6 +232,19 @@ window.__ModuleLoader__.load({
218
232
  } catch (e) { broken.push('displayTimeZone: ' + (e && e.message || String(e))) }
219
233
  }
220
234
 
235
+ try {
236
+ await scope.set('useOpenRouter', Boolean(draft.useOpenRouter))
237
+ } catch (e) { broken.push('useOpenRouter: ' + (e && e.message || String(e))) }
238
+
239
+ const hoursNum = Number(String(draft.refreshHours).trim())
240
+ if (!Number.isFinite(hoursNum) || hoursNum <= 0) {
241
+ broken.push('refreshHours: must be a positive number')
242
+ } else {
243
+ try {
244
+ await scope.set('refreshHours', hoursNum)
245
+ } catch (e) { broken.push('refreshHours: ' + (e && e.message || String(e))) }
246
+ }
247
+
221
248
  setSaving(false)
222
249
  if (broken.length) {
223
250
  setErr(t('settings.saveFailed') + broken.join('; '))
@@ -273,6 +300,28 @@ window.__ModuleLoader__.load({
273
300
  }),
274
301
  React.createElement('span', { className: 'dcm-hint' }, t('hint.displayTimeZone')),
275
302
  ),
303
+ React.createElement('div', { className: 'dcm-row-check' },
304
+ React.createElement('input', {
305
+ id: 'dcm-use-openrouter',
306
+ type: 'checkbox',
307
+ className: 'dcm-check',
308
+ checked: draft ? Boolean(draft.useOpenRouter) : true,
309
+ onChange: (e) => setDraft({ ...draft, useOpenRouter: e.target.checked }),
310
+ }),
311
+ React.createElement('label', { htmlFor: 'dcm-use-openrouter', className: 'dcm-label', style: { cursor: 'pointer' } }, t('field.useOpenRouter')),
312
+ ),
313
+ React.createElement('span', { className: 'dcm-hint', style: { marginTop: -4, display: 'block', paddingBottom: 10 } }, t('hint.useOpenRouter')),
314
+ React.createElement('div', { className: 'dcm-field' },
315
+ React.createElement('label', { className: 'dcm-label' }, t('field.refreshHours')),
316
+ React.createElement('input', {
317
+ className: 'dcm-input',
318
+ type: 'number',
319
+ step: 'any',
320
+ value: (draft && draft.refreshHours) !== undefined ? draft.refreshHours : '',
321
+ onChange: (e) => setDraft({ ...draft, refreshHours: e.target.value }),
322
+ }),
323
+ React.createElement('span', { className: 'dcm-hint' }, t('hint.refreshHours')),
324
+ ),
276
325
  err ? React.createElement('div', { className: 'dcm-msg-err', style: { padding: '4px 0' } }, err) : null,
277
326
  saved ? React.createElement('div', { className: 'dcm-msg-ok', style: { padding: '4px 0' } }, t('settings.saved')) : null,
278
327
  React.createElement('div', { className: 'dcm-foot' },
@@ -487,7 +536,7 @@ window.__ModuleLoader__.load({
487
536
  const [open, setOpen] = React.useState(false)
488
537
  const [now, setNow] = React.useState(() => new Date())
489
538
  const box = React.useRef(null)
490
- const settingsScope = ctx.settingsScope || (ctx.services && ctx.services.settingsScope)
539
+ const settingsScope = (ctx && typeof ctx.get === 'function' ? ctx.get('settingsScope') : null) || (ctx && ctx.settingsScope) || (ctx && ctx.services && ctx.services.settingsScope)
491
540
  const settingsSnap = React.useSyncExternalStore
492
541
  ? React.useSyncExternalStore(
493
542
  (cb) => (settingsScope && typeof settingsScope.subscribe === 'function' ? settingsScope.subscribe(cb) : () => {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cost-meter",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "description": "DeepSeek Harness plugin: a session cost chip in the conversation header - live token usage by bucket, peak/off-peak tariff, and a countdown to the next tariff switch.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",