dsh-skill-picker 0.5.20 → 0.5.22

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
@@ -1,5 +1,5 @@
1
1
  // src/index.js
2
- import { readFile as readFile2, readdir as readdir2 } from "node:fs/promises";
2
+ import { mkdir, readFile as readFile2, readdir as readdir2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
3
3
  import os2 from "node:os";
4
4
  import path3 from "node:path";
5
5
 
@@ -192,6 +192,36 @@ async function patchUiSkillFile(file) {
192
192
  await rename(tmp, file);
193
193
  return result;
194
194
  }
195
+ async function revertUiSkillPatches() {
196
+ const files = await uiSkillClientPaths();
197
+ const restored = [];
198
+ const errors = [];
199
+ for (const file of files) {
200
+ const backup = `${file}.dsh-skill-picker.bak`;
201
+ try {
202
+ let text;
203
+ try {
204
+ text = await readFile(file, "utf8");
205
+ } catch {
206
+ continue;
207
+ }
208
+ if (!PATCHES.some((patch) => patch.isApplied(text))) continue;
209
+ let original;
210
+ try {
211
+ original = await readFile(backup, "utf8");
212
+ } catch {
213
+ continue;
214
+ }
215
+ const tmp = `${file}.dsh-skill-picker.tmp`;
216
+ await writeFile(tmp, original, "utf8");
217
+ await rename(tmp, file);
218
+ restored.push(file);
219
+ } catch (error) {
220
+ errors.push(`${file}: ${String(error?.message ?? error)}`);
221
+ }
222
+ }
223
+ return { restored, errors };
224
+ }
195
225
  async function healUiSkillPatches() {
196
226
  const files = await uiSkillClientPaths();
197
227
  const filesReport = [];
@@ -331,11 +361,93 @@ function reportUiSkillPatches(report, io = console) {
331
361
  io.warn("[dsh-skill-picker] ui-skill patch errors:", JSON.stringify(errors));
332
362
  }
333
363
  }
364
+ function sharedStateFile() {
365
+ return path3.join(dshHome(), "dsh-skill-picker-state.json");
366
+ }
367
+ function normalizeSharedState(value) {
368
+ const source = value !== null && typeof value === "object" ? value : {};
369
+ const pinned = Array.isArray(source.pinned) ? [...new Set(source.pinned.filter((name) => typeof name === "string" && name !== ""))] : [];
370
+ const usage = {};
371
+ const rawUsage = source.usage !== null && typeof source.usage === "object" ? source.usage : {};
372
+ for (const [name, entry] of Object.entries(rawUsage)) {
373
+ if (typeof name !== "string" || name === "") continue;
374
+ const record = entry !== null && typeof entry === "object" ? entry : {};
375
+ const count = Number.isFinite(record.count) ? Math.max(0, Math.trunc(record.count)) : 0;
376
+ const lastUsed = Number.isFinite(record.lastUsed) ? Math.max(0, Math.trunc(record.lastUsed)) : 0;
377
+ if (count === 0 && lastUsed === 0) continue;
378
+ usage[name] = { count, lastUsed };
379
+ }
380
+ return { pinned, usage };
381
+ }
382
+ async function readSharedState() {
383
+ try {
384
+ return normalizeSharedState(JSON.parse(await readFile2(sharedStateFile(), "utf8")));
385
+ } catch {
386
+ return null;
387
+ }
388
+ }
389
+ async function writeSharedState(state) {
390
+ const normal = normalizeSharedState(state);
391
+ const file = sharedStateFile();
392
+ await mkdir(path3.dirname(file), { recursive: true });
393
+ const tmp = `${file}.tmp`;
394
+ await writeFile2(tmp, `${JSON.stringify(normal, null, 2)}
395
+ `, "utf8");
396
+ await rename2(tmp, file);
397
+ return normal;
398
+ }
399
+ function mergeSharedState(a, b) {
400
+ const left = normalizeSharedState(a);
401
+ const right = normalizeSharedState(b);
402
+ const pinned = [...left.pinned];
403
+ for (const name of right.pinned) if (!pinned.includes(name)) pinned.push(name);
404
+ const usage = { ...left.usage };
405
+ for (const [name, entry] of Object.entries(right.usage)) {
406
+ const previous = usage[name];
407
+ usage[name] = previous === void 0 ? entry : { count: Math.max(previous.count, entry.count), lastUsed: Math.max(previous.lastUsed, entry.lastUsed) };
408
+ }
409
+ return normalizeSharedState({ pinned, usage });
410
+ }
411
+ async function readJsonBody(req) {
412
+ const chunks = [];
413
+ let size = 0;
414
+ for await (const chunk of req) {
415
+ size += chunk.length;
416
+ if (size > 1e6) throw new Error("shared state body too large");
417
+ chunks.push(chunk);
418
+ }
419
+ if (chunks.length === 0) return {};
420
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
421
+ }
422
+ async function handleSharedState(req, res, url) {
423
+ const json = (code, payload) => {
424
+ res.writeHead(code, { "content-type": "application/json; charset=utf-8" });
425
+ res.end(JSON.stringify(payload));
426
+ };
427
+ try {
428
+ if (req.method === "PUT" || req.method === "POST") {
429
+ const incoming = normalizeSharedState(await readJsonBody(req));
430
+ const migrate = url.searchParams.get("migrate") === "1";
431
+ const stored = migrate ? await readSharedState() : null;
432
+ const next = stored === null ? await writeSharedState(incoming) : await writeSharedState(mergeSharedState(stored, incoming));
433
+ json(200, { ok: true, state: next });
434
+ return;
435
+ }
436
+ json(200, { ok: true, state: await readSharedState() });
437
+ } catch (error) {
438
+ json(500, { ok: false, error: String(error?.message ?? error) });
439
+ }
440
+ }
334
441
  function apply(ctx) {
335
442
  ctx.effect(() => {
336
443
  const handler = async (req, res) => {
337
444
  try {
338
- const cwd = req.url !== void 0 ? new URL(req.url, "http://dsh").searchParams.get("cwd") ?? void 0 : void 0;
445
+ const url = new URL(req.url ?? "/", "http://dsh");
446
+ if (url.pathname === "/dsh-skill-picker/state") {
447
+ await handleSharedState(req, res, url);
448
+ return;
449
+ }
450
+ const cwd = url.searchParams.get("cwd") ?? void 0;
339
451
  const skills = await scanSkills(cwd);
340
452
  res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
341
453
  res.end(JSON.stringify({ ok: true, complete: true, skills }));
@@ -352,9 +464,19 @@ function apply(ctx) {
352
464
  text: SKILL_PICKER_GUIDANCE
353
465
  }), "dsh-skill-picker: prompt section");
354
466
  ctx.effect(() => {
355
- healUiSkillPatches().then((report) => {
356
- reportUiSkillPatches(report);
357
- }).catch((error) => {
467
+ const enabled = process.env.DSH_SKILL_PICKER_FILE_PATCH === "1";
468
+ const task = enabled ? healUiSkillPatches().then((report) => reportUiSkillPatches(report)) : revertUiSkillPatches().then((report) => {
469
+ if (report.restored.length > 0) {
470
+ console.log(
471
+ "[dsh-skill-picker] ui-skill patch retired; restored original file(s):",
472
+ report.restored.join(", ")
473
+ );
474
+ }
475
+ if (report.errors.length > 0) {
476
+ console.warn("[dsh-skill-picker] ui-skill restore errors:", JSON.stringify(report.errors));
477
+ }
478
+ });
479
+ task.catch((error) => {
358
480
  console.warn("[dsh-skill-picker] ui-skill patch failed:", error);
359
481
  });
360
482
  return () => {
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/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": ";AAoBA,SAAS,YAAAA,WAAU,WAAAC,gBAAe;AAClC,OAAOC,SAAQ;AACf,OAAOC,WAAU;;;ACTjB,SAAS,YAAY;AACrB,OAAO,UAAU;AAejB,eAAsB,iBAAiB,KAAK,OAAO;AACjD,MAAI,MAAM,YAAY,EAAG,QAAO;AAChC,MAAI,CAAC,MAAM,eAAe,EAAG,QAAO;AACpC,MAAI;AACF,YAAQ,MAAM,KAAK,KAAK,KAAK,KAAK,MAAM,IAAI,CAAC,GAAG,YAAY;AAAA,EAC9D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACRA,SAAS,UAAU,WAAW,UAAU,SAAS,QAAQ,UAAU,cAAc;AACjF,SAAS,qBAAqB;AAC9B,OAAO,QAAQ;AACf,OAAOC,WAAU;AAKjB,IAAM,eAAe;AAGrB,IAAM,eAAe;AAGd,IAAM,UAAU;AAAA,EACrB;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,MAAM;AACd,aAAO,qCAAqC,KAAK,IAAI;AAAA,IACvD;AAAA,IACA,MAAM,MAAM;AACV,aAAO,KAAK,QAAQ,gCAAgC,OAAO;AAAA,IAC7D;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,MAAM;AACd,aAAO,KAAK,SAAS,YAAY;AAAA,IACnC;AAAA,IACA,MAAM,MAAM;AAKV,YAAM,UAAU;AAAA,QACd;AAAA,QACA;AAAA,MACF;AACA,iBAAW,MAAM,SAAS;AACxB,cAAM,IAAI,KAAK,MAAM,EAAE;AACvB,YAAI,CAAC,EAAG;AACR,cAAM,CAAC,EAAE,QAAQ,QAAQ,IAAI;AAC7B,eAAO,KAAK;AAAA,UACV;AAAA,UACA,GAAG,MAAM;AAAA,EACJ,MAAM,2BAA2B,QAAQ;AAAA,EACzC,MAAM,iCAAiC,YAAY,4BAA4B,YAAY;AAAA,EAC3F,MAAM;AAAA,QACb;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,MAAM;AACd,aAAO,KAAK,SAAS,YAAY;AAAA,IACnC;AAAA,IACA,MAAM,MAAM;AACV,aAAO,KAAK;AAAA,QACV;AAAA,QACA,CAAC,OAAO,IAAI,IAAI,OACd,GAAG,EAAE;AAAA,EACF,EAAE;AAAA,EACF,EAAE,kBAAkB,YAAY;AAAA,EAChC,EAAE;AAAA,EACF,EAAE;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;AAGO,SAAS,UAAU;AACxB,SAAO,QAAQ,IAAI,YAAYC,MAAK,KAAK,GAAG,QAAQ,GAAG,MAAM;AAC/D;AAGA,IAAM,mBAAmB,CAAC,gBAAgB,gBAAgB,uBAAuB,OAAO,WAAW;AAGnG,eAAe,WAAW,WAAW;AACnC,MAAI;AACF,UAAM,OAAO,SAAS;AACtB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAkBO,SAAS,sBAAsB;AACpC,QAAM,YAAY,QAAQ;AAC1B,MAAI,OAAO,cAAc,YAAY,cAAc,GAAI,QAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,EAAE;AACzF,SAAO;AAAA,IACL,UAAU,CAACA,MAAK,KAAK,WAAW,OAAO,GAAG,gBAAgB,CAAC;AAAA,IAC3D,QAAQ,CAACA,MAAK,KAAK,WAAW,YAAY,OAAO,GAAG,gBAAgB,CAAC;AAAA,EACvE;AACF;AAWA,eAAe,wBAAwB,YAAY;AACjD,MAAI;AACF,UAAM,WAAW,KAAK,MAAM,MAAM,SAASA,MAAK,KAAK,YAAY,cAAc,GAAG,MAAM,CAAC;AACzF,UAAM,WAAW,EAAE,GAAG,UAAU,cAAc,GAAG,UAAU,gBAAgB;AAC3E,WAAO,OAAO,SAAS,kCAAkC,MAAM;AAAA,EACjE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWA,eAAe,oBAAoB;AACjC,QAAM,EAAE,OAAO,IAAI,oBAAoB;AACvC,MAAI,OAAO,WAAW,KAAK,CAAE,MAAM,WAAW,OAAO,CAAC,CAAC,EAAI,QAAO;AAClE,QAAM,cAAcA,MAAK,KAAK,QAAQ,GAAG,UAAU;AACnD,MAAI,aAAa;AACjB,MAAI;AACF,eAAW,SAAS,MAAM,QAAQ,aAAa,EAAE,eAAe,KAAK,CAAC,GAAG;AACvE,UAAI,MAAM,SAAS,eAAgB;AACnC,UAAI,MAAM,wBAAwBA,MAAK,KAAK,aAAa,MAAM,IAAI,CAAC,GAAG;AACrE,qBAAa;AACb;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,MAAI,WAAY,QAAO;AACvB,SAAO;AAGT;AAwBA,eAAsB,qBAAqB;AACzC,QAAM,cAAcA,MAAK,KAAK,QAAQ,GAAG,UAAU;AACnD,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,QAAQ,aAAa,EAAE,eAAe,KAAK,CAAC;AAAA,EAC/D,QAAQ;AAGN,eAAW,CAAC;AAAA,EACd;AACA,QAAM,OAAO,oBAAI,IAAI;AACrB,QAAM,QAAQ,CAAC;AAGf,QAAM,UAAU,OAAO,cAAc;AACnC,QAAI;AACF,YAAM,OAAO,SAAS;AACtB,YAAM,OAAO,MAAM,SAAS,SAAS;AACrC,UAAI,KAAK,IAAI,IAAI,EAAG;AACpB,WAAK,IAAI,IAAI;AACb,YAAM,KAAK,SAAS;AAAA,IACtB,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,mBAAmB,OAAO,eAAe;AAC7C,QAAI;AACF,YAAMC,WAAU,cAAcD,MAAK,KAAK,YAAY,cAAc,CAAC;AACnE,YAAM,QAAQC,SAAQ,QAAQ,gDAAgD,CAAC;AAAA,IACjF,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,QAAQD,MAAK,KAAK,aAAa,gBAAgB,gBAAgB,uBAAuB,OAAO,WAAW,CAAC;AAI/G,aAAW,aAAa,oBAAoB,EAAE,SAAU,OAAM,QAAQ,SAAS;AAE/E,aAAW,SAAS,UAAU;AAE5B,QAAI,MAAM,SAAS,eAAgB;AAGnC,QAAI,CAAE,MAAM,iBAAiB,aAAa,KAAK,EAAI;AAEnD,UAAM,QAAQA,MAAK,KAAK,aAAa,MAAM,MAAM,SAAS,uBAAuB,OAAO,WAAW,CAAC;AAEpG,UAAM,QAAQA,MAAK,KAAK,aAAa,MAAM,MAAM,gBAAgB,gBAAgB,uBAAuB,OAAO,WAAW,CAAC;AAE3H,UAAM,iBAAiBA,MAAK,KAAK,aAAa,MAAM,IAAI,CAAC;AAAA,EAC3D;AACA,SAAO;AACT;AAUA,eAAsB,iBAAiB,MAAM;AAC3C,QAAM,OAAO,MAAM,SAAS,MAAM,MAAM;AACxC,QAAM,SAAS,EAAE,MAAM,SAAS,CAAC,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,EAAE;AAC1D,MAAI,OAAO;AACX,aAAW,SAAS,SAAS;AAC3B,QAAI,MAAM,UAAU,IAAI,GAAG;AACzB,aAAO,QAAQ,KAAK,MAAM,EAAE;AAC5B;AAAA,IACF;AACA,UAAM,YAAY,MAAM,MAAM,IAAI;AAClC,QAAI,cAAc,MAAM;AACtB,aAAO,KAAK,KAAK,MAAM,EAAE;AACzB;AAAA,IACF;AACA,WAAO;AACP,WAAO,QAAQ,KAAK,MAAM,EAAE;AAAA,EAC9B;AACA,MAAI,OAAO,QAAQ,WAAW,EAAG,QAAO;AACxC,QAAM,SAAS,GAAG,IAAI;AACtB,MAAI;AACF,UAAM,OAAO,MAAM;AAAA,EACrB,QAAQ;AACN,UAAM,SAAS,MAAM,MAAM;AAAA,EAC7B;AAOA,QAAM,MAAM,GAAG,IAAI;AACnB,QAAM,UAAU,KAAK,MAAM,MAAM;AACjC,QAAM,OAAO,KAAK,IAAI;AACtB,SAAO;AACT;AAQA,eAAsB,qBAAqB;AACzC,QAAM,QAAQ,MAAM,mBAAmB;AACvC,QAAM,cAAc,CAAC;AACrB,QAAM,SAAS,CAAC;AAChB,aAAW,QAAQ,OAAO;AACxB,QAAI;AACF,kBAAY,KAAK,MAAM,iBAAiB,IAAI,CAAC;AAAA,IAC/C,SAAS,OAAO;AACd,aAAO,KAAK,GAAG,IAAI,KAAK,OAAO,OAAO,WAAW,KAAK,CAAC,EAAE;AAAA,IAC3D;AAAA,EACF;AAIA,QAAM,OAAO,MAAM,kBAAkB;AACrC,MAAI,MAAM,WAAW,GAAG;AACtB,YAAQ,KAAK,qEACNA,MAAK,KAAK,QAAQ,GAAG,UAAU,CAAC,qDAChC,IAAI,+DAA+D;AAAA,EAC5E,WAAW,SAAS,IAAI;AAItB,YAAQ,KAAK,8CAA8C,MAAM,MAAM,sEAClB,IAAI,EAAE;AAAA,EAC7D;AACA,SAAO,EAAE,OAAO,aAAa,OAAO;AACtC;;;AFvUO,IAAM,SAAS,CAAC,aAAa,cAAc;AAGlD,IAAM,gBAAgB;AAGf,IAAM,wBACX;AAGF,SAAS,gBAAgB;AACvB,QAAM,OAAO,QAAQ,IAAI,YAAYE,MAAK,KAAKC,IAAG,QAAQ,GAAG,MAAM;AACnE,SAAOD,MAAK,KAAK,MAAM,QAAQ;AACjC;AAUA,SAAS,sBAAsB;AAC7B,QAAM,aAAa,QAAQ,IAAI,mBAAmBA,MAAK,KAAKC,IAAG,QAAQ,GAAG,SAAS;AACnF,SAAOD,MAAK,KAAK,YAAY,QAAQ;AACvC;AAGA,SAAS,iBAAiB,SAAS;AACjC,QAAM,QAAQ,QAAQ,MAAM,6BAA6B;AACzD,SAAO,UAAU,OAAO,SAAY,MAAM,CAAC;AAC7C;AAGA,SAAS,iBAAiB,SAAS;AACjC,QAAM,QAAQ,iBAAiB,OAAO;AACtC,MAAI,UAAU,OAAW,QAAO,CAAC;AACjC,QAAM,MAAM,CAAC;AACb,aAAW,QAAQ,MAAM,MAAM,OAAO,GAAG;AACvC,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;AAOA,SAAS,kBAAkB,SAAS,KAAK;AACvC,QAAM,QAAQ,iBAAiB,OAAO;AACtC,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,MAAM,MAAM,OAAO,EAAE,KAAK,CAAC,SAAS;AACzC,UAAM,KAAK,KAAK,MAAM,4BAA4B;AAClD,WAAO,OAAO,QAAQ,GAAG,CAAC,MAAM;AAAA,EAClC,CAAC;AACH;AAOA,IAAM,yBAAyB,CAAC,0BAA0B,kBAAkB,eAAe;AAO3F,SAAS,mBAAmB,MAAM,KAAK;AACrC,MAAI,CAAC,OAAO,OAAO,MAAM,GAAG,EAAG,QAAO;AACtC,QAAM,QAAQ,KAAK,GAAG;AACtB,MAAI,OAAO,UAAU,UAAW,QAAO;AACvC,MAAI,UAAU,KAAK,UAAU,IAAK,QAAO;AACzC,MAAI,UAAU,KAAK,UAAU,IAAK,QAAO;AACzC,MAAI,OAAO,UAAU,UAAU;AAC7B,YAAQ,MAAM,YAAY,GAAG;AAAA,MAC3B,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,eAAO;AAAA,IACX;AAAA,EACF;AACA,QAAM,IAAI,UAAU,sBAAsB,GAAG,qBAAqB;AACpE;AAkBA,SAAS,qBAAqB,MAAM,SAAS;AAC3C,aAAW,UAAU,wBAAwB;AAC3C,QAAI,kBAAkB,SAAS,MAAM,EAAG,QAAO;AAAA,EACjD;AACA,MAAI;AAEF,WAAO,mBAAmB,MAAM,gBAAgB,MAAM;AAAA,EACxD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAe,kBAAkB,KAAK,KAAK;AACzC,MAAI;AACJ,MAAI;AACF,cAAU,MAAME,SAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;AAAA,EACtD,QAAQ;AACN;AAAA,EACF;AACA,aAAW,SAAS,SAAS;AAG3B,QAAI,CAAE,MAAM,iBAAiB,KAAK,KAAK,EAAI;AAC3C,UAAM,WAAWF,MAAK,KAAK,KAAK,MAAM,IAAI;AAC1C,QAAI;AACJ,QAAI;AACF,gBAAU,MAAMG,UAASH,MAAK,KAAK,UAAU,UAAU,GAAG,MAAM;AAAA,IAClE,QAAQ;AACN;AAAA,IACF;AACA,UAAM,OAAO,iBAAiB,OAAO;AAOrC,QAAI,CAAC,qBAAqB,MAAM,OAAO,EAAG;AAE1C,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;AAYA,eAAsB,WAAW,KAAK;AACpC,QAAM,MAAM,oBAAI,IAAI;AACpB,QAAM,kBAAkB,KAAK,oBAAoB,CAAC;AAClD,QAAM,kBAAkB,KAAK,cAAc,CAAC;AAC5C,MAAI,OAAO,QAAQ,YAAY,QAAQ,IAAI;AACzC,UAAM,kBAAkB,KAAKA,MAAK,KAAK,KAAK,WAAW,QAAQ,CAAC;AAChE,UAAM,kBAAkB,KAAKA,MAAK,KAAK,KAAK,QAAQ,QAAQ,CAAC;AAAA,EAC/D;AACA,SAAO,CAAC,GAAG,IAAI,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AACtE;AA2BO,SAAS,qBAAqB,QAAQ,KAAK,SAAS;AACzD,QAAM,QAAQ,QAAQ,SAAS,CAAC;AAChC,QAAM,SAAS,QAAQ,UAAU,CAAC;AAElC,QAAM,QAAQ,MAAM,OAAO,CAAC,SAAS,KAAK,KAAK,SAAS,CAAC;AACzD,MAAI,MAAM,SAAS,GAAG;AACpB,OAAG,KAAK,+HAC6C,KAAK,UAAU,KAAK,CAAC;AAAA,EAC5E;AACA,QAAM,UAAU,MAAM,OAAO,CAAC,SAAS,KAAK,QAAQ,SAAS,CAAC;AAE9D,QAAM,QAAQ,QAAQ,IAAI,yBAAyB;AACnD,MAAI,OAAO;AACT,OAAG,IAAI,qDAAqD,KAAK,UAAU,EAAE,OAAO,OAAO,CAAC,CAAC;AAAA,EAC/F,WAAW,QAAQ,SAAS,GAAG;AAE7B,OAAG,IAAI,6CAA6C,KAAK,UAAU,EAAE,OAAO,SAAS,OAAO,CAAC,CAAC;AAAA,EAChG,WAAW,OAAO,SAAS,GAAG;AAC5B,OAAG,KAAK,6CAA6C,KAAK,UAAU,MAAM,CAAC;AAAA,EAC7E;AACF;AAMO,SAAS,MAAM,KAAK;AACzB,MAAI,OAAO,MAAM;AACf,UAAM,UAAU,OAAO,KAAK,QAAQ;AAClC,UAAI;AAEF,cAAM,MAAM,IAAI,QAAQ,SAAY,IAAI,IAAI,IAAI,KAAK,YAAY,EAAE,aAAa,IAAI,KAAK,KAAK,SAAY;AAC1G,cAAM,SAAS,MAAM,WAAW,GAAG;AACnC,YAAI,UAAU,KAAK,EAAE,gBAAgB,kCAAkC,CAAC;AACxE,YAAI,IAAI,KAAK,UAAU,EAAE,IAAI,MAAM,UAAU,MAAM,OAAO,CAAC,CAAC;AAAA,MAC9D,SAAS,OAAO;AACd,YAAI,UAAU,KAAK,EAAE,gBAAgB,kCAAkC,CAAC;AACxE,YAAI,IAAI,KAAK,UAAU,EAAE,IAAI,OAAO,OAAO,OAAO,OAAO,WAAW,KAAK,EAAE,CAAC,CAAC;AAAA,MAC/E;AAAA,IACF;AACA,WAAO,IAAI,UAAU,SAAS,EAAE,MAAM,UAAU,MAAM,qBAAqB,QAAQ,CAAC;AAAA,EACtF,GAAG,0BAA0B;AAE7B,MAAI,OAAO,MAAM,IAAI,aAAa,QAAQ;AAAA,IACxC,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR,CAAC,GAAG,kCAAkC;AAStC,MAAI,OAAO,MAAM;AACf,uBAAmB,EAAE,KAAK,CAAC,WAAW;AACpC,2BAAqB,MAAM;AAAA,IAC7B,CAAC,EAAE,MAAM,CAAC,UAAU;AAClB,cAAQ,KAAK,6CAA6C,KAAK;AAAA,IACjE,CAAC;AACD,WAAO,MAAM;AAAA,IAAC;AAAA,EAChB,GAAG,4CAA4C;AACjD;",
6
- "names": ["readFile", "readdir", "os", "path", "path", "path", "require", "path", "os", "readdir", "readFile"]
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 { mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport { isDirectoryEntry } from './dir-entry.js'\nimport { dshHome, healUiSkillPatches, revertUiSkillPatches } 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 * Shared picker state: the pinned list and the usage history, stored ONCE per\n * DSH home instead of per browser origin.\n *\n * Why it exists: `localStorage` is scoped to an origin, so the desktop app\n * (`dsh-app://app`), the web UI (`http://127.0.0.1:3080`) and a phone all kept\n * their own copy \u2014 pin a skill on the desktop and the web UI never sees it.\n * Both halves now read and write this one file.\n *\n * @returns the absolute path of the shared state file.\n */\nfunction sharedStateFile() {\n return path.join(dshHome(), 'dsh-skill-picker-state.json')\n}\n\n/** Coerce anything into `{ pinned: string[], usage: {name: {count, lastUsed}} }`. */\nfunction normalizeSharedState(value) {\n const source = value !== null && typeof value === 'object' ? value : {}\n const pinned = Array.isArray(source.pinned)\n ? [...new Set(source.pinned.filter((name) => typeof name === 'string' && name !== ''))]\n : []\n const usage = {}\n const rawUsage = source.usage !== null && typeof source.usage === 'object' ? source.usage : {}\n for (const [name, entry] of Object.entries(rawUsage)) {\n if (typeof name !== 'string' || name === '') continue\n const record = entry !== null && typeof entry === 'object' ? entry : {}\n const count = Number.isFinite(record.count) ? Math.max(0, Math.trunc(record.count)) : 0\n const lastUsed = Number.isFinite(record.lastUsed) ? Math.max(0, Math.trunc(record.lastUsed)) : 0\n if (count === 0 && lastUsed === 0) continue\n usage[name] = { count, lastUsed }\n }\n return { pinned, usage }\n}\n\n/** Read the shared state, or null when the file does not exist yet. */\nasync function readSharedState() {\n try {\n return normalizeSharedState(JSON.parse(await readFile(sharedStateFile(), 'utf8')))\n } catch {\n return null\n }\n}\n\n/** Write the shared state atomically (temp + rename), creating the home if needed. */\nasync function writeSharedState(state) {\n const normal = normalizeSharedState(state)\n const file = sharedStateFile()\n await mkdir(path.dirname(file), { recursive: true })\n const tmp = `${file}.tmp`\n await writeFile(tmp, `${JSON.stringify(normal, null, 2)}\\n`, 'utf8')\n await rename(tmp, file)\n return normal\n}\n\n/**\n * First-run migration: union both ends instead of letting whichever browser\n * happened to write first win, so nobody silently loses their pins.\n */\nfunction mergeSharedState(a, b) {\n const left = normalizeSharedState(a)\n const right = normalizeSharedState(b)\n const pinned = [...left.pinned]\n for (const name of right.pinned) if (!pinned.includes(name)) pinned.push(name)\n const usage = { ...left.usage }\n for (const [name, entry] of Object.entries(right.usage)) {\n const previous = usage[name]\n usage[name] = previous === undefined\n ? entry\n : { count: Math.max(previous.count, entry.count), lastUsed: Math.max(previous.lastUsed, entry.lastUsed) }\n }\n return normalizeSharedState({ pinned, usage })\n}\n\n/** Read a JSON request body with a hard cap, so a bad client cannot grow memory. */\nasync function readJsonBody(req) {\n const chunks = []\n let size = 0\n for await (const chunk of req) {\n size += chunk.length\n if (size > 1_000_000) throw new Error('shared state body too large')\n chunks.push(chunk)\n }\n if (chunks.length === 0) return {}\n return JSON.parse(Buffer.concat(chunks).toString('utf8'))\n}\n\n/**\n * `GET /dsh-skill-picker/state` \u2192 `{ ok, state }` (`state: null` before the\n * first write). `PUT` with `{ pinned, usage }` \u2192 the stored state; `?migrate=1`\n * unions the incoming state with the stored one and returns the result.\n */\nasync function handleSharedState(req, res, url) {\n const json = (code, payload) => {\n res.writeHead(code, { 'content-type': 'application/json; charset=utf-8' })\n res.end(JSON.stringify(payload))\n }\n try {\n if (req.method === 'PUT' || req.method === 'POST') {\n const incoming = normalizeSharedState(await readJsonBody(req))\n const migrate = url.searchParams.get('migrate') === '1'\n const stored = migrate ? await readSharedState() : null\n const next = stored === null ? await writeSharedState(incoming) : await writeSharedState(mergeSharedState(stored, incoming))\n json(200, { ok: true, state: next })\n return\n }\n json(200, { ok: true, state: await readSharedState() })\n } catch (error) {\n json(500, { ok: false, error: String(error?.message ?? error) })\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 const url = new URL(req.url ?? '/', 'http://dsh')\n if (url.pathname === '/dsh-skill-picker/state') {\n await handleSharedState(req, res, url)\n return\n }\n // cwd query carries the active session's workspace root from the client.\n const cwd = url.searchParams.get('cwd') ?? 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 // The `/` enhancement no longer edits official files.\n //\n // Since v0.5.15 it takes over the LIVE trigger source at runtime, which does\n // everything the old file patch did (skill group first, fuzzy+pinyin matching,\n // usage tracking) without touching a single file. The patch is therefore\n // redundant \u2014 and a modified copy of an official package is confusing in its\n // own right: it made the desktop and web profiles disagree about one shared\n // file, and it is what made \"is this the original?\" impossible to answer.\n //\n // So the default is now to RESTORE our own patch (from the `.bak` it made).\n // Set DSH_SKILL_PICKER_FILE_PATCH=1 to fall back to the old behaviour on a\n // kernel whose trigger registry has no live sources to take over.\n ctx.effect(() => {\n const enabled = process.env.DSH_SKILL_PICKER_FILE_PATCH === '1'\n const task = enabled\n ? healUiSkillPatches().then((report) => reportUiSkillPatches(report))\n : revertUiSkillPatches().then((report) => {\n if (report.restored.length > 0) {\n console.log('[dsh-skill-picker] ui-skill patch retired; restored original file(s):',\n report.restored.join(', '))\n }\n if (report.errors.length > 0) {\n console.warn('[dsh-skill-picker] ui-skill restore errors:', JSON.stringify(report.errors))\n }\n })\n task.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\n/**\r\n * Undo our own file patch: restore every `<file>.dsh-skill-picker.bak` we made.\r\n *\r\n * The `/` enhancement has not needed this patch since v0.5.15, which takes over\r\n * the LIVE trigger source at runtime instead. Keeping the patch meant shipping a\r\n * modified copy of an official package \u2014 confusing on its own, and it made the\r\n * desktop and web profiles disagree about the same file.\r\n *\r\n * Restores only a file that still carries OUR marker, so a file edited by\r\n * something else since is left alone. Idempotent: after one pass there is\r\n * nothing left to restore.\r\n *\r\n * @returns {Promise<{restored: string[], errors: string[]}>}\r\n */\r\nexport async function revertUiSkillPatches() {\r\n const files = await uiSkillClientPaths()\r\n const restored = []\r\n const errors = []\r\n for (const file of files) {\r\n const backup = `${file}.dsh-skill-picker.bak`\r\n try {\r\n let text\r\n try {\r\n text = await readFile(file, 'utf8')\r\n } catch {\r\n continue\r\n }\r\n if (!PATCHES.some((patch) => patch.isApplied(text))) continue\r\n let original\r\n try {\r\n original = await readFile(backup, 'utf8')\r\n } catch {\r\n continue\r\n }\r\n // Same temp+rename dance as the patch itself: never write a hardlinked\r\n // package file in place (that would mutate the shared pnpm store).\r\n const tmp = `${file}.dsh-skill-picker.tmp`\r\n await writeFile(tmp, original, 'utf8')\r\n await rename(tmp, file)\r\n restored.push(file)\r\n } catch (error) {\r\n errors.push(`${file}: ${String(error?.message ?? error)}`)\r\n }\r\n }\r\n return { restored, errors }\r\n}\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": ";AAoBA,SAAS,OAAO,YAAAA,WAAU,WAAAC,UAAS,UAAAC,SAAQ,aAAAC,kBAAiB;AAC5D,OAAOC,SAAQ;AACf,OAAOC,WAAU;;;ACTjB,SAAS,YAAY;AACrB,OAAO,UAAU;AAejB,eAAsB,iBAAiB,KAAK,OAAO;AACjD,MAAI,MAAM,YAAY,EAAG,QAAO;AAChC,MAAI,CAAC,MAAM,eAAe,EAAG,QAAO;AACpC,MAAI;AACF,YAAQ,MAAM,KAAK,KAAK,KAAK,KAAK,MAAM,IAAI,CAAC,GAAG,YAAY;AAAA,EAC9D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACRA,SAAS,UAAU,WAAW,UAAU,SAAS,QAAQ,UAAU,cAAc;AACjF,SAAS,qBAAqB;AAC9B,OAAO,QAAQ;AACf,OAAOC,WAAU;AAKjB,IAAM,eAAe;AAGrB,IAAM,eAAe;AAGd,IAAM,UAAU;AAAA,EACrB;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,MAAM;AACd,aAAO,qCAAqC,KAAK,IAAI;AAAA,IACvD;AAAA,IACA,MAAM,MAAM;AACV,aAAO,KAAK,QAAQ,gCAAgC,OAAO;AAAA,IAC7D;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,MAAM;AACd,aAAO,KAAK,SAAS,YAAY;AAAA,IACnC;AAAA,IACA,MAAM,MAAM;AAKV,YAAM,UAAU;AAAA,QACd;AAAA,QACA;AAAA,MACF;AACA,iBAAW,MAAM,SAAS;AACxB,cAAM,IAAI,KAAK,MAAM,EAAE;AACvB,YAAI,CAAC,EAAG;AACR,cAAM,CAAC,EAAE,QAAQ,QAAQ,IAAI;AAC7B,eAAO,KAAK;AAAA,UACV;AAAA,UACA,GAAG,MAAM;AAAA,EACJ,MAAM,2BAA2B,QAAQ;AAAA,EACzC,MAAM,iCAAiC,YAAY,4BAA4B,YAAY;AAAA,EAC3F,MAAM;AAAA,QACb;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU,MAAM;AACd,aAAO,KAAK,SAAS,YAAY;AAAA,IACnC;AAAA,IACA,MAAM,MAAM;AACV,aAAO,KAAK;AAAA,QACV;AAAA,QACA,CAAC,OAAO,IAAI,IAAI,OACd,GAAG,EAAE;AAAA,EACF,EAAE;AAAA,EACF,EAAE,kBAAkB,YAAY;AAAA,EAChC,EAAE;AAAA,EACF,EAAE;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;AAGO,SAAS,UAAU;AACxB,SAAO,QAAQ,IAAI,YAAYC,MAAK,KAAK,GAAG,QAAQ,GAAG,MAAM;AAC/D;AAGA,IAAM,mBAAmB,CAAC,gBAAgB,gBAAgB,uBAAuB,OAAO,WAAW;AAGnG,eAAe,WAAW,WAAW;AACnC,MAAI;AACF,UAAM,OAAO,SAAS;AACtB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAkBO,SAAS,sBAAsB;AACpC,QAAM,YAAY,QAAQ;AAC1B,MAAI,OAAO,cAAc,YAAY,cAAc,GAAI,QAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,EAAE;AACzF,SAAO;AAAA,IACL,UAAU,CAACA,MAAK,KAAK,WAAW,OAAO,GAAG,gBAAgB,CAAC;AAAA,IAC3D,QAAQ,CAACA,MAAK,KAAK,WAAW,YAAY,OAAO,GAAG,gBAAgB,CAAC;AAAA,EACvE;AACF;AAWA,eAAe,wBAAwB,YAAY;AACjD,MAAI;AACF,UAAM,WAAW,KAAK,MAAM,MAAM,SAASA,MAAK,KAAK,YAAY,cAAc,GAAG,MAAM,CAAC;AACzF,UAAM,WAAW,EAAE,GAAG,UAAU,cAAc,GAAG,UAAU,gBAAgB;AAC3E,WAAO,OAAO,SAAS,kCAAkC,MAAM;AAAA,EACjE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWA,eAAe,oBAAoB;AACjC,QAAM,EAAE,OAAO,IAAI,oBAAoB;AACvC,MAAI,OAAO,WAAW,KAAK,CAAE,MAAM,WAAW,OAAO,CAAC,CAAC,EAAI,QAAO;AAClE,QAAM,cAAcA,MAAK,KAAK,QAAQ,GAAG,UAAU;AACnD,MAAI,aAAa;AACjB,MAAI;AACF,eAAW,SAAS,MAAM,QAAQ,aAAa,EAAE,eAAe,KAAK,CAAC,GAAG;AACvE,UAAI,MAAM,SAAS,eAAgB;AACnC,UAAI,MAAM,wBAAwBA,MAAK,KAAK,aAAa,MAAM,IAAI,CAAC,GAAG;AACrE,qBAAa;AACb;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,MAAI,WAAY,QAAO;AACvB,SAAO;AAGT;AAwBA,eAAsB,qBAAqB;AACzC,QAAM,cAAcA,MAAK,KAAK,QAAQ,GAAG,UAAU;AACnD,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,QAAQ,aAAa,EAAE,eAAe,KAAK,CAAC;AAAA,EAC/D,QAAQ;AAGN,eAAW,CAAC;AAAA,EACd;AACA,QAAM,OAAO,oBAAI,IAAI;AACrB,QAAM,QAAQ,CAAC;AAGf,QAAM,UAAU,OAAO,cAAc;AACnC,QAAI;AACF,YAAM,OAAO,SAAS;AACtB,YAAM,OAAO,MAAM,SAAS,SAAS;AACrC,UAAI,KAAK,IAAI,IAAI,EAAG;AACpB,WAAK,IAAI,IAAI;AACb,YAAM,KAAK,SAAS;AAAA,IACtB,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,mBAAmB,OAAO,eAAe;AAC7C,QAAI;AACF,YAAMC,WAAU,cAAcD,MAAK,KAAK,YAAY,cAAc,CAAC;AACnE,YAAM,QAAQC,SAAQ,QAAQ,gDAAgD,CAAC;AAAA,IACjF,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,QAAQD,MAAK,KAAK,aAAa,gBAAgB,gBAAgB,uBAAuB,OAAO,WAAW,CAAC;AAI/G,aAAW,aAAa,oBAAoB,EAAE,SAAU,OAAM,QAAQ,SAAS;AAE/E,aAAW,SAAS,UAAU;AAE5B,QAAI,MAAM,SAAS,eAAgB;AAGnC,QAAI,CAAE,MAAM,iBAAiB,aAAa,KAAK,EAAI;AAEnD,UAAM,QAAQA,MAAK,KAAK,aAAa,MAAM,MAAM,SAAS,uBAAuB,OAAO,WAAW,CAAC;AAEpG,UAAM,QAAQA,MAAK,KAAK,aAAa,MAAM,MAAM,gBAAgB,gBAAgB,uBAAuB,OAAO,WAAW,CAAC;AAE3H,UAAM,iBAAiBA,MAAK,KAAK,aAAa,MAAM,IAAI,CAAC;AAAA,EAC3D;AACA,SAAO;AACT;AAUA,eAAsB,iBAAiB,MAAM;AAC3C,QAAM,OAAO,MAAM,SAAS,MAAM,MAAM;AACxC,QAAM,SAAS,EAAE,MAAM,SAAS,CAAC,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,EAAE;AAC1D,MAAI,OAAO;AACX,aAAW,SAAS,SAAS;AAC3B,QAAI,MAAM,UAAU,IAAI,GAAG;AACzB,aAAO,QAAQ,KAAK,MAAM,EAAE;AAC5B;AAAA,IACF;AACA,UAAM,YAAY,MAAM,MAAM,IAAI;AAClC,QAAI,cAAc,MAAM;AACtB,aAAO,KAAK,KAAK,MAAM,EAAE;AACzB;AAAA,IACF;AACA,WAAO;AACP,WAAO,QAAQ,KAAK,MAAM,EAAE;AAAA,EAC9B;AACA,MAAI,OAAO,QAAQ,WAAW,EAAG,QAAO;AACxC,QAAM,SAAS,GAAG,IAAI;AACtB,MAAI;AACF,UAAM,OAAO,MAAM;AAAA,EACrB,QAAQ;AACN,UAAM,SAAS,MAAM,MAAM;AAAA,EAC7B;AAOA,QAAM,MAAM,GAAG,IAAI;AACnB,QAAM,UAAU,KAAK,MAAM,MAAM;AACjC,QAAM,OAAO,KAAK,IAAI;AACtB,SAAO;AACT;AAsBA,eAAsB,uBAAuB;AAC3C,QAAM,QAAQ,MAAM,mBAAmB;AACvC,QAAM,WAAW,CAAC;AAClB,QAAM,SAAS,CAAC;AAChB,aAAW,QAAQ,OAAO;AACxB,UAAM,SAAS,GAAG,IAAI;AACtB,QAAI;AACF,UAAI;AACJ,UAAI;AACF,eAAO,MAAM,SAAS,MAAM,MAAM;AAAA,MACpC,QAAQ;AACN;AAAA,MACF;AACA,UAAI,CAAC,QAAQ,KAAK,CAAC,UAAU,MAAM,UAAU,IAAI,CAAC,EAAG;AACrD,UAAI;AACJ,UAAI;AACF,mBAAW,MAAM,SAAS,QAAQ,MAAM;AAAA,MAC1C,QAAQ;AACN;AAAA,MACF;AAGA,YAAM,MAAM,GAAG,IAAI;AACnB,YAAM,UAAU,KAAK,UAAU,MAAM;AACrC,YAAM,OAAO,KAAK,IAAI;AACtB,eAAS,KAAK,IAAI;AAAA,IACpB,SAAS,OAAO;AACd,aAAO,KAAK,GAAG,IAAI,KAAK,OAAO,OAAO,WAAW,KAAK,CAAC,EAAE;AAAA,IAC3D;AAAA,EACF;AACA,SAAO,EAAE,UAAU,OAAO;AAC5B;AAEA,eAAsB,qBAAqB;AACzC,QAAM,QAAQ,MAAM,mBAAmB;AACvC,QAAM,cAAc,CAAC;AACrB,QAAM,SAAS,CAAC;AAChB,aAAW,QAAQ,OAAO;AACxB,QAAI;AACF,kBAAY,KAAK,MAAM,iBAAiB,IAAI,CAAC;AAAA,IAC/C,SAAS,OAAO;AACd,aAAO,KAAK,GAAG,IAAI,KAAK,OAAO,OAAO,WAAW,KAAK,CAAC,EAAE;AAAA,IAC3D;AAAA,EACF;AAIA,QAAM,OAAO,MAAM,kBAAkB;AACrC,MAAI,MAAM,WAAW,GAAG;AACtB,YAAQ,KAAK,qEACNA,MAAK,KAAK,QAAQ,GAAG,UAAU,CAAC,qDAChC,IAAI,+DAA+D;AAAA,EAC5E,WAAW,SAAS,IAAI;AAItB,YAAQ,KAAK,8CAA8C,MAAM,MAAM,sEAClB,IAAI,EAAE;AAAA,EAC7D;AACA,SAAO,EAAE,OAAO,aAAa,OAAO;AACtC;;;AFtXO,IAAM,SAAS,CAAC,aAAa,cAAc;AAGlD,IAAM,gBAAgB;AAGf,IAAM,wBACX;AAGF,SAAS,gBAAgB;AACvB,QAAM,OAAO,QAAQ,IAAI,YAAYE,MAAK,KAAKC,IAAG,QAAQ,GAAG,MAAM;AACnE,SAAOD,MAAK,KAAK,MAAM,QAAQ;AACjC;AAUA,SAAS,sBAAsB;AAC7B,QAAM,aAAa,QAAQ,IAAI,mBAAmBA,MAAK,KAAKC,IAAG,QAAQ,GAAG,SAAS;AACnF,SAAOD,MAAK,KAAK,YAAY,QAAQ;AACvC;AAGA,SAAS,iBAAiB,SAAS;AACjC,QAAM,QAAQ,QAAQ,MAAM,6BAA6B;AACzD,SAAO,UAAU,OAAO,SAAY,MAAM,CAAC;AAC7C;AAGA,SAAS,iBAAiB,SAAS;AACjC,QAAM,QAAQ,iBAAiB,OAAO;AACtC,MAAI,UAAU,OAAW,QAAO,CAAC;AACjC,QAAM,MAAM,CAAC;AACb,aAAW,QAAQ,MAAM,MAAM,OAAO,GAAG;AACvC,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;AAOA,SAAS,kBAAkB,SAAS,KAAK;AACvC,QAAM,QAAQ,iBAAiB,OAAO;AACtC,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,MAAM,MAAM,OAAO,EAAE,KAAK,CAAC,SAAS;AACzC,UAAM,KAAK,KAAK,MAAM,4BAA4B;AAClD,WAAO,OAAO,QAAQ,GAAG,CAAC,MAAM;AAAA,EAClC,CAAC;AACH;AAOA,IAAM,yBAAyB,CAAC,0BAA0B,kBAAkB,eAAe;AAO3F,SAAS,mBAAmB,MAAM,KAAK;AACrC,MAAI,CAAC,OAAO,OAAO,MAAM,GAAG,EAAG,QAAO;AACtC,QAAM,QAAQ,KAAK,GAAG;AACtB,MAAI,OAAO,UAAU,UAAW,QAAO;AACvC,MAAI,UAAU,KAAK,UAAU,IAAK,QAAO;AACzC,MAAI,UAAU,KAAK,UAAU,IAAK,QAAO;AACzC,MAAI,OAAO,UAAU,UAAU;AAC7B,YAAQ,MAAM,YAAY,GAAG;AAAA,MAC3B,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,eAAO;AAAA,IACX;AAAA,EACF;AACA,QAAM,IAAI,UAAU,sBAAsB,GAAG,qBAAqB;AACpE;AAkBA,SAAS,qBAAqB,MAAM,SAAS;AAC3C,aAAW,UAAU,wBAAwB;AAC3C,QAAI,kBAAkB,SAAS,MAAM,EAAG,QAAO;AAAA,EACjD;AACA,MAAI;AAEF,WAAO,mBAAmB,MAAM,gBAAgB,MAAM;AAAA,EACxD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAe,kBAAkB,KAAK,KAAK;AACzC,MAAI;AACJ,MAAI;AACF,cAAU,MAAME,SAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;AAAA,EACtD,QAAQ;AACN;AAAA,EACF;AACA,aAAW,SAAS,SAAS;AAG3B,QAAI,CAAE,MAAM,iBAAiB,KAAK,KAAK,EAAI;AAC3C,UAAM,WAAWF,MAAK,KAAK,KAAK,MAAM,IAAI;AAC1C,QAAI;AACJ,QAAI;AACF,gBAAU,MAAMG,UAASH,MAAK,KAAK,UAAU,UAAU,GAAG,MAAM;AAAA,IAClE,QAAQ;AACN;AAAA,IACF;AACA,UAAM,OAAO,iBAAiB,OAAO;AAOrC,QAAI,CAAC,qBAAqB,MAAM,OAAO,EAAG;AAE1C,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;AAYA,eAAsB,WAAW,KAAK;AACpC,QAAM,MAAM,oBAAI,IAAI;AACpB,QAAM,kBAAkB,KAAK,oBAAoB,CAAC;AAClD,QAAM,kBAAkB,KAAK,cAAc,CAAC;AAC5C,MAAI,OAAO,QAAQ,YAAY,QAAQ,IAAI;AACzC,UAAM,kBAAkB,KAAKA,MAAK,KAAK,KAAK,WAAW,QAAQ,CAAC;AAChE,UAAM,kBAAkB,KAAKA,MAAK,KAAK,KAAK,QAAQ,QAAQ,CAAC;AAAA,EAC/D;AACA,SAAO,CAAC,GAAG,IAAI,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AACtE;AA2BO,SAAS,qBAAqB,QAAQ,KAAK,SAAS;AACzD,QAAM,QAAQ,QAAQ,SAAS,CAAC;AAChC,QAAM,SAAS,QAAQ,UAAU,CAAC;AAElC,QAAM,QAAQ,MAAM,OAAO,CAAC,SAAS,KAAK,KAAK,SAAS,CAAC;AACzD,MAAI,MAAM,SAAS,GAAG;AACpB,OAAG,KAAK,+HAC6C,KAAK,UAAU,KAAK,CAAC;AAAA,EAC5E;AACA,QAAM,UAAU,MAAM,OAAO,CAAC,SAAS,KAAK,QAAQ,SAAS,CAAC;AAE9D,QAAM,QAAQ,QAAQ,IAAI,yBAAyB;AACnD,MAAI,OAAO;AACT,OAAG,IAAI,qDAAqD,KAAK,UAAU,EAAE,OAAO,OAAO,CAAC,CAAC;AAAA,EAC/F,WAAW,QAAQ,SAAS,GAAG;AAE7B,OAAG,IAAI,6CAA6C,KAAK,UAAU,EAAE,OAAO,SAAS,OAAO,CAAC,CAAC;AAAA,EAChG,WAAW,OAAO,SAAS,GAAG;AAC5B,OAAG,KAAK,6CAA6C,KAAK,UAAU,MAAM,CAAC;AAAA,EAC7E;AACF;AAaA,SAAS,kBAAkB;AACzB,SAAOA,MAAK,KAAK,QAAQ,GAAG,6BAA6B;AAC3D;AAGA,SAAS,qBAAqB,OAAO;AACnC,QAAM,SAAS,UAAU,QAAQ,OAAO,UAAU,WAAW,QAAQ,CAAC;AACtE,QAAM,SAAS,MAAM,QAAQ,OAAO,MAAM,IACtC,CAAC,GAAG,IAAI,IAAI,OAAO,OAAO,OAAO,CAAC,SAAS,OAAO,SAAS,YAAY,SAAS,EAAE,CAAC,CAAC,IACpF,CAAC;AACL,QAAM,QAAQ,CAAC;AACf,QAAM,WAAW,OAAO,UAAU,QAAQ,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ,CAAC;AAC7F,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACpD,QAAI,OAAO,SAAS,YAAY,SAAS,GAAI;AAC7C,UAAM,SAAS,UAAU,QAAQ,OAAO,UAAU,WAAW,QAAQ,CAAC;AACtE,UAAM,QAAQ,OAAO,SAAS,OAAO,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,KAAK,CAAC,IAAI;AACtF,UAAM,WAAW,OAAO,SAAS,OAAO,QAAQ,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,QAAQ,CAAC,IAAI;AAC/F,QAAI,UAAU,KAAK,aAAa,EAAG;AACnC,UAAM,IAAI,IAAI,EAAE,OAAO,SAAS;AAAA,EAClC;AACA,SAAO,EAAE,QAAQ,MAAM;AACzB;AAGA,eAAe,kBAAkB;AAC/B,MAAI;AACF,WAAO,qBAAqB,KAAK,MAAM,MAAMG,UAAS,gBAAgB,GAAG,MAAM,CAAC,CAAC;AAAA,EACnF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAe,iBAAiB,OAAO;AACrC,QAAM,SAAS,qBAAqB,KAAK;AACzC,QAAM,OAAO,gBAAgB;AAC7B,QAAM,MAAMH,MAAK,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AACnD,QAAM,MAAM,GAAG,IAAI;AACnB,QAAMI,WAAU,KAAK,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,GAAM,MAAM;AACnE,QAAMC,QAAO,KAAK,IAAI;AACtB,SAAO;AACT;AAMA,SAAS,iBAAiB,GAAG,GAAG;AAC9B,QAAM,OAAO,qBAAqB,CAAC;AACnC,QAAM,QAAQ,qBAAqB,CAAC;AACpC,QAAM,SAAS,CAAC,GAAG,KAAK,MAAM;AAC9B,aAAW,QAAQ,MAAM,OAAQ,KAAI,CAAC,OAAO,SAAS,IAAI,EAAG,QAAO,KAAK,IAAI;AAC7E,QAAM,QAAQ,EAAE,GAAG,KAAK,MAAM;AAC9B,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,MAAM,KAAK,GAAG;AACvD,UAAM,WAAW,MAAM,IAAI;AAC3B,UAAM,IAAI,IAAI,aAAa,SACvB,QACA,EAAE,OAAO,KAAK,IAAI,SAAS,OAAO,MAAM,KAAK,GAAG,UAAU,KAAK,IAAI,SAAS,UAAU,MAAM,QAAQ,EAAE;AAAA,EAC5G;AACA,SAAO,qBAAqB,EAAE,QAAQ,MAAM,CAAC;AAC/C;AAGA,eAAe,aAAa,KAAK;AAC/B,QAAM,SAAS,CAAC;AAChB,MAAI,OAAO;AACX,mBAAiB,SAAS,KAAK;AAC7B,YAAQ,MAAM;AACd,QAAI,OAAO,IAAW,OAAM,IAAI,MAAM,6BAA6B;AACnE,WAAO,KAAK,KAAK;AAAA,EACnB;AACA,MAAI,OAAO,WAAW,EAAG,QAAO,CAAC;AACjC,SAAO,KAAK,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM,CAAC;AAC1D;AAOA,eAAe,kBAAkB,KAAK,KAAK,KAAK;AAC9C,QAAM,OAAO,CAAC,MAAM,YAAY;AAC9B,QAAI,UAAU,MAAM,EAAE,gBAAgB,kCAAkC,CAAC;AACzE,QAAI,IAAI,KAAK,UAAU,OAAO,CAAC;AAAA,EACjC;AACA,MAAI;AACF,QAAI,IAAI,WAAW,SAAS,IAAI,WAAW,QAAQ;AACjD,YAAM,WAAW,qBAAqB,MAAM,aAAa,GAAG,CAAC;AAC7D,YAAM,UAAU,IAAI,aAAa,IAAI,SAAS,MAAM;AACpD,YAAM,SAAS,UAAU,MAAM,gBAAgB,IAAI;AACnD,YAAM,OAAO,WAAW,OAAO,MAAM,iBAAiB,QAAQ,IAAI,MAAM,iBAAiB,iBAAiB,QAAQ,QAAQ,CAAC;AAC3H,WAAK,KAAK,EAAE,IAAI,MAAM,OAAO,KAAK,CAAC;AACnC;AAAA,IACF;AACA,SAAK,KAAK,EAAE,IAAI,MAAM,OAAO,MAAM,gBAAgB,EAAE,CAAC;AAAA,EACxD,SAAS,OAAO;AACd,SAAK,KAAK,EAAE,IAAI,OAAO,OAAO,OAAO,OAAO,WAAW,KAAK,EAAE,CAAC;AAAA,EACjE;AACF;AAMO,SAAS,MAAM,KAAK;AACzB,MAAI,OAAO,MAAM;AACf,UAAM,UAAU,OAAO,KAAK,QAAQ;AAClC,UAAI;AACF,cAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,YAAY;AAChD,YAAI,IAAI,aAAa,2BAA2B;AAC9C,gBAAM,kBAAkB,KAAK,KAAK,GAAG;AACrC;AAAA,QACF;AAEA,cAAM,MAAM,IAAI,aAAa,IAAI,KAAK,KAAK;AAC3C,cAAM,SAAS,MAAM,WAAW,GAAG;AACnC,YAAI,UAAU,KAAK,EAAE,gBAAgB,kCAAkC,CAAC;AACxE,YAAI,IAAI,KAAK,UAAU,EAAE,IAAI,MAAM,UAAU,MAAM,OAAO,CAAC,CAAC;AAAA,MAC9D,SAAS,OAAO;AACd,YAAI,UAAU,KAAK,EAAE,gBAAgB,kCAAkC,CAAC;AACxE,YAAI,IAAI,KAAK,UAAU,EAAE,IAAI,OAAO,OAAO,OAAO,OAAO,WAAW,KAAK,EAAE,CAAC,CAAC;AAAA,MAC/E;AAAA,IACF;AACA,WAAO,IAAI,UAAU,SAAS,EAAE,MAAM,UAAU,MAAM,qBAAqB,QAAQ,CAAC;AAAA,EACtF,GAAG,0BAA0B;AAE7B,MAAI,OAAO,MAAM,IAAI,aAAa,QAAQ;AAAA,IACxC,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR,CAAC,GAAG,kCAAkC;AActC,MAAI,OAAO,MAAM;AACf,UAAM,UAAU,QAAQ,IAAI,gCAAgC;AAC5D,UAAM,OAAO,UACT,mBAAmB,EAAE,KAAK,CAAC,WAAW,qBAAqB,MAAM,CAAC,IAClE,qBAAqB,EAAE,KAAK,CAAC,WAAW;AACxC,UAAI,OAAO,SAAS,SAAS,GAAG;AAC9B,gBAAQ;AAAA,UAAI;AAAA,UACV,OAAO,SAAS,KAAK,IAAI;AAAA,QAAC;AAAA,MAC9B;AACA,UAAI,OAAO,OAAO,SAAS,GAAG;AAC5B,gBAAQ,KAAK,+CAA+C,KAAK,UAAU,OAAO,MAAM,CAAC;AAAA,MAC3F;AAAA,IACF,CAAC;AACH,SAAK,MAAM,CAAC,UAAU;AACpB,cAAQ,KAAK,6CAA6C,KAAK;AAAA,IACjE,CAAC;AACD,WAAO,MAAM;AAAA,IAAC;AAAA,EAChB,GAAG,4CAA4C;AACjD;",
6
+ "names": ["readFile", "readdir", "rename", "writeFile", "os", "path", "path", "path", "require", "path", "os", "readdir", "readFile", "writeFile", "rename"]
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-skill-picker",
3
3
  "description": "DSH Web GUI skill picker: a button beside the composer opens a searchable list of installed skills; picking one inserts the official `/skill-name` gesture into the input box, so the skill loads with your message (WorkBuddy-style skill invocation for DeepSeek Harness).",
4
- "version": "0.5.20",
4
+ "version": "0.5.22",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -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 { SHARED_STATE_EVENT, pushSharedState, syncSharedState } from './shared-state.js'
23
24
  import { installSlashFuzzy, slashEnhancementMode, warmSlashSkill } from './slash-source.js'
24
25
 
25
26
  /** Required services: slot registry, host connection (official skills API), sessions (workspace cwd fallback), input triggers (/ fuzzy source). */
@@ -43,13 +44,14 @@ function loadPinned() {
43
44
  }
44
45
  }
45
46
 
46
- /** Persist the pinned list; never throws. */
47
+ /** Persist the pinned list; never throws. Also pushes the shared copy. */
47
48
  function savePinned(pinned) {
48
49
  try {
49
50
  localStorage.setItem(PINNED_KEY, JSON.stringify(pinned))
50
51
  } catch {
51
52
  /* storage unavailable — pinning just won't persist */
52
53
  }
54
+ void pushSharedState()
53
55
  }
54
56
 
55
57
  /** Read the usage history {name: {count, lastUsed}}; never throws. */
@@ -64,13 +66,14 @@ function loadUsage() {
64
66
  }
65
67
  }
66
68
 
67
- /** Persist the usage history; never throws. */
69
+ /** Persist the usage history; never throws. Also pushes the shared copy. */
68
70
  function saveUsage(usage) {
69
71
  try {
70
72
  localStorage.setItem(USAGE_KEY, JSON.stringify(usage))
71
73
  } catch {
72
74
  /* storage unavailable — ordering just won't persist */
73
75
  }
76
+ void pushSharedState()
74
77
  }
75
78
 
76
79
  /**
@@ -380,6 +383,20 @@ function SkillPickerButton(props) {
380
383
  return () => window.removeEventListener('dsh-skill-picker:usage-updated', onUsageUpdated)
381
384
  }, [])
382
385
 
386
+ // Pinned list and usage history live in one shared file on the host, so the
387
+ // desktop app, the web UI and a phone all show the same ordering. The mirror
388
+ // in localStorage is what the synchronous ranking reads; pull once on mount
389
+ // and again whenever a slash pick or another client changes the shared copy.
390
+ useEffect(() => {
391
+ const onSharedState = () => {
392
+ setUsage(loadUsage())
393
+ setPinned(loadPinned())
394
+ }
395
+ window.addEventListener(SHARED_STATE_EVENT, onSharedState)
396
+ void syncSharedState()
397
+ return () => window.removeEventListener(SHARED_STATE_EVENT, onSharedState)
398
+ }, [])
399
+
383
400
  // Prime the official `/` catalogue for this Session as soon as it is known.
384
401
  //
385
402
  // The slash menu highlights the FIRST group that settles and scrolls it into
@@ -444,6 +461,10 @@ function SkillPickerButton(props) {
444
461
 
445
462
  const toggle = () => {
446
463
  if (!open) {
464
+ // Pull first: a pin or a pick made on another client (desktop ↔ web ↔
465
+ // phone) should be visible the moment this panel opens. The pull fires
466
+ // SHARED_STATE_EVENT, which is what refreshes `usage`/`pinned` here.
467
+ void syncSharedState()
447
468
  setUsage(loadUsage())
448
469
  void load()
449
470
  }
@@ -687,6 +708,12 @@ function SkillPickerButton(props) {
687
708
 
688
709
  /** Apply the browser half: register the picker into the composer tool row. */
689
710
  export function apply(ctx) {
711
+ // Pull the shared pinned/usage state as early as possible: the `/` menu ranks
712
+ // during render from the localStorage mirror, so the mirror has to be filled
713
+ // before the user can type a slash. Best-effort — on failure the picker keeps
714
+ // working from whatever this browser already had.
715
+ void syncSharedState()
716
+
690
717
  // Primary skill source: the official host skills API. In DSH 0.1.2-alpha.x
691
718
  // the RPC moved from `connection.api.skills` (rc.x) to `remote.skills`
692
719
  // (used by the official ui-skill plugin); try both before falling back.