dsh-skill-importer 0.2.1 → 0.2.2

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 CHANGED
@@ -171,7 +171,7 @@ If `dsh` is not installed globally, replace the last line with `npx @deepseek-ai
171
171
  ## Notes
172
172
 
173
173
  - A single imported file may be up to 256 KB.
174
- - Batch import accepts `.claude/skills`, `.codex/skills`, `.agents/skills`, `.dsh/skills`, or one skill directory directly below those roots. A scan accepts up to 200 skills; each skill may contain up to 2,000 files and 10 MB of resources. Symbolic links are refused.
174
+ - Batch import accepts any agent directory named `skills` (for example `.claude/skills`, `.codex/skills`, or a custom `agent/skills`) or one skill directory directly below it. A scan accepts up to 200 skills; each skill may contain up to 2,000 files and 10 MB of resources. Symbolic links are refused.
175
175
  - URL import is HTTPS-only and refuses loopback, private, link-local, and reserved addresses; every redirect is validated again. `.md` sources are preserved, while HTML pages use lightweight text extraction, so direct Markdown URLs work best.
176
176
  - The installed list polls briefly after import (every 2 seconds for up to 20 seconds). **Refresh** syncs external changes immediately.
177
177
 
package/README.zh.md CHANGED
@@ -171,7 +171,7 @@ dsh plugin --profile web add /path/to/dsh-skill-importer
171
171
  ## 注意事项
172
172
 
173
173
  - 单个导入文件最大 256 KB。
174
- - 批量导入仅接受 `.claude/skills`、`.codex/skills`、`.agents/skills`、`.dsh/skills` 或其中的单个技能目录;每次最多扫描 200 个技能,单个技能最多包含 2,000 个文件和 10 MB 资源,不接受符号链接。
174
+ - 批量导入接受任意 Agent 中名为 `skills` 的目录(例如 `.claude/skills`、`.codex/skills`、项目自定义的 `agent/skills`)或其中的单个技能目录;每次最多扫描 200 个技能,单个技能最多包含 2,000 个文件和 10 MB 资源,不接受符号链接。
175
175
  - URL 导入仅支持 HTTPS,并拒绝本机、私有网络、链路本地和保留地址;每次重定向都会重新校验。`.md` 会原样保留,HTML 页面仅做轻量正文提取,因此推荐使用 Markdown 直链。
176
176
  - 导入后列表会短暂轮询(每 2 秒一次,最多 20 秒);外部改动可点击 **刷新** 立即同步。
177
177
 
package/lib/index.js CHANGED
@@ -99,12 +99,6 @@ const MAX_BATCH_FILES_PER_SKILL = 2e3;
99
99
  const MAX_BATCH_SKILL_BYTES = 10485760;
100
100
  const BATCH_SCAN_TTL_MS = 6e5;
101
101
  const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
102
- const AGENT_SKILL_PARENTS = /* @__PURE__ */ new Set([
103
- ".agents",
104
- ".claude",
105
- ".codex",
106
- ".dsh"
107
- ]);
108
102
  const MAX_URL_REDIRECTS = 5;
109
103
  /** The harness home (`$DSH_HOME`, defaulting to `~/.dsh`). */
110
104
  function dshHomeDir() {
@@ -311,12 +305,9 @@ function scanBatch(request) {
311
305
  if (!isAbsolute(request.sourcePath)) throw new Error("批量导入目录必须是绝对路径");
312
306
  if (!existsSync(request.sourcePath) || !statSync(request.sourcePath).isDirectory()) throw new Error("批量导入目录不存在或不可读");
313
307
  const sourcePath = realpathSync(request.sourcePath);
314
- const leaf = basename(sourcePath);
315
- const parent = basename(dirname(sourcePath));
316
- const grandparent = basename(dirname(dirname(sourcePath)));
317
- const isSkillsRoot = leaf === "skills" && AGENT_SKILL_PARENTS.has(parent);
318
- const isSkillDirectory = parent === "skills" && AGENT_SKILL_PARENTS.has(grandparent);
319
- if (!isSkillsRoot && !isSkillDirectory) throw new Error("批量导入仅支持 .claude、.codex、.agents 或 .dsh 下的 skills 目录");
308
+ const leaf = basename(sourcePath).toLowerCase();
309
+ const parent = basename(dirname(sourcePath)).toLowerCase();
310
+ if (!(leaf === "skills") && !(parent === "skills")) throw new Error("批量导入仅支持名为 skills 的目录或其中的单个技能目录");
320
311
  if (request.target !== "user" && request.workspacePath === void 0) throw new Error("项目目标需要 workspacePath(当前工作区路径)");
321
312
  const sources = batchSources(sourcePath);
322
313
  if (sources.length === 0) throw new Error("所选目录中没有找到可导入的技能");
@@ -761,11 +752,17 @@ function apply(ctx) {
761
752
  }
762
753
  const workspacePath = requireWorkspace(body.target, body.workspacePath);
763
754
  pruneBatchScans();
764
- const session = scanBatch({
765
- sourcePath: body.sourcePath,
766
- target: body.target,
767
- workspacePath
768
- });
755
+ let session;
756
+ try {
757
+ session = scanBatch({
758
+ sourcePath: body.sourcePath,
759
+ target: body.target,
760
+ workspacePath
761
+ });
762
+ } catch (error) {
763
+ sendError(res, 400, error instanceof Error ? error.message : String(error));
764
+ return;
765
+ }
769
766
  batchScans.set(session.scanId, session);
770
767
  sendJson(res, 200, {
771
768
  ok: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-skill-importer",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Web UI plugin for DeepSeek Harness: import skills from local Markdown files or URLs into the skill roots the harness discovers automatically",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
package/src/server.ts CHANGED
@@ -34,7 +34,6 @@ export const MAX_BATCH_SKILL_BYTES = 10 * 1024 * 1024
34
34
  export const BATCH_SCAN_TTL_MS = 10 * 60 * 1000
35
35
 
36
36
  const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
37
- const AGENT_SKILL_PARENTS = new Set(['.agents', '.claude', '.codex', '.dsh'])
38
37
  const MAX_URL_REDIRECTS = 5
39
38
 
40
39
  /** The harness home (`$DSH_HOME`, defaulting to `~/.dsh`). */
@@ -284,13 +283,12 @@ export function scanBatch(request: BatchScanRequest): BatchScanSession {
284
283
  if (!isAbsolute(request.sourcePath)) throw new Error('批量导入目录必须是绝对路径')
285
284
  if (!existsSync(request.sourcePath) || !statSync(request.sourcePath).isDirectory()) throw new Error('批量导入目录不存在或不可读')
286
285
  const sourcePath = realpathSync(request.sourcePath)
287
- const leaf = basename(sourcePath)
288
- const parent = basename(dirname(sourcePath))
289
- const grandparent = basename(dirname(dirname(sourcePath)))
290
- const isSkillsRoot = leaf === 'skills' && AGENT_SKILL_PARENTS.has(parent)
291
- const isSkillDirectory = parent === 'skills' && AGENT_SKILL_PARENTS.has(grandparent)
286
+ const leaf = basename(sourcePath).toLowerCase()
287
+ const parent = basename(dirname(sourcePath)).toLowerCase()
288
+ const isSkillsRoot = leaf === 'skills'
289
+ const isSkillDirectory = parent === 'skills'
292
290
  if (!isSkillsRoot && !isSkillDirectory) {
293
- throw new Error('批量导入仅支持 .claude、.codex、.agents 或 .dsh 下的 skills 目录')
291
+ throw new Error('批量导入仅支持名为 skills 的目录或其中的单个技能目录')
294
292
  }
295
293
  if (request.target !== 'user' && request.workspacePath === undefined) throw new Error('项目目标需要 workspacePath(当前工作区路径)')
296
294
  const sources = batchSources(sourcePath)
@@ -70,13 +70,19 @@ try {
70
70
  assert.match(changedResults[0]?.message ?? '', /发生变化/)
71
71
  assert.equal(existsSync(join(process.env.DSH_HOME, 'skills', 'beta')), false)
72
72
 
73
+ // Any agent may use its own parent directory; the selected root name is the boundary.
74
+ const customAgentSource = join(sandbox, 've_hcs_agent', 'skills')
75
+ mkdirSync(join(customAgentSource, 'gamma'), { recursive: true })
76
+ writeFileSync(join(customAgentSource, 'gamma', 'SKILL.md'), skill('gamma'))
77
+ assert.equal(scanBatch({ sourcePath: customAgentSource, target: 'user' }).entries[0]?.name, 'gamma')
78
+
73
79
  const empty = join(sandbox, '.agents', 'skills')
74
80
  mkdirSync(empty, { recursive: true })
75
81
  assert.throws(() => scanBatch({ sourcePath: empty, target: 'user' }), /没有找到/)
76
82
 
77
83
  const arbitrary = join(sandbox, 'arbitrary')
78
84
  mkdirSync(arbitrary)
79
- assert.throws(() => scanBatch({ sourcePath: arbitrary, target: 'user' }), /仅支持/)
85
+ assert.throws(() => scanBatch({ sourcePath: arbitrary, target: 'user' }), /名为 skills/)
80
86
 
81
87
  assert.equal(originAllowed({ headers: {} }), false)
82
88
  assert.equal(originAllowed({ headers: { origin: 'https://evil.example' } }), false)