@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 +21 -0
- package/README.md +106 -0
- package/README.zh.md +102 -0
- package/cordis.patch.yml +6 -0
- package/lib/cache.js +129 -0
- package/lib/client.js +595 -0
- package/lib/constants.js +11 -0
- package/lib/index.js +147 -0
- package/lib/parser.js +121 -0
- package/lib/tool.js +236 -0
- package/lib/types/cache.d.ts +83 -0
- package/lib/types/client/GithubTrendingPanel.d.ts +23 -0
- package/lib/types/client/index.d.ts +24 -0
- package/lib/types/client/locales.d.ts +9 -0
- package/lib/types/client/store.d.ts +34 -0
- package/lib/types/constants.d.ts +11 -0
- package/lib/types/index.d.ts +41 -0
- package/lib/types/parser.d.ts +41 -0
- package/lib/types/tool.d.ts +118 -0
- package/package.json +121 -0
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 目前尚无原生右侧栏插槽。
|
package/cordis.patch.yml
ADDED
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
|