dsh-skills-manager 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.
@@ -0,0 +1,108 @@
1
+ import { isAbsolute, relative, sep } from 'node:path';
2
+ export const EMPTY_STATE = {
3
+ version: 1,
4
+ skills: {},
5
+ globalEnabled: [],
6
+ projectEnabled: {},
7
+ };
8
+ export function normalizeState(value) {
9
+ if (!isRecord(value) || value.version !== 1 || !isRecord(value.skills) || !Array.isArray(value.globalEnabled) || !isRecord(value.projectEnabled)) {
10
+ throw new Error('skill-manager state.json has an unsupported shape');
11
+ }
12
+ const skills = {};
13
+ for (const [name, entry] of Object.entries(value.skills)) {
14
+ if (!isRecord(entry) || entry.name !== name || typeof entry.description !== 'string' || (entry.layout !== 'directory' && entry.layout !== 'file') || typeof entry.installedAt !== 'string') {
15
+ throw new Error(`skill-manager state.json has an invalid skill entry: ${name}`);
16
+ }
17
+ skills[name] = {
18
+ name,
19
+ description: entry.description,
20
+ layout: entry.layout,
21
+ installedAt: entry.installedAt,
22
+ ...(typeof entry.updatedAt === 'string' ? { updatedAt: entry.updatedAt } : {}),
23
+ ...(entry.sourceType === 'local' || entry.sourceType === 'git' ? { sourceType: entry.sourceType } : {}),
24
+ ...(typeof entry.sourcePath === 'string' && isAbsolute(entry.sourcePath) ? { sourcePath: entry.sourcePath } : {}),
25
+ ...(typeof entry.sourceUrl === 'string' ? { sourceUrl: entry.sourceUrl } : {}),
26
+ ...(typeof entry.sourceSubpath === 'string' ? { sourceSubpath: entry.sourceSubpath } : {}),
27
+ ...(typeof entry.contentHash === 'string' ? { contentHash: entry.contentHash } : {}),
28
+ };
29
+ }
30
+ return {
31
+ version: 1,
32
+ skills,
33
+ globalEnabled: cleanNames(value.globalEnabled, skills),
34
+ projectEnabled: cleanProjects(value.projectEnabled, skills),
35
+ };
36
+ }
37
+ export function setEnabled(state, name, scope, projectRoot, enabled) {
38
+ if (!(name in state.skills))
39
+ throw new Error(`skill is not installed: ${name}`);
40
+ if (scope === 'project' && !projectRoot)
41
+ throw new Error('projectRoot is required for project scope');
42
+ if (scope === 'global') {
43
+ return {
44
+ ...state,
45
+ globalEnabled: updateNames(state.globalEnabled, name, enabled),
46
+ };
47
+ }
48
+ const current = state.projectEnabled[projectRoot] ?? [];
49
+ const next = updateNames(current, name, enabled);
50
+ const projectEnabled = { ...state.projectEnabled };
51
+ if (next.length === 0)
52
+ delete projectEnabled[projectRoot];
53
+ else
54
+ projectEnabled[projectRoot] = next;
55
+ return { ...state, projectEnabled };
56
+ }
57
+ export function removeSkill(state, name) {
58
+ if (!(name in state.skills))
59
+ throw new Error(`skill is not installed: ${name}`);
60
+ const skills = { ...state.skills };
61
+ delete skills[name];
62
+ return {
63
+ version: 1,
64
+ skills,
65
+ globalEnabled: state.globalEnabled.filter(item => item !== name),
66
+ projectEnabled: Object.fromEntries(Object.entries(state.projectEnabled)
67
+ .map(([root, names]) => [root, names.filter(item => item !== name)])
68
+ .filter(([, names]) => names.length > 0)),
69
+ };
70
+ }
71
+ export function isPathWithin(root, target) {
72
+ if (!isAbsolute(root) || !isAbsolute(target))
73
+ return false;
74
+ const child = relative(root, target);
75
+ return child === '' || (child !== '..' && !child.startsWith(`..${sep}`));
76
+ }
77
+ export function isEnabledFor(state, name, cwd) {
78
+ if (state.globalEnabled.includes(name))
79
+ return true;
80
+ if (!cwd)
81
+ return false;
82
+ return Object.entries(state.projectEnabled).some(([root, names]) => names.includes(name) && isPathWithin(root, cwd));
83
+ }
84
+ function updateNames(names, name, enabled) {
85
+ const next = new Set(names);
86
+ if (enabled)
87
+ next.add(name);
88
+ else
89
+ next.delete(name);
90
+ return [...next].sort();
91
+ }
92
+ function cleanNames(value, skills) {
93
+ return [...new Set(value.filter((item) => typeof item === 'string' && item in skills))].sort();
94
+ }
95
+ function cleanProjects(value, skills) {
96
+ const result = {};
97
+ for (const [root, names] of Object.entries(value)) {
98
+ if (!isAbsolute(root) || !Array.isArray(names))
99
+ continue;
100
+ const clean = cleanNames(names, skills);
101
+ if (clean.length > 0)
102
+ result[root] = clean;
103
+ }
104
+ return result;
105
+ }
106
+ function isRecord(value) {
107
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
108
+ }
@@ -0,0 +1,33 @@
1
+ export type SkillScope = 'global' | 'project';
2
+ export type SkillLayout = 'directory' | 'file';
3
+ export type SkillSourceType = 'local' | 'git';
4
+ export interface InstalledSkill {
5
+ readonly name: string;
6
+ readonly description: string;
7
+ readonly layout: SkillLayout;
8
+ readonly installedAt: string;
9
+ readonly updatedAt?: string;
10
+ readonly sourceType?: SkillSourceType;
11
+ readonly sourcePath?: string;
12
+ readonly sourceUrl?: string;
13
+ readonly sourceSubpath?: string;
14
+ readonly contentHash?: string;
15
+ }
16
+ export interface ManagerState {
17
+ readonly version: 1;
18
+ readonly skills: Readonly<Record<string, InstalledSkill>>;
19
+ readonly globalEnabled: readonly string[];
20
+ readonly projectEnabled: Readonly<Record<string, readonly string[]>>;
21
+ }
22
+ export interface SkillCatalogEntry extends InstalledSkill {
23
+ readonly globalEnabled: boolean;
24
+ readonly projectEnabled: boolean;
25
+ readonly effectiveEnabled: boolean;
26
+ readonly updateSupported: boolean;
27
+ readonly updateAvailable: boolean;
28
+ readonly updateError?: string;
29
+ }
30
+ export interface SkillCatalog {
31
+ readonly entries: readonly SkillCatalogEntry[];
32
+ readonly projectRoot?: string;
33
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "dsh-skills-manager",
3
+ "version": "0.1.0",
4
+ "description": "A DeepSeek Harness plugin for installing and managing skills by global or workspace scope.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Xichun123/dsh-skills-manager.git"
8
+ },
9
+ "homepage": "https://github.com/Xichun123/dsh-skills-manager#readme",
10
+ "bugs": "https://github.com/Xichun123/dsh-skills-manager/issues",
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "provenance": true
14
+ },
15
+ "type": "module",
16
+ "main": "lib/index.js",
17
+ "types": "lib/types/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./lib/types/index.d.ts",
21
+ "default": "./lib/index.js"
22
+ },
23
+ "./client": {
24
+ "types": "./lib/types/client/index.d.ts",
25
+ "default": "./lib/client.js"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "files": [
30
+ "lib",
31
+ "cordis.patch.yml",
32
+ "README.md",
33
+ "plugin-design.md"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json && tsdown",
37
+ "prepare": "npm run build",
38
+ "test": "npm run build && node --test test/*.test.mjs",
39
+ "validate": "python3 .agents/skills/dsh-plugin-builder/scripts/validate_dsh_plugin.py ."
40
+ },
41
+ "dsh": {
42
+ "bundle": {
43
+ "patch": "./cordis.patch.yml"
44
+ },
45
+ "client": {
46
+ "inject": [
47
+ "@deepseek-ai/dsh-client-connection",
48
+ "@deepseek-ai/dsh-client-runtime",
49
+ "@deepseek-ai/dsh-client-ui-conversation",
50
+ "@deepseek-ai/dsh-client-ui-settings"
51
+ ],
52
+ "platform": "web"
53
+ }
54
+ },
55
+ "peerDependencies": {
56
+ "@deepseek-ai/cordis": "^4.0.1",
57
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
58
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
59
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
60
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
61
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
62
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
63
+ "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
64
+ "@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.6",
65
+ "@deepseek-ai/schemastery": "^3.18.1",
66
+ "react": "^18.2.0"
67
+ },
68
+ "dependencies": {
69
+ "lucide-react": "^1.31.0",
70
+ "yaml": "^2.4.2"
71
+ },
72
+ "devDependencies": {
73
+ "@deepseek-ai/cordis": "^4.0.1",
74
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
75
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
76
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
77
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
78
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
79
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
80
+ "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
81
+ "@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.6",
82
+ "@deepseek-ai/schemastery": "^3.18.1",
83
+ "@types/node": "^22.0.0",
84
+ "@types/react": "~18.3.1",
85
+ "react": "^18.2.0",
86
+ "tsdown": "^0.15.0",
87
+ "typescript": "^5.9.0"
88
+ },
89
+ "keywords": [
90
+ "dsh-plugin",
91
+ "deepseek-harness",
92
+ "skill-manager",
93
+ "skills"
94
+ ],
95
+ "license": "UNLICENSED"
96
+ }
@@ -0,0 +1,22 @@
1
+ # Plugin Design
2
+
3
+ ## 形态决策
4
+
5
+ - 目标:在 DeepSeek Harness 中统一管理可安装 skill,并按全局或工作区启用。
6
+ - 做插件:是。
7
+ - 树外 / first-party:树外独立 npm 包 `dsh-skills-manager`。
8
+ - 形态:Host skill provider + Host RPC + Client Settings UI。
9
+ - 挂载点:`ctx.skills.registerProvider()`、`ctx.connection.rpc.handle('/skill-manager', ...)`、`settings.section`、`conversation.input.left`。
10
+ - 不选其他形态的原因:不是 Agent Skill 文件本身,也不需要改 agent-loop、注册新工具或替代 MCP;只需要改变 skill 的发现范围并提供管理界面。
11
+ - 拆 seam:否。管理状态、provider 和 RPC 是同一产品能力,暂不拆成多个包。
12
+
13
+ ## 行为
14
+
15
+ - 安装库位于 `$DSH_HOME/skill-manager/library`,状态位于 `$DSH_HOME/skill-manager/state.json`。
16
+ - 导入 skill 时复制完整目录,因此 `references/`、`scripts/`、`assets/` 等伴随资源保留。
17
+ - 安装状态记录本机来源路径,或 Git 仓库 URL 与仓库内 skill 子路径,并保存完整内容哈希;检查更新时重新读取本机来源或浅克隆仓库比较哈希,更新通过临时副本和备份原子替换,并保留启用范围。
18
+ - Git 支持 GitHub `owner/repo`、HTTPS、SSH、Git 与 file URL;私有仓库复用本机 Git credential helper 或 SSH,拒绝 URL 内嵌凭据且不保存 token。
19
+ - 新导入 skill 默认关闭;全局启用会对所有 cwd 生效,项目启用会对所选工作区及其子目录生效。
20
+ - Provider 复用 `@deepseek-ai/dsh-skill-filesystem` 的解析、资源和 watcher 能力,只过滤由本插件管理的库条目。
21
+ - 设置页使用 `settings.section` 作为左侧一级入口;输入框 `+` 右边的扳手图标控件打开当前会话 cwd 的项目级开关。
22
+ - RPC 仅注册 loopback authority,因为导入和持久化操作会触碰本机文件系统。