@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wangjunjian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @wangjunjian/dsh-github-trending
2
+
3
+ [中文](README.zh.md) | English
4
+
5
+ A DeepSeek Harness bundle plugin that exposes a `github_trending` tool backed by
6
+ https://github.com/trending, plus a right-hand trending panel in the `web`
7
+ profile.
8
+
9
+ ## What it does
10
+
11
+ The plugin registers one model-facing tool, `github_trending`, that fetches the
12
+ public GitHub Trending page for a given language and time window and returns a
13
+ structured list of trending repositories.
14
+
15
+ Each result includes:
16
+
17
+ - `rank` — 1-based position on the page
18
+ - `owner` / `name` / `fullName`
19
+ - `url` — absolute GitHub repository URL
20
+ - `description` — repository tagline
21
+ - `language` — primary language
22
+ - `stars` — total star count
23
+ - `forks` — fork count
24
+ - `starsToday` — stars gained today (or this week/month)
25
+
26
+ In the `web` profile, the plugin additionally renders a dockable right-hand
27
+ panel (daily/weekly/monthly tabs, resizable, collapsible to a rail) backed by a
28
+ host-side in-memory cache served over the `/github-trending` HTTP route.
29
+
30
+ ## Installation
31
+
32
+ Install the plugin into a DeepSeek Harness profile. The plugin does not modify
33
+ the `deepseek-harness` repository itself.
34
+
35
+ ```sh
36
+ # From a local checkout
37
+ dsh plugin --profile headless add /Users/junjian/GitHub/wang-junjian/dsh-github-trending
38
+
39
+ # Or from the npm registry
40
+ dsh plugin --profile headless add @wangjunjian/dsh-github-trending
41
+ ```
42
+
43
+ Use `--profile web` to also get the trending panel in the browser UI. The
44
+ bundle is appended to `dsh.profile.bundles` automatically.
45
+
46
+ ## Usage
47
+
48
+ Ask the agent to list trending repositories:
49
+
50
+ ```text
51
+ Show me today's trending Python repositories.
52
+ ```
53
+
54
+ ### Tool parameters
55
+
56
+ | Parameter | Type | Required | Description |
57
+ |---|---|---|---|
58
+ | `language` | `string` | no | Programming language filter, e.g. `python`, `typescript`, `go`. |
59
+ | `since` | `string` | no | `daily` (default), `weekly`, or `monthly`. |
60
+ | `maxResults` | `integer` | no | Maximum repositories to return (default 10, hard cap 25). |
61
+
62
+ ### Cordis config
63
+
64
+ The bundle patch inserts one row by default. You can override its config in your
65
+ profile's `cordis.patch.yml`:
66
+
67
+ ```yaml
68
+ - id: github-trending
69
+ config:
70
+ enabled: true
71
+ timeoutMs: 30000
72
+ maxResults: 10
73
+ refreshIntervalMs: 14400000
74
+ ```
75
+
76
+ | Field | Default | Description |
77
+ |---|---|---|
78
+ | `enabled` | `true` | Whether to register the tool, cache, and web route. |
79
+ | `timeoutMs` | `30000` | Cooperative timeout budget (ms) for each HTTP request. |
80
+ | `maxResults` | `10` | Maximum repositories the tool may return (clamped to 25). |
81
+ | `refreshIntervalMs` | `14400000` | Background refresh interval (ms) for the UI cache (4 hours). |
82
+
83
+ ## Development
84
+
85
+ ```sh
86
+ pnpm install # install dependencies (pnpm 10; also builds via prepare)
87
+ pnpm run build # tsc -b tsconfig.json && tsdown
88
+ pnpm run test # vitest run
89
+ pnpm run typecheck # tsc --noEmit
90
+ pnpm run lint # biome check .
91
+ pnpm run lint:fix # biome check --write .
92
+ ```
93
+
94
+ `scripts/screenshot_test.py` is a manual development aid that drives a local
95
+ Playwright browser against `http://127.0.0.1:3080` to screenshot the web panel;
96
+ it is not part of the test suite and requires a local Playwright install.
97
+
98
+ ## Known limitations
99
+
100
+ - GitHub Trending has no official API. The parser targets the current
101
+ server-rendered HTML layout and may need updates if GitHub changes markup.
102
+ - Unauthenticated requests are subject to GitHub's IP rate limits.
103
+ - The plugin fetches GitHub directly rather than through `ctx.web`, so it does
104
+ not inherit the harness web capability's fetch policies.
105
+ - The panel floats over the app via `shell.overlay`; no native right-column
106
+ slot exists in DSH yet.
package/README.zh.md ADDED
@@ -0,0 +1,102 @@
1
+ # @wangjunjian/dsh-github-trending
2
+
3
+ 中文 | [English](README.md)
4
+
5
+ 一个 DeepSeek Harness bundle 插件:暴露一个基于 https://github.com/trending
6
+ 的 `github_trending` 工具,并在 `web` profile 中提供一个右侧趋势面板。
7
+
8
+ ## 功能
9
+
10
+ 插件注册了一个面向模型的工具 `github_trending`:抓取公开的 GitHub
11
+ Trending 页面(按语言和时间窗口),返回结构化的热门仓库列表。
12
+
13
+ 每条结果包含:
14
+
15
+ - `rank` — 页面上的排名(从 1 开始)
16
+ - `owner` / `name` / `fullName`
17
+ - `url` — 仓库的完整 GitHub URL
18
+ - `description` — 仓库简介
19
+ - `language` — 主要编程语言
20
+ - `stars` — 总 star 数
21
+ - `forks` — fork 数
22
+ - `starsToday` — 今天(或本周/本月)新增的 star 数
23
+
24
+ 在 `web` profile 下,插件还会在浏览器 UI 右侧渲染一个可停靠的趋势面板
25
+ (daily/weekly/monthly 标签页、可拖拽调宽、可折叠为窄条),数据来自宿主侧
26
+ 内存缓存,通过 `/github-trending` HTTP 路由提供。
27
+
28
+ ## 安装
29
+
30
+ 将插件安装到某个 DeepSeek Harness profile,不会修改 `deepseek-harness`
31
+ 仓库本身:
32
+
33
+ ```sh
34
+ # 从本地检出目录安装
35
+ dsh plugin --profile headless add /Users/junjian/GitHub/wang-junjian/dsh-github-trending
36
+
37
+ # 或从 npm registry 安装
38
+ dsh plugin --profile headless add @wangjunjian/dsh-github-trending
39
+ ```
40
+
41
+ 使用 `--profile web` 可同时获得浏览器 UI 中的趋势面板。安装后 bundle 会
42
+ 自动追加到 `dsh.profile.bundles`。
43
+
44
+ ## 使用
45
+
46
+ 直接向 agent 提问即可:
47
+
48
+ ```text
49
+ 列出今天 trending 的 Python 仓库。
50
+ ```
51
+
52
+ ### 工具参数
53
+
54
+ | 参数 | 类型 | 必填 | 说明 |
55
+ |---|---|---|---|
56
+ | `language` | `string` | 否 | 语言过滤,如 `python`、`typescript`、`go`。 |
57
+ | `since` | `string` | 否 | `daily`(默认)、`weekly` 或 `monthly`。 |
58
+ | `maxResults` | `integer` | 否 | 返回的仓库数量上限(默认 10,硬上限 25)。 |
59
+
60
+ ### Cordis 配置
61
+
62
+ bundle patch 默认插入一行配置。可在 profile 的 `cordis.patch.yml` 中覆盖:
63
+
64
+ ```yaml
65
+ - id: github-trending
66
+ config:
67
+ enabled: true
68
+ timeoutMs: 30000
69
+ maxResults: 10
70
+ refreshIntervalMs: 14400000
71
+ ```
72
+
73
+ | 字段 | 默认值 | 说明 |
74
+ |---|---|---|
75
+ | `enabled` | `true` | 是否注册工具、缓存与 web 路由。 |
76
+ | `timeoutMs` | `30000` | 每次 HTTP 请求的协作式超时预算(毫秒)。 |
77
+ | `maxResults` | `10` | 工具返回仓库数上限(最大会被钳制到 25)。 |
78
+ | `refreshIntervalMs` | `14400000` | UI 缓存的后台刷新间隔(毫秒,4 小时)。 |
79
+
80
+ ## 开发
81
+
82
+ ```sh
83
+ pnpm install # 安装依赖(pnpm 10;prepare 钩子会自动构建)
84
+ pnpm run build # tsc -b tsconfig.json && tsdown
85
+ pnpm run test # vitest run
86
+ pnpm run typecheck # tsc --noEmit
87
+ pnpm run lint # biome check .
88
+ pnpm run lint:fix # biome check --write .
89
+ ```
90
+
91
+ `scripts/screenshot_test.py` 是手工开发辅助脚本:用本地 Playwright 驱动
92
+ 浏览器访问 `http://127.0.0.1:3080` 截取 web 面板效果图;它不属于测试套件,
93
+ 需要本地安装 Playwright。
94
+
95
+ ## 已知限制
96
+
97
+ - GitHub Trending 没有官方 API。解析器针对当前服务端渲染的 HTML 结构,
98
+ GitHub 改动页面标记时可能需要更新。
99
+ - 未认证的请求受 GitHub IP 级速率限制约束。
100
+ - 插件直接请求 GitHub,而不是通过 `ctx.web`,因此不继承 harness web
101
+ 能力的抓取策略。
102
+ - 面板通过 `shell.overlay` 浮于应用之上;DSH 目前尚无原生右侧栏插槽。
@@ -0,0 +1,6 @@
1
+ # Patch layer for the @wangjunjian/dsh-github-trending bundle.
2
+ # Inserts one row that registers the github_trending tool.
3
+
4
+ - insert:
5
+ - id: github-trending
6
+ name: '@wangjunjian/dsh-github-trending'
package/lib/cache.js ADDED
@@ -0,0 +1,129 @@
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
+ /** Cache key format: `${language}:${since}`. */
8
+ function cacheKey(language, since) {
9
+ return `${language ?? ''}:${since}`;
10
+ }
11
+ /**
12
+ * Simple in-memory cache with periodic refresh.
13
+ *
14
+ * The cache is deliberately host-local: a browser reload re-fetches from this
15
+ * cache, but a host restart starts cold. This keeps the implementation small
16
+ * and avoids persisting third-party data.
17
+ */
18
+ export class TrendingCache {
19
+ entries = new Map();
20
+ timers = new Map();
21
+ options;
22
+ constructor(options) {
23
+ this.options = options;
24
+ }
25
+ /**
26
+ * Read the current cache entry for a language/time window.
27
+ *
28
+ * @param language - optional language filter.
29
+ * @param since - time window.
30
+ * @returns the cached entry, or undefined when cold.
31
+ */
32
+ get(language, since) {
33
+ return this.entries.get(cacheKey(language, since));
34
+ }
35
+ /**
36
+ * Fetch fresh data and store it. Reuses the configured timeout.
37
+ *
38
+ * @param fetcher - host-side fetcher that returns ranked repositories.
39
+ * @param language - optional language filter.
40
+ * @param since - time window.
41
+ * @returns the freshly cached entry.
42
+ */
43
+ async refresh(fetcher, language, since) {
44
+ const repositories = await this.fetchWithRetry(fetcher);
45
+ return this.set(language, since, repositories);
46
+ }
47
+ /**
48
+ * Fetch with a per-attempt timeout, retrying transient failures.
49
+ *
50
+ * @param fetcher - host-side fetcher that returns ranked repositories.
51
+ * @returns the fetched repositories.
52
+ */
53
+ async fetchWithRetry(fetcher) {
54
+ const MAX_RETRIES = 3;
55
+ const RETRY_DELAY_MS = 500;
56
+ let lastError;
57
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
58
+ const controller = new AbortController();
59
+ const timer = setTimeout(() => {
60
+ controller.abort();
61
+ }, this.options.timeoutMs);
62
+ try {
63
+ return await fetcher(controller.signal);
64
+ }
65
+ catch (error) {
66
+ lastError = error;
67
+ if (attempt < MAX_RETRIES) {
68
+ await new Promise((resolve) => {
69
+ setTimeout(resolve, RETRY_DELAY_MS);
70
+ });
71
+ }
72
+ }
73
+ finally {
74
+ clearTimeout(timer);
75
+ }
76
+ }
77
+ throw lastError;
78
+ }
79
+ /**
80
+ * Store repositories under a key and return the wrapped entry.
81
+ *
82
+ * @param cachedAt - optional ISO timestamp; defaults to now.
83
+ */
84
+ set(language, since, repositories, cachedAt) {
85
+ const entry = { repositories, cachedAt: cachedAt ?? new Date().toISOString() };
86
+ this.entries.set(cacheKey(language, since), entry);
87
+ return entry;
88
+ }
89
+ /**
90
+ * Fetch fresh data for all time windows and store them with a single timestamp.
91
+ *
92
+ * @param fetchers - fetcher for each time window.
93
+ * @param language - optional language filter.
94
+ * @returns the freshly cached entries for all windows.
95
+ */
96
+ async refreshAll(fetchers, language) {
97
+ const cachedAt = new Date().toISOString();
98
+ const results = {};
99
+ for (const [since, fetcher] of Object.entries(fetchers)) {
100
+ const repositories = await this.fetchWithRetry(fetcher);
101
+ results[since] = this.set(language, since, repositories, cachedAt);
102
+ }
103
+ return results;
104
+ }
105
+ /**
106
+ * Ensure a periodic refresh is running for the given key. Idempotent.
107
+ *
108
+ * @param fetcher - host-side fetcher.
109
+ * @param language - optional language filter.
110
+ * @param since - time window.
111
+ */
112
+ ensureScheduled(fetcher, language, since) {
113
+ const key = cacheKey(language, since);
114
+ if (this.timers.has(key))
115
+ return;
116
+ this.timers.set(key, setInterval(() => {
117
+ void this.refresh(fetcher, language, since).catch(() => { });
118
+ }, this.options.intervalMs));
119
+ }
120
+ /**
121
+ * Stop all background refresh timers. Call on plugin teardown.
122
+ */
123
+ dispose() {
124
+ for (const timer of this.timers.values())
125
+ clearInterval(timer);
126
+ this.timers.clear();
127
+ }
128
+ }
129
+ //# sourceMappingURL=cache.js.map