@nanmicoder/dsh-skills-hub 0.0.2 → 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.
- package/CONTRIBUTING.md +6 -2
- package/README.md +8 -7
- package/README_EN.md +8 -7
- package/README_ZH.md +1 -1
- package/assets/readme/marketplace.png +0 -0
- package/compatibility.json +1 -1
- package/docs/CATALOG.md +39 -0
- package/docs/COMPATIBILITY.md +12 -0
- package/docs/CONTRACT.md +14 -9
- package/docs/RELEASE.md +4 -4
- package/lib/catalog/catalog.js +89 -0
- package/lib/catalog/skills.json +8673 -0
- package/lib/client.js +2549 -1819
- package/lib/client.js.map +1 -1
- package/lib/market/clawhub-provider.js +139 -22
- package/lib/market/install-service.js +19 -8
- package/lib/market/market-service.js +88 -9
- package/lib/market/skillhub-provider.js +6 -2
- package/lib/skills/installed.js +5 -0
- package/lib/types/catalog/catalog.d.ts +73 -0
- package/lib/types/client/api.d.ts +12 -3
- package/lib/types/client/components/CategoryBar.d.ts +13 -0
- package/lib/types/client/components/FilterBar.d.ts +4 -0
- package/lib/types/client/components/InstallConfirmDialog.d.ts +14 -11
- package/lib/types/client/components/InstalledSkillDetail.d.ts +1 -1
- package/lib/types/client/components/InstalledSkills.d.ts +1 -1
- package/lib/types/client/components/MarketSkillDetail.d.ts +34 -0
- package/lib/types/client/components/SkillDetailShell.d.ts +66 -43
- package/lib/types/client/locales.d.ts +64 -0
- package/lib/types/client/skill-insights.d.ts +30 -0
- package/lib/types/client/state.d.ts +23 -3
- package/lib/types/market/clawhub-provider.d.ts +15 -0
- package/lib/types/market/install-service.d.ts +1 -0
- package/lib/types/market/market-service.d.ts +25 -3
- package/lib/types/market/types.d.ts +29 -0
- package/lib/types/skills/installed.d.ts +4 -0
- package/lib/types/web-routes.d.ts +1 -1
- package/lib/web-routes.js +44 -5
- package/package.json +1 -1
- package/release-notes/v0.0.3.md +12 -0
- package/lib/types/client/components/InstallStateBadge.d.ts +0 -11
- package/lib/types/client/components/SecurityBadge.d.ts +0 -16
- 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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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.
|
|
7
|
-
*
|
|
8
|
-
* document
|
|
9
|
-
*
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
29
|
-
|
|
17
|
+
export interface DetailTab<K extends string> {
|
|
18
|
+
key: K;
|
|
30
19
|
label: string;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
31
|
+
version?: string;
|
|
32
|
+
/** One line of "by author · source · licence · updated" facts. */
|
|
33
|
+
meta?: ReactNode;
|
|
43
34
|
summary?: string;
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
*
|