dsh-plugin-capabilities 0.1.1 → 0.1.3
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 +17 -12
- package/lib/client.js +60 -6
- package/lib/client.js.map +2 -2
- package/lib/index.js +166 -34
- package/lib/index.js.map +4 -4
- package/package.json +15 -3
- package/src/client/McpTab.tsx +44 -6
- package/src/client/index.ts +8 -1
- package/src/client/locales.ts +10 -0
- package/src/mcp.test.ts +42 -0
- package/src/mcp.ts +132 -38
- package/src/restart.test.ts +52 -0
- package/src/restart.ts +94 -0
- package/src/routes.ts +24 -0
package/lib/index.js
CHANGED
|
@@ -796,6 +796,52 @@ function sendJson(response, status, body) {
|
|
|
796
796
|
response.end(payload);
|
|
797
797
|
}
|
|
798
798
|
|
|
799
|
+
// src/restart.ts
|
|
800
|
+
import { spawn } from "node:child_process";
|
|
801
|
+
import { openSync } from "node:fs";
|
|
802
|
+
import { tmpdir } from "node:os";
|
|
803
|
+
import { dirname, resolve } from "node:path";
|
|
804
|
+
function dshLaunch(argv = process.argv, execArgv = process.execArgv) {
|
|
805
|
+
const entry = argv[1];
|
|
806
|
+
if (entry !== void 0 && /[\\/](?:bin\.(?:js|ts)|dsh)$/.test(entry)) {
|
|
807
|
+
const abs = resolve(entry);
|
|
808
|
+
return { file: process.execPath, args: [...execArgv, abs, ...argv.slice(2)], cwd: dirname(abs), viaShell: false };
|
|
809
|
+
}
|
|
810
|
+
return { file: "dsh", args: [...argv.slice(2)], cwd: void 0, viaShell: process.platform === "win32" };
|
|
811
|
+
}
|
|
812
|
+
function scheduleRestart(launch) {
|
|
813
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
814
|
+
const logOut = `${tmpdir()}${tmpdir().endsWith("/") ? "" : "\\"}dsh-plugin-capabilities-restart-${stamp}.out.log`;
|
|
815
|
+
const logErr = logOut.replace(".out.log", ".err.log");
|
|
816
|
+
const child = spawn(launch.file, launch.args, {
|
|
817
|
+
cwd: launch.cwd,
|
|
818
|
+
stdio: ["ignore", openSync(logOut, "a"), openSync(logErr, "a")],
|
|
819
|
+
env: process.env,
|
|
820
|
+
shell: launch.viaShell,
|
|
821
|
+
windowsHide: true
|
|
822
|
+
});
|
|
823
|
+
child.unref();
|
|
824
|
+
setTimeout(() => process.kill(process.pid, "SIGTERM"), 500);
|
|
825
|
+
return { pid: process.pid, replacementPid: child.pid, logOut, logErr };
|
|
826
|
+
}
|
|
827
|
+
function trustedRestartRequest(request, socketAddress) {
|
|
828
|
+
const address = socketAddress ?? (request.socket.remoteAddress ?? "");
|
|
829
|
+
if (address !== "127.0.0.1" && address !== "::1" && address !== "::ffff:127.0.0.1") return false;
|
|
830
|
+
if (request.headers.forwarded !== void 0 || request.headers["x-forwarded-for"] !== void 0 || request.headers["x-real-ip"] !== void 0) return false;
|
|
831
|
+
const origin = request.headers.origin;
|
|
832
|
+
const host = request.headers.host;
|
|
833
|
+
if (origin === void 0 || host === void 0) return false;
|
|
834
|
+
try {
|
|
835
|
+
const parsed = new URL(origin);
|
|
836
|
+
return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.host === host;
|
|
837
|
+
} catch {
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function restartOwnedByShell(env = process.env) {
|
|
842
|
+
return env.DSH_DESKTOP === "1";
|
|
843
|
+
}
|
|
844
|
+
|
|
799
845
|
// src/skills.ts
|
|
800
846
|
import { existsSync as existsSync2, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
801
847
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -858,7 +904,10 @@ var SERVER_NAME_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
|
|
858
904
|
function loadPatch(profileDirPath) {
|
|
859
905
|
const path = join4(profileDirPath, "cordis.patch.yml");
|
|
860
906
|
const text = existsSync3(path) ? readFileSync2(path, "utf8") : "[]";
|
|
861
|
-
|
|
907
|
+
const doc = parseDocument(text);
|
|
908
|
+
const contents = doc.contents;
|
|
909
|
+
if (contents !== null && contents.flow === true && contents.items.length === 0) contents.flow = false;
|
|
910
|
+
return doc;
|
|
862
911
|
}
|
|
863
912
|
function savePatch(profileDirPath, doc) {
|
|
864
913
|
mkdirSync2(profileDirPath, { recursive: true });
|
|
@@ -868,34 +917,87 @@ function toNode(value) {
|
|
|
868
917
|
return new Document(value).contents;
|
|
869
918
|
}
|
|
870
919
|
function rowSeq(doc) {
|
|
871
|
-
if (doc.contents === null)
|
|
920
|
+
if (doc.contents === null) {
|
|
921
|
+
doc.contents = toNode([]);
|
|
922
|
+
doc.contents.flow = false;
|
|
923
|
+
}
|
|
872
924
|
return doc.contents;
|
|
873
925
|
}
|
|
874
|
-
function
|
|
875
|
-
return
|
|
926
|
+
function isSeqNode(value) {
|
|
927
|
+
return typeof value === "object" && value !== null && Array.isArray(value.items);
|
|
928
|
+
}
|
|
929
|
+
function insertListOf(item) {
|
|
930
|
+
if (item.has("id")) return void 0;
|
|
931
|
+
const node = item.get("insert");
|
|
932
|
+
return isSeqNode(node) ? node : void 0;
|
|
933
|
+
}
|
|
934
|
+
function mcpRowItems(doc) {
|
|
935
|
+
const found = [];
|
|
936
|
+
for (const item of rowSeq(doc).items ?? []) {
|
|
937
|
+
if (item.get("name") === MCP_PLUGIN) found.push({ node: item });
|
|
938
|
+
const list = insertListOf(item);
|
|
939
|
+
for (const row of list?.items ?? []) {
|
|
940
|
+
if (row.get("name") === MCP_PLUGIN) found.push({ node: row, list });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return found;
|
|
944
|
+
}
|
|
945
|
+
function rowToMcp(doc, item) {
|
|
946
|
+
const configNode = item.get("config");
|
|
947
|
+
const plain = typeof configNode === "object" && configNode !== null && typeof configNode.toJS === "function" ? configNode.toJS(doc) : {};
|
|
948
|
+
return {
|
|
949
|
+
id: String(item.get("id") ?? ""),
|
|
950
|
+
serverName: String(plain.serverName ?? ""),
|
|
951
|
+
transport: plain.transport === "streamable-http" ? "streamable-http" : "stdio",
|
|
952
|
+
disabled: item.get("disabled") === true,
|
|
953
|
+
...typeof plain.command === "string" && plain.command !== "" ? { command: plain.command } : {},
|
|
954
|
+
...Array.isArray(plain.args) ? { args: plain.args.map(String) } : {},
|
|
955
|
+
...isStringMap(plain.env) ? { env: plain.env } : {},
|
|
956
|
+
...typeof plain.cwd === "string" && plain.cwd !== "" ? { cwd: plain.cwd } : {},
|
|
957
|
+
...typeof plain.url === "string" && plain.url !== "" ? { url: plain.url } : {},
|
|
958
|
+
...isStringMap(plain.headers) ? { headers: plain.headers } : {}
|
|
959
|
+
};
|
|
876
960
|
}
|
|
877
961
|
function isStringMap(value) {
|
|
878
962
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
879
963
|
return Object.values(value).every((entry) => typeof entry === "string");
|
|
880
964
|
}
|
|
965
|
+
function managedInsert(doc) {
|
|
966
|
+
const seq = rowSeq(doc);
|
|
967
|
+
const bare = [];
|
|
968
|
+
let target;
|
|
969
|
+
for (const item of seq.items ?? []) {
|
|
970
|
+
if (item.get("name") === MCP_PLUGIN) bare.push(item);
|
|
971
|
+
const list = insertListOf(item);
|
|
972
|
+
if (list !== void 0 && list.items.some((row) => row.get("name") === MCP_PLUGIN)) target ??= list;
|
|
973
|
+
}
|
|
974
|
+
if (target === void 0) {
|
|
975
|
+
const entry = toNode({ insert: [] });
|
|
976
|
+
seq.add(entry);
|
|
977
|
+
target = entry.get("insert");
|
|
978
|
+
target.flow = false;
|
|
979
|
+
}
|
|
980
|
+
for (const row of bare) {
|
|
981
|
+
seq.items.splice(seq.items.indexOf(row), 1);
|
|
982
|
+
target.add(row);
|
|
983
|
+
}
|
|
984
|
+
return target;
|
|
985
|
+
}
|
|
986
|
+
function takenIds(doc) {
|
|
987
|
+
const taken = /* @__PURE__ */ new Set();
|
|
988
|
+
for (const item of rowSeq(doc).items ?? []) {
|
|
989
|
+
const id = String(item.get("id") ?? "");
|
|
990
|
+
if (id !== "") taken.add(id);
|
|
991
|
+
for (const row of insertListOf(item)?.items ?? []) {
|
|
992
|
+
const rowId = String(row.get("id") ?? "");
|
|
993
|
+
if (rowId !== "") taken.add(rowId);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return taken;
|
|
997
|
+
}
|
|
881
998
|
function listMcp(profileDirPath) {
|
|
882
999
|
const doc = loadPatch(profileDirPath);
|
|
883
|
-
return
|
|
884
|
-
const configNode = item.get("config");
|
|
885
|
-
const plain = typeof configNode === "object" && configNode !== null && typeof configNode.toJS === "function" ? configNode.toJS(doc) : {};
|
|
886
|
-
return {
|
|
887
|
-
id: String(item.get("id") ?? ""),
|
|
888
|
-
serverName: String(plain.serverName ?? ""),
|
|
889
|
-
transport: plain.transport === "streamable-http" ? "streamable-http" : "stdio",
|
|
890
|
-
disabled: item.get("disabled") === true,
|
|
891
|
-
...typeof plain.command === "string" && plain.command !== "" ? { command: plain.command } : {},
|
|
892
|
-
...Array.isArray(plain.args) ? { args: plain.args.map(String) } : {},
|
|
893
|
-
...isStringMap(plain.env) ? { env: plain.env } : {},
|
|
894
|
-
...typeof plain.cwd === "string" && plain.cwd !== "" ? { cwd: plain.cwd } : {},
|
|
895
|
-
...typeof plain.url === "string" && plain.url !== "" ? { url: plain.url } : {},
|
|
896
|
-
...isStringMap(plain.headers) ? { headers: plain.headers } : {}
|
|
897
|
-
};
|
|
898
|
-
});
|
|
1000
|
+
return mcpRowItems(doc).map(({ node }) => rowToMcp(doc, node));
|
|
899
1001
|
}
|
|
900
1002
|
function validateMcpInput(input) {
|
|
901
1003
|
if (!SERVER_NAME_RE.test(input.serverName)) return "serverName must be 1-32 chars of A-Z a-z 0-9 _ -";
|
|
@@ -911,13 +1013,11 @@ function validateMcpInput(input) {
|
|
|
911
1013
|
function upsertMcp(profileDirPath, input) {
|
|
912
1014
|
const inputId = input.id ?? "";
|
|
913
1015
|
const doc = loadPatch(profileDirPath);
|
|
914
|
-
const
|
|
915
|
-
const existing = inputId !== "" ?
|
|
1016
|
+
const list = managedInsert(doc);
|
|
1017
|
+
const existing = inputId !== "" ? mcpRowItems(doc).find(({ node: node2 }) => String(node2.get("id") ?? "") === inputId) : void 0;
|
|
916
1018
|
let id = inputId !== "" ? inputId : `mcp-${input.serverName}`;
|
|
917
1019
|
if (existing === void 0) {
|
|
918
|
-
const taken =
|
|
919
|
-
(seq.items ?? []).map((item) => String(item.get("id") ?? "")).filter((id2) => id2 !== "")
|
|
920
|
-
);
|
|
1020
|
+
const taken = takenIds(doc);
|
|
921
1021
|
let suffix = 2;
|
|
922
1022
|
while (taken.has(id)) id = `mcp-${input.serverName}-${suffix++}`;
|
|
923
1023
|
}
|
|
@@ -937,26 +1037,37 @@ function upsertMcp(profileDirPath, input) {
|
|
|
937
1037
|
const row = { id, name: MCP_PLUGIN, config };
|
|
938
1038
|
if (input.disabled === true) row.disabled = true;
|
|
939
1039
|
const node = toNode(row);
|
|
940
|
-
if (existing === void 0)
|
|
941
|
-
|
|
1040
|
+
if (existing === void 0) {
|
|
1041
|
+
list.add(node);
|
|
1042
|
+
} else if (existing.list !== void 0) {
|
|
1043
|
+
existing.list.items.splice(existing.list.items.indexOf(existing.node), 1, node);
|
|
1044
|
+
} else {
|
|
1045
|
+
rowSeq(doc).items.splice(rowSeq(doc).items.indexOf(existing.node), 1, node);
|
|
1046
|
+
}
|
|
942
1047
|
savePatch(profileDirPath, doc);
|
|
943
1048
|
return id;
|
|
944
1049
|
}
|
|
945
1050
|
function setMcpDisabled(profileDirPath, id, disabled) {
|
|
946
1051
|
const doc = loadPatch(profileDirPath);
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
if (
|
|
950
|
-
|
|
1052
|
+
managedInsert(doc);
|
|
1053
|
+
const hit = mcpRowItems(doc).find(({ node }) => String(node.get("id") ?? "") === id);
|
|
1054
|
+
if (hit === void 0) return false;
|
|
1055
|
+
if (disabled) hit.node.set("disabled", true);
|
|
1056
|
+
else hit.node.delete("disabled");
|
|
951
1057
|
savePatch(profileDirPath, doc);
|
|
952
1058
|
return true;
|
|
953
1059
|
}
|
|
954
1060
|
function removeMcp(profileDirPath, id) {
|
|
955
1061
|
const doc = loadPatch(profileDirPath);
|
|
956
|
-
|
|
957
|
-
|
|
1062
|
+
managedInsert(doc);
|
|
1063
|
+
const hit = mcpRowItems(doc).find(({ node }) => String(node.get("id") ?? "") === id);
|
|
1064
|
+
if (hit === void 0 || hit.list === void 0) return false;
|
|
1065
|
+
hit.list.items.splice(hit.list.items.indexOf(hit.node), 1);
|
|
958
1066
|
const seq = rowSeq(doc);
|
|
959
|
-
|
|
1067
|
+
const owner = (seq.items ?? []).find((item) => insertListOf(item) === hit.list);
|
|
1068
|
+
if (owner !== void 0 && hit.list.items.length === 0 && owner.items.length === 1) {
|
|
1069
|
+
seq.items.splice(seq.items.indexOf(owner), 1);
|
|
1070
|
+
}
|
|
960
1071
|
savePatch(profileDirPath, doc);
|
|
961
1072
|
return true;
|
|
962
1073
|
}
|
|
@@ -1217,6 +1328,27 @@ function mountCapabilitiesRoutes(host, config) {
|
|
|
1217
1328
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
1218
1329
|
}
|
|
1219
1330
|
}
|
|
1331
|
+
}),
|
|
1332
|
+
host.webServer.register({
|
|
1333
|
+
kind: "exact",
|
|
1334
|
+
path: "/dsh-plugin-capabilities/restart",
|
|
1335
|
+
handler: (request, response) => {
|
|
1336
|
+
if (request.method !== "POST") {
|
|
1337
|
+
response.writeHead(405, { allow: "POST" });
|
|
1338
|
+
response.end();
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (!trustedRestartRequest(request)) {
|
|
1342
|
+
sendJson(response, 403, { error: "untrusted origin" });
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (restartOwnedByShell()) {
|
|
1346
|
+
sendJson(response, 409, { error: "restart is owned by the desktop shell" });
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const { pid, replacementPid, logOut } = scheduleRestart(dshLaunch());
|
|
1350
|
+
sendJson(response, 200, { ok: true, pid, replacementPid, logOut });
|
|
1351
|
+
}
|
|
1220
1352
|
})
|
|
1221
1353
|
];
|
|
1222
1354
|
return () => {
|