dsh-skill-picker 0.3.2 → 0.3.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 +4 -2
- package/lib/client.js +48 -25
- package/lib/client.js.map +2 -2
- package/lib/index.js +6 -1
- package/lib/index.js.map +2 -2
- package/package.json +1 -1
- package/src/client/index.jsx +62 -32
- package/src/index.js +21 -4
package/lib/index.js
CHANGED
|
@@ -9,6 +9,10 @@ function userSkillsDir() {
|
|
|
9
9
|
const home = process.env.DSH_HOME ?? path.join(os.homedir(), ".dsh");
|
|
10
10
|
return path.join(home, "skills");
|
|
11
11
|
}
|
|
12
|
+
function userAgentsSkillsDir() {
|
|
13
|
+
const agentsHome = process.env.DSH_AGENTS_HOME ?? path.join(os.homedir(), ".agents");
|
|
14
|
+
return path.join(agentsHome, "skills");
|
|
15
|
+
}
|
|
12
16
|
function parseFrontmatter(content) {
|
|
13
17
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
14
18
|
if (!match) return {};
|
|
@@ -47,10 +51,11 @@ async function scanSkillsDirInto(map, dir) {
|
|
|
47
51
|
}
|
|
48
52
|
async function scanSkills(cwd) {
|
|
49
53
|
const map = /* @__PURE__ */ new Map();
|
|
54
|
+
await scanSkillsDirInto(map, userAgentsSkillsDir());
|
|
50
55
|
await scanSkillsDirInto(map, userSkillsDir());
|
|
51
56
|
if (typeof cwd === "string" && cwd !== "") {
|
|
52
|
-
await scanSkillsDirInto(map, path.join(cwd, ".dsh", "skills"));
|
|
53
57
|
await scanSkillsDirInto(map, path.join(cwd, ".agents", "skills"));
|
|
58
|
+
await scanSkillsDirInto(map, path.join(cwd, ".dsh", "skills"));
|
|
54
59
|
}
|
|
55
60
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
56
61
|
}
|
package/lib/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.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\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/** 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
|
|
5
|
-
"mappings": ";AAoBA,SAAS,UAAU,eAAe;AAClC,OAAO,QAAQ;AACf,OAAO,UAAU;AAGV,IAAM,SAAS,CAAC,aAAa,cAAc;AAGlD,IAAM,gBAAgB;AAGf,IAAM,wBACX;AAGF,SAAS,gBAAgB;AACvB,QAAM,OAAO,QAAQ,IAAI,YAAY,KAAK,KAAK,GAAG,QAAQ,GAAG,MAAM;AACnE,SAAO,KAAK,KAAK,MAAM,QAAQ;AACjC;AAGA,SAAS,iBAAiB,SAAS;AACjC,QAAM,QAAQ,QAAQ,MAAM,6BAA6B;AACzD,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,QAAM,MAAM,CAAC;AACb,aAAW,QAAQ,MAAM,CAAC,EAAE,MAAM,OAAO,GAAG;AAC1C,UAAM,KAAK,KAAK,MAAM,4BAA4B;AAClD,QAAI,CAAC,GAAI;AACT,UAAM,QAAQ,GAAG,CAAC,EAAE,KAAK,EAAE,QAAQ,gBAAgB,EAAE;AACrD,QAAI,UAAU,GAAI,KAAI,GAAG,CAAC,CAAC,IAAI;AAAA,EACjC;AACA,SAAO;AACT;AAGA,eAAe,kBAAkB,KAAK,KAAK;AACzC,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;AAAA,EACtD,QAAQ;AACN;AAAA,EACF;AACA,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,YAAY,EAAG;AAC1B,UAAM,WAAW,KAAK,KAAK,KAAK,MAAM,IAAI;AAC1C,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,SAAS,KAAK,KAAK,UAAU,UAAU,GAAG,MAAM;AAAA,IAClE,QAAQ;AACN;AAAA,IACF;AACA,UAAM,OAAO,iBAAiB,OAAO;AAErC,QAAI,IAAI,KAAK,QAAQ,MAAM,MAAM;AAAA,MAC/B,MAAM,KAAK,QAAQ,MAAM;AAAA,MACzB,aAAa,KAAK,eAAe;AAAA,MACjC,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;
|
|
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\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"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
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.3.
|
|
4
|
+
"version": "0.3.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/src/client/index.jsx
CHANGED
|
@@ -188,6 +188,26 @@ const statusStyle = {
|
|
|
188
188
|
fontSize: '13px',
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/** Lightweight source badge shown only when the list came from the host scan fallback (official API unavailable). */
|
|
192
|
+
const sourceBadgeStyle = {
|
|
193
|
+
display: 'inline-flex',
|
|
194
|
+
alignItems: 'center',
|
|
195
|
+
alignSelf: 'flex-start',
|
|
196
|
+
margin: '0 8px 8px',
|
|
197
|
+
padding: '2px 8px',
|
|
198
|
+
border: '1px solid rgba(255, 193, 7, 0.35)',
|
|
199
|
+
borderRadius: '999px',
|
|
200
|
+
background: 'rgba(255, 193, 7, 0.1)',
|
|
201
|
+
color: '#d9a520',
|
|
202
|
+
fontSize: '11px',
|
|
203
|
+
lineHeight: '16px',
|
|
204
|
+
flex: 'none',
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const sourceBadgeTextStyle = {
|
|
208
|
+
fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
|
|
209
|
+
}
|
|
210
|
+
|
|
191
211
|
/** The picker's bolt glyph: DeepSeek palette gradient + slim stroke. */
|
|
192
212
|
function BoltIcon() {
|
|
193
213
|
return (
|
|
@@ -218,6 +238,7 @@ function SkillPickerButton(props) {
|
|
|
218
238
|
const [open, setOpen] = useState(false)
|
|
219
239
|
const [skills, setSkills] = useState(undefined)
|
|
220
240
|
const [error, setError] = useState(undefined)
|
|
241
|
+
const [source, setSource] = useState(undefined)
|
|
221
242
|
const [query, setQuery] = useState('')
|
|
222
243
|
const [usage, setUsage] = useState(() => loadUsage())
|
|
223
244
|
const [active, setActive] = useState(0)
|
|
@@ -232,18 +253,20 @@ function SkillPickerButton(props) {
|
|
|
232
253
|
if (typeof props.listSkills === 'function' && props.session?.sessionId !== undefined) {
|
|
233
254
|
const listed = await props.listSkills(props.session.sessionId)
|
|
234
255
|
setSkills(Array.isArray(listed) ? listed : [])
|
|
256
|
+
setSource('official')
|
|
235
257
|
return
|
|
236
258
|
}
|
|
237
259
|
} catch (cause) {
|
|
238
260
|
console.warn('[dsh-skill-picker] official skills API failed, falling back to host route:', cause)
|
|
239
261
|
}
|
|
240
|
-
// Fallback path: the host's own scan route (
|
|
262
|
+
// Fallback path: the host's own scan route (official provider roots).
|
|
241
263
|
try {
|
|
242
264
|
const cwd = typeof props.cwd === 'string' && props.cwd !== '' ? `?cwd=${encodeURIComponent(props.cwd)}` : ''
|
|
243
265
|
const res = await fetch(`/dsh-skill-picker/skills${cwd}`, { headers: { accept: 'application/json' } })
|
|
244
266
|
const json = await res.json()
|
|
245
267
|
if (!json.ok) throw new Error(json.error || 'bad response')
|
|
246
268
|
setSkills(Array.isArray(json.skills) ? json.skills : [])
|
|
269
|
+
setSource('host')
|
|
247
270
|
} catch (cause) {
|
|
248
271
|
setError(String(cause?.message ?? cause))
|
|
249
272
|
}
|
|
@@ -374,38 +397,45 @@ function SkillPickerButton(props) {
|
|
|
374
397
|
) : skills === undefined ? (
|
|
375
398
|
<div style={statusStyle}>加载中…</div>
|
|
376
399
|
) : (
|
|
377
|
-
|
|
378
|
-
{
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
event
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
400
|
+
<>
|
|
401
|
+
<div style={listStyle}>
|
|
402
|
+
{filtered.length === 0 ? (
|
|
403
|
+
<div style={statusStyle}>没有匹配的技能</div>
|
|
404
|
+
) : (
|
|
405
|
+
filtered.map((skill, index) => (
|
|
406
|
+
<button
|
|
407
|
+
key={skill.name}
|
|
408
|
+
type="button"
|
|
409
|
+
ref={(el) => {
|
|
410
|
+
itemRefs.current[index] = el
|
|
411
|
+
}}
|
|
412
|
+
onClick={() => pick(skill.name)}
|
|
413
|
+
onMouseEnter={(event) => {
|
|
414
|
+
setActive(index)
|
|
415
|
+
event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))'
|
|
416
|
+
}}
|
|
417
|
+
onMouseLeave={(event) => {
|
|
418
|
+
event.currentTarget.style.background = 'transparent'
|
|
419
|
+
}}
|
|
420
|
+
style={{
|
|
421
|
+
...itemStyle,
|
|
422
|
+
...(index === active
|
|
423
|
+
? { background: 'var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))' }
|
|
424
|
+
: {}),
|
|
425
|
+
}}
|
|
426
|
+
>
|
|
427
|
+
<span style={nameStyle}>{`/${skill.name}`}</span>
|
|
428
|
+
<span style={descStyle}>{skill.description ?? ''}</span>
|
|
429
|
+
</button>
|
|
430
|
+
))
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
{source === 'host' && (
|
|
434
|
+
<div style={sourceBadgeStyle} title="官方技能 API 不可用,列表来自本地目录扫描(与官方 / 补全同源)">
|
|
435
|
+
<span style={sourceBadgeTextStyle}>本地扫描</span>
|
|
436
|
+
</div>
|
|
407
437
|
)}
|
|
408
|
-
|
|
438
|
+
</>
|
|
409
439
|
)}
|
|
410
440
|
</div>
|
|
411
441
|
)}
|
package/src/index.js
CHANGED
|
@@ -38,6 +38,19 @@ function userSkillsDir() {
|
|
|
38
38
|
return path.join(home, 'skills')
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the user agents-home skills directory, mirroring the official
|
|
43
|
+
* provider's default (`$DSH_AGENTS_HOME` > `~/.agents`). This is the
|
|
44
|
+
* cross-tool `.agents` convention; the official `dsh-skill-filesystem` scans
|
|
45
|
+
* it as its `user-agents` root (rank 500), so the fallback must too — else
|
|
46
|
+
* the picker's list silently misses skills that DSH's own `/` completion
|
|
47
|
+
* shows (issue #5).
|
|
48
|
+
*/
|
|
49
|
+
function userAgentsSkillsDir() {
|
|
50
|
+
const agentsHome = process.env.DSH_AGENTS_HOME ?? path.join(os.homedir(), '.agents')
|
|
51
|
+
return path.join(agentsHome, 'skills')
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
/** Parse a SKILL.md frontmatter block into a key/value map (flat YAML subset). */
|
|
42
55
|
function parseFrontmatter(content) {
|
|
43
56
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
@@ -80,17 +93,21 @@ async function scanSkillsDirInto(map, dir) {
|
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
/**
|
|
83
|
-
* Scan
|
|
84
|
-
*
|
|
85
|
-
*
|
|
96
|
+
* Scan the same roots the official `dsh-skill-filesystem` provider uses, so
|
|
97
|
+
* the fallback route stays in sync with what agents actually load:
|
|
98
|
+
* project `.dsh/skills` (rank 100) > project `.agents/skills` (200) >
|
|
99
|
+
* user `~/.dsh/skills` (400) > user `~/.agents/skills` (500). Scanned
|
|
100
|
+
* low-priority first, so later writes (higher priority) win in the map.
|
|
101
|
+
* Never throws (a missing dir yields []).
|
|
86
102
|
* @param cwd - the active session's workspace root (undefined = user level only).
|
|
87
103
|
*/
|
|
88
104
|
async function scanSkills(cwd) {
|
|
89
105
|
const map = new Map()
|
|
106
|
+
await scanSkillsDirInto(map, userAgentsSkillsDir())
|
|
90
107
|
await scanSkillsDirInto(map, userSkillsDir())
|
|
91
108
|
if (typeof cwd === 'string' && cwd !== '') {
|
|
92
|
-
await scanSkillsDirInto(map, path.join(cwd, '.dsh', 'skills'))
|
|
93
109
|
await scanSkillsDirInto(map, path.join(cwd, '.agents', 'skills'))
|
|
110
|
+
await scanSkillsDirInto(map, path.join(cwd, '.dsh', 'skills'))
|
|
94
111
|
}
|
|
95
112
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))
|
|
96
113
|
}
|