@wangjunjian/dsh-github-trending 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/lib/index.js ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * `@wangjunjian/dsh-github-trending`: a DeepSeek Harness bundle plugin that
3
+ * registers a `github_trending` tool backed by https://github.com/trending.
4
+ *
5
+ * The plugin is a Cordis function/namespace plugin (no default export). It
6
+ * injects `tools` and `systemPrompt` and registers the tool through the
7
+ * harness tool registry.
8
+ *
9
+ * @module @wangjunjian/dsh-github-trending
10
+ */
11
+ import z from '@deepseek-ai/schemastery';
12
+ import { TrendingCache } from './cache.js';
13
+ import { DEFAULT_REFRESH_INTERVAL_MS } from './constants.js';
14
+ import { applyGithubTrendingTool, buildTrendingUrl, fetchTrendingRepositories, MAX_RESULTS_LIMIT } from './tool.js';
15
+ export { DEFAULT_REFRESH_INTERVAL_MS } from './constants.js';
16
+ export { DEFAULT_MAX_RESULTS, MAX_RESULTS_LIMIT } from './tool.js';
17
+ /** Cordis plugin name used by loader diagnostics. */
18
+ export const name = 'github-trending';
19
+ /** Services this plugin requires. */
20
+ export const inject = ['tools', 'systemPrompt', 'webServer'];
21
+ /** Schemastery config schema with defaults and bounds. */
22
+ export const Config = z.object({
23
+ enabled: z.boolean().default(true),
24
+ timeoutMs: z.number().default(30_000),
25
+ maxResults: z.number().default(10),
26
+ refreshIntervalMs: z.number().default(DEFAULT_REFRESH_INTERVAL_MS),
27
+ });
28
+ /**
29
+ * Assert that a numeric config value is a positive finite integer.
30
+ *
31
+ * @param name - the config field name.
32
+ * @param value - the value to validate.
33
+ */
34
+ function assertPositiveInteger(name, value) {
35
+ if (!Number.isInteger(value) || value <= 0) {
36
+ throw new Error(`github-trending: ${name} must be a positive integer`);
37
+ }
38
+ }
39
+ /**
40
+ * Register the `github_trending` tool and its system-prompt guidance.
41
+ *
42
+ * @param ctx - the Cordis context.
43
+ * @param config - plugin config; schemastery has already applied defaults.
44
+ */
45
+ export function apply(ctx, config) {
46
+ const resolved = config;
47
+ assertPositiveInteger('timeoutMs', resolved.timeoutMs);
48
+ assertPositiveInteger('maxResults', resolved.maxResults);
49
+ assertPositiveInteger('refreshIntervalMs', resolved.refreshIntervalMs);
50
+ if (!resolved.enabled)
51
+ return;
52
+ applyGithubTrendingTool(ctx, {
53
+ timeoutMs: resolved.timeoutMs,
54
+ maxResults: Math.min(resolved.maxResults, MAX_RESULTS_LIMIT),
55
+ });
56
+ const cache = new TrendingCache({
57
+ intervalMs: resolved.refreshIntervalMs,
58
+ timeoutMs: resolved.timeoutMs,
59
+ });
60
+ ctx.effect(() => () => {
61
+ cache.dispose();
62
+ }, 'github-trending: cache disposal');
63
+ // Pre-load daily/weekly/monthly so the browser UI can switch tabs instantly.
64
+ for (const since of ['daily', 'weekly', 'monthly']) {
65
+ const fetcher = async (signal) => {
66
+ const trendingUrl = buildTrendingUrl({ since });
67
+ return fetchTrendingRepositories(trendingUrl, signal);
68
+ };
69
+ cache.ensureScheduled(fetcher, undefined, since);
70
+ void cache.refresh(fetcher, undefined, since).catch(() => { });
71
+ }
72
+ ctx.effect(() => ctx.webServer.register({
73
+ kind: 'prefix',
74
+ path: '/github-trending',
75
+ handler: createTrendingHandler(cache),
76
+ }), 'github-trending: web route');
77
+ }
78
+ /**
79
+ * Create the `/github-trending` HTTP handler that serves cached GitHub Trending
80
+ * data to the browser side. Runs from the host so it is not subject to browser
81
+ * CORS restrictions. Supports `?refresh=1` to force a host-side refetch.
82
+ *
83
+ * @param cache - shared trending cache.
84
+ * @returns the request handler.
85
+ */
86
+ function createTrendingHandler(cache) {
87
+ return async (req, res) => {
88
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
89
+ res.writeHead(405, { 'content-type': 'application/json' });
90
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
91
+ return;
92
+ }
93
+ const url = new URL(req.url ?? '/', 'http://x');
94
+ const language = url.searchParams.get('language') ?? undefined;
95
+ const sinceRaw = url.searchParams.get('since') ?? 'daily';
96
+ const since = sinceRaw === 'weekly' || sinceRaw === 'monthly' ? sinceRaw : 'daily';
97
+ const forceRefresh = url.searchParams.get('refresh') === '1';
98
+ try {
99
+ const fetchers = {
100
+ daily: async (signal) => {
101
+ return fetchTrendingRepositories(buildTrendingUrl({ language, since: 'daily' }), signal);
102
+ },
103
+ weekly: async (signal) => {
104
+ return fetchTrendingRepositories(buildTrendingUrl({ language, since: 'weekly' }), signal);
105
+ },
106
+ monthly: async (signal) => {
107
+ return fetchTrendingRepositories(buildTrendingUrl({ language, since: 'monthly' }), signal);
108
+ },
109
+ };
110
+ for (const sinceValue of ['daily', 'weekly', 'monthly']) {
111
+ cache.ensureScheduled(fetchers[sinceValue], language, sinceValue);
112
+ }
113
+ let entry;
114
+ if (forceRefresh) {
115
+ const all = await cache.refreshAll(fetchers, language);
116
+ entry = all[since];
117
+ }
118
+ else if (cache.get(language, since) === undefined) {
119
+ entry = await cache.refresh(fetchers[since], language, since);
120
+ }
121
+ else {
122
+ entry = cache.get(language, since);
123
+ }
124
+ res.writeHead(200, {
125
+ 'content-type': 'application/json; charset=utf-8',
126
+ 'cache-control': 'no-cache',
127
+ });
128
+ res.end(JSON.stringify(entry));
129
+ }
130
+ catch (error) {
131
+ const cached = cache.get(language, since);
132
+ if (cached !== undefined) {
133
+ // Serve stale data when a background refresh fails.
134
+ res.writeHead(200, {
135
+ 'content-type': 'application/json; charset=utf-8',
136
+ 'cache-control': 'no-cache',
137
+ });
138
+ res.end(JSON.stringify(cached));
139
+ return;
140
+ }
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ res.writeHead(502, { 'content-type': 'application/json; charset=utf-8' });
143
+ res.end(JSON.stringify({ error: message }));
144
+ }
145
+ };
146
+ }
147
+ //# sourceMappingURL=index.js.map
package/lib/parser.js ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * HTML parser for the public GitHub Trending page (https://github.com/trending).
3
+ *
4
+ * GitHub Trending has no official API, so this module scrapes the server-rendered
5
+ * HTML. The selectors target the current `article.Box-row` layout and degrade
6
+ * gracefully when GitHub changes markup: missing fields become `undefined` or `0`
7
+ * rather than throwing.
8
+ *
9
+ * @module @wangjunjian/dsh-github-trending/parser
10
+ */
11
+ import { parse as parseHtml } from 'node-html-parser';
12
+ /**
13
+ * Parse a numeric string like "47,534" or "1.2k" into an integer.
14
+ *
15
+ * GitHub displays large counts with comma separators or compact suffixes.
16
+ * Unparseable values return `undefined` so the caller can default to `0`.
17
+ *
18
+ * @param text - raw numeric text from the page.
19
+ * @returns the parsed integer, or `undefined` when the text is not a count.
20
+ */
21
+ function parseCount(text) {
22
+ if (text === undefined)
23
+ return undefined;
24
+ const normalized = text.trim().toLowerCase().replace(/,/g, '');
25
+ if (normalized === '')
26
+ return undefined;
27
+ const compact = /^(?<whole>\d+(?:\.\d+)?)(?<suffix>[km])$/.exec(normalized);
28
+ if (compact?.groups !== undefined) {
29
+ const value = Number.parseFloat(compact.groups.whole);
30
+ const multiplier = compact.groups.suffix === 'k' ? 1_000 : 1_000_000;
31
+ return Math.round(value * multiplier);
32
+ }
33
+ const numeric = Number(normalized);
34
+ return Number.isFinite(numeric) ? Math.round(numeric) : undefined;
35
+ }
36
+ /**
37
+ * Extract the owner and repository name from a heading link.
38
+ *
39
+ * The heading contains `<span class="text-normal">owner /</span> repo`, and the
40
+ * link `href` is `/owner/repo`. Prefer the `href` because it is unambiguous.
41
+ *
42
+ * @param link - the heading anchor element.
43
+ * @returns owner and name, or `undefined` when the link is malformed.
44
+ */
45
+ function parseRepoName(link) {
46
+ const href = link.getAttribute('href');
47
+ if (href !== undefined) {
48
+ const parts = href.split('/').filter(Boolean);
49
+ if (parts.length >= 2) {
50
+ return { owner: parts[0], name: parts[1] };
51
+ }
52
+ }
53
+ const text = link.textContent ?? '';
54
+ const match = /^(?<owner>[^/\s]+)\s*\/\s*(?<name>[^/\s]+)$/.exec(text.trim());
55
+ if (match?.groups !== undefined) {
56
+ return { owner: match.groups.owner, name: match.groups.name };
57
+ }
58
+ return undefined;
59
+ }
60
+ /**
61
+ * Parse one `article.Box-row` into a {@link TrendingRepository}.
62
+ *
63
+ * @param article - the article element.
64
+ * @param rank - the 1-based position on the page.
65
+ * @returns the parsed repository, or `undefined` when the article has no
66
+ * identifiable repository name.
67
+ */
68
+ function parseArticle(article, rank) {
69
+ const link = article.querySelector('h2 a[href^="/"]');
70
+ if (link === null)
71
+ return undefined;
72
+ const repo = parseRepoName(link);
73
+ if (repo === undefined)
74
+ return undefined;
75
+ const description = article.querySelector('p')?.textContent?.trim() || undefined;
76
+ const language = article.querySelector('span[itemprop="programmingLanguage"]')?.textContent?.trim() || undefined;
77
+ const starsText = article.querySelector('a[href$="/stargazers"]')?.textContent;
78
+ const forksText = article.querySelector('a[href$="/forks"]')?.textContent;
79
+ let starsToday = 0;
80
+ const spans = article.querySelectorAll('span');
81
+ for (const span of spans) {
82
+ const text = span.textContent?.trim() ?? '';
83
+ // GitHub uses "X stars today" for daily, "X stars this week/month" for longer windows.
84
+ const match = /^(?<count>[\d,.]+[km]?)\s+stars?\s+(today|this week|this month)$/i.exec(text);
85
+ if (match?.groups !== undefined) {
86
+ starsToday = parseCount(match.groups.count) ?? 0;
87
+ break;
88
+ }
89
+ }
90
+ return {
91
+ rank,
92
+ owner: repo.owner,
93
+ name: repo.name,
94
+ fullName: `${repo.owner}/${repo.name}`,
95
+ url: `https://github.com/${repo.owner}/${repo.name}`,
96
+ description,
97
+ language,
98
+ stars: parseCount(starsText) ?? 0,
99
+ forks: parseCount(forksText) ?? 0,
100
+ starsToday,
101
+ };
102
+ }
103
+ /**
104
+ * Parse the GitHub Trending HTML and return the extracted repositories.
105
+ *
106
+ * @param html - the raw HTML body from https://github.com/trending.
107
+ * @returns the list of trending repositories in page order.
108
+ */
109
+ export function parseTrendingHtml(html) {
110
+ const root = parseHtml(html);
111
+ const articles = root.querySelectorAll('article.Box-row');
112
+ const repositories = [];
113
+ for (let index = 0; index < articles.length; index += 1) {
114
+ const parsed = parseArticle(articles[index], index + 1);
115
+ if (parsed !== undefined) {
116
+ repositories.push(parsed);
117
+ }
118
+ }
119
+ return repositories;
120
+ }
121
+ //# sourceMappingURL=parser.js.map
package/lib/tool.js ADDED
@@ -0,0 +1,236 @@
1
+ /**
2
+ * The model-facing `github_trending` tool.
3
+ *
4
+ * @module @wangjunjian/dsh-github-trending/tool
5
+ */
6
+ import { defineTool } from '@deepseek-ai/dsh-tools';
7
+ import { parseTrendingHtml } from './parser.js';
8
+ /** Default cap on repositories returned by one tool call. */
9
+ export const DEFAULT_MAX_RESULTS = 10;
10
+ /** Hard upper bound on repositories returned by one tool call. */
11
+ export const MAX_RESULTS_LIMIT = 25;
12
+ /** User-Agent identifying this plugin's requests to GitHub. */
13
+ export const USER_AGENT = '@wangjunjian/dsh-github-trending/0.1.0 (+https://github.com/wang-junjian/dsh-github-trending)';
14
+ /**
15
+ * Resolve the GitHub Trending URL from tool arguments.
16
+ *
17
+ * @param args - validated tool arguments.
18
+ * @returns the HTTPS URL to fetch.
19
+ */
20
+ export function buildTrendingUrl(args) {
21
+ const base = 'https://github.com/trending';
22
+ const path = args.language !== undefined && args.language.trim() !== ''
23
+ ? `${base}/${encodeURIComponent(args.language.trim())}`
24
+ : base;
25
+ const since = args.since === 'weekly' || args.since === 'monthly' ? args.since : 'daily';
26
+ return `${path}?since=${since}`;
27
+ }
28
+ /**
29
+ * Validate and clamp `maxResults` to the configured cap.
30
+ *
31
+ * @param requested - the model-requested limit.
32
+ * @param configCap - the plugin-level cap from config.
33
+ * @returns a positive integer not exceeding the cap.
34
+ */
35
+ export function resolveMaxResults(requested, configCap) {
36
+ const cap = Math.min(configCap, MAX_RESULTS_LIMIT);
37
+ if (requested === undefined || !Number.isFinite(requested))
38
+ return Math.min(DEFAULT_MAX_RESULTS, cap);
39
+ return Math.max(1, Math.min(Math.round(requested), cap));
40
+ }
41
+ /**
42
+ * Fetch the GitHub Trending page and parse it into repositories.
43
+ *
44
+ * @param url - the trending URL to fetch.
45
+ * @param signal - cancellation signal forwarded from the tool execution.
46
+ * @returns the parsed repositories.
47
+ */
48
+ export async function fetchTrendingRepositories(url, signal) {
49
+ const response = await fetch(url, {
50
+ headers: {
51
+ 'User-Agent': USER_AGENT,
52
+ Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
53
+ },
54
+ signal,
55
+ });
56
+ if (!response.ok) {
57
+ throw new Error(`GitHub Trending returned HTTP ${response.status}`);
58
+ }
59
+ const html = await response.text();
60
+ return parseTrendingHtml(html);
61
+ }
62
+ /**
63
+ * Format the canonical result as a concise markdown list for the model.
64
+ *
65
+ * @param result - the tool result value.
66
+ * @returns one text content block.
67
+ */
68
+ export function formatTrendingOutput(result) {
69
+ if (result.repositories.length === 0) {
70
+ return 'No trending repositories found.';
71
+ }
72
+ const header = result.truncated
73
+ ? `Top ${result.repositories.length} trending repositories (truncated):`
74
+ : `Top ${result.repositories.length} trending repositories:`;
75
+ const lines = result.repositories.map((repo) => {
76
+ const today = repo.starsToday > 0 ? ` | +${repo.starsToday.toLocaleString()} today` : '';
77
+ const lang = repo.language !== undefined ? ` | ${repo.language}` : '';
78
+ const stars = repo.stars > 0 ? ` | ⭐ ${repo.stars.toLocaleString()}` : '';
79
+ const forks = repo.forks > 0 ? ` | 🍴 ${repo.forks.toLocaleString()}` : '';
80
+ const description = repo.description !== undefined ? `\n ${repo.description}` : '';
81
+ return `${repo.rank}. [${repo.fullName}](${repo.url})${lang}${stars}${forks}${today}${description}`;
82
+ });
83
+ return [header, ...lines].join('\n');
84
+ }
85
+ /**
86
+ * Pending-call presentation: a search-style generic card titled by the language
87
+ * and time window.
88
+ *
89
+ * @param args - the raw tool arguments.
90
+ * @returns the generic pending card.
91
+ */
92
+ export function presentTrendingCall(args) {
93
+ const language = args.language !== undefined && args.language.trim() !== '' ? args.language.trim() : 'all languages';
94
+ return {
95
+ card: 'generic',
96
+ title: `GitHub Trending: ${language} (${args.since})`,
97
+ kind: 'search',
98
+ rawInput: `${language} / ${args.since}`,
99
+ };
100
+ }
101
+ /**
102
+ * Build replayable presentation metadata for the completed call.
103
+ *
104
+ * @param result - the canonical tool result.
105
+ * @returns a compact JSON summary for UI cards.
106
+ */
107
+ export function trendingMetaFromValue(result) {
108
+ return {
109
+ count: result.repositories.length,
110
+ truncated: result.truncated,
111
+ repositories: result.repositories.map((repo) => ({
112
+ fullName: repo.fullName,
113
+ url: repo.url,
114
+ starsToday: repo.starsToday,
115
+ })),
116
+ };
117
+ }
118
+ /**
119
+ * Narrow opaque replayed result metadata to the presentation shape.
120
+ *
121
+ * @param meta - result metadata.
122
+ * @returns the validated meta, or `undefined` for absent/malformed data.
123
+ */
124
+ export function trendingMetaFromResult(meta) {
125
+ if (typeof meta !== 'object' || meta === null || Array.isArray(meta))
126
+ return undefined;
127
+ const { count, truncated, repositories } = meta;
128
+ if (typeof count !== 'number' || typeof truncated !== 'boolean' || !Array.isArray(repositories))
129
+ return undefined;
130
+ const repos = repositories.filter((repo) => {
131
+ if (typeof repo !== 'object' || repo === null)
132
+ return false;
133
+ const r = repo;
134
+ return typeof r.fullName === 'string' && typeof r.url === 'string' && typeof r.starsToday === 'number';
135
+ });
136
+ return { count, truncated, repositories: repos };
137
+ }
138
+ /**
139
+ * Completed-call presentation: a search result card with the repository list.
140
+ *
141
+ * @param args - the raw tool arguments.
142
+ * @param result - the final tool result.
143
+ * @returns the search result view, or `undefined` on error/malformed meta.
144
+ */
145
+ export function presentTrendingResult(args, result) {
146
+ if (result.isError)
147
+ return undefined;
148
+ const meta = trendingMetaFromResult(result.meta);
149
+ if (meta === undefined)
150
+ return undefined;
151
+ const language = args.language !== undefined && args.language.trim() !== '' ? args.language.trim() : 'all languages';
152
+ return {
153
+ card: 'search',
154
+ shape: 'paths',
155
+ title: `GitHub Trending: ${language} (${args.since})`,
156
+ paths: meta.repositories.map((repo) => repo.url),
157
+ total: meta.count,
158
+ truncated: meta.truncated,
159
+ };
160
+ }
161
+ /**
162
+ * Register the `github_trending` tool and its system-prompt guidance.
163
+ *
164
+ * @param ctx - the Cordis context whose `tools` and `systemPrompt` registries receive the registrations.
165
+ * @param config - resolved plugin config: timeout and result cap.
166
+ */
167
+ export function applyGithubTrendingTool(ctx, config) {
168
+ ctx.systemPrompt.section({
169
+ name: 'tool:github_trending',
170
+ order: 112,
171
+ text: 'Use the github_trending tool to discover currently popular repositories on GitHub. It returns repository names, descriptions, languages, star counts, and stars gained today.',
172
+ });
173
+ ctx.tools.register(defineTool({
174
+ name: 'github_trending',
175
+ description: 'Fetch currently trending GitHub repositories for a language and time window.',
176
+ parameters: {
177
+ language: {
178
+ type: 'string',
179
+ description: 'Optional programming language filter (e.g. "python", "typescript", "go"). Omit to list trending repositories across all languages.',
180
+ },
181
+ since: {
182
+ type: 'string',
183
+ description: 'Time window: "daily", "weekly", or "monthly". Defaults to "daily".',
184
+ },
185
+ maxResults: {
186
+ type: 'integer',
187
+ description: `Maximum number of repositories to return (1-${Math.min(config.maxResults, MAX_RESULTS_LIMIT)}).`,
188
+ },
189
+ },
190
+ output: {
191
+ schema: {
192
+ type: 'object',
193
+ additionalProperties: false,
194
+ properties: {
195
+ repositories: {
196
+ type: 'array',
197
+ required: true,
198
+ items: {
199
+ type: 'object',
200
+ additionalProperties: false,
201
+ properties: {
202
+ rank: { type: 'integer', required: true },
203
+ owner: { type: 'string', required: true },
204
+ name: { type: 'string', required: true },
205
+ fullName: { type: 'string', required: true },
206
+ url: { type: 'string', required: true },
207
+ description: { type: 'string' },
208
+ language: { type: 'string' },
209
+ stars: { type: 'integer', required: true },
210
+ forks: { type: 'integer', required: true },
211
+ starsToday: { type: 'integer', required: true },
212
+ },
213
+ },
214
+ },
215
+ truncated: { type: 'boolean', required: true },
216
+ },
217
+ },
218
+ render: (_args, value) => [{ type: 'text', text: formatTrendingOutput(value) }],
219
+ presentationMeta: (_args, value) => trendingMetaFromValue(value),
220
+ },
221
+ timeoutMs: config.timeoutMs,
222
+ isConcurrencySafe: () => true,
223
+ async execute(args, exec) {
224
+ const language = typeof args.language === 'string' ? args.language : undefined;
225
+ const maxResults = resolveMaxResults(args.maxResults, config.maxResults);
226
+ const url = buildTrendingUrl({ language, since: args.since, maxResults });
227
+ const repositories = await fetchTrendingRepositories(url, exec.signal);
228
+ const truncated = repositories.length > maxResults;
229
+ const capped = repositories.slice(0, maxResults);
230
+ return { repositories: capped, truncated };
231
+ },
232
+ presentCall: presentTrendingCall,
233
+ presentResult: (args, result) => presentTrendingResult(args, result),
234
+ }));
235
+ }
236
+ //# sourceMappingURL=tool.js.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * In-memory cache for GitHub Trending data, shared by the host-side web route
3
+ * and the periodic background refresh.
4
+ *
5
+ * @module @wangjunjian/dsh-github-trending/cache
6
+ */
7
+ import type { TrendingRepository } from './parser.js';
8
+ export interface TrendingCacheEntry {
9
+ /** Cached repositories in page order. */
10
+ repositories: TrendingRepository[];
11
+ /** ISO timestamp of the cache write. */
12
+ cachedAt: string;
13
+ }
14
+ export interface TrendingCacheOptions {
15
+ /** Refresh interval in milliseconds for the periodic background refresh. */
16
+ intervalMs: number;
17
+ /** Request timeout in milliseconds. */
18
+ timeoutMs: number;
19
+ }
20
+ /**
21
+ * Simple in-memory cache with periodic refresh.
22
+ *
23
+ * The cache is deliberately host-local: a browser reload re-fetches from this
24
+ * cache, but a host restart starts cold. This keeps the implementation small
25
+ * and avoids persisting third-party data.
26
+ */
27
+ export declare class TrendingCache {
28
+ private readonly entries;
29
+ private readonly timers;
30
+ private readonly options;
31
+ constructor(options: TrendingCacheOptions);
32
+ /**
33
+ * Read the current cache entry for a language/time window.
34
+ *
35
+ * @param language - optional language filter.
36
+ * @param since - time window.
37
+ * @returns the cached entry, or undefined when cold.
38
+ */
39
+ get(language: string | undefined, since: string): TrendingCacheEntry | undefined;
40
+ /**
41
+ * Fetch fresh data and store it. Reuses the configured timeout.
42
+ *
43
+ * @param fetcher - host-side fetcher that returns ranked repositories.
44
+ * @param language - optional language filter.
45
+ * @param since - time window.
46
+ * @returns the freshly cached entry.
47
+ */
48
+ refresh(fetcher: (signal?: AbortSignal) => Promise<TrendingRepository[]>, language: string | undefined, since: string): Promise<TrendingCacheEntry>;
49
+ /**
50
+ * Fetch with a per-attempt timeout, retrying transient failures.
51
+ *
52
+ * @param fetcher - host-side fetcher that returns ranked repositories.
53
+ * @returns the fetched repositories.
54
+ */
55
+ private fetchWithRetry;
56
+ /**
57
+ * Store repositories under a key and return the wrapped entry.
58
+ *
59
+ * @param cachedAt - optional ISO timestamp; defaults to now.
60
+ */
61
+ set(language: string | undefined, since: string, repositories: TrendingRepository[], cachedAt?: string): TrendingCacheEntry;
62
+ /**
63
+ * Fetch fresh data for all time windows and store them with a single timestamp.
64
+ *
65
+ * @param fetchers - fetcher for each time window.
66
+ * @param language - optional language filter.
67
+ * @returns the freshly cached entries for all windows.
68
+ */
69
+ refreshAll(fetchers: Record<string, (signal?: AbortSignal) => Promise<TrendingRepository[]>>, language: string | undefined): Promise<Record<string, TrendingCacheEntry>>;
70
+ /**
71
+ * Ensure a periodic refresh is running for the given key. Idempotent.
72
+ *
73
+ * @param fetcher - host-side fetcher.
74
+ * @param language - optional language filter.
75
+ * @param since - time window.
76
+ */
77
+ ensureScheduled(fetcher: (signal?: AbortSignal) => Promise<TrendingRepository[]>, language: string | undefined, since: string): void;
78
+ /**
79
+ * Stop all background refresh timers. Call on plugin teardown.
80
+ */
81
+ dispose(): void;
82
+ }
83
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Fixed right-hand overlay panel that displays cached GitHub Trending data.
3
+ *
4
+ * The panel is designed to feel like a layout column: it is docked to the right
5
+ * edge, has its own scrollbar, can be collapsed to a narrow rail, and its width
6
+ * is resizable. Because external plugins cannot declare new AppFrame grid
7
+ * columns in this version of DSH, the panel lives in `shell.overlay`. To avoid
8
+ * covering the conversation, it pushes the center/details columns via a runtime
9
+ * CSS variable set on the AppFrame root.
10
+ *
11
+ * @module @wangjunjian/dsh-github-trending/client/GithubTrendingPanel
12
+ */
13
+ import type { GithubTrendingKey } from './locales.js';
14
+ export interface GithubTrendingPanelProps {
15
+ /** Locale translator for the panel namespace. */
16
+ t: (key: GithubTrendingKey) => string;
17
+ }
18
+ /**
19
+ * Render the right-hand overlay panel.
20
+ * @param props - panel props.
21
+ */
22
+ export declare function GithubTrendingPanel({ t }: GithubTrendingPanelProps): JSX.Element | null;
23
+ //# sourceMappingURL=GithubTrendingPanel.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Browser half of the GitHub Trending plugin.
3
+ *
4
+ * Registers a `shell.overlay` entry that renders the right-hand trending panel.
5
+ * The panel is shown by default so no sidebar trigger is needed.
6
+ *
7
+ * @module @wangjunjian/dsh-github-trending/client
8
+ */
9
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
10
+ import { type GithubTrendingKey } from './locales.js';
11
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
12
+ interface LocaleNamespaceMap {
13
+ /** GitHub Trending panel copy. */
14
+ 'github-trending': GithubTrendingKey;
15
+ }
16
+ }
17
+ /** Required client services. */
18
+ export declare const inject: string[];
19
+ /**
20
+ * Register the GitHub Trending right-hand overlay panel.
21
+ * @param ctx - client root context.
22
+ */
23
+ export declare function apply(ctx: ClientContext): void;
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Locale strings for the GitHub Trending sidebar action.
3
+ *
4
+ * @module @wangjunjian/dsh-github-trending/client/locales
5
+ */
6
+ export type GithubTrendingKey = 'action.label' | 'action.close' | 'panel.title' | 'panel.daily' | 'panel.weekly' | 'panel.monthly' | 'panel.refresh' | 'panel.loading' | 'panel.empty' | 'panel.error' | 'panel.cachedAt' | 'panel.autoRefresh' | 'panel.collapse' | 'repo.stars' | 'repo.forks' | 'repo.today';
7
+ export declare const en: Record<GithubTrendingKey, string>;
8
+ export declare const zh: Record<GithubTrendingKey, string>;
9
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Module-level store holding the overlay panel's UI state: open/closed,
3
+ * collapsed, selected time window, and width.
4
+ *
5
+ * @module @wangjunjian/dsh-github-trending/client/store
6
+ */
7
+ export type Since = 'daily' | 'weekly' | 'monthly';
8
+ export interface PanelState {
9
+ /** Whether the right-hand overlay panel is mounted at all. */
10
+ open: boolean;
11
+ /** Whether the panel is collapsed to a narrow rail. */
12
+ collapsed: boolean;
13
+ /** Selected time window. */
14
+ since: Since;
15
+ /** Panel width in pixels when expanded. */
16
+ width: number;
17
+ }
18
+ type Listener = () => void;
19
+ declare class PanelStore {
20
+ private state;
21
+ private readonly listeners;
22
+ subscribe(listener: Listener): () => void;
23
+ getSnapshot(): PanelState;
24
+ private emit;
25
+ setOpen(open: boolean): void;
26
+ setCollapsed(collapsed: boolean): void;
27
+ toggleCollapsed(): void;
28
+ setSince(since: Since): void;
29
+ setWidth(width: number): void;
30
+ }
31
+ /** Singleton panel store used by the overlay panel. */
32
+ export declare const panelStore: PanelStore;
33
+ export {};
34
+ //# sourceMappingURL=store.d.ts.map