@goodandready/dsh-model-sync 0.5.3 → 0.5.5

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.
@@ -12,7 +12,7 @@ const PROVIDER_ADAPTERS = Object.freeze({
12
12
  clinebot: {
13
13
  endpoint: 'https://api.cline.bot/api/v1/users/me/plan',
14
14
  auth: 'bearer',
15
- parser: 'clinebot-plan',
15
+ parse: 'clinebot-plan',
16
16
  defaultModels: [
17
17
  'cline-pass/deepseek-v4-flash',
18
18
  'cline-pass/deepseek-v4-pro',
@@ -99,12 +99,13 @@ function mappedRows(provider, rows, descriptor) {
99
99
  }
100
100
 
101
101
  function parseRows(provider, payload, descriptor) {
102
- if (descriptor.parse === 'clinebot-plan') {
102
+ const parserKind = descriptor.parse || descriptor.parser
103
+ if (parserKind === 'clinebot-plan') {
103
104
  const plan = payload?.data?.plan || payload?.data || payload?.plan || payload
104
105
  const included = plan?.features?.included || plan?.includedModels || []
105
106
  return parseClinePassModels(included)
106
107
  }
107
- if (descriptor.parse === 'google') {
108
+ if (parserKind === 'google') {
108
109
  const rows = Array.isArray(payload?.models) ? payload.models : []
109
110
  return normalizeModels(provider, rows.map((row) => ({
110
111
  ...row,
@@ -224,9 +225,6 @@ async function fetchSpecific(profile, descriptor, options) {
224
225
  return rows
225
226
  }
226
227
  } catch (error) {
227
- if (descriptor.defaultModels && Array.isArray(descriptor.defaultModels) && descriptor.defaultModels.length > 0) {
228
- return normalizeModels(profile.provider, descriptor.defaultModels.map((id) => ({ id })))
229
- }
230
228
  throw error
231
229
  }
232
230
  if (descriptor.defaultModels && Array.isArray(descriptor.defaultModels) && descriptor.defaultModels.length > 0) {
package/lib/client.js CHANGED
@@ -785,6 +785,20 @@ window.__ModuleLoader__.load({
785
785
  .finally(() => setBusy(false))
786
786
  }
787
787
 
788
+ const resolveSmartAlias = (aliasName) => {
789
+ setResolvingKey(aliasName)
790
+ fetch(`${API}/aliases/resolve?alias=${encodeURIComponent(aliasName)}`)
791
+ .then(async (r) => {
792
+ const res = await r.json()
793
+ if (!r.ok) throw new Error(res.error?.message ?? `HTTP ${r.status}`)
794
+ setResolvedAliases((prev) => ({ ...prev, [aliasName]: res }))
795
+ })
796
+ .catch((e) => {
797
+ setResolvedAliases((prev) => ({ ...prev, [aliasName]: { error: String(e?.message ?? e) } }))
798
+ })
799
+ .finally(() => setResolvingKey(''))
800
+ }
801
+
788
802
  const load = () => {
789
803
  if (typeof document !== 'undefined' && document.hidden) return
790
804
  setError('')
package/lib/http.js CHANGED
@@ -240,6 +240,12 @@ export function normalizeSelectionRequest(body) {
240
240
  }
241
241
 
242
242
  export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () => null } = {}) {
243
+ if (typeof ctx?.webServer?.register !== 'function') {
244
+ if (typeof ctx?.logger?.warn === 'function') {
245
+ ctx.logger.warn('[dsh-model-sync] webServer service unavailable, HTTP API skipped')
246
+ }
247
+ return []
248
+ }
243
249
  const makeHandler = (route) => async (req, res) => {
244
250
  if (!trusted(req, { requireLoopback: req.method === 'POST' })) return json(res, 403, { error: { code: 'forbidden', message: 'dsh-model-sync: cross-origin or non-loopback request rejected' } })
245
251
  if (route === 'status') {
package/lib/index.js CHANGED
@@ -122,11 +122,43 @@ export function plainConfig(value) {
122
122
  }
123
123
 
124
124
  export function apply(ctx, config) {
125
- const baseConfig = structuredClone(config || {})
126
- const resolveConfig = (value) => Config(structuredClone(value || {}))
125
+ const baseConfig = structuredClone(plainConfig(config || {}))
126
+ const resolveConfig = (value) => {
127
+ try {
128
+ return plainConfig(Config(plainConfig(value) || {}))
129
+ } catch (err) {
130
+ if (typeof ctx?.logger?.warn === "function") {
131
+ ctx.logger.warn("[dsh-model-sync] invalid config payload, fallback to current config:", err?.message || err)
132
+ }
133
+ return liveConfig ?? baseConfig
134
+ }
135
+ }
127
136
  let liveConfig = resolveConfig(baseConfig)
128
137
  let reconfigure = () => {}
129
- let saveConfigImpl
138
+ let saveConfigImpl = async (patch) => {
139
+ const plainPatch = plainConfig(patch)
140
+ let settings
141
+ try {
142
+ settings = (typeof ctx.get === 'function' ? ctx.get('settings') : null) ?? ctx.settings
143
+ } catch (err) {
144
+ if (typeof ctx.logger?.debug === 'function') {
145
+ ctx.logger.debug('[dsh-model-sync] settings service read failed:', err)
146
+ }
147
+ }
148
+ if (settings && typeof settings.update === 'function') {
149
+ try {
150
+ await settings.update(NS, plainPatch)
151
+ } catch (err) {
152
+ if (typeof ctx.logger?.warn === 'function') {
153
+ ctx.logger.warn('[dsh-model-sync] settings.update failed:', err)
154
+ }
155
+ }
156
+ }
157
+ liveConfig = resolveConfig({ ...liveConfig, ...plainPatch })
158
+ reconfigure()
159
+ return liveConfig
160
+ }
161
+
130
162
  // ctx.inject starts a child fiber; keep a stable function reference for the
131
163
  // synchronizer while that fiber connects to the Settings service.
132
164
  const saveConfig = async (patch) => {
@@ -136,6 +168,23 @@ export function apply(ctx, config) {
136
168
  return saveConfigImpl(patch)
137
169
  }
138
170
 
171
+ const service = createProviderService(ctx)
172
+ const synchronizer = createModelSynchronizer(ctx, { getConfig: () => liveConfig, saveConfig })
173
+ const scheduler = createSyncScheduler(synchronizer, { getConfig: () => liveConfig })
174
+ reconfigure = () => scheduler.reconfigure()
175
+ scheduler.start()
176
+
177
+ ctx.effect(() => {
178
+ if (typeof ctx.on !== 'function') return () => {}
179
+ const dispose = ctx.on('loader/volatile-update', () => {
180
+ liveConfig = resolveConfig(config)
181
+ reconfigure()
182
+ })
183
+ return () => {
184
+ if (typeof dispose === 'function') dispose()
185
+ }
186
+ }, 'dsh-model-sync: volatile-update listener')
187
+
139
188
  // settings.register was removed before 0.1.7-rc.2. The host hands the profile entry
140
189
  // config to apply() directly, and liveConfig above is derived from it; the
141
190
  // Loader mutates the boxes in place and re-announces them with
@@ -153,7 +202,6 @@ export function apply(ctx, config) {
153
202
  manifestUrl: new URL('../package.json', import.meta.url),
154
203
  }), 'dsh-model-sync: plugin updater')
155
204
  ctx.effect(() => {
156
- if (!liveConfig.enabled) return () => {}
157
205
  ctx.provide('modelSync', service)
158
206
  ctx.provide('modelSyncRunner', synchronizer)
159
207
  return () => {}
package/lib/inventory.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { normalizeModels } from './models.js'
2
+ import { sectionOf } from './synchronizer-helpers.js'
2
3
 
3
4
  export const API_KEY_PROVIDER_IDS = Object.freeze([
4
5
  'anthropic',
@@ -42,7 +43,7 @@ const API_KEY_PROVIDER_SET = new Set(API_KEY_PROVIDER_IDS)
42
43
 
43
44
  function settingsProfiles(settings) {
44
45
  try {
45
- return settings?.get('llm-pi-ai')?.providers ?? {}
46
+ return sectionOf(settings, 'llm-pi-ai')?.providers ?? {}
46
47
  } catch {
47
48
  return {}
48
49
  }
@@ -66,7 +67,7 @@ export function listApiKeyProviders(llm, settings) {
66
67
  for (const entry of directory) {
67
68
  let profile = profiles[entry.provider]
68
69
  if (!profile && (entry.provider === 'commandcode' || entry.settingsNs === 'llm-commandcode')) {
69
- const cc = settings?.get?.('llm-commandcode')
70
+ const cc = sectionOf(settings, 'llm-commandcode')
70
71
  profile = {
71
72
  apiKeyEnv: cc?.apiKeyEnv ?? 'COMMANDCODE_API_KEY',
72
73
  credentialRef: cc?.apiKeyEnv ?? 'COMMANDCODE_API_KEY',
@@ -55,12 +55,23 @@ export function descriptorOf(settings, ns) {
55
55
  catch { return undefined }
56
56
  }
57
57
 
58
+ export function sectionOf(settings, ns) {
59
+ try {
60
+ const desc = descriptorOf(settings, ns)
61
+ if (desc && desc.value !== undefined) return desc.value
62
+ if (typeof settings?.get === 'function') return settings.get(ns)
63
+ return undefined
64
+ } catch {
65
+ return undefined
66
+ }
67
+ }
68
+
58
69
  export function profileOf(settings, provider) {
59
70
  try {
60
- const piai = settings?.get(PIAI_NS)?.providers?.[provider]
71
+ const piai = sectionOf(settings, PIAI_NS)?.providers?.[provider]
61
72
  if (piai) return piai
62
73
  if (provider === 'commandcode') {
63
- const cc = settings?.get?.('llm-commandcode')
74
+ const cc = sectionOf(settings, 'llm-commandcode')
64
75
  return {
65
76
  apiKeyEnv: cc?.apiKeyEnv ?? 'COMMANDCODE_API_KEY',
66
77
  credentialRef: cc?.apiKeyEnv ?? 'COMMANDCODE_API_KEY',
@@ -97,7 +108,7 @@ export function adapterErrorType(error) {
97
108
  export async function replaceWithRetry(settings, ns, patchFn, attempts = 3) {
98
109
  let lastError
99
110
  for (let i = 0; i < attempts; i++) {
100
- const section = settings?.get?.(ns)
111
+ const section = sectionOf(settings, ns)
101
112
  const descriptor = descriptorOf(settings, ns)
102
113
  if (!section || !descriptor || typeof settings.replace !== 'function') {
103
114
  throw Object.assign(new Error(`${ns} settings are not writable`), { code: 'SETTINGS_UNAVAILABLE' })
@@ -120,7 +131,7 @@ export async function replaceWithRetry(settings, ns, patchFn, attempts = 3) {
120
131
 
121
132
  export function rotationRefs(settings, provider) {
122
133
  try {
123
- const rows = settings?.get?.('dsh-key-rotation')?.providers
134
+ const rows = sectionOf(settings, 'dsh-key-rotation')?.providers
124
135
  if (!Array.isArray(rows)) return []
125
136
  return rows.filter((row) => row?.provider === provider).flatMap((row) => Array.isArray(row.keys) ? row.keys : [])
126
137
  } catch {
@@ -149,7 +160,7 @@ export async function performHistoryRollback({ ctx, config, providers = [], hist
149
160
  if (!snapshot || !Array.isArray(snapshot.before)) throw Object.assign(new Error(`history snapshot not found: ${provider}`), { code: 'HISTORY_NOT_FOUND' })
150
161
  if (!providers.some((row) => row.provider === provider && row.configured)) throw Object.assign(new Error(`provider is not configured: ${provider}`), { code: 'PROVIDER_NOT_CONFIGURED' })
151
162
  const settings = settingsOf(ctx)
152
- const section = settings?.get?.(PIAI_NS)
163
+ const section = sectionOf(settings, PIAI_NS)
153
164
  const descriptor = descriptorOf(settings, PIAI_NS)
154
165
  if (!section || !descriptor || typeof settings.replace !== 'function') {
155
166
  throw Object.assign(new Error('llm-pi-ai settings are not writable'), { code: 'SETTINGS_UNAVAILABLE' })
@@ -18,6 +18,7 @@ import {
18
18
  settingsOf,
19
19
  llmOf,
20
20
  descriptorOf,
21
+ sectionOf,
21
22
  profileOf,
22
23
  errorMessage,
23
24
  diagnosticMessage,
@@ -488,7 +489,7 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
488
489
  if (typeof saveConfig !== 'function') throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
489
490
 
490
491
  const settings = settingsOf(ctx)
491
- const section = settings?.get?.(PIAI_NS)
492
+ const section = sectionOf(settings, PIAI_NS)
492
493
  const descriptor = descriptorOf(settings, PIAI_NS)
493
494
  const current = normalizeModels(provider, profileOf(settings, provider).models ?? row.models)
494
495
  const nextModels = selectedModels.length
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-model-sync",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Automatic model catalog synchronization for API-key DeepSeek Harness providers.",
5
5
  "keywords": [
6
6
  "deepseek-harness",