modwright 0.1.0 → 0.1.2

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/server.js CHANGED
@@ -7,6 +7,7 @@ import { describeProject, findProjectFile, loadProject } from "./core/project/in
7
7
  import { userDataDir } from "./core/home.js";
8
8
  import { applyDataFetch, applyManagedInstall, checkToolchain, featureReadiness, locateTool, planDataFetch, planDataMemory, planManagedInstall, planToolMemory, readinessSummary, TOOL_SPECS, toolsToLocate } from "./core/toolchain/index.js";
9
9
  import { loadUserConfig } from "./core/userconfig.js";
10
+ import { describeNeeds, isProjectTrusted, planTrust, projectTrustNeeds, trustRefusal } from "./core/trust.js";
10
11
  import { runCompat } from "./core/compat/check.js";
11
12
  import { SignatureRegistry, triageLogs } from "./core/logs/index.js";
12
13
  import { buildContext, createValidatorRegistry, runValidators } from "./core/validate/index.js";
@@ -16,7 +17,6 @@ import { armBridge, bridgeCandidateDirs, bridgeStatus, cetLogCandidates, listPen
16
17
  import { runTestPlan } from "./core/testplan/index.js";
17
18
  import { applyBuildLedgerHook, applyClaimTransition, applyDecisionRecord, applyValidateLedgerHook, checkLedger, claimsFilePath, listClaims, loadClaims, loadDecisions, saveClaims, saveDecisions, showClaim, } from "./core/ledger/index.js";
18
19
  import { IndexStore, defaultCacheDir, findLegacyIndex, indexFile, legacyIndexHint, loadKnowledge, resolveIndexFile, searchFacts, } from "./core/knowledge/index.js";
19
- import { pathExists as fileExists } from "./core/fsutil.js";
20
20
  import { executePlan, listBackups, restoreBackup } from "./core/safety/index.js";
21
21
  import { readTextIfExists } from "./core/fsutil.js";
22
22
  import * as path from "node:path";
@@ -368,9 +368,11 @@ server.registerTool("check_toolchain", {
368
368
  "with data=<id> fetches a data folder ModWright can fetch (Stardew Valley's wiki pages) and remembers it; " +
369
369
  "action 'remember' records a tool's path in the " +
370
370
  "per-user config so every session finds it (no environment variable, no restart); " +
371
- "'forget' drops it. Both are dry-run unless mode is 'apply'.",
371
+ "'forget' drops it. 'trust' records a project root (projectPath) as allowed to run its own toolchain overrides and " +
372
+ "exec build steps; until then an applied build, convert, template extract or index build that would run them is " +
373
+ "refused. 'untrust' removes it. All of these are dry-run unless mode is 'apply'.",
372
374
  inputSchema: {
373
- action: z.enum(["check", "remember", "forget", "install"]).default("check").describe("check (default) reports; remember/forget change the per-user config; install fetches a pinned tool into ModWright's tools folder (dry-run shows the file, source, size and sha256 first), or with data a fetchable data folder (dry-run lists every URL first)."),
375
+ action: z.enum(["check", "remember", "forget", "install", "trust", "untrust"]).default("check").describe("check (default) reports; remember/forget change the per-user config; trust/untrust change the trusted-projects list for projectPath; install fetches a pinned tool into ModWright's tools folder (dry-run shows the file, source, size and sha256 first), or with data a fetchable data folder (dry-run lists every URL first)."),
374
376
  game: gameArg.optional().describe("Restrict to tools used by this game (check only)."),
375
377
  projectPath: z
376
378
  .string()
@@ -380,9 +382,47 @@ server.registerTool("check_toolchain", {
380
382
  toolPath: z.string().optional().describe("remember: absolute path to the executable, or the folder that directly holds it."),
381
383
  data: z.string().optional().describe("remember/forget/install: a data folder id instead of a tool, e.g. 'valheim.jotunn' (a Jotunn checkout) or 'stardewvalley.wiki' (saved wiki pages); check_toolchain game=<id> lists the ones a game reads."),
382
384
  dataPath: z.string().optional().describe("remember with data: absolute path to that folder. install with data: where to save it (default: ModWright's per-user data folder)."),
383
- mode: z.enum(["dry-run", "apply"]).default("dry-run").describe("remember/forget: dry-run shows the config change; apply writes it (backing up the old file)."),
385
+ mode: z.enum(["dry-run", "apply"]).default("dry-run").describe("remember/forget/trust/untrust: dry-run shows the config change; apply writes it (backing up the old file)."),
384
386
  },
385
387
  }, async ({ action, game, projectPath, tool, toolPath, data, dataPath, mode }) => {
388
+ if (action === "trust" || action === "untrust") {
389
+ if (tool || toolPath || data || dataPath)
390
+ return fail(`${action} takes projectPath only.`);
391
+ if (!projectPath)
392
+ return fail(`${action} needs projectPath: the mod project's modwright.json or its folder.`);
393
+ let resolved;
394
+ try {
395
+ resolved = await resolveProject(projectPath);
396
+ }
397
+ catch (error) {
398
+ return fail(`Could not load project: ${error.message}`);
399
+ }
400
+ const planned = await planTrust(action, resolved.root, resolved.project.name);
401
+ if (!planned.ok)
402
+ return fail(planned.error);
403
+ const report = await executePlan(planned.plan, {
404
+ mode,
405
+ confidence: { level: "high", assumptions: ["The trusted-projects list is ModWright's own file; nothing in a game or a project is touched."] },
406
+ });
407
+ const needs = projectTrustNeeds(resolved.project);
408
+ return json({
409
+ action,
410
+ project: resolved.project.name,
411
+ root: planned.root,
412
+ mode,
413
+ applied: report.applied,
414
+ file: planned.file,
415
+ runs: { exec: needs.exec, toolchain: Object.fromEntries(needs.toolchain.map((id) => [id, resolved.project.toolchain?.[id]])) },
416
+ ...(planned.previous ? { previous: planned.previous } : {}),
417
+ operations: report.operations,
418
+ ...(report.backupDir ? { backupDir: report.backupDir } : {}),
419
+ note: report.applied
420
+ ? action === "trust"
421
+ ? `Trusted. Applied builds, converts, extracts and index builds may now run this project's exec steps and toolchain overrides.`
422
+ : `No longer trusted. Its exec steps and toolchain overrides are refused until it is trusted again.`
423
+ : `Dry run: nothing written. Read what it runs (runs) first; repeat with mode 'apply' to ${action} it.`,
424
+ });
425
+ }
386
426
  if ((action === "remember" || action === "forget") && data) {
387
427
  if (tool)
388
428
  return fail("Give either tool or data, not both.");
@@ -501,9 +541,12 @@ server.registerTool("check_toolchain", {
501
541
  }
502
542
  let projectOverrides;
503
543
  let project;
544
+ let projectRoot;
504
545
  if (projectPath) {
505
546
  try {
506
- project = (await loadProject(projectPath)).project;
547
+ const loaded = await loadProject(projectPath);
548
+ project = loaded.project;
549
+ projectRoot = loaded.root;
507
550
  projectOverrides = project.toolchain;
508
551
  }
509
552
  catch (error) {
@@ -521,11 +564,13 @@ server.registerTool("check_toolchain", {
521
564
  if (spec && !located.has(id))
522
565
  located.set(id, await locateTool(spec, { projectOverrides, probe: "trusted", userTools: userConfig.config.tools }));
523
566
  }
524
- const indexBuilt = surface.index ? await fileExists(resolveIndexFile(surface.id).file) : undefined;
567
+ const indexBuilt = surface.index ? await pathExists(resolveIndexFile(surface.id).file) : undefined;
525
568
  features = featureReadiness(surface, [...located.values()], { ...(project ? { project } : {}), ...(indexBuilt !== undefined ? { indexBuilt } : {}), userData: userConfig.config.data });
526
569
  }
570
+ const trusted = projectRoot ? await isProjectTrusted(projectRoot) : undefined;
527
571
  return json({
528
572
  game: surfaceId,
573
+ ...(projectRoot ? { project: { root: projectRoot, trusted, ...(trusted ? {} : { note: "Not trusted: an applied build, convert, extract or index build refuses to run its exec steps or toolchain overrides until check_toolchain action=trust." }) } } : {}),
529
574
  ...(features ? { summary: readinessSummary(features), features } : {}),
530
575
  found: tools.filter((t) => t.found).length,
531
576
  missing: tools.filter((t) => !t.found).map((t) => t.id),
@@ -686,7 +731,7 @@ function getStore(gameId, reopen = false) {
686
731
  legacyStores.set(gameId, legacy);
687
732
  else
688
733
  legacyStores.delete(gameId);
689
- pending = fileExists(file).then((exists) => (exists ? IndexStore.open(file) : undefined));
734
+ pending = pathExists(file).then((exists) => (exists ? IndexStore.open(file) : undefined));
690
735
  stores.set(gameId, pending);
691
736
  }
692
737
  return pending;
@@ -870,7 +915,12 @@ server.registerTool("build_index", {
870
915
  const file = (await pathExists(projectPath)) && projectPath.toLowerCase().endsWith(".json") ? projectPath : await findProjectFile(projectPath);
871
916
  if (!file)
872
917
  return fail(`No modwright.json found at or above ${projectPath}.`);
873
- toolOverrides = (await loadProject(file)).project.toolchain;
918
+ const loaded = await loadProject(file);
919
+ toolOverrides = loaded.project.toolchain;
920
+ const used = Object.keys(toolOverrides ?? {}).filter((id) => TOOL_SPECS.find((s) => s.id === id)?.usedBy.includes(surface.id));
921
+ if (used.length > 0 && !(await isProjectTrusted(loaded.root))) {
922
+ return fail(trustRefusal(loaded.root, `its modwright.json overrides the tool path(s) for ${used.join(", ")}`));
923
+ }
874
924
  }
875
925
  catch (error) {
876
926
  return fail(`Could not load project for tool overrides: ${error.message}`);
@@ -881,7 +931,7 @@ server.registerTool("build_index", {
881
931
  if (legacy) {
882
932
  return json({ game: surface.id, rebuilt: false, file: legacy.file, legacyIndex: legacy.file, note: `${legacyIndexHint(surface.id, legacy)} lookup already uses it. Pass force to build a fresh index in the new location instead.` });
883
933
  }
884
- if (!force && (await fileExists(file))) {
934
+ if (!force && (await pathExists(file))) {
885
935
  const existing = await surface.index.info(install);
886
936
  if (!existing.stale)
887
937
  return json({ ...existing, note: "Index already exists; pass force to rebuild." });
@@ -960,7 +1010,10 @@ server.registerTool("validate", {
960
1010
  });
961
1011
  const findings = includeInfo ? report.findings : report.findings.filter((finding) => finding.severity !== "info");
962
1012
  const blockingFiles = report.findings.filter((f) => f.severity === "block" && !f.suppressedBy).map((f) => f.file);
963
- const ledger = await applyValidateLedgerHook(resolved.root, blockingFiles, changedFiles, "validate");
1013
+ const narrowed = Boolean(ids?.length) || Boolean(groups?.length) || cheapOnly;
1014
+ const ledger = narrowed
1015
+ ? { skipped: true, applied: [], refused: [], reason: "a run narrowed by rule, groups or cheapOnly never promotes a claim; run the full set" }
1016
+ : await applyValidateLedgerHook(resolved.root, blockingFiles, changedFiles, "validate");
964
1017
  return json({
965
1018
  ...report,
966
1019
  findings: findings.slice(0, 200),
@@ -1025,7 +1078,13 @@ server.registerTool("build", {
1025
1078
  const ledger = mode === "apply" && !result.failed && result.manifestPath
1026
1079
  ? await applyBuildLedgerHook(resolved.root, result.manifestPath, "build")
1027
1080
  : { skipped: true, applied: [], refused: [] };
1028
- return json({ ...result, summary, ledger });
1081
+ let trust;
1082
+ if (mode === "dry-run") {
1083
+ const what = describeNeeds(projectTrustNeeds(resolved.project));
1084
+ if (what && !(await isProjectTrusted(resolved.root)))
1085
+ trust = { trusted: false, note: trustRefusal(resolved.root, what) };
1086
+ }
1087
+ return json({ ...result, summary, ledger, ...(trust ? { trust } : {}) });
1029
1088
  });
1030
1089
  const RELOAD_PATH_LIMIT = 200;
1031
1090
  function trimReload(reload) {
@@ -1113,13 +1172,19 @@ server.registerTool("rollback", {
1113
1172
  "purpose, entry count, how many of those entries are junction pointers rather than content, and total " +
1114
1173
  "size. With a backupId it restores that backup: dry-run by default, and every file's sha256 is re-checked " +
1115
1174
  "against the manifest immediately before it is written back. A junction is recreated as a junction, never " +
1116
- "as a copy of what it pointed at.",
1175
+ "as a copy of what it pointed at. A restore writes only under the project and the game's detected mod roots unless " +
1176
+ "allowOutsideRoots is set.",
1117
1177
  inputSchema: {
1118
1178
  path: z.string().optional().describe("modwright.json path or a directory to search from. Default: current directory."),
1119
1179
  backupId: z.string().optional().describe("The stamp naming one backup directory under .modwright/backups (from this tool's own listing). Omit to list."),
1120
1180
  mode: z.enum(["dry-run", "apply"]).default("dry-run").describe("dry-run reports what would be restored without writing a byte."),
1181
+ installPath: installPathArg,
1182
+ allowOutsideRoots: z
1183
+ .boolean()
1184
+ .default(false)
1185
+ .describe("Restore entries outside the project and the game's mod roots too. Off by default: a backup that names any other path is refused whole."),
1121
1186
  },
1122
- }, async ({ path: start, backupId, mode }) => {
1187
+ }, async ({ path: start, backupId, mode, installPath, allowOutsideRoots }) => {
1123
1188
  const resolved = await projectOrFail(start);
1124
1189
  if ("isError" in resolved)
1125
1190
  return resolved;
@@ -1141,17 +1206,30 @@ server.registerTool("rollback", {
1141
1206
  if (!(await pathExists(backupDir))) {
1142
1207
  return fail(`No backup "${backupId}" under ${backupRoot}. Call rollback with no backupId to list what is there.`);
1143
1208
  }
1209
+ let confineTo;
1210
+ let noInstall = false;
1211
+ if (!allowOutsideRoots) {
1212
+ const surface = getSurface(resolved.project.game);
1213
+ const install = surface ? await resolveInstall(surface, installPath).catch(() => undefined) : undefined;
1214
+ noInstall = !install;
1215
+ confineTo = [resolved.root, ...(surface && install ? surface.modRoots(install).map((r) => r.path) : [])];
1216
+ }
1144
1217
  try {
1145
- const report = await restoreBackup(backupDir, { mode });
1146
- return json({ ...report, backupId, backupDir });
1218
+ const report = await restoreBackup(backupDir, { mode, ...(confineTo ? { confineTo } : {}) });
1219
+ const operations = report.operations.map(({ op, ...rest }) => ({
1220
+ ...rest,
1221
+ op: "content" in op ? { kind: op.kind, path: op.path, bytes: rest.sizeBytes } : op,
1222
+ }));
1223
+ return json({ ...report, operations, backupId, backupDir });
1147
1224
  }
1148
1225
  catch (error) {
1149
- return fail(`rollback failed: ${error.message}`);
1226
+ const hint = noInstall ? " The game's install was not detected, so only the project counts; pass installPath to add its mod roots." : "";
1227
+ return fail(`rollback failed: ${error.message}${hint}`);
1150
1228
  }
1151
1229
  });
1152
1230
  server.registerTool("reload_plan", {
1153
1231
  title: "Classify what a set of deployed paths needs to take effect",
1154
- description: "Map deployed paths to a reload recommendation — hot, hot-degraded, save-reload, restart or unknown — " +
1232
+ description: "Map deployed paths to a reload recommendation — hot, hot-degraded, save-reload, restart or unknown (none when nothing was written) — " +
1155
1233
  "using the project's own reload.rules first and then the per-surface table, and reduce them to the single " +
1156
1234
  "most conservative answer with its caveats. An unmatched path is reported as unknown ('restart to be sure'), " +
1157
1235
  "never promoted. ModWright never launches the game and never sends a reload command; this is a " +
@@ -1566,15 +1644,24 @@ server.registerTool("convert", {
1566
1644
  },
1567
1645
  }, async ({ conversionId, source, dest, mode, verifyRoundTrip, path: projectPath }) => {
1568
1646
  let overrides;
1569
- if (projectPath) {
1647
+ let projectTrust;
1648
+ let backupRoot;
1649
+ const projectStart = projectPath ?? (path.isAbsolute(dest) ? (await findProjectFile(path.dirname(dest))) : undefined);
1650
+ if (projectStart) {
1570
1651
  try {
1571
- overrides = (await resolveProject(projectPath)).project.toolchain;
1652
+ const resolved = await resolveProject(projectStart);
1653
+ backupRoot = path.join(resolved.root, ".modwright", "backups");
1654
+ if (projectPath) {
1655
+ overrides = resolved.project.toolchain;
1656
+ if (overrides && Object.keys(overrides).length > 0)
1657
+ projectTrust = { root: resolved.root, trusted: await isProjectTrusted(resolved.root) };
1658
+ }
1572
1659
  }
1573
1660
  catch {
1574
1661
  }
1575
1662
  }
1576
1663
  try {
1577
- const report = await runConvert(conversionId, source, dest, { mode, verifyRoundTrip, locateTool: defaultTextLocateTool(overrides, mode === "apply" ? "all" : "none") });
1664
+ const report = await runConvert(conversionId, source, dest, { mode, verifyRoundTrip, locateTool: defaultTextLocateTool(overrides, mode === "apply" ? "all" : "none", projectTrust), ...(backupRoot ? { backupRoot } : {}) });
1578
1665
  return json(report);
1579
1666
  }
1580
1667
  catch (error) {
@@ -1667,7 +1754,9 @@ server.registerTool("template", {
1667
1754
  catch (error) {
1668
1755
  return fail(`Could not load project: ${error.message}`);
1669
1756
  }
1670
- const locateTool = defaultTextLocateTool(resolved.project.toolchain, action === "extract" && mode === "apply" ? "all" : action === "extract" ? "none" : "trusted");
1757
+ const overrides = resolved.project.toolchain;
1758
+ const projectTrust = action === "extract" && overrides && Object.keys(overrides).length > 0 ? { root: resolved.root, trusted: await isProjectTrusted(resolved.root) } : undefined;
1759
+ const locateTool = defaultTextLocateTool(overrides, action === "extract" && mode === "apply" ? "all" : action === "extract" ? "none" : "trusted", projectTrust);
1671
1760
  const templateGameVersion = async (surfaceId) => {
1672
1761
  const gameSurface = getSurface(surfaceId);
1673
1762
  if (!gameSurface)
@@ -210,11 +210,15 @@ const RULES = [
210
210
  requires: [],
211
211
  advisory: true,
212
212
  severity: "info",
213
- note: "RED4ext plugins are locked to the game build. After a game patch, wait for RED4ext and its " +
214
- "plugins to publish updated releases before assuming a mod is broken.",
213
+ note: "RED4ext plugins are tied to the game build they were compiled for. Before RED4ext 1.30.0 the " +
214
+ "loader itself refuses an unrecognized patch and no plugin loads; from 1.30.0 the loader runs on " +
215
+ "any build and skips each plugin pinned to another runtime (\"is incompatible with the current " +
216
+ "patch\" in red4ext.log), so a patch now breaks plugins one at a time. After a game patch, wait for " +
217
+ "RED4ext and its plugins to publish updated releases before assuming a mod is broken.",
215
218
  source: "https://wiki.redmodding.org/cyberpunk-2077-modding/for-mod-users/user-guide-troubleshooting " +
216
- "(\"prone to breaking with every update\"); RED4ext itself refuses to run against a game patch " +
217
- "it does not recognize (WopsS/RED4ext#33).",
219
+ "(\"prone to breaking with every update\"); RED4ext before 1.30.0 refuses a game patch it does " +
220
+ "not recognize (WopsS/RED4ext#33); 1.30.0 moved the check to each plugin's requested runtime " +
221
+ "(WopsS/RED4ext v1.30.0, src/dll/Systems/PluginSystem.cpp).",
218
222
  },
219
223
  ];
220
224
  export const cyberpunk2077Compat = {
@@ -137,8 +137,9 @@ const RED4EXT = [
137
137
  explain: "RED4ext refuses to run against this game build, so no native plugin loads at " +
138
138
  "all this launch. Everything downstream (TweakXL tweaks, ArchiveXL .xl files, " +
139
139
  "Codeware-dependent scripts) is dead, and none of them will say so.",
140
- hint: "Update RED4ext to a release that supports this build, or roll the game back " +
141
- "to the supported patch named on the following line.",
140
+ hint: "Update RED4ext (1.30.0 and later no longer refuse an unrecognized build; they " +
141
+ "check each plugin instead), or roll the game back to the supported patch named " +
142
+ "on the following line.",
142
143
  seenIn: [
143
144
  `${FIXTURES}/red4ext.synthetic.log`,
144
145
  "https://github.com/WopsS/RED4ext/issues/33",
@@ -222,7 +223,7 @@ const REDSCRIPT = [
222
223
  "or its data, not in compilation.",
223
224
  seenIn: [
224
225
  `${FIXTURES}/redscript_rCURRENT.ok.synthetic.log`,
225
- "https://wiki.redmodding.org/redscript/help/troubleshooting",
226
+ "https://wiki.redmodding.org/scripting-cyberpunk/redscript/help/troubleshooting",
226
227
  ],
227
228
  extract: ["output"],
228
229
  },
@@ -59,7 +59,8 @@ export const cyberpunk2077 = {
59
59
  extensions: [".archive", ".xl"],
60
60
  ordered: true,
61
61
  conflictScope: "opaque",
62
- notes: "Loaded alphabetically; later names win on identical resource paths. " +
62
+ notes: "Loaded in ASCII order of filename (modlist.txt overrides it when present); on identical " +
63
+ "resource paths the FIRST-loaded archive wins. " +
63
64
  "A .xl sidecar is ArchiveXL and must keep the same base name as its .archive.",
64
65
  },
65
66
  {
@@ -169,6 +169,10 @@ const UNIQUE_IDS_EVIDENCE = [
169
169
  "cp2077.sectors.sector-json-file-uniqueness",
170
170
  "cp2077.toolchain.cli-convert-deserialize",
171
171
  ];
172
+ function idSpace(jsonPath) {
173
+ const at = jsonPath.lastIndexOf(".Data.Chunks[");
174
+ return at === -1 ? "$" : jsonPath.slice(0, at);
175
+ }
172
176
  async function checkUniqueIds(text, root, file) {
173
177
  const findings = [];
174
178
  for (const key of ["HandleId", "BufferId"]) {
@@ -177,13 +181,15 @@ async function checkUniqueIds(text, root, file) {
177
181
  for (const hit of hits) {
178
182
  if (typeof hit.value !== "string")
179
183
  continue;
180
- const list = byValue.get(hit.value) ?? [];
184
+ const spaceKey = `${idSpace(hit.jsonPath)}\u0000${hit.value}`;
185
+ const list = byValue.get(spaceKey) ?? [];
181
186
  list.push(hit);
182
- byValue.set(hit.value, list);
187
+ byValue.set(spaceKey, list);
183
188
  }
184
- for (const [value, list] of byValue) {
189
+ for (const [spaceKey, list] of byValue) {
185
190
  if (list.length < 2)
186
191
  continue;
192
+ const value = spaceKey.slice(spaceKey.indexOf("\u0000") + 1);
187
193
  const lines = list.map((_, i) => lineOfOccurrence(text, `"${key}": "${value}"`, i)).filter((l) => l !== undefined);
188
194
  findings.push({
189
195
  validatorId: "cp2077.vehicles.cr2w-unique-ids",
@@ -117,7 +117,13 @@ export async function readModEngine2LoadOrder(install) {
117
117
  const configPath = path.join(gameDir(install), MODENGINE2_CONFIG);
118
118
  if (!(await pathExists(configPath)))
119
119
  return { configPath, entries: [] };
120
- const config = parseModEngine2Config(await fs.readFile(configPath, "utf8"));
120
+ let config;
121
+ try {
122
+ config = parseModEngine2Config(await fs.readFile(configPath, "utf8"));
123
+ }
124
+ catch (err) {
125
+ return { configPath, entries: [], error: `${configPath}: ${err.message}` };
126
+ }
121
127
  const modDir = path.resolve(modengine2ModDir(install)).toLowerCase();
122
128
  const entries = [];
123
129
  let index = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modwright",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A game-agnostic MCP server for game modding, with per-game surfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,6 +12,10 @@
12
12
  },
13
13
  "license": "MIT",
14
14
  "author": "Jonathan Trachtenberg",
15
+ "homepage": "https://github.com/jtrachtenberg/modwright-community#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/jtrachtenberg/modwright-community/issues"
18
+ },
15
19
  "keywords": [
16
20
  "mcp",
17
21
  "modding",
@@ -19,10 +19,13 @@ inputs:
19
19
  name: version
20
20
  prompt: Initial version (semver-ish, major.minor.revision).
21
21
  default: "0.1.0"
22
+ # Interpolated raw into modwright.json; the same guard the Stardew scaffold carries.
23
+ pattern: "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:\\.(?:0|[1-9][0-9]*))?(?:-(?:[a-zA-Z0-9]+[-.]?)+)?$"
22
24
  - type: string
23
25
  name: author
24
- prompt: Author name.
26
+ prompt: Author name (no double quotes or backslashes — it is written into modwright.json as-is).
25
27
  default: "Unknown"
28
+ pattern: '^[^"\\\x00-\x1f]+$'
26
29
  # A project can keep its
27
30
  # LSLib divine path as a toolchain override in modwright.json; a fresh
28
31
  # project on the same machine needs the same line or its first build
@@ -29,10 +29,13 @@ inputs:
29
29
  name: version
30
30
  prompt: Initial version (semver-ish, major.minor.revision).
31
31
  default: "0.1.0"
32
+ # Interpolated raw into modwright.json; the same guard the Stardew scaffold carries.
33
+ pattern: "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:\\.(?:0|[1-9][0-9]*))?(?:-(?:[a-zA-Z0-9]+[-.]?)+)?$"
32
34
  - type: string
33
35
  name: author
34
- prompt: Author name.
36
+ prompt: Author name (no double quotes or backslashes — it is written into modwright.json as-is).
35
37
  default: "Unknown"
38
+ pattern: '^[^"\\\x00-\x1f]+$'
36
39
  # Observed in practice (2026-09-12): the WolvenKit CLI override had to be
37
40
  # added to a fresh project by hand because the locator does not find a CLI
38
41
  # installed outside the usual places. The path is machine-specific and