dsh-plugin-capabilities 0.1.5 → 0.2.0
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 +16 -2
- package/lib/client.js +936 -220
- package/lib/client.js.map +4 -4
- package/lib/index.js +813 -38
- package/lib/index.js.map +4 -4
- package/market/mcp.json +164 -0
- package/market/skills.json +36 -0
- package/package.json +3 -1
- package/skills/find-skills/LICENSE +21 -0
- package/skills/find-skills/SKILL.md +141 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/SKILL.md +485 -0
- package/skills/skill-creator/agents/analyzer.md +274 -0
- package/skills/skill-creator/agents/comparator.md +202 -0
- package/skills/skill-creator/agents/grader.md +223 -0
- package/skills/skill-creator/assets/eval_review.html +146 -0
- package/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/skill-creator/references/schemas.md +430 -0
- package/skills/skill-creator/scripts/__init__.py +0 -0
- package/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/skill-creator/scripts/generate_report.py +326 -0
- package/skills/skill-creator/scripts/improve_description.py +247 -0
- package/skills/skill-creator/scripts/package_skill.py +136 -0
- package/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/skills/skill-creator/scripts/run_eval.py +310 -0
- package/skills/skill-creator/scripts/run_loop.py +328 -0
- package/skills/skill-creator/scripts/utils.py +47 -0
- package/src/client/CapabilitiesSection.tsx +11 -7
- package/src/client/MarketTab.tsx +263 -0
- package/src/client/McpTab.tsx +198 -0
- package/src/client/SkillsTab.tsx +261 -26
- package/src/client/css.ts +42 -0
- package/src/client/index.ts +58 -5
- package/src/client/locales.ts +112 -2
- package/src/index.ts +76 -20
- package/src/market.test.ts +57 -0
- package/src/market.ts +166 -0
- package/src/opener.ts +26 -0
- package/src/packaged-skills.test.ts +40 -0
- package/src/repos.test.ts +213 -0
- package/src/repos.ts +207 -0
- package/src/routes.ts +336 -6
- package/src/skills.test.ts +45 -1
- package/src/skills.ts +22 -1
- package/src/smoke.test.ts +61 -2
- package/src/state.test.ts +64 -0
- package/src/state.ts +109 -0
- package/src/tar.test.ts +100 -0
- package/src/tar.ts +154 -0
- package/src/types.ts +11 -1
package/lib/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
3
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
4
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5
|
+
|
|
1
6
|
// src/agents.ts
|
|
2
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
8
|
import { homedir } from "node:os";
|
|
@@ -796,16 +801,447 @@ function sendJson(response, status, body) {
|
|
|
796
801
|
response.end(payload);
|
|
797
802
|
}
|
|
798
803
|
|
|
799
|
-
// src/
|
|
804
|
+
// src/market.ts
|
|
805
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
806
|
+
import { dirname, join as join3 } from "node:path";
|
|
807
|
+
import { fileURLToPath } from "node:url";
|
|
808
|
+
function marketIndexUrl(kind) {
|
|
809
|
+
return `https://raw.githubusercontent.com/qinyre/dsh-plugin-capabilities/main/market/${kind}.json`;
|
|
810
|
+
}
|
|
811
|
+
function bundledMarketPath(kind) {
|
|
812
|
+
return join3(dirname(fileURLToPath(import.meta.url)), "..", "market", `${kind}.json`);
|
|
813
|
+
}
|
|
814
|
+
var isString = (value) => typeof value === "string" && value !== "";
|
|
815
|
+
var isStringArray = (value) => Array.isArray(value) && value.every(isString);
|
|
816
|
+
function parseSkillsIndex(parsed) {
|
|
817
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
818
|
+
const repos = parsed.repos;
|
|
819
|
+
if (!Array.isArray(repos)) return null;
|
|
820
|
+
const out = [];
|
|
821
|
+
for (const entry of repos) {
|
|
822
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
823
|
+
const record = entry;
|
|
824
|
+
if (!isString(record.id) || !isString(record.name) || !isString(record.description) || !isString(record.url)) continue;
|
|
825
|
+
out.push({
|
|
826
|
+
id: record.id,
|
|
827
|
+
name: record.name,
|
|
828
|
+
...isString(record.nameZh) ? { nameZh: record.nameZh } : {},
|
|
829
|
+
description: record.description,
|
|
830
|
+
...isString(record.descriptionZh) ? { descriptionZh: record.descriptionZh } : {},
|
|
831
|
+
url: record.url,
|
|
832
|
+
...isString(record.homepage) ? { homepage: record.homepage } : {},
|
|
833
|
+
...typeof record.skillCount === "number" ? { skillCount: record.skillCount } : {}
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
return out.length > 0 ? out : null;
|
|
837
|
+
}
|
|
838
|
+
function parseMcpIndex(parsed) {
|
|
839
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
840
|
+
const servers = parsed.servers;
|
|
841
|
+
if (!Array.isArray(servers)) return null;
|
|
842
|
+
const out = [];
|
|
843
|
+
for (const entry of servers) {
|
|
844
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
845
|
+
const record = entry;
|
|
846
|
+
if (!isString(record.id) || !isString(record.name) || !isString(record.description) || !isString(record.homepage)) continue;
|
|
847
|
+
const transport = record.transport === "streamable-http" ? "streamable-http" : "stdio";
|
|
848
|
+
if (transport === "stdio" && !isString(record.command)) continue;
|
|
849
|
+
if (transport === "streamable-http" && !isString(record.url)) continue;
|
|
850
|
+
out.push({
|
|
851
|
+
id: record.id,
|
|
852
|
+
name: record.name,
|
|
853
|
+
...isString(record.nameZh) ? { nameZh: record.nameZh } : {},
|
|
854
|
+
description: record.description,
|
|
855
|
+
...isString(record.descriptionZh) ? { descriptionZh: record.descriptionZh } : {},
|
|
856
|
+
transport,
|
|
857
|
+
...isString(record.command) ? { command: record.command } : {},
|
|
858
|
+
...isStringArray(record.args) ? { args: record.args } : {},
|
|
859
|
+
...isStringArray(record.envKeys) ? { envKeys: record.envKeys } : {},
|
|
860
|
+
...isString(record.url) ? { url: record.url } : {},
|
|
861
|
+
homepage: record.homepage,
|
|
862
|
+
...isString(record.category) ? { category: record.category } : {},
|
|
863
|
+
...isString(record.runtime) ? { runtime: record.runtime } : {}
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
return out.length > 0 ? out : null;
|
|
867
|
+
}
|
|
868
|
+
async function loadMarketIndex(kind, options = {}) {
|
|
869
|
+
const doFetch = options.fetcher ?? fetch;
|
|
870
|
+
if (kind === "skills") {
|
|
871
|
+
const remote = await fetchIndex(doFetch, "skills", parseSkillsIndex, options);
|
|
872
|
+
if (remote !== null) return { source: "remote", skills: remote };
|
|
873
|
+
} else {
|
|
874
|
+
const remote = await fetchIndex(doFetch, "mcp", parseMcpIndex, options);
|
|
875
|
+
if (remote !== null) return { source: "remote", servers: remote };
|
|
876
|
+
}
|
|
877
|
+
const bundled = bundledMarketPath(kind);
|
|
878
|
+
if (!existsSync2(bundled)) return null;
|
|
879
|
+
try {
|
|
880
|
+
const parsed = JSON.parse(readFileSync2(bundled, "utf8"));
|
|
881
|
+
if (kind === "skills") {
|
|
882
|
+
const skills = parseSkillsIndex(parsed);
|
|
883
|
+
return skills === null ? null : { source: "bundled", skills };
|
|
884
|
+
}
|
|
885
|
+
const servers = parseMcpIndex(parsed);
|
|
886
|
+
return servers === null ? null : { source: "bundled", servers };
|
|
887
|
+
} catch {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
async function fetchIndex(doFetch, kind, parse2, options) {
|
|
892
|
+
try {
|
|
893
|
+
const response = await doFetch(marketIndexUrl(kind), { signal: AbortSignal.timeout(options.timeoutMs ?? 8e3) });
|
|
894
|
+
if (!response.ok) return null;
|
|
895
|
+
return parse2(await response.json());
|
|
896
|
+
} catch {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/opener.ts
|
|
800
902
|
import { spawn } from "node:child_process";
|
|
903
|
+
import { statSync } from "node:fs";
|
|
904
|
+
function openDirectory(dir) {
|
|
905
|
+
try {
|
|
906
|
+
if (!statSync(dir).isDirectory()) return false;
|
|
907
|
+
} catch {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
const launcher = process.platform === "win32" ? "explorer" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
911
|
+
try {
|
|
912
|
+
const child = spawn(launcher, [dir], { detached: true, stdio: "ignore", windowsHide: true });
|
|
913
|
+
child.unref();
|
|
914
|
+
return true;
|
|
915
|
+
} catch {
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/repos.ts
|
|
921
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync, rmSync as rmSync2, statSync as statSync2, symlinkSync } from "node:fs";
|
|
922
|
+
import { basename, join as join6, resolve } from "node:path";
|
|
923
|
+
|
|
924
|
+
// src/state.ts
|
|
925
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, rmSync, writeFileSync } from "node:fs";
|
|
926
|
+
import { homedir as homedir3 } from "node:os";
|
|
927
|
+
import { join as join4 } from "node:path";
|
|
928
|
+
import { randomBytes } from "node:crypto";
|
|
929
|
+
function pluginStateDir(dshHome = process.env.DSH_HOME) {
|
|
930
|
+
return join4(dshHome ?? join4(homedir3(), ".dsh"), "dsh-plugin-capabilities");
|
|
931
|
+
}
|
|
932
|
+
function statePath(dshHome) {
|
|
933
|
+
return join4(pluginStateDir(dshHome), "state.json");
|
|
934
|
+
}
|
|
935
|
+
function emptyState() {
|
|
936
|
+
return { skillRoots: [] };
|
|
937
|
+
}
|
|
938
|
+
function loadState(dshHome) {
|
|
939
|
+
const path = statePath(dshHome);
|
|
940
|
+
if (!existsSync3(path)) return emptyState();
|
|
941
|
+
try {
|
|
942
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
943
|
+
if (!Array.isArray(parsed.skillRoots)) return emptyState();
|
|
944
|
+
const roots = parsed.skillRoots.filter((entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string" && Array.isArray(entry.roots));
|
|
945
|
+
return { skillRoots: roots };
|
|
946
|
+
} catch {
|
|
947
|
+
return emptyState();
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
function saveState(state, dshHome) {
|
|
951
|
+
const dir = pluginStateDir(dshHome);
|
|
952
|
+
mkdirSync(dir, { recursive: true });
|
|
953
|
+
writeFileSync(statePath(dshHome), JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
954
|
+
}
|
|
955
|
+
function newEntryId(kind) {
|
|
956
|
+
return `${kind}-${randomBytes(4).toString("hex")}`;
|
|
957
|
+
}
|
|
958
|
+
function materialDirFor(entryId, dshHome) {
|
|
959
|
+
return join4(pluginStateDir(dshHome), "repos", entryId);
|
|
960
|
+
}
|
|
961
|
+
function addSkillRoot(entry, dshHome) {
|
|
962
|
+
const state = loadState(dshHome);
|
|
963
|
+
const stored = { ...entry, addedAt: Date.now() };
|
|
964
|
+
state.skillRoots.push(stored);
|
|
965
|
+
saveState(state, dshHome);
|
|
966
|
+
return stored;
|
|
967
|
+
}
|
|
968
|
+
function removeSkillRoot(id, dshHome) {
|
|
969
|
+
const state = loadState(dshHome);
|
|
970
|
+
const at = state.skillRoots.findIndex((entry) => entry.id === id);
|
|
971
|
+
if (at === -1) return false;
|
|
972
|
+
const [removed] = state.skillRoots.splice(at, 1);
|
|
973
|
+
saveState(state, dshHome);
|
|
974
|
+
if (removed.materialDir !== void 0) {
|
|
975
|
+
rmSync(removed.materialDir, { recursive: true, force: true, maxRetries: 2 });
|
|
976
|
+
}
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
function findRootByUrl(url, dshHome) {
|
|
980
|
+
return loadState(dshHome).skillRoots.find((entry) => entry.url === url);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/tar.ts
|
|
984
|
+
import { gunzipSync } from "node:zlib";
|
|
985
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
986
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
987
|
+
var LIMITS = {
|
|
988
|
+
/** Per-entry cap (a skill repo holds markdown and small assets). */
|
|
989
|
+
entryBytes: 64 * 1024 * 1024,
|
|
990
|
+
/** Whole-archive content cap. */
|
|
991
|
+
totalBytes: 256 * 1024 * 1024,
|
|
992
|
+
/** Entry count cap (also bounds loop time on corrupt input). */
|
|
993
|
+
entries: 2e4
|
|
994
|
+
};
|
|
995
|
+
function octal(block, offset, length) {
|
|
996
|
+
const raw = block.toString("utf8", offset, offset + length).replace(/[\0 ]+$/, "");
|
|
997
|
+
return raw === "" ? 0 : Number.parseInt(raw, 8);
|
|
998
|
+
}
|
|
999
|
+
function field(block, offset, length) {
|
|
1000
|
+
const at = block.indexOf(0, offset);
|
|
1001
|
+
return block.toString("utf8", offset, Math.min(at === -1 ? offset + length : at, offset + length));
|
|
1002
|
+
}
|
|
1003
|
+
function readHeader(block) {
|
|
1004
|
+
if (block.every((byte) => byte === 0)) return null;
|
|
1005
|
+
const checksum = octal(block, 148, 8);
|
|
1006
|
+
let sum = 0;
|
|
1007
|
+
for (let at = 0; at < 512; at++) sum += at >= 148 && at < 156 ? 32 : block[at];
|
|
1008
|
+
if (sum !== checksum) return null;
|
|
1009
|
+
const magic = block.toString("utf8", 257, 257 + 6);
|
|
1010
|
+
let name2 = field(block, 0, 100);
|
|
1011
|
+
if (magic.startsWith("ustar")) {
|
|
1012
|
+
const prefix = field(block, 345, 155);
|
|
1013
|
+
if (prefix !== "") name2 = `${prefix}/${name2}`;
|
|
1014
|
+
}
|
|
1015
|
+
return { name: name2, size: octal(block, 124, 12), type: block.toString("utf8", 156, 157) };
|
|
1016
|
+
}
|
|
1017
|
+
function safeJoin(target, name2) {
|
|
1018
|
+
const normalized = name2.replace(/\\/g, "/");
|
|
1019
|
+
if (normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized)) return null;
|
|
1020
|
+
const parts = [];
|
|
1021
|
+
for (const part of normalized.split("/")) {
|
|
1022
|
+
if (part === "" || part === ".") continue;
|
|
1023
|
+
if (part === "..") return null;
|
|
1024
|
+
parts.push(part);
|
|
1025
|
+
}
|
|
1026
|
+
if (parts.length === 0) return null;
|
|
1027
|
+
return join5(target, ...parts);
|
|
1028
|
+
}
|
|
1029
|
+
function paxRecords(content) {
|
|
1030
|
+
const out = /* @__PURE__ */ new Map();
|
|
1031
|
+
let cursor = 0;
|
|
1032
|
+
while (cursor < content.length) {
|
|
1033
|
+
const spaceAt = content.indexOf(" ", cursor);
|
|
1034
|
+
if (spaceAt === -1) break;
|
|
1035
|
+
const length = Number.parseInt(content.toString("utf8", cursor, spaceAt), 10);
|
|
1036
|
+
if (!Number.isInteger(length) || length <= 0 || cursor + length > content.length) break;
|
|
1037
|
+
const record = content.toString("utf8", spaceAt + 1, cursor + length).trimEnd();
|
|
1038
|
+
const eq = record.indexOf("=");
|
|
1039
|
+
if (eq > 0) out.set(record.slice(0, eq), record.slice(eq + 1));
|
|
1040
|
+
cursor += length;
|
|
1041
|
+
}
|
|
1042
|
+
return out;
|
|
1043
|
+
}
|
|
1044
|
+
function extractTarGz(archive, target, options = {}) {
|
|
1045
|
+
const tar = gunzipSync(archive);
|
|
1046
|
+
const strip = options.stripComponents ?? 0;
|
|
1047
|
+
let offset = 0;
|
|
1048
|
+
let written = 0;
|
|
1049
|
+
let total = 0;
|
|
1050
|
+
let pendingLongName;
|
|
1051
|
+
let pendingPath;
|
|
1052
|
+
while (offset + 512 <= tar.length) {
|
|
1053
|
+
const header = readHeader(tar.subarray(offset, offset + 512));
|
|
1054
|
+
if (header === null) break;
|
|
1055
|
+
offset += 512;
|
|
1056
|
+
const contentEnd = offset + header.size;
|
|
1057
|
+
if (contentEnd > tar.length) throw new Error("truncated tar entry");
|
|
1058
|
+
const content = tar.subarray(offset, contentEnd);
|
|
1059
|
+
offset += Math.ceil(header.size / 512) * 512;
|
|
1060
|
+
if (header.size > LIMITS.entryBytes) throw new Error("tar entry too large");
|
|
1061
|
+
total += header.size;
|
|
1062
|
+
if (total > LIMITS.totalBytes) throw new Error("tar archive too large");
|
|
1063
|
+
if (++written > LIMITS.entries) throw new Error("too many tar entries");
|
|
1064
|
+
if (header.type === "L") {
|
|
1065
|
+
pendingLongName = field(content, 0, content.length);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
if (header.type === "x" || header.type === "X") {
|
|
1069
|
+
pendingPath = paxRecords(content).get("path");
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
if (header.type === "g") continue;
|
|
1073
|
+
let name2 = pendingLongName ?? pendingPath ?? header.name;
|
|
1074
|
+
pendingLongName = void 0;
|
|
1075
|
+
pendingPath = void 0;
|
|
1076
|
+
const parts = name2.split("/");
|
|
1077
|
+
if (parts.length <= strip) continue;
|
|
1078
|
+
name2 = parts.slice(strip).join("/");
|
|
1079
|
+
if (name2 === "" || name2 === "/") continue;
|
|
1080
|
+
const isDir = header.type === "5" || (header.type === "0" || header.type === "\0") && name2.endsWith("/");
|
|
1081
|
+
if (header.type !== "0" && header.type !== "\0" && header.type !== "5") continue;
|
|
1082
|
+
const resolved = safeJoin(target, name2);
|
|
1083
|
+
if (resolved === null) throw new Error(`unsafe tar entry name: ${name2}`);
|
|
1084
|
+
if (isDir) {
|
|
1085
|
+
mkdirSync2(resolved, { recursive: true });
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
mkdirSync2(dirname2(resolved), { recursive: true });
|
|
1089
|
+
writeFileSync2(resolved, content);
|
|
1090
|
+
}
|
|
1091
|
+
return written;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/repos.ts
|
|
1095
|
+
var GITHUB_URL_RE = /^(?:https?:\/\/)?github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:\/(?:tree|archive)\/([^/#?]+?)(?:\.tar\.gz)?)?(?:#([^/?#]+))?(?:[/?#].*)?$/;
|
|
1096
|
+
var GITHUB_SHORT_RE = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;
|
|
1097
|
+
function parseGitHubSource(input) {
|
|
1098
|
+
const trimmed = input.trim();
|
|
1099
|
+
if (trimmed === "") return null;
|
|
1100
|
+
let owner = "";
|
|
1101
|
+
let repo = "";
|
|
1102
|
+
let ref;
|
|
1103
|
+
let match = GITHUB_URL_RE.exec(trimmed);
|
|
1104
|
+
if (match !== null) {
|
|
1105
|
+
const treeRef = match[3] !== void 0 ? match[3] : void 0;
|
|
1106
|
+
const hashRef = match[4] !== void 0 ? match[4] : void 0;
|
|
1107
|
+
owner = match[1];
|
|
1108
|
+
repo = match[2];
|
|
1109
|
+
ref = treeRef ?? hashRef;
|
|
1110
|
+
} else {
|
|
1111
|
+
match = GITHUB_SHORT_RE.exec(trimmed);
|
|
1112
|
+
if (match === null) return null;
|
|
1113
|
+
[, owner, repo] = match;
|
|
1114
|
+
ref = void 0;
|
|
1115
|
+
}
|
|
1116
|
+
const decodedRef = ref !== void 0 ? decodeURIComponent(ref) : void 0;
|
|
1117
|
+
const label = `${owner}/${repo}`;
|
|
1118
|
+
const tarballUrl = `https://codeload.github.com/${owner}/${repo}/tar.gz/${decodedRef ?? "HEAD"}`;
|
|
1119
|
+
return { owner, repo, ref: decodedRef, label, tarballUrl };
|
|
1120
|
+
}
|
|
1121
|
+
function looksLikeSkillFile(file) {
|
|
1122
|
+
try {
|
|
1123
|
+
return readFileSync4(file, "utf8").startsWith("---");
|
|
1124
|
+
} catch {
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function holdsSkills(dir) {
|
|
1129
|
+
let entries;
|
|
1130
|
+
try {
|
|
1131
|
+
entries = readdirSync(dir);
|
|
1132
|
+
} catch {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
if (existsSync4(join6(dir, "SKILL.md"))) return true;
|
|
1136
|
+
for (const name2 of entries) {
|
|
1137
|
+
if (name2.endsWith(".md") && looksLikeSkillFile(join6(dir, name2))) return true;
|
|
1138
|
+
}
|
|
1139
|
+
for (const name2 of entries) {
|
|
1140
|
+
const child = join6(dir, name2);
|
|
1141
|
+
try {
|
|
1142
|
+
if (statSync2(child).isDirectory() && existsSync4(join6(child, "SKILL.md"))) return true;
|
|
1143
|
+
} catch {
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
function detectSkillRoots(checkout) {
|
|
1149
|
+
if (existsSync4(join6(checkout, "SKILL.md"))) return { roots: [], single: true };
|
|
1150
|
+
const roots = [];
|
|
1151
|
+
if (holdsSkills(checkout)) roots.push(checkout);
|
|
1152
|
+
let entries;
|
|
1153
|
+
try {
|
|
1154
|
+
entries = readdirSync(checkout);
|
|
1155
|
+
} catch {
|
|
1156
|
+
return { roots, single: false };
|
|
1157
|
+
}
|
|
1158
|
+
for (const name2 of entries) {
|
|
1159
|
+
if (name2.startsWith(".")) continue;
|
|
1160
|
+
const child = join6(checkout, name2);
|
|
1161
|
+
try {
|
|
1162
|
+
if (!statSync2(child).isDirectory()) continue;
|
|
1163
|
+
} catch {
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
if (existsSync4(join6(child, "SKILL.md"))) continue;
|
|
1167
|
+
if (holdsSkills(child)) roots.push(child);
|
|
1168
|
+
}
|
|
1169
|
+
return { roots, single: false };
|
|
1170
|
+
}
|
|
1171
|
+
async function addLocalRepo(path, dshHome) {
|
|
1172
|
+
const resolved = resolve(path.trim().replace(/^"|"$/g, ""));
|
|
1173
|
+
if (!existsSync4(resolved) || !statSync2(resolved).isDirectory()) {
|
|
1174
|
+
throw new Error(`not a directory: ${resolved}`);
|
|
1175
|
+
}
|
|
1176
|
+
const detected = detectSkillRoots(resolved);
|
|
1177
|
+
if (!detected.single && detected.roots.length === 0) {
|
|
1178
|
+
throw new Error("no SKILL.md found under that path (expected a skill folder or a folder of skill folders)");
|
|
1179
|
+
}
|
|
1180
|
+
if (detected.single) {
|
|
1181
|
+
const id = newEntryId("local");
|
|
1182
|
+
const material = materialDirFor(id, dshHome);
|
|
1183
|
+
rmSync2(material, { recursive: true, force: true });
|
|
1184
|
+
mkdirSync3(material, { recursive: true });
|
|
1185
|
+
try {
|
|
1186
|
+
symlinkSync(resolved, join6(material, "skill"), process.platform === "win32" ? "junction" : "dir");
|
|
1187
|
+
} catch {
|
|
1188
|
+
rmSync2(material, { recursive: true, force: true });
|
|
1189
|
+
throw new Error("single-skill local folders need a directory link; try adding their parent folder instead");
|
|
1190
|
+
}
|
|
1191
|
+
return addSkillRoot({ id, kind: "local", label: basename(resolved), path: resolved, roots: [material], materialDir: material }, dshHome);
|
|
1192
|
+
}
|
|
1193
|
+
return addSkillRoot({ id: newEntryId("local"), kind: "local", label: basename(resolved), path: resolved, roots: detected.roots }, dshHome);
|
|
1194
|
+
}
|
|
1195
|
+
var TARBALL_MAX_BYTES = 256 * 1024 * 1024;
|
|
1196
|
+
async function addGitRepo(url, options = {}) {
|
|
1197
|
+
const source = parseGitHubSource(url);
|
|
1198
|
+
if (source === null) throw new Error("expected a GitHub repository URL or owner/repo");
|
|
1199
|
+
const existing = findRootByUrl(source.label, options.dshHome);
|
|
1200
|
+
if (existing !== void 0) throw new Error(`${source.label} is already registered`);
|
|
1201
|
+
const doFetch = options.fetcher ?? fetch;
|
|
1202
|
+
const response = await doFetch(source.tarballUrl, { signal: AbortSignal.timeout(6e4) });
|
|
1203
|
+
if (!response.ok) throw new Error(`download failed (HTTP ${response.status}) for ${source.tarballUrl}`);
|
|
1204
|
+
const archive = Buffer.from(await response.arrayBuffer());
|
|
1205
|
+
if (archive.byteLength > TARBALL_MAX_BYTES) throw new Error("repository tarball too large");
|
|
1206
|
+
const id = newEntryId("git");
|
|
1207
|
+
const material = materialDirFor(id, options.dshHome);
|
|
1208
|
+
rmSync2(material, { recursive: true, force: true });
|
|
1209
|
+
const checkout = join6(material, "repo");
|
|
1210
|
+
try {
|
|
1211
|
+
mkdirSync3(checkout, { recursive: true });
|
|
1212
|
+
extractTarGz(archive, checkout, { stripComponents: 1 });
|
|
1213
|
+
const detected = detectSkillRoots(checkout);
|
|
1214
|
+
if (!detected.single && detected.roots.length === 0) {
|
|
1215
|
+
throw new Error("no SKILL.md found in that repository");
|
|
1216
|
+
}
|
|
1217
|
+
const roots = detected.single ? [material] : detected.roots;
|
|
1218
|
+
return addSkillRoot(
|
|
1219
|
+
{ id, kind: "git", label: source.label, url: source.label, ref: source.ref, roots, materialDir: material },
|
|
1220
|
+
options.dshHome
|
|
1221
|
+
);
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
rmSync2(material, { recursive: true, force: true });
|
|
1224
|
+
throw error;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
function rootExists(path) {
|
|
1228
|
+
try {
|
|
1229
|
+
return statSync2(path).isDirectory();
|
|
1230
|
+
} catch {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/restart.ts
|
|
1236
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
801
1237
|
import { openSync } from "node:fs";
|
|
802
1238
|
import { tmpdir } from "node:os";
|
|
803
|
-
import { dirname, resolve } from "node:path";
|
|
1239
|
+
import { dirname as dirname3, resolve as resolve2 } from "node:path";
|
|
804
1240
|
function dshLaunch(argv = process.argv, execArgv = process.execArgv) {
|
|
805
1241
|
const entry = argv[1];
|
|
806
1242
|
if (entry !== void 0 && /[\\/](?:bin\.(?:js|ts)|dsh)$/.test(entry)) {
|
|
807
|
-
const abs =
|
|
808
|
-
return { file: process.execPath, args: [...execArgv, abs, ...argv.slice(2)], cwd:
|
|
1243
|
+
const abs = resolve2(entry);
|
|
1244
|
+
return { file: process.execPath, args: [...execArgv, abs, ...argv.slice(2)], cwd: dirname3(abs), viaShell: false };
|
|
809
1245
|
}
|
|
810
1246
|
return { file: "dsh", args: [...argv.slice(2)], cwd: void 0, viaShell: process.platform === "win32" };
|
|
811
1247
|
}
|
|
@@ -813,7 +1249,7 @@ function scheduleRestart(launch) {
|
|
|
813
1249
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
814
1250
|
const logOut = `${tmpdir()}${tmpdir().endsWith("/") ? "" : "\\"}dsh-plugin-capabilities-restart-${stamp}.out.log`;
|
|
815
1251
|
const logErr = logOut.replace(".out.log", ".err.log");
|
|
816
|
-
const child =
|
|
1252
|
+
const child = spawn2(launch.file, launch.args, {
|
|
817
1253
|
cwd: launch.cwd,
|
|
818
1254
|
stdio: ["ignore", openSync(logOut, "a"), openSync(logErr, "a")],
|
|
819
1255
|
env: process.env,
|
|
@@ -843,12 +1279,12 @@ function restartOwnedByShell(env = process.env) {
|
|
|
843
1279
|
}
|
|
844
1280
|
|
|
845
1281
|
// src/skills.ts
|
|
846
|
-
import { existsSync as
|
|
847
|
-
import { homedir as
|
|
848
|
-
import { join as
|
|
1282
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1283
|
+
import { homedir as homedir4 } from "node:os";
|
|
1284
|
+
import { join as join7 } from "node:path";
|
|
849
1285
|
var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
850
1286
|
function userSkillsDir(dshHome = process.env.DSH_HOME) {
|
|
851
|
-
return
|
|
1287
|
+
return join7(dshHome ?? join7(homedir4(), ".dsh"), "skills");
|
|
852
1288
|
}
|
|
853
1289
|
function quote(value) {
|
|
854
1290
|
return JSON.stringify(value);
|
|
@@ -878,40 +1314,51 @@ function validateSkillInput(input) {
|
|
|
878
1314
|
return null;
|
|
879
1315
|
}
|
|
880
1316
|
function skillDir(name2, dshHome) {
|
|
881
|
-
return
|
|
1317
|
+
return join7(userSkillsDir(dshHome), name2);
|
|
882
1318
|
}
|
|
883
1319
|
function writeSkill(input, dshHome) {
|
|
884
1320
|
const dir = skillDir(input.name, dshHome);
|
|
885
|
-
|
|
886
|
-
const file =
|
|
887
|
-
|
|
1321
|
+
mkdirSync4(dir, { recursive: true });
|
|
1322
|
+
const file = join7(dir, "SKILL.md");
|
|
1323
|
+
writeFileSync3(file, serializeSkill(input), "utf8");
|
|
888
1324
|
return file;
|
|
889
1325
|
}
|
|
890
1326
|
function deleteSkill(name2, dshHome) {
|
|
891
1327
|
if (!SKILL_NAME_RE.test(name2)) return false;
|
|
892
1328
|
const dir = skillDir(name2, dshHome);
|
|
893
|
-
if (!
|
|
894
|
-
|
|
1329
|
+
if (!existsSync5(dir) || !statSync3(dir).isDirectory()) return false;
|
|
1330
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
895
1331
|
return true;
|
|
896
1332
|
}
|
|
1333
|
+
function setSkillPolicy(file, enabled) {
|
|
1334
|
+
const text = readFileSync5(file, "utf8");
|
|
1335
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
1336
|
+
if (match === null) throw new Error("skill file has no frontmatter block");
|
|
1337
|
+
const newline = text.includes("\r\n---") ? "\r\n" : "\n";
|
|
1338
|
+
const body = text.slice(match[0].length);
|
|
1339
|
+
const lines = match[1].split(/\r?\n/);
|
|
1340
|
+
const kept = lines.filter((line) => !/^(disable-model-invocation|user-invocable):/.test(line));
|
|
1341
|
+
if (!enabled) kept.push("disable-model-invocation: true", "user-invocable: false");
|
|
1342
|
+
writeFileSync3(file, `---${newline}${kept.join(newline)}${newline}---${body}`, "utf8");
|
|
1343
|
+
}
|
|
897
1344
|
|
|
898
1345
|
// src/mcp.ts
|
|
899
|
-
import { existsSync as
|
|
900
|
-
import { join as
|
|
1346
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1347
|
+
import { join as join8 } from "node:path";
|
|
901
1348
|
import { parseDocument, Document } from "yaml";
|
|
902
1349
|
var MCP_PLUGIN = "@deepseek-ai/dsh-mcp-client";
|
|
903
1350
|
var SERVER_NAME_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
|
904
1351
|
function loadPatch(profileDirPath) {
|
|
905
|
-
const path =
|
|
906
|
-
const text =
|
|
1352
|
+
const path = join8(profileDirPath, "cordis.patch.yml");
|
|
1353
|
+
const text = existsSync6(path) ? readFileSync6(path, "utf8") : "[]";
|
|
907
1354
|
const doc = parseDocument(text);
|
|
908
1355
|
const contents = doc.contents;
|
|
909
1356
|
if (contents !== null && contents.flow === true && contents.items.length === 0) contents.flow = false;
|
|
910
1357
|
return doc;
|
|
911
1358
|
}
|
|
912
1359
|
function savePatch(profileDirPath, doc) {
|
|
913
|
-
|
|
914
|
-
|
|
1360
|
+
mkdirSync5(profileDirPath, { recursive: true });
|
|
1361
|
+
writeFileSync4(join8(profileDirPath, "cordis.patch.yml"), String(doc), "utf8");
|
|
915
1362
|
}
|
|
916
1363
|
function toNode(value) {
|
|
917
1364
|
return new Document(value).contents;
|
|
@@ -1074,6 +1521,13 @@ function removeMcp(profileDirPath, id) {
|
|
|
1074
1521
|
|
|
1075
1522
|
// src/routes.ts
|
|
1076
1523
|
var EDITABLE_SOURCE = "user-dsh";
|
|
1524
|
+
function toSkillRow(skill) {
|
|
1525
|
+
const dir = skill.resourceBase?.kind === "directory" ? skill.resourceBase.path : void 0;
|
|
1526
|
+
return { ...skill, editable: skill.source === EDITABLE_SOURCE, ...dir !== void 0 ? { dir } : {}, policyEditable: dir !== void 0 };
|
|
1527
|
+
}
|
|
1528
|
+
function toRootView(entry) {
|
|
1529
|
+
return { ...entry, live: entry.roots.every((root) => rootExists(root)) };
|
|
1530
|
+
}
|
|
1077
1531
|
function mountCapabilitiesRoutes(host, config) {
|
|
1078
1532
|
const disposers = [
|
|
1079
1533
|
host.webServer.register({
|
|
@@ -1087,9 +1541,7 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
1087
1541
|
}
|
|
1088
1542
|
try {
|
|
1089
1543
|
const skills = await host.skills.list();
|
|
1090
|
-
sendJson(response, 200, {
|
|
1091
|
-
skills: skills.map((skill) => ({ ...skill, editable: skill.source === EDITABLE_SOURCE }))
|
|
1092
|
-
});
|
|
1544
|
+
sendJson(response, 200, { skills: skills.map(toSkillRow) });
|
|
1093
1545
|
} catch (error) {
|
|
1094
1546
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
1095
1547
|
}
|
|
@@ -1108,6 +1560,10 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
1108
1560
|
const name2 = url.searchParams.get("name") ?? "";
|
|
1109
1561
|
try {
|
|
1110
1562
|
const definition = await host.skills.get(name2);
|
|
1563
|
+
if (definition === void 0) {
|
|
1564
|
+
sendJson(response, 404, { error: "skill not found" });
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1111
1567
|
sendJson(response, 200, { name: definition.name, content: definition.content });
|
|
1112
1568
|
} catch (error) {
|
|
1113
1569
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
@@ -1172,6 +1628,290 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
1172
1628
|
}
|
|
1173
1629
|
}
|
|
1174
1630
|
}),
|
|
1631
|
+
host.webServer.register({
|
|
1632
|
+
kind: "exact",
|
|
1633
|
+
path: "/dsh-plugin-capabilities/skill/policy",
|
|
1634
|
+
handler: async (request, response) => {
|
|
1635
|
+
if (request.method !== "POST") {
|
|
1636
|
+
response.writeHead(405, { allow: "POST" });
|
|
1637
|
+
response.end();
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
if (!sameOrigin(request)) {
|
|
1641
|
+
sendJson(response, 403, { error: "untrusted origin" });
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
try {
|
|
1645
|
+
const body = await readJsonBody(request);
|
|
1646
|
+
if (typeof body.name !== "string" || typeof body.enabled !== "boolean") {
|
|
1647
|
+
sendJson(response, 400, { error: "name and enabled are required" });
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
const definition = await host.skills.get(body.name);
|
|
1651
|
+
if (definition === void 0) {
|
|
1652
|
+
sendJson(response, 404, { error: "skill not found" });
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
if (definition.path === void 0) {
|
|
1656
|
+
sendJson(response, 422, { error: "skill has no file on disk (runtime-registered)" });
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
setSkillPolicy(definition.path, body.enabled);
|
|
1660
|
+
sendJson(response, 200, { ok: true });
|
|
1661
|
+
} catch (error) {
|
|
1662
|
+
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}),
|
|
1666
|
+
host.webServer.register({
|
|
1667
|
+
kind: "exact",
|
|
1668
|
+
path: "/dsh-plugin-capabilities/open",
|
|
1669
|
+
handler: async (request, response) => {
|
|
1670
|
+
if (request.method !== "POST") {
|
|
1671
|
+
response.writeHead(405, { allow: "POST" });
|
|
1672
|
+
response.end();
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
if (!sameOrigin(request)) {
|
|
1676
|
+
sendJson(response, 403, { error: "untrusted origin" });
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
try {
|
|
1680
|
+
const body = await readJsonBody(request);
|
|
1681
|
+
if (typeof body.target !== "string") {
|
|
1682
|
+
sendJson(response, 400, { error: "target is required" });
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
let dir;
|
|
1686
|
+
if (body.target === "user-skills") {
|
|
1687
|
+
dir = userSkillsDir();
|
|
1688
|
+
} else if (body.target === "plugin-state") {
|
|
1689
|
+
dir = pluginStateDir();
|
|
1690
|
+
} else if (body.target === "skill") {
|
|
1691
|
+
if (typeof body.name !== "string") {
|
|
1692
|
+
sendJson(response, 400, { error: "name is required" });
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const definition = await host.skills.get(body.name);
|
|
1696
|
+
if (definition === void 0) {
|
|
1697
|
+
sendJson(response, 404, { error: "skill not found" });
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
dir = definition.path !== void 0 ? definition.path.replace(/[/\\]SKILL\.md$/, "").replace(/[/\\][^/\\]+\.md$/, "") : definition.resourceBase?.kind === "directory" ? definition.resourceBase.path : void 0;
|
|
1701
|
+
} else if (body.target === "root") {
|
|
1702
|
+
if (typeof body.id !== "string") {
|
|
1703
|
+
sendJson(response, 400, { error: "id is required" });
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
const entry = loadState().skillRoots.find((row) => row.id === body.id);
|
|
1707
|
+
if (entry === void 0) {
|
|
1708
|
+
sendJson(response, 404, { error: "repository not found" });
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
dir = entry.materialDir ?? entry.path ?? entry.roots[0];
|
|
1712
|
+
} else {
|
|
1713
|
+
sendJson(response, 400, { error: "unknown target" });
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
if (dir === void 0 || !openDirectory(dir)) {
|
|
1717
|
+
sendJson(response, 422, { error: "directory is not available on disk" });
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
sendJson(response, 200, { ok: true });
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}),
|
|
1726
|
+
host.webServer.register({
|
|
1727
|
+
kind: "exact",
|
|
1728
|
+
path: "/dsh-plugin-capabilities/roots",
|
|
1729
|
+
handler: async (request, response) => {
|
|
1730
|
+
if (request.method !== "GET") {
|
|
1731
|
+
response.writeHead(405, { allow: "GET" });
|
|
1732
|
+
response.end();
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
sendJson(response, 200, { roots: loadState().skillRoots.map(toRootView) });
|
|
1736
|
+
}
|
|
1737
|
+
}),
|
|
1738
|
+
host.webServer.register({
|
|
1739
|
+
kind: "exact",
|
|
1740
|
+
path: "/dsh-plugin-capabilities/roots/add",
|
|
1741
|
+
handler: async (request, response) => {
|
|
1742
|
+
if (request.method !== "POST") {
|
|
1743
|
+
response.writeHead(405, { allow: "POST" });
|
|
1744
|
+
response.end();
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
if (!sameOrigin(request)) {
|
|
1748
|
+
sendJson(response, 403, { error: "untrusted origin" });
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
try {
|
|
1752
|
+
const body = await readJsonBody(request);
|
|
1753
|
+
if (body.kind !== "local" && body.kind !== "git") {
|
|
1754
|
+
sendJson(response, 400, { error: "kind must be local or git" });
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
const entry = body.kind === "local" ? typeof body.path === "string" && body.path.trim() !== "" ? await addLocalRepo(body.path) : void 0 : typeof body.url === "string" && body.url.trim() !== "" ? await addGitRepo(body.url) : void 0;
|
|
1758
|
+
if (entry === void 0) {
|
|
1759
|
+
sendJson(response, 400, { error: body.kind === "local" ? "path is required" : "url is required" });
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
await config.remountProvider();
|
|
1763
|
+
sendJson(response, 200, { ok: true, root: toRootView(entry) });
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
sendJson(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}),
|
|
1769
|
+
host.webServer.register({
|
|
1770
|
+
kind: "exact",
|
|
1771
|
+
path: "/dsh-plugin-capabilities/roots/remove",
|
|
1772
|
+
handler: async (request, response) => {
|
|
1773
|
+
if (request.method !== "POST") {
|
|
1774
|
+
response.writeHead(405, { allow: "POST" });
|
|
1775
|
+
response.end();
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (!sameOrigin(request)) {
|
|
1779
|
+
sendJson(response, 403, { error: "untrusted origin" });
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
try {
|
|
1783
|
+
const body = await readJsonBody(request);
|
|
1784
|
+
if (typeof body.id !== "string") {
|
|
1785
|
+
sendJson(response, 400, { error: "id is required" });
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
const ok = removeSkillRoot(body.id);
|
|
1789
|
+
if (!ok) {
|
|
1790
|
+
sendJson(response, 404, { error: "repository not found" });
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
await config.remountProvider();
|
|
1794
|
+
sendJson(response, 200, { ok: true });
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}),
|
|
1800
|
+
host.webServer.register({
|
|
1801
|
+
kind: "exact",
|
|
1802
|
+
path: "/dsh-plugin-capabilities/market/skills",
|
|
1803
|
+
handler: async (request, response) => {
|
|
1804
|
+
if (request.method !== "GET") {
|
|
1805
|
+
response.writeHead(405, { allow: "GET" });
|
|
1806
|
+
response.end();
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
const index = await loadMarketIndex("skills");
|
|
1810
|
+
if (index === null) {
|
|
1811
|
+
sendJson(response, 502, { error: "market index unavailable (offline?)" });
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
sendJson(response, 200, {
|
|
1815
|
+
source: index.source,
|
|
1816
|
+
repos: (index.skills ?? []).map((repo) => ({ ...repo, installedId: findRootByUrl(repo.url)?.id ?? null }))
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
}),
|
|
1820
|
+
host.webServer.register({
|
|
1821
|
+
kind: "exact",
|
|
1822
|
+
path: "/dsh-plugin-capabilities/market/mcp",
|
|
1823
|
+
handler: async (request, response) => {
|
|
1824
|
+
if (request.method !== "GET") {
|
|
1825
|
+
response.writeHead(405, { allow: "GET" });
|
|
1826
|
+
response.end();
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
const index = await loadMarketIndex("mcp");
|
|
1830
|
+
if (index === null) {
|
|
1831
|
+
sendJson(response, 502, { error: "market index unavailable (offline?)" });
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
const existing = new Set(listMcp(config.profileDirPath).map((row) => row.serverName));
|
|
1835
|
+
sendJson(response, 200, {
|
|
1836
|
+
source: index.source,
|
|
1837
|
+
servers: (index.servers ?? []).map((server) => ({ ...server, installed: existing.has(server.id) }))
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
}),
|
|
1841
|
+
host.webServer.register({
|
|
1842
|
+
kind: "exact",
|
|
1843
|
+
path: "/dsh-plugin-capabilities/market/skills/install",
|
|
1844
|
+
handler: async (request, response) => {
|
|
1845
|
+
if (request.method !== "POST") {
|
|
1846
|
+
response.writeHead(405, { allow: "POST" });
|
|
1847
|
+
response.end();
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (!sameOrigin(request)) {
|
|
1851
|
+
sendJson(response, 403, { error: "untrusted origin" });
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
try {
|
|
1855
|
+
const body = await readJsonBody(request);
|
|
1856
|
+
if (typeof body.url !== "string") {
|
|
1857
|
+
sendJson(response, 400, { error: "url is required" });
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const entry = await addGitRepo(body.url);
|
|
1861
|
+
await config.remountProvider();
|
|
1862
|
+
sendJson(response, 200, { ok: true, root: toRootView(entry) });
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
sendJson(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}),
|
|
1868
|
+
host.webServer.register({
|
|
1869
|
+
kind: "exact",
|
|
1870
|
+
path: "/dsh-plugin-capabilities/market/mcp/install",
|
|
1871
|
+
handler: async (request, response) => {
|
|
1872
|
+
if (request.method !== "POST") {
|
|
1873
|
+
response.writeHead(405, { allow: "POST" });
|
|
1874
|
+
response.end();
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
if (!sameOrigin(request)) {
|
|
1878
|
+
sendJson(response, 403, { error: "untrusted origin" });
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
try {
|
|
1882
|
+
const body = await readJsonBody(request);
|
|
1883
|
+
if (typeof body.id !== "string") {
|
|
1884
|
+
sendJson(response, 400, { error: "id is required" });
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
const index = await loadMarketIndex("mcp");
|
|
1888
|
+
const server = index?.servers?.find((row) => row.id === body.id);
|
|
1889
|
+
if (server === void 0) {
|
|
1890
|
+
sendJson(response, 404, { error: "server not found in the market index" });
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
if (listMcp(config.profileDirPath).some((row) => row.serverName === server.id)) {
|
|
1894
|
+
sendJson(response, 409, { error: "already installed" });
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
const input = {
|
|
1898
|
+
id: "",
|
|
1899
|
+
serverName: server.id,
|
|
1900
|
+
transport: server.transport,
|
|
1901
|
+
...server.transport === "stdio" ? { command: server.command, args: server.args, env: void 0 } : { url: server.url }
|
|
1902
|
+
};
|
|
1903
|
+
const invalid = validateMcpInput(input);
|
|
1904
|
+
if (invalid !== null) {
|
|
1905
|
+
sendJson(response, 400, { error: invalid });
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
const id = upsertMcp(config.profileDirPath, input);
|
|
1909
|
+
sendJson(response, 200, { ok: true, id, restartNeeded: true });
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}),
|
|
1175
1915
|
host.webServer.register({
|
|
1176
1916
|
kind: "exact",
|
|
1177
1917
|
path: "/dsh-plugin-capabilities/mcp",
|
|
@@ -1358,29 +2098,64 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
1358
2098
|
|
|
1359
2099
|
// src/index.ts
|
|
1360
2100
|
var name = "dsh-plugin-capabilities";
|
|
2101
|
+
function packagedSkillsDir() {
|
|
2102
|
+
return join9(dirname4(fileURLToPath2(import.meta.url)), "..", "skills");
|
|
2103
|
+
}
|
|
1361
2104
|
var inject = ["webServer", "skills"];
|
|
1362
2105
|
function apply(ctx, config) {
|
|
1363
2106
|
const profile = config?.profile ?? argvProfile() ?? "web";
|
|
1364
2107
|
ctx.inject(["webServer", "skills"], (hostCtx) => {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
2108
|
+
let providerFiber;
|
|
2109
|
+
let disposed = false;
|
|
2110
|
+
ctx.effect(() => {
|
|
2111
|
+
const disposer = () => {
|
|
2112
|
+
disposed = true;
|
|
2113
|
+
};
|
|
2114
|
+
let chain = Promise.resolve();
|
|
2115
|
+
const remountProvider = () => {
|
|
2116
|
+
chain = chain.then(async () => {
|
|
2117
|
+
if (disposed) return;
|
|
2118
|
+
const mod = await import("@deepseek-ai/dsh-skill-filesystem");
|
|
2119
|
+
const plugin = mod.default ?? mod;
|
|
2120
|
+
if (providerFiber !== void 0) {
|
|
2121
|
+
const old = providerFiber;
|
|
2122
|
+
providerFiber = void 0;
|
|
2123
|
+
try {
|
|
2124
|
+
await old.dispose();
|
|
2125
|
+
} catch {
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (disposed) return;
|
|
2129
|
+
const roots = [
|
|
2130
|
+
packagedSkillsDir(),
|
|
2131
|
+
...loadState().skillRoots.flatMap((entry) => entry.roots),
|
|
2132
|
+
...agentSkillRoots()
|
|
2133
|
+
].filter((dir) => existsSync7(dir));
|
|
2134
|
+
try {
|
|
2135
|
+
providerFiber = hostCtx.plugin(plugin, roots.length > 0 ? { customSkillDirs: roots } : {});
|
|
2136
|
+
} catch {
|
|
2137
|
+
}
|
|
2138
|
+
});
|
|
2139
|
+
chain = chain.catch(() => void 0);
|
|
2140
|
+
return chain;
|
|
2141
|
+
};
|
|
2142
|
+
void remountProvider();
|
|
2143
|
+
ctx.effect(
|
|
2144
|
+
() => mountCapabilitiesRoutes(hostCtx, {
|
|
2145
|
+
profileDirPath: profileDir(profile),
|
|
2146
|
+
remountProvider
|
|
2147
|
+
}),
|
|
2148
|
+
"dsh-plugin-capabilities: http routes"
|
|
2149
|
+
);
|
|
2150
|
+
return disposer;
|
|
2151
|
+
}, "dsh-plugin-capabilities: skill provider");
|
|
1378
2152
|
});
|
|
1379
2153
|
}
|
|
1380
2154
|
export {
|
|
1381
2155
|
apply,
|
|
1382
2156
|
inject,
|
|
1383
|
-
name
|
|
2157
|
+
name,
|
|
2158
|
+
packagedSkillsDir
|
|
1384
2159
|
};
|
|
1385
2160
|
/*! Bundled license information:
|
|
1386
2161
|
|