dsh-skill-picker 0.5.7 → 0.5.9
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 +2 -0
- package/lib/index.js +48 -23
- package/lib/index.js.map +4 -4
- package/package.json +7 -3
- package/src/dir-entry.js +38 -0
- package/src/index.js +6 -2
- package/src/patch-ui-skill.js +26 -8
package/README.md
CHANGED
|
@@ -124,6 +124,7 @@ DSH 的 [dsh-tool-skill](https://github.com/deepseek-ai/deepseek-harness) 在 `a
|
|
|
124
124
|
|
|
125
125
|
## 更新日志
|
|
126
126
|
|
|
127
|
+
- **v0.5.9**:**修复符号链接 / Junction 型技能查不到(对应 issue #6)**——扫描技能目录时 `readdir` 的 `Dirent` 走的是 lstat 语义:Windows 下符号链接**和 Junction** 都报告 `isDirectory() === false` / `isSymbolicLink() === true`,于是链接型技能(如 `~/.agents/skills/neat` → `D:\repos\icraft-toolkit\skills\neat`)在第 79 行的目录过滤里被静默 `continue` 掉。现在链接条目改用 `stat`(跟随链接)判定真实类型:链接型技能与普通目录**完全一视同仁**,断链或指向普通文件的链接安全跳过(不再抛错、也不占用列表)。四个扫描根(`~/.agents/skills`、`~/.dsh/skills`、项目级 `.agents/skills` / `.dsh/skills`)与 profile 枚举路径全部受益;新增 `npm test`(`node --test`)回归用例:普通目录、链接目录、链接+普通混排、断链、无 `SKILL.md` 的链接、项目级链接技能
|
|
127
128
|
- **v0.5.7**:**搜索结果按匹配相关度排序**——⚡ 面板与 `/` 补全统一:名字开头匹配 > 名字包含 > 描述 > 拼音,置顶/最近使用只做同级次序;同时过滤掉纯粹"字母分散"的子序列噪音(如搜 `svg` 不再混入 deepseek/openviking 等恰好含 s-v-g 分散字母的技能)。`svg` → svg-diagram 稳居第一
|
|
128
129
|
- **v0.5.6**:**AI 安装指引升级为「GitHub 直装优先」**——快速安装部分改为给 AI/安装助手的优先级决策树:①要最新版/不确定 → `git+ssh` GitHub 直装(git 依赖拉最新 commit,**天然绕过 npm 24h 门禁,百分百新版**);②要 npm 正式版 → 先 `npm view` 查版本再指定 `@版本` 安装;③**禁止裸名安装**(24h 内会落回旧版)
|
|
129
130
|
- **v0.5.5**:**安装指引升级(AI 友好)**——README 快速安装改为「先 `npm view dsh-skill-picker version` 查版本号 → 再 `add dsh-skill-picker@版本号` 指定安装」,并给 AI/安装助手显式提示:新版本发布后 **24 小时内裸名安装会被 minimumReleaseAge 门禁拦截并自动落回旧版**,必须指定版本号才能装到最新
|
|
@@ -149,6 +150,7 @@ DSH 的 [dsh-tool-skill](https://github.com/deepseek-ai/deepseek-harness) 在 `a
|
|
|
149
150
|
|
|
150
151
|
- **技能来源**:**优先走官方宿主 skills API**(`connection.api.skills.list`——与 DSH 内置 `/` 补全**完全同一个数据源**,会话作用域,自动覆盖全部官方目录);官方 API 不可用时**自动回退**到内置扫描。两条路都支持 `DSH_HOME` 环境变量。
|
|
151
152
|
- **兜底扫描范围**:与官方 `dsh-skill-filesystem` provider 的默认根完全同源——项目级 `<workspace>/.dsh/skills`、`<workspace>/.agents/skills`,用户级 `~/.dsh/skills`、`~/.agents/skills`(`$DSH_AGENTS_HOME` 可覆盖),同名时按官方 rank 项目级优先。走兜底时 ⚡ 面板底部显示「本地扫描」徽标。
|
|
153
|
+
- **链接型技能**:技能目录里的**符号链接 / Junction**会被跟随读取(v0.5.9 起,对应 issue #6),链接型技能与普通目录一视同仁;断链、指向普通文件的链接静默跳过,不影响其它技能。
|
|
152
154
|
- **暂不支持**:自定义技能目录(官方 `customSkillDirs` 配置)——需要的话欢迎 PR。
|
|
153
155
|
- **失败保护**:client 端用 `ctx.slots.inject`(等 `conversation.input.right` 插槽声明存在才注册,插槽缺失时静默跳过,不会拖垮启动);host 端路由 try/catch,扫描目录不存在时返回空列表而非报错。
|
|
154
156
|
- **依赖版本**:按 DSH `0.1.0-rc.6` API 编写(cordis 4 / web profile 标准装配)。如遇 DSH 大版本更新导致 API 变化,插件会以启动日志的插件错误提示为准,卸载 `dsh plugin --profile web remove dsh-skill-picker` 即可回退。
|
package/lib/index.js
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
// src/index.js
|
|
2
2
|
import { readFile as readFile2, readdir as readdir2 } from "node:fs/promises";
|
|
3
3
|
import os2 from "node:os";
|
|
4
|
-
import
|
|
4
|
+
import path3 from "node:path";
|
|
5
|
+
|
|
6
|
+
// src/dir-entry.js
|
|
7
|
+
import { stat } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
async function isDirectoryEntry(dir, entry) {
|
|
10
|
+
if (entry.isDirectory()) return true;
|
|
11
|
+
if (!entry.isSymbolicLink()) return false;
|
|
12
|
+
try {
|
|
13
|
+
return (await stat(path.join(dir, entry.name))).isDirectory();
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
5
18
|
|
|
6
19
|
// src/patch-ui-skill.js
|
|
7
20
|
import { readFile, writeFile, copyFile, readdir, access } from "node:fs/promises";
|
|
8
21
|
import os from "node:os";
|
|
9
|
-
import
|
|
22
|
+
import path2 from "node:path";
|
|
10
23
|
var FUZZY_MARKER = "__dshSkillPickerFuzzy";
|
|
11
24
|
var TRACK_MARKER = "__dshSkillPickerTrack";
|
|
12
25
|
var PATCHES = [
|
|
@@ -22,17 +35,28 @@ var PATCHES = [
|
|
|
22
35
|
},
|
|
23
36
|
{
|
|
24
37
|
id: "fuzzy-candidates",
|
|
25
|
-
title: "prefix matcher \u2192 fuzzy+pinyin matcher",
|
|
38
|
+
title: "prefix/rank matcher \u2192 fuzzy+pinyin matcher",
|
|
26
39
|
isApplied(text) {
|
|
27
40
|
return text.includes(FUZZY_MARKER);
|
|
28
41
|
},
|
|
29
42
|
apply(text) {
|
|
30
|
-
|
|
31
|
-
/(\t*)return
|
|
32
|
-
(
|
|
33
|
-
|
|
43
|
+
const ANCHORS = [
|
|
44
|
+
/(\t*)return (\(0, [\w.$]+\.rankByName\)\(skills, query\)|rankByName\(skills, query\))\.map\(\(skill\) => \(\{/,
|
|
45
|
+
/(\t*)return (skills\.filter\(\(skill\) => skill\.name\.startsWith\(query\)\))\.map\(\(skill\) => \(\{/
|
|
46
|
+
];
|
|
47
|
+
for (const re of ANCHORS) {
|
|
48
|
+
const m = text.match(re);
|
|
49
|
+
if (!m) continue;
|
|
50
|
+
const [, indent, official] = m;
|
|
51
|
+
return text.replace(
|
|
52
|
+
re,
|
|
53
|
+
`${indent}// dsh-skill-picker patch: fuzzy+pinyin matcher (self-healed)
|
|
54
|
+
${indent}const officialMatcher = ${official};
|
|
55
|
+
${indent}const matcher = typeof window.${FUZZY_MARKER} === "function" ? window.${FUZZY_MARKER}(skills, query) : officialMatcher;
|
|
34
56
|
${indent}return matcher.map((skill) => ({`
|
|
35
|
-
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return text;
|
|
36
60
|
}
|
|
37
61
|
},
|
|
38
62
|
{
|
|
@@ -54,10 +78,10 @@ ${i3}}`
|
|
|
54
78
|
}
|
|
55
79
|
];
|
|
56
80
|
function dshHome() {
|
|
57
|
-
return process.env.DSH_HOME ??
|
|
81
|
+
return process.env.DSH_HOME ?? path2.join(os.homedir(), ".dsh");
|
|
58
82
|
}
|
|
59
83
|
async function uiSkillClientPaths() {
|
|
60
|
-
const profilesDir =
|
|
84
|
+
const profilesDir = path2.join(dshHome(), "profiles");
|
|
61
85
|
let profiles;
|
|
62
86
|
try {
|
|
63
87
|
profiles = await readdir(profilesDir, { withFileTypes: true });
|
|
@@ -67,10 +91,10 @@ async function uiSkillClientPaths() {
|
|
|
67
91
|
const seen = /* @__PURE__ */ new Set();
|
|
68
92
|
const found = [];
|
|
69
93
|
for (const entry of profiles) {
|
|
70
|
-
if (!entry
|
|
94
|
+
if (!await isDirectoryEntry(profilesDir, entry)) continue;
|
|
71
95
|
const candidates = [
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
path2.join(profilesDir, entry.name, "local", "dsh-client-ui-skill", "lib", "client.js"),
|
|
97
|
+
path2.join(profilesDir, entry.name, "node_modules", "@deepseek-ai", "dsh-client-ui-skill", "lib", "client.js")
|
|
74
98
|
];
|
|
75
99
|
for (const candidate of candidates) {
|
|
76
100
|
try {
|
|
@@ -132,12 +156,12 @@ var inject = ["webServer", "systemPrompt"];
|
|
|
132
156
|
var SECTION_ORDER = 215;
|
|
133
157
|
var SKILL_PICKER_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-skill-picker \u63D2\u4EF6\uFF08Web GUI \u7684\u6280\u80FD\u9009\u62E9\u5668\uFF09\uFF1A\u8F93\u5165\u6846\u65C1\u6709\u6280\u80FD\u6309\u94AE\uFF0C\u7528\u6237\u70B9\u9009\u6280\u80FD\u540E\u4F1A\u628A `/\u6280\u80FD\u540D`\uFF08\u5982 /duo-xuan-pi-gai\uFF09\u63D2\u5165\u53D1\u9001\u6846\u5E76\u968F\u6D88\u606F\u53D1\u51FA\u3002DSH \u5B98\u65B9\u673A\u5236\u4F1A\u628A\u7528\u6237\u6D88\u606F\u91CC\u7684 `/\u6280\u80FD\u540D` \u624B\u52BF\u5F53\u4F5C\u6280\u80FD\u76F4\u63A5\u8C03\u7528\u5E76\u81EA\u52A8\u52A0\u8F7D\u6280\u80FD\u5185\u5BB9\u2014\u2014\u4F60\u7167\u5E38\u6309\u52A0\u8F7D\u540E\u7684\u6280\u80FD\u6307\u4EE4\u6267\u884C\u5373\u53EF\uFF0C\u65E0\u9700\u989D\u5916\u64CD\u4F5C\u3002\u7528\u6237\u8BF4\u300C\u6280\u80FD\u9009\u62E9\u5668 / \u9009\u4E2A\u6280\u80FD / \u6280\u80FD\u5217\u8868\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\u3002";
|
|
134
158
|
function userSkillsDir() {
|
|
135
|
-
const home = process.env.DSH_HOME ??
|
|
136
|
-
return
|
|
159
|
+
const home = process.env.DSH_HOME ?? path3.join(os2.homedir(), ".dsh");
|
|
160
|
+
return path3.join(home, "skills");
|
|
137
161
|
}
|
|
138
162
|
function userAgentsSkillsDir() {
|
|
139
|
-
const agentsHome = process.env.DSH_AGENTS_HOME ??
|
|
140
|
-
return
|
|
163
|
+
const agentsHome = process.env.DSH_AGENTS_HOME ?? path3.join(os2.homedir(), ".agents");
|
|
164
|
+
return path3.join(agentsHome, "skills");
|
|
141
165
|
}
|
|
142
166
|
function parseFrontmatter(content) {
|
|
143
167
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -159,11 +183,11 @@ async function scanSkillsDirInto(map, dir) {
|
|
|
159
183
|
return;
|
|
160
184
|
}
|
|
161
185
|
for (const entry of entries) {
|
|
162
|
-
if (!entry
|
|
163
|
-
const skillDir =
|
|
186
|
+
if (!await isDirectoryEntry(dir, entry)) continue;
|
|
187
|
+
const skillDir = path3.join(dir, entry.name);
|
|
164
188
|
let content;
|
|
165
189
|
try {
|
|
166
|
-
content = await readFile2(
|
|
190
|
+
content = await readFile2(path3.join(skillDir, "SKILL.md"), "utf8");
|
|
167
191
|
} catch {
|
|
168
192
|
continue;
|
|
169
193
|
}
|
|
@@ -180,8 +204,8 @@ async function scanSkills(cwd) {
|
|
|
180
204
|
await scanSkillsDirInto(map, userAgentsSkillsDir());
|
|
181
205
|
await scanSkillsDirInto(map, userSkillsDir());
|
|
182
206
|
if (typeof cwd === "string" && cwd !== "") {
|
|
183
|
-
await scanSkillsDirInto(map,
|
|
184
|
-
await scanSkillsDirInto(map,
|
|
207
|
+
await scanSkillsDirInto(map, path3.join(cwd, ".agents", "skills"));
|
|
208
|
+
await scanSkillsDirInto(map, path3.join(cwd, ".dsh", "skills"));
|
|
185
209
|
}
|
|
186
210
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
187
211
|
}
|
|
@@ -220,6 +244,7 @@ function apply(ctx) {
|
|
|
220
244
|
export {
|
|
221
245
|
SKILL_PICKER_GUIDANCE,
|
|
222
246
|
apply,
|
|
223
|
-
inject
|
|
247
|
+
inject,
|
|
248
|
+
scanSkills
|
|
224
249
|
};
|
|
225
250
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/index.js", "../src/patch-ui-skill.js"],
|
|
4
|
-
"sourcesContent": ["/**\n * dsh-skill-picker \u2014 host half: exposes the installed-skill catalog to the\n * browser half through a small JSON route (`/dsh-skill-picker/skills`) and\n * announces the picker to every agent through the system-prompt section\n * mechanism.\n *\n * The catalog is scanned directly from the DSH user skills directory\n * (`$DSH_HOME/skills`, default `~/.dsh/skills`) by reading each skill's\n * `SKILL.md` frontmatter. Why not `ctx.skills`? The filesystem skill provider\n * is mounted in agent scope (the standard preset's standing mount), so a\n * host-context `ctx.skills.snapshot({})` sees only the global layer \u2014 which\n * is empty for user skills. Scanning the same roots the provider uses keeps\n * the picker's list in sync with what agents actually load.\n *\n * The browser half (exports \"./client\") is served by client-modules from the\n * same package's dsh.client declaration.\n *\n * @module dsh-skill-picker\n */\n\nimport { readFile, readdir } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport { healUiSkillPatches } from './patch-ui-skill.js'\n\n/** Required services: the route registry and the prompt band. */\nexport const inject = ['webServer', 'systemPrompt']\n\n/** Order of the announcement section within the tool-guidance band. */\nconst SECTION_ORDER = 215\n\n/** Model-facing announcement: picker presence and the user-visible gesture. */\nexport const SKILL_PICKER_GUIDANCE =\n '\u672C\u673A\u5DF2\u5B89\u88C5 dsh-skill-picker \u63D2\u4EF6\uFF08Web GUI \u7684\u6280\u80FD\u9009\u62E9\u5668\uFF09\uFF1A\u8F93\u5165\u6846\u65C1\u6709\u6280\u80FD\u6309\u94AE\uFF0C\u7528\u6237\u70B9\u9009\u6280\u80FD\u540E\u4F1A\u628A `/\u6280\u80FD\u540D`\uFF08\u5982 /duo-xuan-pi-gai\uFF09\u63D2\u5165\u53D1\u9001\u6846\u5E76\u968F\u6D88\u606F\u53D1\u51FA\u3002DSH \u5B98\u65B9\u673A\u5236\u4F1A\u628A\u7528\u6237\u6D88\u606F\u91CC\u7684 `/\u6280\u80FD\u540D` \u624B\u52BF\u5F53\u4F5C\u6280\u80FD\u76F4\u63A5\u8C03\u7528\u5E76\u81EA\u52A8\u52A0\u8F7D\u6280\u80FD\u5185\u5BB9\u2014\u2014\u4F60\u7167\u5E38\u6309\u52A0\u8F7D\u540E\u7684\u6280\u80FD\u6307\u4EE4\u6267\u884C\u5373\u53EF\uFF0C\u65E0\u9700\u989D\u5916\u64CD\u4F5C\u3002\u7528\u6237\u8BF4\u300C\u6280\u80FD\u9009\u62E9\u5668 / \u9009\u4E2A\u6280\u80FD / \u6280\u80FD\u5217\u8868\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\u3002'\n\n/** Resolve the user skills directory, mirroring the official provider's default. */\nfunction userSkillsDir() {\n const home = process.env.DSH_HOME ?? path.join(os.homedir(), '.dsh')\n return path.join(home, 'skills')\n}\n\n/**\n * Resolve the user agents-home skills directory, mirroring the official\n * provider's default (`$DSH_AGENTS_HOME` > `~/.agents`). This is the\n * cross-tool `.agents` convention; the official `dsh-skill-filesystem` scans\n * it as its `user-agents` root (rank 500), so the fallback must too \u2014 else\n * the picker's list silently misses skills that DSH's own `/` completion\n * shows (issue #5).\n */\nfunction userAgentsSkillsDir() {\n const agentsHome = process.env.DSH_AGENTS_HOME ?? path.join(os.homedir(), '.agents')\n return path.join(agentsHome, 'skills')\n}\n\n/** Parse a SKILL.md frontmatter block into a key/value map (flat YAML subset). */\nfunction parseFrontmatter(content) {\n const match = content.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---/)\n if (!match) return {}\n const out = {}\n for (const line of match[1].split(/\\r?\\n/)) {\n const kv = line.match(/^([A-Za-z0-9_-]+):\\s*(.*)$/)\n if (!kv) continue\n const value = kv[2].trim().replace(/^[\"']|[\"']$/g, '')\n if (value !== '') out[kv[1]] = value\n }\n return out\n}\n\n/** Scan one skill directory into the map; never throws (missing dir is a no-op). */\nasync function scanSkillsDirInto(map, dir) {\n let entries\n try {\n entries = await readdir(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n const skillDir = path.join(dir, entry.name)\n let content\n try {\n content = await readFile(path.join(skillDir, 'SKILL.md'), 'utf8')\n } catch {\n continue\n }\n const meta = parseFrontmatter(content)\n // Later writes win, so project-level skills override same-named user skills.\n map.set(meta.name ?? entry.name, {\n name: meta.name ?? entry.name,\n description: meta.description ?? '',\n path: skillDir,\n })\n }\n}\n\n/**\n * Scan the same roots the official `dsh-skill-filesystem` provider uses, so\n * the fallback route stays in sync with what agents actually load:\n * project `.dsh/skills` (rank 100) > project `.agents/skills` (200) >\n * user `~/.dsh/skills` (400) > user `~/.agents/skills` (500). Scanned\n * low-priority first, so later writes (higher priority) win in the map.\n * Never throws (a missing dir yields []).\n * @param cwd - the active session's workspace root (undefined = user level only).\n */\nasync function scanSkills(cwd) {\n const map = new Map()\n await scanSkillsDirInto(map, userAgentsSkillsDir())\n await scanSkillsDirInto(map, userSkillsDir())\n if (typeof cwd === 'string' && cwd !== '') {\n await scanSkillsDirInto(map, path.join(cwd, '.agents', 'skills'))\n await scanSkillsDirInto(map, path.join(cwd, '.dsh', 'skills'))\n }\n return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/**\n * Mount the skills route and the prompt section.\n * @param ctx - context carrying webServer and systemPrompt.\n */\nexport function apply(ctx) {\n ctx.effect(() => {\n const handler = async (req, res) => {\n try {\n // cwd query carries the active session's workspace root from the client.\n const cwd = req.url !== undefined ? new URL(req.url, 'http://dsh').searchParams.get('cwd') ?? undefined : undefined\n const skills = await scanSkills(cwd)\n res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })\n res.end(JSON.stringify({ ok: true, complete: true, skills }))\n } catch (error) {\n res.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })\n res.end(JSON.stringify({ ok: false, error: String(error?.message ?? error) }))\n }\n }\n return ctx.webServer.register({ kind: 'prefix', path: '/dsh-skill-picker', handler })\n }, 'dsh-skill-picker: routes')\n\n ctx.effect(() => ctx.systemPrompt.section({\n name: 'plugin:skill-picker',\n order: SECTION_ORDER,\n text: SKILL_PICKER_GUIDANCE,\n }), 'dsh-skill-picker: prompt section')\n\n // Self-healing patch for the official ui-skill package: keeps the `/`\n // completion's skill group ordered above commands and its matching fuzzy\n // across DSH upgrades. Runs once per boot; idempotent, backed up, and\n // never allowed to take the host down.\n ctx.effect(() => {\n healUiSkillPatches().then((report) => {\n if (report.files.length > 0) {\n console.log('[dsh-skill-picker] ui-skill patch report:', JSON.stringify(report))\n }\n }).catch((error) => {\n console.warn('[dsh-skill-picker] ui-skill patch failed:', error)\n })\n return () => {}\n }, 'dsh-skill-picker: ui-skill self-heal patch')\n}\n", "/**\n * dsh-skill-picker \u2014 host-side self-healing patch for the official\n * `@deepseek-ai/dsh-client-ui-skill` package.\n *\n * The picker upgrades the official `/` completion in two ways that the\n * official package does not provide out of the box:\n *\n * 1. `order: 2 \u2192 -1` \u2014 the skill group sorts ABOVE the command group\n * (commands register with the default order 0; lower = higher in the\n * official menu).\n * 2. fuzzy+pinyin candidates \u2014 the official prefix-only matcher\n * (`skill.name.startsWith(query)`) is replaced by the picker's\n * `window.__dshSkillPickerFuzzy` matcher when the picker is mounted\n * (single source group, same list, upgraded matching).\n *\n * Every DSH boot this module scans every profile under `$DSH_HOME/profiles`\n * (default `~/.dsh/profiles`) for an installed ui-skill `lib/client.js` \u2014\n * either the user's local patched copy (`local/dsh-client-ui-skill`) or the\n * plain npm install (`node_modules/@deepseek-ai/dsh-client-ui-skill`) \u2014 and\n * re-applies both patches when DSH upgrades overwrote them. The original file\n * is backed up once as `<file>.dsh-skill-picker.bak` before the first write.\n * All operations are idempotent and never throw: a missing profile, package\n * or read failure is reported and skipped so a broken patch can never take\n * the host down.\n *\n * @module dsh-skill-picker/patch-ui-skill\n */\n\nimport { readFile, writeFile, copyFile, readdir, access } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\n\n/** Marker that the candidates patch is already in place. */\nconst FUZZY_MARKER = '__dshSkillPickerFuzzy'\n\n/** Marker that the pick-tracking patch is already in place. */\nconst TRACK_MARKER = '__dshSkillPickerTrack'\n\n/** The self-healing patches, in application order. */\nexport const PATCHES = [\n {\n id: 'order',\n title: 'skill group order 2 \u2192 -1 (above commands)',\n isApplied(text) {\n return /name: \"skill\",[\\s\\S]*?order:\\s*-1,/.test(text)\n },\n apply(text) {\n return text.replace(/(name: \"skill\",\\s*order: )2,/, '$1-1,')\n },\n },\n {\n id: 'fuzzy-candidates',\n title: 'prefix matcher \u2192 fuzzy+pinyin matcher',\n isApplied(text) {\n return text.includes(FUZZY_MARKER)\n },\n apply(text) {\n return text.replace(\n /(\\t*)return skills\\.filter\\(\\(skill\\) => skill\\.name\\.startsWith\\(query\\)\\)\\.map\\(\\(skill\\) => \\(\\{/,\n (match, indent) =>\n `${indent}// dsh-skill-picker patch: fuzzy+pinyin matcher (self-healed)\\n` +\n `${indent}const matcher = typeof window.${FUZZY_MARKER} === \"function\" ? window.${FUZZY_MARKER}(skills, query) : skills.filter((skill) => skill.name.startsWith(query));\\n` +\n `${indent}return matcher.map((skill) => ({`,\n )\n },\n },\n {\n id: 'pick-tracking',\n title: 'record usage when picked from the official / menu',\n isApplied(text) {\n return text.includes(TRACK_MARKER)\n },\n apply(text) {\n return text.replace(\n /(\\t*)onPick\\(\\{ candidate \\}\\) \\{\\n(\\t*)return \\{ text: `\\/\\$\\{candidate\\.name\\} ` \\};\\n(\\t*)\\}/,\n (match, i1, i2, i3) =>\n `${i1}onPick({ candidate }) {\\n` +\n `${i2} // dsh-skill-picker patch: usage tracking (self-healed)\\n` +\n `${i2} try { window.${TRACK_MARKER}?.(candidate.name) } catch { /* best-effort */ }\\n` +\n `${i2} return { text: \\`/\\${candidate.name} \\` };\\n` +\n `${i3}}`,\n )\n },\n },\n]\n\n/** Resolve the DSH home directory, mirroring the official convention. */\nexport function dshHome() {\n return process.env.DSH_HOME ?? path.join(os.homedir(), '.dsh')\n}\n\n/**\n * Enumerate every installed ui-skill `lib/client.js` across all profiles:\n * the user's local patched copy first (that is what the profile actually\n * loads when linked), then the plain npm install. Deduplicated by real path.\n * Never throws \u2014 a missing profiles dir yields [].\n * @returns {Promise<string[]>} candidate file paths.\n */\nexport async function uiSkillClientPaths() {\n const profilesDir = path.join(dshHome(), 'profiles')\n let profiles\n try {\n profiles = await readdir(profilesDir, { withFileTypes: true })\n } catch {\n return []\n }\n const seen = new Set()\n const found = []\n for (const entry of profiles) {\n if (!entry.isDirectory()) continue\n const candidates = [\n path.join(profilesDir, entry.name, 'local', 'dsh-client-ui-skill', 'lib', 'client.js'),\n path.join(profilesDir, entry.name, 'node_modules', '@deepseek-ai', 'dsh-client-ui-skill', 'lib', 'client.js'),\n ]\n for (const candidate of candidates) {\n try {\n await access(candidate)\n const real = await import('node:fs/promises').then(({ realpath }) => realpath(candidate))\n if (!seen.has(real)) {\n seen.add(real)\n found.push(candidate)\n }\n } catch {\n /* not present at this location */\n }\n }\n }\n return found\n}\n\n/**\n * Apply both patches to one ui-skill client.js. Idempotent: already-applied\n * patches are reported as skipped; the original file is backed up once before\n * the first modification. Never throws for a patch that does not match (it is\n * reported as `noop`), only for actual I/O failures.\n * @param {string} file - absolute path to the target client.js.\n * @returns {Promise<{file: string, patched: string[], skipped: string[], noop: string[]}>}\n */\nexport async function patchUiSkillFile(file) {\n const text = await readFile(file, 'utf8')\n const result = { file, patched: [], skipped: [], noop: [] }\n let next = text\n for (const patch of PATCHES) {\n if (patch.isApplied(next)) {\n result.skipped.push(patch.id)\n continue\n }\n const candidate = patch.apply(next)\n if (candidate === next) {\n result.noop.push(patch.id)\n continue\n }\n next = candidate\n result.patched.push(patch.id)\n }\n if (result.patched.length === 0) return result\n const backup = `${file}.dsh-skill-picker.bak`\n try {\n await access(backup)\n } catch {\n await copyFile(file, backup)\n }\n await writeFile(file, next, 'utf8')\n return result\n}\n\n/**\n * Self-heal entry point: scan all profiles, patch every ui-skill copy found,\n * and return one combined report. Never throws \u2014 each failure is collected\n * into `errors` so the host boot is never taken down by a broken patch.\n * @returns {Promise<{files: Array, errors: string[]}>}\n */\nexport async function healUiSkillPatches() {\n const files = await uiSkillClientPaths()\n const filesReport = []\n const errors = []\n for (const file of files) {\n try {\n filesReport.push(await patchUiSkillFile(file))\n } catch (error) {\n errors.push(`${file}: ${String(error?.message ?? error)}`)\n }\n }\n return { files: filesReport, errors }\n}\n"],
|
|
5
|
-
"mappings": ";AAoBA,SAAS,YAAAA,WAAU,WAAAC,gBAAe;AAClC,OAAOC,SAAQ;AACf,OAAOC,WAAU;;;
|
|
6
|
-
"names": ["readFile", "readdir", "os", "path", "path", "os", "readdir", "readFile"]
|
|
3
|
+
"sources": ["../src/index.js", "../src/dir-entry.js", "../src/patch-ui-skill.js"],
|
|
4
|
+
"sourcesContent": ["/**\n * dsh-skill-picker \u2014 host half: exposes the installed-skill catalog to the\n * browser half through a small JSON route (`/dsh-skill-picker/skills`) and\n * announces the picker to every agent through the system-prompt section\n * mechanism.\n *\n * The catalog is scanned directly from the DSH user skills directory\n * (`$DSH_HOME/skills`, default `~/.dsh/skills`) by reading each skill's\n * `SKILL.md` frontmatter. Why not `ctx.skills`? The filesystem skill provider\n * is mounted in agent scope (the standard preset's standing mount), so a\n * host-context `ctx.skills.snapshot({})` sees only the global layer \u2014 which\n * is empty for user skills. Scanning the same roots the provider uses keeps\n * the picker's list in sync with what agents actually load.\n *\n * The browser half (exports \"./client\") is served by client-modules from the\n * same package's dsh.client declaration.\n *\n * @module dsh-skill-picker\n */\n\nimport { readFile, readdir } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport { isDirectoryEntry } from './dir-entry.js'\nimport { healUiSkillPatches } from './patch-ui-skill.js'\n\n/** Required services: the route registry and the prompt band. */\nexport const inject = ['webServer', 'systemPrompt']\n\n/** Order of the announcement section within the tool-guidance band. */\nconst SECTION_ORDER = 215\n\n/** Model-facing announcement: picker presence and the user-visible gesture. */\nexport const SKILL_PICKER_GUIDANCE =\n '\u672C\u673A\u5DF2\u5B89\u88C5 dsh-skill-picker \u63D2\u4EF6\uFF08Web GUI \u7684\u6280\u80FD\u9009\u62E9\u5668\uFF09\uFF1A\u8F93\u5165\u6846\u65C1\u6709\u6280\u80FD\u6309\u94AE\uFF0C\u7528\u6237\u70B9\u9009\u6280\u80FD\u540E\u4F1A\u628A `/\u6280\u80FD\u540D`\uFF08\u5982 /duo-xuan-pi-gai\uFF09\u63D2\u5165\u53D1\u9001\u6846\u5E76\u968F\u6D88\u606F\u53D1\u51FA\u3002DSH \u5B98\u65B9\u673A\u5236\u4F1A\u628A\u7528\u6237\u6D88\u606F\u91CC\u7684 `/\u6280\u80FD\u540D` \u624B\u52BF\u5F53\u4F5C\u6280\u80FD\u76F4\u63A5\u8C03\u7528\u5E76\u81EA\u52A8\u52A0\u8F7D\u6280\u80FD\u5185\u5BB9\u2014\u2014\u4F60\u7167\u5E38\u6309\u52A0\u8F7D\u540E\u7684\u6280\u80FD\u6307\u4EE4\u6267\u884C\u5373\u53EF\uFF0C\u65E0\u9700\u989D\u5916\u64CD\u4F5C\u3002\u7528\u6237\u8BF4\u300C\u6280\u80FD\u9009\u62E9\u5668 / \u9009\u4E2A\u6280\u80FD / \u6280\u80FD\u5217\u8868\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\u3002'\n\n/** Resolve the user skills directory, mirroring the official provider's default. */\nfunction userSkillsDir() {\n const home = process.env.DSH_HOME ?? path.join(os.homedir(), '.dsh')\n return path.join(home, 'skills')\n}\n\n/**\n * Resolve the user agents-home skills directory, mirroring the official\n * provider's default (`$DSH_AGENTS_HOME` > `~/.agents`). This is the\n * cross-tool `.agents` convention; the official `dsh-skill-filesystem` scans\n * it as its `user-agents` root (rank 500), so the fallback must too \u2014 else\n * the picker's list silently misses skills that DSH's own `/` completion\n * shows (issue #5).\n */\nfunction userAgentsSkillsDir() {\n const agentsHome = process.env.DSH_AGENTS_HOME ?? path.join(os.homedir(), '.agents')\n return path.join(agentsHome, 'skills')\n}\n\n/** Parse a SKILL.md frontmatter block into a key/value map (flat YAML subset). */\nfunction parseFrontmatter(content) {\n const match = content.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---/)\n if (!match) return {}\n const out = {}\n for (const line of match[1].split(/\\r?\\n/)) {\n const kv = line.match(/^([A-Za-z0-9_-]+):\\s*(.*)$/)\n if (!kv) continue\n const value = kv[2].trim().replace(/^[\"']|[\"']$/g, '')\n if (value !== '') out[kv[1]] = value\n }\n return out\n}\n\n/** Scan one skill directory into the map; never throws (missing dir is a no-op). */\nasync function scanSkillsDirInto(map, dir) {\n let entries\n try {\n entries = await readdir(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n // Links are followed (`isDirectoryEntry`), so a skill may live behind a\n // symlink/junction \u2014 e.g. `~/.agents/skills/neat` \u2192 another repo (#6).\n if (!(await isDirectoryEntry(dir, entry))) continue\n const skillDir = path.join(dir, entry.name)\n let content\n try {\n content = await readFile(path.join(skillDir, 'SKILL.md'), 'utf8')\n } catch {\n continue\n }\n const meta = parseFrontmatter(content)\n // Later writes win, so project-level skills override same-named user skills.\n map.set(meta.name ?? entry.name, {\n name: meta.name ?? entry.name,\n description: meta.description ?? '',\n path: skillDir,\n })\n }\n}\n\n/**\n * Scan the same roots the official `dsh-skill-filesystem` provider uses, so\n * the fallback route stays in sync with what agents actually load:\n * project `.dsh/skills` (rank 100) > project `.agents/skills` (200) >\n * user `~/.dsh/skills` (400) > user `~/.agents/skills` (500). Scanned\n * low-priority first, so later writes (higher priority) win in the map.\n * Never throws (a missing dir yields []).\n * @param cwd - the active session's workspace root (undefined = user level only).\n * @returns the deduplicated, name-sorted skill list.\n */\nexport async function scanSkills(cwd) {\n const map = new Map()\n await scanSkillsDirInto(map, userAgentsSkillsDir())\n await scanSkillsDirInto(map, userSkillsDir())\n if (typeof cwd === 'string' && cwd !== '') {\n await scanSkillsDirInto(map, path.join(cwd, '.agents', 'skills'))\n await scanSkillsDirInto(map, path.join(cwd, '.dsh', 'skills'))\n }\n return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/**\n * Mount the skills route and the prompt section.\n * @param ctx - context carrying webServer and systemPrompt.\n */\nexport function apply(ctx) {\n ctx.effect(() => {\n const handler = async (req, res) => {\n try {\n // cwd query carries the active session's workspace root from the client.\n const cwd = req.url !== undefined ? new URL(req.url, 'http://dsh').searchParams.get('cwd') ?? undefined : undefined\n const skills = await scanSkills(cwd)\n res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })\n res.end(JSON.stringify({ ok: true, complete: true, skills }))\n } catch (error) {\n res.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })\n res.end(JSON.stringify({ ok: false, error: String(error?.message ?? error) }))\n }\n }\n return ctx.webServer.register({ kind: 'prefix', path: '/dsh-skill-picker', handler })\n }, 'dsh-skill-picker: routes')\n\n ctx.effect(() => ctx.systemPrompt.section({\n name: 'plugin:skill-picker',\n order: SECTION_ORDER,\n text: SKILL_PICKER_GUIDANCE,\n }), 'dsh-skill-picker: prompt section')\n\n // Self-healing patch for the official ui-skill package: keeps the `/`\n // completion's skill group ordered above commands and its matching fuzzy\n // across DSH upgrades. Runs once per boot; idempotent, backed up, and\n // never allowed to take the host down.\n ctx.effect(() => {\n healUiSkillPatches().then((report) => {\n if (report.files.length > 0) {\n console.log('[dsh-skill-picker] ui-skill patch report:', JSON.stringify(report))\n }\n }).catch((error) => {\n console.warn('[dsh-skill-picker] ui-skill patch failed:', error)\n })\n return () => {}\n }, 'dsh-skill-picker: ui-skill self-heal patch')\n}\n", "/**\n * Directory-entry helpers shared by the skill scan and the ui-skill patch scan.\n *\n * `readdir(dir, { withFileTypes: true })` describes the entry itself, not what\n * it points at: on Windows a symbolic link **and a junction** both come back as\n * `isDirectory() === false` / `isSymbolicLink() === true` (Node reports junction\n * through the same lstat semantics). A directory filter that only tests\n * `isDirectory()` therefore drops every linked skill silently \u2014 that is exactly\n * the `~/.agents/skills/neat` \u2192 repo case in issue #6.\n *\n * @module dsh-skill-picker/dir-entry\n */\n\nimport { stat } from 'node:fs/promises'\nimport path from 'node:path'\n\n/**\n * Decide whether a dirent points at a directory we may descend into.\n *\n * Real directories answer straight from the dirent (no extra syscall). Link\n * entries are resolved with `stat`, which follows the link, so junctions and\n * symlinks are treated exactly like the directories they point at. A broken\n * link (or a link into a permission wall) makes `stat` throw and is reported as\n * \"not a directory\" rather than taking the caller down.\n *\n * @param dir - the directory the entry was read from.\n * @param entry - the `Dirent` returned by `readdir`.\n * @returns whether the entry is, or points at, a directory.\n */\nexport async function isDirectoryEntry(dir, entry) {\n if (entry.isDirectory()) return true\n if (!entry.isSymbolicLink()) return false\n try {\n return (await stat(path.join(dir, entry.name))).isDirectory()\n } catch {\n return false\n }\n}\n", "/**\n * dsh-skill-picker \u2014 host-side self-healing patch for the official\n * `@deepseek-ai/dsh-client-ui-skill` package.\n *\n * The picker upgrades the official `/` completion in two ways that the\n * official package does not provide out of the box:\n *\n * 1. `order: 2 \u2192 -1` \u2014 the skill group sorts ABOVE the command group\n * (commands register with the default order 0; lower = higher in the\n * official menu).\n * 2. fuzzy+pinyin candidates \u2014 the official prefix-only matcher\n * (`skill.name.startsWith(query)`) is replaced by the picker's\n * `window.__dshSkillPickerFuzzy` matcher when the picker is mounted\n * (single source group, same list, upgraded matching).\n *\n * Every DSH boot this module scans every profile under `$DSH_HOME/profiles`\n * (default `~/.dsh/profiles`) for an installed ui-skill `lib/client.js` \u2014\n * either the user's local patched copy (`local/dsh-client-ui-skill`) or the\n * plain npm install (`node_modules/@deepseek-ai/dsh-client-ui-skill`) \u2014 and\n * re-applies both patches when DSH upgrades overwrote them. The original file\n * is backed up once as `<file>.dsh-skill-picker.bak` before the first write.\n * All operations are idempotent and never throw: a missing profile, package\n * or read failure is reported and skipped so a broken patch can never take\n * the host down.\n *\n * @module dsh-skill-picker/patch-ui-skill\n */\n\nimport { readFile, writeFile, copyFile, readdir, access } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport { isDirectoryEntry } from './dir-entry.js'\n\n/** Marker that the candidates patch is already in place. */\nconst FUZZY_MARKER = '__dshSkillPickerFuzzy'\n\n/** Marker that the pick-tracking patch is already in place. */\nconst TRACK_MARKER = '__dshSkillPickerTrack'\n\n/** The self-healing patches, in application order. */\nexport const PATCHES = [\n {\n id: 'order',\n title: 'skill group order 2 \u2192 -1 (above commands)',\n isApplied(text) {\n return /name: \"skill\",[\\s\\S]*?order:\\s*-1,/.test(text)\n },\n apply(text) {\n return text.replace(/(name: \"skill\",\\s*order: )2,/, '$1-1,')\n },\n },\n {\n id: 'fuzzy-candidates',\n title: 'prefix/rank matcher \u2192 fuzzy+pinyin matcher',\n isApplied(text) {\n return text.includes(FUZZY_MARKER)\n },\n apply(text) {\n // \u5B98\u65B9\u5B9E\u73B0\u968F\u7248\u672C\u53D8\u8FC7\u4E24\u6B21\uFF0C\u4E24\u4E2A\u5F62\u6001\u90FD\u8981\u8BA4\uFF082026-09-11 \u5B9E\u6D4B\uFF09\uFF1A\n // 0.1.2-alpha.x : return skills.filter((skill) => skill.name.startsWith(query)).map(...)\n // 0.1.5-rc.x : return (0, _xxx.rankByName)(skills, query).map(...) \u2190 \u6539\u6210 async candidates\n // \u6BCF\u7EC4\u6355\u83B7\uFF1A$1 = \u7F29\u8FDB\uFF0C$2 = \u5B98\u65B9\u90A3\u4E00\u6BB5\u8868\u8FBE\u5F0F\uFF08\u539F\u6837\u4FDD\u7559\u505A fallback\uFF09\u3002\n const ANCHORS = [\n /(\\t*)return (\\(0, [\\w.$]+\\.rankByName\\)\\(skills, query\\)|rankByName\\(skills, query\\))\\.map\\(\\(skill\\) => \\(\\{/,\n /(\\t*)return (skills\\.filter\\(\\(skill\\) => skill\\.name\\.startsWith\\(query\\)\\))\\.map\\(\\(skill\\) => \\(\\{/,\n ]\n for (const re of ANCHORS) {\n const m = text.match(re)\n if (!m) continue\n const [, indent, official] = m\n return text.replace(\n re,\n `${indent}// dsh-skill-picker patch: fuzzy+pinyin matcher (self-healed)\\n` +\n `${indent}const officialMatcher = ${official};\\n` +\n `${indent}const matcher = typeof window.${FUZZY_MARKER} === \"function\" ? window.${FUZZY_MARKER}(skills, query) : officialMatcher;\\n` +\n `${indent}return matcher.map((skill) => ({`,\n )\n }\n return text\n },\n },\n {\n id: 'pick-tracking',\n title: 'record usage when picked from the official / menu',\n isApplied(text) {\n return text.includes(TRACK_MARKER)\n },\n apply(text) {\n return text.replace(\n /(\\t*)onPick\\(\\{ candidate \\}\\) \\{\\n(\\t*)return \\{ text: `\\/\\$\\{candidate\\.name\\} ` \\};\\n(\\t*)\\}/,\n (match, i1, i2, i3) =>\n `${i1}onPick({ candidate }) {\\n` +\n `${i2} // dsh-skill-picker patch: usage tracking (self-healed)\\n` +\n `${i2} try { window.${TRACK_MARKER}?.(candidate.name) } catch { /* best-effort */ }\\n` +\n `${i2} return { text: \\`/\\${candidate.name} \\` };\\n` +\n `${i3}}`,\n )\n },\n },\n]\n\n/** Resolve the DSH home directory, mirroring the official convention. */\nexport function dshHome() {\n return process.env.DSH_HOME ?? path.join(os.homedir(), '.dsh')\n}\n\n/**\n * Enumerate every installed ui-skill `lib/client.js` across all profiles:\n * the user's local patched copy first (that is what the profile actually\n * loads when linked), then the plain npm install. Deduplicated by real path.\n * Never throws \u2014 a missing profiles dir yields [].\n * @returns {Promise<string[]>} candidate file paths.\n */\nexport async function uiSkillClientPaths() {\n const profilesDir = path.join(dshHome(), 'profiles')\n let profiles\n try {\n profiles = await readdir(profilesDir, { withFileTypes: true })\n } catch {\n return []\n }\n const seen = new Set()\n const found = []\n for (const entry of profiles) {\n // A profile directory may itself be reached through a link; follow it\n // instead of relying on the lstat-based dirent (see dir-entry.js).\n if (!(await isDirectoryEntry(profilesDir, entry))) continue\n const candidates = [\n path.join(profilesDir, entry.name, 'local', 'dsh-client-ui-skill', 'lib', 'client.js'),\n path.join(profilesDir, entry.name, 'node_modules', '@deepseek-ai', 'dsh-client-ui-skill', 'lib', 'client.js'),\n ]\n for (const candidate of candidates) {\n try {\n await access(candidate)\n const real = await import('node:fs/promises').then(({ realpath }) => realpath(candidate))\n if (!seen.has(real)) {\n seen.add(real)\n found.push(candidate)\n }\n } catch {\n /* not present at this location */\n }\n }\n }\n return found\n}\n\n/**\n * Apply both patches to one ui-skill client.js. Idempotent: already-applied\n * patches are reported as skipped; the original file is backed up once before\n * the first modification. Never throws for a patch that does not match (it is\n * reported as `noop`), only for actual I/O failures.\n * @param {string} file - absolute path to the target client.js.\n * @returns {Promise<{file: string, patched: string[], skipped: string[], noop: string[]}>}\n */\nexport async function patchUiSkillFile(file) {\n const text = await readFile(file, 'utf8')\n const result = { file, patched: [], skipped: [], noop: [] }\n let next = text\n for (const patch of PATCHES) {\n if (patch.isApplied(next)) {\n result.skipped.push(patch.id)\n continue\n }\n const candidate = patch.apply(next)\n if (candidate === next) {\n result.noop.push(patch.id)\n continue\n }\n next = candidate\n result.patched.push(patch.id)\n }\n if (result.patched.length === 0) return result\n const backup = `${file}.dsh-skill-picker.bak`\n try {\n await access(backup)\n } catch {\n await copyFile(file, backup)\n }\n await writeFile(file, next, 'utf8')\n return result\n}\n\n/**\n * Self-heal entry point: scan all profiles, patch every ui-skill copy found,\n * and return one combined report. Never throws \u2014 each failure is collected\n * into `errors` so the host boot is never taken down by a broken patch.\n * @returns {Promise<{files: Array, errors: string[]}>}\n */\nexport async function healUiSkillPatches() {\n const files = await uiSkillClientPaths()\n const filesReport = []\n const errors = []\n for (const file of files) {\n try {\n filesReport.push(await patchUiSkillFile(file))\n } catch (error) {\n errors.push(`${file}: ${String(error?.message ?? error)}`)\n }\n }\n return { files: filesReport, errors }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["readFile", "readdir", "os", "path", "path", "path", "path", "os", "readdir", "readFile"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-picker",
|
|
3
3
|
"description": "DSH Web GUI skill picker: a button beside the composer opens a searchable list of installed skills; picking one inserts the official `/skill-name` gesture into the input box, so the skill loads with your message (WorkBuddy-style skill invocation for DeepSeek Harness).",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"./package.json": "./package.json"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "node build.mjs"
|
|
13
|
+
"build": "node build.mjs",
|
|
14
|
+
"test": "node --test"
|
|
14
15
|
},
|
|
15
16
|
"devDependencies": {
|
|
16
17
|
"esbuild": "^0.25.0",
|
|
@@ -34,7 +35,10 @@
|
|
|
34
35
|
"0.1.2-alpha.2": "compatible",
|
|
35
36
|
"0.1.2-alpha.3": "compatible",
|
|
36
37
|
"0.1.2-alpha.4": "compatible",
|
|
37
|
-
"0.1.2-alpha.5": "compatible"
|
|
38
|
+
"0.1.2-alpha.5": "compatible",
|
|
39
|
+
"0.1.2-rc.1": "compatible",
|
|
40
|
+
"0.1.5-rc.1": "compatible",
|
|
41
|
+
"0.1.5-rc.2": "compatible"
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
},
|
package/src/dir-entry.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directory-entry helpers shared by the skill scan and the ui-skill patch scan.
|
|
3
|
+
*
|
|
4
|
+
* `readdir(dir, { withFileTypes: true })` describes the entry itself, not what
|
|
5
|
+
* it points at: on Windows a symbolic link **and a junction** both come back as
|
|
6
|
+
* `isDirectory() === false` / `isSymbolicLink() === true` (Node reports junction
|
|
7
|
+
* through the same lstat semantics). A directory filter that only tests
|
|
8
|
+
* `isDirectory()` therefore drops every linked skill silently — that is exactly
|
|
9
|
+
* the `~/.agents/skills/neat` → repo case in issue #6.
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-skill-picker/dir-entry
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { stat } from 'node:fs/promises'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Decide whether a dirent points at a directory we may descend into.
|
|
19
|
+
*
|
|
20
|
+
* Real directories answer straight from the dirent (no extra syscall). Link
|
|
21
|
+
* entries are resolved with `stat`, which follows the link, so junctions and
|
|
22
|
+
* symlinks are treated exactly like the directories they point at. A broken
|
|
23
|
+
* link (or a link into a permission wall) makes `stat` throw and is reported as
|
|
24
|
+
* "not a directory" rather than taking the caller down.
|
|
25
|
+
*
|
|
26
|
+
* @param dir - the directory the entry was read from.
|
|
27
|
+
* @param entry - the `Dirent` returned by `readdir`.
|
|
28
|
+
* @returns whether the entry is, or points at, a directory.
|
|
29
|
+
*/
|
|
30
|
+
export async function isDirectoryEntry(dir, entry) {
|
|
31
|
+
if (entry.isDirectory()) return true
|
|
32
|
+
if (!entry.isSymbolicLink()) return false
|
|
33
|
+
try {
|
|
34
|
+
return (await stat(path.join(dir, entry.name))).isDirectory()
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { readFile, readdir } from 'node:fs/promises'
|
|
|
22
22
|
import os from 'node:os'
|
|
23
23
|
import path from 'node:path'
|
|
24
24
|
|
|
25
|
+
import { isDirectoryEntry } from './dir-entry.js'
|
|
25
26
|
import { healUiSkillPatches } from './patch-ui-skill.js'
|
|
26
27
|
|
|
27
28
|
/** Required services: the route registry and the prompt band. */
|
|
@@ -76,7 +77,9 @@ async function scanSkillsDirInto(map, dir) {
|
|
|
76
77
|
return
|
|
77
78
|
}
|
|
78
79
|
for (const entry of entries) {
|
|
79
|
-
|
|
80
|
+
// Links are followed (`isDirectoryEntry`), so a skill may live behind a
|
|
81
|
+
// symlink/junction — e.g. `~/.agents/skills/neat` → another repo (#6).
|
|
82
|
+
if (!(await isDirectoryEntry(dir, entry))) continue
|
|
80
83
|
const skillDir = path.join(dir, entry.name)
|
|
81
84
|
let content
|
|
82
85
|
try {
|
|
@@ -102,8 +105,9 @@ async function scanSkillsDirInto(map, dir) {
|
|
|
102
105
|
* low-priority first, so later writes (higher priority) win in the map.
|
|
103
106
|
* Never throws (a missing dir yields []).
|
|
104
107
|
* @param cwd - the active session's workspace root (undefined = user level only).
|
|
108
|
+
* @returns the deduplicated, name-sorted skill list.
|
|
105
109
|
*/
|
|
106
|
-
async function scanSkills(cwd) {
|
|
110
|
+
export async function scanSkills(cwd) {
|
|
107
111
|
const map = new Map()
|
|
108
112
|
await scanSkillsDirInto(map, userAgentsSkillsDir())
|
|
109
113
|
await scanSkillsDirInto(map, userSkillsDir())
|
package/src/patch-ui-skill.js
CHANGED
|
@@ -30,6 +30,8 @@ import { readFile, writeFile, copyFile, readdir, access } from 'node:fs/promises
|
|
|
30
30
|
import os from 'node:os'
|
|
31
31
|
import path from 'node:path'
|
|
32
32
|
|
|
33
|
+
import { isDirectoryEntry } from './dir-entry.js'
|
|
34
|
+
|
|
33
35
|
/** Marker that the candidates patch is already in place. */
|
|
34
36
|
const FUZZY_MARKER = '__dshSkillPickerFuzzy'
|
|
35
37
|
|
|
@@ -50,18 +52,32 @@ export const PATCHES = [
|
|
|
50
52
|
},
|
|
51
53
|
{
|
|
52
54
|
id: 'fuzzy-candidates',
|
|
53
|
-
title: 'prefix matcher → fuzzy+pinyin matcher',
|
|
55
|
+
title: 'prefix/rank matcher → fuzzy+pinyin matcher',
|
|
54
56
|
isApplied(text) {
|
|
55
57
|
return text.includes(FUZZY_MARKER)
|
|
56
58
|
},
|
|
57
59
|
apply(text) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
// 官方实现随版本变过两次,两个形态都要认(2026-09-11 实测):
|
|
61
|
+
// 0.1.2-alpha.x : return skills.filter((skill) => skill.name.startsWith(query)).map(...)
|
|
62
|
+
// 0.1.5-rc.x : return (0, _xxx.rankByName)(skills, query).map(...) ← 改成 async candidates
|
|
63
|
+
// 每组捕获:$1 = 缩进,$2 = 官方那一段表达式(原样保留做 fallback)。
|
|
64
|
+
const ANCHORS = [
|
|
65
|
+
/(\t*)return (\(0, [\w.$]+\.rankByName\)\(skills, query\)|rankByName\(skills, query\))\.map\(\(skill\) => \(\{/,
|
|
66
|
+
/(\t*)return (skills\.filter\(\(skill\) => skill\.name\.startsWith\(query\)\))\.map\(\(skill\) => \(\{/,
|
|
67
|
+
]
|
|
68
|
+
for (const re of ANCHORS) {
|
|
69
|
+
const m = text.match(re)
|
|
70
|
+
if (!m) continue
|
|
71
|
+
const [, indent, official] = m
|
|
72
|
+
return text.replace(
|
|
73
|
+
re,
|
|
61
74
|
`${indent}// dsh-skill-picker patch: fuzzy+pinyin matcher (self-healed)\n` +
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
`${indent}const officialMatcher = ${official};\n` +
|
|
76
|
+
`${indent}const matcher = typeof window.${FUZZY_MARKER} === "function" ? window.${FUZZY_MARKER}(skills, query) : officialMatcher;\n` +
|
|
77
|
+
`${indent}return matcher.map((skill) => ({`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
return text
|
|
65
81
|
},
|
|
66
82
|
},
|
|
67
83
|
{
|
|
@@ -107,7 +123,9 @@ export async function uiSkillClientPaths() {
|
|
|
107
123
|
const seen = new Set()
|
|
108
124
|
const found = []
|
|
109
125
|
for (const entry of profiles) {
|
|
110
|
-
|
|
126
|
+
// A profile directory may itself be reached through a link; follow it
|
|
127
|
+
// instead of relying on the lstat-based dirent (see dir-entry.js).
|
|
128
|
+
if (!(await isDirectoryEntry(profilesDir, entry))) continue
|
|
111
129
|
const candidates = [
|
|
112
130
|
path.join(profilesDir, entry.name, 'local', 'dsh-client-ui-skill', 'lib', 'client.js'),
|
|
113
131
|
path.join(profilesDir, entry.name, 'node_modules', '@deepseek-ai', 'dsh-client-ui-skill', 'lib', 'client.js'),
|