@nanmicoder/dsh-skills-hub 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +18 -0
- package/README.md +31 -82
- package/README_EN.md +31 -82
- package/README_ZH.md +2 -0
- package/compatibility.json +1 -1
- package/docs/COMPATIBILITY.md +34 -0
- package/docs/CONFIGURATION.md +38 -0
- package/docs/CONTRACT.md +25 -7
- package/docs/RELEASE.md +5 -5
- package/lib/client.js +1916 -1025
- package/lib/client.js.map +1 -1
- package/lib/config.js +2 -0
- package/lib/index.js +16 -6
- package/lib/market/cache.js +104 -30
- package/lib/market/disk-cache.js +113 -0
- package/lib/market/install-service.js +8 -6
- package/lib/market/market-service.js +79 -29
- package/lib/market/provider-fetch.js +9 -2
- package/lib/market/types.js +2 -1
- package/lib/skills/management.js +182 -4
- package/lib/skills/root.js +1 -1
- package/lib/types/client/api.d.ts +33 -5
- package/lib/types/client/components/InstallConfirmDialog.d.ts +6 -0
- package/lib/types/client/components/InstalledSkillDetail.d.ts +24 -0
- package/lib/types/client/components/InstalledSkills.d.ts +18 -1
- package/lib/types/client/components/MarkdownView.d.ts +7 -1
- package/lib/types/client/components/SkillAvatar.d.ts +1 -1
- package/lib/types/client/components/SkillDetailShell.d.ts +63 -0
- package/lib/types/client/components/SkillDetailView.d.ts +4 -6
- package/lib/types/client/components/SkillFiles.d.ts +32 -0
- package/lib/types/client/components/SourceStatusBar.d.ts +4 -0
- package/lib/types/client/icons.d.ts +4 -0
- package/lib/types/client/locales.d.ts +30 -0
- package/lib/types/client/relative-time.d.ts +13 -0
- package/lib/types/client/skill-markdown.d.ts +39 -0
- package/lib/types/client/state.d.ts +13 -4
- package/lib/types/config.d.ts +2 -0
- package/lib/types/market/cache.d.ts +62 -14
- package/lib/types/market/disk-cache.d.ts +16 -0
- package/lib/types/market/market-service.d.ts +15 -3
- package/lib/types/market/provider-fetch.d.ts +2 -0
- package/lib/types/skills/management.d.ts +63 -2
- package/lib/types/skills/root.d.ts +8 -0
- package/lib/types/web-routes.d.ts +2 -0
- package/lib/web-routes.js +42 -4
- package/package.json +1 -1
- package/release-notes/v0.0.1.md +1 -1
- package/release-notes/v0.0.2.md +12 -0
package/lib/types/config.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface SkillsHubConfig {
|
|
|
29
29
|
allowUninstall: boolean;
|
|
30
30
|
/** Default `limit` for `GET /skills` when the request omits one (clamped to 1…100). */
|
|
31
31
|
pageSize: number;
|
|
32
|
+
/** Lifetime of persisted market snapshots in minutes (1…1440). */
|
|
33
|
+
cacheTtlMinutes: number;
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* Shipped defaults. Exported so `apply()` can fill in a partially resolved
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skills Market —
|
|
2
|
+
* Skills Market — memory + persistent TTL cache with stale-while-error support, plus
|
|
3
3
|
* per-source health tracking.
|
|
4
4
|
*
|
|
5
5
|
* Only *upstream* data is cached here (list pages, details, file bodies).
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* and is deliberately never cached: a skill installed a second ago must show up
|
|
8
8
|
* as installed on the very next list call.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The host configures disk storage at activation. Without a directory the
|
|
11
|
+
* cache stays memory-only, so provider tests never touch a user's Harness home.
|
|
12
12
|
*/
|
|
13
13
|
import type { MarketSource, SourceHealthStatus, SourceStatusInfo } from './types.ts';
|
|
14
|
-
/**
|
|
14
|
+
/** Default snapshot lifetime; activation can override it with cacheTtlMinutes. */
|
|
15
15
|
export declare const MARKET_TTL: {
|
|
16
16
|
readonly list: number;
|
|
17
17
|
readonly search: number;
|
|
@@ -19,18 +19,67 @@ export declare const MARKET_TTL: {
|
|
|
19
19
|
readonly files: number;
|
|
20
20
|
readonly fileContent: number;
|
|
21
21
|
};
|
|
22
|
-
|
|
22
|
+
export interface MarketCacheOptions {
|
|
23
|
+
directory?: string;
|
|
24
|
+
ttlMs?: number;
|
|
25
|
+
onError?: (error: unknown) => void;
|
|
26
|
+
}
|
|
27
|
+
/** One fresh hit, together with the moment its payload was actually fetched. */
|
|
28
|
+
export interface MarketCacheRecord<T> {
|
|
29
|
+
value: T;
|
|
30
|
+
/** Epoch millis of the upstream read this payload came from. */
|
|
31
|
+
storedAt: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Counters describing what the cache actually did.
|
|
35
|
+
*
|
|
36
|
+
* "How much traffic does the cache save" used to be an argument. Every claim
|
|
37
|
+
* about hits, misses and needless upstream reads can now be checked against
|
|
38
|
+
* these numbers on a real machine instead of reasoned about.
|
|
39
|
+
*/
|
|
40
|
+
export interface MarketCacheStats {
|
|
41
|
+
/** Answers served from a fresh memory or disk entry. */
|
|
42
|
+
hits: number;
|
|
43
|
+
/** Lookups that had to go upstream (including forced refreshes). */
|
|
44
|
+
misses: number;
|
|
45
|
+
/** Answers served from an expired entry because the source failed. */
|
|
46
|
+
staleServed: number;
|
|
47
|
+
/** Upstream page/detail requests actually issued. */
|
|
48
|
+
upstreamRequests: number;
|
|
49
|
+
/** Refreshes a reader explicitly asked for. */
|
|
50
|
+
forcedRefreshes: number;
|
|
51
|
+
}
|
|
52
|
+
/** Record one cache event. Kept tiny so it can sit on the request path. */
|
|
53
|
+
export declare function noteMarketStat(entry: keyof MarketCacheStats): void;
|
|
54
|
+
/** Snapshot of the counters; the caller cannot mutate the live object. */
|
|
55
|
+
export declare function getMarketStats(): MarketCacheStats;
|
|
56
|
+
export declare class MarketCache {
|
|
57
|
+
private options;
|
|
23
58
|
private entries;
|
|
24
|
-
|
|
59
|
+
private disk?;
|
|
60
|
+
private warned;
|
|
61
|
+
private ttlMs;
|
|
62
|
+
constructor(options?: MarketCacheOptions);
|
|
63
|
+
private warn;
|
|
64
|
+
private remember;
|
|
65
|
+
private read;
|
|
66
|
+
/**
|
|
67
|
+
* Read a fresh entry.
|
|
68
|
+
*
|
|
69
|
+
* The record carries `storedAt` on purpose: a hit is a *snapshot*, and the
|
|
70
|
+
* caller must be able to say how old it is. Returning only the payload is how
|
|
71
|
+
* the panel ended up reporting "fetched just now" for data it had not touched
|
|
72
|
+
* in ten minutes.
|
|
73
|
+
*/
|
|
74
|
+
getRecord<T>(key: string): Promise<MarketCacheRecord<T> | undefined>;
|
|
25
75
|
/** Returns the entry even when expired — used for the stale-while-error fallback. */
|
|
26
|
-
getStale<T>(key: string):
|
|
27
|
-
|
|
28
|
-
storedAt: number;
|
|
29
|
-
} | undefined;
|
|
30
|
-
set(key: string, value: unknown, ttlMs: number): void;
|
|
76
|
+
getStale<T>(key: string): Promise<MarketCacheRecord<T> | undefined>;
|
|
77
|
+
set(key: string, value: unknown, ttlMs?: number): Promise<number>;
|
|
31
78
|
clear(): void;
|
|
32
79
|
}
|
|
33
|
-
export declare
|
|
80
|
+
export declare let marketCache: MarketCache;
|
|
81
|
+
/** Replace memory state when activating a profile; disk entries are read lazily. */
|
|
82
|
+
export declare function configureMarketCache(options: MarketCacheOptions): void;
|
|
34
83
|
/**
|
|
35
84
|
* Record the outcome of one upstream interaction.
|
|
36
85
|
*
|
|
@@ -44,6 +93,5 @@ export declare function markSourceHealth(source: MarketSource, status: SourceHea
|
|
|
44
93
|
export declare function getSourceHealth(source: MarketSource): SourceStatusInfo;
|
|
45
94
|
/** Test hook: forget every recorded success/failure. */
|
|
46
95
|
export declare function resetSourceHealth(): void;
|
|
47
|
-
/** Test hook: drop
|
|
96
|
+
/** Test hook: drop memory payloads, reset source health and the counters. */
|
|
48
97
|
export declare function resetMarketCache(): void;
|
|
49
|
-
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface CacheEntry {
|
|
2
|
+
value: unknown;
|
|
3
|
+
expiresAt: number;
|
|
4
|
+
storedAt: number;
|
|
5
|
+
}
|
|
6
|
+
export declare const MAX_CACHE_ENTRIES = 500;
|
|
7
|
+
export declare const MAX_STALE_AGE_MS: number;
|
|
8
|
+
export declare class DiskMarketCache {
|
|
9
|
+
private directory;
|
|
10
|
+
private writes;
|
|
11
|
+
constructor(directory: string);
|
|
12
|
+
private filename;
|
|
13
|
+
read(key: string): Promise<CacheEntry | undefined>;
|
|
14
|
+
write(key: string, entry: CacheEntry): Promise<void>;
|
|
15
|
+
private prune;
|
|
16
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* cross-source dedupe, per-source health/degradation reporting, TTL caching
|
|
6
6
|
* (stale-while-error) and locally-computed install state.
|
|
7
7
|
*
|
|
8
|
-
* The module is deliberately free of DSH services
|
|
8
|
+
* The module is deliberately free of DSH services: the
|
|
9
9
|
* "what is installed locally" question is answered by an injected
|
|
10
10
|
* `InstalledLookup` (see `setInstalledLookup`), so this file can be unit-tested
|
|
11
11
|
* by pointing the providers at a stub server.
|
|
@@ -60,15 +60,27 @@ export interface ListMarketSkillsParams {
|
|
|
60
60
|
installed: 'all' | 'installed' | 'installable';
|
|
61
61
|
cursor?: string;
|
|
62
62
|
limit: number;
|
|
63
|
+
/** Reader-requested refresh: ignore fresh entries (they are still rewritten). */
|
|
64
|
+
refresh?: boolean;
|
|
63
65
|
}
|
|
64
66
|
export declare function listMarketSkills(params: ListMarketSkillsParams): Promise<MarketListResult>;
|
|
65
|
-
export declare function getMarketSkillDetail(source: MarketSource, slug: string
|
|
67
|
+
export declare function getMarketSkillDetail(source: MarketSource, slug: string, options?: {
|
|
68
|
+
force?: boolean;
|
|
69
|
+
allowStale?: boolean;
|
|
70
|
+
}): Promise<{
|
|
66
71
|
skill: NormalizedSkillDetail;
|
|
67
72
|
sourceStatus: SourceStatusInfo;
|
|
68
73
|
}>;
|
|
69
74
|
export declare function isValidMarketFilePath(filePath: string): boolean;
|
|
70
75
|
export declare function getMarketFileContent(source: MarketSource, slug: string, filePath: string): Promise<MarketFileContent>;
|
|
71
76
|
export declare function getMarketStatus(): Record<MarketSource, SourceStatusInfo>;
|
|
72
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Look up a single skill for an install (the detail path, bypassing the list).
|
|
79
|
+
*
|
|
80
|
+
* Always forced: a cached manifest is a snapshot of the *catalogue*, and
|
|
81
|
+
* installing is the one action that has to be authorized by what upstream says
|
|
82
|
+
* right now — the file bytes are fetched fresh and hash-verified against the
|
|
83
|
+
* list, so the list must come from the same generation as those bytes.
|
|
84
|
+
*/
|
|
73
85
|
export declare function resolveMarketSkill(source: MarketSource, slug: string): Promise<NormalizedSkillDetail>;
|
|
74
86
|
export {};
|
|
@@ -18,6 +18,8 @@ export interface ProviderEndpointConfig {
|
|
|
18
18
|
retries: number;
|
|
19
19
|
userAgent: string;
|
|
20
20
|
}
|
|
21
|
+
/** Keep cache keys and all chained upstream reads on one configuration snapshot. */
|
|
22
|
+
export declare function withProviderConfiguration<T>(operation: () => Promise<T>): Promise<T>;
|
|
21
23
|
/**
|
|
22
24
|
* Point the providers at different hosts / limits. Called by the plugin with the
|
|
23
25
|
* resolved Config; tests call it to aim the providers at a local stub server.
|
|
@@ -1,7 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local skill management: one discovered entry at a time.
|
|
3
|
+
*
|
|
4
|
+
* Every function here starts from a key, never from a client-supplied path: the
|
|
5
|
+
* roots are re-scanned on each call, and only an entry that scan just produced
|
|
6
|
+
* can be read or removed. A path that arrives from the browser is therefore
|
|
7
|
+
* always interpreted *inside* an entry this process has already vouched for.
|
|
8
|
+
*
|
|
9
|
+
* The detail read is what the panel's local skill page renders: the SKILL.md
|
|
10
|
+
* document, its raw frontmatter block and a bounded inventory of the skill's own
|
|
11
|
+
* files. Nothing here consults a market provider — an installed skill stays
|
|
12
|
+
* readable with the network down.
|
|
13
|
+
*/
|
|
1
14
|
import { type InstalledSkillRecord } from './installed.ts';
|
|
2
|
-
|
|
15
|
+
/** One file of an installed skill, relative to the entry. */
|
|
16
|
+
export interface InstalledFileEntry {
|
|
17
|
+
path: string;
|
|
18
|
+
size: number;
|
|
19
|
+
language: string;
|
|
20
|
+
}
|
|
21
|
+
/** `GET /installed/detail` body. */
|
|
22
|
+
export interface InstalledSkillDetail {
|
|
3
23
|
item: InstalledSkillRecord;
|
|
24
|
+
/** Full SKILL.md text, frontmatter included: the browser strips it. */
|
|
4
25
|
markdown: string;
|
|
5
|
-
|
|
26
|
+
/** Verbatim YAML header without its fences, or `null` when the file has none. */
|
|
27
|
+
frontmatter: string | null;
|
|
28
|
+
files: InstalledFileEntry[];
|
|
29
|
+
}
|
|
30
|
+
/** `GET /installed/file` body; the shape mirrors the market file preview. */
|
|
31
|
+
export interface InstalledFileContent {
|
|
32
|
+
path: string;
|
|
33
|
+
content: string;
|
|
34
|
+
language: string;
|
|
35
|
+
size: number;
|
|
36
|
+
truncated: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Raw frontmatter block, kept as YAML text.
|
|
40
|
+
*
|
|
41
|
+
* Parsing it into structure would mean shipping a YAML reader for a panel that
|
|
42
|
+
* only needs to *show* the header, and the market detail's structured panel is
|
|
43
|
+
* fed by upstream JSON this plugin does not have for a hand-written skill. The
|
|
44
|
+
* verbatim block is also what the file actually contains, so nothing is lost.
|
|
45
|
+
*/
|
|
46
|
+
export declare function extractFrontmatter(markdown: string): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Inventory one entry's own files.
|
|
49
|
+
*
|
|
50
|
+
* The walk never leaves the entry: directories are descended, symlinked entries
|
|
51
|
+
* are listed as nothing at all, and the provenance sidecar (a dot-file, like
|
|
52
|
+
* every other dot-entry) is skipped because it is plugin bookkeeping rather than
|
|
53
|
+
* installed payload. A flat skill is a single file and reports itself.
|
|
54
|
+
*/
|
|
55
|
+
export declare function listEntryFiles(item: InstalledSkillRecord): Promise<InstalledFileEntry[]>;
|
|
56
|
+
/** Read the SKILL.md document plus the entry inventory the local detail page renders. */
|
|
57
|
+
export declare function readInstalledSkill(roots: string[], key: string): Promise<InstalledSkillDetail>;
|
|
58
|
+
/**
|
|
59
|
+
* Read one file of one entry.
|
|
60
|
+
*
|
|
61
|
+
* A flat skill exposes exactly itself — never its siblings in the same root —
|
|
62
|
+
* and a directory bundle exposes only what resolves back inside its own real
|
|
63
|
+
* path, so a symlink planted in the tree cannot turn this endpoint into an
|
|
64
|
+
* arbitrary-file reader.
|
|
65
|
+
*/
|
|
66
|
+
export declare function readInstalledFile(roots: string[], key: string, requested: string): Promise<InstalledFileContent>;
|
|
6
67
|
/** Rename the selected entry out of discovery before deletion. Symlink targets are never removed. */
|
|
7
68
|
export declare function removeInstalledSkill(roots: string[], key: string, allowUninstall: boolean): Promise<InstalledSkillRecord>;
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
* explicit path → `DSH_HOME` (blank treated as unset) → `~/.dsh`, plus the
|
|
21
21
|
* `~`, `~/`, `~\` tilde expansion of `expandHomePath`.
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the single-root harness home.
|
|
25
|
+
*
|
|
26
|
+
* Precedence, highest first: an explicit path, `$DSH_HOME`, then `~/.dsh`. A
|
|
27
|
+
* blank `$DSH_HOME` is treated as unset so a stray empty variable can never
|
|
28
|
+
* resolve the harness home to the current working directory.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveDshHome(configured?: string | null): string;
|
|
23
31
|
/**
|
|
24
32
|
* Absolute skills directory skills are installed into, created on demand.
|
|
25
33
|
*
|
|
@@ -30,6 +30,8 @@ export interface SkillsHubRoutesDeps {
|
|
|
30
30
|
skillsRoot: () => Promise<string>;
|
|
31
31
|
allowUninstall: () => boolean;
|
|
32
32
|
rescan: () => Promise<void>;
|
|
33
|
+
/** Initial local inventory scan, required before annotating persisted data. */
|
|
34
|
+
ready?: () => Promise<void>;
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
37
|
* Browser-trust gate, structurally equal to the frozen contract's predicate.
|
package/lib/web-routes.js
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { installMarketSkill, MarketInstallError, uninstallMarketSkill, } from "./market/install-service.js";
|
|
19
19
|
import { getMarketFileContent, getMarketSkillDetail, getMarketStatus, isValidMarketFilePath, listMarketSkills, } from "./market/market-service.js";
|
|
20
|
-
import {
|
|
20
|
+
import { getMarketStats } from "./market/cache.js";
|
|
21
|
+
import { readInstalledFile, readInstalledSkill, removeInstalledSkill } from "./skills/management.js";
|
|
21
22
|
import { scanInstalledSkills } from "./skills/installed.js";
|
|
22
23
|
import { resolveSkillsScanRoots } from "./skills/root.js";
|
|
23
24
|
import { MARKET_ERROR_CODES, MARKET_SOURCES, MarketUpstreamError, parseSkillId, } from "./market/types.js";
|
|
@@ -265,9 +266,30 @@ function parseBodyId(body) {
|
|
|
265
266
|
throw new RouteError(400, 'BAD_REQUEST', `Invalid skill id: ${id === '' ? '(missing)' : id}`);
|
|
266
267
|
return parsed;
|
|
267
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* `refresh` query parameter.
|
|
271
|
+
*
|
|
272
|
+
* A reader who presses refresh means "do not answer me from your cache". Before
|
|
273
|
+
* this existed the request looked exactly like the automatic one, so the panel
|
|
274
|
+
* re-rendered the same cached page and presented it as fresh. Only the explicit
|
|
275
|
+
* spellings are accepted; anything else is a bad request rather than a silently
|
|
276
|
+
* ignored intent.
|
|
277
|
+
*/
|
|
278
|
+
function parseRefresh(url) {
|
|
279
|
+
const raw = url.searchParams.get('refresh');
|
|
280
|
+
if (raw === null || raw === '' || raw === '0' || raw === 'false')
|
|
281
|
+
return false;
|
|
282
|
+
if (raw === '1' || raw === 'true')
|
|
283
|
+
return true;
|
|
284
|
+
throw new RouteError(400, 'BAD_REQUEST', `Invalid refresh flag: ${raw}`);
|
|
285
|
+
}
|
|
268
286
|
async function handleStatus(res) {
|
|
269
287
|
sendJson(res, 200, { sources: getMarketStatus() });
|
|
270
288
|
}
|
|
289
|
+
/** Cache and upstream counters: the evidence behind every traffic claim. */
|
|
290
|
+
async function handleStats(res) {
|
|
291
|
+
sendJson(res, 200, { stats: getMarketStats() });
|
|
292
|
+
}
|
|
271
293
|
async function handleList(url, res) {
|
|
272
294
|
const params = {
|
|
273
295
|
q: optionalQuery(url, 'q', MAX_QUERY_LENGTH),
|
|
@@ -276,12 +298,13 @@ async function handleList(url, res) {
|
|
|
276
298
|
installed: enumQuery(url, 'installed', INSTALLED_FILTERS, 'all'),
|
|
277
299
|
cursor: optionalQuery(url, 'cursor', MAX_CURSOR_LENGTH),
|
|
278
300
|
limit: parseLimit(url),
|
|
301
|
+
refresh: parseRefresh(url),
|
|
279
302
|
};
|
|
280
303
|
const result = await listMarketSkills(params);
|
|
281
304
|
sendJson(res, 200, result);
|
|
282
305
|
}
|
|
283
|
-
async function handleDetail(source, slug, res) {
|
|
284
|
-
sendJson(res, 200, await getMarketSkillDetail(source, slug));
|
|
306
|
+
async function handleDetail(source, slug, url, res) {
|
|
307
|
+
sendJson(res, 200, await getMarketSkillDetail(source, slug, { force: parseRefresh(url) }));
|
|
285
308
|
}
|
|
286
309
|
async function handleFile(source, slug, url, res) {
|
|
287
310
|
const filePath = url.searchParams.get('path') ?? '';
|
|
@@ -335,6 +358,7 @@ async function handle(req, res, gate, deps) {
|
|
|
335
358
|
return;
|
|
336
359
|
}
|
|
337
360
|
}
|
|
361
|
+
await deps.ready?.();
|
|
338
362
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
339
363
|
const segments = parseSegments(url.pathname);
|
|
340
364
|
const method = req.method ?? 'GET';
|
|
@@ -344,12 +368,26 @@ async function handle(req, res, gate, deps) {
|
|
|
344
368
|
await handleStatus(res);
|
|
345
369
|
return;
|
|
346
370
|
}
|
|
371
|
+
if (head === 'stats' && segments.length === 1) {
|
|
372
|
+
requireMethod(method, 'GET');
|
|
373
|
+
await handleStats(res);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
347
376
|
if (head === 'installed' && segments.length === 2 && second === 'detail') {
|
|
348
377
|
requireMethod(method, 'GET');
|
|
349
378
|
const roots = await resolveSkillsScanRoots(await deps.skillsRoot());
|
|
350
379
|
sendJson(res, 200, await readInstalledSkill(roots, url.searchParams.get('key') ?? ''));
|
|
351
380
|
return;
|
|
352
381
|
}
|
|
382
|
+
if (head === 'installed' && segments.length === 2 && second === 'file') {
|
|
383
|
+
requireMethod(method, 'GET');
|
|
384
|
+
const roots = await resolveSkillsScanRoots(await deps.skillsRoot());
|
|
385
|
+
// Both parameters are validated inside: a key must resolve to a freshly
|
|
386
|
+
// discovered entry, and the path must stay inside that entry.
|
|
387
|
+
const file = await readInstalledFile(roots, url.searchParams.get('key') ?? '', url.searchParams.get('path') ?? '');
|
|
388
|
+
sendJson(res, 200, { file });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
353
391
|
if (head === 'installed' && segments.length === 2 && second === 'uninstall') {
|
|
354
392
|
requireMethod(method, 'POST');
|
|
355
393
|
const body = await readJsonBody(req);
|
|
@@ -387,7 +425,7 @@ async function handle(req, res, gate, deps) {
|
|
|
387
425
|
const slug = parseSlug(third);
|
|
388
426
|
requireMethod(method, 'GET');
|
|
389
427
|
if (segments.length === 3) {
|
|
390
|
-
await handleDetail(source, slug, res);
|
|
428
|
+
await handleDetail(source, slug, url, res);
|
|
391
429
|
}
|
|
392
430
|
else {
|
|
393
431
|
await handleFile(source, slug, url, res);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanmicoder/dsh-skills-hub",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Skills Hub for DeepSeek Harness: browse, preview, install and remove third-party skills from ClawHub and SkillHub inside the DSH Web GUI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/release-notes/v0.0.1.md
CHANGED
|
@@ -8,4 +8,4 @@ Initial release of `@nanmicoder/dsh-skills-hub` for DeepSeek Harness's 0.2.0 des
|
|
|
8
8
|
- Atomic installation, existing-directory conflict protection and restricted filesystem removal.
|
|
9
9
|
- Scoped npm package, bilingual documentation, pinned compatibility metadata and reproducible build/package/release checks.
|
|
10
10
|
|
|
11
|
-
Install
|
|
11
|
+
Install in **DeepSeek Harness desktop → Plugins → Add plugin**. Paste `https://github.com/NanmiCoder/dsh-skills-hub` (recommended) or `@nanmicoder/dsh-skills-hub`, click **Install**, then **Enable now**. Open **Skills Hub** from the sidebar. Installing the plugin does not migrate or delete existing skills.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Skills Hub 0.0.2
|
|
2
|
+
|
|
3
|
+
`@nanmicoder/dsh-skills-hub` for DeepSeek Harness **0.2.0-rc.2**.
|
|
4
|
+
|
|
5
|
+
- Persist browsed catalogue pages, searches, skill details and file previews across desktop restarts. The default cache lifetime is 60 minutes, configurable through `cacheTtlMinutes`.
|
|
6
|
+
- Preserve snapshot timestamps and each page's provenance. Reopening a detail checks the Host's cache lifetime and current local installation state.
|
|
7
|
+
- Keep installation manifests fresh: actual installs bypass cached details and refuse stale fallback when upstream fails.
|
|
8
|
+
- Bound and validate disk snapshots, recover abandoned temporary writes, and keep provider configurations isolated during reloads.
|
|
9
|
+
- Show installed skills in a full detail page, with rendered Markdown previews, structured frontmatter and a source view for file contents.
|
|
10
|
+
- Prefetch the next catalogue page near the end of the current view, and show snapshot age with an explicit refresh action.
|
|
11
|
+
|
|
12
|
+
Install in **DeepSeek Harness desktop → Plugins → Add plugin** using `@nanmicoder/dsh-skills-hub@0.0.2`, then enable the plugin. Existing skills remain in place. Advanced cache settings are documented in [CONFIGURATION.md](../docs/CONFIGURATION.md).
|