@nanmicoder/dsh-skills-hub 0.0.2 → 0.0.4

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 (44) hide show
  1. package/CONTRIBUTING.md +7 -3
  2. package/README.md +24 -12
  3. package/README_EN.md +24 -12
  4. package/README_ZH.md +1 -1
  5. package/assets/readme/marketplace.png +0 -0
  6. package/compatibility.json +6 -6
  7. package/docs/CATALOG.md +39 -0
  8. package/docs/COMPATIBILITY.md +31 -1
  9. package/docs/CONTRACT.md +15 -10
  10. package/docs/RELEASE.md +4 -4
  11. package/lib/catalog/catalog.js +89 -0
  12. package/lib/catalog/skills.json +8673 -0
  13. package/lib/client.js +2549 -1819
  14. package/lib/client.js.map +1 -1
  15. package/lib/market/clawhub-provider.js +139 -22
  16. package/lib/market/install-service.js +19 -8
  17. package/lib/market/market-service.js +88 -9
  18. package/lib/market/skillhub-provider.js +6 -2
  19. package/lib/skills/installed.js +5 -0
  20. package/lib/types/catalog/catalog.d.ts +73 -0
  21. package/lib/types/client/api.d.ts +12 -3
  22. package/lib/types/client/components/CategoryBar.d.ts +13 -0
  23. package/lib/types/client/components/FilterBar.d.ts +4 -0
  24. package/lib/types/client/components/InstallConfirmDialog.d.ts +14 -11
  25. package/lib/types/client/components/InstalledSkillDetail.d.ts +1 -1
  26. package/lib/types/client/components/InstalledSkills.d.ts +1 -1
  27. package/lib/types/client/components/MarketSkillDetail.d.ts +34 -0
  28. package/lib/types/client/components/SkillDetailShell.d.ts +66 -43
  29. package/lib/types/client/locales.d.ts +64 -0
  30. package/lib/types/client/skill-insights.d.ts +30 -0
  31. package/lib/types/client/state.d.ts +23 -3
  32. package/lib/types/market/clawhub-provider.d.ts +15 -0
  33. package/lib/types/market/install-service.d.ts +1 -0
  34. package/lib/types/market/market-service.d.ts +25 -3
  35. package/lib/types/market/types.d.ts +29 -0
  36. package/lib/types/skills/installed.d.ts +4 -0
  37. package/lib/types/web-routes.d.ts +4 -13
  38. package/lib/web-routes.js +48 -19
  39. package/package.json +26 -27
  40. package/release-notes/v0.0.3.md +12 -0
  41. package/release-notes/v0.0.4.md +11 -0
  42. package/lib/types/client/components/InstallStateBadge.d.ts +0 -11
  43. package/lib/types/client/components/SecurityBadge.d.ts +0 -16
  44. package/lib/types/client/components/SkillDetailView.d.ts +0 -19
@@ -8,6 +8,10 @@ import type { MarketFilters } from '../state.ts';
8
8
  * platform keyboard behaviour, a real label association, and renders inside a
9
9
  * 720px panel without a portal.
10
10
  *
11
+ * The search field is a draft: typing only edits local state, and the query is
12
+ * submitted on Enter (or when the field is emptied/cleared). Enter during IME
13
+ * composition confirms the candidate and must not submit.
14
+ *
11
15
  * The result count is announced through a visually hidden live region instead
12
16
  * of being printed twice: the visible count lives in the home header, and a
13
17
  * screen reader still hears it change when a filter narrows the list.
@@ -1,12 +1,18 @@
1
- import type { MarketSource, SecurityStatus } from '../../market/types.ts';
1
+ import type { MarketSource, NormalizedSkillDetail, SecurityStatus } from '../../market/types.ts';
2
2
  /** The subset of a skill the confirmation needs; the caller owns the source object. */
3
3
  export interface InstallConfirmSkill {
4
4
  id: string;
5
5
  name: string;
6
6
  source: MarketSource;
7
7
  version?: string;
8
+ iconUrl?: string;
8
9
  securityStatus: SecurityStatus;
9
10
  authorName: string;
11
+ /**
12
+ * The loaded detail, when the dialog was opened from the detail page: only
13
+ * then are the skill's files known, and with them what it will be able to do.
14
+ */
15
+ detail?: NormalizedSkillDetail;
10
16
  /**
11
17
  * When the catalogue data behind this dialog was read from upstream, if it
12
18
  * came from a snapshot. Installing is a trust decision, so the confirmation
@@ -15,17 +21,14 @@ export interface InstallConfirmSkill {
15
21
  snapshotAt?: number;
16
22
  }
17
23
  /**
18
- * Install confirmation.
19
- *
20
- * Mounted only while a confirmation is pending — the frozen props carry no
21
- * `skill | null` and no `open`, so the caller's conditional render *is* the
22
- * open state (the optional `open` prop exists only for a caller that prefers
23
- * to keep the element mounted).
24
+ * Install confirmation (design: install dialog of the detail page).
24
25
  *
25
- * Escape and mask-click closing come from `Modal` itself; while `busy` both
26
- * are neutralised and the buttons are disabled, because an install already in
27
- * flight cannot be recalled and a dialog that vanishes mid-request reads as a
28
- * cancel that never happened.
26
+ * Mounted only while a confirmation is pending. A skill that is not scanned
27
+ * clean (flagged or unknown) needs an explicit acknowledgement before the
28
+ * confirm button arms: installing is a trust decision about a third party, and
29
+ * the dialog says so instead of making it a reflex click. Escape and mask-click
30
+ * come from `Modal`; while `busy` both are neutralised, because an install in
31
+ * flight cannot be recalled.
29
32
  */
30
33
  export declare function InstallConfirmDialog(props: {
31
34
  skill: InstallConfirmSkill;
@@ -20,5 +20,5 @@ export declare function InstalledSkillDetail(props: {
20
20
  onBack: () => void;
21
21
  onUninstall: (item: InstalledSkillRecord) => void;
22
22
  /** Open the same skill's market page; only offered for market provenance. */
23
- onOpenMarket: (id: string) => void;
23
+ onOpenMarket: (id: string, owner?: string) => void;
24
24
  }): JSX.Element;
@@ -16,5 +16,5 @@ export declare function InstalledSkills(props: {
16
16
  /** A removal changed the local inventory; the market half refreshes too. */
17
17
  onChanged: () => void;
18
18
  /** Open the market page of a skill that came from a market. */
19
- onOpenMarket: (id: string) => void;
19
+ onOpenMarket: (id: string, owner?: string) => void;
20
20
  }): JSX.Element;
@@ -0,0 +1,34 @@
1
+ import type { NormalizedSkillDetail } from '../../market/types.ts';
2
+ import type { MarketController, MarketState } from '../state.ts';
3
+ import { type Translate } from '../locale-context.tsx';
4
+ import { type Capability } from '../skill-insights.ts';
5
+ /** Compact counts: 482069 → 482.1k. */
6
+ export declare function formatCount(value: number): string;
7
+ /** Upstream timestamps are epoch millis; render as an ISO date (stable across locales). */
8
+ export declare function formatDate(timestamp: number | undefined): string;
9
+ /** Only absolute http(s) links from upstream may become an `href`. */
10
+ export declare function safeUrl(url: string | undefined): string | undefined;
11
+ /** One capability, phrased with its evidence. */
12
+ export declare function capabilityText(t: Translate, capability: Capability): {
13
+ title: string;
14
+ detail: string;
15
+ };
16
+ /** Capabilities and triggers, derived once per detail. */
17
+ export declare function useSkillInsights(detail: NormalizedSkillDetail): {
18
+ capabilities: Capability[];
19
+ triggers: string[];
20
+ };
21
+ /**
22
+ * Market skill detail page.
23
+ *
24
+ * Owns no data: tab, selected file and file contents live in `MarketState`.
25
+ * Upstream facts (stats, version, scans, files) come from the live detail; the
26
+ * catalogue adds category and the Chinese summary. "What this skill does" and
27
+ * "when it triggers" are read off its own SKILL.md and file list by explicit
28
+ * rules (`skill-insights.ts`) and are hidden when no rule fires.
29
+ */
30
+ export declare function MarketSkillDetail(props: {
31
+ detail: NormalizedSkillDetail;
32
+ state: MarketState;
33
+ controller: MarketController;
34
+ }): JSX.Element;
@@ -1,63 +1,86 @@
1
1
  /**
2
- * The detail surface shared by the market catalogue and the installed inventory.
2
+ * The detail surface shared by the market catalogue and the installed inventory
3
+ * (design: "技能市场重构 – 详情页").
3
4
  *
4
5
  * Both pages answer the same question — "what is inside this skill?" — and the
5
6
  * only real difference is where the facts come from: an upstream provider or the
6
- * filesystem. Everything that is *reading* rather than *fetching* therefore
7
- * lives here: the back affordance, the identity header, the two tabs, the
8
- * document column, the facts rail and the focus move that keeps a keyboard user
9
- * from being left at the top of the document when the list is replaced.
10
- *
11
- * The two pages are not identical, so the shell takes slots rather than a data
12
- * model: `badges`, `notice`, `overview`, `files` and `rail` are composed by the
13
- * caller, while each caller owns its own loading state and its own choice of tab
14
- * storage (the market page keeps it in `MarketState`, the local page in its own
15
- * component state).
7
+ * filesystem. The shell therefore owns everything that is *layout*: breadcrumb,
8
+ * identity header, stats strip, underlined tabs, the grey canvas, and the
9
+ * document/side-rail columns. Callers compose the header pieces and the active
10
+ * tab's content, and keep their own tab state (the market page in
11
+ * `MarketState`, the local page in component state).
16
12
  */
17
13
  import { type ReactNode } from 'react';
18
- /** Tab values every detail page offers, in render order. */
19
- export type SkillDetailTab = 'overview' | 'files';
20
- /**
21
- * The shell's stylesheet, re-exported for its callers.
22
- *
23
- * The pages built on this shell also render shell-shaped fragments (a report
24
- * row, a path line, a facts list) and reaching them through an import is more
25
- * honest than re-declaring near-copies of the same rules in a second module.
26
- */
14
+ import type { SecurityStatus } from '../../market/types.ts';
15
+ /** The shell's stylesheet, for callers composing shell-shaped fragments. */
27
16
  export declare const detailStyles: Record<string, string>;
28
- /** One label/value row of the facts rail. */
29
- export declare function Fact(props: {
17
+ export interface DetailTab<K extends string> {
18
+ key: K;
30
19
  label: string;
31
- children: ReactNode;
32
- }): JSX.Element;
33
- export declare function SkillDetailShell(props: {
34
- /** Identity of the rendered subject; changing it moves focus back to the heading. */
20
+ /** Small trailing marker: a count, or a warning dot. */
21
+ badge?: ReactNode;
22
+ }
23
+ export declare function SkillDetailShell<K extends string>(props: {
24
+ /** Identity of the rendered subject; changing it moves focus to the heading. */
35
25
  focusKey: string;
36
26
  onBack: () => void;
27
+ /** Breadcrumb root: the list this page was opened from. */
37
28
  backLabel: string;
38
29
  avatar: ReactNode;
39
- /** Small line above the name: source, version, provenance. */
40
- eyebrow: ReactNode;
41
30
  name: string;
42
- badges?: ReactNode;
31
+ version?: string;
32
+ /** One line of "by author · source · licence · updated" facts. */
33
+ meta?: ReactNode;
43
34
  summary?: string;
44
- /** Full-width band under the header (warnings, security reports). */
35
+ chips?: ReactNode;
36
+ actions?: ReactNode;
37
+ stats?: Array<{
38
+ label: string;
39
+ value: string;
40
+ }>;
41
+ tabs: Array<DetailTab<K>>;
42
+ activeTab: K;
43
+ onTabChange: (tab: K) => void;
44
+ /** Notes above the tab content (snapshot age, location, warnings). */
45
45
  notice?: ReactNode;
46
- overviewLabel: string;
47
- filesLabel: string;
48
- activeTab: SkillDetailTab;
49
- onTabChange: (tab: SkillDetailTab) => void;
50
- overview: ReactNode;
51
- files: ReactNode;
52
- rail: ReactNode;
46
+ children: ReactNode;
47
+ }): JSX.Element;
48
+ /** Document column plus side rail; the rail moves below on a narrow panel. */
49
+ export declare function DetailColumns(props: {
50
+ main: ReactNode;
51
+ side: ReactNode;
52
+ }): JSX.Element;
53
+ export declare function SideCard(props: {
54
+ title: string;
55
+ extra?: ReactNode;
56
+ children: ReactNode;
57
+ }): JSX.Element;
58
+ export declare function InfoRow(props: {
59
+ label: string;
60
+ value: ReactNode;
61
+ mono?: boolean;
62
+ }): JSX.Element;
63
+ /** Count in a tab label ("文件 15"). */
64
+ export declare function TabCount(props: {
65
+ value: number;
66
+ }): JSX.Element;
67
+ type ChipTone = 'plain' | 'ok' | 'warn' | 'blue';
68
+ export declare function Chip(props: {
69
+ tone?: ChipTone;
70
+ title?: string;
71
+ children: ReactNode;
72
+ }): JSX.Element;
73
+ /** Upstream scan verdict, shared by the cards, the detail header and the install dialog. */
74
+ export declare function SecurityChip(props: {
75
+ status: SecurityStatus;
76
+ short?: boolean;
53
77
  }): JSX.Element;
54
78
  /**
55
- * Keeps the detail layout stable while its request is in flight.
56
- *
57
- * It renders the same boxes as the real page rather than a spinner, because the
58
- * click that opens a detail is the moment the panel changes shape; a centered
59
- * "loading" line would make that change happen twice.
79
+ * Keeps the detail layout stable while its request is in flight: the same
80
+ * boxes as the real page rather than a spinner, so the page changes shape once.
60
81
  */
61
- export declare function SkillDetailSkeleton({ onBack }: {
82
+ export declare function SkillDetailSkeleton({ onBack, backLabel }: {
62
83
  onBack: () => void;
84
+ backLabel?: string;
63
85
  }): JSX.Element;
86
+ export {};
@@ -74,6 +74,70 @@ declare const zhDictionary: {
74
74
  'installed.all': string;
75
75
  'installed.installed': string;
76
76
  'installed.installable': string;
77
+ 'category.lang': string;
78
+ 'category.label': string;
79
+ 'category.all': string;
80
+ categoryTag: string;
81
+ breadcrumb: string;
82
+ updatedOn: string;
83
+ needsApiKey: string;
84
+ sourcePage: string;
85
+ securityReport: string;
86
+ changelog: string;
87
+ whenToUse: string;
88
+ info: string;
89
+ latestUpdate: string;
90
+ fullChangelog: string;
91
+ olderVersions: string;
92
+ openChangelogFile: string;
93
+ noChangelog: string;
94
+ scanReports: string;
95
+ noReports: string;
96
+ scanDisclaimer: string;
97
+ readingTime: string;
98
+ expand: string;
99
+ collapse: string;
100
+ capTitle: string;
101
+ viewFullReport: string;
102
+ capNote: string;
103
+ 'cap.shell': string;
104
+ 'cap.shell.detail': string;
105
+ 'cap.hooks': string;
106
+ 'cap.hooks.detail': string;
107
+ 'cap.secrets': string;
108
+ 'cap.secrets.detail': string;
109
+ 'cap.network': string;
110
+ 'cap.network.detail': string;
111
+ 'cap.writes': string;
112
+ 'cap.writes.detail': string;
113
+ 'cap.binaries': string;
114
+ 'cap.binaries.detail': string;
115
+ 'scan.verified': string;
116
+ 'scan.benign': string;
117
+ 'scan.unknown': string;
118
+ 'scan.flagged': string;
119
+ installTitle: string;
120
+ confirmInstall: string;
121
+ willGet: string;
122
+ 'level.high': string;
123
+ 'level.medium': string;
124
+ 'level.low': string;
125
+ installWarnFlagged: string;
126
+ installWarnUnknown: string;
127
+ ack: string;
128
+ installedOn: string;
129
+ 'scanShort.verified': string;
130
+ 'scanShort.benign': string;
131
+ 'scanShort.unknown': string;
132
+ 'scanShort.flagged': string;
133
+ catalogSummary: string;
134
+ catalogUpdated: string;
135
+ liveResults: string;
136
+ featured: string;
137
+ 'scope.catalogHint': string;
138
+ 'scope.marketHint': string;
139
+ 'scope.searchMarket': string;
140
+ 'scope.backToCatalog': string;
77
141
  'filter.source': string;
78
142
  'filter.security': string;
79
143
  'filter.installed': string;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * What a skill will do, read off its own files — before anyone installs it.
3
+ *
4
+ * Everything here is rule-based and explainable: each finding points at the
5
+ * file, command, variable or host it came from, and nothing is inferred that
6
+ * the SKILL.md and file list do not show. When no rule fires, the panel is not
7
+ * rendered at all; an empty "this skill does nothing risky" claim would be a
8
+ * verdict this module cannot make.
9
+ */
10
+ import type { MarketFileMeta } from '../market/types.ts';
11
+ export type CapabilityKind = 'shell' | 'hooks' | 'network' | 'secrets' | 'binaries' | 'writes';
12
+ export type CapabilityLevel = 'high' | 'medium' | 'low';
13
+ export interface Capability {
14
+ kind: CapabilityKind;
15
+ level: CapabilityLevel;
16
+ /** The evidence, verbatim: paths, commands, variable names or hosts. */
17
+ evidence: string[];
18
+ }
19
+ export declare function detectCapabilities(input: {
20
+ markdown: string;
21
+ frontmatter?: Record<string, unknown>;
22
+ files: readonly Pick<MarketFileMeta, 'path'>[];
23
+ }): Capability[];
24
+ /**
25
+ * The "when to use" list a SKILL.md description usually carries:
26
+ * `Use when (1) …, (2) …` / `Use when: …; …` / `当……时使用`.
27
+ */
28
+ export declare function extractTriggers(description: string | undefined): string[];
29
+ /** Rough reading time: ~400 CJK characters or ~220 Latin words per minute. */
30
+ export declare function readingMinutes(markdown: string): number;
@@ -23,7 +23,7 @@
23
23
  * requests on purpose, and `isAbortError` keeps those rejections out of
24
24
  * `state.error` / `state.notice`.
25
25
  */
26
- import type { MarketFileContent, MarketSource, NormalizedSkill, NormalizedSkillDetail, SecurityStatus, SourceStatusInfo } from '../market/types.ts';
26
+ import type { MarketCategory, MarketFileContent, MarketSource, NormalizedSkill, NormalizedSkillDetail, SecurityStatus, SourceStatusInfo } from '../market/types.ts';
27
27
  /**
28
28
  * Notice values that are dictionary *codes* rather than literal text.
29
29
  *
@@ -36,9 +36,19 @@ export declare const MARKET_NOTICE_CODES: readonly ["installDone", "uninstallDon
36
36
  export type MarketNoticeCode = (typeof MARKET_NOTICE_CODES)[number];
37
37
  /** Whether a `state.notice` value is a dictionary code (as opposed to verbatim text). */
38
38
  export declare function isMarketNoticeCode(notice: string): notice is MarketNoticeCode;
39
+ /** Tabs of the market detail page. */
40
+ export type MarketDetailTab = 'overview' | 'files' | 'security' | 'changelog';
39
41
  /** Active catalogue filters. */
40
42
  export interface MarketFilters {
41
43
  q: string;
44
+ /**
45
+ * `catalog`: the curated list shipped with the plugin (default).
46
+ * `market`: live search across both registries, entered explicitly from a
47
+ * catalogue search; clearing the query returns to the catalogue.
48
+ */
49
+ scope: 'catalog' | 'market';
50
+ /** Catalogue category key, or `'all'`. */
51
+ category: string;
42
52
  source: 'all' | MarketSource;
43
53
  security: 'all' | SecurityStatus;
44
54
  installed: 'all' | 'installed' | 'installable';
@@ -46,8 +56,12 @@ export interface MarketFilters {
46
56
  /** Everything the panel renders from. Immutable: each commit publishes a new object. */
47
57
  export interface MarketState {
48
58
  filters: MarketFilters;
59
+ /** Category bar entries; empty until loaded. */
60
+ categories: MarketCategory[];
49
61
  items: NormalizedSkill[];
50
62
  nextCursor: string | null;
63
+ /** Matching skills in total, when the list knows it (the curated catalogue does). */
64
+ total: number | null;
51
65
  sources: Record<MarketSource, SourceStatusInfo>;
52
66
  /** Each card retains the provenance of its own page when more pages append. */
53
67
  itemStatuses: Record<string, SourceStatusInfo>;
@@ -65,7 +79,7 @@ export interface MarketState {
65
79
  detailStatus: SourceStatusInfo | null;
66
80
  detailLoading: boolean;
67
81
  detailError: string | null;
68
- activeTab: 'overview' | 'files';
82
+ activeTab: MarketDetailTab;
69
83
  file: MarketFileContent | null;
70
84
  fileLoading: boolean;
71
85
  installingIds: ReadonlySet<string>;
@@ -83,14 +97,20 @@ export interface MarketController {
83
97
  }): Promise<void>;
84
98
  loadMore(): Promise<void>;
85
99
  setQuery(q: string): void;
100
+ setCategory(category: MarketFilters['category']): void;
101
+ setScope(scope: MarketFilters['scope']): void;
102
+ /** Load the category bar once per controller (repeat calls share it). */
103
+ loadCategories(): Promise<void>;
86
104
  setSource(source: MarketFilters['source']): void;
87
105
  setSecurity(security: MarketFilters['security']): void;
88
106
  setInstalledFilter(installed: MarketFilters['installed']): void;
107
+ /** `owner` names the registry author when the id alone is ambiguous (ClawHub). */
89
108
  openDetail(id: string, options?: {
90
109
  refresh?: boolean;
110
+ owner?: string;
91
111
  }): Promise<void>;
92
112
  closeDetail(): void;
93
- setTab(tab: 'overview' | 'files'): void;
113
+ setTab(tab: MarketDetailTab): void;
94
114
  selectFile(path: string): Promise<void>;
95
115
  requestInstall(id: string): void;
96
116
  cancelInstall(): void;
@@ -7,6 +7,9 @@
7
7
  * - GET /api/v1/skills/{slug} → {skill, latestVersion, owner, metadata, moderation}
8
8
  * - GET /api/v1/skills/{slug}/versions/{v} → {version:{license, files[], security}}
9
9
  * - GET /api/v1/skills/{slug}/file?path= → raw file text
10
+ *
11
+ * Slugs are not unique: every endpoint accepts `?owner=`, and without it a
12
+ * shared slug answers 409 AMBIGUOUS_SKILL_SLUG. See `clawhubOwnerFor`.
10
13
  */
11
14
  import { type MarketProvider } from './types.ts';
12
15
  export type ParsedFrontmatter = {
@@ -23,6 +26,18 @@ export type ParsedFrontmatter = {
23
26
  * value it cannot understand is dropped rather than throwing.
24
27
  */
25
28
  export declare function parseFrontmatter(markdown: string): ParsedFrontmatter;
29
+ /** Shape of a ClawHub owner handle (also the route's validation rule). */
30
+ export declare const CLAWHUB_OWNER_PATTERN: RegExp;
31
+ export declare function setClawhubOwnerHints(hints: Iterable<readonly [string, string]>): void;
32
+ /**
33
+ * Run one operation pinned to `owner` for `slug`. Without an owner the
34
+ * operation runs unpinned and falls back to the hint/cache/probe order.
35
+ */
36
+ export declare function withClawhubOwner<T>(slug: string, owner: string | undefined, operation: () => Promise<T>): Promise<T>;
37
+ /** The owner a read of `slug` will use right now, if one is known before asking upstream. */
38
+ export declare function clawhubOwnerFor(slug: string): string | undefined;
39
+ /** A release note worth showing: registries stamp placeholders on synced versions. */
40
+ export declare function meaningfulChangelog(text: unknown): string | undefined;
26
41
  /** Test hook: forget which owner disambiguated an ambiguous slug. */
27
42
  export declare function resetClawhubOwnerCache(): void;
28
43
  export declare const clawhubProvider: MarketProvider;
@@ -56,6 +56,7 @@ export declare class MarketInstallError extends Error {
56
56
  export declare function installMarketSkill(source: MarketSource, slug: string, options: {
57
57
  skillsRoot: string;
58
58
  allowUninstall: boolean;
59
+ owner?: string;
59
60
  }): Promise<InstallResult>;
60
61
  /**
61
62
  * Remove one skill this plugin installed.
@@ -10,7 +10,7 @@
10
10
  * `InstalledLookup` (see `setInstalledLookup`), so this file can be unit-tested
11
11
  * by pointing the providers at a stub server.
12
12
  */
13
- import { type MarketFileContent, type MarketListResult, type MarketSource, type NormalizedSkill, type NormalizedSkillDetail, type SecurityStatus, type SourceStatusInfo } from './types.ts';
13
+ import { type MarketCategoriesResult, type MarketFileContent, type MarketListResult, type MarketSource, type NormalizedSkill, type NormalizedSkillDetail, type SecurityStatus, type SourceStatusInfo } from './types.ts';
14
14
  /**
15
15
  * Local install state, injected by the plugin host (which owns the skills
16
16
  * directory). Both methods are synchronous: the host keeps an in-memory index
@@ -55,6 +55,13 @@ export declare function applyFileLimits(detail: NormalizedSkillDetail): Normaliz
55
55
  export declare function dedupeSkills(items: NormalizedSkill[]): NormalizedSkill[];
56
56
  export interface ListMarketSkillsParams {
57
57
  q?: string;
58
+ /**
59
+ * `catalog` (default): the curated snapshot shipped with the plugin.
60
+ * `market`: live upstream list/search across both registries.
61
+ */
62
+ scope?: 'catalog' | 'market';
63
+ /** Catalogue category key; ignored by the live market scope. */
64
+ category?: string;
58
65
  source: 'all' | MarketSource;
59
66
  security: 'all' | SecurityStatus;
60
67
  installed: 'all' | 'installed' | 'installable';
@@ -64,15 +71,30 @@ export interface ListMarketSkillsParams {
64
71
  refresh?: boolean;
65
72
  }
66
73
  export declare function listMarketSkills(params: ListMarketSkillsParams): Promise<MarketListResult>;
74
+ /**
75
+ * One page of the curated catalogue.
76
+ *
77
+ * Everything is local, so every filter — security and installed included — runs
78
+ * before pagination: a page is always full until the list is exhausted, which
79
+ * is what keeps infinite scroll from stalling on a filtered-out page.
80
+ */
81
+ export declare function listCatalogSkills(params: ListMarketSkillsParams): MarketListResult;
82
+ export declare function listMarketCategories(): MarketCategoriesResult;
67
83
  export declare function getMarketSkillDetail(source: MarketSource, slug: string, options?: {
68
84
  force?: boolean;
69
85
  allowStale?: boolean;
86
+ owner?: string;
70
87
  }): Promise<{
71
88
  skill: NormalizedSkillDetail;
72
89
  sourceStatus: SourceStatusInfo;
73
90
  }>;
74
91
  export declare function isValidMarketFilePath(filePath: string): boolean;
75
- export declare function getMarketFileContent(source: MarketSource, slug: string, filePath: string): Promise<MarketFileContent>;
92
+ export declare function getMarketFileContent(source: MarketSource, slug: string, filePath: string, owner?: string): Promise<MarketFileContent>;
93
+ /**
94
+ * Pin a ClawHub read to the owner the reader asked for. ClawHub slugs are
95
+ * shared across authors, so `clawhub:<slug>` alone does not name one skill.
96
+ */
97
+ export declare function withMarketOwner<T>(source: MarketSource, slug: string, owner: string | undefined, operation: () => Promise<T>): Promise<T>;
76
98
  export declare function getMarketStatus(): Record<MarketSource, SourceStatusInfo>;
77
99
  /**
78
100
  * Look up a single skill for an install (the detail path, bypassing the list).
@@ -82,5 +104,5 @@ export declare function getMarketStatus(): Record<MarketSource, SourceStatusInfo
82
104
  * right now — the file bytes are fetched fresh and hash-verified against the
83
105
  * list, so the list must come from the same generation as those bytes.
84
106
  */
85
- export declare function resolveMarketSkill(source: MarketSource, slug: string): Promise<NormalizedSkillDetail>;
107
+ export declare function resolveMarketSkill(source: MarketSource, slug: string, owner?: string): Promise<NormalizedSkillDetail>;
86
108
  export {};
@@ -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,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,6 +16,7 @@
16
16
  * otherwise be reachable by a cross-site request or a DNS-rebound page.
17
17
  */
18
18
  import type { IncomingMessage, ServerResponse } from 'node:http';
19
+ import type { HostConnectionService } from '@deepseek-ai/dsh-client-connection';
19
20
  /** Path prefix of the whole Skills Hub API. */
20
21
  export declare const ROUTE_PREFIX = "/api/skills-hub";
21
22
  /** Structural Web server contract (mirrors `WebServer.register`). */
@@ -33,18 +34,8 @@ export interface SkillsHubRoutesDeps {
33
34
  /** Initial local inventory scan, required before annotating persisted data. */
34
35
  ready?: () => Promise<void>;
35
36
  }
36
- /**
37
- * Browser-trust gate, structurally equal to the frozen contract's predicate.
38
- *
39
- * The real `HostConnectionService.requestRejection` returns `401 | 403 |
40
- * undefined` rather than a boolean (see
41
- * `@deepseek-ai/dsh-client-connection/lib/types/rpc.d.ts`), so this plugin
42
- * accepts both shapes: see {@link evaluateGate} for why the numeric form is
43
- * worth preserving.
44
- */
45
- export interface SkillsHubRequestGate {
46
- requestRejection(req: IncomingMessage, res: ServerResponse): boolean;
47
- }
37
+ /** Use the host's actual trust predicate; this import is erased from the runtime. */
38
+ export type SkillsHubRequestGate = Pick<HostConnectionService, 'requestRejection'>;
48
39
  /**
49
40
  * Resolves the gate for one request.
50
41
  *