dsh-skill-picker 0.5.13 → 0.5.18

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/lib/index.js CHANGED
@@ -205,7 +205,7 @@ async function healUiSkillPatches() {
205
205
  }
206
206
  const note = await packedDesktopNote();
207
207
  if (files.length === 0) {
208
- console.warn(`[dsh-skill-picker] ui-skill patch: 0 target client.js found under ${path2.join(dshHome(), "profiles")} \u2014 fuzzy+pinyin matching will NOT be applied.${note} See https://github.com/a735624258/dsh-skill-picker/issues/9`);
208
+ console.warn(`[dsh-skill-picker] ui-skill patch: 0 target client.js found under ${path2.join(dshHome(), "profiles")} \u2014 fuzzy+pinyin matching will NOT be applied.${note} See https://github.com/a735624258/dsh-skill-picker/issues/14`);
209
209
  } else if (note !== "") {
210
210
  console.warn(`[dsh-skill-picker] ui-skill patch: patched ${files.length} profile copy/copies, but none of them is the one this host serves.${note}`);
211
211
  }
package/lib/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
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/** The YAML frontmatter block of a SKILL.md body, or undefined when absent. */\nfunction frontmatterBlock(content) {\n const match = content.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---/)\n return match === null ? undefined : match[1]\n}\n\n/** Parse a SKILL.md frontmatter block into a key/value map (flat YAML subset). */\nfunction parseFrontmatter(content) {\n const block = frontmatterBlock(content)\n if (block === undefined) return {}\n const out = {}\n for (const line of block.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/**\n * Whether the frontmatter carries `key` at all, whatever its value.\n * `parseFrontmatter` drops empty values, so presence checks that must also\n * see a bare `key:` line go through here instead.\n */\nfunction hasFrontmatterKey(content, key) {\n const block = frontmatterBlock(content)\n if (block === undefined) return false\n return block.split(/\\r?\\n/).some((line) => {\n const kv = line.match(/^([A-Za-z0-9_-]+):\\s*(.*)$/)\n return kv !== null && kv[1] === key\n })\n}\n\n/**\n * Legacy invocation spellings the official `dsh-skill-filesystem` provider\n * rejects outright (`rejectLegacyInvocationKey`) \u2014 a skill carrying one is\n * dropped from every surface, so the picker must drop it too.\n */\nconst LEGACY_INVOCATION_KEYS = ['disableModelInvocation', 'modelInvocable', 'userInvocable']\n\n/**\n * Read a frontmatter boolean with the official provider's semantics: YAML\n * booleans plus the case-insensitive `true`/`false`, `yes`/`no`, `on`/`off`\n * and `1`/`0` forms. An absent key yields undefined (\"surface permitted\").\n */\nfunction frontmatterBoolean(meta, key) {\n if (!Object.hasOwn(meta, key)) return undefined\n const value = meta[key]\n if (typeof value === 'boolean') return value\n if (value === 1 || value === '1') return true\n if (value === 0 || value === '0') return false\n if (typeof value === 'string') {\n switch (value.toLowerCase()) {\n case 'true':\n case 'yes':\n case 'on':\n return true\n case 'false':\n case 'no':\n case 'off':\n return false\n }\n }\n throw new TypeError(`frontmatter field \"${key}\" must be a boolean`)\n}\n\n/**\n * Whether a scanned skill may be offered by the picker, i.e. whether it is\n * user-invocable. Mirrors `parseInvocationPolicy` in\n * `@deepseek-ai/dsh-skill-filesystem`: `user-invocable: false` keeps the skill\n * out of human-facing commands, a rejected spelling or a legacy invocation key\n * drops it entirely.\n *\n * Why this exists (issue #10): the official `skills/list` Remote filters with\n * `isUserInvocable` before answering, but this host's own fallback scan read\n * name/description only \u2014 so the picker offered skills the official `/` menu\n * hides, and picking one inserted a `/name` gesture that `dsh-tool-skill` then\n * refused to load (a silent no-op).\n *\n * @param meta - parsed frontmatter map.\n * @param content - the raw SKILL.md body (legacy keys are detected on it).\n */\nfunction isUserInvocableSkill(meta, content) {\n for (const legacy of LEGACY_INVOCATION_KEYS) {\n if (hasFrontmatterKey(content, legacy)) return false\n }\n try {\n // Omitted \u2192 permitted; only an explicit `false` hides the skill.\n return frontmatterBoolean(meta, 'user-invocable') !== false\n } catch {\n return false\n }\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 // The picker is a human-facing surface, so honour the invocation policy:\n // `user-invocable: false` (and the legacy spellings the official provider\n // rejects) must never be offered. The official `skills/list` Remote filters\n // the same way \u2014 without this the fallback route listed skills that the\n // official `/` menu hides and that the `/name` gesture then refuses to load\n // (issue #10).\n if (!isUserInvocableSkill(meta, content)) continue\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 * Decide what (if anything) the ui-skill self-heal patch should say on boot\n * (issue #8).\n *\n * `healUiSkillPatches()` reports **every target file it found**, whether or not\n * this boot changed it. Keying the log off `report.files.length` therefore\n * printed a report-shaped JSON line on *every* start \u2014 even when the patches\n * were already up to date \u2014 which reads like a warning and buries the lines\n * that really matter. Speak up only when there is something to say:\n *\n * - `noop` \u2192 an anchor did not match: the official implementation moved and\n * the enhancement is NOT applied. Always a warning (this is the\n * same class of silent failure as issue #7).\n * - `patched` \u2192 this boot really rewrote a file: one report line.\n * - `errors` \u2192 I/O failures: one warning (folded into the report line when a\n * patch also succeeded, so a single line carries both).\n * - none \u2192 silent: the install is already up to date.\n *\n * Set `DSH_SKILL_PICKER_LOG=debug` for the full report including `skipped`.\n *\n * Exported for tests; `io` is injectable so they can capture the output.\n *\n * @param {{files?: Array, errors?: string[]}} report - result of healUiSkillPatches().\n * @param {Pick<Console, 'log' | 'warn'>} [io] - sinks (defaults to the console).\n */\nexport function reportUiSkillPatches(report, io = console) {\n const files = report?.files ?? []\n const errors = report?.errors ?? []\n // An anchor miss means the enhancement silently did not apply \u2014 never bury it.\n const stale = files.filter((file) => file.noop.length > 0)\n if (stale.length > 0) {\n io.warn('[dsh-skill-picker] ui-skill patch: anchors not found, enhancement NOT applied '\n + '(the official implementation likely changed):', JSON.stringify(stale))\n }\n const changed = files.filter((file) => file.patched.length > 0)\n // Read per call so the switch also works when set by tests or a wrapper.\n const debug = process.env.DSH_SKILL_PICKER_LOG === 'debug'\n if (debug) {\n io.log('[dsh-skill-picker] ui-skill patch report (debug):', JSON.stringify({ files, errors }))\n } else if (changed.length > 0) {\n // Only the files this boot actually touched, plus every error.\n io.log('[dsh-skill-picker] ui-skill patch report:', JSON.stringify({ files: changed, errors }))\n } else if (errors.length > 0) {\n io.warn('[dsh-skill-picker] ui-skill patch errors:', JSON.stringify(errors))\n }\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 //\n // Logging is gated on \"did this boot change anything / fail\" rather than\n // \"was a target found\" \u2014 see `reportUiSkillPatches` (issue #8).\n ctx.effect(() => {\n healUiSkillPatches().then((report) => {\n reportUiSkillPatches(report)\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", "/**\r\n * dsh-skill-picker \u2014 host-side self-healing patch for the official\r\n * `@deepseek-ai/dsh-client-ui-skill` package.\r\n *\r\n * The picker upgrades the official `/` completion in two ways that the\r\n * official package does not provide out of the box:\r\n *\r\n * 1. `order: 2 \u2192 -1` \u2014 the skill group sorts ABOVE the command group\r\n * (commands register with the default order 0; lower = higher in the\r\n * official menu).\r\n * 2. fuzzy+pinyin candidates \u2014 the official prefix-only matcher\r\n * (`skill.name.startsWith(query)`) is replaced by the picker's\r\n * `window.__dshSkillPickerFuzzy` matcher when the picker is mounted\r\n * (single source group, same list, upgraded matching).\r\n *\r\n * Every DSH boot this module scans every profile under `$DSH_HOME/profiles`\r\n * (default `~/.dsh/profiles`) for an installed ui-skill `lib/client.js` \u2014 the\r\n * shared core root (`profiles/node_modules/@deepseek-ai/\u2026`, used by global\r\n * installs), the user's local patched copy (`local/dsh-client-ui-skill`), or\r\n * the plain npm install (`node_modules/@deepseek-ai/dsh-client-ui-skill`) \u2014 and\r\n * re-applies both patches when DSH upgrades overwrote them. The original file\r\n * is backed up once as `<file>.dsh-skill-picker.bak` before the first write.\r\n * All operations are idempotent and never throw: a missing profile, package\r\n * or read failure is reported and skipped so a broken patch can never take\r\n * the host down.\r\n *\r\n * @module dsh-skill-picker/patch-ui-skill\r\n */\r\n\r\nimport { readFile, writeFile, copyFile, readdir, access, realpath, rename } from 'node:fs/promises'\r\nimport { createRequire } from 'node:module'\r\nimport os from 'node:os'\r\nimport path from 'node:path'\r\n\r\nimport { isDirectoryEntry } from './dir-entry.js'\r\n\r\n/** Marker that the candidates patch is already in place. */\r\nconst FUZZY_MARKER = '__dshSkillPickerFuzzy'\r\n\r\n/** Marker that the pick-tracking patch is already in place. */\r\nconst TRACK_MARKER = '__dshSkillPickerTrack'\r\n\r\n/** The self-healing patches, in application order. */\r\nexport const PATCHES = [\r\n {\r\n id: 'order',\r\n title: 'skill group order 2 \u2192 -1 (above commands)',\r\n isApplied(text) {\r\n return /name: \"skill\",[\\s\\S]*?order:\\s*-1,/.test(text)\r\n },\r\n apply(text) {\r\n return text.replace(/(name: \"skill\",\\s*order: )2,/, '$1-1,')\r\n },\r\n },\r\n {\r\n id: 'fuzzy-candidates',\r\n title: 'prefix/rank matcher \u2192 fuzzy+pinyin matcher',\r\n isApplied(text) {\r\n return text.includes(FUZZY_MARKER)\r\n },\r\n apply(text) {\r\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\r\n // 0.1.2-alpha.x : return skills.filter((skill) => skill.name.startsWith(query)).map(...)\r\n // 0.1.5-rc.x : return (0, _xxx.rankByName)(skills, query).map(...) \u2190 \u6539\u6210 async candidates\r\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\r\n const ANCHORS = [\r\n /(\\t*)return (\\(0, [\\w.$]+\\.rankByName\\)\\(skills, query\\)|rankByName\\(skills, query\\))\\.map\\(\\(skill\\) => \\(\\{/,\r\n /(\\t*)return (skills\\.filter\\(\\(skill\\) => skill\\.name\\.startsWith\\(query\\)\\))\\.map\\(\\(skill\\) => \\(\\{/,\r\n ]\r\n for (const re of ANCHORS) {\r\n const m = text.match(re)\r\n if (!m) continue\r\n const [, indent, official] = m\r\n return text.replace(\r\n re,\r\n `${indent}// dsh-skill-picker patch: fuzzy+pinyin matcher (self-healed)\\n` +\r\n `${indent}const officialMatcher = ${official};\\n` +\r\n `${indent}const matcher = typeof window.${FUZZY_MARKER} === \"function\" ? window.${FUZZY_MARKER}(skills, query) : officialMatcher;\\n` +\r\n `${indent}return matcher.map((skill) => ({`,\r\n )\r\n }\r\n return text\r\n },\r\n },\r\n {\r\n id: 'pick-tracking',\r\n title: 'record usage when picked from the official / menu',\r\n isApplied(text) {\r\n return text.includes(TRACK_MARKER)\r\n },\r\n apply(text) {\r\n return text.replace(\r\n /(\\t*)onPick\\(\\{ candidate \\}\\) \\{\\n(\\t*)return \\{ text: `\\/\\$\\{candidate\\.name\\} ` \\};\\n(\\t*)\\}/,\r\n (match, i1, i2, i3) =>\r\n `${i1}onPick({ candidate }) {\\n` +\r\n `${i2} // dsh-skill-picker patch: usage tracking (self-healed)\\n` +\r\n `${i2} try { window.${TRACK_MARKER}?.(candidate.name) } catch { /* best-effort */ }\\n` +\r\n `${i2} return { text: \\`/\\${candidate.name} \\` };\\n` +\r\n `${i3}}`,\r\n )\r\n },\r\n },\r\n]\r\n\r\n/** Resolve the DSH home directory, mirroring the official convention. */\r\nexport function dshHome() {\r\n return process.env.DSH_HOME ?? path.join(os.homedir(), '.dsh')\r\n}\r\n\r\n/** Subpath of the official skill UI inside any installation tree. */\r\nconst UI_SKILL_SUBPATH = ['node_modules', '@deepseek-ai', 'dsh-client-ui-skill', 'lib', 'client.js']\r\n\r\n/** Whether a path exists (any type). Never throws. */\r\nasync function pathExists(candidate) {\r\n try {\r\n await access(candidate)\r\n return true\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\n/**\r\n * Where the *running desktop build* keeps its own copy of the official skill UI.\r\n *\r\n * A packaged DSH Desktop ships that UI in one of two shapes (issue #9):\r\n *\r\n * - `resources/app/node_modules/\u2026` \u2014 an **unpacked** build. The app is the\r\n * installation-scoped owner, so this file is what gets served, and it is an\r\n * ordinary writable file.\r\n * - `resources/app.asar/dsh/node_modules/\u2026` \u2014 a **packed** build. Electron's\r\n * `fs` shim can *read* through the archive but cannot write into it, so this\r\n * path is evidence for diagnosis only and is never a patch target.\r\n *\r\n * `process.resourcesPath` only exists inside Electron; a plain `dsh` CLI or web\r\n * run returns empty lists and nothing changes for it.\r\n * @returns {{writable: string[], packed: string[]}} candidate paths.\r\n */\r\nexport function desktopUiSkillPaths() {\r\n const resources = process.resourcesPath\r\n if (typeof resources !== 'string' || resources === '') return { writable: [], packed: [] }\r\n return {\r\n writable: [path.join(resources, 'app', ...UI_SKILL_SUBPATH)],\r\n packed: [path.join(resources, 'app.asar', 'dsh', ...UI_SKILL_SUBPATH)],\r\n }\r\n}\r\n\r\n/**\r\n * Whether a profile manifest redirects the official skill UI to its own copy.\r\n *\r\n * This is the switch that decides which copy a desktop build serves: without a\r\n * declared dependency the profile resolves the package from the installation\r\n * (`resources/app` or `app.asar`), and every profile-local copy is dead weight.\r\n * @param profileDir - absolute profile directory.\r\n * @returns true when the manifest declares `@deepseek-ai/dsh-client-ui-skill`.\r\n */\r\nasync function profileRedirectsUiSkill(profileDir) {\r\n try {\r\n const manifest = JSON.parse(await readFile(path.join(profileDir, 'package.json'), 'utf8'))\r\n const declared = { ...manifest?.dependencies, ...manifest?.devDependencies }\r\n return typeof declared['@deepseek-ai/dsh-client-ui-skill'] === 'string'\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\n/**\r\n * Explain why the patch targets found (or not found) may not be what the host\r\n * actually serves, for a packed desktop build (issue #9).\r\n *\r\n * Silence here was the worst part of #9: patching a stale profile copy looks\r\n * exactly like success, so the `/` menu kept its official matcher with no\r\n * signal at all.\r\n * @returns a sentence to append to a warning, or '' when there is nothing to say.\r\n */\r\nasync function packedDesktopNote() {\r\n const { packed } = desktopUiSkillPaths()\r\n if (packed.length === 0 || !(await pathExists(packed[0]))) return ''\r\n const profilesDir = path.join(dshHome(), 'profiles')\r\n let redirected = false\r\n try {\r\n for (const entry of await readdir(profilesDir, { withFileTypes: true })) {\r\n if (entry.name === 'node_modules') continue\r\n if (await profileRedirectsUiSkill(path.join(profilesDir, entry.name))) {\r\n redirected = true\r\n break\r\n }\r\n }\r\n } catch {\r\n /* unreadable profile list: report the pessimistic case */\r\n }\r\n if (redirected) return ''\r\n return ' This desktop build keeps the official skill UI inside `app.asar`, which cannot be written in place,'\r\n + ' and no profile redirects the package to a local copy \u2014 so the served `/` menu keeps the official matcher.'\r\n + ' Install a profile-local override (`\"@deepseek-ai/dsh-client-ui-skill\": \"link:\u2026\"`) or run DSH from the CLI.'\r\n}\r\n\r\n/**\r\n * Enumerate every installed ui-skill `lib/client.js` across all profiles.\r\n *\r\n * Four layouts are covered (deduplicated by real path):\r\n * 1. the **shared core root** `profiles/node_modules/@deepseek-ai/\u2026` \u2014 used by\r\n * a global `npm i -g @deepseek-ai/dsh`, where the official package is NOT\r\n * below any single profile (issue #7 \u2014 missing this made the whole patch\r\n * silently no-op for those installs);\r\n * 2. the user's local patched copy (`profiles/<p>/local/dsh-client-ui-skill`)\r\n * \u2014 that is what a linked profile actually loads;\r\n * 3. the plain npm install inside a profile\r\n * (`profiles/<p>/node_modules/@deepseek-ai/\u2026`);\r\n * 4. the **running desktop build's own unpacked tree**\r\n * (`resources/app/node_modules/@deepseek-ai/\u2026`) \u2014 issue #9: a desktop\r\n * install owns the package at installation scope, so its copy is the one\r\n * served whenever no profile redirects the name.\r\n *\r\n * As a defensive extra, each profile is also asked through Node's own resolver\r\n * (`createRequire().resolve()`), so a layout we did not enumerate still works.\r\n * Never throws \u2014 a missing profiles dir yields [].\r\n * @returns {Promise<string[]>} candidate file paths.\r\n */\r\nexport async function uiSkillClientPaths() {\r\n const profilesDir = path.join(dshHome(), 'profiles')\r\n let profiles\r\n try {\r\n profiles = await readdir(profilesDir, { withFileTypes: true })\r\n } catch {\r\n // A desktop build can still be patchable through its own resource tree\r\n // even when $DSH_HOME/profiles is absent or unreadable.\r\n profiles = []\r\n }\r\n const seen = new Set()\r\n const found = []\r\n\r\n /** Add one candidate if it exists; deduplicate by real path. */\r\n const collect = async (candidate) => {\r\n try {\r\n await access(candidate)\r\n const real = await realpath(candidate)\r\n if (seen.has(real)) return\r\n seen.add(real)\r\n found.push(candidate)\r\n } catch {\r\n /* not present at this location */\r\n }\r\n }\r\n\r\n /** Ask Node's resolver where this profile would load the package from. */\r\n const collectByResolve = async (profileDir) => {\r\n try {\r\n const require = createRequire(path.join(profileDir, 'package.json'))\r\n await collect(require.resolve('@deepseek-ai/dsh-client-ui-skill/lib/client.js'))\r\n } catch {\r\n /* not resolvable from this profile */\r\n }\r\n }\r\n\r\n // 1. shared core root (global installs) \u2014 see the doc comment above.\r\n await collect(path.join(profilesDir, 'node_modules', '@deepseek-ai', 'dsh-client-ui-skill', 'lib', 'client.js'))\r\n\r\n // 4. the running desktop build's own unpacked tree (issue #9): this is the\r\n // copy a packaged Electron app serves when no profile redirects the name.\r\n for (const candidate of desktopUiSkillPaths().writable) await collect(candidate)\r\n\r\n for (const entry of profiles) {\r\n // `node_modules` is a sibling of the profiles, not a profile itself.\r\n if (entry.name === 'node_modules') continue\r\n // A profile directory may itself be reached through a link; follow it\r\n // instead of relying on the lstat-based dirent (see dir-entry.js).\r\n if (!(await isDirectoryEntry(profilesDir, entry))) continue\r\n // 2. the user's local patched copy (what a linked profile actually loads).\r\n await collect(path.join(profilesDir, entry.name, 'local', 'dsh-client-ui-skill', 'lib', 'client.js'))\r\n // 3. the plain npm install inside the profile.\r\n await collect(path.join(profilesDir, entry.name, 'node_modules', '@deepseek-ai', 'dsh-client-ui-skill', 'lib', 'client.js'))\r\n // Defensive: whatever Node itself would resolve (dupes removed above).\r\n await collectByResolve(path.join(profilesDir, entry.name))\r\n }\r\n return found\r\n}\r\n\r\n/**\r\n * Apply both patches to one ui-skill client.js. Idempotent: already-applied\r\n * patches are reported as skipped; the original file is backed up once before\r\n * the first modification. Never throws for a patch that does not match (it is\r\n * reported as `noop`), only for actual I/O failures.\r\n * @param {string} file - absolute path to the target client.js.\r\n * @returns {Promise<{file: string, patched: string[], skipped: string[], noop: string[]}>}\r\n */\r\nexport async function patchUiSkillFile(file) {\r\n const text = await readFile(file, 'utf8')\r\n const result = { file, patched: [], skipped: [], noop: [] }\r\n let next = text\r\n for (const patch of PATCHES) {\r\n if (patch.isApplied(next)) {\r\n result.skipped.push(patch.id)\r\n continue\r\n }\r\n const candidate = patch.apply(next)\r\n if (candidate === next) {\r\n result.noop.push(patch.id)\r\n continue\r\n }\r\n next = candidate\r\n result.patched.push(patch.id)\r\n }\r\n if (result.patched.length === 0) return result\r\n const backup = `${file}.dsh-skill-picker.bak`\r\n try {\r\n await access(backup)\r\n } catch {\r\n await copyFile(file, backup)\r\n }\r\n // Write through a temp file + rename instead of an in-place `writeFile`:\r\n // - pnpm installs are HARDLINKED to a shared content-addressable store, so\r\n // writing in place would mutate that shared inode and silently change\r\n // every other project using the same package version. rename() swaps the\r\n // directory entry instead, leaving the shared inode untouched.\r\n // - rename is also atomic, so an interrupted boot cannot leave a torn file.\r\n const tmp = `${file}.dsh-skill-picker.tmp`\r\n await writeFile(tmp, next, 'utf8')\r\n await rename(tmp, file)\r\n return result\r\n}\r\n\r\n/**\r\n * Self-heal entry point: scan all profiles, patch every ui-skill copy found,\r\n * and return one combined report. Never throws \u2014 each failure is collected\r\n * into `errors` so the host boot is never taken down by a broken patch.\r\n * @returns {Promise<{files: Array, errors: string[]}>}\r\n */\r\nexport async function healUiSkillPatches() {\r\n const files = await uiSkillClientPaths()\r\n const filesReport = []\r\n const errors = []\r\n for (const file of files) {\r\n try {\r\n filesReport.push(await patchUiSkillFile(file))\r\n } catch (error) {\r\n errors.push(`${file}: ${String(error?.message ?? error)}`)\r\n }\r\n }\r\n // Loud on the empty case. Silence here was the most confusing part of\r\n // issue #7: \"found 0 targets\" and \"everything already applied\" used to print\r\n // identically, so nobody could tell the patch had never run at all.\r\n const note = await packedDesktopNote()\r\n if (files.length === 0) {\r\n console.warn('[dsh-skill-picker] ui-skill patch: 0 target client.js found under '\r\n + `${path.join(dshHome(), 'profiles')} \u2014 fuzzy+pinyin matching will NOT be applied.`\r\n + `${note} See https://github.com/a735624258/dsh-skill-picker/issues/9`)\r\n } else if (note !== '') {\r\n // Targets existed, so the old code stayed silent \u2014 but none of them is the\r\n // copy the host serves. That is issue #9's silent failure: a \"successful\"\r\n // patch run that changes nothing the user can see.\r\n console.warn(`[dsh-skill-picker] ui-skill patch: patched ${files.length} profile copy/copies,`\r\n + ` but none of them is the one this host serves.${note}`)\r\n }\r\n return { files: filesReport, errors }\r\n}\r\n"],
5
- "mappings": "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",
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/** The YAML frontmatter block of a SKILL.md body, or undefined when absent. */\nfunction frontmatterBlock(content) {\n const match = content.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---/)\n return match === null ? undefined : match[1]\n}\n\n/** Parse a SKILL.md frontmatter block into a key/value map (flat YAML subset). */\nfunction parseFrontmatter(content) {\n const block = frontmatterBlock(content)\n if (block === undefined) return {}\n const out = {}\n for (const line of block.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/**\n * Whether the frontmatter carries `key` at all, whatever its value.\n * `parseFrontmatter` drops empty values, so presence checks that must also\n * see a bare `key:` line go through here instead.\n */\nfunction hasFrontmatterKey(content, key) {\n const block = frontmatterBlock(content)\n if (block === undefined) return false\n return block.split(/\\r?\\n/).some((line) => {\n const kv = line.match(/^([A-Za-z0-9_-]+):\\s*(.*)$/)\n return kv !== null && kv[1] === key\n })\n}\n\n/**\n * Legacy invocation spellings the official `dsh-skill-filesystem` provider\n * rejects outright (`rejectLegacyInvocationKey`) \u2014 a skill carrying one is\n * dropped from every surface, so the picker must drop it too.\n */\nconst LEGACY_INVOCATION_KEYS = ['disableModelInvocation', 'modelInvocable', 'userInvocable']\n\n/**\n * Read a frontmatter boolean with the official provider's semantics: YAML\n * booleans plus the case-insensitive `true`/`false`, `yes`/`no`, `on`/`off`\n * and `1`/`0` forms. An absent key yields undefined (\"surface permitted\").\n */\nfunction frontmatterBoolean(meta, key) {\n if (!Object.hasOwn(meta, key)) return undefined\n const value = meta[key]\n if (typeof value === 'boolean') return value\n if (value === 1 || value === '1') return true\n if (value === 0 || value === '0') return false\n if (typeof value === 'string') {\n switch (value.toLowerCase()) {\n case 'true':\n case 'yes':\n case 'on':\n return true\n case 'false':\n case 'no':\n case 'off':\n return false\n }\n }\n throw new TypeError(`frontmatter field \"${key}\" must be a boolean`)\n}\n\n/**\n * Whether a scanned skill may be offered by the picker, i.e. whether it is\n * user-invocable. Mirrors `parseInvocationPolicy` in\n * `@deepseek-ai/dsh-skill-filesystem`: `user-invocable: false` keeps the skill\n * out of human-facing commands, a rejected spelling or a legacy invocation key\n * drops it entirely.\n *\n * Why this exists (issue #10): the official `skills/list` Remote filters with\n * `isUserInvocable` before answering, but this host's own fallback scan read\n * name/description only \u2014 so the picker offered skills the official `/` menu\n * hides, and picking one inserted a `/name` gesture that `dsh-tool-skill` then\n * refused to load (a silent no-op).\n *\n * @param meta - parsed frontmatter map.\n * @param content - the raw SKILL.md body (legacy keys are detected on it).\n */\nfunction isUserInvocableSkill(meta, content) {\n for (const legacy of LEGACY_INVOCATION_KEYS) {\n if (hasFrontmatterKey(content, legacy)) return false\n }\n try {\n // Omitted \u2192 permitted; only an explicit `false` hides the skill.\n return frontmatterBoolean(meta, 'user-invocable') !== false\n } catch {\n return false\n }\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 // The picker is a human-facing surface, so honour the invocation policy:\n // `user-invocable: false` (and the legacy spellings the official provider\n // rejects) must never be offered. The official `skills/list` Remote filters\n // the same way \u2014 without this the fallback route listed skills that the\n // official `/` menu hides and that the `/name` gesture then refuses to load\n // (issue #10).\n if (!isUserInvocableSkill(meta, content)) continue\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 * Decide what (if anything) the ui-skill self-heal patch should say on boot\n * (issue #8).\n *\n * `healUiSkillPatches()` reports **every target file it found**, whether or not\n * this boot changed it. Keying the log off `report.files.length` therefore\n * printed a report-shaped JSON line on *every* start \u2014 even when the patches\n * were already up to date \u2014 which reads like a warning and buries the lines\n * that really matter. Speak up only when there is something to say:\n *\n * - `noop` \u2192 an anchor did not match: the official implementation moved and\n * the enhancement is NOT applied. Always a warning (this is the\n * same class of silent failure as issue #7).\n * - `patched` \u2192 this boot really rewrote a file: one report line.\n * - `errors` \u2192 I/O failures: one warning (folded into the report line when a\n * patch also succeeded, so a single line carries both).\n * - none \u2192 silent: the install is already up to date.\n *\n * Set `DSH_SKILL_PICKER_LOG=debug` for the full report including `skipped`.\n *\n * Exported for tests; `io` is injectable so they can capture the output.\n *\n * @param {{files?: Array, errors?: string[]}} report - result of healUiSkillPatches().\n * @param {Pick<Console, 'log' | 'warn'>} [io] - sinks (defaults to the console).\n */\nexport function reportUiSkillPatches(report, io = console) {\n const files = report?.files ?? []\n const errors = report?.errors ?? []\n // An anchor miss means the enhancement silently did not apply \u2014 never bury it.\n const stale = files.filter((file) => file.noop.length > 0)\n if (stale.length > 0) {\n io.warn('[dsh-skill-picker] ui-skill patch: anchors not found, enhancement NOT applied '\n + '(the official implementation likely changed):', JSON.stringify(stale))\n }\n const changed = files.filter((file) => file.patched.length > 0)\n // Read per call so the switch also works when set by tests or a wrapper.\n const debug = process.env.DSH_SKILL_PICKER_LOG === 'debug'\n if (debug) {\n io.log('[dsh-skill-picker] ui-skill patch report (debug):', JSON.stringify({ files, errors }))\n } else if (changed.length > 0) {\n // Only the files this boot actually touched, plus every error.\n io.log('[dsh-skill-picker] ui-skill patch report:', JSON.stringify({ files: changed, errors }))\n } else if (errors.length > 0) {\n io.warn('[dsh-skill-picker] ui-skill patch errors:', JSON.stringify(errors))\n }\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 //\n // Logging is gated on \"did this boot change anything / fail\" rather than\n // \"was a target found\" \u2014 see `reportUiSkillPatches` (issue #8).\n ctx.effect(() => {\n healUiSkillPatches().then((report) => {\n reportUiSkillPatches(report)\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", "/**\r\n * dsh-skill-picker \u2014 host-side self-healing patch for the official\r\n * `@deepseek-ai/dsh-client-ui-skill` package.\r\n *\r\n * The picker upgrades the official `/` completion in two ways that the\r\n * official package does not provide out of the box:\r\n *\r\n * 1. `order: 2 \u2192 -1` \u2014 the skill group sorts ABOVE the command group\r\n * (commands register with the default order 0; lower = higher in the\r\n * official menu).\r\n * 2. fuzzy+pinyin candidates \u2014 the official prefix-only matcher\r\n * (`skill.name.startsWith(query)`) is replaced by the picker's\r\n * `window.__dshSkillPickerFuzzy` matcher when the picker is mounted\r\n * (single source group, same list, upgraded matching).\r\n *\r\n * Every DSH boot this module scans every profile under `$DSH_HOME/profiles`\r\n * (default `~/.dsh/profiles`) for an installed ui-skill `lib/client.js` \u2014 the\r\n * shared core root (`profiles/node_modules/@deepseek-ai/\u2026`, used by global\r\n * installs), the user's local patched copy (`local/dsh-client-ui-skill`), or\r\n * the plain npm install (`node_modules/@deepseek-ai/dsh-client-ui-skill`) \u2014 and\r\n * re-applies both patches when DSH upgrades overwrote them. The original file\r\n * is backed up once as `<file>.dsh-skill-picker.bak` before the first write.\r\n * All operations are idempotent and never throw: a missing profile, package\r\n * or read failure is reported and skipped so a broken patch can never take\r\n * the host down.\r\n *\r\n * @module dsh-skill-picker/patch-ui-skill\r\n */\r\n\r\nimport { readFile, writeFile, copyFile, readdir, access, realpath, rename } from 'node:fs/promises'\r\nimport { createRequire } from 'node:module'\r\nimport os from 'node:os'\r\nimport path from 'node:path'\r\n\r\nimport { isDirectoryEntry } from './dir-entry.js'\r\n\r\n/** Marker that the candidates patch is already in place. */\r\nconst FUZZY_MARKER = '__dshSkillPickerFuzzy'\r\n\r\n/** Marker that the pick-tracking patch is already in place. */\r\nconst TRACK_MARKER = '__dshSkillPickerTrack'\r\n\r\n/** The self-healing patches, in application order. */\r\nexport const PATCHES = [\r\n {\r\n id: 'order',\r\n title: 'skill group order 2 \u2192 -1 (above commands)',\r\n isApplied(text) {\r\n return /name: \"skill\",[\\s\\S]*?order:\\s*-1,/.test(text)\r\n },\r\n apply(text) {\r\n return text.replace(/(name: \"skill\",\\s*order: )2,/, '$1-1,')\r\n },\r\n },\r\n {\r\n id: 'fuzzy-candidates',\r\n title: 'prefix/rank matcher \u2192 fuzzy+pinyin matcher',\r\n isApplied(text) {\r\n return text.includes(FUZZY_MARKER)\r\n },\r\n apply(text) {\r\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\r\n // 0.1.2-alpha.x : return skills.filter((skill) => skill.name.startsWith(query)).map(...)\r\n // 0.1.5-rc.x : return (0, _xxx.rankByName)(skills, query).map(...) \u2190 \u6539\u6210 async candidates\r\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\r\n const ANCHORS = [\r\n /(\\t*)return (\\(0, [\\w.$]+\\.rankByName\\)\\(skills, query\\)|rankByName\\(skills, query\\))\\.map\\(\\(skill\\) => \\(\\{/,\r\n /(\\t*)return (skills\\.filter\\(\\(skill\\) => skill\\.name\\.startsWith\\(query\\)\\))\\.map\\(\\(skill\\) => \\(\\{/,\r\n ]\r\n for (const re of ANCHORS) {\r\n const m = text.match(re)\r\n if (!m) continue\r\n const [, indent, official] = m\r\n return text.replace(\r\n re,\r\n `${indent}// dsh-skill-picker patch: fuzzy+pinyin matcher (self-healed)\\n` +\r\n `${indent}const officialMatcher = ${official};\\n` +\r\n `${indent}const matcher = typeof window.${FUZZY_MARKER} === \"function\" ? window.${FUZZY_MARKER}(skills, query) : officialMatcher;\\n` +\r\n `${indent}return matcher.map((skill) => ({`,\r\n )\r\n }\r\n return text\r\n },\r\n },\r\n {\r\n id: 'pick-tracking',\r\n title: 'record usage when picked from the official / menu',\r\n isApplied(text) {\r\n return text.includes(TRACK_MARKER)\r\n },\r\n apply(text) {\r\n return text.replace(\r\n /(\\t*)onPick\\(\\{ candidate \\}\\) \\{\\n(\\t*)return \\{ text: `\\/\\$\\{candidate\\.name\\} ` \\};\\n(\\t*)\\}/,\r\n (match, i1, i2, i3) =>\r\n `${i1}onPick({ candidate }) {\\n` +\r\n `${i2} // dsh-skill-picker patch: usage tracking (self-healed)\\n` +\r\n `${i2} try { window.${TRACK_MARKER}?.(candidate.name) } catch { /* best-effort */ }\\n` +\r\n `${i2} return { text: \\`/\\${candidate.name} \\` };\\n` +\r\n `${i3}}`,\r\n )\r\n },\r\n },\r\n]\r\n\r\n/** Resolve the DSH home directory, mirroring the official convention. */\r\nexport function dshHome() {\r\n return process.env.DSH_HOME ?? path.join(os.homedir(), '.dsh')\r\n}\r\n\r\n/** Subpath of the official skill UI inside any installation tree. */\r\nconst UI_SKILL_SUBPATH = ['node_modules', '@deepseek-ai', 'dsh-client-ui-skill', 'lib', 'client.js']\r\n\r\n/** Whether a path exists (any type). Never throws. */\r\nasync function pathExists(candidate) {\r\n try {\r\n await access(candidate)\r\n return true\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\n/**\r\n * Where the *running desktop build* keeps its own copy of the official skill UI.\r\n *\r\n * A packaged DSH Desktop ships that UI in one of two shapes (issue #9):\r\n *\r\n * - `resources/app/node_modules/\u2026` \u2014 an **unpacked** build. The app is the\r\n * installation-scoped owner, so this file is what gets served, and it is an\r\n * ordinary writable file.\r\n * - `resources/app.asar/dsh/node_modules/\u2026` \u2014 a **packed** build. Electron's\r\n * `fs` shim can *read* through the archive but cannot write into it, so this\r\n * path is evidence for diagnosis only and is never a patch target.\r\n *\r\n * `process.resourcesPath` only exists inside Electron; a plain `dsh` CLI or web\r\n * run returns empty lists and nothing changes for it.\r\n * @returns {{writable: string[], packed: string[]}} candidate paths.\r\n */\r\nexport function desktopUiSkillPaths() {\r\n const resources = process.resourcesPath\r\n if (typeof resources !== 'string' || resources === '') return { writable: [], packed: [] }\r\n return {\r\n writable: [path.join(resources, 'app', ...UI_SKILL_SUBPATH)],\r\n packed: [path.join(resources, 'app.asar', 'dsh', ...UI_SKILL_SUBPATH)],\r\n }\r\n}\r\n\r\n/**\r\n * Whether a profile manifest redirects the official skill UI to its own copy.\r\n *\r\n * This is the switch that decides which copy a desktop build serves: without a\r\n * declared dependency the profile resolves the package from the installation\r\n * (`resources/app` or `app.asar`), and every profile-local copy is dead weight.\r\n * @param profileDir - absolute profile directory.\r\n * @returns true when the manifest declares `@deepseek-ai/dsh-client-ui-skill`.\r\n */\r\nasync function profileRedirectsUiSkill(profileDir) {\r\n try {\r\n const manifest = JSON.parse(await readFile(path.join(profileDir, 'package.json'), 'utf8'))\r\n const declared = { ...manifest?.dependencies, ...manifest?.devDependencies }\r\n return typeof declared['@deepseek-ai/dsh-client-ui-skill'] === 'string'\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\n/**\r\n * Explain why the patch targets found (or not found) may not be what the host\r\n * actually serves, for a packed desktop build (issue #9).\r\n *\r\n * Silence here was the worst part of #9: patching a stale profile copy looks\r\n * exactly like success, so the `/` menu kept its official matcher with no\r\n * signal at all.\r\n * @returns a sentence to append to a warning, or '' when there is nothing to say.\r\n */\r\nasync function packedDesktopNote() {\r\n const { packed } = desktopUiSkillPaths()\r\n if (packed.length === 0 || !(await pathExists(packed[0]))) return ''\r\n const profilesDir = path.join(dshHome(), 'profiles')\r\n let redirected = false\r\n try {\r\n for (const entry of await readdir(profilesDir, { withFileTypes: true })) {\r\n if (entry.name === 'node_modules') continue\r\n if (await profileRedirectsUiSkill(path.join(profilesDir, entry.name))) {\r\n redirected = true\r\n break\r\n }\r\n }\r\n } catch {\r\n /* unreadable profile list: report the pessimistic case */\r\n }\r\n if (redirected) return ''\r\n return ' This desktop build keeps the official skill UI inside `app.asar`, which cannot be written in place,'\r\n + ' and no profile redirects the package to a local copy \u2014 so the served `/` menu keeps the official matcher.'\r\n + ' Install a profile-local override (`\"@deepseek-ai/dsh-client-ui-skill\": \"link:\u2026\"`) or run DSH from the CLI.'\r\n}\r\n\r\n/**\r\n * Enumerate every installed ui-skill `lib/client.js` across all profiles.\r\n *\r\n * Four layouts are covered (deduplicated by real path):\r\n * 1. the **shared core root** `profiles/node_modules/@deepseek-ai/\u2026` \u2014 used by\r\n * a global `npm i -g @deepseek-ai/dsh`, where the official package is NOT\r\n * below any single profile (issue #7 \u2014 missing this made the whole patch\r\n * silently no-op for those installs);\r\n * 2. the user's local patched copy (`profiles/<p>/local/dsh-client-ui-skill`)\r\n * \u2014 that is what a linked profile actually loads;\r\n * 3. the plain npm install inside a profile\r\n * (`profiles/<p>/node_modules/@deepseek-ai/\u2026`);\r\n * 4. the **running desktop build's own unpacked tree**\r\n * (`resources/app/node_modules/@deepseek-ai/\u2026`) \u2014 issue #9: a desktop\r\n * install owns the package at installation scope, so its copy is the one\r\n * served whenever no profile redirects the name.\r\n *\r\n * As a defensive extra, each profile is also asked through Node's own resolver\r\n * (`createRequire().resolve()`), so a layout we did not enumerate still works.\r\n * Never throws \u2014 a missing profiles dir yields [].\r\n * @returns {Promise<string[]>} candidate file paths.\r\n */\r\nexport async function uiSkillClientPaths() {\r\n const profilesDir = path.join(dshHome(), 'profiles')\r\n let profiles\r\n try {\r\n profiles = await readdir(profilesDir, { withFileTypes: true })\r\n } catch {\r\n // A desktop build can still be patchable through its own resource tree\r\n // even when $DSH_HOME/profiles is absent or unreadable.\r\n profiles = []\r\n }\r\n const seen = new Set()\r\n const found = []\r\n\r\n /** Add one candidate if it exists; deduplicate by real path. */\r\n const collect = async (candidate) => {\r\n try {\r\n await access(candidate)\r\n const real = await realpath(candidate)\r\n if (seen.has(real)) return\r\n seen.add(real)\r\n found.push(candidate)\r\n } catch {\r\n /* not present at this location */\r\n }\r\n }\r\n\r\n /** Ask Node's resolver where this profile would load the package from. */\r\n const collectByResolve = async (profileDir) => {\r\n try {\r\n const require = createRequire(path.join(profileDir, 'package.json'))\r\n await collect(require.resolve('@deepseek-ai/dsh-client-ui-skill/lib/client.js'))\r\n } catch {\r\n /* not resolvable from this profile */\r\n }\r\n }\r\n\r\n // 1. shared core root (global installs) \u2014 see the doc comment above.\r\n await collect(path.join(profilesDir, 'node_modules', '@deepseek-ai', 'dsh-client-ui-skill', 'lib', 'client.js'))\r\n\r\n // 4. the running desktop build's own unpacked tree (issue #9): this is the\r\n // copy a packaged Electron app serves when no profile redirects the name.\r\n for (const candidate of desktopUiSkillPaths().writable) await collect(candidate)\r\n\r\n for (const entry of profiles) {\r\n // `node_modules` is a sibling of the profiles, not a profile itself.\r\n if (entry.name === 'node_modules') continue\r\n // A profile directory may itself be reached through a link; follow it\r\n // instead of relying on the lstat-based dirent (see dir-entry.js).\r\n if (!(await isDirectoryEntry(profilesDir, entry))) continue\r\n // 2. the user's local patched copy (what a linked profile actually loads).\r\n await collect(path.join(profilesDir, entry.name, 'local', 'dsh-client-ui-skill', 'lib', 'client.js'))\r\n // 3. the plain npm install inside the profile.\r\n await collect(path.join(profilesDir, entry.name, 'node_modules', '@deepseek-ai', 'dsh-client-ui-skill', 'lib', 'client.js'))\r\n // Defensive: whatever Node itself would resolve (dupes removed above).\r\n await collectByResolve(path.join(profilesDir, entry.name))\r\n }\r\n return found\r\n}\r\n\r\n/**\r\n * Apply both patches to one ui-skill client.js. Idempotent: already-applied\r\n * patches are reported as skipped; the original file is backed up once before\r\n * the first modification. Never throws for a patch that does not match (it is\r\n * reported as `noop`), only for actual I/O failures.\r\n * @param {string} file - absolute path to the target client.js.\r\n * @returns {Promise<{file: string, patched: string[], skipped: string[], noop: string[]}>}\r\n */\r\nexport async function patchUiSkillFile(file) {\r\n const text = await readFile(file, 'utf8')\r\n const result = { file, patched: [], skipped: [], noop: [] }\r\n let next = text\r\n for (const patch of PATCHES) {\r\n if (patch.isApplied(next)) {\r\n result.skipped.push(patch.id)\r\n continue\r\n }\r\n const candidate = patch.apply(next)\r\n if (candidate === next) {\r\n result.noop.push(patch.id)\r\n continue\r\n }\r\n next = candidate\r\n result.patched.push(patch.id)\r\n }\r\n if (result.patched.length === 0) return result\r\n const backup = `${file}.dsh-skill-picker.bak`\r\n try {\r\n await access(backup)\r\n } catch {\r\n await copyFile(file, backup)\r\n }\r\n // Write through a temp file + rename instead of an in-place `writeFile`:\r\n // - pnpm installs are HARDLINKED to a shared content-addressable store, so\r\n // writing in place would mutate that shared inode and silently change\r\n // every other project using the same package version. rename() swaps the\r\n // directory entry instead, leaving the shared inode untouched.\r\n // - rename is also atomic, so an interrupted boot cannot leave a torn file.\r\n const tmp = `${file}.dsh-skill-picker.tmp`\r\n await writeFile(tmp, next, 'utf8')\r\n await rename(tmp, file)\r\n return result\r\n}\r\n\r\n/**\r\n * Self-heal entry point: scan all profiles, patch every ui-skill copy found,\r\n * and return one combined report. Never throws \u2014 each failure is collected\r\n * into `errors` so the host boot is never taken down by a broken patch.\r\n * @returns {Promise<{files: Array, errors: string[]}>}\r\n */\r\nexport async function healUiSkillPatches() {\r\n const files = await uiSkillClientPaths()\r\n const filesReport = []\r\n const errors = []\r\n for (const file of files) {\r\n try {\r\n filesReport.push(await patchUiSkillFile(file))\r\n } catch (error) {\r\n errors.push(`${file}: ${String(error?.message ?? error)}`)\r\n }\r\n }\r\n // Loud on the empty case. Silence here was the most confusing part of\r\n // issue #7: \"found 0 targets\" and \"everything already applied\" used to print\r\n // identically, so nobody could tell the patch had never run at all.\r\n const note = await packedDesktopNote()\r\n if (files.length === 0) {\r\n console.warn('[dsh-skill-picker] ui-skill patch: 0 target client.js found under '\r\n + `${path.join(dshHome(), 'profiles')} \u2014 fuzzy+pinyin matching will NOT be applied.`\r\n + `${note} See https://github.com/a735624258/dsh-skill-picker/issues/14`)\r\n } else if (note !== '') {\r\n // Targets existed, so the old code stayed silent \u2014 but none of them is the\r\n // copy the host serves. That is issue #9's silent failure: a \"successful\"\r\n // patch run that changes nothing the user can see.\r\n console.warn(`[dsh-skill-picker] ui-skill patch: patched ${files.length} profile copy/copies,`\r\n + ` but none of them is the one this host serves.${note}`)\r\n }\r\n return { files: filesReport, errors }\r\n}\r\n"],
5
+ "mappings": "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",
6
6
  "names": ["readFile", "readdir", "os", "path", "path", "path", "require", "path", "os", "readdir", "readFile"]
7
7
  }
package/package.json CHANGED
@@ -1,76 +1,76 @@
1
- {
2
- "name": "dsh-skill-picker",
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.13",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": "./lib/index.js",
9
- "./client": "./lib/client.js",
10
- "./package.json": "./package.json"
11
- },
12
- "scripts": {
13
- "build": "node build.mjs",
14
- "test": "node --test"
15
- },
16
- "devDependencies": {
17
- "esbuild": "^0.25.0",
18
- "fuzzysort": "^4.0.2"
19
- },
20
- "dsh": {
21
- "bundle": {
22
- "patch": "./cordis.patch.yml"
23
- },
24
- "client": {
25
- "inject": [
26
- "@deepseek-ai/dsh-client-runtime",
27
- "@deepseek-ai/dsh-client-locale"
28
- ],
29
- "platform": "web"
30
- },
31
- "compatibility": {
32
- "dshReleases": {
33
- "0.1.1-rc.1": "unknown",
34
- "0.1.1-rc.2": "compatible",
35
- "0.1.2-alpha.2": "compatible",
36
- "0.1.2-alpha.3": "compatible",
37
- "0.1.2-alpha.4": "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",
42
- "0.1.7-rc.1": "unknown",
43
- "0.1.7-rc.2": "unknown",
44
- "0.2.0-rc.1": "compatible",
45
- "0.2.0-rc.2": "compatible"
46
- }
47
- }
48
- },
49
- "engines": {
50
- "node": ">=22"
51
- },
52
- "peerDependencies": {
53
- "@deepseek-ai/cordis": "^4.0.1",
54
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0",
55
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0",
56
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0",
57
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0",
58
- "@deepseek-ai/dsh-skill": "^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0",
59
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0",
60
- "react": "^18.2.0"
61
- },
62
- "files": [
63
- "lib",
64
- "src",
65
- "cordis.patch.yml",
66
- "README.md"
67
- ],
68
- "license": "MIT",
69
- "repository": {
70
- "type": "git",
71
- "url": "https://github.com/a735624258/dsh-skill-picker.git"
72
- },
73
- "dependencies": {
74
- "pinyin-pro": "^3.29.3"
75
- }
76
- }
1
+ {
2
+ "name": "dsh-skill-picker",
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.18",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "scripts": {
13
+ "build": "node build.mjs",
14
+ "test": "node --test"
15
+ },
16
+ "devDependencies": {
17
+ "esbuild": "^0.25.0",
18
+ "fuzzysort": "^4.0.2"
19
+ },
20
+ "dsh": {
21
+ "bundle": {
22
+ "patch": "./cordis.patch.yml"
23
+ },
24
+ "client": {
25
+ "inject": [
26
+ "@deepseek-ai/dsh-client-runtime",
27
+ "@deepseek-ai/dsh-client-locale"
28
+ ],
29
+ "platform": "web"
30
+ },
31
+ "compatibility": {
32
+ "dshReleases": {
33
+ "0.1.1-rc.1": "unknown",
34
+ "0.1.1-rc.2": "compatible",
35
+ "0.1.2-alpha.2": "compatible",
36
+ "0.1.2-alpha.3": "compatible",
37
+ "0.1.2-alpha.4": "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",
42
+ "0.1.7-rc.1": "unknown",
43
+ "0.1.7-rc.2": "unknown",
44
+ "0.2.0-rc.1": "compatible",
45
+ "0.2.0-rc.2": "compatible"
46
+ }
47
+ }
48
+ },
49
+ "engines": {
50
+ "node": ">=22"
51
+ },
52
+ "peerDependencies": {
53
+ "@deepseek-ai/cordis": "^4.0.1",
54
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0",
55
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0",
56
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0",
57
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0",
58
+ "@deepseek-ai/dsh-skill": "^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0",
59
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0",
60
+ "react": "^18.2.0"
61
+ },
62
+ "files": [
63
+ "lib",
64
+ "src",
65
+ "cordis.patch.yml",
66
+ "README.md"
67
+ ],
68
+ "license": "MIT",
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "https://github.com/a735624258/dsh-skill-picker.git"
72
+ },
73
+ "dependencies": {
74
+ "pinyin-pro": "^3.29.3"
75
+ }
76
+ }
@@ -20,6 +20,7 @@ import fuzzysort from 'fuzzysort'
20
20
  import { pinyin } from 'pinyin-pro'
21
21
 
22
22
  import { sessionIdOf, useWorkspaceCwd } from './session-view.js'
23
+ import { installSlashFuzzy, slashEnhancementMode, warmSlashSkill } from './slash-source.js'
23
24
 
24
25
  /** Required services: slot registry, host connection (official skills API), sessions (workspace cwd fallback), input triggers (/ fuzzy source). */
25
26
  export const inject = ['slots', 'connection', 'sessions', 'inputTriggers']
@@ -179,6 +180,47 @@ function matchRank(skill, q) {
179
180
  return 4
180
181
  }
181
182
 
183
+ /**
184
+ * The picker's single ranking rule, shared by the ⚡ panel, the legacy `/`
185
+ * matcher and the runtime `/` takeover (issue #14) so the two lists can never
186
+ * disagree about matching OR order.
187
+ *
188
+ * Only `name` and `description` are read, which is what both raw skill entries
189
+ * and the official menu's already-mapped display items carry.
190
+ *
191
+ * Relevance first (name-startsWith > name-contains > description > pinyin),
192
+ * then the ⚡ panel's pinned/usage order as the tiebreak — fuzzysort decides
193
+ * WHO matches, never the display order. Pure subsequence noise (dispersed
194
+ * letters that never form an actual substring) is dropped.
195
+ *
196
+ * @param items - [{ name, description }]
197
+ * @param query - the raw query string.
198
+ * @returns the ranked subset (all items, in panel order, for an empty query).
199
+ */
200
+ function rankPickerItems(items, query) {
201
+ const ordered = groupByPinned(
202
+ Array.isArray(items) ? items : [],
203
+ loadUsage(),
204
+ loadPinned(),
205
+ ).flatMap((group) => group.items)
206
+ const q = String(query ?? '').trim().toLowerCase()
207
+ if (q === '') return ordered
208
+ const order = new Map(ordered.map((skill, index) => [skill.name, index]))
209
+ const targets = ordered.map((s) => ({
210
+ s,
211
+ search: `${s.name} ${s.description ?? ''} ${skillPinyinText(s.name, s.description ?? '')}`,
212
+ }))
213
+ return fuzzysort.go(q, targets, {
214
+ key: 'search',
215
+ limit: 30,
216
+ threshold: -10000,
217
+ })
218
+ .filter((r) => r.score > 0)
219
+ .map((r) => r.obj.s)
220
+ .filter((s) => matchRank(s, q) < 4)
221
+ .sort((a, b) => matchRank(a, q) - matchRank(b, q) || (order.get(a.name) ?? 0) - (order.get(b.name) ?? 0))
222
+ }
223
+
182
224
  /** Row height matches the resident chrome (access mode, plan, attach, model). */
183
225
  const buttonStyle = {
184
226
  display: 'inline-flex',
@@ -338,6 +380,21 @@ function SkillPickerButton(props) {
338
380
  return () => window.removeEventListener('dsh-skill-picker:usage-updated', onUsageUpdated)
339
381
  }, [])
340
382
 
383
+ // Prime the official `/` catalogue for this Session as soon as it is known.
384
+ //
385
+ // The slash menu highlights the FIRST group that settles and scrolls it into
386
+ // view; the highlight then sticks. The picker keeps the skill group first
387
+ // (`order: -1`), so if the command group wins that race the menu opens
388
+ // scrolled down to 添加/指令 instead of sitting on the first skill. Nothing
389
+ // warms the catalogue on a normal boot (`input-trigger` only calls
390
+ // `source.warm?.()` from `sourceAdded`, i.e. for a source registered after a
391
+ // session controller already exists), so the first `/` of every session is
392
+ // cold. Warming here costs one background request and removes that first loss.
393
+ const activeSessionId = sessionIdOf(props)
394
+ useEffect(() => {
395
+ if (activeSessionId !== undefined) warmSlashSkill(activeSessionId)
396
+ }, [activeSessionId])
397
+
341
398
  // Latest draft mirror: `useInput` is a selector hook and may only be called
342
399
  // during render, while the pick handler runs from a click callback. Sync the
343
400
  // store's current draft into a ref here (render time), so the click handler
@@ -612,6 +669,14 @@ function SkillPickerButton(props) {
612
669
  <span style={sourceBadgeTextStyle}>本地扫描</span>
613
670
  </div>
614
671
  )}
672
+ {slashEnhancementMode() === 'runtime' && (
673
+ <div
674
+ style={sourceBadgeStyle}
675
+ title="`/` 菜单的模糊+拼音搜索由本插件在运行时接管官方技能源(不依赖改写任何文件,issue #14)"
676
+ >
677
+ <span style={sourceBadgeTextStyle}>/ 增强:运行时接管</span>
678
+ </div>
679
+ )}
615
680
  </>
616
681
  )}
617
682
  </div>
@@ -692,11 +757,34 @@ export function apply(ctx) {
692
757
  }
693
758
  }, 'dsh-skill-picker: composer input slot')
694
759
 
695
- // Fuzzy `/` completion: instead of registering a parallel source group
696
- // (which would appear as a second list next to the official one), expose a
697
- // global matcher that the patched official ui-skill candidates calls. The
698
- // official group stays THE single `/` list; only its matching behaviour is
699
- // upgraded to fuzzy + pinyin (name AND description, subsequence scoring).
760
+ // Fuzzy `/` completion.
761
+ //
762
+ // Primary mechanism (v0.5.14, issue #14): take the official source over at
763
+ // runtime through the public `inputTriggers` service — matching, group order
764
+ // AND pick tracking. Nothing on disk is touched, so a packaged desktop build
765
+ // (`app.asar`), a pnpm-hardlinked install and a profile copy reached through
766
+ // a stale symlink all work the same way. See ./slash-source.js.
767
+ //
768
+ // Legacy mechanism (still installed): a global matcher that the *file*-patched
769
+ // official ui-skill calls. Kept because it is the only path on a kernel whose
770
+ // input-trigger service does not expose the registry; harmless when the
771
+ // runtime takeover is doing the work, because the takeover always asks the
772
+ // official candidates for the whole catalogue (empty query) and re-ranks
773
+ // afterwards.
774
+ const trackPick = (name) => {
775
+ const usage = loadUsage()
776
+ const nextUsage = { ...usage, [name]: { count: (usage[name]?.count ?? 0) + 1, lastUsed: Date.now() } }
777
+ saveUsage(nextUsage)
778
+ // Notify the bolt panel (and any other listeners) to re-read storage so a
779
+ // slash pick ranks as "recently used" there too, not only in the official
780
+ // `/` menu.
781
+ try {
782
+ window.dispatchEvent(new CustomEvent('dsh-skill-picker:usage-updated'))
783
+ } catch {
784
+ /* best-effort */
785
+ }
786
+ }
787
+
700
788
  ctx.effect(() => {
701
789
  // Mirror the ⚡ panel's ordering: pinned first, then recently/frequently
702
790
  // used skills, then the untouched rest — so both stay in sync.
@@ -704,55 +792,30 @@ export function apply(ctx) {
704
792
  // Same visibility rule as the ⚡ panel: never let the `/` list surface a
705
793
  // skill the user may not invoke (issue #10).
706
794
  const visible = (Array.isArray(skills) ? skills : []).filter(isUserFacingSkill)
707
- const ordered = groupByPinned(visible, loadUsage(), loadPinned()).flatMap((group) => group.items)
708
- const q = String(query).trim().toLowerCase()
709
- if (q === '') return ordered
710
- // Rank by the ⚡ panel's exact order (pinned → recent → frequent → rest)
711
- // so both lists stay in sync: fuzzysort only decides WHO matches, not
712
- // the display order. Without this, a slash query re-sorts matches by
713
- // match score and the two menus diverge for the same skill.
714
- const rankByName = new Map(ordered.map((skill, index) => [skill.name, index]))
715
- const targets = ordered.map((s) => ({
716
- s,
717
- search: `${s.name} ${s.description ?? ''} ${skillPinyinText(s.name, s.description ?? '')}`,
718
- }))
719
- const results = fuzzysort.go(q, targets, {
720
- key: 'search',
721
- limit: 30,
722
- threshold: -10000,
723
- })
724
- // Relevance first (name-startsWith > name-contains > description >
725
- // pinyin), then the ⚡ panel's pinned/usage order as the tiebreak —
726
- // the shared rule with the bolt panel, so a name-exact skill like
727
- // svg-diagram for "svg" surfaces above merely-recently-used ones.
728
- return results
729
- .filter((r) => r.score > 0)
730
- .map((r) => r.obj.s)
731
- // Drop pure subsequence noise (dispersed letters that never form an
732
- // actual substring): keep only name/description/pinyin hits.
733
- .filter((s) => matchRank(s, q) < 4)
734
- .sort((a, b) => matchRank(a, q) - matchRank(b, q) || (rankByName.get(a.name) ?? 0) - (rankByName.get(b.name) ?? 0))
795
+ return rankPickerItems(visible, query)
735
796
  }
736
797
  window.__dshSkillPickerFuzzy = fuzzyMatch
737
798
  // Usage tracking for picks made from the official `/` menu: the patched
738
799
  // ui-skill onPick calls this so a slash pick ranks like a bolt-panel pick.
739
- const trackPick = (name) => {
740
- const usage = loadUsage()
741
- const nextUsage = { ...usage, [name]: { count: (usage[name]?.count ?? 0) + 1, lastUsed: Date.now() } }
742
- saveUsage(nextUsage)
743
- // Notify the bolt panel (and any other listeners) to re-read storage so
744
- // a slash pick ranks as "recently used" there too, not only in the
745
- // official / menu.
746
- try {
747
- window.dispatchEvent(new CustomEvent('dsh-skill-picker:usage-updated'))
748
- } catch {
749
- /* best-effort */
750
- }
751
- }
752
800
  window.__dshSkillPickerTrack = trackPick
753
801
  return () => {
754
802
  if (window.__dshSkillPickerFuzzy === fuzzyMatch) delete window.__dshSkillPickerFuzzy
755
803
  if (window.__dshSkillPickerTrack === trackPick) delete window.__dshSkillPickerTrack
756
804
  }
757
805
  }, 'dsh-skill-picker: fuzzy matcher for official / source')
806
+
807
+ // Take the official `/` source over at runtime (issue #14): the same three
808
+ // upgrades the file patch applies, without writing to disk.
809
+ ctx.effect(() => {
810
+ const inputTriggers = ctx.inputTriggers ?? ctx.get?.('inputTriggers')
811
+ if (inputTriggers === undefined) return () => {}
812
+ return installSlashFuzzy(inputTriggers, {
813
+ // Rank the official display items ({name, description}) by the same rule
814
+ // the bolt panel uses, so the two lists agree on order as well as match.
815
+ rank: (items, query) => rankPickerItems(items, query),
816
+ // `order: 2` (official) → `-1`: the skill group sorts above commands.
817
+ order: -1,
818
+ onPick: trackPick,
819
+ })
820
+ }, 'dsh-skill-picker: runtime takeover of the official / skill source')
758
821
  }