dsh-skill-hub 0.1.0 → 0.2.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.
Files changed (79) hide show
  1. package/README.md +146 -33
  2. package/lib/client.js +3477 -436
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2408 -366
  5. package/lib/types/client/SkillHubSettingsCard.d.ts +5 -0
  6. package/lib/types/client/api-config-scope.d.ts +6 -0
  7. package/lib/types/client/api.d.ts +44 -2
  8. package/lib/types/client/grouping.d.ts +64 -0
  9. package/lib/types/client/locales.d.ts +155 -18
  10. package/lib/types/client/market-catalog.d.ts +14 -0
  11. package/lib/types/client/panel/DisabledRow.d.ts +12 -0
  12. package/lib/types/client/panel/GroupSummary.d.ts +10 -0
  13. package/lib/types/client/panel/MarketView.d.ts +11 -0
  14. package/lib/types/client/panel/ScenesView.d.ts +11 -0
  15. package/lib/types/client/panel/SkillDetailView.d.ts +31 -0
  16. package/lib/types/client/panel/SkillHubPanel.d.ts +11 -3
  17. package/lib/types/client/panel/SkillRow.d.ts +11 -0
  18. package/lib/types/client/panel/SourceStatusBadge.d.ts +18 -0
  19. package/lib/types/client/panel/SourcesView.d.ts +10 -0
  20. package/lib/types/client/panel/TagEditorView.d.ts +29 -0
  21. package/lib/types/client/panel/dialogs.d.ts +73 -0
  22. package/lib/types/client/panel/format.d.ts +14 -0
  23. package/lib/types/client/panel/useSkillHub.d.ts +183 -0
  24. package/lib/types/client/settings-card.d.ts +21 -4
  25. package/lib/types/client/settings-form.d.ts +2 -0
  26. package/lib/types/client/settings-nav-icon.d.ts +8 -0
  27. package/lib/types/index.d.ts +6 -0
  28. package/lib/types/protocol.d.ts +428 -7
  29. package/lib/types/provider.d.ts +6 -1
  30. package/lib/types/repo.d.ts +133 -0
  31. package/lib/types/routes.d.ts +9 -4
  32. package/lib/types/skillfs.d.ts +19 -14
  33. package/lib/types/stats.d.ts +17 -4
  34. package/lib/types/store.d.ts +112 -6
  35. package/lib/types/update.d.ts +20 -0
  36. package/package.json +5 -3
  37. package/src/client/SkillHubSettingsCard.tsx +61 -8
  38. package/src/client/api-config-scope.test.ts +79 -0
  39. package/src/client/api-config-scope.ts +28 -4
  40. package/src/client/api.ts +239 -9
  41. package/src/client/grouping.test.ts +109 -0
  42. package/src/client/grouping.ts +120 -0
  43. package/src/client/index.tsx +5 -2
  44. package/src/client/locales.ts +314 -40
  45. package/src/client/market-catalog.ts +22 -0
  46. package/src/client/panel/DisabledRow.tsx +32 -0
  47. package/src/client/panel/GroupSummary.tsx +27 -0
  48. package/src/client/panel/MarketView.tsx +173 -0
  49. package/src/client/panel/ScenesView.tsx +75 -0
  50. package/src/client/panel/SkillDetailView.tsx +95 -0
  51. package/src/client/panel/SkillHubPanel.tsx +251 -257
  52. package/src/client/panel/SkillRow.tsx +54 -0
  53. package/src/client/panel/SourceStatusBadge.tsx +61 -0
  54. package/src/client/panel/SourcesView.tsx +104 -0
  55. package/src/client/panel/TagEditorView.tsx +89 -0
  56. package/src/client/panel/dialogs.tsx +185 -0
  57. package/src/client/panel/format.ts +31 -0
  58. package/src/client/panel/panel.module.css +209 -45
  59. package/src/client/panel/useSkillHub.ts +797 -0
  60. package/src/client/settings-card.module.css +44 -28
  61. package/src/client/settings-card.tsx +64 -8
  62. package/src/client/settings-form.ts +12 -0
  63. package/src/client/settings-nav-icon.ts +42 -0
  64. package/src/index.ts +19 -15
  65. package/src/protocol.ts +460 -7
  66. package/src/provider.test.ts +2 -4
  67. package/src/provider.ts +18 -14
  68. package/src/repo.test.ts +212 -0
  69. package/src/repo.ts +528 -0
  70. package/src/routes.test.ts +564 -52
  71. package/src/routes.ts +999 -153
  72. package/src/skillfs.test.ts +98 -16
  73. package/src/skillfs.ts +68 -33
  74. package/src/stats.test.ts +52 -17
  75. package/src/stats.ts +55 -18
  76. package/src/store.test.ts +205 -1
  77. package/src/store.ts +527 -24
  78. package/src/update.test.ts +73 -0
  79. package/src/update.ts +136 -0
@@ -8,10 +8,31 @@
8
8
  export declare const SKILL_HUB_API: {
9
9
  readonly catalog: "/api/skill-hub/catalog";
10
10
  readonly skill: "/api/skill-hub/skill";
11
+ readonly skillDelete: "/api/skill-hub/skill/delete";
11
12
  readonly toggle: "/api/skill-hub/toggle";
13
+ readonly toggleBatch: "/api/skill-hub/toggle-batch";
12
14
  readonly create: "/api/skill-hub/create";
13
15
  readonly stats: "/api/skill-hub/stats";
14
16
  readonly config: "/api/skill-hub/config";
17
+ readonly market: "/api/skill-hub/market";
18
+ readonly marketSource: "/api/skill-hub/market/source";
19
+ readonly marketSourceDelete: "/api/skill-hub/market/source/delete";
20
+ readonly marketSourceRef: "/api/skill-hub/market/source/ref";
21
+ readonly marketCheck: "/api/skill-hub/market/check";
22
+ readonly marketSync: "/api/skill-hub/market/source/sync";
23
+ readonly repo: "/api/skill-hub/repo";
24
+ readonly repoImport: "/api/skill-hub/repo/import";
25
+ readonly update: "/api/skill-hub/update";
26
+ readonly groups: "/api/skill-hub/groups";
27
+ readonly tag: "/api/skill-hub/tag";
28
+ readonly tagDelete: "/api/skill-hub/tag/delete";
29
+ readonly tagMembers: "/api/skill-hub/tag/members";
30
+ readonly sources: "/api/skill-hub/sources";
31
+ readonly sourceCheck: "/api/skill-hub/sources/check";
32
+ readonly sourceSync: "/api/skill-hub/sources/sync";
33
+ readonly sourceDelete: "/api/skill-hub/sources/delete";
34
+ readonly sourceRestore: "/api/skill-hub/sources/restore";
35
+ readonly sourceTrashClear: "/api/skill-hub/sources/trash/clear";
15
36
  };
16
37
  /** User-level roots the hub may write to (matches dsh-skill-filesystem ranks 400/500). */
17
38
  export type WritableRoot = 'user-dsh' | 'user-agents';
@@ -27,13 +48,14 @@ export interface CatalogSkill {
27
48
  name: string;
28
49
  description: string;
29
50
  whenToUse?: string;
30
- /** Optional group names declared in the skill's frontmatter (`sets`). */
31
- sets?: string[];
32
51
  invocation: HubInvocation;
33
- source: string;
34
52
  provider: string;
35
53
  /** Whether the hub may toggle this skill (user-level filesystem skills only). */
36
54
  writable: boolean;
55
+ /** SKILL.md creation time (epoch ms); used for "added" sorting. Absent when unknown. */
56
+ addedAt?: number;
57
+ /** SKILL.md last-modified time (epoch ms); used for "updated" display. Absent when unknown. */
58
+ updatedAt?: number;
37
59
  }
38
60
  /** One disabled skill tracked by the hub sidecar (SKILL.md renamed away). */
39
61
  export interface DisabledSkill {
@@ -67,13 +89,14 @@ export interface SkillDetail {
67
89
  name: string;
68
90
  description: string;
69
91
  whenToUse?: string;
70
- /** Optional group names declared in the skill's frontmatter (`sets`). */
71
- sets?: string[];
72
92
  invocation: HubInvocation;
73
- source: string;
74
93
  provider: string;
75
94
  /** Absolute file path when the skill came from disk. */
76
95
  path?: string;
96
+ /** SKILL.md creation time (epoch ms); absent when the file is unreadable. */
97
+ addedAt?: number;
98
+ /** SKILL.md last-modified time (epoch ms); absent when the file is unreadable. */
99
+ updatedAt?: number;
77
100
  /** Markdown instruction body. */
78
101
  content: string;
79
102
  }
@@ -81,6 +104,17 @@ export interface SkillDetailResponse {
81
104
  ok: true;
82
105
  skill: SkillDetail;
83
106
  }
107
+ /** POST /api/skill-hub/skill/delete — 把单个技能移入回收站(可恢复)。 */
108
+ export interface SkillDeleteRequest {
109
+ name: string;
110
+ }
111
+ /** POST /api/skill-hub/skill/delete */
112
+ export interface SkillDeleteResponse {
113
+ ok: true;
114
+ name: string;
115
+ /** 移入回收站后的路径(目录或文件)。 */
116
+ path: string;
117
+ }
84
118
  /** POST /api/skill-hub/toggle */
85
119
  export interface ToggleRequest {
86
120
  /** Kebab-case skill name. */
@@ -93,6 +127,21 @@ export interface ToggleResponse {
93
127
  /** Fresh catalog after the mutation (the filesystem provider may lag a beat). */
94
128
  catalog: CatalogResponse;
95
129
  }
130
+ /** POST /api/skill-hub/toggle-batch — one group of skills, one write. */
131
+ export interface ToggleBatchRequest {
132
+ names: string[];
133
+ enabled: boolean;
134
+ }
135
+ /** POST /api/skill-hub/toggle-batch */
136
+ export interface ToggleBatchResponse {
137
+ ok: true;
138
+ catalog: CatalogResponse;
139
+ /** Per-name failures (unknown/read-only skills); empty means all landed. */
140
+ failures: Array<{
141
+ name: string;
142
+ error: string;
143
+ }>;
144
+ }
96
145
  /** POST /api/skill-hub/create */
97
146
  export interface CreateRequest {
98
147
  /** Kebab-case skill name (validated with the official isSkillName grammar). */
@@ -107,12 +156,14 @@ export interface CreateResponse {
107
156
  path: string;
108
157
  root: WritableRoot;
109
158
  }
110
- /** One skill's invocation count across the local session logs. */
159
+ /** One skill's invocation stats across the local session logs. */
111
160
  export interface SkillStat {
112
161
  /** Kebab-case skill name. */
113
162
  name: string;
114
163
  /** How many sessions recorded a user-explicit invocation of this skill. */
115
164
  count: number;
165
+ /** Unix epoch ms of the most recent invocation (absent when never called). */
166
+ lastUsed?: number;
116
167
  }
117
168
  /** GET /api/skill-hub/stats */
118
169
  export interface StatsResponse {
@@ -132,7 +183,38 @@ export interface HubConfig {
132
183
  enabled: boolean;
133
184
  /** When true, a system-prompt section announces the hub to every agent. */
134
185
  announceToAgent: boolean;
186
+ /** Model-invocable dot color (#rrggbb); absent means the panel default. */
187
+ dotModelColor?: string;
188
+ /** User-invocable dot color (#rrggbb); absent means the panel default. */
189
+ dotUserColor?: string;
190
+ /** Show per-skill invocation count chip. Default true. */
191
+ showUseCount?: boolean;
192
+ /** Show per-skill last-used relative time. Default true. */
193
+ showUseTime?: boolean;
194
+ /** Show group-header usage summaries (count + last used). Default true. */
195
+ showGroupSummary?: boolean;
135
196
  }
197
+ /**
198
+ * Hub config defaults — the single source every layer reads: the cordis
199
+ * schema (index.ts), the host's saved-override merge, and the routes'
200
+ * fallback view. Changing a default here changes all three.
201
+ */
202
+ export declare const HUB_CONFIG_DEFAULTS: {
203
+ readonly enabled: true;
204
+ readonly announceToAgent: true;
205
+ readonly showUseCount: true;
206
+ readonly showUseTime: true;
207
+ readonly showGroupSummary: true;
208
+ };
209
+ /**
210
+ * Resolve the effective hub config: saved sidecar overrides win over the
211
+ * cordis composition entry (the web card owns runtime config), missing
212
+ * booleans fall back to HUB_CONFIG_DEFAULTS, and dot colors pass through
213
+ * (saved first, then base) only when set.
214
+ */
215
+ export declare function resolveHubConfig(saved: Partial<HubConfig>, base?: Partial<HubConfig>): HubConfig;
216
+ /** HEX color validation shared by host routes and the settings card. */
217
+ export declare const HEX_COLOR_RE: RegExp;
136
218
  /** GET /api/skill-hub/config */
137
219
  export interface ConfigResponse {
138
220
  ok: true;
@@ -147,4 +229,343 @@ export interface ConfigRequest {
147
229
  enabled?: boolean | null;
148
230
  /** Set the field; null clears the saved override so it re-inherits the default. */
149
231
  announceToAgent?: boolean | null;
232
+ /** Set the field; null clears the saved override so it re-inherits the default. */
233
+ showUseCount?: boolean | null;
234
+ /** Set the field; null clears the saved override so it re-inherits the default. */
235
+ showUseTime?: boolean | null;
236
+ /** Set the field; null clears the saved override so it re-inherits the default. */
237
+ showGroupSummary?: boolean | null;
238
+ /** Set the dot color; null clears the saved override so it re-inherits the default. */
239
+ dotModelColor?: string | null;
240
+ /** Set the dot color; null clears the saved override so it re-inherits the default. */
241
+ dotUserColor?: string | null;
242
+ }
243
+ /** One market source: a tracked upstream repo plus its pinned version. */
244
+ export interface MarketSourceRecord {
245
+ /** Repo slug (owner/repo). */
246
+ repo: string;
247
+ /** Pinned version: release tag, branch name, or custom ref. Absent = not yet pinned. */
248
+ ref?: string;
249
+ /** Commit the pinned ref last resolved to (update-check baseline). */
250
+ commitSha?: string;
251
+ }
252
+ /** GET /api/skill-hub/market — the user's added market sources. */
253
+ export interface MarketSourcesResponse {
254
+ ok: true;
255
+ /** Added market sources, in addition order. */
256
+ repos: MarketSourceRecord[];
257
+ }
258
+ /** POST /api/skill-hub/market/source — add one repo as a market source. */
259
+ export interface MarketSourceRequest {
260
+ /** owner/repo, owner/repo@ref, or a github.com URL. */
261
+ repo: string;
262
+ }
263
+ /** POST /api/skill-hub/market/source|delete */
264
+ export interface MarketSourceResponse {
265
+ ok: true;
266
+ repos: MarketSourceRecord[];
267
+ }
268
+ /** POST /api/skill-hub/market/source/ref — pin a market source to an explicit ref. */
269
+ export interface MarketSourceRefRequest {
270
+ repo: string;
271
+ /** Release tag or branch name chosen for the repo. */
272
+ ref: string;
273
+ }
274
+ /** GET /api/skill-hub/market/check — update check over market sources. */
275
+ export interface MarketCheckResponse {
276
+ ok: true;
277
+ results: Array<{
278
+ repo: string;
279
+ /** Pinned ref, when one is recorded. */
280
+ ref?: string;
281
+ /** Latest release tag on the repo, when it has one. */
282
+ latestTag?: string;
283
+ /** Whether the pinned ref (or the repo) has moved ahead. */
284
+ updateAvailable: boolean;
285
+ /** Latest commit of the checked ref. */
286
+ commitSha: string;
287
+ /** Throttled: the check was skipped within the interval. */
288
+ throttled?: boolean;
289
+ error?: string;
290
+ }>;
291
+ }
292
+ /** POST /api/skill-hub/market/source/sync — align a market source to its pinned ref. */
293
+ export interface MarketSyncResponse {
294
+ ok: true;
295
+ repo: string;
296
+ /** The ref the source is now pinned to. */
297
+ ref: string;
298
+ commitSha: string;
299
+ /** Local skills tracked from this repo (candidates for a batch update). */
300
+ skills: string[];
301
+ }
302
+ /** A skill discovered in a GitHub repo under skills/ or design-templates/. */
303
+ export interface RepoSkillEntry {
304
+ /** Kebab-case skill name (directory basename). */
305
+ name: string;
306
+ /** Root-relative skill directory, e.g. skills/code-review. */
307
+ dir: string;
308
+ /** Root-relative SKILL.md path, e.g. skills/code-review/SKILL.md. */
309
+ path: string;
310
+ /** Repo root the skill lives in. */
311
+ root: RepoRoot;
312
+ /** Collection name recorded as origin after import. */
313
+ origin: string;
314
+ /** Number of files in the skill directory. */
315
+ fileCount: number;
316
+ /** Total bytes across all files in the skill directory. */
317
+ totalBytes: number;
318
+ /** Whether a same-named skill already exists locally. */
319
+ existing: boolean;
320
+ }
321
+ /** Supported skill roots in a GitHub repo. */
322
+ export type RepoRoot = 'skills' | 'design-templates';
323
+ /** GET /api/skill-hub/repo — discover importable skills in a GitHub repo. */
324
+ export interface RepoDiscoverResponse {
325
+ ok: true;
326
+ repo: string;
327
+ /**
328
+ * Ref the discovery ran against (release tag / branch / default branch).
329
+ * Null means the repo has no release and no pinned ref — the client must
330
+ * ask the user to pick a branch (see `branches`) and pin it first.
331
+ */
332
+ ref: string | null;
333
+ /** Branch names to choose from when ref is null (default branch first). */
334
+ branches?: string[];
335
+ entries: RepoSkillEntry[];
336
+ }
337
+ /** POST /api/skill-hub/repo/import — install selected repo skills. */
338
+ export interface RepoImportRequest {
339
+ repo: string;
340
+ /** Selected SKILL.md paths from the discover response. */
341
+ paths: string[];
342
+ /** The ref the discovery ran against; the import must use the same one. */
343
+ ref?: string;
344
+ }
345
+ /** POST /api/skill-hub/repo/import */
346
+ export interface RepoImportResponse {
347
+ ok: true;
348
+ imported: Array<{
349
+ name: string;
350
+ origin: string;
351
+ path: string;
352
+ }>;
353
+ skipped: Array<{
354
+ name: string;
355
+ reason: 'exists';
356
+ }>;
357
+ failed: Array<{
358
+ name: string;
359
+ error: string;
360
+ }>;
361
+ }
362
+ /** GET /api/skill-hub/update — check the plugin's own latest GitHub release. */
363
+ export interface UpdateCheckResponse {
364
+ ok: true;
365
+ /** Installed plugin version (from the bundled host build). */
366
+ currentVersion: string;
367
+ /** Latest release tag, normalized without the leading v; null when unknown. */
368
+ latestVersion: string | null;
369
+ /** True when latestVersion is strictly newer than currentVersion. */
370
+ updateAvailable: boolean;
371
+ /** Release page URL; null when unavailable. */
372
+ url: string | null;
373
+ /** Best-effort message when the check failed or no release exists. */
374
+ error?: string;
375
+ }
376
+ /** 用户自定义 tag 分组:纯组织视图,不改技能文件。 */
377
+ export interface SkillTag {
378
+ /** 稳定 ID(新建时由宿主生成)。 */
379
+ id: string;
380
+ /** 显示名。 */
381
+ name: string;
382
+ /** 成员技能名(集合技能与独立技能都可加入)。 */
383
+ skillNames: string[];
384
+ /** 默认场景(「通用」):系统预置、不可删除、新技能自动归入。 */
385
+ default?: boolean;
386
+ }
387
+ /** 系统集合组:按 origin(来源集合标识)自动聚合。 */
388
+ export interface CollectionGroup {
389
+ /** 集合名(origin 值,如 "superpowers" / "anthropics/skills")。 */
390
+ name: string;
391
+ /** 该集合下的技能名,按名称排序。 */
392
+ skillNames: string[];
393
+ }
394
+ /** GET /api/skill-hub/groups */
395
+ export interface GroupsResponse {
396
+ ok: true;
397
+ /** 用户自定义 tag 分组(按创建顺序)。 */
398
+ tags: SkillTag[];
399
+ /** 系统集合组(按 origin 聚合,名称排序)。 */
400
+ collections: CollectionGroup[];
401
+ /** skillName → 集合名 的完整映射。 */
402
+ origins: Record<string, string>;
403
+ }
404
+ /** POST /api/skill-hub/tag — 新建(缺省 id)或重命名(带 id)。 */
405
+ export interface TagSaveRequest {
406
+ /** 已有 tag 的 id;缺省表示新建。 */
407
+ id?: string;
408
+ /** 显示名(去空格后非空)。 */
409
+ name: string;
410
+ }
411
+ /** POST /api/skill-hub/tag */
412
+ export interface TagSaveResponse {
413
+ ok: true;
414
+ /** 变更后的全部 tag。 */
415
+ tags: SkillTag[];
416
+ }
417
+ /** POST /api/skill-hub/tag/delete */
418
+ export interface TagDeleteRequest {
419
+ id: string;
420
+ }
421
+ /** POST /api/skill-hub/tag/delete */
422
+ export interface TagDeleteResponse {
423
+ ok: true;
424
+ tags: SkillTag[];
425
+ }
426
+ /** POST /api/skill-hub/tag/members — 直接设置某 tag 的完整成员列表(幂等)。 */
427
+ export interface TagMembersRequest {
428
+ id: string;
429
+ /** 目标成员技能名;后端只保留目录中存在的名字。 */
430
+ skillNames: string[];
431
+ }
432
+ /** POST /api/skill-hub/tag/members */
433
+ export interface TagMembersResponse {
434
+ ok: true;
435
+ tags: SkillTag[];
436
+ }
437
+ /** 来源跟踪记录:一组来自同一上游仓库根目录的技能。 */
438
+ export interface SourceRecord {
439
+ /** GitHub owner/repo。 */
440
+ repo: string;
441
+ /** 显式分支/tag;缺省表示默认分支。 */
442
+ ref?: string;
443
+ /** 技能在仓库中的根目录。 */
444
+ root: RepoRoot;
445
+ /** 上次导入/同步时的上游 commit SHA;空 = 尚未核对过。 */
446
+ commitSha: string;
447
+ /** 该来源下的技能名(来源组 = 这些技能)。 */
448
+ skills: string[];
449
+ /** 仓库内路径 → 文件大小 的基线清单(差异检测用;缺失 = 无基线)。 */
450
+ manifest?: Record<string, number>;
451
+ }
452
+ /** 回收站条目:上游删除后跟进删除(移入 .trash)的技能。 */
453
+ export interface TrashEntry {
454
+ name: string;
455
+ /** .trash 下的绝对路径。 */
456
+ path: string;
457
+ /** 移入回收站的时间(epoch ms)。 */
458
+ movedAt: number;
459
+ /** 移入回收站前的原始路径(目录或文件);旧版本条目没有该字段。 */
460
+ sourcePath?: string;
461
+ /**
462
+ * 移入回收站前的来源跟踪快照(恢复时用它把技能重新挂回来源记录,
463
+ * 否则恢复后的技能会丢失来源归属、变成「个人技能」)。
464
+ */
465
+ origin?: {
466
+ repo: string;
467
+ root: RepoRoot;
468
+ ref?: string;
469
+ commitSha: string;
470
+ };
471
+ /** 移入回收站前所属的场景(tag)ID;恢复时重新加回这些场景。 */
472
+ tagIds?: string[];
473
+ }
474
+ /** GET /api/skill-hub/sources */
475
+ export interface SourcesResponse {
476
+ ok: true;
477
+ /** 全部来源记录(按 repo 排序)。 */
478
+ sources: SourceRecord[];
479
+ /** skillName → 集合名(由 sources 派生)。 */
480
+ origins: Record<string, string>;
481
+ /** 来源集合组(按 origin 聚合)。 */
482
+ collections: CollectionGroup[];
483
+ /** 回收站内容。 */
484
+ trash: TrashEntry[];
485
+ }
486
+ /** POST /api/skill-hub/sources/check — 检查指定(或全部)来源的上游更新。 */
487
+ export interface SourceCheckRequest {
488
+ /** 仅检查该 repo;缺省检查全部。 */
489
+ repo?: string;
490
+ }
491
+ /** 单个来源的检查结果。 */
492
+ export interface SourceCheckResult {
493
+ repo: string;
494
+ ref?: string;
495
+ /** 检查失败时的原因(如仓库不可达/限流)。 */
496
+ error?: string;
497
+ /** 上游 commit 是否变化。 */
498
+ changed: boolean;
499
+ /** 上游最新 commit SHA(检查成功时)。 */
500
+ commitSha?: string;
501
+ /** 上游有更新的技能名(changed 时经 tree 差异得出)。 */
502
+ updated: string[];
503
+ /** 上游已删除的技能名(changed 时经 tree 差异得出)。 */
504
+ deleted: string[];
505
+ /** 节流跳过(距上次检查不足 5 分钟,未访问网络)。 */
506
+ throttled?: boolean;
507
+ /** 该来源此前没有快照(迁移/旧记录),本次已回填 commit,尚未有差异基线。 */
508
+ unverified?: boolean;
509
+ }
510
+ /** POST /api/skill-hub/sources/check */
511
+ export interface SourceCheckResponse {
512
+ ok: true;
513
+ results: SourceCheckResult[];
514
+ }
515
+ /** POST /api/skill-hub/sources/sync — 按上游重新下载所选技能并更新快照。 */
516
+ export interface SourceSyncRequest {
517
+ repo: string;
518
+ /** 要同步的技能名;缺省同步该来源全部技能。 */
519
+ skills?: string[];
520
+ }
521
+ /** POST /api/skill-hub/sources/sync */
522
+ export interface SourceSyncResponse {
523
+ ok: true;
524
+ repo: string;
525
+ /** 同步后的上游 commit SHA。 */
526
+ commitSha: string;
527
+ /** 成功同步的技能。 */
528
+ synced: string[];
529
+ /** 失败的技能。 */
530
+ failed: Array<{
531
+ name: string;
532
+ error: string;
533
+ }>;
534
+ }
535
+ /** POST /api/skill-hub/sources/delete — 跟进上游删除,本地移入回收站。 */
536
+ export interface SourceDeleteRequest {
537
+ repo: string;
538
+ /** 要删除的技能名(移入 .trash)。 */
539
+ skills: string[];
540
+ }
541
+ /** POST /api/skill-hub/sources/delete */
542
+ export interface SourceDeleteResponse {
543
+ ok: true;
544
+ /** 已移入回收站的技能。 */
545
+ trashed: string[];
546
+ failed: Array<{
547
+ name: string;
548
+ error: string;
549
+ }>;
550
+ }
551
+ /** POST /api/skill-hub/sources/restore — 从回收站恢复一个技能。 */
552
+ export interface SourceRestoreRequest {
553
+ name: string;
554
+ }
555
+ /** POST /api/skill-hub/sources/restore */
556
+ export interface SourceRestoreResponse {
557
+ ok: true;
558
+ name: string;
559
+ path: string;
560
+ }
561
+ /** POST /api/skill-hub/sources/trash/clear — 永久删除回收站里的全部技能。 */
562
+ export interface SourceTrashClearResponse {
563
+ ok: true;
564
+ /** 已永久删除的技能名。 */
565
+ deleted: string[];
566
+ /** 未能删除的技能(保留在回收站中)。 */
567
+ failed: Array<{
568
+ name: string;
569
+ error: string;
570
+ }>;
150
571
  }
@@ -28,7 +28,12 @@ export declare class SkillHubProvider implements SkillProvider {
28
28
  private readonly watchTimer;
29
29
  private rootStamp;
30
30
  constructor(control: SkillProviderControl, home?: string);
31
- /** Invalidate the registry cache when the top-level root shape changed. */
31
+ /**
32
+ * Invalidate the registry cache when the discovered skill files changed.
33
+ * The stamp covers the per-skill discovery file's mtime + size (plus the
34
+ * entry set itself), so editing a SKILL.md body — not just adding or
35
+ * renaming directories — refreshes the GUI catalog.
36
+ */
32
37
  private checkRoots;
33
38
  list(options: {
34
39
  cwd?: string;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * GitHub repository skill discovery/import helpers.
3
+ *
4
+ * Kept dependency-free and mostly pure so the root/origin rules are easy to
5
+ * test. The two supported roots mirror the repositories we care about:
6
+ * `skills/**` (standard skill bundles) and `design-templates/**` (template
7
+ * skill bundles in nexu-io/open-design).
8
+ */
9
+ import type { RepoRoot, RepoSkillEntry } from './protocol.ts';
10
+ /** Supported skill roots in a GitHub repo. */
11
+ export declare const REPO_ROOTS: readonly RepoRoot[];
12
+ /** Parsed GitHub repository reference. */
13
+ export interface RepoRef {
14
+ owner: string;
15
+ repo: string;
16
+ /** Explicit branch/tag when supplied; undefined means use the default branch. */
17
+ ref?: string;
18
+ }
19
+ /** One file inside a GitHub repo tree. */
20
+ export interface RepoTreeItem {
21
+ path: string;
22
+ type: 'blob' | 'tree';
23
+ size?: number;
24
+ }
25
+ /** One file belonging to a skill directory. */
26
+ export interface RepoFile {
27
+ path: string;
28
+ size: number;
29
+ }
30
+ /** Fetch failure carrying a useful HTTP status for the route layer. */
31
+ export declare class RepoFetchError extends Error {
32
+ readonly status: number;
33
+ constructor(message: string, status?: number);
34
+ }
35
+ /** Override the GitHub token at runtime ('' falls back to the env var). */
36
+ export declare function setGithubToken(token: string | undefined): void;
37
+ /** Authorization header for api.github.com / raw.githubusercontent.com calls. */
38
+ export declare function githubAuthHeaders(): Record<string, string>;
39
+ /**
40
+ * Build a RepoFetchError; when the response shows an exhausted rate limit,
41
+ * report the reset time instead of a bare HTTP status.
42
+ */
43
+ export declare function fetchError(context: string, response: Response, fallbackStatus?: number): RepoFetchError;
44
+ /** `owner/repo` slug for a parsed reference. */
45
+ export declare function repoSlug(ref: RepoRef): string;
46
+ /**
47
+ * Normalize a GitHub URL or `owner/repo`/`owner/repo@ref` input.
48
+ * Only github.com URLs are accepted in v1.
49
+ */
50
+ export declare function normalizeRepoInput(input: string): RepoRef | null;
51
+ /**
52
+ * Load repo metadata and the recursive git tree at the given ref. The meta
53
+ * request always resolves the default branch (used when ref is absent); the
54
+ * tree is fetched at `ref ?? default_branch` so an explicit branch/tag scans
55
+ * the exact same content a later download would fetch.
56
+ */
57
+ export declare function loadRepoTree(repo: string, ref?: string, fetchImpl?: typeof fetch): Promise<{
58
+ ref: string;
59
+ tree: RepoTreeItem[];
60
+ }>;
61
+ /** Collect every file inside a skill directory, including SKILL.md itself. */
62
+ export declare function collectRepoSkillFiles(tree: readonly RepoTreeItem[], dir: string): RepoFile[];
63
+ /** Compute an origin collection name. Multiple roots split by root, one root keeps the repo slug. */
64
+ export declare function originForRoot(repo: string, rootsPresent: ReadonlySet<RepoRoot>, root: RepoRoot): string;
65
+ /** Discover importable skills from a repo tree. Invalid names are ignored. */
66
+ export declare function discoverRepoEntries(tree: readonly RepoTreeItem[], repo: string, existingNames?: ReadonlySet<string>): RepoSkillEntry[];
67
+ /** Run an async worker over items with a bounded concurrency. */
68
+ export declare function mapConcurrent<T, R>(items: readonly T[], limit: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]>;
69
+ /**
70
+ * Download one GitHub file as a buffer. Tries raw.githubusercontent.com
71
+ * first (no API quota); on any failure falls back to the api.github.com
72
+ * contents endpoint with the raw media type (rate-limited, but reachable
73
+ * from networks that block the raw host).
74
+ */
75
+ export declare function downloadGitHubFile(repo: string, ref: string, path: string, fetchImpl?: typeof fetch): Promise<Buffer>;
76
+ /**
77
+ * Download a full skill directory into a temporary dir, validate SKILL.md,
78
+ * then atomically rename it into place. The target root is created when missing.
79
+ * Callers pass a target root and a skill entry plus its collected files.
80
+ */
81
+ export declare function downloadRepoSkill(repo: string, ref: string, entry: RepoSkillEntry, files: readonly RepoFile[], targetRoot: string, fetchImpl?: typeof fetch): Promise<{
82
+ targetDir: string;
83
+ skillPath: string;
84
+ }>;
85
+ /**
86
+ * The latest non-prerelease release tag of a repo, or undefined when the
87
+ * repo has no releases (GitHub's /releases/latest skips prereleases/drafts).
88
+ */
89
+ export declare function getLatestReleaseTag(repo: string, fetchImpl?: typeof fetch): Promise<string | undefined>;
90
+ /** Branch names of a repo, default branch first (one GitHub API request). */
91
+ export declare function listRepoBranches(repo: string, fetchImpl?: typeof fetch): Promise<string[]>;
92
+ /**
93
+ * Fetch the latest commit of a repo (explicit ref or default branch) with
94
+ * its tree SHA. One GitHub API request; the tree SHA is used to diff the
95
+ * upstream tree on change (one extra request).
96
+ */
97
+ export declare function getLatestCommit(repo: string, ref?: string, fetchImpl?: typeof fetch): Promise<{
98
+ commitSha: string;
99
+ treeSha: string;
100
+ }>;
101
+ /** Load the recursive git tree at an explicit tree SHA (one API request). */
102
+ export declare function loadRepoTreeAt(repo: string, treeSha: string, fetchImpl?: typeof fetch): Promise<RepoTreeItem[]>;
103
+ /** Build a path→size manifest for one skill directory from a repo tree. */
104
+ export declare function skillManifest(tree: readonly RepoTreeItem[], dir: string): Record<string, number>;
105
+ /**
106
+ * The real upstream directory of one tracked skill. Source records keep only
107
+ * the short name and top-level root (e.g. root "skills", name "ask-matt"),
108
+ * but upstream repos may nest skills under category directories
109
+ * (skills/engineering/ask-matt/) or move them between categories. Resolution
110
+ * order: exact match under the top-level root in the upstream tree, then the
111
+ * manifest's recorded blob path, then any same-name skill in the tree, then
112
+ * the flat root/name fallback. Passing the tree paths makes the lookup
113
+ * resilient to incomplete manifests and upstream moves.
114
+ */
115
+ export declare function skillDirOf(source: {
116
+ root: RepoRoot;
117
+ manifest?: Record<string, number>;
118
+ }, name: string, treePaths?: readonly string[]): string;
119
+ /**
120
+ * Diff one source record against an upstream tree at treeSha: which tracked
121
+ * skills disappeared (no SKILL.md blob) and which changed (manifest baseline
122
+ * differs, or no baseline exists — treated as changed). Pure over the tree.
123
+ */
124
+ export declare function diffRemoteSkills(tree: readonly RepoTreeItem[], source: {
125
+ root: RepoRoot;
126
+ skills: readonly string[];
127
+ manifest?: Record<string, number>;
128
+ }): {
129
+ updated: string[];
130
+ deleted: string[];
131
+ };
132
+ /** Minimal RepoSkillEntry for a tracked skill name (sync re-downloads by name). */
133
+ export declare function repoSkillEntry(name: string, root: RepoRoot, repo: string): RepoSkillEntry;
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * The /api/skill-hub route family: full catalog (enabled skills from the
3
3
  * official registry + hub-disabled skills + discovery diagnostics), skill
4
- * detail, enable/disable toggle, and new-skill scaffold. Every route
5
- * carries a loopback-only trust fence — these endpoints rename files under
6
- * the user's skill roots, so LAN-exposed dsh web deployments must not
7
- * serve them.
4
+ * detail, enable/disable toggle, new-skill scaffold, user groups (tags +
5
+ * origin collections), and upstream source tracking (check/sync/follow
6
+ * upstream deletion into a restorable trash). Every route carries a
7
+ * loopback-only trust fence — these endpoints rename files under the user's
8
+ * skill roots, so LAN-exposed dsh web deployments must not serve them.
9
+ *
10
+ * Every handler is declared through the local `route()` wrapper, which owns
11
+ * the shared request fences (loopback trust, HTTP method, master switch,
12
+ * JSON body) exactly once, so a new route cannot forget them.
8
13
  */
9
14
  import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
10
15
  import type { SkillDefinition, SkillSummary } from '@deepseek-ai/dsh-skill';