dsh-skill-picker 0.5.22 → 0.5.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/lib/client.js +312 -2
- package/lib/client.js.map +2 -2
- package/lib/index.js +244 -18
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/client/index.jsx +382 -2
- package/src/index.js +129 -2
- package/src/skill-ops.js +233 -0
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.js
|
|
2
|
-
import { mkdir, readFile as
|
|
2
|
+
import { mkdir as mkdir2, readFile as readFile3, readdir as readdir3, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
|
|
3
3
|
import os2 from "node:os";
|
|
4
|
-
import
|
|
4
|
+
import path4 from "node:path";
|
|
5
5
|
|
|
6
6
|
// src/dir-entry.js
|
|
7
7
|
import { stat } from "node:fs/promises";
|
|
@@ -242,17 +242,160 @@ async function healUiSkillPatches() {
|
|
|
242
242
|
return { files: filesReport, errors };
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
// src/skill-ops.js
|
|
246
|
+
import { spawn } from "node:child_process";
|
|
247
|
+
import { mkdir, readFile as readFile2, readdir as readdir2, rename as rename2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
248
|
+
import path3 from "node:path";
|
|
249
|
+
var ENTRY = "SKILL.md";
|
|
250
|
+
var DISABLED_ENTRY = `${ENTRY}.disabled`;
|
|
251
|
+
var CLIENT_HEADER = "x-dsh-skill-picker";
|
|
252
|
+
function skillBackupDir() {
|
|
253
|
+
return path3.join(dshHome(), "skill-backups");
|
|
254
|
+
}
|
|
255
|
+
function resolveSkillPath(candidate, roots) {
|
|
256
|
+
if (typeof candidate !== "string" || candidate === "") return void 0;
|
|
257
|
+
const list = Array.isArray(roots) ? roots : [];
|
|
258
|
+
const resolved = path3.resolve(candidate);
|
|
259
|
+
for (const root of list) {
|
|
260
|
+
const base = path3.resolve(root);
|
|
261
|
+
const relative = path3.relative(base, resolved);
|
|
262
|
+
if (relative === "" || relative.startsWith("..") || path3.isAbsolute(relative)) continue;
|
|
263
|
+
if (relative.split(path3.sep).filter(Boolean).length !== 1) continue;
|
|
264
|
+
return resolved;
|
|
265
|
+
}
|
|
266
|
+
return void 0;
|
|
267
|
+
}
|
|
268
|
+
async function pathKind(target) {
|
|
269
|
+
try {
|
|
270
|
+
return (await stat2(target)).isDirectory() ? "dir" : "file";
|
|
271
|
+
} catch {
|
|
272
|
+
return void 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function exists(candidate) {
|
|
276
|
+
try {
|
|
277
|
+
await stat2(candidate);
|
|
278
|
+
return true;
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function setSkillDisabled(target, disabled) {
|
|
284
|
+
const kind = await pathKind(target);
|
|
285
|
+
if (kind === void 0) throw new Error(`skill not found: ${target}`);
|
|
286
|
+
const dir = kind === "dir" ? target : path3.dirname(target);
|
|
287
|
+
const base = kind === "dir" ? ENTRY : path3.basename(target);
|
|
288
|
+
const plain = path3.join(dir, base);
|
|
289
|
+
const off = path3.join(dir, `${base}.disabled`);
|
|
290
|
+
const hasPlain = await exists(plain);
|
|
291
|
+
const hasOff = await exists(off);
|
|
292
|
+
if (disabled) {
|
|
293
|
+
if (hasOff && !hasPlain) throw new Error("already disabled");
|
|
294
|
+
if (!hasPlain) throw new Error(`cannot disable: ${base} is not there`);
|
|
295
|
+
if (hasOff) throw new Error(`cannot disable: ${base} and ${base}.disabled both exist`);
|
|
296
|
+
await rename2(plain, off);
|
|
297
|
+
return { entry: `${base}.disabled`, disabled: true };
|
|
298
|
+
}
|
|
299
|
+
if (hasPlain && !hasOff) throw new Error("already enabled");
|
|
300
|
+
if (!hasOff) throw new Error(`cannot enable: ${base}.disabled is not there`);
|
|
301
|
+
if (hasPlain) throw new Error(`cannot enable: ${base} and ${base}.disabled both exist`);
|
|
302
|
+
await rename2(off, plain);
|
|
303
|
+
return { entry: base, disabled: false };
|
|
304
|
+
}
|
|
305
|
+
async function uninstallSkill(target) {
|
|
306
|
+
const kind = await pathKind(target);
|
|
307
|
+
if (kind === void 0) throw new Error(`skill not found: ${target}`);
|
|
308
|
+
const name = path3.basename(target);
|
|
309
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
310
|
+
const backupPath = path3.join(skillBackupDir(), `${stamp}-${name}`);
|
|
311
|
+
await mkdir(backupPath, { recursive: true });
|
|
312
|
+
await rename2(target, path3.join(backupPath, name));
|
|
313
|
+
await writeFile2(path3.join(backupPath, "manifest.json"), `${JSON.stringify({
|
|
314
|
+
name,
|
|
315
|
+
kind,
|
|
316
|
+
originalPath: target,
|
|
317
|
+
movedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
318
|
+
}, null, 2)}
|
|
319
|
+
`, "utf8");
|
|
320
|
+
return { backupPath, restoreTarget: target };
|
|
321
|
+
}
|
|
322
|
+
async function listSkillBackups(roots) {
|
|
323
|
+
let entries;
|
|
324
|
+
try {
|
|
325
|
+
entries = await readdir2(skillBackupDir(), { withFileTypes: true });
|
|
326
|
+
} catch {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
const backups = [];
|
|
330
|
+
for (const entry of entries) {
|
|
331
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
332
|
+
const dir = path3.join(skillBackupDir(), entry.name);
|
|
333
|
+
try {
|
|
334
|
+
const manifest = JSON.parse(await readFile2(path3.join(dir, "manifest.json"), "utf8"));
|
|
335
|
+
backups.push({
|
|
336
|
+
id: entry.name,
|
|
337
|
+
name: typeof manifest.name === "string" ? manifest.name : entry.name,
|
|
338
|
+
originalPath: typeof manifest.originalPath === "string" ? manifest.originalPath : void 0,
|
|
339
|
+
movedAt: typeof manifest.movedAt === "string" ? manifest.movedAt : void 0,
|
|
340
|
+
restorable: resolveSkillPath(manifest.originalPath, roots) !== void 0
|
|
341
|
+
});
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return backups.sort((a, b) => String(b.movedAt ?? "").localeCompare(String(a.movedAt ?? "")));
|
|
346
|
+
}
|
|
347
|
+
async function restoreSkillBackup(id, roots) {
|
|
348
|
+
if (typeof id !== "string" || id === "" || id.includes("/") || id.includes("\\") || id.includes("..")) {
|
|
349
|
+
throw new Error("invalid backup id");
|
|
350
|
+
}
|
|
351
|
+
const dir = path3.join(skillBackupDir(), id);
|
|
352
|
+
let manifest;
|
|
353
|
+
try {
|
|
354
|
+
manifest = JSON.parse(await readFile2(path3.join(dir, "manifest.json"), "utf8"));
|
|
355
|
+
} catch {
|
|
356
|
+
throw new Error("that backup is gone");
|
|
357
|
+
}
|
|
358
|
+
const original = resolveSkillPath(manifest.originalPath, roots);
|
|
359
|
+
if (original === void 0) throw new Error("that backup no longer points inside a skill root");
|
|
360
|
+
if (await exists(original)) {
|
|
361
|
+
throw new Error(`${path3.basename(original)} is occupied again \u2014 move it away first`);
|
|
362
|
+
}
|
|
363
|
+
await rename2(path3.join(dir, path3.basename(original)), original);
|
|
364
|
+
const done = path3.join(skillBackupDir(), ".restored");
|
|
365
|
+
await mkdir(done, { recursive: true });
|
|
366
|
+
await rename2(dir, path3.join(done, id)).catch(() => {
|
|
367
|
+
});
|
|
368
|
+
return { restored: original };
|
|
369
|
+
}
|
|
370
|
+
function revealSkill(target, revealPath) {
|
|
371
|
+
const show = typeof revealPath === "string" && revealPath !== "" ? revealPath : target;
|
|
372
|
+
try {
|
|
373
|
+
if (process.platform === "win32") {
|
|
374
|
+
spawn("explorer.exe", [`/select,${show}`], { detached: true, stdio: "ignore" }).unref();
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
if (process.platform === "darwin") {
|
|
378
|
+
spawn("open", ["-R", show], { detached: true, stdio: "ignore" }).unref();
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
spawn("xdg-open", [path3.dirname(show)], { detached: true, stdio: "ignore" }).unref();
|
|
382
|
+
return true;
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
245
388
|
// src/index.js
|
|
246
389
|
var inject = ["webServer", "systemPrompt"];
|
|
247
390
|
var SECTION_ORDER = 215;
|
|
248
391
|
var SKILL_PICKER_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-skill-picker \u63D2\u4EF6\uFF08Web GUI \u7684\u6280\u80FD\u9009\u62E9\u5668\uFF09\uFF1A\u8F93\u5165\u6846\u65C1\u6709\u6280\u80FD\u6309\u94AE\uFF0C\u7528\u6237\u70B9\u9009\u6280\u80FD\u540E\u4F1A\u628A `/\u6280\u80FD\u540D`\uFF08\u5982 /duo-xuan-pi-gai\uFF09\u63D2\u5165\u53D1\u9001\u6846\u5E76\u968F\u6D88\u606F\u53D1\u51FA\u3002DSH \u5B98\u65B9\u673A\u5236\u4F1A\u628A\u7528\u6237\u6D88\u606F\u91CC\u7684 `/\u6280\u80FD\u540D` \u624B\u52BF\u5F53\u4F5C\u6280\u80FD\u76F4\u63A5\u8C03\u7528\u5E76\u81EA\u52A8\u52A0\u8F7D\u6280\u80FD\u5185\u5BB9\u2014\u2014\u4F60\u7167\u5E38\u6309\u52A0\u8F7D\u540E\u7684\u6280\u80FD\u6307\u4EE4\u6267\u884C\u5373\u53EF\uFF0C\u65E0\u9700\u989D\u5916\u64CD\u4F5C\u3002\u7528\u6237\u8BF4\u300C\u6280\u80FD\u9009\u62E9\u5668 / \u9009\u4E2A\u6280\u80FD / \u6280\u80FD\u5217\u8868\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\u3002";
|
|
249
392
|
function userSkillsDir() {
|
|
250
|
-
const home = process.env.DSH_HOME ??
|
|
251
|
-
return
|
|
393
|
+
const home = process.env.DSH_HOME ?? path4.join(os2.homedir(), ".dsh");
|
|
394
|
+
return path4.join(home, "skills");
|
|
252
395
|
}
|
|
253
396
|
function userAgentsSkillsDir() {
|
|
254
|
-
const agentsHome = process.env.DSH_AGENTS_HOME ??
|
|
255
|
-
return
|
|
397
|
+
const agentsHome = process.env.DSH_AGENTS_HOME ?? path4.join(os2.homedir(), ".agents");
|
|
398
|
+
return path4.join(agentsHome, "skills");
|
|
256
399
|
}
|
|
257
400
|
function frontmatterBlock(content) {
|
|
258
401
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -312,25 +455,35 @@ function isUserInvocableSkill(meta, content) {
|
|
|
312
455
|
async function scanSkillsDirInto(map, dir) {
|
|
313
456
|
let entries;
|
|
314
457
|
try {
|
|
315
|
-
entries = await
|
|
458
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
316
459
|
} catch {
|
|
317
460
|
return;
|
|
318
461
|
}
|
|
319
462
|
for (const entry of entries) {
|
|
320
463
|
if (!await isDirectoryEntry(dir, entry)) continue;
|
|
321
|
-
const skillDir =
|
|
464
|
+
const skillDir = path4.join(dir, entry.name);
|
|
322
465
|
let content;
|
|
466
|
+
let entryFile = ENTRY;
|
|
467
|
+
let disabled = false;
|
|
323
468
|
try {
|
|
324
|
-
content = await
|
|
469
|
+
content = await readFile3(path4.join(skillDir, ENTRY), "utf8");
|
|
325
470
|
} catch {
|
|
326
|
-
|
|
471
|
+
try {
|
|
472
|
+
content = await readFile3(path4.join(skillDir, DISABLED_ENTRY), "utf8");
|
|
473
|
+
entryFile = DISABLED_ENTRY;
|
|
474
|
+
disabled = true;
|
|
475
|
+
} catch {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
327
478
|
}
|
|
328
479
|
const meta = parseFrontmatter(content);
|
|
329
480
|
if (!isUserInvocableSkill(meta, content)) continue;
|
|
330
481
|
map.set(meta.name ?? entry.name, {
|
|
331
482
|
name: meta.name ?? entry.name,
|
|
332
483
|
description: meta.description ?? "",
|
|
333
|
-
path: skillDir
|
|
484
|
+
path: skillDir,
|
|
485
|
+
entry: entryFile,
|
|
486
|
+
disabled
|
|
334
487
|
});
|
|
335
488
|
}
|
|
336
489
|
}
|
|
@@ -339,8 +492,8 @@ async function scanSkills(cwd) {
|
|
|
339
492
|
await scanSkillsDirInto(map, userAgentsSkillsDir());
|
|
340
493
|
await scanSkillsDirInto(map, userSkillsDir());
|
|
341
494
|
if (typeof cwd === "string" && cwd !== "") {
|
|
342
|
-
await scanSkillsDirInto(map,
|
|
343
|
-
await scanSkillsDirInto(map,
|
|
495
|
+
await scanSkillsDirInto(map, path4.join(cwd, ".agents", "skills"));
|
|
496
|
+
await scanSkillsDirInto(map, path4.join(cwd, ".dsh", "skills"));
|
|
344
497
|
}
|
|
345
498
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
346
499
|
}
|
|
@@ -362,7 +515,7 @@ function reportUiSkillPatches(report, io = console) {
|
|
|
362
515
|
}
|
|
363
516
|
}
|
|
364
517
|
function sharedStateFile() {
|
|
365
|
-
return
|
|
518
|
+
return path4.join(dshHome(), "dsh-skill-picker-state.json");
|
|
366
519
|
}
|
|
367
520
|
function normalizeSharedState(value) {
|
|
368
521
|
const source = value !== null && typeof value === "object" ? value : {};
|
|
@@ -381,7 +534,7 @@ function normalizeSharedState(value) {
|
|
|
381
534
|
}
|
|
382
535
|
async function readSharedState() {
|
|
383
536
|
try {
|
|
384
|
-
return normalizeSharedState(JSON.parse(await
|
|
537
|
+
return normalizeSharedState(JSON.parse(await readFile3(sharedStateFile(), "utf8")));
|
|
385
538
|
} catch {
|
|
386
539
|
return null;
|
|
387
540
|
}
|
|
@@ -389,11 +542,11 @@ async function readSharedState() {
|
|
|
389
542
|
async function writeSharedState(state) {
|
|
390
543
|
const normal = normalizeSharedState(state);
|
|
391
544
|
const file = sharedStateFile();
|
|
392
|
-
await
|
|
545
|
+
await mkdir2(path4.dirname(file), { recursive: true });
|
|
393
546
|
const tmp = `${file}.tmp`;
|
|
394
|
-
await
|
|
547
|
+
await writeFile3(tmp, `${JSON.stringify(normal, null, 2)}
|
|
395
548
|
`, "utf8");
|
|
396
|
-
await
|
|
549
|
+
await rename3(tmp, file);
|
|
397
550
|
return normal;
|
|
398
551
|
}
|
|
399
552
|
function mergeSharedState(a, b) {
|
|
@@ -438,6 +591,75 @@ async function handleSharedState(req, res, url) {
|
|
|
438
591
|
json(500, { ok: false, error: String(error?.message ?? error) });
|
|
439
592
|
}
|
|
440
593
|
}
|
|
594
|
+
function allSkillRoots(cwd) {
|
|
595
|
+
const roots = [userAgentsSkillsDir(), userSkillsDir()];
|
|
596
|
+
if (typeof cwd === "string" && cwd !== "") {
|
|
597
|
+
roots.push(path4.join(cwd, ".agents", "skills"), path4.join(cwd, ".dsh", "skills"));
|
|
598
|
+
}
|
|
599
|
+
return roots;
|
|
600
|
+
}
|
|
601
|
+
async function handleSkillOp(req, res, url) {
|
|
602
|
+
const json = (code, payload) => {
|
|
603
|
+
res.writeHead(code, { "content-type": "application/json; charset=utf-8" });
|
|
604
|
+
res.end(JSON.stringify(payload));
|
|
605
|
+
};
|
|
606
|
+
if (req.method !== "POST") {
|
|
607
|
+
json(405, { ok: false, error: "POST only" });
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (req.headers[CLIENT_HEADER] !== "1") {
|
|
611
|
+
json(403, { ok: false, error: "missing client header" });
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const body = await readJsonBody(req);
|
|
616
|
+
const cwd = url.searchParams.get("cwd") ?? void 0;
|
|
617
|
+
const roots = allSkillRoots(cwd);
|
|
618
|
+
const action = String(body?.action ?? "");
|
|
619
|
+
if (action === "backups") {
|
|
620
|
+
json(200, { ok: true, backups: await listSkillBackups(roots) });
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (action === "restore") {
|
|
624
|
+
json(200, { ok: true, ...await restoreSkillBackup(body?.id, roots) });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const name = String(body?.name ?? "");
|
|
628
|
+
if (name === "") {
|
|
629
|
+
json(400, { ok: false, error: "name is required" });
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const known = (await scanSkills(cwd)).find((skill) => skill.name === name);
|
|
633
|
+
if (known === void 0) {
|
|
634
|
+
json(404, { ok: false, error: `no skill named "${name}"` });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const target = resolveSkillPath(known.path, roots);
|
|
638
|
+
if (target === void 0) {
|
|
639
|
+
json(403, { ok: false, error: "that skill lives outside the skill roots" });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (action === "disable") {
|
|
643
|
+
json(200, { ok: true, ...await setSkillDisabled(target, true) });
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (action === "enable") {
|
|
647
|
+
json(200, { ok: true, ...await setSkillDisabled(target, false) });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (action === "reveal") {
|
|
651
|
+
json(200, { ok: true, revealed: revealSkill(target) });
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (action === "uninstall") {
|
|
655
|
+
json(200, { ok: true, ...await uninstallSkill(target) });
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
json(400, { ok: false, error: `unknown action: ${action}` });
|
|
659
|
+
} catch (error) {
|
|
660
|
+
json(500, { ok: false, error: String(error?.message ?? error) });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
441
663
|
function apply(ctx) {
|
|
442
664
|
ctx.effect(() => {
|
|
443
665
|
const handler = async (req, res) => {
|
|
@@ -447,6 +669,10 @@ function apply(ctx) {
|
|
|
447
669
|
await handleSharedState(req, res, url);
|
|
448
670
|
return;
|
|
449
671
|
}
|
|
672
|
+
if (url.pathname === "/dsh-skill-picker/skill") {
|
|
673
|
+
await handleSkillOp(req, res, url);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
450
676
|
const cwd = url.searchParams.get("cwd") ?? void 0;
|
|
451
677
|
const skills = await scanSkills(cwd);
|
|
452
678
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|