@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.
- package/CONTRIBUTING.md +22 -0
- package/README.md +32 -82
- package/README_EN.md +32 -82
- package/README_ZH.md +2 -0
- package/assets/readme/marketplace.png +0 -0
- package/compatibility.json +1 -1
- package/docs/CATALOG.md +39 -0
- package/docs/COMPATIBILITY.md +46 -0
- package/docs/CONFIGURATION.md +38 -0
- package/docs/CONTRACT.md +35 -12
- package/docs/RELEASE.md +5 -5
- package/lib/catalog/catalog.js +89 -0
- package/lib/catalog/skills.json +8673 -0
- package/lib/client.js +3349 -1728
- package/lib/client.js.map +1 -1
- package/lib/config.js +2 -0
- package/lib/index.js +16 -6
- package/lib/market/cache.js +104 -30
- package/lib/market/clawhub-provider.js +139 -22
- package/lib/market/disk-cache.js +113 -0
- package/lib/market/install-service.js +24 -11
- package/lib/market/market-service.js +161 -32
- package/lib/market/provider-fetch.js +9 -2
- package/lib/market/skillhub-provider.js +6 -2
- package/lib/market/types.js +2 -1
- package/lib/skills/installed.js +5 -0
- package/lib/skills/management.js +182 -4
- package/lib/skills/root.js +1 -1
- package/lib/types/catalog/catalog.d.ts +73 -0
- package/lib/types/client/api.d.ts +45 -8
- 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 +20 -11
- package/lib/types/client/components/InstalledSkillDetail.d.ts +24 -0
- package/lib/types/client/components/InstalledSkills.d.ts +18 -1
- package/lib/types/client/components/MarkdownView.d.ts +7 -1
- package/lib/types/client/components/MarketSkillDetail.d.ts +34 -0
- package/lib/types/client/components/SkillAvatar.d.ts +1 -1
- package/lib/types/client/components/SkillDetailShell.d.ts +86 -0
- package/lib/types/client/components/SkillFiles.d.ts +32 -0
- package/lib/types/client/components/SourceStatusBar.d.ts +4 -0
- package/lib/types/client/icons.d.ts +4 -0
- package/lib/types/client/locales.d.ts +94 -0
- package/lib/types/client/relative-time.d.ts +13 -0
- package/lib/types/client/skill-insights.d.ts +30 -0
- package/lib/types/client/skill-markdown.d.ts +39 -0
- package/lib/types/client/state.d.ts +36 -7
- package/lib/types/config.d.ts +2 -0
- package/lib/types/market/cache.d.ts +62 -14
- package/lib/types/market/clawhub-provider.d.ts +15 -0
- package/lib/types/market/disk-cache.d.ts +16 -0
- package/lib/types/market/install-service.d.ts +1 -0
- package/lib/types/market/market-service.d.ts +40 -6
- package/lib/types/market/provider-fetch.d.ts +2 -0
- package/lib/types/market/types.d.ts +29 -0
- package/lib/types/skills/installed.d.ts +4 -0
- package/lib/types/skills/management.d.ts +63 -2
- package/lib/types/skills/root.d.ts +8 -0
- package/lib/types/web-routes.d.ts +3 -1
- package/lib/web-routes.js +85 -8
- package/package.json +1 -1
- package/release-notes/v0.0.1.md +1 -1
- package/release-notes/v0.0.2.md +12 -0
- 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 -21
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files tab body, shared by the two detail pages.
|
|
3
|
+
*
|
|
4
|
+
* The market page lists provider file metadata and the local page lists the
|
|
5
|
+
* bytes on disk, but both are the same control: a picker on the left, one
|
|
6
|
+
* document on the right, and a copy button that confirms itself. The only
|
|
7
|
+
* difference the caller keeps is how a selection is fetched.
|
|
8
|
+
*
|
|
9
|
+
* A markdown document opens *rendered*: SKILL.md is prose meant to be read, and
|
|
10
|
+
* showing its source first answers a question nobody asked. Installing still has
|
|
11
|
+
* to be a trust decision, though, so the raw bytes are one toggle away — with
|
|
12
|
+
* line numbers and the copy button pointed at exactly what is on disk. Other
|
|
13
|
+
* files have no rendered form and stay source-only.
|
|
14
|
+
*/
|
|
15
|
+
import type { MarketFileContent } from '../../market/types.ts';
|
|
16
|
+
/** The subset of a file row this control renders; both pages' metadata fits it. */
|
|
17
|
+
export interface SkillFileListItem {
|
|
18
|
+
path: string;
|
|
19
|
+
size: number;
|
|
20
|
+
language: string;
|
|
21
|
+
/** Provider flagged the file as beyond the preview/install limit. */
|
|
22
|
+
tooBig?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function SkillFiles(props: {
|
|
25
|
+
files: SkillFileListItem[];
|
|
26
|
+
/** Already-fetched document for the selected path, or `null` while none is loaded. */
|
|
27
|
+
selected: MarketFileContent | null;
|
|
28
|
+
loading: boolean;
|
|
29
|
+
/** Reader-facing failure of the last selection, if any. */
|
|
30
|
+
error?: string | null;
|
|
31
|
+
onSelect: (path: string) => void;
|
|
32
|
+
}): JSX.Element;
|
|
@@ -5,6 +5,10 @@ import type { MarketSource, SourceStatusInfo } from '../../market/types.ts';
|
|
|
5
5
|
* The upstream error string rides the pill's `title` instead of being printed:
|
|
6
6
|
* a 720px panel has no room for a stack trace next to two source names, and an
|
|
7
7
|
* operator who needs it can hover.
|
|
8
|
+
*
|
|
9
|
+
* The snapshot age *is* printed: a healthy source can still be answering from a
|
|
10
|
+
* cache, and a reader deciding whether to install third-party code is entitled
|
|
11
|
+
* to see that the page in front of them was read minutes ago rather than now.
|
|
8
12
|
*/
|
|
9
13
|
export declare function SourceStatusBar(props: {
|
|
10
14
|
sources: Record<MarketSource, SourceStatusInfo>;
|
|
@@ -69,3 +69,7 @@ export declare function ChevronRightIcon({ size, className }: SkillsHubIconProps
|
|
|
69
69
|
export declare function CloseIcon({ size, className }: SkillsHubIconProps): JSX.Element;
|
|
70
70
|
/** Price tag: leading glyph of a card's tag row. */
|
|
71
71
|
export declare function TagIcon({ size, className }: SkillsHubIconProps): JSX.Element;
|
|
72
|
+
/** Folder: leading glyph of the installed skill's location line. */
|
|
73
|
+
export declare function FolderIcon({ size, className }: SkillsHubIconProps): JSX.Element;
|
|
74
|
+
/** Arrow leaving a frame: open the same skill in the marketplace. */
|
|
75
|
+
export declare function ExternalLinkIcon({ size, className }: SkillsHubIconProps): JSX.Element;
|
|
@@ -29,6 +29,34 @@ declare const zhDictionary: {
|
|
|
29
29
|
localUninstallDescription: string;
|
|
30
30
|
unlinkDescription: string;
|
|
31
31
|
uninstalling: string;
|
|
32
|
+
uninstallDisabled: string;
|
|
33
|
+
copyPath: string;
|
|
34
|
+
copiedPath: string;
|
|
35
|
+
sourceLabel: string;
|
|
36
|
+
managedByHub: string;
|
|
37
|
+
unmanagedSkill: string;
|
|
38
|
+
symlinkSkill: string;
|
|
39
|
+
installedAtLabel: string;
|
|
40
|
+
skillKind: string;
|
|
41
|
+
bundleSkill: string;
|
|
42
|
+
flatSkill: string;
|
|
43
|
+
filesCount: string;
|
|
44
|
+
emptyDocument: string;
|
|
45
|
+
installedReadFailed: string;
|
|
46
|
+
openInMarket: string;
|
|
47
|
+
fileView: string;
|
|
48
|
+
filePreview: string;
|
|
49
|
+
fileSource: string;
|
|
50
|
+
snapshotAge: string;
|
|
51
|
+
ageJustNow: string;
|
|
52
|
+
ageMinutes: string;
|
|
53
|
+
ageHours: string;
|
|
54
|
+
ageDays: string;
|
|
55
|
+
refreshNow: string;
|
|
56
|
+
refreshSnapshot: string;
|
|
57
|
+
detailSnapshot: string;
|
|
58
|
+
installSnapshot: string;
|
|
59
|
+
marketProvenance: string;
|
|
32
60
|
panel: string;
|
|
33
61
|
title: string;
|
|
34
62
|
subtitle: string;
|
|
@@ -37,6 +65,7 @@ declare const zhDictionary: {
|
|
|
37
65
|
'source.all': string;
|
|
38
66
|
'source.clawhub': string;
|
|
39
67
|
'source.skillhub': string;
|
|
68
|
+
'source.local': string;
|
|
40
69
|
'security.all': string;
|
|
41
70
|
'security.verified': string;
|
|
42
71
|
'security.benign': string;
|
|
@@ -45,6 +74,70 @@ declare const zhDictionary: {
|
|
|
45
74
|
'installed.all': string;
|
|
46
75
|
'installed.installed': string;
|
|
47
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;
|
|
48
141
|
'filter.source': string;
|
|
49
142
|
'filter.security': string;
|
|
50
143
|
'filter.installed': string;
|
|
@@ -96,6 +189,7 @@ declare const zhDictionary: {
|
|
|
96
189
|
uninstallDone: string;
|
|
97
190
|
noFiles: string;
|
|
98
191
|
fileTooLarge: string;
|
|
192
|
+
fileTruncated: string;
|
|
99
193
|
copy: string;
|
|
100
194
|
copied: string;
|
|
101
195
|
unoaudited: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How old a snapshot is, in the panel's own vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* The panel shows cached catalogue data, so "when was this actually fetched" is
|
|
5
|
+
* part of the answer rather than a debug detail: a reader who is about to
|
|
6
|
+
* install third-party code is entitled to know whether the security report in
|
|
7
|
+
* front of them was read a minute ago or yesterday.
|
|
8
|
+
*/
|
|
9
|
+
import type { Translate } from './locale-context.tsx';
|
|
10
|
+
/** Coarse buckets: the first match wins, and precision beyond them is noise. */
|
|
11
|
+
export declare function formatAge(t: Translate, from: number, now?: number): string;
|
|
12
|
+
/** Absolute timestamp for the hover text, so the bucket never hides the fact. */
|
|
13
|
+
export declare function formatStamp(from: number): 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;
|
|
@@ -1,2 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontmatter handling for skill documents.
|
|
3
|
+
*
|
|
4
|
+
* A `SKILL.md` opens with a YAML block, and feeding that block to a markdown
|
|
5
|
+
* renderer is not merely ugly — it is wrong: a lone `---` is a setext underline,
|
|
6
|
+
* so the closing fence promotes the whole metadata block into one giant heading.
|
|
7
|
+
* The block is split off here instead, so the UI can render it as structured
|
|
8
|
+
* metadata (`FrontmatterPanel`) while the rest of the document goes to the
|
|
9
|
+
* renderer. The approach mirrors the desktop project's `skillFrontmatter.ts`.
|
|
10
|
+
*
|
|
11
|
+
* The parser is a deliberately small YAML subset — scalars, inline and block
|
|
12
|
+
* sequences, `|`/`>` block scalars, and one level of nested mapping that is kept
|
|
13
|
+
* as the raw YAML it is — because skill frontmatter is flat by convention and a
|
|
14
|
+
* full YAML engine is a dependency this browser bundle does not need. It never
|
|
15
|
+
* throws: a document without a fence, or with an unterminated one, comes back as
|
|
16
|
+
* body only.
|
|
17
|
+
*/
|
|
18
|
+
/** One document separated into its metadata and everything else. */
|
|
19
|
+
export interface SkillFrontmatterSplit {
|
|
20
|
+
/** Parsed metadata, or `null` when the document carries none worth showing. */
|
|
21
|
+
frontmatter: Record<string, unknown> | null;
|
|
22
|
+
/** The document with its metadata block removed. */
|
|
23
|
+
body: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse a metadata block that has already been extracted — the Host bounds its
|
|
27
|
+
* size before it reaches the browser, so this entry point works on that text.
|
|
28
|
+
*
|
|
29
|
+
* @param yaml - the block's text, without its fences.
|
|
30
|
+
* @returns the parsed keys, or `null` when the block carries nothing to show.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseSkillFrontmatter(yaml: string): Record<string, unknown> | null;
|
|
33
|
+
/**
|
|
34
|
+
* Separate a document's frontmatter from its body.
|
|
35
|
+
*
|
|
36
|
+
* @param markdown - the whole document.
|
|
37
|
+
* @returns the parsed metadata (or `null`) plus the body to render.
|
|
38
|
+
*/
|
|
39
|
+
export declare function splitSkillFrontmatter(markdown: string): SkillFrontmatterSplit;
|
|
1
40
|
/** Frontmatter is metadata, not part of the skill's rendered instructions. */
|
|
2
41
|
export declare function stripSkillFrontmatter(markdown: string): string;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* already holds.
|
|
14
14
|
* - **in-flight guards**: one install/uninstall per id, one page per cursor, one
|
|
15
15
|
* detail/file request per target.
|
|
16
|
-
* - **detail
|
|
17
|
-
*
|
|
16
|
+
* - **detail snapshots**: reopening can show the previous copy immediately,
|
|
17
|
+
* while the Host rechecks its TTL and current install annotations.
|
|
18
18
|
* - **local management**: the InstalledSkills component independently lists what is already
|
|
19
19
|
* on disk (`fetchInstalled`) instead of asking the market, matching the
|
|
20
20
|
* contract's §5.3 behaviour.
|
|
@@ -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,9 +56,15 @@ 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>;
|
|
66
|
+
/** Each card retains the provenance of its own page when more pages append. */
|
|
67
|
+
itemStatuses: Record<string, SourceStatusInfo>;
|
|
52
68
|
loading: boolean;
|
|
53
69
|
loadingMore: boolean;
|
|
54
70
|
error: string | null;
|
|
@@ -59,9 +75,11 @@ export interface MarketState {
|
|
|
59
75
|
id: string;
|
|
60
76
|
};
|
|
61
77
|
detail: NormalizedSkillDetail | null;
|
|
78
|
+
/** Provenance of `detail`: when it was fetched upstream, and whether it is a snapshot. */
|
|
79
|
+
detailStatus: SourceStatusInfo | null;
|
|
62
80
|
detailLoading: boolean;
|
|
63
81
|
detailError: string | null;
|
|
64
|
-
activeTab:
|
|
82
|
+
activeTab: MarketDetailTab;
|
|
65
83
|
file: MarketFileContent | null;
|
|
66
84
|
fileLoading: boolean;
|
|
67
85
|
installingIds: ReadonlySet<string>;
|
|
@@ -73,15 +91,26 @@ export interface MarketState {
|
|
|
73
91
|
export interface MarketController {
|
|
74
92
|
state: MarketState;
|
|
75
93
|
readonly injected: MarketPageInjected;
|
|
76
|
-
refresh
|
|
94
|
+
/** `force` is the reader's refresh: it must not be answered from the cache. */
|
|
95
|
+
refresh(options?: {
|
|
96
|
+
force?: boolean;
|
|
97
|
+
}): Promise<void>;
|
|
77
98
|
loadMore(): Promise<void>;
|
|
78
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>;
|
|
79
104
|
setSource(source: MarketFilters['source']): void;
|
|
80
105
|
setSecurity(security: MarketFilters['security']): void;
|
|
81
106
|
setInstalledFilter(installed: MarketFilters['installed']): void;
|
|
82
|
-
|
|
107
|
+
/** `owner` names the registry author when the id alone is ambiguous (ClawHub). */
|
|
108
|
+
openDetail(id: string, options?: {
|
|
109
|
+
refresh?: boolean;
|
|
110
|
+
owner?: string;
|
|
111
|
+
}): Promise<void>;
|
|
83
112
|
closeDetail(): void;
|
|
84
|
-
setTab(tab:
|
|
113
|
+
setTab(tab: MarketDetailTab): void;
|
|
85
114
|
selectFile(path: string): Promise<void>;
|
|
86
115
|
requestInstall(id: string): void;
|
|
87
116
|
cancelInstall(): void;
|
package/lib/types/config.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface SkillsHubConfig {
|
|
|
29
29
|
allowUninstall: boolean;
|
|
30
30
|
/** Default `limit` for `GET /skills` when the request omits one (clamped to 1…100). */
|
|
31
31
|
pageSize: number;
|
|
32
|
+
/** Lifetime of persisted market snapshots in minutes (1…1440). */
|
|
33
|
+
cacheTtlMinutes: number;
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* Shipped defaults. Exported so `apply()` can fill in a partially resolved
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skills Market —
|
|
2
|
+
* Skills Market — memory + persistent TTL cache with stale-while-error support, plus
|
|
3
3
|
* per-source health tracking.
|
|
4
4
|
*
|
|
5
5
|
* Only *upstream* data is cached here (list pages, details, file bodies).
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* and is deliberately never cached: a skill installed a second ago must show up
|
|
8
8
|
* as installed on the very next list call.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The host configures disk storage at activation. Without a directory the
|
|
11
|
+
* cache stays memory-only, so provider tests never touch a user's Harness home.
|
|
12
12
|
*/
|
|
13
13
|
import type { MarketSource, SourceHealthStatus, SourceStatusInfo } from './types.ts';
|
|
14
|
-
/**
|
|
14
|
+
/** Default snapshot lifetime; activation can override it with cacheTtlMinutes. */
|
|
15
15
|
export declare const MARKET_TTL: {
|
|
16
16
|
readonly list: number;
|
|
17
17
|
readonly search: number;
|
|
@@ -19,18 +19,67 @@ export declare const MARKET_TTL: {
|
|
|
19
19
|
readonly files: number;
|
|
20
20
|
readonly fileContent: number;
|
|
21
21
|
};
|
|
22
|
-
|
|
22
|
+
export interface MarketCacheOptions {
|
|
23
|
+
directory?: string;
|
|
24
|
+
ttlMs?: number;
|
|
25
|
+
onError?: (error: unknown) => void;
|
|
26
|
+
}
|
|
27
|
+
/** One fresh hit, together with the moment its payload was actually fetched. */
|
|
28
|
+
export interface MarketCacheRecord<T> {
|
|
29
|
+
value: T;
|
|
30
|
+
/** Epoch millis of the upstream read this payload came from. */
|
|
31
|
+
storedAt: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Counters describing what the cache actually did.
|
|
35
|
+
*
|
|
36
|
+
* "How much traffic does the cache save" used to be an argument. Every claim
|
|
37
|
+
* about hits, misses and needless upstream reads can now be checked against
|
|
38
|
+
* these numbers on a real machine instead of reasoned about.
|
|
39
|
+
*/
|
|
40
|
+
export interface MarketCacheStats {
|
|
41
|
+
/** Answers served from a fresh memory or disk entry. */
|
|
42
|
+
hits: number;
|
|
43
|
+
/** Lookups that had to go upstream (including forced refreshes). */
|
|
44
|
+
misses: number;
|
|
45
|
+
/** Answers served from an expired entry because the source failed. */
|
|
46
|
+
staleServed: number;
|
|
47
|
+
/** Upstream page/detail requests actually issued. */
|
|
48
|
+
upstreamRequests: number;
|
|
49
|
+
/** Refreshes a reader explicitly asked for. */
|
|
50
|
+
forcedRefreshes: number;
|
|
51
|
+
}
|
|
52
|
+
/** Record one cache event. Kept tiny so it can sit on the request path. */
|
|
53
|
+
export declare function noteMarketStat(entry: keyof MarketCacheStats): void;
|
|
54
|
+
/** Snapshot of the counters; the caller cannot mutate the live object. */
|
|
55
|
+
export declare function getMarketStats(): MarketCacheStats;
|
|
56
|
+
export declare class MarketCache {
|
|
57
|
+
private options;
|
|
23
58
|
private entries;
|
|
24
|
-
|
|
59
|
+
private disk?;
|
|
60
|
+
private warned;
|
|
61
|
+
private ttlMs;
|
|
62
|
+
constructor(options?: MarketCacheOptions);
|
|
63
|
+
private warn;
|
|
64
|
+
private remember;
|
|
65
|
+
private read;
|
|
66
|
+
/**
|
|
67
|
+
* Read a fresh entry.
|
|
68
|
+
*
|
|
69
|
+
* The record carries `storedAt` on purpose: a hit is a *snapshot*, and the
|
|
70
|
+
* caller must be able to say how old it is. Returning only the payload is how
|
|
71
|
+
* the panel ended up reporting "fetched just now" for data it had not touched
|
|
72
|
+
* in ten minutes.
|
|
73
|
+
*/
|
|
74
|
+
getRecord<T>(key: string): Promise<MarketCacheRecord<T> | undefined>;
|
|
25
75
|
/** Returns the entry even when expired — used for the stale-while-error fallback. */
|
|
26
|
-
getStale<T>(key: string):
|
|
27
|
-
|
|
28
|
-
storedAt: number;
|
|
29
|
-
} | undefined;
|
|
30
|
-
set(key: string, value: unknown, ttlMs: number): void;
|
|
76
|
+
getStale<T>(key: string): Promise<MarketCacheRecord<T> | undefined>;
|
|
77
|
+
set(key: string, value: unknown, ttlMs?: number): Promise<number>;
|
|
31
78
|
clear(): void;
|
|
32
79
|
}
|
|
33
|
-
export declare
|
|
80
|
+
export declare let marketCache: MarketCache;
|
|
81
|
+
/** Replace memory state when activating a profile; disk entries are read lazily. */
|
|
82
|
+
export declare function configureMarketCache(options: MarketCacheOptions): void;
|
|
34
83
|
/**
|
|
35
84
|
* Record the outcome of one upstream interaction.
|
|
36
85
|
*
|
|
@@ -44,6 +93,5 @@ export declare function markSourceHealth(source: MarketSource, status: SourceHea
|
|
|
44
93
|
export declare function getSourceHealth(source: MarketSource): SourceStatusInfo;
|
|
45
94
|
/** Test hook: forget every recorded success/failure. */
|
|
46
95
|
export declare function resetSourceHealth(): void;
|
|
47
|
-
/** Test hook: drop
|
|
96
|
+
/** Test hook: drop memory payloads, reset source health and the counters. */
|
|
48
97
|
export declare function resetMarketCache(): void;
|
|
49
|
-
export {};
|
|
@@ -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;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface CacheEntry {
|
|
2
|
+
value: unknown;
|
|
3
|
+
expiresAt: number;
|
|
4
|
+
storedAt: number;
|
|
5
|
+
}
|
|
6
|
+
export declare const MAX_CACHE_ENTRIES = 500;
|
|
7
|
+
export declare const MAX_STALE_AGE_MS: number;
|
|
8
|
+
export declare class DiskMarketCache {
|
|
9
|
+
private directory;
|
|
10
|
+
private writes;
|
|
11
|
+
constructor(directory: string);
|
|
12
|
+
private filename;
|
|
13
|
+
read(key: string): Promise<CacheEntry | undefined>;
|
|
14
|
+
write(key: string, entry: CacheEntry): Promise<void>;
|
|
15
|
+
private prune;
|
|
16
|
+
}
|
|
@@ -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.
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* cross-source dedupe, per-source health/degradation reporting, TTL caching
|
|
6
6
|
* (stale-while-error) and locally-computed install state.
|
|
7
7
|
*
|
|
8
|
-
* The module is deliberately free of DSH services
|
|
8
|
+
* The module is deliberately free of DSH services: the
|
|
9
9
|
* "what is installed locally" question is answered by an injected
|
|
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,20 +55,54 @@ 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';
|
|
61
68
|
cursor?: string;
|
|
62
69
|
limit: number;
|
|
70
|
+
/** Reader-requested refresh: ignore fresh entries (they are still rewritten). */
|
|
71
|
+
refresh?: boolean;
|
|
63
72
|
}
|
|
64
73
|
export declare function listMarketSkills(params: ListMarketSkillsParams): Promise<MarketListResult>;
|
|
65
|
-
|
|
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;
|
|
83
|
+
export declare function getMarketSkillDetail(source: MarketSource, slug: string, options?: {
|
|
84
|
+
force?: boolean;
|
|
85
|
+
allowStale?: boolean;
|
|
86
|
+
owner?: string;
|
|
87
|
+
}): Promise<{
|
|
66
88
|
skill: NormalizedSkillDetail;
|
|
67
89
|
sourceStatus: SourceStatusInfo;
|
|
68
90
|
}>;
|
|
69
91
|
export declare function isValidMarketFilePath(filePath: string): boolean;
|
|
70
|
-
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>;
|
|
71
98
|
export declare function getMarketStatus(): Record<MarketSource, SourceStatusInfo>;
|
|
72
|
-
/**
|
|
73
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Look up a single skill for an install (the detail path, bypassing the list).
|
|
101
|
+
*
|
|
102
|
+
* Always forced: a cached manifest is a snapshot of the *catalogue*, and
|
|
103
|
+
* installing is the one action that has to be authorized by what upstream says
|
|
104
|
+
* right now — the file bytes are fetched fresh and hash-verified against the
|
|
105
|
+
* list, so the list must come from the same generation as those bytes.
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolveMarketSkill(source: MarketSource, slug: string, owner?: string): Promise<NormalizedSkillDetail>;
|
|
74
108
|
export {};
|
|
@@ -18,6 +18,8 @@ export interface ProviderEndpointConfig {
|
|
|
18
18
|
retries: number;
|
|
19
19
|
userAgent: string;
|
|
20
20
|
}
|
|
21
|
+
/** Keep cache keys and all chained upstream reads on one configuration snapshot. */
|
|
22
|
+
export declare function withProviderConfiguration<T>(operation: () => Promise<T>): Promise<T>;
|
|
21
23
|
/**
|
|
22
24
|
* Point the providers at different hosts / limits. Called by the plugin with the
|
|
23
25
|
* resolved Config; tests call it to aim the providers at a local stub server.
|