@edgemodders/edgedeck-mcp 0.27.3 → 0.29.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 +5 -4
- package/bin/edgedeck.mjs +10 -5
- package/dist/core.js +491 -143
- package/dist/mcp.js +256 -52
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -2984,7 +2984,7 @@ var require_compile = __commonJS({
|
|
|
2984
2984
|
const schOrFunc = root.refs[ref];
|
|
2985
2985
|
if (schOrFunc)
|
|
2986
2986
|
return schOrFunc;
|
|
2987
|
-
let _sch =
|
|
2987
|
+
let _sch = resolve5.call(this, root, ref);
|
|
2988
2988
|
if (_sch === void 0) {
|
|
2989
2989
|
const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
|
|
2990
2990
|
const { schemaId } = this.opts;
|
|
@@ -3011,7 +3011,7 @@ var require_compile = __commonJS({
|
|
|
3011
3011
|
function sameSchemaEnv(s1, s2) {
|
|
3012
3012
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3013
3013
|
}
|
|
3014
|
-
function
|
|
3014
|
+
function resolve5(root, ref) {
|
|
3015
3015
|
let sch;
|
|
3016
3016
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3017
3017
|
ref = sch;
|
|
@@ -3841,7 +3841,7 @@ var require_fast_uri = __commonJS({
|
|
|
3841
3841
|
}
|
|
3842
3842
|
return uri;
|
|
3843
3843
|
}
|
|
3844
|
-
function
|
|
3844
|
+
function resolve5(baseURI, relativeURI, options2) {
|
|
3845
3845
|
const schemelessOptions = options2 ? Object.assign({ scheme: "null" }, options2) : { scheme: "null" };
|
|
3846
3846
|
const {
|
|
3847
3847
|
parsed: baseParsed,
|
|
@@ -4210,7 +4210,7 @@ var require_fast_uri = __commonJS({
|
|
|
4210
4210
|
var fastUri = {
|
|
4211
4211
|
SCHEMES,
|
|
4212
4212
|
normalize,
|
|
4213
|
-
resolve:
|
|
4213
|
+
resolve: resolve5,
|
|
4214
4214
|
resolveComponent,
|
|
4215
4215
|
equal,
|
|
4216
4216
|
serialize,
|
|
@@ -15901,7 +15901,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
|
|
|
15901
15901
|
var NONE = 0;
|
|
15902
15902
|
var ASSUMED = 1;
|
|
15903
15903
|
var PROVEN = 2;
|
|
15904
|
-
function isRecursive(inst, stack,
|
|
15904
|
+
function isRecursive(inst, stack, resolve5) {
|
|
15905
15905
|
const cached2 = recursive.get(inst);
|
|
15906
15906
|
if (cached2 !== void 0)
|
|
15907
15907
|
return cached2 ? PROVEN : NONE;
|
|
@@ -15911,7 +15911,7 @@ function isRecursive(inst, stack, resolve4) {
|
|
|
15911
15911
|
let result = NONE;
|
|
15912
15912
|
const check2 = (child) => {
|
|
15913
15913
|
if (result !== PROVEN && child?._zod) {
|
|
15914
|
-
const answer = isRecursive(child, stack,
|
|
15914
|
+
const answer = isRecursive(child, stack, resolve5);
|
|
15915
15915
|
if (answer > result)
|
|
15916
15916
|
result = answer;
|
|
15917
15917
|
}
|
|
@@ -15922,7 +15922,7 @@ function isRecursive(inst, stack, resolve4) {
|
|
|
15922
15922
|
const desc = Object.getOwnPropertyDescriptor(sh, key2);
|
|
15923
15923
|
if (spread && !desc.enumerable)
|
|
15924
15924
|
continue;
|
|
15925
|
-
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack,
|
|
15925
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
|
|
15926
15926
|
if (child > answer)
|
|
15927
15927
|
answer = child;
|
|
15928
15928
|
}
|
|
@@ -15986,7 +15986,7 @@ function isRecursive(inst, stack, resolve4) {
|
|
|
15986
15986
|
break;
|
|
15987
15987
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
15988
15988
|
case "lazy": {
|
|
15989
|
-
const inner = def._cachedInner ?? (
|
|
15989
|
+
const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
|
|
15990
15990
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
15991
15991
|
break;
|
|
15992
15992
|
}
|
|
@@ -34424,7 +34424,7 @@ var Protocol = class {
|
|
|
34424
34424
|
return;
|
|
34425
34425
|
}
|
|
34426
34426
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
34427
|
-
await new Promise((
|
|
34427
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
|
|
34428
34428
|
options2?.signal?.throwIfAborted();
|
|
34429
34429
|
}
|
|
34430
34430
|
} catch (error62) {
|
|
@@ -34441,7 +34441,7 @@ var Protocol = class {
|
|
|
34441
34441
|
*/
|
|
34442
34442
|
request(request, resultSchema, options2) {
|
|
34443
34443
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options2 ?? {};
|
|
34444
|
-
return new Promise((
|
|
34444
|
+
return new Promise((resolve5, reject) => {
|
|
34445
34445
|
const earlyReject = (error62) => {
|
|
34446
34446
|
reject(error62);
|
|
34447
34447
|
};
|
|
@@ -34519,7 +34519,7 @@ var Protocol = class {
|
|
|
34519
34519
|
if (!parseResult.success) {
|
|
34520
34520
|
reject(parseResult.error);
|
|
34521
34521
|
} else {
|
|
34522
|
-
|
|
34522
|
+
resolve5(parseResult.data);
|
|
34523
34523
|
}
|
|
34524
34524
|
} catch (error62) {
|
|
34525
34525
|
reject(error62);
|
|
@@ -34781,12 +34781,12 @@ var Protocol = class {
|
|
|
34781
34781
|
}
|
|
34782
34782
|
} catch {
|
|
34783
34783
|
}
|
|
34784
|
-
return new Promise((
|
|
34784
|
+
return new Promise((resolve5, reject) => {
|
|
34785
34785
|
if (signal.aborted) {
|
|
34786
34786
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
34787
34787
|
return;
|
|
34788
34788
|
}
|
|
34789
|
-
const timeoutId = setTimeout(
|
|
34789
|
+
const timeoutId = setTimeout(resolve5, interval);
|
|
34790
34790
|
signal.addEventListener("abort", () => {
|
|
34791
34791
|
clearTimeout(timeoutId);
|
|
34792
34792
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -35917,7 +35917,7 @@ var McpServer = class {
|
|
|
35917
35917
|
let task = createTaskResult.task;
|
|
35918
35918
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
35919
35919
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
35920
|
-
await new Promise((
|
|
35920
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
|
|
35921
35921
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
35922
35922
|
if (!updatedTask) {
|
|
35923
35923
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -36581,12 +36581,12 @@ var StdioServerTransport = class {
|
|
|
36581
36581
|
this.onclose?.();
|
|
36582
36582
|
}
|
|
36583
36583
|
send(message) {
|
|
36584
|
-
return new Promise((
|
|
36584
|
+
return new Promise((resolve5) => {
|
|
36585
36585
|
const json2 = serializeMessage(message);
|
|
36586
36586
|
if (this._stdout.write(json2)) {
|
|
36587
|
-
|
|
36587
|
+
resolve5();
|
|
36588
36588
|
} else {
|
|
36589
|
-
this._stdout.once("drain",
|
|
36589
|
+
this._stdout.once("drain", resolve5);
|
|
36590
36590
|
}
|
|
36591
36591
|
});
|
|
36592
36592
|
}
|
|
@@ -36932,6 +36932,7 @@ function rednodePackageId(mod) {
|
|
|
36932
36932
|
return void 0;
|
|
36933
36933
|
}
|
|
36934
36934
|
function familyKey(mod) {
|
|
36935
|
+
if (mod.family) return mod.family;
|
|
36935
36936
|
if (mod.source.modId) return `nexus:${mod.source.modId}`;
|
|
36936
36937
|
const pkg = rednodePackageId(mod);
|
|
36937
36938
|
if (pkg) return `rednode:${pkg}`;
|
|
@@ -36963,10 +36964,25 @@ function families(lib) {
|
|
|
36963
36964
|
function familyOf(lib, mod) {
|
|
36964
36965
|
return families(lib).find((f) => f.key === familyKey(mod)) ?? { key: familyKey(mod), name: mod.name, nexusId: mod.source.modId, mods: [mod], active: mod, on: mod.enabled };
|
|
36965
36966
|
}
|
|
36967
|
+
function activateVersion(lib, chosen) {
|
|
36968
|
+
const fam = familyOf(lib, chosen);
|
|
36969
|
+
const previous = fam.active;
|
|
36970
|
+
const on = fam.on;
|
|
36971
|
+
if (previous.id !== chosen.id) placeAbove(lib, chosen, previous);
|
|
36972
|
+
for (const m of fam.mods) {
|
|
36973
|
+
const want = m.id === chosen.id ? on : false;
|
|
36974
|
+
if (m.enabled !== want) {
|
|
36975
|
+
m.enabled = want;
|
|
36976
|
+
saveMod(lib, m);
|
|
36977
|
+
}
|
|
36978
|
+
}
|
|
36979
|
+
return { previous, on };
|
|
36980
|
+
}
|
|
36966
36981
|
function settleNewVersion(lib, mod) {
|
|
36967
36982
|
const fam = familyOf(lib, mod);
|
|
36968
36983
|
const others = fam.mods.filter((m) => m.id !== mod.id);
|
|
36969
36984
|
if (!others.length) return {};
|
|
36985
|
+
takeFamilyName(lib, mod, others);
|
|
36970
36986
|
const previous = others.find((m) => m.enabled) ?? [...others].sort(newestFirst)[0];
|
|
36971
36987
|
if (mod.version && previous.version && versionNewer(previous.version, mod.version)) {
|
|
36972
36988
|
if (mod.enabled) {
|
|
@@ -36989,6 +37005,31 @@ function settleNewVersion(lib, mod) {
|
|
|
36989
37005
|
}
|
|
36990
37006
|
return { replaced: previous };
|
|
36991
37007
|
}
|
|
37008
|
+
function renameFamily(lib, mod, name) {
|
|
37009
|
+
const fam = familyOf(lib, mod);
|
|
37010
|
+
const to = name.trim();
|
|
37011
|
+
for (const m of fam.mods) {
|
|
37012
|
+
m.family = fam.key;
|
|
37013
|
+
if (!to) {
|
|
37014
|
+
if (m.renamedFrom !== void 0) m.name = m.renamedFrom;
|
|
37015
|
+
delete m.renamedFrom;
|
|
37016
|
+
} else if (m.name !== to) {
|
|
37017
|
+
m.renamedFrom ??= m.name;
|
|
37018
|
+
m.name = to;
|
|
37019
|
+
if (m.renamedFrom === to) delete m.renamedFrom;
|
|
37020
|
+
}
|
|
37021
|
+
saveMod(lib, m);
|
|
37022
|
+
}
|
|
37023
|
+
return familyOf(lib, mod);
|
|
37024
|
+
}
|
|
37025
|
+
function takeFamilyName(lib, mod, others) {
|
|
37026
|
+
const renamed = others.find((m) => m.renamedFrom !== void 0);
|
|
37027
|
+
if (!renamed || mod.name === renamed.name) return;
|
|
37028
|
+
mod.family = familyKey(renamed);
|
|
37029
|
+
mod.renamedFrom = mod.name;
|
|
37030
|
+
mod.name = renamed.name;
|
|
37031
|
+
saveMod(lib, mod);
|
|
37032
|
+
}
|
|
36992
37033
|
|
|
36993
37034
|
// src/deploy.ts
|
|
36994
37035
|
var fs4 = __toESM(require("node:fs"));
|
|
@@ -37107,6 +37148,21 @@ function canHardlink(libraryRoot, gamePath) {
|
|
|
37107
37148
|
return false;
|
|
37108
37149
|
}
|
|
37109
37150
|
}
|
|
37151
|
+
var hardlinkable = /* @__PURE__ */ new Map();
|
|
37152
|
+
function deployMethod(libraryRoot, gamePath, current) {
|
|
37153
|
+
try {
|
|
37154
|
+
if (fs4.statSync(libraryRoot).dev !== fs4.statSync(gamePath).dev) return "copy";
|
|
37155
|
+
} catch {
|
|
37156
|
+
return current ?? "copy";
|
|
37157
|
+
}
|
|
37158
|
+
if (current === "link") return "link";
|
|
37159
|
+
const k = `${path4.resolve(libraryRoot).toLowerCase()}|${path4.resolve(gamePath).toLowerCase()}`;
|
|
37160
|
+
if (!hardlinkable.has(k)) hardlinkable.set(k, canHardlink(libraryRoot, gamePath));
|
|
37161
|
+
return hardlinkable.get(k) ? "link" : "copy";
|
|
37162
|
+
}
|
|
37163
|
+
function otherMethod(entry, method) {
|
|
37164
|
+
return method === "link" !== !!entry.ino;
|
|
37165
|
+
}
|
|
37110
37166
|
function sourceReplaced(entry, method, d) {
|
|
37111
37167
|
if (d.content !== void 0) return false;
|
|
37112
37168
|
const src = identity(d.src);
|
|
@@ -37114,10 +37170,10 @@ function sourceReplaced(entry, method, d) {
|
|
|
37114
37170
|
if (method === "link" && entry.ino) return src.ino !== entry.ino;
|
|
37115
37171
|
return src.size !== entry.size || Math.abs(src.mtimeMs - entry.mtimeMs) >= 2e3;
|
|
37116
37172
|
}
|
|
37117
|
-
function stillOurs(entry,
|
|
37173
|
+
function stillOurs(entry, _method, gamePath) {
|
|
37118
37174
|
const id = identity(gameFile(gamePath, entry.path));
|
|
37119
37175
|
if (!id) return "missing";
|
|
37120
|
-
if (
|
|
37176
|
+
if (entry.ino) {
|
|
37121
37177
|
if (id.ino !== entry.ino) return "changed";
|
|
37122
37178
|
if (id.nlink < 2 && entry.mod !== Generated) return "sourceMissing";
|
|
37123
37179
|
return "ok";
|
|
@@ -37133,7 +37189,7 @@ function generatedDiffers(file2, content) {
|
|
|
37133
37189
|
}
|
|
37134
37190
|
function plan(lib, gamePath) {
|
|
37135
37191
|
const manifest = readManifest(gamePath);
|
|
37136
|
-
const method =
|
|
37192
|
+
const method = deployMethod(lib.root, gamePath, manifest?.method);
|
|
37137
37193
|
const desired = /* @__PURE__ */ new Map();
|
|
37138
37194
|
const conflicts = /* @__PURE__ */ new Map();
|
|
37139
37195
|
for (const mod of lib.mods) {
|
|
@@ -37164,6 +37220,7 @@ function plan(lib, gamePath) {
|
|
|
37164
37220
|
if (!e || (d.mod?.id ?? Generated) !== e.mod) return true;
|
|
37165
37221
|
if (d.content !== void 0 && generatedDiffers(gameFile(gamePath, d.rel), d.content)) return true;
|
|
37166
37222
|
if (stillOurs(e, method, gamePath) !== "ok") return true;
|
|
37223
|
+
if (d.content === void 0 && otherMethod(e, method)) return true;
|
|
37167
37224
|
return sourceReplaced(e, method, d);
|
|
37168
37225
|
});
|
|
37169
37226
|
return { method, desired: [...desired.values()], conflicts: [...conflicts.values()], stale, add, vortex: vortexManages(gamePath) };
|
|
@@ -37178,6 +37235,18 @@ function detectChanges(gamePath) {
|
|
|
37178
37235
|
}
|
|
37179
37236
|
return changes;
|
|
37180
37237
|
}
|
|
37238
|
+
function lostFiles(p) {
|
|
37239
|
+
const out = /* @__PURE__ */ new Map();
|
|
37240
|
+
for (const c of p.conflicts) {
|
|
37241
|
+
for (const loser of c.losers) {
|
|
37242
|
+
const k = `${loser}\0${c.winner}`;
|
|
37243
|
+
const l = out.get(k) ?? { mod: loser, files: 0, winner: c.winner };
|
|
37244
|
+
l.files++;
|
|
37245
|
+
out.set(k, l);
|
|
37246
|
+
}
|
|
37247
|
+
}
|
|
37248
|
+
return [...out.values()];
|
|
37249
|
+
}
|
|
37181
37250
|
function modsTouched(result) {
|
|
37182
37251
|
return [...new Set(result.changes.filter((c) => c.kind !== "removed" && c.mod !== Generated).map((c) => c.mod))];
|
|
37183
37252
|
}
|
|
@@ -37267,28 +37336,47 @@ function cleanDirs(gamePath, manifest) {
|
|
|
37267
37336
|
}
|
|
37268
37337
|
manifest.dirs = manifest.dirs.filter((d) => fs4.existsSync(path4.join(gamePath, ...d.split("/"))));
|
|
37269
37338
|
}
|
|
37270
|
-
function
|
|
37339
|
+
function modDeployPaths(gamePath, p, id) {
|
|
37340
|
+
const paths = new Set((readManifest(gamePath)?.files ?? []).filter((e) => e.mod === id).map((e) => key(e.path)));
|
|
37341
|
+
for (const d of p.desired) if (d.mod?.id === id) paths.add(key(d.rel));
|
|
37342
|
+
return paths;
|
|
37343
|
+
}
|
|
37344
|
+
function modlistFor(lib, files) {
|
|
37345
|
+
const rank = new Map(lib.mods.map((m, i) => [m.id, i]));
|
|
37346
|
+
const archives = files.filter((e) => e.mod !== Generated && e.path.toLowerCase().startsWith("archive/pc/mod/") && e.path.toLowerCase().endsWith(".archive")).sort((a, b) => (rank.get(a.mod) ?? 1e9) - (rank.get(b.mod) ?? 1e9) || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
37347
|
+
return archives.length ? archives.map((e) => path4.posix.basename(e.path)).join("\r\n") + "\r\n" : "";
|
|
37348
|
+
}
|
|
37349
|
+
function deploy(lib, gamePath, decisions = /* @__PURE__ */ new Map(), progress, gameVersion2, only) {
|
|
37271
37350
|
if (vortexManages(gamePath)) throw new Error("Vortex manages this game folder. Purge in Vortex first (Mods > Purge), then deploy with EDGEdeck.");
|
|
37272
37351
|
if (!isGameFolder(gamePath)) throw new Error(`${gamePath} does not look like the Cyberpunk 2077 folder (no bin\\x64\\Cyberpunk2077.exe).`);
|
|
37273
37352
|
const p = plan(lib, gamePath);
|
|
37274
37353
|
const manifest = readManifest(gamePath) ?? { version: 1, library: lib.root, gamePath, method: p.method, time: "", files: [], dirs: [] };
|
|
37275
37354
|
manifest.library = lib.root;
|
|
37276
|
-
|
|
37355
|
+
manifest.method = p.method;
|
|
37356
|
+
const result = { linked: 0, removed: 0, kept: 0, backedUp: 0, method: manifest.method, skipped: [], changes: [], modsInGame: 0, lost: lostFiles(p) };
|
|
37277
37357
|
const entries = new Map(manifest.files.map((e) => [key(e.path), e]));
|
|
37358
|
+
const scope = only ? modDeployPaths(gamePath, p, only) : void 0;
|
|
37359
|
+
const inScope = (rel) => !scope || scope.has(key(rel));
|
|
37360
|
+
const stale = p.stale.filter((e) => inScope(e.path) && !(scope && e.mod === Generated));
|
|
37361
|
+
const add = p.add.filter((d) => inScope(d.rel) && !(scope && d.content !== void 0));
|
|
37362
|
+
if (scope) {
|
|
37363
|
+
const waiting = new Set([...p.stale.map((e) => e.path), ...p.add.map((d) => d.rel)].filter((r) => !scope.has(key(r)) && key(r) !== key(ModListPath)).map(key));
|
|
37364
|
+
result.waiting = waiting.size;
|
|
37365
|
+
}
|
|
37278
37366
|
for (const [rel, decision] of decisions) {
|
|
37279
37367
|
const d = p.desired.find((x) => key(x.rel) === key(rel));
|
|
37280
37368
|
if (decision === "keep" && d?.mod) {
|
|
37281
37369
|
fs4.copyFileSync(gameFile(gamePath, rel), d.src);
|
|
37282
37370
|
}
|
|
37283
37371
|
}
|
|
37284
|
-
const total =
|
|
37372
|
+
const total = stale.length + add.length;
|
|
37285
37373
|
let done = 0;
|
|
37286
37374
|
const save = () => {
|
|
37287
37375
|
manifest.time = (/* @__PURE__ */ new Date()).toISOString();
|
|
37288
37376
|
if (gameVersion2) manifest.gameVersion = gameVersion2;
|
|
37289
37377
|
writeManifest(gamePath, manifest);
|
|
37290
37378
|
};
|
|
37291
|
-
|
|
37379
|
+
const takeOut = (entry) => {
|
|
37292
37380
|
const decided = decisions.get(entry.path);
|
|
37293
37381
|
if (removeDeployed(lib, gamePath, manifest, entry, decided !== void 0, result.skipped)) {
|
|
37294
37382
|
entries.delete(key(entry.path));
|
|
@@ -37296,10 +37384,8 @@ function deploy(lib, gamePath, decisions = /* @__PURE__ */ new Map(), progress,
|
|
|
37296
37384
|
result.changes.push({ path: entry.path, mod: entry.mod, kind: "removed", ...entry.backup ? { backup: true } : {} });
|
|
37297
37385
|
}
|
|
37298
37386
|
manifest.files = [...entries.values()];
|
|
37299
|
-
|
|
37300
|
-
|
|
37301
|
-
if (p.stale.length) cleanDirs(gamePath, manifest);
|
|
37302
|
-
for (const d of p.add) {
|
|
37387
|
+
};
|
|
37388
|
+
const putIn = (d) => {
|
|
37303
37389
|
const target = gameFile(gamePath, d.rel);
|
|
37304
37390
|
const existing = entries.get(key(d.rel));
|
|
37305
37391
|
let backup = existing?.backup ?? false;
|
|
@@ -37307,7 +37393,7 @@ function deploy(lib, gamePath, decisions = /* @__PURE__ */ new Map(), progress,
|
|
|
37307
37393
|
const state = stillOurs(existing, manifest.method, gamePath);
|
|
37308
37394
|
if (state === "changed" && !decisions.has(d.rel)) {
|
|
37309
37395
|
result.skipped.push(d.rel);
|
|
37310
|
-
|
|
37396
|
+
return;
|
|
37311
37397
|
}
|
|
37312
37398
|
fs4.rmSync(target, { force: true });
|
|
37313
37399
|
} else if (fs4.existsSync(target)) {
|
|
@@ -37333,10 +37419,29 @@ function deploy(lib, gamePath, decisions = /* @__PURE__ */ new Map(), progress,
|
|
|
37333
37419
|
manifest.files = [...entries.values()];
|
|
37334
37420
|
result.linked++;
|
|
37335
37421
|
result.changes.push({ path: d.rel, mod: entry.mod, kind: existing ? "replaced" : "added", ...backup && !existing ? { backup: true } : {} });
|
|
37422
|
+
};
|
|
37423
|
+
for (const entry of stale) {
|
|
37424
|
+
takeOut(entry);
|
|
37425
|
+
progress?.(++done, total);
|
|
37426
|
+
}
|
|
37427
|
+
if (stale.length) cleanDirs(gamePath, manifest);
|
|
37428
|
+
for (const d of add) {
|
|
37429
|
+
putIn(d);
|
|
37336
37430
|
progress?.(++done, total);
|
|
37337
37431
|
if (done % 200 === 0) save();
|
|
37338
37432
|
}
|
|
37339
|
-
|
|
37433
|
+
if (scope) {
|
|
37434
|
+
const content = modlistFor(lib, manifest.files);
|
|
37435
|
+
const e = entries.get(key(ModListPath));
|
|
37436
|
+
if (!content && e) {
|
|
37437
|
+
takeOut(e);
|
|
37438
|
+
cleanDirs(gamePath, manifest);
|
|
37439
|
+
} else if (content && (!e || generatedDiffers(gameFile(gamePath, ModListPath), content))) {
|
|
37440
|
+
putIn({ rel: ModListPath, mod: void 0, src: "", content });
|
|
37441
|
+
}
|
|
37442
|
+
}
|
|
37443
|
+
result.kept = (scope ? p.desired.filter((d) => scope.has(key(d.rel))).length : p.desired.length) - result.linked - result.skipped.length;
|
|
37444
|
+
if (scope) result.kept = Math.max(0, result.kept);
|
|
37340
37445
|
result.modsInGame = new Set(manifest.files.map((e) => e.mod).filter((m) => m !== Generated)).size;
|
|
37341
37446
|
save();
|
|
37342
37447
|
return result;
|
|
@@ -37374,6 +37479,7 @@ function pendingChanges(lib, gamePath, p) {
|
|
|
37374
37479
|
const st = stillOurs(e, p.method, gamePath);
|
|
37375
37480
|
if (st === "missing") out.push({ path: d.rel, mod: id, kind: "restore", detail: "missing from the game folder" });
|
|
37376
37481
|
else if (st === "changed") out.push({ path: d.rel, mod: id, kind: "restore", detail: "changed in the game folder; deploy asks whether to keep it" });
|
|
37482
|
+
else if (d.content === void 0 && otherMethod(e, p.method)) out.push({ path: d.rel, mod: id, kind: "relink", detail: p.method === "link" ? "the library is on the game's drive now: a hardlink instead of a copy" : "the library is on another drive now: a copy instead of a hardlink" });
|
|
37377
37483
|
else out.push({ path: d.rel, mod: id, kind: "relink", detail: d.content !== void 0 ? "the generated content changed (load order)" : "the library file was replaced (new version or rebuild)" });
|
|
37378
37484
|
}
|
|
37379
37485
|
}
|
|
@@ -37620,11 +37726,48 @@ function classifyProject(projectFile) {
|
|
|
37620
37726
|
if (!fs7.existsSync(packed)) return { placements: [], kinds: [], unplaced: [], ignored: [] };
|
|
37621
37727
|
return classify(packed, projectName(projectFile));
|
|
37622
37728
|
}
|
|
37623
|
-
function
|
|
37729
|
+
function projectMod(lib, projectFile) {
|
|
37730
|
+
return lib.mods.find((m) => m.source.type === "project" && m.source.path?.toLowerCase() === projectFile.toLowerCase());
|
|
37731
|
+
}
|
|
37732
|
+
function sameNameMod(lib, projectFile) {
|
|
37733
|
+
if (projectMod(lib, projectFile)) return void 0;
|
|
37734
|
+
const name = baseName(projectName(projectFile));
|
|
37735
|
+
const match = name ? lib.mods.find((m) => baseName(m.name) === name || m.renamedFrom !== void 0 && baseName(m.renamedFrom) === name) : void 0;
|
|
37736
|
+
return match && familyOf(lib, match).active;
|
|
37737
|
+
}
|
|
37738
|
+
function devVersion(lib, family) {
|
|
37739
|
+
const taken = new Set(lib.mods.filter((m) => familyKey(m) === family).map((m) => m.version.toLowerCase()));
|
|
37740
|
+
if (!taken.has("dev")) return "dev";
|
|
37741
|
+
let n = 2;
|
|
37742
|
+
while (taken.has(`dev ${n}`)) n++;
|
|
37743
|
+
return `dev ${n}`;
|
|
37744
|
+
}
|
|
37745
|
+
function installProject(lib, projectFile, beside) {
|
|
37624
37746
|
const c = classifyProject(projectFile);
|
|
37625
37747
|
if (!c.placements.length) throw new Error(`${projectName(projectFile)} has no packed output yet. Pack the project first (packed/ next to the project file).`);
|
|
37626
|
-
|
|
37748
|
+
let existing = projectMod(lib, projectFile);
|
|
37749
|
+
if (!existing && beside?.how === "follow") {
|
|
37750
|
+
existing = beside.mod;
|
|
37751
|
+
existing.family = familyKey(existing);
|
|
37752
|
+
existing.source = { type: "project", path: projectFile };
|
|
37753
|
+
}
|
|
37627
37754
|
if (!existing) {
|
|
37755
|
+
if (beside?.how === "version") {
|
|
37756
|
+
const family = familyKey(beside.mod);
|
|
37757
|
+
const previous = familyOf(lib, beside.mod);
|
|
37758
|
+
const mod2 = addMod(lib, c.placements, { name: projectName(projectFile), version: devVersion(lib, family), source: { type: "project", path: projectFile } });
|
|
37759
|
+
mod2.family = family;
|
|
37760
|
+
takeFamilyName(lib, mod2, previous.mods);
|
|
37761
|
+
placeAbove(lib, mod2, previous.active);
|
|
37762
|
+
for (const m of previous.mods) {
|
|
37763
|
+
if (m.enabled) {
|
|
37764
|
+
m.enabled = false;
|
|
37765
|
+
saveMod(lib, m);
|
|
37766
|
+
}
|
|
37767
|
+
}
|
|
37768
|
+
saveMod(lib, mod2);
|
|
37769
|
+
return { mod: mod2, added: true, beside };
|
|
37770
|
+
}
|
|
37628
37771
|
const mod = addMod(lib, c.placements, { name: projectName(projectFile), version: "dev", source: { type: "project", path: projectFile } });
|
|
37629
37772
|
return { mod, added: true };
|
|
37630
37773
|
}
|
|
@@ -37638,9 +37781,10 @@ function installProject(lib, projectFile) {
|
|
|
37638
37781
|
for (const f of existing.files) if (!keep.has(f)) fs7.rmSync(sourcePath(lib, existing, f), { force: true });
|
|
37639
37782
|
existing.files = [...keep].sort();
|
|
37640
37783
|
existing.kinds = c.kinds;
|
|
37784
|
+
activateVersion(lib, existing);
|
|
37641
37785
|
existing.enabled = true;
|
|
37642
37786
|
saveMod(lib, existing);
|
|
37643
|
-
return { mod: existing, added: false };
|
|
37787
|
+
return { mod: existing, added: false, ...beside ? { beside } : {} };
|
|
37644
37788
|
}
|
|
37645
37789
|
|
|
37646
37790
|
// src/redmod.ts
|
|
@@ -37663,8 +37807,18 @@ function enabledRedmodPackages(lib) {
|
|
|
37663
37807
|
}
|
|
37664
37808
|
return names;
|
|
37665
37809
|
}
|
|
37666
|
-
|
|
37667
|
-
const
|
|
37810
|
+
function deployedRedmodPackages(lib, gamePath) {
|
|
37811
|
+
const inGame = new Set((readManifest(gamePath)?.files ?? []).map((e) => /^mods\/([^/]+)\//i.exec(e.path)?.[1]).filter((n) => !!n));
|
|
37812
|
+
const names = [];
|
|
37813
|
+
for (const mod of lib.mods) {
|
|
37814
|
+
for (const f of mod.files) {
|
|
37815
|
+
const m = /^mods\/([^/]+)\//i.exec(f);
|
|
37816
|
+
if (m && inGame.has(m[1]) && !names.includes(m[1])) names.push(m[1]);
|
|
37817
|
+
}
|
|
37818
|
+
}
|
|
37819
|
+
return names;
|
|
37820
|
+
}
|
|
37821
|
+
async function deployRedmod(lib, gamePath, log, packages = enabledRedmodPackages(lib)) {
|
|
37668
37822
|
const exe = redModExe(gamePath);
|
|
37669
37823
|
if (!packages.length) return { ran: false, packages, output: "no REDmod packages enabled" };
|
|
37670
37824
|
if (!fs8.existsSync(exe)) return { ran: false, packages, output: `redMod.exe not found at ${exe}; the REDmod tools are a free DLC on the game's store page` };
|
|
@@ -37879,6 +38033,13 @@ function importFromVortex(lib, mods) {
|
|
|
37879
38033
|
}
|
|
37880
38034
|
|
|
37881
38035
|
// src/verbs.ts
|
|
38036
|
+
function lostProjectsText(lib, lost) {
|
|
38037
|
+
const lines = lost.flatMap((l) => {
|
|
38038
|
+
const mod = lib.mods.find((m) => m.id === l.mod);
|
|
38039
|
+
return mod?.source.type === "project" ? [`the game still has ${l.winner} for ${l.files} file(s), not the project ${l.mod} (move ${l.mod} up to let it win)`] : [];
|
|
38040
|
+
});
|
|
38041
|
+
return lines.length ? `; ${lines.join("; ")}` : "";
|
|
38042
|
+
}
|
|
37882
38043
|
var DefaultGame = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Cyberpunk 2077";
|
|
37883
38044
|
function openSession(opts) {
|
|
37884
38045
|
const libraryRoot = opts.library ?? defaultLibraryPath();
|
|
@@ -37973,12 +38134,20 @@ ${mod.files.join("\n")}`;
|
|
|
37973
38134
|
}
|
|
37974
38135
|
},
|
|
37975
38136
|
/** Adds a REDcode or WolvenKit project's packed output, or refreshes the mod that follows it, and turns it on. */
|
|
37976
|
-
|
|
38137
|
+
/**
|
|
38138
|
+
* `sameName` answers for a mod of the same name that does not follow this project: follow (it
|
|
38139
|
+
* follows this project from now on) or version (the project is another version of it, on in its
|
|
38140
|
+
* place). Without it, the project is a mod of its own and the text says what it sits beside.
|
|
38141
|
+
*/
|
|
38142
|
+
installProject(s, projectFile, sameName) {
|
|
37977
38143
|
if (!fs11.existsSync(projectFile)) throw new Error(`${projectFile} does not exist`);
|
|
37978
|
-
const
|
|
38144
|
+
const other = sameNameMod(s.lib, projectFile);
|
|
38145
|
+
const before = other && `${other.id}${other.source.type === "project" && other.source.path ? ` (${path11.dirname(other.source.path)})` : ""}`;
|
|
38146
|
+
const { mod, added, beside } = installProject(s.lib, projectFile, other && sameName ? { mod: other, how: sameName } : void 0);
|
|
38147
|
+
const note = beside?.how === "follow" ? `; ${before} follows this project now` : beside ? `; another version of ${before}, on in its place` : other ? `; note: the library also has ${before} under the same name, a separate mod; the one higher in the list wins the files they share (call again with sameName follow or version to settle it)` : "";
|
|
37979
38148
|
return {
|
|
37980
|
-
data: { id: mod.id, name: mod.name, version: mod.version, kinds: mod.kinds, files: mod.files.length, added },
|
|
37981
|
-
text: `${added ? "added" : "refreshed"} ${mod.id} from its project (${mod.files.length} files, ${mod.kinds.join(", ")}); it is on, deploy to put it in the game`
|
|
38149
|
+
data: { id: mod.id, name: mod.name, version: mod.version, kinds: mod.kinds, files: mod.files.length, added, sameName: other?.id, beside: beside?.how },
|
|
38150
|
+
text: `${added ? "added" : "refreshed"} ${mod.id} from its project (${mod.files.length} files, ${mod.kinds.join(", ")}); it is on, deploy to put it in the game${note}`
|
|
37982
38151
|
};
|
|
37983
38152
|
},
|
|
37984
38153
|
turn(s, id, on) {
|
|
@@ -37992,22 +38161,37 @@ ${mod.files.join("\n")}`;
|
|
|
37992
38161
|
moveMod(s.lib, mod, direction === "up" ? -1 : 1);
|
|
37993
38162
|
return { data: { order: s.lib.info.order }, text: s.lib.info.order.join("\n") };
|
|
37994
38163
|
},
|
|
38164
|
+
/** A new name in EDGEdeck for a mod and all its versions; an empty name puts the first name back. */
|
|
38165
|
+
rename(s, id, name) {
|
|
38166
|
+
const mod = find(s, id);
|
|
38167
|
+
const before = familyOf(s.lib, mod).name;
|
|
38168
|
+
const fam = renameFamily(s.lib, mod, name);
|
|
38169
|
+
return {
|
|
38170
|
+
data: { id: mod.id, name: fam.name, versions: fam.mods.map((m) => m.id) },
|
|
38171
|
+
text: `${before} is now ${fam.name}${fam.mods.length > 1 ? ` (${fam.mods.length} versions)` : ""}; its files and the game do not change`
|
|
38172
|
+
};
|
|
38173
|
+
},
|
|
37995
38174
|
remove(s, id) {
|
|
37996
38175
|
const mod = find(s, id);
|
|
37997
38176
|
removeMod(s.lib, mod);
|
|
37998
38177
|
return { data: { removed: mod.id }, text: `removed ${mod.id} from the library; its deployed files leave on the next deploy` };
|
|
37999
38178
|
},
|
|
38000
|
-
/**
|
|
38001
|
-
|
|
38179
|
+
/**
|
|
38180
|
+
* Deploys. A file changed outside EDGEdeck is skipped unless named in keep (import it) or restore
|
|
38181
|
+
* (overwrite it). With `only` (a mod id), deploys that mod's changes and leaves the rest waiting.
|
|
38182
|
+
*/
|
|
38183
|
+
async deploy(s, keep = [], restore = [], only) {
|
|
38002
38184
|
gameReady(s);
|
|
38003
38185
|
if (gameRunning()) throw new Error("Cyberpunk 2077 is running; it keeps mod files open. Quit the game, then deploy");
|
|
38186
|
+
if (only) find(s, only);
|
|
38004
38187
|
const decisions = /* @__PURE__ */ new Map();
|
|
38005
38188
|
for (const p of keep) decisions.set(p, "keep");
|
|
38006
38189
|
for (const p of restore) decisions.set(p, "restore");
|
|
38007
|
-
const result = deploy(s.lib, s.game, decisions, void 0, await gameVersion(s.game));
|
|
38008
|
-
const
|
|
38190
|
+
const result = deploy(s.lib, s.game, decisions, void 0, await gameVersion(s.game), only);
|
|
38191
|
+
const packages = only ? result.changes.some((c) => c.path.toLowerCase().startsWith("mods/")) ? deployedRedmodPackages(s.lib, s.game) : [] : enabledRedmodPackages(s.lib);
|
|
38192
|
+
const redmod = packages.length ? await deployRedmod(s.lib, s.game, void 0, packages) : void 0;
|
|
38009
38193
|
const report = writeDeployReport(s.lib, s.game, result);
|
|
38010
|
-
const text = `deployed (${result.method}): ${result.linked} added for ${modsTouched(result).length} mod(s), ${result.removed} removed, ${result.kept} unchanged; ${result.modsInGame} mod(s) in the game; report ${report}${result.skipped.length ? `; skipped (changed in game folder, answer with keep or restore): ${result.skipped.join(", ")}` : ""}${redmod ? `; REDmod: ${redmod.ran ? `${redmod.packages.length} prepared` : redmod.output}` : ""}`;
|
|
38194
|
+
const text = `deployed${only ? ` ${only} only` : ""} (${result.method}): ${result.linked} added for ${modsTouched(result).length} mod(s), ${result.removed} removed, ${result.kept} unchanged; ${result.modsInGame} mod(s) in the game; report ${report}${result.skipped.length ? `; skipped (changed in game folder, answer with keep or restore): ${result.skipped.join(", ")}` : ""}${redmod ? `; REDmod: ${redmod.ran ? `${redmod.packages.length} prepared` : redmod.output}` : ""}${result.waiting ? `; ${result.waiting} file(s) of other mods' changes still waiting` : ""}${lostProjectsText(s.lib, result.lost)}`;
|
|
38011
38195
|
return { data: { ...result, redmod, report }, text };
|
|
38012
38196
|
},
|
|
38013
38197
|
undo(s) {
|
|
@@ -38093,6 +38277,7 @@ ${JSON.stringify(v.data, null, 2)}` }] };
|
|
|
38093
38277
|
};
|
|
38094
38278
|
}
|
|
38095
38279
|
var session = () => openSession(options);
|
|
38280
|
+
var AskFirst = { "anthropic/requiresUserInteraction": true };
|
|
38096
38281
|
var server = new McpServer({ name: "edgedeck", version: version2 }, { instructions: Instructions });
|
|
38097
38282
|
server.registerTool(
|
|
38098
38283
|
"edgedeck_list",
|
|
@@ -38127,10 +38312,13 @@ server.registerTool(
|
|
|
38127
38312
|
"edgedeck_install_project",
|
|
38128
38313
|
{
|
|
38129
38314
|
title: "Install a project",
|
|
38130
|
-
description: "Takes a REDcode or WolvenKit project's packed/ folder into the library: adds it as version dev the first time, and on later calls replaces the files of the mod that follows the project. The mod is turned on; deploy to put it in the game. Pack the project first.",
|
|
38131
|
-
inputSchema: {
|
|
38315
|
+
description: "Takes a REDcode or WolvenKit project's packed/ folder into the library: adds it as version dev the first time, and on later calls replaces the files of the mod that follows the project. The mod is turned on; deploy to put it in the game. Pack the project first. When the library has a mod of the same name that does not follow this project (another project folder, or a download), the result says so; ask the player and call again with sameName.",
|
|
38316
|
+
inputSchema: {
|
|
38317
|
+
project: external_exports.string().describe("Full path of the .cpmodproj file"),
|
|
38318
|
+
sameName: external_exports.enum(["follow", "version"]).optional().describe("For a mod of the same name: follow = it follows this project from now on (no second mod); version = the project becomes another version of it, on in its place")
|
|
38319
|
+
}
|
|
38132
38320
|
},
|
|
38133
|
-
({ project }) => run4(() => verbs.installProject(session(), project))()
|
|
38321
|
+
({ project, sameName }) => run4(() => verbs.installProject(session(), project, sameName))()
|
|
38134
38322
|
);
|
|
38135
38323
|
server.registerTool(
|
|
38136
38324
|
"edgedeck_turn",
|
|
@@ -38151,13 +38339,23 @@ server.registerTool(
|
|
|
38151
38339
|
},
|
|
38152
38340
|
({ id, direction }) => run4(() => verbs.move(session(), id, direction))()
|
|
38153
38341
|
);
|
|
38342
|
+
server.registerTool(
|
|
38343
|
+
"edgedeck_rename",
|
|
38344
|
+
{
|
|
38345
|
+
title: "Rename a mod",
|
|
38346
|
+
description: "Gives a mod a new name in EDGEdeck, for all its versions; its files and the game do not change. New versions of it take the name too. An empty name puts the first name back.",
|
|
38347
|
+
inputSchema: { id: external_exports.string(), name: external_exports.string().describe("The new name, or empty for the first one") }
|
|
38348
|
+
},
|
|
38349
|
+
({ id, name }) => run4(() => verbs.rename(session(), id, name))()
|
|
38350
|
+
);
|
|
38154
38351
|
server.registerTool(
|
|
38155
38352
|
"edgedeck_remove",
|
|
38156
38353
|
{
|
|
38157
38354
|
title: "Remove a mod",
|
|
38158
38355
|
description: "Deletes a mod from the library. Its deployed files leave the game on the next deploy. The kept download, if any, stays.",
|
|
38159
38356
|
inputSchema: { id: external_exports.string() },
|
|
38160
|
-
annotations: { destructiveHint: true }
|
|
38357
|
+
annotations: { destructiveHint: true },
|
|
38358
|
+
_meta: AskFirst
|
|
38161
38359
|
},
|
|
38162
38360
|
({ id }) => run4(() => verbs.remove(session(), id))()
|
|
38163
38361
|
);
|
|
@@ -38175,17 +38373,23 @@ server.registerTool(
|
|
|
38175
38373
|
{
|
|
38176
38374
|
title: "Deploy",
|
|
38177
38375
|
description: "Hardlinks (or copies, on another drive) the files of every mod that is on into the game folder, writes the archive load order, prepares REDmod packages and records a manifest. A file changed in the game folder since the last deploy is skipped and named; call again with keep (import the changed file into the library) or restore (put the mod's file back). Refuses while Vortex manages the game folder.",
|
|
38178
|
-
inputSchema: {
|
|
38179
|
-
|
|
38376
|
+
inputSchema: {
|
|
38377
|
+
keep: external_exports.array(external_exports.string()).optional().describe("Game-folder paths whose changed file to keep"),
|
|
38378
|
+
restore: external_exports.array(external_exports.string()).optional().describe("Game-folder paths to overwrite with the mod's file"),
|
|
38379
|
+
mod: external_exports.string().optional().describe("A mod id: deploy only that mod's changes and leave other mods' changes waiting")
|
|
38380
|
+
},
|
|
38381
|
+
annotations: { destructiveHint: true },
|
|
38382
|
+
_meta: AskFirst
|
|
38180
38383
|
},
|
|
38181
|
-
({ keep, restore }) => run4(() => verbs.deploy(session(), keep, restore))()
|
|
38384
|
+
({ keep, restore, mod }) => run4(() => verbs.deploy(session(), keep, restore, mod))()
|
|
38182
38385
|
);
|
|
38183
38386
|
server.registerTool(
|
|
38184
38387
|
"edgedeck_undo",
|
|
38185
38388
|
{
|
|
38186
38389
|
title: "Undo deploy",
|
|
38187
38390
|
description: "Removes every file EDGEdeck deployed, and nothing else, and puts backed-up files back.",
|
|
38188
|
-
annotations: { destructiveHint: true }
|
|
38391
|
+
annotations: { destructiveHint: true },
|
|
38392
|
+
_meta: AskFirst
|
|
38189
38393
|
},
|
|
38190
38394
|
run4(() => verbs.undo(session()))
|
|
38191
38395
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edgemodders/edgedeck-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "EDGEdeck's MCP server and command line: manage Cyberpunk 2077 mods (add, turn on or off, order, deploy, undo, conflicts, loadouts) from an AI assistant or a script.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Edgemodders",
|