@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/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 = resolve4.call(this, root, ref);
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 resolve4(root, ref) {
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 resolve4(baseURI, relativeURI, options2) {
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: resolve4,
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, resolve4) {
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, resolve4);
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, resolve4) : NONE;
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 ?? (resolve4 ? inst._zod.innerType : void 0);
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((resolve4) => setTimeout(resolve4, pollInterval));
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((resolve4, reject) => {
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
- resolve4(parseResult.data);
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((resolve4, reject) => {
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(resolve4, interval);
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((resolve4) => setTimeout(resolve4, pollInterval));
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((resolve4) => {
36584
+ return new Promise((resolve5) => {
36585
36585
  const json2 = serializeMessage(message);
36586
36586
  if (this._stdout.write(json2)) {
36587
- resolve4();
36587
+ resolve5();
36588
36588
  } else {
36589
- this._stdout.once("drain", resolve4);
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, method, gamePath) {
37173
+ function stillOurs(entry, _method, gamePath) {
37118
37174
  const id = identity(gameFile(gamePath, entry.path));
37119
37175
  if (!id) return "missing";
37120
- if (method === "link" && entry.ino) {
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 = manifest?.method ?? (canHardlink(lib.root, gamePath) ? "link" : "copy");
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 deploy(lib, gamePath, decisions = /* @__PURE__ */ new Map(), progress, gameVersion2) {
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
- const result = { linked: 0, removed: 0, kept: 0, backedUp: 0, method: manifest.method, skipped: [], changes: [], modsInGame: 0 };
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 = p.stale.length + p.add.length;
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
- for (const entry of p.stale) {
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
- progress?.(++done, total);
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
- continue;
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
- result.kept = p.desired.length - result.linked - result.skipped.length;
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 installProject(lib, projectFile) {
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
- const existing = lib.mods.find((m) => m.source.type === "project" && m.source.path?.toLowerCase() === projectFile.toLowerCase());
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
- async function deployRedmod(lib, gamePath, log) {
37667
- const packages = enabledRedmodPackages(lib);
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
- installProject(s, projectFile) {
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 { mod, added } = installProject(s.lib, projectFile);
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
- /** Deploys. A file changed outside EDGEdeck is skipped unless named in keep (import it) or restore (overwrite it). */
38001
- async deploy(s, keep = [], restore = []) {
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 redmod = enabledRedmodPackages(s.lib).length ? await deployRedmod(s.lib, s.game) : void 0;
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: { project: external_exports.string().describe("Full path of the .cpmodproj file") }
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: { keep: external_exports.array(external_exports.string()).optional().describe("Game-folder paths whose changed file to keep"), restore: external_exports.array(external_exports.string()).optional().describe("Game-folder paths to overwrite with the mod's file") },
38179
- annotations: { destructiveHint: true }
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.27.3",
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",