@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.
Files changed (67) hide show
  1. package/AGENTS.md +9 -0
  2. package/CONTRIBUTING.md +11 -0
  3. package/LICENSE +21 -0
  4. package/README.md +120 -0
  5. package/README_EN.md +120 -0
  6. package/README_ZH.md +5 -0
  7. package/assets/readme/hero-en.png +0 -0
  8. package/assets/readme/hero-zh.png +0 -0
  9. package/assets/readme/marketplace.png +0 -0
  10. package/compatibility.json +10 -0
  11. package/cordis.patch.yml +5 -0
  12. package/docs/COMPATIBILITY.md +31 -0
  13. package/docs/CONTRACT.md +125 -0
  14. package/docs/RELEASE.md +24 -0
  15. package/lib/client.js +25115 -0
  16. package/lib/client.js.map +1 -0
  17. package/lib/config.js +51 -0
  18. package/lib/index.js +176 -0
  19. package/lib/market/cache.js +113 -0
  20. package/lib/market/clawhub-provider.js +598 -0
  21. package/lib/market/install-service.js +394 -0
  22. package/lib/market/market-service.js +312 -0
  23. package/lib/market/provider-fetch.js +216 -0
  24. package/lib/market/skillhub-provider.js +277 -0
  25. package/lib/market/types.js +82 -0
  26. package/lib/skills/installed.js +426 -0
  27. package/lib/skills/management.js +36 -0
  28. package/lib/skills/read-text.js +23 -0
  29. package/lib/skills/root.js +112 -0
  30. package/lib/types/client/api.d.ts +106 -0
  31. package/lib/types/client/components/FilterBar.d.ts +20 -0
  32. package/lib/types/client/components/FrontmatterPanel.d.ts +15 -0
  33. package/lib/types/client/components/InstallConfirmDialog.d.ts +31 -0
  34. package/lib/types/client/components/InstallStateBadge.d.ts +11 -0
  35. package/lib/types/client/components/InstalledSkills.d.ts +3 -0
  36. package/lib/types/client/components/MarkdownView.d.ts +4 -0
  37. package/lib/types/client/components/MarketDisclaimer.d.ts +10 -0
  38. package/lib/types/client/components/MarketHome.d.ts +12 -0
  39. package/lib/types/client/components/SecurityBadge.d.ts +16 -0
  40. package/lib/types/client/components/SkillAvatar.d.ts +14 -0
  41. package/lib/types/client/components/SkillCard.d.ts +17 -0
  42. package/lib/types/client/components/SkillDetailView.d.ts +21 -0
  43. package/lib/types/client/components/SourceStatusBar.d.ts +13 -0
  44. package/lib/types/client/icons.d.ts +71 -0
  45. package/lib/types/client/index.d.ts +56 -0
  46. package/lib/types/client/locale-context.d.ts +31 -0
  47. package/lib/types/client/locales.d.ts +117 -0
  48. package/lib/types/client/page/MarketPage.d.ts +21 -0
  49. package/lib/types/client/skill-markdown.d.ts +2 -0
  50. package/lib/types/client/state.d.ts +119 -0
  51. package/lib/types/config.d.ts +51 -0
  52. package/lib/types/index.d.ts +42 -0
  53. package/lib/types/market/cache.d.ts +49 -0
  54. package/lib/types/market/clawhub-provider.d.ts +28 -0
  55. package/lib/types/market/install-service.d.ts +75 -0
  56. package/lib/types/market/market-service.d.ts +74 -0
  57. package/lib/types/market/provider-fetch.d.ts +61 -0
  58. package/lib/types/market/skillhub-provider.d.ts +13 -0
  59. package/lib/types/market/types.d.ts +163 -0
  60. package/lib/types/skills/installed.d.ts +117 -0
  61. package/lib/types/skills/management.d.ts +7 -0
  62. package/lib/types/skills/read-text.d.ts +5 -0
  63. package/lib/types/skills/root.d.ts +48 -0
  64. package/lib/types/web-routes.d.ts +74 -0
  65. package/lib/web-routes.js +427 -0
  66. package/package.json +160 -0
  67. 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;