@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,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Market — shared types for the market aggregation layer.
|
|
3
|
+
*
|
|
4
|
+
* Upstream sources:
|
|
5
|
+
* - ClawHub (https://clawhub.ai) — cursor pagination, no security audits
|
|
6
|
+
* - SkillHub (https://api.skillhub.cn) — page/pageSize pagination, security reports
|
|
7
|
+
*/
|
|
8
|
+
export type MarketSource = 'clawhub' | 'skillhub';
|
|
9
|
+
export declare const MARKET_SOURCES: MarketSource[];
|
|
10
|
+
export type SecurityStatus = 'verified' | 'benign' | 'unknown' | 'flagged';
|
|
11
|
+
export type InstallState = 'installed' | 'installable' | 'not-installable';
|
|
12
|
+
export type SourceHealthStatus = 'ok' | 'degraded' | 'failed' | 'cached';
|
|
13
|
+
export type NotInstallableReason = 'empty-file-list' | 'file-too-large' | 'too-many-files' | 'invalid-name' | 'name-conflict' | 'source-unavailable';
|
|
14
|
+
export type SecurityReport = {
|
|
15
|
+
vendor: string;
|
|
16
|
+
status: string;
|
|
17
|
+
statusText: string;
|
|
18
|
+
reportUrl?: string;
|
|
19
|
+
};
|
|
20
|
+
export type NormalizedSkill = {
|
|
21
|
+
/** `${source}:${slug}` — globally unique */
|
|
22
|
+
id: string;
|
|
23
|
+
source: MarketSource;
|
|
24
|
+
slug: string;
|
|
25
|
+
name: string;
|
|
26
|
+
summary: string;
|
|
27
|
+
author: {
|
|
28
|
+
handle: string;
|
|
29
|
+
displayName?: string;
|
|
30
|
+
avatarUrl?: string;
|
|
31
|
+
};
|
|
32
|
+
stats: {
|
|
33
|
+
downloads: number;
|
|
34
|
+
installs?: number;
|
|
35
|
+
stars?: number;
|
|
36
|
+
};
|
|
37
|
+
tags: string[];
|
|
38
|
+
category?: string;
|
|
39
|
+
version?: string;
|
|
40
|
+
updatedAt?: number;
|
|
41
|
+
iconUrl?: string;
|
|
42
|
+
securityStatus: SecurityStatus;
|
|
43
|
+
securityReports?: SecurityReport[];
|
|
44
|
+
requiresApiKey?: boolean;
|
|
45
|
+
verified?: boolean;
|
|
46
|
+
/** Set on SkillHub entries that mirror a ClawHub skill */
|
|
47
|
+
upstream?: {
|
|
48
|
+
source: MarketSource;
|
|
49
|
+
slug: string;
|
|
50
|
+
};
|
|
51
|
+
/** After dedupe: ids of merged duplicate entries from other sources */
|
|
52
|
+
mirrors?: string[];
|
|
53
|
+
installState: InstallState;
|
|
54
|
+
notInstallableReason?: NotInstallableReason;
|
|
55
|
+
installedInfo?: {
|
|
56
|
+
version?: string;
|
|
57
|
+
installedAt?: string;
|
|
58
|
+
dirName: string;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
export type MarketFileMeta = {
|
|
62
|
+
path: string;
|
|
63
|
+
size: number;
|
|
64
|
+
sha256?: string;
|
|
65
|
+
contentType?: string;
|
|
66
|
+
language: string;
|
|
67
|
+
/** File exceeds the preview/install size limit */
|
|
68
|
+
tooBig: boolean;
|
|
69
|
+
};
|
|
70
|
+
export type NormalizedSkillDetail = NormalizedSkill & {
|
|
71
|
+
/** Full SKILL.md body (markdown, frontmatter stripped) */
|
|
72
|
+
description: string;
|
|
73
|
+
/** Raw frontmatter parsed from SKILL.md description, when present */
|
|
74
|
+
descriptionFrontmatter?: Record<string, unknown>;
|
|
75
|
+
license?: string;
|
|
76
|
+
files: MarketFileMeta[];
|
|
77
|
+
totalSize: number;
|
|
78
|
+
};
|
|
79
|
+
export type MarketFileContent = {
|
|
80
|
+
path: string;
|
|
81
|
+
content: string;
|
|
82
|
+
language: string;
|
|
83
|
+
size: number;
|
|
84
|
+
truncated: boolean;
|
|
85
|
+
};
|
|
86
|
+
export type SourceStatusInfo = {
|
|
87
|
+
status: SourceHealthStatus;
|
|
88
|
+
fetchedAt?: number;
|
|
89
|
+
fromCache?: boolean;
|
|
90
|
+
error?: string;
|
|
91
|
+
};
|
|
92
|
+
export type MarketListResult = {
|
|
93
|
+
items: NormalizedSkill[];
|
|
94
|
+
nextCursor: string | null;
|
|
95
|
+
sources: Record<MarketSource, SourceStatusInfo>;
|
|
96
|
+
};
|
|
97
|
+
export type ProviderListPage = {
|
|
98
|
+
items: NormalizedSkill[];
|
|
99
|
+
/** Provider-native cursor for the next page; undefined = exhausted */
|
|
100
|
+
nextCursor?: string;
|
|
101
|
+
total?: number;
|
|
102
|
+
};
|
|
103
|
+
export type ProviderFileEntry = {
|
|
104
|
+
path: string;
|
|
105
|
+
size: number;
|
|
106
|
+
sha256?: string;
|
|
107
|
+
contentType?: string;
|
|
108
|
+
};
|
|
109
|
+
export interface MarketProvider {
|
|
110
|
+
readonly source: MarketSource;
|
|
111
|
+
list(params: {
|
|
112
|
+
cursor?: string;
|
|
113
|
+
limit: number;
|
|
114
|
+
}): Promise<ProviderListPage>;
|
|
115
|
+
search(params: {
|
|
116
|
+
q: string;
|
|
117
|
+
cursor?: string;
|
|
118
|
+
limit: number;
|
|
119
|
+
}): Promise<ProviderListPage>;
|
|
120
|
+
detail(slug: string): Promise<NormalizedSkillDetail>;
|
|
121
|
+
listFiles(slug: string, version?: string): Promise<ProviderFileEntry[]>;
|
|
122
|
+
fetchFile(slug: string, filePath: string): Promise<{
|
|
123
|
+
content: string;
|
|
124
|
+
size: number;
|
|
125
|
+
}>;
|
|
126
|
+
}
|
|
127
|
+
export declare const MARKET_ERROR_CODES: {
|
|
128
|
+
readonly upstreamError: "MARKET_UPSTREAM_ERROR";
|
|
129
|
+
readonly upstreamTimeout: "MARKET_UPSTREAM_TIMEOUT";
|
|
130
|
+
readonly upstreamBadResponse: "MARKET_UPSTREAM_BAD_RESPONSE";
|
|
131
|
+
readonly installInProgress: "MARKET_INSTALL_IN_PROGRESS";
|
|
132
|
+
readonly alreadyInstalled: "MARKET_ALREADY_INSTALLED";
|
|
133
|
+
readonly notInstallable: "MARKET_NOT_INSTALLABLE";
|
|
134
|
+
readonly checksumMismatch: "MARKET_CHECKSUM_MISMATCH";
|
|
135
|
+
readonly diskError: "MARKET_DISK_ERROR";
|
|
136
|
+
readonly notInstalled: "MARKET_NOT_INSTALLED";
|
|
137
|
+
readonly notManaged: "MARKET_NOT_MANAGED";
|
|
138
|
+
};
|
|
139
|
+
export declare class MarketUpstreamError extends Error {
|
|
140
|
+
source: MarketSource;
|
|
141
|
+
code: string;
|
|
142
|
+
constructor(source: MarketSource, code: string, message: string);
|
|
143
|
+
}
|
|
144
|
+
export declare const MARKET_LIMITS: {
|
|
145
|
+
/** Max bytes for a single skill file (install + preview) */
|
|
146
|
+
readonly maxFileSize: number;
|
|
147
|
+
/** Max total bytes for an installable skill */
|
|
148
|
+
readonly maxTotalSize: number;
|
|
149
|
+
/** Max file count for an installable skill */
|
|
150
|
+
readonly maxFileCount: 200;
|
|
151
|
+
/** File preview content is truncated beyond this many bytes */
|
|
152
|
+
readonly previewTruncateBytes: number;
|
|
153
|
+
/** ClawHub search has no pagination — cap merged search results */
|
|
154
|
+
readonly searchResultCap: 50;
|
|
155
|
+
};
|
|
156
|
+
export declare function skillId(source: MarketSource, slug: string): string;
|
|
157
|
+
export declare function parseSkillId(id: string): {
|
|
158
|
+
source: MarketSource;
|
|
159
|
+
slug: string;
|
|
160
|
+
} | null;
|
|
161
|
+
/** Directory-name whitelist: lowercase alnum, dash, underscore, dot (no leading dot). */
|
|
162
|
+
export declare function sanitizeDirName(slug: string): string | null;
|
|
163
|
+
export declare function detectMarketLanguage(filename: string): string;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installed-skill index.
|
|
3
|
+
*
|
|
4
|
+
* This module is the filesystem truth behind two things the market layer cannot
|
|
5
|
+
* know by itself:
|
|
6
|
+
*
|
|
7
|
+
* - `InstalledLookup` — which market ids (`source:slug`) are already present
|
|
8
|
+
* locally, so the list/detail responses can report `installState` and the
|
|
9
|
+
* installer can refuse to clobber a directory it does not own.
|
|
10
|
+
* - `GET /api/skills-hub/installed` — the panel's "installed" filter, including
|
|
11
|
+
* skills the user dropped into the skills directory by hand.
|
|
12
|
+
*
|
|
13
|
+
* A directory is *managed* when it carries the provenance sidecar
|
|
14
|
+
* ({@link INSTALL_META_FILE}) written by the installer. Nothing else in the tree
|
|
15
|
+
* is overwritten by the market installer; local entries can be explicitly
|
|
16
|
+
* removed through the installed-management endpoint after user confirmation.
|
|
17
|
+
*/
|
|
18
|
+
import type { InstalledLookup } from '../market/market-service.ts';
|
|
19
|
+
import { type MarketSource } from '../market/types.ts';
|
|
20
|
+
/** Provenance sidecar written next to `SKILL.md` by the installer. */
|
|
21
|
+
export declare const INSTALL_META_FILE = ".skills-hub.json";
|
|
22
|
+
/**
|
|
23
|
+
* Parsed {@link INSTALL_META_FILE} content.
|
|
24
|
+
*
|
|
25
|
+
* `files` is the ledger of the installed bytes (path + SHA-256) used for
|
|
26
|
+
* integrity reporting; removal decisions are made from the identity fields
|
|
27
|
+
* only, so a sidecar with a damaged ledger still proves ownership.
|
|
28
|
+
*/
|
|
29
|
+
export interface InstalledMetaFile {
|
|
30
|
+
id: string;
|
|
31
|
+
source: MarketSource;
|
|
32
|
+
slug: string;
|
|
33
|
+
version?: string;
|
|
34
|
+
installedAt: string;
|
|
35
|
+
files: Array<{
|
|
36
|
+
path: string;
|
|
37
|
+
sha256: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
export interface InstalledSkillRecord {
|
|
41
|
+
/** Opaque identity of this exact filesystem entry (duplicates remain addressable). */
|
|
42
|
+
key: string;
|
|
43
|
+
/** Symlink removal only unlinks this entry, never its target. */
|
|
44
|
+
linked: boolean;
|
|
45
|
+
removable: boolean;
|
|
46
|
+
/** `source:slug` when the sidecar provenance file exists, else `local:<dir>` */
|
|
47
|
+
id: string;
|
|
48
|
+
source: MarketSource | 'local';
|
|
49
|
+
slug: string;
|
|
50
|
+
name: string;
|
|
51
|
+
dirName: string;
|
|
52
|
+
/**
|
|
53
|
+
* Absolute path of the skill entry: its directory for a directory bundle,
|
|
54
|
+
* the Markdown file itself for a flat `<name>.md` skill (both layouts are
|
|
55
|
+
* discovered by the DSH filesystem skill provider).
|
|
56
|
+
*/
|
|
57
|
+
dirPath: string;
|
|
58
|
+
version?: string;
|
|
59
|
+
summary?: string;
|
|
60
|
+
installedAt?: string;
|
|
61
|
+
/** installed by this plugin (sidecar `.skills-hub.json` present) */
|
|
62
|
+
managed: boolean;
|
|
63
|
+
bytes: number;
|
|
64
|
+
fileCount: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate and normalize a sidecar payload.
|
|
68
|
+
*
|
|
69
|
+
* The identity fields are checked against each other (`id` must be
|
|
70
|
+
* `skillId(source, slug)`) because a sidecar that disagrees with itself cannot
|
|
71
|
+
* authorize a delete: the reference implementation's "not managed" branch
|
|
72
|
+
* exists for exactly this class of tampering, and the same check is reused on
|
|
73
|
+
* the uninstall path.
|
|
74
|
+
*
|
|
75
|
+
* @param value - parsed JSON of unknown shape.
|
|
76
|
+
* @returns the normalized sidecar, or `null` when it does not prove ownership.
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseInstalledMeta(value: unknown): InstalledMetaFile | null;
|
|
79
|
+
/**
|
|
80
|
+
* Scan every root for installed skills, most specific root first.
|
|
81
|
+
*
|
|
82
|
+
* Each filesystem entry remains individually addressable for management; the
|
|
83
|
+
* market lookup separately gives earlier roots precedence. Dot-prefixed entries are skipped — DSH's own
|
|
84
|
+
* filesystem provider skips `.system` in the user-dsh root, and the installer's
|
|
85
|
+
* staging/trash directories are dot-prefixed so no scanner ever sees a
|
|
86
|
+
* half-published skill.
|
|
87
|
+
*
|
|
88
|
+
* @param roots - absolute skill roots, most specific first (see `resolveSkillsScanRoots`).
|
|
89
|
+
* @returns one record per filesystem entry, in scan order.
|
|
90
|
+
*/
|
|
91
|
+
export declare function scanInstalledSkills(roots: string[]): Promise<InstalledSkillRecord[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Read just the display identity of a skill file.
|
|
94
|
+
*
|
|
95
|
+
* Used by the uninstall path, which must describe a skill after its directory is
|
|
96
|
+
* gone: the frontmatter has to be captured while the file still exists, and it
|
|
97
|
+
* must not fail the removal when it cannot be read.
|
|
98
|
+
*
|
|
99
|
+
* @param skillFile - absolute path of a `SKILL.md`.
|
|
100
|
+
* @returns `name`/`summary` as far as they could be read, or `undefined`.
|
|
101
|
+
*/
|
|
102
|
+
export declare function readSkillHeadline(skillFile: string): Promise<{
|
|
103
|
+
name?: string;
|
|
104
|
+
summary?: string;
|
|
105
|
+
} | undefined>;
|
|
106
|
+
/**
|
|
107
|
+
* Project scanned records into the market layer's installed-id seam.
|
|
108
|
+
*
|
|
109
|
+
* The lookup is a pure function of the records so the caller can rebuild it
|
|
110
|
+
* after an install/uninstall without any shared mutable state.
|
|
111
|
+
*
|
|
112
|
+
* @param records - output of {@link scanInstalledSkills}.
|
|
113
|
+
* @returns the lookup W1's `setInstalledLookup()` expects.
|
|
114
|
+
*/
|
|
115
|
+
export declare function installedLookupFrom(records: InstalledSkillRecord[]): InstalledLookup;
|
|
116
|
+
/** Path-derived identifiers never expose a caller-controlled filesystem path. */
|
|
117
|
+
export declare function installedEntryKey(entryPath: string): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type InstalledSkillRecord } from './installed.ts';
|
|
2
|
+
export declare function readInstalledSkill(roots: string[], key: string): Promise<{
|
|
3
|
+
item: InstalledSkillRecord;
|
|
4
|
+
markdown: string;
|
|
5
|
+
}>;
|
|
6
|
+
/** Rename the selected entry out of discovery before deletion. Symlink targets are never removed. */
|
|
7
|
+
export declare function removeInstalledSkill(roots: string[], key: string, allowUninstall: boolean): Promise<InstalledSkillRecord>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills-directory resolution.
|
|
3
|
+
*
|
|
4
|
+
* Two different questions are answered here, and they must not be conflated:
|
|
5
|
+
*
|
|
6
|
+
* - {@link resolveSkillsRoot} — *where do we install?* Exactly one directory:
|
|
7
|
+
* the configured override, or the user skills directory of the harness home.
|
|
8
|
+
* - {@link resolveSkillsScanRoots} — *what does "already installed" mean?*
|
|
9
|
+
* Every directory the local filesystem skill provider scans for user-level
|
|
10
|
+
* skills, most specific first, so a skill that is already present anywhere in
|
|
11
|
+
* the DSH user skill roots is reported as installed instead of being
|
|
12
|
+
* downloaded and clobbered.
|
|
13
|
+
*
|
|
14
|
+
* `$DSH_HOME` is resolved here instead of through `@deepseek-ai/dsh-home-paths`
|
|
15
|
+
* on purpose: that package is a dev-only dependency of this plugin (it is not in
|
|
16
|
+
* `peerDependencies`), so a runtime value import would make the published plugin
|
|
17
|
+
* fail to load in a profile where it is not hoisted. The resolution rules below
|
|
18
|
+
* are the documented ones from
|
|
19
|
+
* `@deepseek-ai/dsh-home-paths/lib/types/index.d.ts` (0.1.7-rc.2):
|
|
20
|
+
* explicit path → `DSH_HOME` (blank treated as unset) → `~/.dsh`, plus the
|
|
21
|
+
* `~`, `~/`, `~\` tilde expansion of `expandHomePath`.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Absolute skills directory skills are installed into, created on demand.
|
|
25
|
+
*
|
|
26
|
+
* A relative configured path is resolved against the host process's working
|
|
27
|
+
* directory: a skills root must be absolute before any containment check can
|
|
28
|
+
* mean anything, and refusing relative input outright would break the common
|
|
29
|
+
* `./tmp-skills` test setup.
|
|
30
|
+
*
|
|
31
|
+
* @param configured - `Config.skillsRoot`; `null`/blank selects the DSH user skills directory.
|
|
32
|
+
* @returns the absolute, existing skills directory.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveSkillsRoot(configured: string | null): Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Every directory DSH will scan for skills, most specific first (installed lookup).
|
|
37
|
+
*
|
|
38
|
+
* Order is the install root, then the two user roots the local filesystem skill
|
|
39
|
+
* provider mounts by default (`$DSH_HOME/skills` with `user-dsh` rank, then
|
|
40
|
+
* `$DSH_AGENTS_HOME/skills` with `user-agents` rank — see
|
|
41
|
+
* `@deepseek-ai/dsh-skill-filesystem/lib/index.js#roots`). Project roots
|
|
42
|
+
* (`.dsh/skills`, `.agents/skills` under a session's cwd) are deliberately
|
|
43
|
+
* absent: this plugin runs at host scope, has no session, and must not claim
|
|
44
|
+
* that a skill in whichever directory the server happened to start in is
|
|
45
|
+
* "installed" for every session. Duplicates (a configured root that *is* the
|
|
46
|
+
* harness home) collapse to their first, most specific occurrence.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveSkillsScanRoots(configured: string | null): Promise<string[]>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Hub HTTP surface.
|
|
3
|
+
*
|
|
4
|
+
* One `prefix` route (`/api/skills-hub`) owns the seven endpoints the panel
|
|
5
|
+
* needs; every response is JSON with `Cache-Control: no-store`, because the
|
|
6
|
+
* panel polls and every payload is either live upstream data or live disk state.
|
|
7
|
+
*
|
|
8
|
+
* Two properties matter more than the routing itself:
|
|
9
|
+
*
|
|
10
|
+
* - **Nothing escapes as a non-JSON error.** The handler catches its own
|
|
11
|
+
* rejections, the web server's own catch-all would answer a thrown handler
|
|
12
|
+
* with a bare `400` and no body, and the browser client parses the
|
|
13
|
+
* `{ error: { code, message } }` envelope.
|
|
14
|
+
* - **The browser-trust gate runs first.** Raw `webServer` routes do not inherit
|
|
15
|
+
* the Connection service's Host/Origin fence, so a route registered here would
|
|
16
|
+
* otherwise be reachable by a cross-site request or a DNS-rebound page.
|
|
17
|
+
*/
|
|
18
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
19
|
+
/** Path prefix of the whole Skills Hub API. */
|
|
20
|
+
export declare const ROUTE_PREFIX = "/api/skills-hub";
|
|
21
|
+
/** Structural Web server contract (mirrors `WebServer.register`). */
|
|
22
|
+
export interface WebServerLike {
|
|
23
|
+
register(route: {
|
|
24
|
+
kind: 'exact' | 'prefix';
|
|
25
|
+
path: string;
|
|
26
|
+
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
|
27
|
+
}): () => void;
|
|
28
|
+
}
|
|
29
|
+
export interface SkillsHubRoutesDeps {
|
|
30
|
+
skillsRoot: () => Promise<string>;
|
|
31
|
+
allowUninstall: () => boolean;
|
|
32
|
+
rescan: () => Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Browser-trust gate, structurally equal to the frozen contract's predicate.
|
|
36
|
+
*
|
|
37
|
+
* The real `HostConnectionService.requestRejection` returns `401 | 403 |
|
|
38
|
+
* undefined` rather than a boolean (see
|
|
39
|
+
* `@deepseek-ai/dsh-client-connection/lib/types/rpc.d.ts`), so this plugin
|
|
40
|
+
* accepts both shapes: see {@link evaluateGate} for why the numeric form is
|
|
41
|
+
* worth preserving.
|
|
42
|
+
*/
|
|
43
|
+
export interface SkillsHubRequestGate {
|
|
44
|
+
requestRejection(req: IncomingMessage, res: ServerResponse): boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolves the gate for one request.
|
|
48
|
+
*
|
|
49
|
+
* A getter, not the service itself: Loader rows activate concurrently, so the
|
|
50
|
+
* Connection service frequently appears *after* this plugin registered its
|
|
51
|
+
* route. Snapshotting it at registration time silently produced an unfenced
|
|
52
|
+
* marketplace (verified against a real host: official `/api/*` routes answered
|
|
53
|
+
* `403` to an untrusted `Origin` while `/api/skills-hub/*` answered `200`).
|
|
54
|
+
*/
|
|
55
|
+
export type SkillsHubGateSource = () => SkillsHubRequestGate | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Set the default `/skills` page size from `Config.pageSize`.
|
|
58
|
+
*
|
|
59
|
+
* The frozen `SkillsHubRoutesDeps` has no config seat, and widening it would
|
|
60
|
+
* change a frozen interface; this setter is the additive seam instead.
|
|
61
|
+
*/
|
|
62
|
+
export declare function setSkillsHubPageSize(pageSize: number): void;
|
|
63
|
+
/**
|
|
64
|
+
* Register the Skills Hub prefix route.
|
|
65
|
+
*
|
|
66
|
+
* @param webServer - the `webServer`/`httpServer` service (`ctx.get` guarded by the caller).
|
|
67
|
+
* @param gate - resolves the browser-trust gate (`ctx.get('connection')`) *per
|
|
68
|
+
* request*; `undefined` means the composition has no Connection fence to
|
|
69
|
+
* consult. It is a function rather than a snapshot because the Connection row
|
|
70
|
+
* may activate after this route is registered.
|
|
71
|
+
* @param deps - resolved skills root, the uninstall switch, and the index refresh.
|
|
72
|
+
* @returns the route disposer (yield it from `ctx.effect`).
|
|
73
|
+
*/
|
|
74
|
+
export declare function registerSkillsHubRoutes(webServer: WebServerLike, gate: SkillsHubGateSource, deps: SkillsHubRoutesDeps): () => void;
|