@czj-git/dsh-plugin-hub 0.1.0

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/dist/api.js ADDED
@@ -0,0 +1,243 @@
1
+ import { z } from 'zod';
2
+ /** Locales supported by the public DSH Plugin Hub API. */
3
+ export const pluginHubLocales = ['zh', 'en'];
4
+ /** Public plugin categories exposed by DSH Plugin Hub. */
5
+ export const pluginHubCategories = [
6
+ 'agent-workflow',
7
+ 'coding-tools',
8
+ 'models-data',
9
+ 'ui-experience',
10
+ 'integrations',
11
+ 'security-governance',
12
+ 'multimodal-creative',
13
+ 'observability-cost',
14
+ 'other',
15
+ ];
16
+ /** Plugin runtime shapes supported by the public API. */
17
+ export const pluginHubTypes = ['host', 'client', 'hybrid'];
18
+ /** Install sources supported by the public API. */
19
+ export const pluginHubSources = ['npm', 'github'];
20
+ /** Ordering modes supported by the public API. */
21
+ export const pluginHubSorts = ['relevance', 'growth', 'stars', 'newest', 'active'];
22
+ const nullableInteger = z.number().int().nullable();
23
+ const pluginSchema = z.object({
24
+ id: z.string(),
25
+ slug: z.string(),
26
+ name: z.string(),
27
+ owner: z.string(),
28
+ repo: z.string(),
29
+ description: z.string(),
30
+ type: z.enum(pluginHubTypes),
31
+ category: z.enum(pluginHubCategories),
32
+ topics: z.array(z.string()),
33
+ language: z.string(),
34
+ license: z.string(),
35
+ package: z.object({
36
+ name: z.string(),
37
+ version: z.string(),
38
+ source: z.enum(pluginHubSources),
39
+ sourceSpec: z.string(),
40
+ installCommand: z.string(),
41
+ profile: z.string(),
42
+ }),
43
+ compatibility: z.object({
44
+ harnessVersion: z.string(),
45
+ verificationLevel: z.enum(['static-checked', 'runtime-verified']),
46
+ smokeStatus: z.enum(['static-passed', 'passed', 'manual-step-required', 'failed', 'not-run']),
47
+ validatedAt: z.string(),
48
+ }),
49
+ metrics: z.object({
50
+ stars: z.number().int(),
51
+ starsDelta1d: nullableInteger,
52
+ forks: z.number().int(),
53
+ openIssues: z.number().int(),
54
+ views: z.number().int(),
55
+ }),
56
+ timestamps: z.object({
57
+ listedAt: z.string(),
58
+ lastPushedAt: z.string(),
59
+ sourceUpdatedAt: z.string(),
60
+ }),
61
+ links: z.object({
62
+ detail: z.url(),
63
+ repository: z.url(),
64
+ }),
65
+ });
66
+ const responseSchema = z.object({
67
+ items: z.array(pluginSchema),
68
+ pagination: z.object({
69
+ page: z.number().int(),
70
+ perPage: z.number().int(),
71
+ total: z.number().int(),
72
+ totalPages: z.number().int(),
73
+ }),
74
+ meta: z.object({
75
+ apiVersion: z.literal('v1'),
76
+ locale: z.enum(pluginHubLocales),
77
+ query: z.string(),
78
+ sort: z.enum(pluginHubSorts),
79
+ dataUpdatedAt: z.string(),
80
+ }),
81
+ });
82
+ const errorSchema = z.object({
83
+ error: z.object({
84
+ code: z.string(),
85
+ message: z.string(),
86
+ fields: z.record(z.string(), z.string()).optional(),
87
+ }),
88
+ });
89
+ /** An HTTP or response-validation failure returned by DSH Plugin Hub. */
90
+ export class PluginHubApiError extends Error {
91
+ code;
92
+ status;
93
+ fields;
94
+ retryAfterSeconds;
95
+ /**
96
+ * Create one stable public-API failure.
97
+ * @param message - Human-readable failure.
98
+ * @param options - Structured HTTP and API diagnostics.
99
+ */
100
+ constructor(message, options) {
101
+ super(message, { cause: options.cause });
102
+ this.name = 'PluginHubApiError';
103
+ this.code = options.code;
104
+ this.status = options.status ?? null;
105
+ this.fields = Object.freeze({ ...(options.fields ?? {}) });
106
+ this.retryAfterSeconds = options.retryAfterSeconds ?? null;
107
+ }
108
+ }
109
+ /**
110
+ * Validate and canonicalize the configured service origin.
111
+ * @param raw - User-configured base URL.
112
+ * @returns An HTTP(S) origin without a trailing slash.
113
+ */
114
+ export function normalizePluginHubBaseUrl(raw) {
115
+ const url = new URL(raw);
116
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') {
117
+ throw new Error('baseUrl must use http or https');
118
+ }
119
+ if (url.username || url.password || url.search || url.hash) {
120
+ throw new Error('baseUrl must not contain credentials, a query, or a fragment');
121
+ }
122
+ url.pathname = url.pathname.replace(/\/+$/, '');
123
+ return url.toString().replace(/\/$/, '');
124
+ }
125
+ function positiveInteger(name, value, maximum) {
126
+ if (!Number.isInteger(value) || value < 1 || value > maximum) {
127
+ throw new Error(`${name} must be an integer from 1 to ${maximum}`);
128
+ }
129
+ return value;
130
+ }
131
+ function optionalHeaderInteger(headers, name) {
132
+ const raw = headers.get(name);
133
+ if (raw === null || raw.trim() === '')
134
+ return null;
135
+ const value = Number(raw);
136
+ return Number.isSafeInteger(value) && value >= 0 ? value : null;
137
+ }
138
+ function rateLimitFrom(headers) {
139
+ return {
140
+ limit: optionalHeaderInteger(headers, 'RateLimit-Limit'),
141
+ remaining: optionalHeaderInteger(headers, 'RateLimit-Remaining'),
142
+ reset: headers.get('RateLimit-Reset'),
143
+ retryAfterSeconds: optionalHeaderInteger(headers, 'Retry-After'),
144
+ };
145
+ }
146
+ function normalizeQuery(query) {
147
+ const normalized = query?.trim().replace(/\s+/g, ' ') ?? '';
148
+ if (normalized.length > 100)
149
+ throw new Error('query must contain at most 100 characters');
150
+ return normalized;
151
+ }
152
+ /** Read-only client for the versioned DSH Plugin Hub search endpoint. */
153
+ export class PluginHubClient {
154
+ #baseUrl;
155
+ #timeoutMs;
156
+ #fetch;
157
+ /**
158
+ * Create a client with one deployment's network policy.
159
+ * @param options - Service origin, request timeout, and optional test transport.
160
+ */
161
+ constructor(options) {
162
+ this.#baseUrl = normalizePluginHubBaseUrl(options.baseUrl);
163
+ this.#timeoutMs = positiveInteger('timeoutMs', options.timeoutMs, 120_000);
164
+ this.#fetch = options.fetch ?? globalThis.fetch;
165
+ }
166
+ /**
167
+ * Search published, verified plugins.
168
+ * @param input - Public search filters and page controls.
169
+ * @param signal - Caller-owned cancellation signal.
170
+ * @returns Validated API data plus anonymous quota headers.
171
+ */
172
+ async search(input, signal) {
173
+ const url = new URL('/api/v1/plugins/search', this.#baseUrl);
174
+ const query = normalizeQuery(input.query);
175
+ if (query)
176
+ url.searchParams.set('q', query);
177
+ if (input.locale)
178
+ url.searchParams.set('locale', input.locale);
179
+ if (input.category)
180
+ url.searchParams.set('category', input.category);
181
+ if (input.type)
182
+ url.searchParams.set('type', input.type);
183
+ if (input.source)
184
+ url.searchParams.set('source', input.source);
185
+ if (input.sort)
186
+ url.searchParams.set('sort', input.sort);
187
+ url.searchParams.set('page', String(positiveInteger('page', input.page ?? 1, 1000)));
188
+ url.searchParams.set('per_page', String(positiveInteger('perPage', input.perPage ?? 10, 50)));
189
+ const timeout = AbortSignal.timeout(this.#timeoutMs);
190
+ const requestSignal = AbortSignal.any([signal, timeout]);
191
+ let response;
192
+ try {
193
+ response = await this.#fetch(url, {
194
+ method: 'GET',
195
+ headers: { Accept: 'application/json' },
196
+ signal: requestSignal,
197
+ });
198
+ }
199
+ catch (error) {
200
+ const message = signal.aborted
201
+ ? 'DSH Plugin Hub request was cancelled.'
202
+ : timeout.aborted
203
+ ? `DSH Plugin Hub request timed out after ${this.#timeoutMs} ms.`
204
+ : 'Could not reach DSH Plugin Hub.';
205
+ throw new PluginHubApiError(message, { code: signal.aborted ? 'CANCELLED' : timeout.aborted ? 'TIMEOUT' : 'NETWORK_ERROR', cause: error });
206
+ }
207
+ const rateLimit = rateLimitFrom(response.headers);
208
+ let payload;
209
+ try {
210
+ payload = await response.json();
211
+ }
212
+ catch (error) {
213
+ throw new PluginHubApiError('DSH Plugin Hub returned non-JSON data.', {
214
+ code: 'INVALID_RESPONSE',
215
+ status: response.status,
216
+ cause: error,
217
+ });
218
+ }
219
+ if (!response.ok) {
220
+ const parsed = errorSchema.safeParse(payload);
221
+ const code = parsed.success ? parsed.data.error.code : `HTTP_${response.status}`;
222
+ const detail = parsed.success ? parsed.data.error.message : `HTTP ${response.status}`;
223
+ throw new PluginHubApiError(`DSH Plugin Hub request failed: ${detail}`, {
224
+ code,
225
+ status: response.status,
226
+ ...(parsed.success && parsed.data.error.fields
227
+ ? { fields: parsed.data.error.fields }
228
+ : {}),
229
+ retryAfterSeconds: rateLimit.retryAfterSeconds,
230
+ });
231
+ }
232
+ const parsed = responseSchema.safeParse(payload);
233
+ if (!parsed.success) {
234
+ throw new PluginHubApiError('DSH Plugin Hub returned an unsupported response.', {
235
+ code: 'INVALID_RESPONSE',
236
+ status: response.status,
237
+ cause: parsed.error,
238
+ });
239
+ }
240
+ return { ...parsed.data, rateLimit };
241
+ }
242
+ }
243
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Model-facing DSH Plugin Hub search and ranking tools.
3
+ * @module dsh-plugin-hub
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ import Schema from '@deepseek-ai/schemastery';
7
+ import { type PluginHubLocale } from './api.js';
8
+ export declare const name = "dsh-plugin-hub";
9
+ export declare const inject: string[];
10
+ /** Deployment settings for the public Plugin Hub client. */
11
+ export interface Config {
12
+ baseUrl: string;
13
+ locale: PluginHubLocale;
14
+ timeoutMs: number;
15
+ maxResults: number;
16
+ }
17
+ /** Validated plugin configuration. */
18
+ export declare const Config: Schema<Config>;
19
+ /**
20
+ * Register the read-only search and ranking tools.
21
+ * @param ctx - Cordis context carrying the Harness tool registry.
22
+ * @param config - Validated service and result bounds.
23
+ */
24
+ export declare function apply(ctx: Context, config: Config): void;
25
+ export * from './api.js';
26
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Model-facing DSH Plugin Hub search and ranking tools.
3
+ * @module dsh-plugin-hub
4
+ */
5
+ import Schema from '@deepseek-ai/schemastery';
6
+ import { defineTool } from '@deepseek-ai/dsh-tools';
7
+ import { PluginHubClient, normalizePluginHubBaseUrl, pluginHubCategories, pluginHubLocales, pluginHubSorts, pluginHubSources, pluginHubTypes, } from './api.js';
8
+ import { rankingLabel, renderPluginHubResult } from './render.js';
9
+ import { pluginHubResultSchema } from './tool-schema.js';
10
+ export const name = 'dsh-plugin-hub';
11
+ export const inject = ['tools'];
12
+ /** Validated plugin configuration. */
13
+ export const Config = Schema.object({
14
+ baseUrl: Schema.string().default('https://dshpluginhub.dev'),
15
+ locale: Schema.union(pluginHubLocales).default('en'),
16
+ timeoutMs: Schema.number().step(1).min(1).max(120_000).default(15_000),
17
+ maxResults: Schema.number().step(1).min(1).max(50).default(10),
18
+ });
19
+ function boundedInteger(name, value, fallback, maximum) {
20
+ const resolved = value ?? fallback;
21
+ if (!Number.isInteger(resolved) || resolved < 1 || resolved > maximum) {
22
+ throw new Error(`${name} must be an integer from 1 to ${maximum}`);
23
+ }
24
+ return resolved;
25
+ }
26
+ /**
27
+ * Register the read-only search and ranking tools.
28
+ * @param ctx - Cordis context carrying the Harness tool registry.
29
+ * @param config - Validated service and result bounds.
30
+ */
31
+ export function apply(ctx, config) {
32
+ const client = new PluginHubClient({
33
+ baseUrl: normalizePluginHubBaseUrl(config.baseUrl),
34
+ timeoutMs: config.timeoutMs,
35
+ });
36
+ ctx.tools.register(defineTool({
37
+ name: 'dsh_plugin_search',
38
+ description: 'Search published and verified DeepSeek Harness plugins by task, feature, plugin name, repository, or author. Returns install commands, compatibility, metrics, and source links.',
39
+ parameters: {
40
+ query: { type: 'string', required: true, description: 'What the user needs, or a plugin/author/repository name. Maximum 100 characters.' },
41
+ locale: { type: 'string', enum: [...pluginHubLocales], description: `Localized descriptions; defaults to ${config.locale}.` },
42
+ category: { type: 'string', enum: [...pluginHubCategories], description: 'Optional marketplace category.' },
43
+ type: { type: 'string', enum: [...pluginHubTypes], description: 'Optional host/client/hybrid runtime filter.' },
44
+ source: { type: 'string', enum: [...pluginHubSources], description: 'Optional npm or GitHub installation source.' },
45
+ sort: { type: 'string', enum: [...pluginHubSorts], description: 'Ordering; relevance is the default for a non-empty query.' },
46
+ page: { type: 'integer', description: 'Result page from 1 to 1000; defaults to 1.' },
47
+ limit: { type: 'integer', description: `Items to return from 1 to ${config.maxResults}; defaults to ${config.maxResults}.` },
48
+ },
49
+ output: {
50
+ schema: pluginHubResultSchema,
51
+ render: (_args, result) => [{ type: 'text', text: renderPluginHubResult(result, 'Plugin search') }],
52
+ },
53
+ async execute(args, exec) {
54
+ const query = args.query.trim().replace(/\s+/g, ' ');
55
+ if (!query)
56
+ throw new Error('query must be a non-empty string');
57
+ return client.search({
58
+ query,
59
+ locale: args.locale ?? config.locale,
60
+ ...(args.category ? { category: args.category } : {}),
61
+ ...(args.type ? { type: args.type } : {}),
62
+ ...(args.source ? { source: args.source } : {}),
63
+ ...(args.sort ? { sort: args.sort } : {}),
64
+ page: boundedInteger('page', args.page, 1, 1000),
65
+ perPage: boundedInteger('limit', args.limit, config.maxResults, config.maxResults),
66
+ }, exec.signal);
67
+ },
68
+ presentCall: args => ({
69
+ card: 'generic',
70
+ title: `Search DSH plugins: ${args.query}`,
71
+ kind: 'search',
72
+ rawInput: args,
73
+ }),
74
+ }));
75
+ ctx.tools.register(defineTool({
76
+ name: 'dsh_plugin_rankings',
77
+ description: 'List published and verified DeepSeek Harness plugins by daily Star growth, total Stars, newest listing, or recent GitHub activity.',
78
+ parameters: {
79
+ ranking: { type: 'string', required: true, enum: ['growth', 'stars', 'newest', 'active'], description: 'growth | stars | newest | active' },
80
+ locale: { type: 'string', enum: [...pluginHubLocales], description: `Localized descriptions; defaults to ${config.locale}.` },
81
+ category: { type: 'string', enum: [...pluginHubCategories], description: 'Optional marketplace category.' },
82
+ page: { type: 'integer', description: 'Result page from 1 to 1000; defaults to 1.' },
83
+ limit: { type: 'integer', description: `Items to return from 1 to ${config.maxResults}; defaults to ${config.maxResults}.` },
84
+ },
85
+ output: {
86
+ schema: pluginHubResultSchema,
87
+ render: (args, result) => [{ type: 'text', text: renderPluginHubResult(result, rankingLabel(args.ranking)) }],
88
+ },
89
+ async execute(args, exec) {
90
+ return client.search({
91
+ locale: args.locale ?? config.locale,
92
+ ...(args.category ? { category: args.category } : {}),
93
+ sort: args.ranking,
94
+ page: boundedInteger('page', args.page, 1, 1000),
95
+ perPage: boundedInteger('limit', args.limit, config.maxResults, config.maxResults),
96
+ }, exec.signal);
97
+ },
98
+ presentCall: args => ({
99
+ card: 'generic',
100
+ title: rankingLabel(args.ranking),
101
+ kind: 'search',
102
+ rawInput: args,
103
+ }),
104
+ }));
105
+ }
106
+ export * from './api.js';
107
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ import type { PluginHubSearchResult, PluginHubSort } from './api.js';
2
+ /**
3
+ * Render API results into bounded model-facing discovery text.
4
+ * @param result - Validated search response.
5
+ * @param label - Description of the completed operation.
6
+ * @returns Compact text retaining install and follow-up links.
7
+ */
8
+ export declare function renderPluginHubResult(result: PluginHubSearchResult, label: string): string;
9
+ /** Human label for one public ranking sort. */
10
+ export declare function rankingLabel(sort: Exclude<PluginHubSort, 'relevance'>): string;
11
+ //# sourceMappingURL=render.d.ts.map
package/dist/render.js ADDED
@@ -0,0 +1,41 @@
1
+ function growth(value) {
2
+ if (value === null)
3
+ return '1d growth unavailable';
4
+ return `1d growth ${value > 0 ? '+' : ''}${value}`;
5
+ }
6
+ function itemLines(plugin, index) {
7
+ return [
8
+ `${index + 1}. ${plugin.name} (${plugin.slug})`,
9
+ ` ${plugin.description}`,
10
+ ` ${plugin.category} · ${plugin.type} · ${plugin.metrics.stars} stars · ${growth(plugin.metrics.starsDelta1d)}`,
11
+ ` Install: ${plugin.package.installCommand}`,
12
+ ` Details: ${plugin.links.detail}`,
13
+ ` Repository: ${plugin.links.repository}`,
14
+ ];
15
+ }
16
+ /**
17
+ * Render API results into bounded model-facing discovery text.
18
+ * @param result - Validated search response.
19
+ * @param label - Description of the completed operation.
20
+ * @returns Compact text retaining install and follow-up links.
21
+ */
22
+ export function renderPluginHubResult(result, label) {
23
+ if (result.items.length === 0) {
24
+ return `${label}: no published, verified plugins matched. Try a broader query or remove a filter.`;
25
+ }
26
+ const header = `${label}: ${result.pagination.total} matches; showing ${result.items.length} on page ${result.pagination.page}.`;
27
+ const quota = result.rateLimit.remaining === null
28
+ ? null
29
+ : `Anonymous API quota remaining: ${result.rateLimit.remaining}${result.rateLimit.limit === null ? '' : `/${result.rateLimit.limit}`}.`;
30
+ return [header, ...result.items.flatMap(itemLines), quota].filter((line) => line !== null).join('\n');
31
+ }
32
+ /** Human label for one public ranking sort. */
33
+ export function rankingLabel(sort) {
34
+ switch (sort) {
35
+ case 'growth': return 'Daily growth ranking';
36
+ case 'stars': return 'Top Stars ranking';
37
+ case 'newest': return 'Newly listed ranking';
38
+ case 'active': return 'Recently active ranking';
39
+ }
40
+ }
41
+ //# sourceMappingURL=render.js.map