dsh-skill-picker 0.5.21 → 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 +3 -1
- package/lib/client.js +437 -8
- package/lib/client.js.map +4 -4
- package/lib/index.js +322 -14
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/client/index.jsx +411 -4
- package/src/client/shared-state.js +189 -0
- package/src/index.js +248 -5
- package/src/skill-ops.js +233 -0
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.js
|
|
2
|
-
import { 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
|
}
|
|
@@ -361,11 +514,166 @@ function reportUiSkillPatches(report, io = console) {
|
|
|
361
514
|
io.warn("[dsh-skill-picker] ui-skill patch errors:", JSON.stringify(errors));
|
|
362
515
|
}
|
|
363
516
|
}
|
|
517
|
+
function sharedStateFile() {
|
|
518
|
+
return path4.join(dshHome(), "dsh-skill-picker-state.json");
|
|
519
|
+
}
|
|
520
|
+
function normalizeSharedState(value) {
|
|
521
|
+
const source = value !== null && typeof value === "object" ? value : {};
|
|
522
|
+
const pinned = Array.isArray(source.pinned) ? [...new Set(source.pinned.filter((name) => typeof name === "string" && name !== ""))] : [];
|
|
523
|
+
const usage = {};
|
|
524
|
+
const rawUsage = source.usage !== null && typeof source.usage === "object" ? source.usage : {};
|
|
525
|
+
for (const [name, entry] of Object.entries(rawUsage)) {
|
|
526
|
+
if (typeof name !== "string" || name === "") continue;
|
|
527
|
+
const record = entry !== null && typeof entry === "object" ? entry : {};
|
|
528
|
+
const count = Number.isFinite(record.count) ? Math.max(0, Math.trunc(record.count)) : 0;
|
|
529
|
+
const lastUsed = Number.isFinite(record.lastUsed) ? Math.max(0, Math.trunc(record.lastUsed)) : 0;
|
|
530
|
+
if (count === 0 && lastUsed === 0) continue;
|
|
531
|
+
usage[name] = { count, lastUsed };
|
|
532
|
+
}
|
|
533
|
+
return { pinned, usage };
|
|
534
|
+
}
|
|
535
|
+
async function readSharedState() {
|
|
536
|
+
try {
|
|
537
|
+
return normalizeSharedState(JSON.parse(await readFile3(sharedStateFile(), "utf8")));
|
|
538
|
+
} catch {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async function writeSharedState(state) {
|
|
543
|
+
const normal = normalizeSharedState(state);
|
|
544
|
+
const file = sharedStateFile();
|
|
545
|
+
await mkdir2(path4.dirname(file), { recursive: true });
|
|
546
|
+
const tmp = `${file}.tmp`;
|
|
547
|
+
await writeFile3(tmp, `${JSON.stringify(normal, null, 2)}
|
|
548
|
+
`, "utf8");
|
|
549
|
+
await rename3(tmp, file);
|
|
550
|
+
return normal;
|
|
551
|
+
}
|
|
552
|
+
function mergeSharedState(a, b) {
|
|
553
|
+
const left = normalizeSharedState(a);
|
|
554
|
+
const right = normalizeSharedState(b);
|
|
555
|
+
const pinned = [...left.pinned];
|
|
556
|
+
for (const name of right.pinned) if (!pinned.includes(name)) pinned.push(name);
|
|
557
|
+
const usage = { ...left.usage };
|
|
558
|
+
for (const [name, entry] of Object.entries(right.usage)) {
|
|
559
|
+
const previous = usage[name];
|
|
560
|
+
usage[name] = previous === void 0 ? entry : { count: Math.max(previous.count, entry.count), lastUsed: Math.max(previous.lastUsed, entry.lastUsed) };
|
|
561
|
+
}
|
|
562
|
+
return normalizeSharedState({ pinned, usage });
|
|
563
|
+
}
|
|
564
|
+
async function readJsonBody(req) {
|
|
565
|
+
const chunks = [];
|
|
566
|
+
let size = 0;
|
|
567
|
+
for await (const chunk of req) {
|
|
568
|
+
size += chunk.length;
|
|
569
|
+
if (size > 1e6) throw new Error("shared state body too large");
|
|
570
|
+
chunks.push(chunk);
|
|
571
|
+
}
|
|
572
|
+
if (chunks.length === 0) return {};
|
|
573
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
574
|
+
}
|
|
575
|
+
async function handleSharedState(req, res, url) {
|
|
576
|
+
const json = (code, payload) => {
|
|
577
|
+
res.writeHead(code, { "content-type": "application/json; charset=utf-8" });
|
|
578
|
+
res.end(JSON.stringify(payload));
|
|
579
|
+
};
|
|
580
|
+
try {
|
|
581
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
582
|
+
const incoming = normalizeSharedState(await readJsonBody(req));
|
|
583
|
+
const migrate = url.searchParams.get("migrate") === "1";
|
|
584
|
+
const stored = migrate ? await readSharedState() : null;
|
|
585
|
+
const next = stored === null ? await writeSharedState(incoming) : await writeSharedState(mergeSharedState(stored, incoming));
|
|
586
|
+
json(200, { ok: true, state: next });
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
json(200, { ok: true, state: await readSharedState() });
|
|
590
|
+
} catch (error) {
|
|
591
|
+
json(500, { ok: false, error: String(error?.message ?? error) });
|
|
592
|
+
}
|
|
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
|
+
}
|
|
364
663
|
function apply(ctx) {
|
|
365
664
|
ctx.effect(() => {
|
|
366
665
|
const handler = async (req, res) => {
|
|
367
666
|
try {
|
|
368
|
-
const
|
|
667
|
+
const url = new URL(req.url ?? "/", "http://dsh");
|
|
668
|
+
if (url.pathname === "/dsh-skill-picker/state") {
|
|
669
|
+
await handleSharedState(req, res, url);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (url.pathname === "/dsh-skill-picker/skill") {
|
|
673
|
+
await handleSkillOp(req, res, url);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const cwd = url.searchParams.get("cwd") ?? void 0;
|
|
369
677
|
const skills = await scanSkills(cwd);
|
|
370
678
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
371
679
|
res.end(JSON.stringify({ ok: true, complete: true, skills }));
|