dsh-skill-hub 0.2.4 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-skill-hub",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "In-GUI skill hub for DeepSeek Harness (dsh): browse the full local skill catalog from the official ctx.skills registry (every root + third-party providers), toggle skills on/off, inspect bodies, surface frontmatter diagnostics, and scaffold new skills — plus a codex-style skill market (built-in catalog, upstream update checks, one-click update-all) with tracked source sync. The full manager beyond the read-only dsh-skill-manager browser.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -9,8 +9,8 @@
9
9
  import type { ReactElement } from 'react'
10
10
  import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
11
11
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
12
- import { ColorField, PluginSettingsCard, SwitchField } from './settings-card.tsx'
13
- import { booleanField, CardForm, colorField, type CardShell, type FieldState, type FormScope } from './settings-form.ts'
12
+ import { ColorField, NumberField, PluginSettingsCard, SwitchField } from './settings-card.tsx'
13
+ import { booleanField, CardForm, colorField, numberField, type CardShell, type FieldState, type FormScope } from './settings-form.ts'
14
14
  // Single source for the TS-side dot defaults (the panel CSS mirrors these).
15
15
  import { DEFAULT_DOT_MODEL_COLOR, DEFAULT_DOT_USER_COLOR } from './panel/format.ts'
16
16
  // Re-export for any consumer that imported them from the card before the move.
@@ -25,6 +25,8 @@ export interface SkillHubSettingsState extends CardShell {
25
25
  showUseCount: FieldState
26
26
  showUseTime: FieldState
27
27
  showGroupSummary: FieldState
28
+ statsWindowDays: FieldState
29
+ statsScanMinutes: FieldState
28
30
  }
29
31
 
30
32
  /** The business face the card's slot registration injects. */
@@ -50,7 +52,7 @@ export class SkillHubSettingsCardController {
50
52
 
51
53
  /** @param scope - the hub settings scope the card edits (FormScope-compatible). */
52
54
  constructor(scope: FormScope) {
53
- this.form = new CardForm(scope, [booleanField('enabled'), booleanField('announceToAgent'), colorField('dotModelColor'), colorField('dotUserColor'), booleanField('showUseCount'), booleanField('showUseTime'), booleanField('showGroupSummary')])
55
+ this.form = new CardForm(scope, [booleanField('enabled'), booleanField('announceToAgent'), colorField('dotModelColor'), colorField('dotUserColor'), booleanField('showUseCount'), booleanField('showUseTime'), booleanField('showGroupSummary'), numberField('statsWindowDays', { min: 0, max: 3650 }), numberField('statsScanMinutes', { min: 1, max: 1440 })])
54
56
  this.store = this.form.bind(() => this.projection())
55
57
  }
56
58
 
@@ -64,6 +66,8 @@ export class SkillHubSettingsCardController {
64
66
  showUseCount: this.form.field('showUseCount'),
65
67
  showUseTime: this.form.field('showUseTime'),
66
68
  showGroupSummary: this.form.field('showGroupSummary'),
69
+ statsWindowDays: this.form.field('statsWindowDays'),
70
+ statsScanMinutes: this.form.field('statsScanMinutes'),
67
71
  }
68
72
  }
69
73
 
@@ -164,6 +168,30 @@ export function SkillHubSettingsCard(props: SkillHubSettingsCardProps): ReactEle
164
168
  onEdit={(text) => { props.edit('showGroupSummary', text) }}
165
169
  onReset={() => { props.resetField('showGroupSummary') }}
166
170
  />
171
+ <NumberField
172
+ id='skill-hub-stats-window-days'
173
+ label={t('settings.statsWindowDays')}
174
+ hint={t('settings.statsWindowDaysHint')}
175
+ inheritLabel={t('settings.inherit')}
176
+ {...fieldProps}
177
+ text={state.statsWindowDays.text}
178
+ overridden={state.statsWindowDays.overridden}
179
+ invalid={state.statsWindowDays.invalid}
180
+ onEdit={(text) => { props.edit('statsWindowDays', text) }}
181
+ onReset={() => { props.resetField('statsWindowDays') }}
182
+ />
183
+ <NumberField
184
+ id='skill-hub-stats-scan-minutes'
185
+ label={t('settings.statsScanMinutes')}
186
+ hint={t('settings.statsScanMinutesHint')}
187
+ inheritLabel={t('settings.inherit')}
188
+ {...fieldProps}
189
+ text={state.statsScanMinutes.text}
190
+ overridden={state.statsScanMinutes.overridden}
191
+ invalid={state.statsScanMinutes.invalid}
192
+ onEdit={(text) => { props.edit('statsScanMinutes', text) }}
193
+ onReset={() => { props.resetField('statsScanMinutes') }}
194
+ />
167
195
  </PluginSettingsCard>
168
196
  )
169
197
  }
@@ -197,6 +197,10 @@ export const zh = {
197
197
  'settings.showUseTimeHint': '在技能名行右侧显示相对时间(如「3 天前」)。',
198
198
  'settings.showGroupSummary': '显示分组汇总',
199
199
  'settings.showGroupSummaryHint': '在分组标题后汇总调用次数与最近调用时间。',
200
+ 'settings.statsWindowDays': '统计窗口(天)',
201
+ 'settings.statsWindowDaysHint': '只统计最近 N 天的使用次数,默认 14 天;0 = 全部历史。改动立即生效。',
202
+ 'settings.statsScanMinutes': '自动统计间隔(分钟)',
203
+ 'settings.statsScanMinutesHint': '后台扫描会话日志的间隔,最小 1 分钟;扫描耗时会自动拉长间隔。',
200
204
  'settings.inherit': '继承',
201
205
  'settings.on': '开',
202
206
  'settings.off': '关',
@@ -406,6 +410,10 @@ export const en: Record<HubKey, string> = {
406
410
  'settings.showUseTimeHint': 'Show relative last-used time on the name row (e.g. "3 d ago").',
407
411
  'settings.showGroupSummary': 'Show group summaries',
408
412
  'settings.showGroupSummaryHint': 'Summarize invocation count and last-used time after group titles.',
413
+ 'settings.statsWindowDays': 'Stats window (days)',
414
+ 'settings.statsWindowDaysHint': 'Count only usage within the last N days (default 14); 0 = full history. Changes apply immediately.',
415
+ 'settings.statsScanMinutes': 'Auto stats interval (minutes)',
416
+ 'settings.statsScanMinutesHint': 'How often the background session-log scan runs (min 1); heavy scans automatically stretch it.',
409
417
  'settings.inherit': 'Inherit',
410
418
  'settings.on': 'On',
411
419
  'settings.off': 'Off',
@@ -119,7 +119,7 @@ export function SkillHubPanel(props: SkillHubPanelProps): React.JSX.Element {
119
119
  return (
120
120
  <div className={css.panel}>
121
121
  <div className={css.header}>
122
- <h2 className={css.title}><IconSkillOutline16 size={16} className={css.titleIcon} /> {tt('panel.title')}</h2>
122
+ <h2 className={css.title}><IconSkillOutline16 size={16} className={css.titleIcon} /> {tt('panel.title')}{catalog !== null ? <span className={css.pluginVersion}>v{catalog.pluginVersion}</span> : null}</h2>
123
123
  {catalog !== null
124
124
  ? <span className={css.headerCount}>
125
125
  {tt('panel.count', { count: catalog.skills.length + catalog.disabled.length })}
@@ -120,6 +120,7 @@
120
120
 
121
121
  /* Reduced header: title + quiet count on one line, controls on the next. */
122
122
  .headerCount { font-size: 12px; opacity: .5; margin-left: 2px; white-space: nowrap; }
123
+ .pluginVersion { font-size: 12px; font-weight: 500; opacity: .45; margin-left: 6px; white-space: nowrap; font-variant-numeric: tabular-nums; }
123
124
  .subbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
124
125
  .legendToggle { flex: none; width: 22px; height: 22px; border-radius: 50%; border: none; background: rgba(128,128,128,.14); color: inherit; font-size: 12px; line-height: 1; cursor: pointer; opacity: .55; transition: opacity .15s ease, background .15s ease; }
125
126
  .legendToggle:hover { opacity: 1; }
@@ -236,3 +236,51 @@ export function ColorField(props: ColorFieldProps): ReactElement {
236
236
  </div>
237
237
  )
238
238
  }
239
+
240
+ /** One staged numeric field: a numeric draft text input with inherit/reset semantics. */
241
+ export interface NumberFieldProps {
242
+ id: string
243
+ label: string
244
+ hint: string
245
+ /** Placeholder shown while the field inherits its default. */
246
+ inheritLabel: string
247
+ overriddenLabel: string
248
+ resetLabel: string
249
+ disabled: boolean
250
+ /** Effective number when overridden; empty string means inherit. */
251
+ text: string
252
+ overridden: boolean
253
+ invalid?: boolean
254
+ onEdit: (text: string) => void
255
+ onReset: () => void
256
+ }
257
+
258
+ export function NumberField(props: NumberFieldProps): ReactElement {
259
+ return (
260
+ <div className={css.field}>
261
+ <div className={css.head}>
262
+ <label className={css.label} htmlFor={props.id}>{props.label}</label>
263
+ {props.overridden ? (
264
+ <span className={css.badges}>
265
+ <span className={css.badge}>{props.overriddenLabel}</span>
266
+ <button type='button' className={css.reset} disabled={props.disabled} onClick={props.onReset}>
267
+ {props.resetLabel}
268
+ </button>
269
+ </span>
270
+ ) : null}
271
+ </div>
272
+ <input
273
+ id={props.id}
274
+ type='text'
275
+ inputMode='numeric'
276
+ className={css.input}
277
+ value={props.text}
278
+ disabled={props.disabled}
279
+ placeholder={props.inheritLabel}
280
+ aria-invalid={props.invalid === true}
281
+ onChange={(event) => { props.onEdit(event.target.value) }}
282
+ />
283
+ <p className={css.hint}>{props.hint}</p>
284
+ </div>
285
+ )
286
+ }
@@ -49,6 +49,27 @@ export function colorField(field: string): FieldSpec {
49
49
  }
50
50
  }
51
51
 
52
+ /**
53
+ * A numeric field with range clamping. Integer by default: a fractional draft
54
+ * is truncated so "5.5 分钟" cannot sneak past an integer-only schema.
55
+ */
56
+ export function numberField(field: string, options: { min?: number; max?: number; integer?: boolean } = {}): FieldSpec {
57
+ const min = options.min ?? Number.NEGATIVE_INFINITY
58
+ const max = options.max ?? Number.POSITIVE_INFINITY
59
+ const integer = options.integer ?? true
60
+ return {
61
+ field,
62
+ format: (value) => (typeof value === 'number' && Number.isFinite(value) ? String(value) : ''),
63
+ parse: (text) => {
64
+ const trimmed = text.trim()
65
+ if (!/^-?\d+(\.\d+)?$/.test(trimmed)) return undefined
66
+ const parsed = integer ? Math.trunc(Number(trimmed)) : Number(trimmed)
67
+ if (!Number.isFinite(parsed) || parsed < min || parsed > max) return undefined
68
+ return { kind: 'set', value: parsed }
69
+ },
70
+ }
71
+ }
72
+
52
73
  /** Card-level state the chrome renders. */
53
74
  export interface CardShell {
54
75
  available: boolean
package/src/index.ts CHANGED
@@ -40,6 +40,10 @@ export interface Config {
40
40
  showUseTime?: boolean
41
41
  /** Show group-header usage summaries (count + last used). Default true. */
42
42
  showGroupSummary?: boolean
43
+ /** 统计滚动窗口天数:只统计最近 N 天的使用;0 = 全部历史。默认 0。 */
44
+ statsWindowDays?: number
45
+ /** 自动统计扫描间隔(分钟,最小 1)。默认 5。 */
46
+ statsScanMinutes?: number
43
47
  }
44
48
 
45
49
  export const Config: z<Config> = z.object({
@@ -48,6 +52,8 @@ export const Config: z<Config> = z.object({
48
52
  showUseCount: z.boolean().default(HUB_CONFIG_DEFAULTS.showUseCount),
49
53
  showUseTime: z.boolean().default(HUB_CONFIG_DEFAULTS.showUseTime),
50
54
  showGroupSummary: z.boolean().default(HUB_CONFIG_DEFAULTS.showGroupSummary),
55
+ statsWindowDays: z.number().min(0).max(3650).default(HUB_CONFIG_DEFAULTS.statsWindowDays),
56
+ statsScanMinutes: z.number().min(1).max(1440).default(HUB_CONFIG_DEFAULTS.statsScanMinutes),
51
57
  })
52
58
 
53
59
  /**
@@ -68,6 +74,8 @@ export const HubSettingsSchema: z<HubSettingsValue> = z.object({
68
74
  showGroupSummary: z.boolean().default(HUB_CONFIG_DEFAULTS.showGroupSummary),
69
75
  dotModelColor: z.string().pattern(HEX_COLOR_RE),
70
76
  dotUserColor: z.string().pattern(HEX_COLOR_RE),
77
+ statsWindowDays: z.number().min(0).max(3650).default(HUB_CONFIG_DEFAULTS.statsWindowDays),
78
+ statsScanMinutes: z.number().min(1).max(1440).default(HUB_CONFIG_DEFAULTS.statsScanMinutes),
71
79
  })
72
80
 
73
81
  /** Order of the announcement section within the tool-guidance band. */
@@ -126,7 +134,7 @@ export function apply(ctx: Context, config?: Config): void {
126
134
  // surfaces once the namespace commits.
127
135
  const updateConfig = async (patch: Partial<HubConfig>): Promise<HubConfig> => {
128
136
  const user: Record<string, unknown> = { ...saved() }
129
- for (const [key, value] of Object.entries(patch) as Array<[keyof HubConfig, boolean | string | undefined]>) {
137
+ for (const [key, value] of Object.entries(patch) as Array<[keyof HubConfig, boolean | string | number | undefined]>) {
130
138
  if (value === undefined) delete user[key]
131
139
  else user[key] = value
132
140
  }
@@ -222,7 +230,30 @@ export function apply(ctx: Context, config?: Config): void {
222
230
  // stats wiring — no sessionQuery service ever mounted means none of this
223
231
  // runs).
224
232
  ctx.inject(['sessionQuery'], (sctx) => {
225
- stats = createSkillStatsReader(sctx.sessionQuery)
226
- sync()
233
+ // 恢复 sidecar 里的增量扫描检查点后再建 reader(异步、不阻塞注入回调):
234
+ // 重启后无需重新解压全部历史日志;检查点只在全量对账后落盘(约每天一次,
235
+ // 或统计窗口配置变化后的下一次扫描)。扫描间隔与滚动窗口都从设置命名空间
236
+ // 实时读取——卡片里改完即生效,无需重启。
237
+ void (async () => {
238
+ const saved = await store.getSkillStatsState().catch(() => undefined)
239
+ const scanMinutes = (): number => {
240
+ const value = current().statsScanMinutes
241
+ return typeof value === 'number' && value >= 1 ? Math.floor(value) : HUB_CONFIG_DEFAULTS.statsScanMinutes
242
+ }
243
+ const windowDays = (): number => {
244
+ const value = current().statsWindowDays
245
+ return typeof value === 'number' && value >= 0 ? Math.floor(value) : HUB_CONFIG_DEFAULTS.statsWindowDays
246
+ }
247
+ stats = createSkillStatsReader(sctx.sessionQuery, () => scanMinutes() * 60_000, {
248
+ checkpoint: saved,
249
+ windowDays,
250
+ onCheckpoint: (next) => {
251
+ void store.saveSkillStatsState(next).catch((error) => {
252
+ ctx.logger.warn('[dsh-skill-hub] persisting skill-stats checkpoint failed', error)
253
+ })
254
+ },
255
+ })
256
+ sync()
257
+ })()
227
258
  })
228
259
  }
package/src/protocol.ts CHANGED
@@ -88,6 +88,8 @@ export interface DiagnosticEntry {
88
88
  /** GET /api/skill-hub/catalog */
89
89
  export interface CatalogResponse {
90
90
  ok: true
91
+ /** 已安装插件自身的版本号(package.json version),面板标题旁显示。 */
92
+ pluginVersion: string
91
93
  /** Whether discovery completed within a stable catalog revision. */
92
94
  complete: boolean
93
95
  /** Sorted winning summaries of every enabled skill (all roots + providers). */
@@ -196,6 +198,25 @@ export interface StatsResponse {
196
198
  stats: SkillStat[]
197
199
  }
198
200
 
201
+ /**
202
+ * Persisted incremental-scan checkpoint for the usage statistics (sidecar
203
+ * `skillStats` field). Sessions created before `frozenBefore` are treated as
204
+ * finalized: their per-session counts live in `frozenSessions` (only sessions
205
+ * with at least one invocation are kept) and they are not re-read on
206
+ * incremental scans. A daily full reconciliation rebuilds the cache and
207
+ * advances the watermark, so a resumed old session is eventually re-counted.
208
+ */
209
+ export interface SkillStatsCheckpoint {
210
+ /** The rolling-window configuration this checkpoint was built for (0 = all history). */
211
+ windowDays: number
212
+ /** Watermark: every session with header.createdAt < this value is frozen. */
213
+ frozenBefore: number
214
+ /** Per-session counts of finalized sessions, keyed by session id. */
215
+ frozenSessions: Record<string, { createdAt: number; counts: Record<string, { count: number; lastUsed: number }> }>
216
+ /** Epoch ms of the last full reconciliation (drives the daily cadence). */
217
+ lastFullReconcile: number
218
+ }
219
+
199
220
  /** JSON error body shared by every route. */
200
221
  export interface ErrorResponse {
201
222
  error: string
@@ -217,6 +238,10 @@ export interface HubConfig {
217
238
  showUseTime?: boolean
218
239
  /** Show group-header usage summaries (count + last used). Default true. */
219
240
  showGroupSummary?: boolean
241
+ /** 统计滚动窗口天数:只统计最近 N 天的使用;0 = 全部历史。默认 0。 */
242
+ statsWindowDays?: number
243
+ /** 自动统计扫描间隔(分钟,最小 1)。默认 5。 */
244
+ statsScanMinutes?: number
220
245
  }
221
246
 
222
247
  /**
@@ -235,6 +260,10 @@ export type HubSettingsValue = {
235
260
  dotModelColor?: string
236
261
  /** User-invocable dot color (#rrggbb); absent means the panel default. */
237
262
  dotUserColor?: string
263
+ /** 统计滚动窗口天数(0 = 全部历史)。 */
264
+ statsWindowDays?: number
265
+ /** 自动统计扫描间隔(分钟)。 */
266
+ statsScanMinutes?: number
238
267
  }
239
268
 
240
269
  /**
@@ -248,28 +277,41 @@ export const HUB_CONFIG_DEFAULTS = {
248
277
  showUseCount: true,
249
278
  showUseTime: true,
250
279
  showGroupSummary: true,
280
+ statsWindowDays: 14,
281
+ statsScanMinutes: 5,
251
282
  } as const
252
283
 
253
284
  /**
254
285
  * Resolve the effective hub config: saved sidecar overrides win over the
255
286
  * cordis composition entry (the web card owns runtime config), missing
256
287
  * booleans fall back to HUB_CONFIG_DEFAULTS, and dot colors pass through
257
- * (saved first, then base) only when set.
288
+ * (saved first, then base) only when set. Numeric stats knobs are clamped to
289
+ * their sane ranges (window ≥ 0, scan interval ≥ 1 minute).
258
290
  */
259
291
  export function resolveHubConfig(saved: Partial<HubConfig>, base: Partial<HubConfig> = {}): HubConfig {
260
292
  const dotModelColor = saved.dotModelColor !== undefined ? saved.dotModelColor : base.dotModelColor
261
293
  const dotUserColor = saved.dotUserColor !== undefined ? saved.dotUserColor : base.dotUserColor
294
+ const windowDays = clampNumber(saved.statsWindowDays ?? base.statsWindowDays, 0) ?? HUB_CONFIG_DEFAULTS.statsWindowDays
295
+ const scanMinutes = clampNumber(saved.statsScanMinutes ?? base.statsScanMinutes, 1) ?? HUB_CONFIG_DEFAULTS.statsScanMinutes
262
296
  return {
263
297
  enabled: saved.enabled ?? base.enabled ?? HUB_CONFIG_DEFAULTS.enabled,
264
298
  announceToAgent: saved.announceToAgent ?? base.announceToAgent ?? HUB_CONFIG_DEFAULTS.announceToAgent,
265
299
  showUseCount: saved.showUseCount ?? base.showUseCount ?? HUB_CONFIG_DEFAULTS.showUseCount,
266
300
  showUseTime: saved.showUseTime ?? base.showUseTime ?? HUB_CONFIG_DEFAULTS.showUseTime,
267
301
  showGroupSummary: saved.showGroupSummary ?? base.showGroupSummary ?? HUB_CONFIG_DEFAULTS.showGroupSummary,
302
+ statsWindowDays: windowDays,
303
+ statsScanMinutes: scanMinutes,
268
304
  ...(dotModelColor !== undefined ? { dotModelColor } : {}),
269
305
  ...(dotUserColor !== undefined ? { dotUserColor } : {}),
270
306
  }
271
307
  }
272
308
 
309
+ /** Clamp a numeric override into a valid value; undefined/invalid stays undefined. */
310
+ function clampNumber(value: unknown, min: number): number | undefined {
311
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < min) return undefined
312
+ return Math.floor(value)
313
+ }
314
+
273
315
  /** HEX color validation shared by host routes and the settings card. */
274
316
  export const HEX_COLOR_RE = /^#[0-9a-f]{6}$/i
275
317
 
@@ -298,6 +340,10 @@ export interface ConfigRequest {
298
340
  dotModelColor?: string | null
299
341
  /** Set the dot color; null clears the saved override so it re-inherits the default. */
300
342
  dotUserColor?: string | null
343
+ /** 统计滚动窗口天数(0 = 全部历史);null 清除覆盖回默认。 */
344
+ statsWindowDays?: number | null
345
+ /** 自动统计扫描间隔(分钟);null 清除覆盖回默认。 */
346
+ statsScanMinutes?: number | null
301
347
  }
302
348
 
303
349
  /** One market source: a tracked upstream repo plus its pinned version. */
@@ -127,6 +127,8 @@ describe('skill-hub routes', () => {
127
127
  expect(body.skills[1].writable).toBe(false)
128
128
  expect(body.diagnostics).toHaveLength(1)
129
129
  expect(body.diagnostics[0].reason).toBe('missing YAML frontmatter (--- block)')
130
+ // 面板标题旁的插件版本徽标数据源:随 catalog 附带当前安装版本。
131
+ expect(body.pluginVersion).toMatch(/^\d+\.\d+\.\d+$/)
130
132
  })
131
133
 
132
134
  it('forwards the cwd query to the registry snapshot', async () => {
@@ -394,7 +396,7 @@ describe('skill-hub routes', () => {
394
396
  expect(res.status).toBe(200)
395
397
  const body = res.json() as ConfigResponse
396
398
  expect(body.ok).toBe(true)
397
- expect(body.config).toEqual({ enabled: false, announceToAgent: true, showUseCount: true, showUseTime: true, showGroupSummary: true })
399
+ expect(body.config).toEqual({ enabled: false, announceToAgent: true, showUseCount: true, showUseTime: true, showGroupSummary: true, statsWindowDays: 14, statsScanMinutes: 5 })
398
400
  expect(body.saved).toEqual({ enabled: false })
399
401
  })
400
402
 
package/src/routes.ts CHANGED
@@ -85,7 +85,7 @@ import {
85
85
  resolveHubConfig,
86
86
  } from './protocol.ts'
87
87
  import { clearTrash, createSkill, disableSkill, enableSkill, restoreSkill, rootOfPath, rootPath, scanDiagnostics, trashSkill } from './skillfs.ts'
88
- import { checkLatestRelease } from './update.ts'
88
+ import { checkLatestRelease, CURRENT_VERSION } from './update.ts'
89
89
  import { dshHome, StoreError, type SkillHubStore } from './store.ts'
90
90
  import type { SkillStatsReader } from './stats.ts'
91
91
 
@@ -410,7 +410,7 @@ async function buildCatalog(deps: SkillHubRouteDeps, cwd?: string): Promise<Cata
410
410
  ...(await scanDiagnostics('user-dsh', home)),
411
411
  ...(await scanDiagnostics('user-agents', home)),
412
412
  ]
413
- return { ok: true, complete, skills, disabled, diagnostics }
413
+ return { ok: true, pluginVersion: CURRENT_VERSION, complete, skills, disabled, diagnostics }
414
414
  }
415
415
 
416
416
  /** Map a loaded definition onto the wire shape. */