dsh-plugin-tool-management 0.1.2 → 0.1.3
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/README.md +43 -13
- package/README_EN.md +43 -13
- package/cordis.patch.yml +18 -0
- package/lib/agents-md/service.js +240 -0
- package/lib/client.js +637 -30
- package/lib/history/projcache.js +335 -0
- package/lib/history/tombstone.js +32 -0
- package/lib/history/workspace.js +892 -0
- package/lib/imports/parsers.js +181 -0
- package/lib/index.js +651 -1
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -3,24 +3,36 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/dsh-plugin-tool-management)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](package.json)
|
|
6
|
-
[](https://github.com/ouli-1242/dsh-plugin-tool-management)
|
|
6
|
+
[](https://github.com/ouli-1242/dsh-plugin-tool-management)
|
|
7
|
+
|
|
8
|
+
**简体中文** · [English](README_EN.md)
|
|
7
9
|
|
|
8
|
-
**DeepSeek Harness 的 MCP 服务与技能管理插件。**
|
|
10
|
+
**DeepSeek Harness 的 MCP 服务与技能管理插件。** 一个设置面板管好四件事:
|
|
9
11
|
|
|
10
12
|
- **MCP**:连接了哪些服务、每个服务有哪些工具、哪些工具该让模型用——增删改查、启停、重启,全部即改即生效;
|
|
11
|
-
- **Skills**:本机各处的技能(DSH / Agents / Codex / Claude / 项目级 /
|
|
13
|
+
- **Skills**:本机各处的技能(DSH / Agents / Codex / Claude / 项目级 / 你自己指定的任意目录)一目了然,逐个或整组启停、创建、导入、回收;
|
|
14
|
+
- **AGENTS.md**:管理多套全局指令基线预设,一键「应用」写入 `~/.dsh/AGENTS.md`,新会话生效、当前会话不变;
|
|
15
|
+
- **History**:已归档会话统一管理,按项目分组、批量恢复/删除,对话导入/导出,保留期自动清理。
|
|
12
16
|
|
|
13
17
|
不手改 `cordis.patch.yml`,不碰任何技能源文件,重启与升级后配置依旧。
|
|
14
18
|
|
|
15
19
|
---
|
|
16
20
|
|
|
17
|
-
<!-- 图片占位 1:MCP 管理页截图 → docs/images/mcp
|
|
21
|
+
<!-- 图片占位 1:MCP 管理页截图 → docs/images/mcp.png -->
|
|
18
22
|
|
|
19
|
-

|
|
20
24
|
|
|
21
|
-
<!-- 图片占位 2:Skills 管理页截图 → docs/images/skills
|
|
25
|
+
<!-- 图片占位 2:Skills 管理页截图 → docs/images/skills.png -->
|
|
22
26
|
|
|
23
|
-

|
|
28
|
+
|
|
29
|
+
<!-- 图片占位 3:AGENTS.md 预设页截图 → docs/images/agents-md.png -->
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
<!-- 图片占位 4:History 归档会话页截图 → docs/images/history.png -->
|
|
34
|
+
|
|
35
|
+

|
|
24
36
|
|
|
25
37
|
## 核心亮点
|
|
26
38
|
|
|
@@ -34,9 +46,12 @@
|
|
|
34
46
|
| 技能来源 | 接入 `~/.agents` / `~/.codex` / `~/.claude` 三个官方不加载的技能目录,并支持**自定义任意技能目录**(只读接入、重叠拒绝) |
|
|
35
47
|
| 技能操作 | 创建技能、ZIP/文件夹导入、插件回收站(恢复 / 永久删除 / 系统回收站兜底)、系统编辑器打开源文件 |
|
|
36
48
|
| 即时刷新 | 技能目录由后台线程监听,编辑器里改完技能页面自动刷新 |
|
|
37
|
-
|
|
|
49
|
+
| AGENTS.md 预设 | 多套全局指令基线预设库:新建 / 导入 / 编辑 / 应用 / 删除;「应用」写入 `~/.dsh/AGENTS.md`(新会话生效,当前会话不变) |
|
|
50
|
+
| 会话归档管理 | History 页按项目分组展示已归档会话:搜索、全选、批量恢复 / 永久删除、保留期自动清理(改保留期后倒计时以修改时间为基准重置) |
|
|
51
|
+
| 对话导入 / 导出 | 从 Claude Code / Cursor(JSONL)、Codex(Markdown)、任意文本无痛接管对话;导出可选会话范围,目录默认桌面,支持 Markdown / JSONL |
|
|
52
|
+
| 斜杠命令 | 聊天框直接输入 `/mcp`、`/skills`、`/agents-md` 查看状态 |
|
|
38
53
|
| 模型工具 | **7 个**:`skill_mcp_manager_*` 管 MCP,`skill_manager_*` 管技能(创建前需用户确认) |
|
|
39
|
-
| 界面 | 独立的 `dsm-*`
|
|
54
|
+
| 界面 | 独立的 `dsm-*` 设计系统,四页风格统一 |
|
|
40
55
|
|
|
41
56
|
## 快速开始
|
|
42
57
|
|
|
@@ -51,7 +66,7 @@ dsh plugin --profile web add dsh-plugin-tool-management@latest
|
|
|
51
66
|
dsh plugin --profile web remove dsh-plugin-tool-management
|
|
52
67
|
```
|
|
53
68
|
|
|
54
|
-
装完硬刷新浏览器(Cmd/Ctrl+Shift-R),设置里出现 **MCP** 与 **
|
|
69
|
+
装完硬刷新浏览器(Cmd/Ctrl+Shift-R),设置里出现 **MCP**、**Skills**、**AGENTS.md** 与 **History** 四页即安装成功(客户端改动由 DSH 热加载,无需重启)。
|
|
55
70
|
|
|
56
71
|
也可以直接对任意 DSH 会话说:
|
|
57
72
|
|
|
@@ -76,13 +91,26 @@ dsh plugin --profile web add dsh-plugin-tool-management@latest
|
|
|
76
91
|
- **看全貌**:按来源分组列出所有技能——项目级、运行时、内置、插件自带,以及四个用户目录(`~/.dsh` / `~/.agents` / `~/.codex` / `~/.claude`,后三个由本插件接入)和你自己添加的自定义目录。
|
|
77
92
|
- **启停**:单个技能、整个来源、整个项目,随时切换;实现是 override provider 的遮蔽策略,源文件一个字节都不动,换机或重装只要复制状态文件。
|
|
78
93
|
- **自定义目录**:点「添加目录」输入绝对路径,该目录即成为只读技能来源——适合管理散落在仓库、网盘同步目录里的技能合集;与已有来源重叠的路径会被拒绝,避免遮蔽失效。
|
|
79
|
-
- **创建与导入**:表单直接创建;ZIP、`.md`、技能文件夹拖进来就能装;删除先进回收站,可恢复,永久删除前还会尝试移入系统回收站兜底。
|
|
94
|
+
- **创建与导入**:表单直接创建;ZIP、`.md`、技能文件夹拖进来就能装;删除先进回收站,可恢复,永久删除前还会尝试移入系统回收站兜底。
|
|
95
|
+
|
|
96
|
+
### 管 AGENTS.md 预设
|
|
97
|
+
|
|
98
|
+
- **预设库**:新建、导入、编辑多套全局指令基线(如不同团队的 coding standard 或不同角色的行为规范)。
|
|
99
|
+
- **应用即写入**:「应用」把选中预设写入 `~/.dsh/AGENTS.md`,**新会话生效、当前会话不变**;编辑后点「重新应用」即可同步最新内容,删除前请先切换到其他预设。
|
|
100
|
+
|
|
101
|
+
### 管历史会话
|
|
102
|
+
|
|
103
|
+
- **按项目分组**:归档会话按工作区自动分组展示,搜索标题 / 会话 ID / 项目路径快速定位;工作区目录已不存在的会话会打上 ⚠ 标记。
|
|
104
|
+
- **批量操作**:「全选」后批量恢复或永久删除;恢复的会话回到工作区列表,删除会连同其子代理子会话一并清理。
|
|
105
|
+
- **保留期**:顶部下拉选择自动清理周期(0 = 永久保留),到期自动清除;修改保留期后,所有会话的倒计时以修改时间为基准重新计算。
|
|
106
|
+
- **导入对话**:无痛接管其他工具的会话——Claude Code / Cursor 的 JSONL、Codex 的 Markdown、以及任意文本格式,导入后即可继续对话。
|
|
107
|
+
- **导出对话**:按会话范围(全部 / 仅归档 / 按工作区)导出,每个会话一个 Markdown 或 JSONL 文件;导出目录默认桌面,旁边带「选择文件夹」按钮弹出目录树,逐级浏览选中后自动回填绝对路径。
|
|
80
108
|
|
|
81
109
|
### 让模型和脚本参与管理
|
|
82
110
|
|
|
83
111
|
| 入口 | 能做什么 |
|
|
84
112
|
|---|---|
|
|
85
|
-
| `/mcp`、`/skills` | 聊天框查看当前状态 |
|
|
113
|
+
| `/mcp`、`/skills`、`/agents-md` | 聊天框查看当前状态 |
|
|
86
114
|
| `skill_mcp_manager_list / set_enabled / restart / add` | 模型查询与操作 MCP 服务 |
|
|
87
115
|
| `skill_manager_list / set_enabled / create` | 模型查询与操作技能(创建前会征求你同意) |
|
|
88
116
|
| `POST /dsh-plugin-tool-management/api` | 脚本调用的 HTTP API(`{op, args}` 协议) |
|
|
@@ -106,6 +134,8 @@ dsh plugin --profile web add dsh-plugin-tool-management@latest
|
|
|
106
134
|
| 服务器备注 / 页面设置 / 工具停用列表 / 导出 | DSH 主目录下的旁路 JSON(`skill-mcp-manager-*.json`) |
|
|
107
135
|
| 技能启停策略 / 自定义目录 | `~/.dsh/tool-management/state.json` |
|
|
108
136
|
| 技能回收站 / 导入暂存 | `~/.dsh/tool-management/trash`、`uploads` |
|
|
137
|
+
| AGENTS.md 预设库 / 应用结果 | 插件目录 `data/agents-md-presets/`;「应用」写入 `~/.dsh/AGENTS.md` |
|
|
138
|
+
| 归档会话账本 / 保留期 | 插件目录 `data/history-archived-at.json`、`data/history-retention.json` |
|
|
109
139
|
| 运行日志 | `~/.dsh/dsh-plugin-tool-management.log`(滚动) |
|
|
110
140
|
|
|
111
141
|
## 常见问题
|
|
@@ -127,7 +157,7 @@ npm run test:fast # 跳过构建直接跑测试
|
|
|
127
157
|
npm run build # 仅构建(tsc + 同步客户端 bundle)
|
|
128
158
|
```
|
|
129
159
|
|
|
130
|
-
结构:宿主端 `src/index.ts`(Cordis 对象插件,`lib/index.js` 为发布产物);技能核心 `src/skills/core.js`(纯 Node
|
|
160
|
+
结构:宿主端 `src/index.ts`(Cordis 对象插件,`lib/index.js` 为发布产物);技能核心 `src/skills/core.js`(纯 Node,可独立单测);AGENTS.md 预设库 `src/agents-md/service.ts`;归档会话管理 `lib/history/`(`workspace.js` / `projcache.js` / `tombstone.js`);对话导入解析 `src/imports/parsers.js`;浏览器端 `src/client.js`(ModuleLoader CJS bundle,`dsm-*` 设计系统,经同源 API 与宿主通信)。运行时依赖仅 `fflate`(ZIP 解压)。
|
|
131
161
|
|
|
132
162
|
发布:`npm version patch && npm publish`(`prepublishOnly` 自动构建)。
|
|
133
163
|
|
package/README_EN.md
CHANGED
|
@@ -3,24 +3,36 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/dsh-plugin-tool-management)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](package.json)
|
|
6
|
-
[](https://github.com/ouli-1242/dsh-plugin-tool-management)
|
|
6
|
+
[](https://github.com/ouli-1242/dsh-plugin-tool-management)
|
|
7
|
+
|
|
8
|
+
[简体中文](README.md) · **English**
|
|
7
9
|
|
|
8
|
-
**An MCP server & skills manager for DeepSeek Harness.** One settings panel keeps
|
|
10
|
+
**An MCP server & skills manager for DeepSeek Harness.** One settings panel keeps four things under control:
|
|
9
11
|
|
|
10
12
|
- **MCP**: which servers are configured, what tools each one exposes, and which tools the model may call — add, edit, remove, toggle, restart; every change takes effect immediately;
|
|
11
|
-
- **Skills**: every skill on the machine (DSH / Agents / Codex / Claude / project-level / any directory you add) at a glance — toggle individually or per source, create, import, recycle
|
|
13
|
+
- **Skills**: every skill on the machine (DSH / Agents / Codex / Claude / project-level / any directory you add) at a glance — toggle individually or per source, create, import, recycle;
|
|
14
|
+
- **AGENTS.md**: keep multiple global instruction baselines as presets, apply one with a click to write `~/.dsh/AGENTS.md` — new sessions pick it up, current sessions stay unchanged;
|
|
15
|
+
- **History**: archived sessions in one place — grouped by project, batch restore / delete, import & export transcripts, retention-based auto-cleanup.
|
|
12
16
|
|
|
13
17
|
No hand-editing of `cordis.patch.yml`, and skill source files are never touched. Configuration survives restarts and upgrades.
|
|
14
18
|
|
|
15
19
|
---
|
|
16
20
|
|
|
17
|
-
<!-- Image slot 1: MCP management page screenshot → docs/images/mcp
|
|
21
|
+
<!-- Image slot 1: MCP management page screenshot → docs/images/mcp.png -->
|
|
18
22
|
|
|
19
|
-

|
|
20
24
|
|
|
21
|
-
<!-- Image slot 2: Skills management page screenshot → docs/images/skills
|
|
25
|
+
<!-- Image slot 2: Skills management page screenshot → docs/images/skills.png -->
|
|
22
26
|
|
|
23
|
-

|
|
28
|
+
|
|
29
|
+
<!-- Image slot 3: AGENTS.md presets page screenshot → docs/images/agents-md.png -->
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
<!-- Image slot 4: History archived sessions page screenshot → docs/images/history.png -->
|
|
34
|
+
|
|
35
|
+

|
|
24
36
|
|
|
25
37
|
## Highlights
|
|
26
38
|
|
|
@@ -34,9 +46,12 @@ No hand-editing of `cordis.patch.yml`, and skill source files are never touched.
|
|
|
34
46
|
| Skill sources | Hooks up `~/.agents` / `~/.codex` / `~/.claude` (three directories official DSH does not load) plus **any custom skill directory** you add (read-only, overlapping paths rejected) |
|
|
35
47
|
| Skill operations | Create skills, import ZIP / folders, plugin recycle bin (restore / permanent delete with OS-trash fallback), open the source file in the system editor |
|
|
36
48
|
| Live refresh | Skill directories are watched from a background thread — edits made in an editor show up automatically |
|
|
37
|
-
|
|
|
49
|
+
| AGENTS.md presets | Multiple global instruction baselines as presets — create / import / edit / apply / delete; "Apply" writes `~/.dsh/AGENTS.md` (new sessions pick it up, current sessions stay unchanged) |
|
|
50
|
+
| Archived session management | History page groups archived sessions by project: search, select-all, batch restore / permanent delete, retention-based auto-cleanup (changing the retention resets the countdown from the change time) |
|
|
51
|
+
| Transcript import / export | Seamlessly take over conversations from Claude Code / Cursor (JSONL), Codex (Markdown), or any text; export picks the session scope, defaults to the desktop, in Markdown / JSONL |
|
|
52
|
+
| Slash commands | `/mcp`, `/skills`, `/agents-md` right from the chat box |
|
|
38
53
|
| Model tools | **7 tools**: `skill_mcp_manager_*` for MCP servers, `skill_manager_*` for skills (creating asks for user confirmation first) |
|
|
39
|
-
| UI | Its own `dsm-*` design system, consistent across
|
|
54
|
+
| UI | Its own `dsm-*` design system, consistent across all four pages |
|
|
40
55
|
|
|
41
56
|
## Getting started
|
|
42
57
|
|
|
@@ -51,7 +66,7 @@ dsh plugin --profile web add dsh-plugin-tool-management@latest
|
|
|
51
66
|
dsh plugin --profile web remove dsh-plugin-tool-management
|
|
52
67
|
```
|
|
53
68
|
|
|
54
|
-
Hard-refresh the browser (Cmd/Ctrl+Shift-R) after installing — the **MCP** and **
|
|
69
|
+
Hard-refresh the browser (Cmd/Ctrl+Shift-R) after installing — the **MCP**, **Skills**, **AGENTS.md** and **History** pages appear in Settings (client changes are hot-loaded by DSH, no restart needed).
|
|
55
70
|
|
|
56
71
|
You can also tell any DSH session:
|
|
57
72
|
|
|
@@ -76,13 +91,26 @@ Then remind me to hard-refresh the browser.
|
|
|
76
91
|
- **See everything**: skills are grouped by source — project, runtime, built-in, plugin-shipped, the four user directories (`~/.dsh` / `~/.agents` / `~/.codex` / `~/.claude`; the last three are hooked up by this plugin) and any custom directories you added.
|
|
77
92
|
- **Toggle**: individual skills, whole sources or whole projects — implemented as an override-provider shadow policy, so not a single byte of the source file changes; moving machines is just copying the state file.
|
|
78
93
|
- **Custom directories**: click "Add directory", enter an absolute path, and that directory becomes a read-only skill source — ideal for skill collections living in repos or synced folders; overlapping paths are rejected to keep the shadow policy sound.
|
|
79
|
-
- **Create / import / recycle**: create from a form; drag in a ZIP, a `.md` file or a skill folder; deleted skills go to the plugin recycle bin first, and permanent delete still tries the OS trash as a last safety net.
|
|
94
|
+
- **Create / import / recycle**: create from a form; drag in a ZIP, a `.md` file or a skill folder; deleted skills go to the plugin recycle bin first, and permanent delete still tries the OS trash as a last safety net.
|
|
95
|
+
|
|
96
|
+
### Managing AGENTS.md presets
|
|
97
|
+
|
|
98
|
+
- **Preset library**: create, import and edit multiple global instruction baselines (e.g. different teams' coding standards or role behaviors).
|
|
99
|
+
- **Apply = write**: "Apply" writes the selected preset to `~/.dsh/AGENTS.md` — **new sessions pick it up, current sessions stay unchanged**; "Re-apply" syncs the latest content after editing; switch to another preset before deleting.
|
|
100
|
+
|
|
101
|
+
### Managing archived sessions
|
|
102
|
+
|
|
103
|
+
- **Grouped by project**: archived sessions are grouped by workspace automatically; search by title / session ID / project path; sessions whose workspace folder no longer exists are flagged with ⚠.
|
|
104
|
+
- **Batch operations**: "Select all" then batch-restore or permanently delete; restored sessions return to the workspace list, and deletion cascades to their subagent sessions.
|
|
105
|
+
- **Retention**: pick the cleanup period from the dropdown (0 = keep forever); the expiry baseline is the later of the archive time and the last retention change, so changing the retention resets the countdown.
|
|
106
|
+
- **Import conversations**: take over sessions from other tools — Claude Code / Cursor JSONL, Codex Markdown, and arbitrary text — and keep chatting right after import.
|
|
107
|
+
- **Export conversations**: pick a session scope (all / archived only / by workspace); each session becomes a Markdown or JSONL file; the export directory defaults to the desktop, and the adjacent "Select" button opens a directory tree to browse and fill in the absolute path.
|
|
80
108
|
|
|
81
109
|
### Let the model and scripts help
|
|
82
110
|
|
|
83
111
|
| Entry point | What it does |
|
|
84
112
|
|---|---|
|
|
85
|
-
| `/mcp`, `/skills` | Check the current state from the chat box |
|
|
113
|
+
| `/mcp`, `/skills`, `/agents-md` | Check the current state from the chat box |
|
|
86
114
|
| `skill_mcp_manager_list / set_enabled / restart / add` | Let the model query and operate MCP servers |
|
|
87
115
|
| `skill_manager_list / set_enabled / create` | Let the model query and operate skills (creating asks for your consent) |
|
|
88
116
|
| `POST /dsh-plugin-tool-management/api` | HTTP API for scripts (`{op, args}` protocol) |
|
|
@@ -106,6 +134,8 @@ Why a token: the cross-site protection (POST-only + custom header + same-origin
|
|
|
106
134
|
| Server notes / page settings / disabled tools / export | Sidecar JSON files under the DSH home (`dsh-plugin-tool-management-*.json`) |
|
|
107
135
|
| Skill toggle policy / custom directories | `~/.dsh/tool-management/state.json` |
|
|
108
136
|
| Skill recycle bin / import staging | `~/.dsh/tool-management/trash`, `uploads` |
|
|
137
|
+
| AGENTS.md presets / applied file | Plugin dir `data/agents-md-presets/`; "Apply" writes `~/.dsh/AGENTS.md` |
|
|
138
|
+
| Archived session ledger / retention | Plugin dir `data/history-archived-at.json`, `data/history-retention.json` |
|
|
109
139
|
| Runtime log | `~/.dsh/dsh-plugin-tool-management.log` (rolling) |
|
|
110
140
|
|
|
111
141
|
## FAQ
|
|
@@ -127,7 +157,7 @@ npm run test:fast # run tests without building
|
|
|
127
157
|
npm run build # build only (tsc + sync client bundle)
|
|
128
158
|
```
|
|
129
159
|
|
|
130
|
-
Layout: host half `src/index.ts` (object-form Cordis plugin, `lib/index.js` is the shipped artifact); skill core `src/skills/core.js` (pure Node, unit-testable); browser half `src/client.js` (ModuleLoader CJS bundle, `dsm-*` design system, talks to the host through the same-origin API). The only runtime dependency is `fflate` (ZIP extraction).
|
|
160
|
+
Layout: host half `src/index.ts` (object-form Cordis plugin, `lib/index.js` is the shipped artifact); skill core `src/skills/core.js` (pure Node, unit-testable); AGENTS.md presets `src/agents-md/service.ts`; archived session management `lib/history/` (`workspace.js` / `projcache.js` / `tombstone.js`); transcript import parsing `src/imports/parsers.js`; browser half `src/client.js` (ModuleLoader CJS bundle, `dsm-*` design system, talks to the host through the same-origin API). The only runtime dependency is `fflate` (ZIP extraction).
|
|
131
161
|
|
|
132
162
|
Publish: `npm version patch && npm publish` (`prepublishOnly` builds automatically).
|
|
133
163
|
|
package/cordis.patch.yml
CHANGED
|
@@ -25,7 +25,25 @@
|
|
|
25
25
|
# short-circuits on every unrelated entry (e.g. dsh-better-sidebar's own
|
|
26
26
|
# guard), so `!e.disabled` is only ever read on a genuinely matching
|
|
27
27
|
# legacy/aggregate row (a plain boolean, no re-entry).
|
|
28
|
+
# 折叠 dsh-archive-manager:替换官方 workspace 与 session-projection-cache
|
|
29
|
+
# 服务为归档感知子类(永久删除 + unarchive + 批量 + archivedAt 保留期账本)。
|
|
30
|
+
# 官方 ui-workspace 保持启用;归档菜单仍由官方 UI 提供,删除/恢复/保留期
|
|
31
|
+
# 由本插件的 TOOLS → History 页面经 HTTP API 驱动。
|
|
32
|
+
#
|
|
33
|
+
# 冲突告警:不要同时安装独立的 @michengai/dsh-archive-manager——两个补丁都
|
|
34
|
+
# 禁用官方 workspace 行并插入替换,会导致重复服务注册、启动失败。
|
|
35
|
+
- id: workspace
|
|
36
|
+
disabled: true
|
|
37
|
+
- id: session-projection-cache
|
|
38
|
+
disabled: true
|
|
28
39
|
- insert:
|
|
29
40
|
- id: dsh-plugin-tool-management
|
|
30
41
|
name: 'dsh-plugin-tool-management'
|
|
31
42
|
disabled: !!js "[...ctx.loader.entries()].some((e) => e.options.id !== 'dsh-plugin-tool-management' && e.options.name === 'dsh-plugin-tool-management' && !e.disabled)"
|
|
43
|
+
- id: workspace-tool-management-history
|
|
44
|
+
name: 'dsh-plugin-tool-management/workspace'
|
|
45
|
+
- id: session-projection-cache-tool-management-history
|
|
46
|
+
name: 'dsh-plugin-tool-management/projcache'
|
|
47
|
+
config:
|
|
48
|
+
writeEveryEvents: 200
|
|
49
|
+
writeIntervalMs: 5000
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// dsh-plugin-tool-management —— AGENTS.md 预设库 + 切换 服务层。
|
|
2
|
+
//
|
|
3
|
+
// DSH 全局指令基线只有一个文件 ~/.dsh/AGENTS.md(USER_GLOBAL_FILE 固定),
|
|
4
|
+
// 没有内置的「多份全局 AGENTS.md 切换」机制。本服务在插件目录内维护一个
|
|
5
|
+
// 预设库(每套一个子目录 + AGENTS.md),「应用」= 把选中预设内容写入
|
|
6
|
+
// ~/.dsh/AGENTS.md,新会话生效(当前会话不变,DSH 本身如此)。
|
|
7
|
+
//
|
|
8
|
+
// 约定镜像 @deepseek-ai/dsh-agent-presets:id 即目录名,正则
|
|
9
|
+
// /^[a-z0-9][a-z0-9-]*$/;__last-applied__ 是备份槽,不算用户预设。
|
|
10
|
+
// 「当前生效」靠比对 ~/.dsh/AGENTS.md 的 sha256 与各预设 sha256 推断,
|
|
11
|
+
// 无状态文件——用户手改全局文件也能如实反映。
|
|
12
|
+
import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
const PRESET_ID_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
16
|
+
const FILENAME = 'AGENTS.md';
|
|
17
|
+
const LAST_APPLIED_ID = '__last-applied__';
|
|
18
|
+
export function createAgentsMdService(_ctx, deps) {
|
|
19
|
+
const message = (e) => String((e && e.message) || e);
|
|
20
|
+
async function sha256OfFile(abs) {
|
|
21
|
+
try {
|
|
22
|
+
const buf = await readFile(abs, 'utf8');
|
|
23
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// 幂等初始化:仅当库完全无预设且全局 AGENTS.md 存在时,拷贝全局为 default
|
|
30
|
+
// 预设(首次运行引导)。用户已 create/apply 过任何预设则不插手,避免把
|
|
31
|
+
// 当前生效内容误存成 default 与真正生效的预设冲突。
|
|
32
|
+
async function ensureInit() {
|
|
33
|
+
const [presetsDir, globalPath] = await Promise.all([deps.presetsDir, deps.getGlobalAgentsMdPath()]);
|
|
34
|
+
let existing = [];
|
|
35
|
+
try {
|
|
36
|
+
existing = await readdir(presetsDir, { withFileTypes: true });
|
|
37
|
+
}
|
|
38
|
+
catch { /* 库不存在 */ }
|
|
39
|
+
const hasPreset = existing.some((e) => e.isDirectory() && PRESET_ID_RE.test(e.name) && e.name !== LAST_APPLIED_ID);
|
|
40
|
+
if (hasPreset)
|
|
41
|
+
return;
|
|
42
|
+
let globalContent;
|
|
43
|
+
try {
|
|
44
|
+
globalContent = await readFile(globalPath, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await mkdir(presetsDir, { recursive: true });
|
|
51
|
+
const defaultDir = join(presetsDir, 'default');
|
|
52
|
+
await mkdir(defaultDir, { recursive: true });
|
|
53
|
+
await writeFile(join(defaultDir, FILENAME), globalContent, 'utf8');
|
|
54
|
+
}
|
|
55
|
+
catch { /* best effort */ }
|
|
56
|
+
}
|
|
57
|
+
async function list() {
|
|
58
|
+
try {
|
|
59
|
+
await ensureInit();
|
|
60
|
+
const [presetsDir, globalPath] = await Promise.all([deps.presetsDir, deps.getGlobalAgentsMdPath()]);
|
|
61
|
+
let entries = [];
|
|
62
|
+
try {
|
|
63
|
+
entries = await readdir(presetsDir, { withFileTypes: true });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// 库目录不存在视作空(首次运行尚未创建)
|
|
67
|
+
}
|
|
68
|
+
const globalHash = await sha256OfFile(globalPath);
|
|
69
|
+
const ids = entries
|
|
70
|
+
.filter((e) => e.isDirectory() && PRESET_ID_RE.test(e.name) && e.name !== LAST_APPLIED_ID)
|
|
71
|
+
.map((e) => e.name);
|
|
72
|
+
const presets = await Promise.all(ids.map(async (id) => {
|
|
73
|
+
const contentHash = await sha256OfFile(join(presetsDir, id, FILENAME));
|
|
74
|
+
return {
|
|
75
|
+
id,
|
|
76
|
+
active: globalHash !== null && contentHash !== null && globalHash === contentHash,
|
|
77
|
+
};
|
|
78
|
+
}));
|
|
79
|
+
presets.sort((a, b) => a.id.localeCompare(b.id));
|
|
80
|
+
return { ok: true, presets };
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
return { ok: false, error: message(e) };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function read(id) {
|
|
87
|
+
const safeId = String(id || '');
|
|
88
|
+
// id 即目录名,严格校验防目录穿越:仅允许小写字母/数字/连字符。
|
|
89
|
+
// __last-applied__ 含下划线会被拒(它是备份槽,不走 read,由 apply 内部直写)。
|
|
90
|
+
if (!PRESET_ID_RE.test(safeId))
|
|
91
|
+
return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
|
|
92
|
+
try {
|
|
93
|
+
const content = await readFile(join(deps.presetsDir, safeId, FILENAME), 'utf8');
|
|
94
|
+
return { ok: true, content };
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return { ok: false, error: '预设不存在:' + safeId };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function create(id, from) {
|
|
101
|
+
const safeId = String(id || '');
|
|
102
|
+
if (!PRESET_ID_RE.test(safeId))
|
|
103
|
+
return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
|
|
104
|
+
const dir = join(deps.presetsDir, safeId);
|
|
105
|
+
try {
|
|
106
|
+
await stat(dir);
|
|
107
|
+
return { ok: false, error: 'id 已存在:' + safeId };
|
|
108
|
+
}
|
|
109
|
+
catch { /* 不存在,继续 */ }
|
|
110
|
+
let content;
|
|
111
|
+
const srcId = String(from || '');
|
|
112
|
+
if (srcId) {
|
|
113
|
+
if (!PRESET_ID_RE.test(srcId))
|
|
114
|
+
return { ok: false, error: '非法 from id:' + srcId };
|
|
115
|
+
try {
|
|
116
|
+
content = await readFile(join(deps.presetsDir, srcId, FILENAME), 'utf8');
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { ok: false, error: '来源预设不存在:' + srcId };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
content = '# AGENTS.md\n\n(DSH 全局指令基线预设,待编辑)\n';
|
|
124
|
+
}
|
|
125
|
+
await mkdir(dir, { recursive: true });
|
|
126
|
+
await writeFile(join(dir, FILENAME), content, 'utf8');
|
|
127
|
+
return { ok: true, id: safeId };
|
|
128
|
+
}
|
|
129
|
+
async function update(id, content) {
|
|
130
|
+
const safeId = String(id || '');
|
|
131
|
+
if (!PRESET_ID_RE.test(safeId))
|
|
132
|
+
return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
|
|
133
|
+
const dir = join(deps.presetsDir, safeId);
|
|
134
|
+
try {
|
|
135
|
+
await stat(dir);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return { ok: false, error: '预设不存在:' + safeId };
|
|
139
|
+
}
|
|
140
|
+
await writeFile(join(dir, FILENAME), String(content ?? ''), 'utf8');
|
|
141
|
+
return { ok: true, id: safeId };
|
|
142
|
+
}
|
|
143
|
+
async function apply(id) {
|
|
144
|
+
const safeId = String(id || '');
|
|
145
|
+
if (!PRESET_ID_RE.test(safeId))
|
|
146
|
+
return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
|
|
147
|
+
let content;
|
|
148
|
+
try {
|
|
149
|
+
content = await readFile(join(deps.presetsDir, safeId, FILENAME), 'utf8');
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return { ok: false, error: '预设不存在:' + safeId };
|
|
153
|
+
}
|
|
154
|
+
const globalPath = await deps.getGlobalAgentsMdPath();
|
|
155
|
+
// 备份当前全局内容(若存在)到 __last-applied__,再覆盖写入。
|
|
156
|
+
let prev = null;
|
|
157
|
+
try {
|
|
158
|
+
prev = await readFile(globalPath, 'utf8');
|
|
159
|
+
}
|
|
160
|
+
catch { /* 当前不存在,不备份 */ }
|
|
161
|
+
if (prev !== null) {
|
|
162
|
+
const backupDir = join(deps.presetsDir, LAST_APPLIED_ID);
|
|
163
|
+
await mkdir(backupDir, { recursive: true });
|
|
164
|
+
await writeFile(join(backupDir, FILENAME), prev, 'utf8');
|
|
165
|
+
}
|
|
166
|
+
await writeFile(globalPath, content, 'utf8');
|
|
167
|
+
return { ok: true, id: safeId, backedUp: prev !== null };
|
|
168
|
+
}
|
|
169
|
+
async function getCurrent() {
|
|
170
|
+
try {
|
|
171
|
+
await ensureInit();
|
|
172
|
+
const [globalPath, presetsDir] = await Promise.all([deps.getGlobalAgentsMdPath(), deps.presetsDir]);
|
|
173
|
+
let content;
|
|
174
|
+
try {
|
|
175
|
+
content = await readFile(globalPath, 'utf8');
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return { ok: true, content: '', presetId: null, exists: false };
|
|
179
|
+
}
|
|
180
|
+
const globalHash = createHash('sha256').update(content).digest('hex');
|
|
181
|
+
let presetId = null;
|
|
182
|
+
let entries = [];
|
|
183
|
+
try {
|
|
184
|
+
entries = await readdir(presetsDir, { withFileTypes: true });
|
|
185
|
+
}
|
|
186
|
+
catch { /* 库不存在 */ }
|
|
187
|
+
for (const e of entries) {
|
|
188
|
+
if (!e.isDirectory() || !PRESET_ID_RE.test(e.name) || e.name === LAST_APPLIED_ID)
|
|
189
|
+
continue;
|
|
190
|
+
let c;
|
|
191
|
+
try {
|
|
192
|
+
c = await readFile(join(presetsDir, e.name, FILENAME), 'utf8');
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (createHash('sha256').update(c).digest('hex') === globalHash) {
|
|
198
|
+
presetId = e.name;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { ok: true, content, presetId, exists: true };
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
return { ok: false, error: message(e) };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function remove(id) {
|
|
209
|
+
const safeId = String(id || '');
|
|
210
|
+
if (safeId === LAST_APPLIED_ID)
|
|
211
|
+
return { ok: false, error: '备份槽不可删除' };
|
|
212
|
+
if (!PRESET_ID_RE.test(safeId))
|
|
213
|
+
return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
|
|
214
|
+
const dir = join(deps.presetsDir, safeId);
|
|
215
|
+
try {
|
|
216
|
+
await stat(dir);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return { ok: false, error: '预设不存在:' + safeId };
|
|
220
|
+
}
|
|
221
|
+
await rm(dir, { recursive: true, force: true });
|
|
222
|
+
return { ok: true, id: safeId };
|
|
223
|
+
}
|
|
224
|
+
// 从外部文本内容(如导入的 .md 文件)建预设:id 校验 + 重复检查 + 写 AGENTS.md。
|
|
225
|
+
async function importPreset(id, content) {
|
|
226
|
+
const safeId = String(id || '');
|
|
227
|
+
if (!PRESET_ID_RE.test(safeId))
|
|
228
|
+
return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
|
|
229
|
+
const dir = join(deps.presetsDir, safeId);
|
|
230
|
+
try {
|
|
231
|
+
await stat(dir);
|
|
232
|
+
return { ok: false, error: 'id 已存在:' + safeId };
|
|
233
|
+
}
|
|
234
|
+
catch { /* 不存在,继续 */ }
|
|
235
|
+
await mkdir(dir, { recursive: true });
|
|
236
|
+
await writeFile(join(dir, FILENAME), String(content ?? ''), 'utf8');
|
|
237
|
+
return { ok: true, id: safeId };
|
|
238
|
+
}
|
|
239
|
+
return { list, read, create, update, apply, getCurrent, remove, importPreset };
|
|
240
|
+
}
|