dsh-plugin-capabilities 0.1.6 → 0.2.1

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