@nanmicoder/dsh-skills-hub 0.0.1 → 0.0.3

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 (67) hide show
  1. package/CONTRIBUTING.md +22 -0
  2. package/README.md +32 -82
  3. package/README_EN.md +32 -82
  4. package/README_ZH.md +2 -0
  5. package/assets/readme/marketplace.png +0 -0
  6. package/compatibility.json +1 -1
  7. package/docs/CATALOG.md +39 -0
  8. package/docs/COMPATIBILITY.md +46 -0
  9. package/docs/CONFIGURATION.md +38 -0
  10. package/docs/CONTRACT.md +35 -12
  11. package/docs/RELEASE.md +5 -5
  12. package/lib/catalog/catalog.js +89 -0
  13. package/lib/catalog/skills.json +8673 -0
  14. package/lib/client.js +3349 -1728
  15. package/lib/client.js.map +1 -1
  16. package/lib/config.js +2 -0
  17. package/lib/index.js +16 -6
  18. package/lib/market/cache.js +104 -30
  19. package/lib/market/clawhub-provider.js +139 -22
  20. package/lib/market/disk-cache.js +113 -0
  21. package/lib/market/install-service.js +24 -11
  22. package/lib/market/market-service.js +161 -32
  23. package/lib/market/provider-fetch.js +9 -2
  24. package/lib/market/skillhub-provider.js +6 -2
  25. package/lib/market/types.js +2 -1
  26. package/lib/skills/installed.js +5 -0
  27. package/lib/skills/management.js +182 -4
  28. package/lib/skills/root.js +1 -1
  29. package/lib/types/catalog/catalog.d.ts +73 -0
  30. package/lib/types/client/api.d.ts +45 -8
  31. package/lib/types/client/components/CategoryBar.d.ts +13 -0
  32. package/lib/types/client/components/FilterBar.d.ts +4 -0
  33. package/lib/types/client/components/InstallConfirmDialog.d.ts +20 -11
  34. package/lib/types/client/components/InstalledSkillDetail.d.ts +24 -0
  35. package/lib/types/client/components/InstalledSkills.d.ts +18 -1
  36. package/lib/types/client/components/MarkdownView.d.ts +7 -1
  37. package/lib/types/client/components/MarketSkillDetail.d.ts +34 -0
  38. package/lib/types/client/components/SkillAvatar.d.ts +1 -1
  39. package/lib/types/client/components/SkillDetailShell.d.ts +86 -0
  40. package/lib/types/client/components/SkillFiles.d.ts +32 -0
  41. package/lib/types/client/components/SourceStatusBar.d.ts +4 -0
  42. package/lib/types/client/icons.d.ts +4 -0
  43. package/lib/types/client/locales.d.ts +94 -0
  44. package/lib/types/client/relative-time.d.ts +13 -0
  45. package/lib/types/client/skill-insights.d.ts +30 -0
  46. package/lib/types/client/skill-markdown.d.ts +39 -0
  47. package/lib/types/client/state.d.ts +36 -7
  48. package/lib/types/config.d.ts +2 -0
  49. package/lib/types/market/cache.d.ts +62 -14
  50. package/lib/types/market/clawhub-provider.d.ts +15 -0
  51. package/lib/types/market/disk-cache.d.ts +16 -0
  52. package/lib/types/market/install-service.d.ts +1 -0
  53. package/lib/types/market/market-service.d.ts +40 -6
  54. package/lib/types/market/provider-fetch.d.ts +2 -0
  55. package/lib/types/market/types.d.ts +29 -0
  56. package/lib/types/skills/installed.d.ts +4 -0
  57. package/lib/types/skills/management.d.ts +63 -2
  58. package/lib/types/skills/root.d.ts +8 -0
  59. package/lib/types/web-routes.d.ts +3 -1
  60. package/lib/web-routes.js +85 -8
  61. package/package.json +1 -1
  62. package/release-notes/v0.0.1.md +1 -1
  63. package/release-notes/v0.0.2.md +12 -0
  64. package/release-notes/v0.0.3.md +12 -0
  65. package/lib/types/client/components/InstallStateBadge.d.ts +0 -11
  66. package/lib/types/client/components/SecurityBadge.d.ts +0 -16
  67. package/lib/types/client/components/SkillDetailView.d.ts +0 -21
@@ -16,6 +16,8 @@ export type SecurityReport = {
16
16
  status: string;
17
17
  statusText: string;
18
18
  reportUrl?: string;
19
+ /** The scanner's own explanation of its verdict, when it gives one */
20
+ summary?: string;
19
21
  };
20
22
  export type NormalizedSkill = {
21
23
  /** `${source}:${slug}` — globally unique */
@@ -35,7 +37,10 @@ export type NormalizedSkill = {
35
37
  stars?: number;
36
38
  };
37
39
  tags: string[];
40
+ /** Curated-catalogue category key, e.g. `dev` (absent for live upstream results) */
38
41
  category?: string;
42
+ /** Editor's pick in the curated catalogue */
43
+ featured?: boolean;
39
44
  version?: string;
40
45
  updatedAt?: number;
41
46
  iconUrl?: string;
@@ -47,6 +52,7 @@ export type NormalizedSkill = {
47
52
  upstream?: {
48
53
  source: MarketSource;
49
54
  slug: string;
55
+ owner?: string;
50
56
  };
51
57
  /** After dedupe: ids of merged duplicate entries from other sources */
52
58
  mirrors?: string[];
@@ -75,6 +81,14 @@ export type NormalizedSkillDetail = NormalizedSkill & {
75
81
  license?: string;
76
82
  files: MarketFileMeta[];
77
83
  totalSize: number;
84
+ /** Release note of the latest version, when upstream has a meaningful one */
85
+ changelog?: {
86
+ version?: string;
87
+ text: string;
88
+ publishedAt?: number;
89
+ };
90
+ /** The skill's page on its registry (http(s) only) */
91
+ pageUrl?: string;
78
92
  };
79
93
  export type MarketFileContent = {
80
94
  path: string;
@@ -89,9 +103,24 @@ export type SourceStatusInfo = {
89
103
  fromCache?: boolean;
90
104
  error?: string;
91
105
  };
106
+ /** One curated-catalogue category. */
107
+ export type MarketCategory = {
108
+ key: string;
109
+ name: string;
110
+ nameEn?: string;
111
+ sortOrder: number;
112
+ /** Skills the catalogue holds in this category */
113
+ count?: number;
114
+ };
115
+ export type MarketCategoriesResult = {
116
+ items: MarketCategory[];
117
+ status: SourceStatusInfo;
118
+ };
92
119
  export type MarketListResult = {
93
120
  items: NormalizedSkill[];
94
121
  nextCursor: string | null;
122
+ /** Matching skills in total, when known (the curated catalogue always knows) */
123
+ total?: number;
95
124
  sources: Record<MarketSource, SourceStatusInfo>;
96
125
  };
97
126
  export type ProviderListPage = {
@@ -30,6 +30,8 @@ export interface InstalledMetaFile {
30
30
  id: string;
31
31
  source: MarketSource;
32
32
  slug: string;
33
+ /** Registry author (ClawHub slugs are shared across authors); absent in older sidecars. */
34
+ owner?: string;
33
35
  version?: string;
34
36
  installedAt: string;
35
37
  files: Array<{
@@ -47,6 +49,8 @@ export interface InstalledSkillRecord {
47
49
  id: string;
48
50
  source: MarketSource | 'local';
49
51
  slug: string;
52
+ /** Registry author recorded at install time, when known */
53
+ owner?: string;
50
54
  name: string;
51
55
  dirName: string;
52
56
  /**
@@ -1,7 +1,68 @@
1
+ /**
2
+ * Local skill management: one discovered entry at a time.
3
+ *
4
+ * Every function here starts from a key, never from a client-supplied path: the
5
+ * roots are re-scanned on each call, and only an entry that scan just produced
6
+ * can be read or removed. A path that arrives from the browser is therefore
7
+ * always interpreted *inside* an entry this process has already vouched for.
8
+ *
9
+ * The detail read is what the panel's local skill page renders: the SKILL.md
10
+ * document, its raw frontmatter block and a bounded inventory of the skill's own
11
+ * files. Nothing here consults a market provider — an installed skill stays
12
+ * readable with the network down.
13
+ */
1
14
  import { type InstalledSkillRecord } from './installed.ts';
2
- export declare function readInstalledSkill(roots: string[], key: string): Promise<{
15
+ /** One file of an installed skill, relative to the entry. */
16
+ export interface InstalledFileEntry {
17
+ path: string;
18
+ size: number;
19
+ language: string;
20
+ }
21
+ /** `GET /installed/detail` body. */
22
+ export interface InstalledSkillDetail {
3
23
  item: InstalledSkillRecord;
24
+ /** Full SKILL.md text, frontmatter included: the browser strips it. */
4
25
  markdown: string;
5
- }>;
26
+ /** Verbatim YAML header without its fences, or `null` when the file has none. */
27
+ frontmatter: string | null;
28
+ files: InstalledFileEntry[];
29
+ }
30
+ /** `GET /installed/file` body; the shape mirrors the market file preview. */
31
+ export interface InstalledFileContent {
32
+ path: string;
33
+ content: string;
34
+ language: string;
35
+ size: number;
36
+ truncated: boolean;
37
+ }
38
+ /**
39
+ * Raw frontmatter block, kept as YAML text.
40
+ *
41
+ * Parsing it into structure would mean shipping a YAML reader for a panel that
42
+ * only needs to *show* the header, and the market detail's structured panel is
43
+ * fed by upstream JSON this plugin does not have for a hand-written skill. The
44
+ * verbatim block is also what the file actually contains, so nothing is lost.
45
+ */
46
+ export declare function extractFrontmatter(markdown: string): string | null;
47
+ /**
48
+ * Inventory one entry's own files.
49
+ *
50
+ * The walk never leaves the entry: directories are descended, symlinked entries
51
+ * are listed as nothing at all, and the provenance sidecar (a dot-file, like
52
+ * every other dot-entry) is skipped because it is plugin bookkeeping rather than
53
+ * installed payload. A flat skill is a single file and reports itself.
54
+ */
55
+ export declare function listEntryFiles(item: InstalledSkillRecord): Promise<InstalledFileEntry[]>;
56
+ /** Read the SKILL.md document plus the entry inventory the local detail page renders. */
57
+ export declare function readInstalledSkill(roots: string[], key: string): Promise<InstalledSkillDetail>;
58
+ /**
59
+ * Read one file of one entry.
60
+ *
61
+ * A flat skill exposes exactly itself — never its siblings in the same root —
62
+ * and a directory bundle exposes only what resolves back inside its own real
63
+ * path, so a symlink planted in the tree cannot turn this endpoint into an
64
+ * arbitrary-file reader.
65
+ */
66
+ export declare function readInstalledFile(roots: string[], key: string, requested: string): Promise<InstalledFileContent>;
6
67
  /** Rename the selected entry out of discovery before deletion. Symlink targets are never removed. */
7
68
  export declare function removeInstalledSkill(roots: string[], key: string, allowUninstall: boolean): Promise<InstalledSkillRecord>;
@@ -20,6 +20,14 @@
20
20
  * explicit path → `DSH_HOME` (blank treated as unset) → `~/.dsh`, plus the
21
21
  * `~`, `~/`, `~\` tilde expansion of `expandHomePath`.
22
22
  */
23
+ /**
24
+ * Resolve the single-root harness home.
25
+ *
26
+ * Precedence, highest first: an explicit path, `$DSH_HOME`, then `~/.dsh`. A
27
+ * blank `$DSH_HOME` is treated as unset so a stray empty variable can never
28
+ * resolve the harness home to the current working directory.
29
+ */
30
+ export declare function resolveDshHome(configured?: string | null): string;
23
31
  /**
24
32
  * Absolute skills directory skills are installed into, created on demand.
25
33
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Skills Hub HTTP surface.
3
3
  *
4
- * One `prefix` route (`/api/skills-hub`) owns the seven endpoints the panel
4
+ * One `prefix` route (`/api/skills-hub`) owns the endpoints the panel
5
5
  * needs; every response is JSON with `Cache-Control: no-store`, because the
6
6
  * panel polls and every payload is either live upstream data or live disk state.
7
7
  *
@@ -30,6 +30,8 @@ export interface SkillsHubRoutesDeps {
30
30
  skillsRoot: () => Promise<string>;
31
31
  allowUninstall: () => boolean;
32
32
  rescan: () => Promise<void>;
33
+ /** Initial local inventory scan, required before annotating persisted data. */
34
+ ready?: () => Promise<void>;
33
35
  }
34
36
  /**
35
37
  * Browser-trust gate, structurally equal to the frozen contract's predicate.
package/lib/web-routes.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Skills Hub HTTP surface.
3
3
  *
4
- * One `prefix` route (`/api/skills-hub`) owns the seven endpoints the panel
4
+ * One `prefix` route (`/api/skills-hub`) owns the endpoints the panel
5
5
  * needs; every response is JSON with `Cache-Control: no-store`, because the
6
6
  * panel polls and every payload is either live upstream data or live disk state.
7
7
  *
@@ -16,8 +16,10 @@
16
16
  * otherwise be reachable by a cross-site request or a DNS-rebound page.
17
17
  */
18
18
  import { installMarketSkill, MarketInstallError, uninstallMarketSkill, } from "./market/install-service.js";
19
- import { getMarketFileContent, getMarketSkillDetail, getMarketStatus, isValidMarketFilePath, listMarketSkills, } from "./market/market-service.js";
20
- import { readInstalledSkill, removeInstalledSkill } from "./skills/management.js";
19
+ import { getMarketFileContent, getMarketSkillDetail, getMarketStatus, isValidMarketFilePath, listMarketCategories, listMarketSkills, } from "./market/market-service.js";
20
+ import { getMarketStats } from "./market/cache.js";
21
+ import { CLAWHUB_OWNER_PATTERN } from "./market/clawhub-provider.js";
22
+ import { readInstalledFile, readInstalledSkill, removeInstalledSkill } from "./skills/management.js";
21
23
  import { scanInstalledSkills } from "./skills/installed.js";
22
24
  import { resolveSkillsScanRoots } from "./skills/root.js";
23
25
  import { MARKET_ERROR_CODES, MARKET_SOURCES, MarketUpstreamError, parseSkillId, } from "./market/types.js";
@@ -247,6 +249,30 @@ function enumQuery(url, name, allowed, fallback) {
247
249
  return raw;
248
250
  throw new RouteError(400, 'BAD_REQUEST', `Invalid ${name} filter: ${raw}`);
249
251
  }
252
+ /** Shape of a catalogue category key. */
253
+ const CATEGORY_KEY_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
254
+ /** `category` query parameter: a catalogue category key, shape-checked only. */
255
+ function parseCategory(url) {
256
+ const raw = url.searchParams.get('category');
257
+ if (raw === null || raw === '' || raw === 'all')
258
+ return undefined;
259
+ if (!CATEGORY_KEY_PATTERN.test(raw))
260
+ throw new RouteError(400, 'BAD_REQUEST', `Invalid category: ${raw}`);
261
+ return raw;
262
+ }
263
+ /**
264
+ * Optional registry owner. ClawHub slugs are shared across authors, so the
265
+ * panel names the author of the card it opened; a malformed owner is refused
266
+ * rather than silently dropped (that would fall back to guessing).
267
+ */
268
+ function parseOwner(raw) {
269
+ if (raw === undefined || raw === null || raw === '')
270
+ return undefined;
271
+ if (typeof raw !== 'string' || !CLAWHUB_OWNER_PATTERN.test(raw)) {
272
+ throw new RouteError(400, 'BAD_REQUEST', `Invalid owner: ${String(raw)}`);
273
+ }
274
+ return raw;
275
+ }
250
276
  /** `limit` query parameter, clamped into the documented range. */
251
277
  function parseLimit(url) {
252
278
  const raw = url.searchParams.get('limit');
@@ -265,30 +291,59 @@ function parseBodyId(body) {
265
291
  throw new RouteError(400, 'BAD_REQUEST', `Invalid skill id: ${id === '' ? '(missing)' : id}`);
266
292
  return parsed;
267
293
  }
294
+ /**
295
+ * `refresh` query parameter.
296
+ *
297
+ * A reader who presses refresh means "do not answer me from your cache". Before
298
+ * this existed the request looked exactly like the automatic one, so the panel
299
+ * re-rendered the same cached page and presented it as fresh. Only the explicit
300
+ * spellings are accepted; anything else is a bad request rather than a silently
301
+ * ignored intent.
302
+ */
303
+ function parseRefresh(url) {
304
+ const raw = url.searchParams.get('refresh');
305
+ if (raw === null || raw === '' || raw === '0' || raw === 'false')
306
+ return false;
307
+ if (raw === '1' || raw === 'true')
308
+ return true;
309
+ throw new RouteError(400, 'BAD_REQUEST', `Invalid refresh flag: ${raw}`);
310
+ }
268
311
  async function handleStatus(res) {
269
312
  sendJson(res, 200, { sources: getMarketStatus() });
270
313
  }
314
+ /** Cache and upstream counters: the evidence behind every traffic claim. */
315
+ async function handleStats(res) {
316
+ sendJson(res, 200, { stats: getMarketStats() });
317
+ }
271
318
  async function handleList(url, res) {
272
319
  const params = {
273
320
  q: optionalQuery(url, 'q', MAX_QUERY_LENGTH),
321
+ scope: enumQuery(url, 'scope', ['catalog', 'market'], 'catalog'),
322
+ category: parseCategory(url),
274
323
  source: enumQuery(url, 'source', ['all', ...MARKET_SOURCES], 'all'),
275
324
  security: enumQuery(url, 'security', ['all', ...SECURITY_FILTERS], 'all'),
276
325
  installed: enumQuery(url, 'installed', INSTALLED_FILTERS, 'all'),
277
326
  cursor: optionalQuery(url, 'cursor', MAX_CURSOR_LENGTH),
278
327
  limit: parseLimit(url),
328
+ refresh: parseRefresh(url),
279
329
  };
280
330
  const result = await listMarketSkills(params);
281
331
  sendJson(res, 200, result);
282
332
  }
283
- async function handleDetail(source, slug, res) {
284
- sendJson(res, 200, await getMarketSkillDetail(source, slug));
333
+ async function handleCategories(res) {
334
+ sendJson(res, 200, listMarketCategories());
335
+ }
336
+ async function handleDetail(source, slug, url, res) {
337
+ const owner = parseOwner(url.searchParams.get('owner'));
338
+ sendJson(res, 200, await getMarketSkillDetail(source, slug, { force: parseRefresh(url), owner }));
285
339
  }
286
340
  async function handleFile(source, slug, url, res) {
287
341
  const filePath = url.searchParams.get('path') ?? '';
288
342
  if (!isValidMarketFilePath(filePath)) {
289
343
  throw new RouteError(400, 'BAD_REQUEST', `Invalid file path: ${filePath}`);
290
344
  }
291
- sendJson(res, 200, { file: await getMarketFileContent(source, slug, filePath) });
345
+ const owner = parseOwner(url.searchParams.get('owner'));
346
+ sendJson(res, 200, { file: await getMarketFileContent(source, slug, filePath, owner) });
292
347
  }
293
348
  /**
294
349
  * List installed skills from disk.
@@ -303,10 +358,12 @@ async function handleInstalled(deps, res) {
303
358
  sendJson(res, 200, { items: (await scanInstalledSkills(roots)).map(item => ({ ...item, removable: deps.allowUninstall() })) });
304
359
  }
305
360
  async function handleInstall(req, deps, res) {
306
- const { source, slug } = parseBodyId(await readJsonBody(req));
361
+ const body = await readJsonBody(req);
362
+ const { source, slug } = parseBodyId(body);
307
363
  const result = await installMarketSkill(source, slug, {
308
364
  skillsRoot: await deps.skillsRoot(),
309
365
  allowUninstall: deps.allowUninstall(),
366
+ owner: parseOwner(body['owner']),
310
367
  });
311
368
  // Refresh before answering, so the next /skills or /installed call is correct.
312
369
  await deps.rescan().catch(() => undefined);
@@ -335,6 +392,7 @@ async function handle(req, res, gate, deps) {
335
392
  return;
336
393
  }
337
394
  }
395
+ await deps.ready?.();
338
396
  const url = new URL(req.url ?? '/', 'http://localhost');
339
397
  const segments = parseSegments(url.pathname);
340
398
  const method = req.method ?? 'GET';
@@ -344,12 +402,31 @@ async function handle(req, res, gate, deps) {
344
402
  await handleStatus(res);
345
403
  return;
346
404
  }
405
+ if (head === 'categories' && segments.length === 1) {
406
+ requireMethod(method, 'GET');
407
+ await handleCategories(res);
408
+ return;
409
+ }
410
+ if (head === 'stats' && segments.length === 1) {
411
+ requireMethod(method, 'GET');
412
+ await handleStats(res);
413
+ return;
414
+ }
347
415
  if (head === 'installed' && segments.length === 2 && second === 'detail') {
348
416
  requireMethod(method, 'GET');
349
417
  const roots = await resolveSkillsScanRoots(await deps.skillsRoot());
350
418
  sendJson(res, 200, await readInstalledSkill(roots, url.searchParams.get('key') ?? ''));
351
419
  return;
352
420
  }
421
+ if (head === 'installed' && segments.length === 2 && second === 'file') {
422
+ requireMethod(method, 'GET');
423
+ const roots = await resolveSkillsScanRoots(await deps.skillsRoot());
424
+ // Both parameters are validated inside: a key must resolve to a freshly
425
+ // discovered entry, and the path must stay inside that entry.
426
+ const file = await readInstalledFile(roots, url.searchParams.get('key') ?? '', url.searchParams.get('path') ?? '');
427
+ sendJson(res, 200, { file });
428
+ return;
429
+ }
353
430
  if (head === 'installed' && segments.length === 2 && second === 'uninstall') {
354
431
  requireMethod(method, 'POST');
355
432
  const body = await readJsonBody(req);
@@ -387,7 +464,7 @@ async function handle(req, res, gate, deps) {
387
464
  const slug = parseSlug(third);
388
465
  requireMethod(method, 'GET');
389
466
  if (segments.length === 3) {
390
- await handleDetail(source, slug, res);
467
+ await handleDetail(source, slug, url, res);
391
468
  }
392
469
  else {
393
470
  await handleFile(source, slug, url, res);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanmicoder/dsh-skills-hub",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Skills Hub for DeepSeek Harness: browse, preview, install and remove third-party skills from ClawHub and SkillHub inside the DSH Web GUI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -8,4 +8,4 @@ Initial release of `@nanmicoder/dsh-skills-hub` for DeepSeek Harness's 0.2.0 des
8
8
  - Atomic installation, existing-directory conflict protection and restricted filesystem removal.
9
9
  - Scoped npm package, bilingual documentation, pinned compatibility metadata and reproducible build/package/release checks.
10
10
 
11
- Install: `dsh plugin --profile desktop add @nanmicoder/dsh-skills-hub@0.0.1`, then fully restart the desktop app. Installing the plugin does not migrate or delete existing skills.
11
+ Install in **DeepSeek Harness desktop → Plugins → Add plugin**. Paste `https://github.com/NanmiCoder/dsh-skills-hub` (recommended) or `@nanmicoder/dsh-skills-hub`, click **Install**, then **Enable now**. Open **Skills Hub** from the sidebar. Installing the plugin does not migrate or delete existing skills.
@@ -0,0 +1,12 @@
1
+ # Skills Hub 0.0.2
2
+
3
+ `@nanmicoder/dsh-skills-hub` for DeepSeek Harness **0.2.0-rc.2**.
4
+
5
+ - Persist browsed catalogue pages, searches, skill details and file previews across desktop restarts. The default cache lifetime is 60 minutes, configurable through `cacheTtlMinutes`.
6
+ - Preserve snapshot timestamps and each page's provenance. Reopening a detail checks the Host's cache lifetime and current local installation state.
7
+ - Keep installation manifests fresh: actual installs bypass cached details and refuse stale fallback when upstream fails.
8
+ - Bound and validate disk snapshots, recover abandoned temporary writes, and keep provider configurations isolated during reloads.
9
+ - Show installed skills in a full detail page, with rendered Markdown previews, structured frontmatter and a source view for file contents.
10
+ - Prefetch the next catalogue page near the end of the current view, and show snapshot age with an explicit refresh action.
11
+
12
+ Install in **DeepSeek Harness desktop → Plugins → Add plugin** using `@nanmicoder/dsh-skills-hub@0.0.2`, then enable the plugin. Existing skills remain in place. Advanced cache settings are documented in [CONFIGURATION.md](../docs/CONFIGURATION.md).
@@ -0,0 +1,12 @@
1
+ # Skills Hub 0.0.3
2
+
3
+ `@nanmicoder/dsh-skills-hub` for DeepSeek Harness **0.2.0-rc.2**.
4
+
5
+ - **Curated catalogue.** The home page now opens on 398 hand-picked skills from ClawHub and SkillHub, grouped into 13 categories (agents, research, development, office, productivity, content, media, data, marketing, finance, professional, life, security) with editor's picks first. The list ships with the plugin, so it renders instantly and offline; details, files and installs are still read live from upstream and cached. Searching switches to live results from both marketplaces.
6
+ - **Redesigned detail page.** Header with downloads, installs, stars and file count; Overview, Files, Security and Changelog tabs; a rule-based "what this skill will do" panel (shell commands, hooks, secrets, network hosts, workspace writes, required binaries) and "when it triggers" list, both derived from the skill's own files; per-scanner security reports from upstream.
7
+ - **Install confirmation.** The dialog lists what the skill will be able to do, where it will be installed, and requires an explicit acknowledgement for skills that are unscanned or flagged by upstream scanners.
8
+ - **Same-name skills resolved by author.** ClawHub slugs can be shared by several authors. Details, file previews and installs are now pinned to the author shown on the card, live search shows one entry per slug, and a mirror is only folded into an original by the same author. Installed skills record their author.
9
+ - **Unified look.** Marketplace home, detail pages and Installed share one visual system, and fit the docked 720px panel in light and dark themes.
10
+ - Copy now states plainly that Skills Hub is a community plugin, not a DeepSeek product, and that the curated list is a recommendation rather than a security guarantee.
11
+
12
+ Install in **DeepSeek Harness desktop → Plugins → Add plugin** using `@nanmicoder/dsh-skills-hub@0.0.3`, then enable the plugin. Existing skills and cache settings remain in place. Catalogue policy is documented in [CATALOG.md](../docs/CATALOG.md).
@@ -1,11 +0,0 @@
1
- import type { InstallState } from '../../market/types.ts';
2
- /**
3
- * Local install state chip.
4
- *
5
- * The three labels come from dictionary keys the contract already fixes —
6
- * `installed`, `installed.installable` and `notInstallable` — so this badge
7
- * adds no vocabulary of its own.
8
- */
9
- export declare function InstallStateBadge(props: {
10
- state: InstallState;
11
- }): JSX.Element;
@@ -1,16 +0,0 @@
1
- import type { SecurityReport, SecurityStatus } from '../../market/types.ts';
2
- /**
3
- * Audit verdict chip.
4
- *
5
- * Shows a shield for every verdict except `flagged`, which uses the warning
6
- * triangle: a scan that found something must not look like a scan that merely
7
- * ran, and shape reads faster than color alone.
8
- *
9
- * `reports` supplies the upstream status line for the hover title, so the chip
10
- * can stay a two-word summary without hiding what the vendor actually said.
11
- */
12
- export declare function SecurityBadge(props: {
13
- status: SecurityStatus;
14
- reports?: SecurityReport[];
15
- compact?: boolean;
16
- }): JSX.Element;
@@ -1,21 +0,0 @@
1
- import type { NormalizedSkillDetail } from '../../market/types.ts';
2
- import type { MarketController, MarketState } from '../state.ts';
3
- /**
4
- * Skill detail page.
5
- *
6
- * Like the home page this component owns no data: the tab, the selected file
7
- * and the file contents all live in `MarketState`, so switching tabs or
8
- * re-opening a skill does not lose its place.
9
- *
10
- * The bundled GFM renderer handles catalogue Markdown without relying on a
11
- * host renderer delegate. Raw HTML is disabled and URLs use its safe default.
12
- */
13
- export declare function SkillDetailView(props: {
14
- detail: NormalizedSkillDetail;
15
- state: MarketState;
16
- controller: MarketController;
17
- }): JSX.Element;
18
- /** Keeps the detail layout stable while its network request is in flight. */
19
- export declare function SkillDetailSkeleton({ onBack }: {
20
- onBack: () => void;
21
- }): JSX.Element;