@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,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,5 @@
1
+ /** Read at most limit + 1 bytes; reject special files and detect growth without an unbounded allocation. */
2
+ export declare function readBoundedText(filePath: string, limit: number): Promise<{
3
+ text: string;
4
+ truncated: boolean;
5
+ }>;
@@ -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;