@nanmicoder/dsh-skills-hub 0.0.1
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/AGENTS.md +9 -0
- package/CONTRIBUTING.md +11 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/README_EN.md +120 -0
- package/README_ZH.md +5 -0
- package/assets/readme/hero-en.png +0 -0
- package/assets/readme/hero-zh.png +0 -0
- package/assets/readme/marketplace.png +0 -0
- package/compatibility.json +10 -0
- package/cordis.patch.yml +5 -0
- package/docs/COMPATIBILITY.md +31 -0
- package/docs/CONTRACT.md +125 -0
- package/docs/RELEASE.md +24 -0
- package/lib/client.js +25115 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +51 -0
- package/lib/index.js +176 -0
- package/lib/market/cache.js +113 -0
- package/lib/market/clawhub-provider.js +598 -0
- package/lib/market/install-service.js +394 -0
- package/lib/market/market-service.js +312 -0
- package/lib/market/provider-fetch.js +216 -0
- package/lib/market/skillhub-provider.js +277 -0
- package/lib/market/types.js +82 -0
- package/lib/skills/installed.js +426 -0
- package/lib/skills/management.js +36 -0
- package/lib/skills/read-text.js +23 -0
- package/lib/skills/root.js +112 -0
- package/lib/types/client/api.d.ts +106 -0
- package/lib/types/client/components/FilterBar.d.ts +20 -0
- package/lib/types/client/components/FrontmatterPanel.d.ts +15 -0
- package/lib/types/client/components/InstallConfirmDialog.d.ts +31 -0
- package/lib/types/client/components/InstallStateBadge.d.ts +11 -0
- package/lib/types/client/components/InstalledSkills.d.ts +3 -0
- package/lib/types/client/components/MarkdownView.d.ts +4 -0
- package/lib/types/client/components/MarketDisclaimer.d.ts +10 -0
- package/lib/types/client/components/MarketHome.d.ts +12 -0
- package/lib/types/client/components/SecurityBadge.d.ts +16 -0
- package/lib/types/client/components/SkillAvatar.d.ts +14 -0
- package/lib/types/client/components/SkillCard.d.ts +17 -0
- package/lib/types/client/components/SkillDetailView.d.ts +21 -0
- package/lib/types/client/components/SourceStatusBar.d.ts +13 -0
- package/lib/types/client/icons.d.ts +71 -0
- package/lib/types/client/index.d.ts +56 -0
- package/lib/types/client/locale-context.d.ts +31 -0
- package/lib/types/client/locales.d.ts +117 -0
- package/lib/types/client/page/MarketPage.d.ts +21 -0
- package/lib/types/client/skill-markdown.d.ts +2 -0
- package/lib/types/client/state.d.ts +119 -0
- package/lib/types/config.d.ts +51 -0
- package/lib/types/index.d.ts +42 -0
- package/lib/types/market/cache.d.ts +49 -0
- package/lib/types/market/clawhub-provider.d.ts +28 -0
- package/lib/types/market/install-service.d.ts +75 -0
- package/lib/types/market/market-service.d.ts +74 -0
- package/lib/types/market/provider-fetch.d.ts +61 -0
- package/lib/types/market/skillhub-provider.d.ts +13 -0
- package/lib/types/market/types.d.ts +163 -0
- package/lib/types/skills/installed.d.ts +117 -0
- package/lib/types/skills/management.d.ts +7 -0
- package/lib/types/skills/read-text.d.ts +5 -0
- package/lib/types/skills/root.d.ts +48 -0
- package/lib/types/web-routes.d.ts +74 -0
- package/lib/web-routes.js +427 -0
- package/package.json +160 -0
- package/release-notes/v0.0.1.md +11 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Hub panel state machine — framework-free, so it is unit-testable
|
|
3
|
+
* outside React.
|
|
4
|
+
*
|
|
5
|
+
* Ported from the reference desktop store (`desktop/src/stores/marketStore.ts`)
|
|
6
|
+
* with the same invariants:
|
|
7
|
+
*
|
|
8
|
+
* - **stale-response rejection**: every request family carries a monotonically
|
|
9
|
+
* increasing sequence; a response whose sequence is no longer current is
|
|
10
|
+
* dropped instead of overwriting newer state.
|
|
11
|
+
* - **merge by id**: a page append and an install/uninstall patch both merge on
|
|
12
|
+
* `NormalizedSkill.id`, and pagination never duplicates a skill the list
|
|
13
|
+
* already holds.
|
|
14
|
+
* - **in-flight guards**: one install/uninstall per id, one page per cursor, one
|
|
15
|
+
* detail/file request per target.
|
|
16
|
+
* - **detail/file caching**: reopening a skill is instant and cannot blank the
|
|
17
|
+
* pane with a spinner.
|
|
18
|
+
* - **local management**: the InstalledSkills component independently lists what is already
|
|
19
|
+
* on disk (`fetchInstalled`) instead of asking the market, matching the
|
|
20
|
+
* contract's §5.3 behaviour.
|
|
21
|
+
*
|
|
22
|
+
* Aborts are control flow, never user-visible: the panel aborts superseded
|
|
23
|
+
* requests on purpose, and `isAbortError` keeps those rejections out of
|
|
24
|
+
* `state.error` / `state.notice`.
|
|
25
|
+
*/
|
|
26
|
+
import type { MarketFileContent, MarketSource, NormalizedSkill, NormalizedSkillDetail, SecurityStatus, SourceStatusInfo } from '../market/types.ts';
|
|
27
|
+
/**
|
|
28
|
+
* Notice values that are dictionary *codes* rather than literal text.
|
|
29
|
+
*
|
|
30
|
+
* The controller has no locale (it is framework-free and testable), so it stores
|
|
31
|
+
* a code for the panel to translate and stores a verbatim message for anything
|
|
32
|
+
* that has no dictionary entry — which is exactly what a Host error is.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MARKET_NOTICE_CODES: readonly ["installDone", "uninstallDone"];
|
|
35
|
+
/** A notice value the panel translates through its dictionary. */
|
|
36
|
+
export type MarketNoticeCode = (typeof MARKET_NOTICE_CODES)[number];
|
|
37
|
+
/** Whether a `state.notice` value is a dictionary code (as opposed to verbatim text). */
|
|
38
|
+
export declare function isMarketNoticeCode(notice: string): notice is MarketNoticeCode;
|
|
39
|
+
/** Active catalogue filters. */
|
|
40
|
+
export interface MarketFilters {
|
|
41
|
+
q: string;
|
|
42
|
+
source: 'all' | MarketSource;
|
|
43
|
+
security: 'all' | SecurityStatus;
|
|
44
|
+
installed: 'all' | 'installed' | 'installable';
|
|
45
|
+
}
|
|
46
|
+
/** Everything the panel renders from. Immutable: each commit publishes a new object. */
|
|
47
|
+
export interface MarketState {
|
|
48
|
+
filters: MarketFilters;
|
|
49
|
+
items: NormalizedSkill[];
|
|
50
|
+
nextCursor: string | null;
|
|
51
|
+
sources: Record<MarketSource, SourceStatusInfo>;
|
|
52
|
+
loading: boolean;
|
|
53
|
+
loadingMore: boolean;
|
|
54
|
+
error: string | null;
|
|
55
|
+
view: {
|
|
56
|
+
kind: 'home';
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'detail';
|
|
59
|
+
id: string;
|
|
60
|
+
};
|
|
61
|
+
detail: NormalizedSkillDetail | null;
|
|
62
|
+
detailLoading: boolean;
|
|
63
|
+
detailError: string | null;
|
|
64
|
+
activeTab: 'overview' | 'files';
|
|
65
|
+
file: MarketFileContent | null;
|
|
66
|
+
fileLoading: boolean;
|
|
67
|
+
installingIds: ReadonlySet<string>;
|
|
68
|
+
confirmInstallId: string | null;
|
|
69
|
+
disclaimerDismissed: boolean;
|
|
70
|
+
notice: string | null;
|
|
71
|
+
}
|
|
72
|
+
/** Actions and state the panel root drives. */
|
|
73
|
+
export interface MarketController {
|
|
74
|
+
state: MarketState;
|
|
75
|
+
readonly injected: MarketPageInjected;
|
|
76
|
+
refresh(): Promise<void>;
|
|
77
|
+
loadMore(): Promise<void>;
|
|
78
|
+
setQuery(q: string): void;
|
|
79
|
+
setSource(source: MarketFilters['source']): void;
|
|
80
|
+
setSecurity(security: MarketFilters['security']): void;
|
|
81
|
+
setInstalledFilter(installed: MarketFilters['installed']): void;
|
|
82
|
+
openDetail(id: string): Promise<void>;
|
|
83
|
+
closeDetail(): void;
|
|
84
|
+
setTab(tab: 'overview' | 'files'): void;
|
|
85
|
+
selectFile(path: string): Promise<void>;
|
|
86
|
+
requestInstall(id: string): void;
|
|
87
|
+
cancelInstall(): void;
|
|
88
|
+
confirmInstall(): Promise<void>;
|
|
89
|
+
uninstall(id: string): Promise<void>;
|
|
90
|
+
dismissDisclaimer(): void;
|
|
91
|
+
dismissNotice(): void;
|
|
92
|
+
}
|
|
93
|
+
/** The business face one `main` registration injects into the panel component. */
|
|
94
|
+
export interface MarketPageInjected {
|
|
95
|
+
useMarketController: () => MarketController;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Create one panel controller.
|
|
99
|
+
*
|
|
100
|
+
* `useMarketController` calls this per mounted panel; `createMarketController` is
|
|
101
|
+
* also the entry point for tests, which is why the factory performs no I/O — the
|
|
102
|
+
* first request only happens when the panel asks for it.
|
|
103
|
+
*/
|
|
104
|
+
export declare function createMarketController(): MarketController;
|
|
105
|
+
/**
|
|
106
|
+
* React binding.
|
|
107
|
+
*
|
|
108
|
+
* `useSyncExternalStore` is the whole subscription: React owns the subscribe /
|
|
109
|
+
* unsubscribe pairing, so an unmount cannot leak a listener, and the initializer
|
|
110
|
+
* runs at most twice under StrictMode's development double-invocation (the extra
|
|
111
|
+
* controller is discarded and holds no resources — no request, timer or
|
|
112
|
+
* subscription exists until the panel asks for one).
|
|
113
|
+
*
|
|
114
|
+
* There is deliberately no unmount disposer: a StrictMode cleanup would tear down
|
|
115
|
+
* the controller React is still using, and a late response writing into this
|
|
116
|
+
* store is inert — nothing but the snapshot identity of an external store
|
|
117
|
+
* changes, so no React state update happens after unmount.
|
|
118
|
+
*/
|
|
119
|
+
export declare function useMarketController(): MarketController;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Hub configuration.
|
|
3
|
+
*
|
|
4
|
+
* The schema below is the plugin's whole configuration surface: DSH validates
|
|
5
|
+
* the profile's plugin row against it and hands `apply()` the resolved value.
|
|
6
|
+
* Every field carries a default, so a row written as `{}` — or an `apply()`
|
|
7
|
+
* called directly from a test — still boots the plugin with the shipped
|
|
8
|
+
* behaviour instead of failing on a missing key.
|
|
9
|
+
*
|
|
10
|
+
* `skillsRoot: null` means "the user skills directory DSH itself scans"
|
|
11
|
+
* (`$DSH_HOME/skills`); a string is an explicit override for tests, for
|
|
12
|
+
* throwaway profiles, or for a deployment that keeps skills outside the
|
|
13
|
+
* harness home. The reference implementation hard-coded its own
|
|
14
|
+
* `~/.claude/skills`, which is exactly the coupling this field removes.
|
|
15
|
+
*/
|
|
16
|
+
import z from '@deepseek-ai/schemastery';
|
|
17
|
+
export interface SkillsHubConfig {
|
|
18
|
+
/** Absolute skills directory to install into; `null` = the DSH user skills directory. */
|
|
19
|
+
skillsRoot: string | null;
|
|
20
|
+
/** ClawHub base URL (default `https://clawhub.ai`). */
|
|
21
|
+
clawhubBaseUrl: string;
|
|
22
|
+
/** SkillHub base URL (default `https://api.skillhub.cn`). */
|
|
23
|
+
skillhubBaseUrl: string;
|
|
24
|
+
/** Per-request upstream timeout in milliseconds. */
|
|
25
|
+
timeoutMs: number;
|
|
26
|
+
/** Extra upstream attempts after the first failure (network error, 429, 5xx). */
|
|
27
|
+
requestRetries: number;
|
|
28
|
+
/** Whether `POST /uninstall` may remove installed skills at all. */
|
|
29
|
+
allowUninstall: boolean;
|
|
30
|
+
/** Default `limit` for `GET /skills` when the request omits one (clamped to 1…100). */
|
|
31
|
+
pageSize: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Shipped defaults. Exported so `apply()` can fill in a partially resolved
|
|
35
|
+
* config when the plugin is mounted without going through the schema (unit
|
|
36
|
+
* tests, direct composition), without restating the literals in two places.
|
|
37
|
+
*/
|
|
38
|
+
export declare const SKILLS_HUB_DEFAULTS: SkillsHubConfig;
|
|
39
|
+
/**
|
|
40
|
+
* `null` must stay a legal value for `skillsRoot` (it is the "use the DSH
|
|
41
|
+
* default" marker), so the field is modelled as a union rather than as a
|
|
42
|
+
* nullable string: a plain `z.string().default('')` would make "unset" and
|
|
43
|
+
* "empty string" indistinguishable and would leak an empty path into `path.join`.
|
|
44
|
+
*
|
|
45
|
+
* Schemastery quirk worth knowing: its object resolver drops a key whose
|
|
46
|
+
* resolved value is nullable and which was absent from the input, so
|
|
47
|
+
* `Config({}).skillsRoot` is `undefined`, not `null`. Both mean "use the DSH
|
|
48
|
+
* default" and `apply()` normalizes either to `null` before using it, so a
|
|
49
|
+
* caller that bypasses the schema cannot change the behaviour.
|
|
50
|
+
*/
|
|
51
|
+
export declare const Config: z<SkillsHubConfig>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Hub — DeepSeek Harness host plugin.
|
|
3
|
+
*
|
|
4
|
+
* Adds the market half of the Skills Hub panel to a DSH profile:
|
|
5
|
+
*
|
|
6
|
+
* - `/api/skills-hub/*` (browse, search, detail, file preview, installed,
|
|
7
|
+
* install, uninstall) served from the composition's Web server;
|
|
8
|
+
* - installation into the DSH user skills directory, so an installed skill is
|
|
9
|
+
* immediately discoverable by the `skill` tool;
|
|
10
|
+
* - the installed-skill index the market layer asks for via
|
|
11
|
+
* `setInstalledLookup()`.
|
|
12
|
+
*
|
|
13
|
+
* Installation is deliberately not a realm feature: it is plain filesystem work
|
|
14
|
+
* plus HTTP, no agent session is involved, and the tools registry is untouched.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-skills-hub
|
|
17
|
+
*/
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
19
|
+
import { Config, type SkillsHubConfig } from './config.ts';
|
|
20
|
+
export declare const name = "skills-hub";
|
|
21
|
+
/**
|
|
22
|
+
* Required services.
|
|
23
|
+
*
|
|
24
|
+
* Empty on purpose, and the honest answer for this plugin: nothing must exist for
|
|
25
|
+
* it to *load*. The Web server is looked up lazily (`ctx.get`, both known keys)
|
|
26
|
+
* and bound whenever it appears, so a headless profile keeps the plugin inert
|
|
27
|
+
* instead of pending forever on a service key that composition never provides;
|
|
28
|
+
* the skill registry is optional (it only informs a post-install catalog read);
|
|
29
|
+
* and the market core is pure code with no service dependency at all. Injecting
|
|
30
|
+
* `'webServer'` here would make the plugin unusable in the very profiles where
|
|
31
|
+
* the harness would still boot it, and would break on a composition that spells
|
|
32
|
+
* the service `httpServer`.
|
|
33
|
+
*/
|
|
34
|
+
export declare const inject: string[];
|
|
35
|
+
export { Config };
|
|
36
|
+
/**
|
|
37
|
+
* Activate the plugin.
|
|
38
|
+
*
|
|
39
|
+
* @param ctx - host plugin context.
|
|
40
|
+
* @param config - resolved {@link SkillsHubConfig} (schema defaults already applied).
|
|
41
|
+
*/
|
|
42
|
+
export declare function apply(ctx: Context, config: SkillsHubConfig): void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Market — in-memory TTL cache with stale-while-error support, plus
|
|
3
|
+
* per-source health tracking.
|
|
4
|
+
*
|
|
5
|
+
* Only *upstream* data is cached here (list pages, details, file bodies).
|
|
6
|
+
* Install state is derived per request from the injected installed-skill index
|
|
7
|
+
* and is deliberately never cached: a skill installed a second ago must show up
|
|
8
|
+
* as installed on the very next list call.
|
|
9
|
+
*
|
|
10
|
+
* Everything lives in module scope, so the module needs no DSH service and no
|
|
11
|
+
* filesystem — a unit test can drive it with plain `node --test`.
|
|
12
|
+
*/
|
|
13
|
+
import type { MarketSource, SourceHealthStatus, SourceStatusInfo } from './types.ts';
|
|
14
|
+
/** Per-payload TTLs: cheap-to-refetch list pages expire sooner than file bodies. */
|
|
15
|
+
export declare const MARKET_TTL: {
|
|
16
|
+
readonly list: number;
|
|
17
|
+
readonly search: number;
|
|
18
|
+
readonly detail: number;
|
|
19
|
+
readonly files: number;
|
|
20
|
+
readonly fileContent: number;
|
|
21
|
+
};
|
|
22
|
+
declare class MarketCache {
|
|
23
|
+
private entries;
|
|
24
|
+
get<T>(key: string): T | undefined;
|
|
25
|
+
/** Returns the entry even when expired — used for the stale-while-error fallback. */
|
|
26
|
+
getStale<T>(key: string): {
|
|
27
|
+
value: T;
|
|
28
|
+
storedAt: number;
|
|
29
|
+
} | undefined;
|
|
30
|
+
set(key: string, value: unknown, ttlMs: number): void;
|
|
31
|
+
clear(): void;
|
|
32
|
+
}
|
|
33
|
+
export declare const marketCache: MarketCache;
|
|
34
|
+
/**
|
|
35
|
+
* Record the outcome of one upstream interaction.
|
|
36
|
+
*
|
|
37
|
+
* A `failed` report is downgraded to `degraded` when the source succeeded
|
|
38
|
+
* recently (it answered a moment ago, this request just went wrong) — repeated
|
|
39
|
+
* failures after the last success report `failed`. This mirrors the reference
|
|
40
|
+
* server's `recordSourceFailure`, which is what the UI's source bar was built
|
|
41
|
+
* around; callers that genuinely know better can pass `degraded` explicitly.
|
|
42
|
+
*/
|
|
43
|
+
export declare function markSourceHealth(source: MarketSource, status: SourceHealthStatus, error?: string): void;
|
|
44
|
+
export declare function getSourceHealth(source: MarketSource): SourceStatusInfo;
|
|
45
|
+
/** Test hook: forget every recorded success/failure. */
|
|
46
|
+
export declare function resetSourceHealth(): void;
|
|
47
|
+
/** Test hook: drop cached payloads and reset source health. */
|
|
48
|
+
export declare function resetMarketCache(): void;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawHub provider (https://clawhub.ai)
|
|
3
|
+
*
|
|
4
|
+
* Endpoints (verified against the live API):
|
|
5
|
+
* - GET /api/v1/skills?limit=&cursor=&sort=downloads → {items, nextCursor}
|
|
6
|
+
* - GET /api/v1/search?q= → {results} (no pagination)
|
|
7
|
+
* - GET /api/v1/skills/{slug} → {skill, latestVersion, owner, metadata, moderation}
|
|
8
|
+
* - GET /api/v1/skills/{slug}/versions/{v} → {version:{license, files[], security}}
|
|
9
|
+
* - GET /api/v1/skills/{slug}/file?path= → raw file text
|
|
10
|
+
*/
|
|
11
|
+
import { type MarketProvider } from './types.ts';
|
|
12
|
+
export type ParsedFrontmatter = {
|
|
13
|
+
frontmatter: Record<string, unknown>;
|
|
14
|
+
content: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Split `---` frontmatter from a markdown body.
|
|
18
|
+
*
|
|
19
|
+
* The reference server delegated YAML to Bun.YAML / the `yaml` package; this
|
|
20
|
+
* plugin must not add a runtime dependency, so `parseYamlSubset` below covers
|
|
21
|
+
* the subset SKILL.md frontmatter actually uses (scalars, quoted strings, flow
|
|
22
|
+
* collections, nested mappings, block sequences and block scalars) and any
|
|
23
|
+
* value it cannot understand is dropped rather than throwing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseFrontmatter(markdown: string): ParsedFrontmatter;
|
|
26
|
+
/** Test hook: forget which owner disambiguated an ambiguous slug. */
|
|
27
|
+
export declare function resetClawhubOwnerCache(): void;
|
|
28
|
+
export declare const clawhubProvider: MarketProvider;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Market — install / uninstall service.
|
|
3
|
+
*
|
|
4
|
+
* Install: fetch the whole file list, enforce `MARKET_LIMITS`, verify every
|
|
5
|
+
* provider-reported SHA-256, sanitize the slug into a directory name, stage the
|
|
6
|
+
* complete skill in a hidden sibling directory and publish it with a single
|
|
7
|
+
* `rename` — the skills root never contains a partially written skill, and a
|
|
8
|
+
* failure leaves nothing behind but the (removed) staging directory.
|
|
9
|
+
*
|
|
10
|
+
* Uninstall: only ever removes a directory that carries our provenance sidecar
|
|
11
|
+
* with an id matching the requested skill, and only inside the configured
|
|
12
|
+
* skills root. Every path is resolved and containment-checked before a delete,
|
|
13
|
+
* because "the plugin deleted something outside its own tree" is the one bug a
|
|
14
|
+
* skills market must not have.
|
|
15
|
+
*
|
|
16
|
+
* Ported from the reference `installService.ts` (Claude Code desktop), with the
|
|
17
|
+
* filesystem layout, the sidecar name and the concurrency key all frozen by the
|
|
18
|
+
* interface contract.
|
|
19
|
+
*/
|
|
20
|
+
import { type MarketSource, type NormalizedSkill } from './types.ts';
|
|
21
|
+
export interface InstallResult {
|
|
22
|
+
installedPath: string;
|
|
23
|
+
skill: NormalizedSkill;
|
|
24
|
+
}
|
|
25
|
+
export interface UninstallResult {
|
|
26
|
+
removedPath: string;
|
|
27
|
+
skill: NormalizedSkill;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One rejected install/uninstall carrying both the market error code and the
|
|
31
|
+
* HTTP status the route layer must answer with.
|
|
32
|
+
*
|
|
33
|
+
* The reference used its server-wide `ApiError`; this plugin owns no server
|
|
34
|
+
* middleware, so the code/status pair travels with the error instead of being
|
|
35
|
+
* re-derived from a message string at the edge. Upstream failures are *not*
|
|
36
|
+
* wrapped: W1's `MarketUpstreamError` already carries the code the route maps
|
|
37
|
+
* (`upstreamBadResponse` → 404, everything else → 502).
|
|
38
|
+
*/
|
|
39
|
+
export declare class MarketInstallError extends Error {
|
|
40
|
+
readonly code: string;
|
|
41
|
+
readonly status: number;
|
|
42
|
+
constructor(status: number, code: string, message: string);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Install one market skill into `options.skillsRoot`.
|
|
46
|
+
*
|
|
47
|
+
* @param source - market the skill comes from.
|
|
48
|
+
* @param slug - provider slug; also the directory name after sanitization.
|
|
49
|
+
* @param options - resolved skills root and the (unused here) uninstall switch,
|
|
50
|
+
* kept in the signature so both verbs take one shape.
|
|
51
|
+
* @returns the absolute installed path and the skill re-annotated as `installed`.
|
|
52
|
+
* @throws {MarketInstallError} for anything this plugin refuses itself.
|
|
53
|
+
* @throws {import('./types.ts').MarketUpstreamError} for upstream failures,
|
|
54
|
+
* unwrapped so the route can classify them (404 vs 502).
|
|
55
|
+
*/
|
|
56
|
+
export declare function installMarketSkill(source: MarketSource, slug: string, options: {
|
|
57
|
+
skillsRoot: string;
|
|
58
|
+
allowUninstall: boolean;
|
|
59
|
+
}): Promise<InstallResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Remove one skill this plugin installed.
|
|
62
|
+
*
|
|
63
|
+
* @param source - market the skill was installed from.
|
|
64
|
+
* @param slug - provider slug; the directory name after sanitization.
|
|
65
|
+
* @param options - resolved skills root and the `allowUninstall` switch.
|
|
66
|
+
* @returns the removed path and a best-effort refreshed skill record.
|
|
67
|
+
* @throws {MarketInstallError} when the switch is off, the skill is missing, or
|
|
68
|
+
* the directory is not provably ours.
|
|
69
|
+
*/
|
|
70
|
+
export declare function uninstallMarketSkill(source: MarketSource, slug: string, options: {
|
|
71
|
+
skillsRoot: string;
|
|
72
|
+
allowUninstall: boolean;
|
|
73
|
+
}): Promise<UninstallResult>;
|
|
74
|
+
/** Drop every install lock. Test hook, mirroring the reference implementation. */
|
|
75
|
+
export declare function resetInstallLocksForTests(): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Market — aggregation service (host half).
|
|
3
|
+
*
|
|
4
|
+
* Merges the two upstream providers into a single paginated feed with
|
|
5
|
+
* cross-source dedupe, per-source health/degradation reporting, TTL caching
|
|
6
|
+
* (stale-while-error) and locally-computed install state.
|
|
7
|
+
*
|
|
8
|
+
* The module is deliberately free of DSH services and of the filesystem: the
|
|
9
|
+
* "what is installed locally" question is answered by an injected
|
|
10
|
+
* `InstalledLookup` (see `setInstalledLookup`), so this file can be unit-tested
|
|
11
|
+
* by pointing the providers at a stub server.
|
|
12
|
+
*/
|
|
13
|
+
import { type MarketFileContent, type MarketListResult, type MarketSource, type NormalizedSkill, type NormalizedSkillDetail, type SecurityStatus, type SourceStatusInfo } from './types.ts';
|
|
14
|
+
/**
|
|
15
|
+
* Local install state, injected by the plugin host (which owns the skills
|
|
16
|
+
* directory). Both methods are synchronous: the host keeps an in-memory index
|
|
17
|
+
* and rescans it after install/uninstall.
|
|
18
|
+
*/
|
|
19
|
+
export interface InstalledLookup {
|
|
20
|
+
/** ids (`source:slug`) of skills already present in the local skills directory */
|
|
21
|
+
has(id: string): boolean;
|
|
22
|
+
info(id: string): {
|
|
23
|
+
dirName: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
installedAt?: string;
|
|
26
|
+
} | undefined;
|
|
27
|
+
}
|
|
28
|
+
export declare function setInstalledLookup(lookup: InstalledLookup): void;
|
|
29
|
+
/** Test hook: fall back to "nothing installed" (also used when a plugin unloads). */
|
|
30
|
+
export declare function resetInstalledLookup(): void;
|
|
31
|
+
type MergedCursor = Partial<Record<MarketSource, string>>;
|
|
32
|
+
export declare function encodeCursor(cursor: MergedCursor): string | null;
|
|
33
|
+
export declare function decodeCursor(raw: string | null | undefined): MergedCursor | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Stamp a market item with its local install state.
|
|
36
|
+
*
|
|
37
|
+
* Computed on every request (never cached): a skill installed a second ago must
|
|
38
|
+
* show up as installed on the next list call. The reference version also probed
|
|
39
|
+
* the skills directory for a name conflict; that check needs a filesystem, so it
|
|
40
|
+
* now belongs to the install path, which refuses to overwrite a directory it did
|
|
41
|
+
* not create.
|
|
42
|
+
*/
|
|
43
|
+
export declare function annotateInstallState<T extends NormalizedSkill>(skill: T): T;
|
|
44
|
+
/** File-level installability checks — only possible once the file list is known. */
|
|
45
|
+
export declare function applyFileLimits(detail: NormalizedSkillDetail): NormalizedSkillDetail;
|
|
46
|
+
/**
|
|
47
|
+
* SkillHub mirrors ClawHub skills (source='clawhub' + upstream_url). When a
|
|
48
|
+
* page contains both the mirror and the ClawHub original, merge them: the
|
|
49
|
+
* ClawHub entry wins (fresher data), enriched with SkillHub-only fields.
|
|
50
|
+
*
|
|
51
|
+
* Unlike the reference this does not mutate its input: those items come from
|
|
52
|
+
* the shared response cache, and mutating them made `mirrors` grow on every
|
|
53
|
+
* repeat request.
|
|
54
|
+
*/
|
|
55
|
+
export declare function dedupeSkills(items: NormalizedSkill[]): NormalizedSkill[];
|
|
56
|
+
export interface ListMarketSkillsParams {
|
|
57
|
+
q?: string;
|
|
58
|
+
source: 'all' | MarketSource;
|
|
59
|
+
security: 'all' | SecurityStatus;
|
|
60
|
+
installed: 'all' | 'installed' | 'installable';
|
|
61
|
+
cursor?: string;
|
|
62
|
+
limit: number;
|
|
63
|
+
}
|
|
64
|
+
export declare function listMarketSkills(params: ListMarketSkillsParams): Promise<MarketListResult>;
|
|
65
|
+
export declare function getMarketSkillDetail(source: MarketSource, slug: string): Promise<{
|
|
66
|
+
skill: NormalizedSkillDetail;
|
|
67
|
+
sourceStatus: SourceStatusInfo;
|
|
68
|
+
}>;
|
|
69
|
+
export declare function isValidMarketFilePath(filePath: string): boolean;
|
|
70
|
+
export declare function getMarketFileContent(source: MarketSource, slug: string, filePath: string): Promise<MarketFileContent>;
|
|
71
|
+
export declare function getMarketStatus(): Record<MarketSource, SourceStatusInfo>;
|
|
72
|
+
/** Look up a single skill (used by install) — the detail path, bypassing list. */
|
|
73
|
+
export declare function resolveMarketSkill(source: MarketSource, slug: string): Promise<NormalizedSkillDetail>;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Market — shared upstream fetch helper.
|
|
3
|
+
*
|
|
4
|
+
* Every upstream request goes through `providerFetch` so timeouts, retries,
|
|
5
|
+
* redirect following (SkillHub file downloads 302 to COS) and per-source health
|
|
6
|
+
* bookkeeping stay in one place.
|
|
7
|
+
*
|
|
8
|
+
* Base URLs, timeout and retry budget are injected with `configureProviderFetch`
|
|
9
|
+
* — the plugin passes its resolved Config in `apply()`. Nothing is read from
|
|
10
|
+
* `process.env` at module scope, so the module (and every provider built on it)
|
|
11
|
+
* is directly unit-testable by pointing the bases at a local stub.
|
|
12
|
+
*/
|
|
13
|
+
import { MarketUpstreamError, type MarketSource } from './types.ts';
|
|
14
|
+
export interface ProviderEndpointConfig {
|
|
15
|
+
clawhubBaseUrl: string;
|
|
16
|
+
skillhubBaseUrl: string;
|
|
17
|
+
timeoutMs: number;
|
|
18
|
+
retries: number;
|
|
19
|
+
userAgent: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Point the providers at different hosts / limits. Called by the plugin with the
|
|
23
|
+
* resolved Config; tests call it to aim the providers at a local stub server.
|
|
24
|
+
* Unspecified fields keep their current value, so a partial override can never
|
|
25
|
+
* silently blank the other source.
|
|
26
|
+
*/
|
|
27
|
+
export declare function configureProviderFetch(config: Partial<ProviderEndpointConfig>): void;
|
|
28
|
+
/** Base URL of one upstream, as resolved from the active configuration. */
|
|
29
|
+
export declare function getProviderBase(source: MarketSource): string;
|
|
30
|
+
/**
|
|
31
|
+
* An upstream answered with a non-2xx status (or a retryable 429/5xx).
|
|
32
|
+
*
|
|
33
|
+
* Extends `MarketUpstreamError` so the existing `instanceof MarketUpstreamError`
|
|
34
|
+
* handling (and its `code`) keeps working, and adds the HTTP `status` so the
|
|
35
|
+
* route layer can tell "skill not found" apart from "upstream is broken".
|
|
36
|
+
*/
|
|
37
|
+
export declare class MarketHttpError extends MarketUpstreamError {
|
|
38
|
+
readonly status: number;
|
|
39
|
+
constructor(source: MarketSource, status: number, code: string, message: string);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Fetch an upstream URL with timeout and retries.
|
|
43
|
+
*
|
|
44
|
+
* `retries` is the number of *extra* attempts (the contract's default of 1 means
|
|
45
|
+
* two attempts), matching the reference server. A 429 or 5xx counts as a failed
|
|
46
|
+
* attempt; every other status — including 404 — is returned to the caller so the
|
|
47
|
+
* provider can map it to a meaningful error.
|
|
48
|
+
*/
|
|
49
|
+
export declare function providerFetch(source: MarketSource, url: string, init?: RequestInit): Promise<Response>;
|
|
50
|
+
/** `providerFetch` plus JSON decoding and non-2xx/parse-failure classification. */
|
|
51
|
+
export declare function providerFetchJson<T>(source: MarketSource, url: string, init?: RequestInit): Promise<T>;
|
|
52
|
+
/**
|
|
53
|
+
* Read a response body as text, aborting as soon as it exceeds `maxBytes`.
|
|
54
|
+
*
|
|
55
|
+
* `content-length` is only a hint (redirects and chunked responses omit it), so
|
|
56
|
+
* the body is streamed and counted rather than trusted.
|
|
57
|
+
*/
|
|
58
|
+
export declare function readResponseTextWithLimit(source: MarketSource, response: Response, maxBytes: number, label: string): Promise<{
|
|
59
|
+
content: string;
|
|
60
|
+
size: number;
|
|
61
|
+
}>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillHub provider (https://api.skillhub.cn)
|
|
3
|
+
*
|
|
4
|
+
* Endpoints (verified against the live API):
|
|
5
|
+
* - GET /api/skills?page=&pageSize=&keyword= → {code, data:{skills[], total}, message}
|
|
6
|
+
* NOTE: pagination param MUST be `pageSize` (a `limit` param is silently ignored)
|
|
7
|
+
* NOTE: search param MUST be `keyword` (a `q` param is silently ignored)
|
|
8
|
+
* - GET /api/v1/skills/{slug} → {skill, owner, latestVersion, securityReports}
|
|
9
|
+
* - GET /api/v1/skills/{slug}/files → {count, files:[{path, sha256, size}]}
|
|
10
|
+
* - GET /api/v1/skills/{slug}/file?path= → 302 redirect to Tencent COS (follow)
|
|
11
|
+
*/
|
|
12
|
+
import { type MarketProvider } from './types.ts';
|
|
13
|
+
export declare const skillhubProvider: MarketProvider;
|