gdharness 0.13.9 → 0.13.11

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/build/cli.js CHANGED
@@ -14897,10 +14897,10 @@ function isValueComplete(value) {
14897
14897
  return depth <= 0 && !inString;
14898
14898
  }
14899
14899
  function settingKeys(content) {
14900
- const named = new Set;
14900
+ const named = new Map;
14901
14901
  for (const [section, entries] of Object.entries(parseProjectGodot(content))) {
14902
- for (const key of Object.keys(entries)) {
14903
- named.add(`${section}/${key}`);
14902
+ for (const [key, value] of Object.entries(entries)) {
14903
+ named.set(`${section}/${key}`, value);
14904
14904
  }
14905
14905
  }
14906
14906
  return named;
@@ -15787,7 +15787,10 @@ var init_tool_definitions = __esm(() => {
15787
15787
  summary: "run the project until it quits or is stopped",
15788
15788
  requires: ["projectPath"]
15789
15789
  },
15790
- stop: { summary: "end the run and answer with what it printed last", requires: [] },
15790
+ stop: {
15791
+ summary: "end the run and answer with what it printed last, naming the process ended under endedPid: a game that started processes of its own keeps them",
15792
+ requires: []
15793
+ },
15791
15794
  check: {
15792
15795
  summary: "boot headless, quit after a few frames, and report the verdict",
15793
15796
  requires: ["projectPath"]
@@ -36452,6 +36455,34 @@ function runningAs(pid) {
36452
36455
  return null;
36453
36456
  }
36454
36457
  }
36458
+ function listeningPid(port) {
36459
+ try {
36460
+ if (process.platform === "win32") {
36461
+ const said = execFileSync("powershell", [
36462
+ "-NoProfile",
36463
+ "-NonInteractive",
36464
+ "-Command",
36465
+ `(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
36466
+ ], { encoding: "utf8", timeout: 15000 }).trim();
36467
+ return /^\d+$/.test(said) ? Number(said) : null;
36468
+ }
36469
+ for (const [command, args, pattern] of [
36470
+ ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
36471
+ ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
36472
+ ]) {
36473
+ try {
36474
+ const said = execFileSync(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
36475
+ const found = pattern.exec(said);
36476
+ if (found?.[1] !== undefined) {
36477
+ return Number(found[1]);
36478
+ }
36479
+ } catch {}
36480
+ }
36481
+ return null;
36482
+ } catch {
36483
+ return null;
36484
+ }
36485
+ }
36455
36486
  function windowsCommandLine(pid) {
36456
36487
  try {
36457
36488
  const answer = execFileSync("powershell", [
@@ -37886,6 +37917,19 @@ class GodotServer {
37886
37917
  async handleDAP(toolName, args) {
37887
37918
  return handleDAPTool(this.dap(), toolName, args);
37888
37919
  }
37920
+ adapterBelongsElsewhere() {
37921
+ const status = this.godotBridge.getStatus();
37922
+ const ours = status.editorPid;
37923
+ if (ours === undefined) {
37924
+ return null;
37925
+ }
37926
+ const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
37927
+ const holder = listeningPid(port);
37928
+ if (holder === null || holder === ours) {
37929
+ return null;
37930
+ }
37931
+ return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
37932
+ }
37889
37933
  dap() {
37890
37934
  const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
37891
37935
  if (this.dapClient !== null && this.dapClient.port !== port) {
@@ -37989,12 +38033,12 @@ class GodotServer {
37989
38033
  }
37990
38034
  settingKeysOf(projectPath) {
37991
38035
  if (projectPath === undefined) {
37992
- return new Set;
38036
+ return new Map;
37993
38037
  }
37994
38038
  try {
37995
38039
  return settingKeys(readFileSync15(join17(projectPath, "project.godot"), "utf8"));
37996
38040
  } catch {
37997
- return new Set;
38041
+ return new Map;
37998
38042
  }
37999
38043
  }
38000
38044
  async handleRestartEditor() {
@@ -38026,7 +38070,7 @@ class GodotServer {
38026
38070
  }
38027
38071
  const now = this.godotBridge.getStatus();
38028
38072
  const after = this.settingKeysOf(before.projectPath);
38029
- const dropped = [...settingsBefore].filter((key) => !after.has(key));
38073
+ const dropped = [...settingsBefore].filter(([key]) => !after.has(key));
38030
38074
  return this.jsonTextResponse({
38031
38075
  restarted: true,
38032
38076
  editorPid: now.editorPid,
@@ -38034,8 +38078,8 @@ class GodotServer {
38034
38078
  serverVersion: SERVER_VERSION,
38035
38079
  addonIsStale: now.addonVersion !== SERVER_VERSION,
38036
38080
  staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
38037
- settingsDropped: dropped.length > 0 ? dropped : undefined,
38038
- settingsNote: dropped.length > 0 ? 'The editor saved project.godot on its way out and these keys are no longer in it. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, put them back: nothing else will say they have gone until something depends on one.' : undefined,
38081
+ settingsDropped: dropped.length > 0 ? dropped.map(([setting, was]) => ({ setting, was })) : undefined,
38082
+ settingsNote: dropped.length > 0 ? `The editor saved project.godot on its way out and these keys are no longer in it, with the value each one held. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save; it will be dropped again on the next restart. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, project_settings set puts one back: ${dropped.map(([setting, was]) => `setting "${setting}" value ${JSON.stringify(was)}`).join(", ")}. Nothing else will say they have gone until something depends on one.` : undefined,
38039
38083
  tookMs: Date.now() - began
38040
38084
  });
38041
38085
  }
@@ -38233,6 +38277,14 @@ class GodotServer {
38233
38277
  "GDHARNESS_DAP_PORT points this server at another one"
38234
38278
  ]);
38235
38279
  }
38280
+ const stranger = this.adapterBelongsElsewhere();
38281
+ if (stranger !== null) {
38282
+ return this.createErrorResponse(stranger, [
38283
+ "editor_status names the editor on the bridge and the port it says it serves",
38284
+ "GDHARNESS_DAP_PORT points this server at the right one",
38285
+ "Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
38286
+ ]);
38287
+ }
38236
38288
  const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
38237
38289
  if (answer.isError === true) {
38238
38290
  return answer;
@@ -38652,11 +38704,13 @@ class GodotServer {
38652
38704
  return this.jsonTextResponse({
38653
38705
  stopped: true,
38654
38706
  through: stopped.throughEditor ? "editor" : "gdharness",
38707
+ endedPid: stopped.pid,
38655
38708
  exitedBeforeStop: stopped.exitCode !== null,
38656
38709
  exitCode: stopped.exitCode,
38657
38710
  errors: stopped.log.count("error"),
38658
38711
  warnings: stopped.log.count("warning"),
38659
38712
  clean: stopped.log.count("error") === 0,
38713
+ note: stopped.throughEditor ? "The editor was asked to stop the scene it is playing. Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and is still running." : "The process named under endedPid was ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and is still running.",
38660
38714
  entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
38661
38715
  });
38662
38716
  }
package/build/index.js CHANGED
@@ -27558,10 +27558,10 @@ function isValueComplete(value) {
27558
27558
  return depth <= 0 && !inString;
27559
27559
  }
27560
27560
  function settingKeys(content) {
27561
- const named = new Set;
27561
+ const named = new Map;
27562
27562
  for (const [section, entries] of Object.entries(parseProjectGodot(content))) {
27563
- for (const key of Object.keys(entries)) {
27564
- named.add(`${section}/${key}`);
27563
+ for (const [key, value] of Object.entries(entries)) {
27564
+ named.set(`${section}/${key}`, value);
27565
27565
  }
27566
27566
  }
27567
27567
  return named;
@@ -28099,6 +28099,34 @@ function runningAs(pid) {
28099
28099
  return null;
28100
28100
  }
28101
28101
  }
28102
+ function listeningPid(port) {
28103
+ try {
28104
+ if (process.platform === "win32") {
28105
+ const said = execFileSync("powershell", [
28106
+ "-NoProfile",
28107
+ "-NonInteractive",
28108
+ "-Command",
28109
+ `(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
28110
+ ], { encoding: "utf8", timeout: 15000 }).trim();
28111
+ return /^\d+$/.test(said) ? Number(said) : null;
28112
+ }
28113
+ for (const [command, args, pattern] of [
28114
+ ["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
28115
+ ["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
28116
+ ]) {
28117
+ try {
28118
+ const said = execFileSync(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
28119
+ const found = pattern.exec(said);
28120
+ if (found?.[1] !== undefined) {
28121
+ return Number(found[1]);
28122
+ }
28123
+ } catch {}
28124
+ }
28125
+ return null;
28126
+ } catch {
28127
+ return null;
28128
+ }
28129
+ }
28102
28130
  function windowsCommandLine(pid) {
28103
28131
  try {
28104
28132
  const answer = execFileSync("powershell", [
@@ -28849,7 +28877,10 @@ var TOOL_SPECS = [
28849
28877
  summary: "run the project until it quits or is stopped",
28850
28878
  requires: ["projectPath"]
28851
28879
  },
28852
- stop: { summary: "end the run and answer with what it printed last", requires: [] },
28880
+ stop: {
28881
+ summary: "end the run and answer with what it printed last, naming the process ended under endedPid: a game that started processes of its own keeps them",
28882
+ requires: []
28883
+ },
28853
28884
  check: {
28854
28885
  summary: "boot headless, quit after a few frames, and report the verdict",
28855
28886
  requires: ["projectPath"]
@@ -30640,6 +30671,19 @@ class GodotServer {
30640
30671
  async handleDAP(toolName, args) {
30641
30672
  return handleDAPTool(this.dap(), toolName, args);
30642
30673
  }
30674
+ adapterBelongsElsewhere() {
30675
+ const status = this.godotBridge.getStatus();
30676
+ const ours = status.editorPid;
30677
+ if (ours === undefined) {
30678
+ return null;
30679
+ }
30680
+ const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
30681
+ const holder = listeningPid(port);
30682
+ if (holder === null || holder === ours) {
30683
+ return null;
30684
+ }
30685
+ return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
30686
+ }
30643
30687
  dap() {
30644
30688
  const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
30645
30689
  if (this.dapClient !== null && this.dapClient.port !== port) {
@@ -30743,12 +30787,12 @@ class GodotServer {
30743
30787
  }
30744
30788
  settingKeysOf(projectPath) {
30745
30789
  if (projectPath === undefined) {
30746
- return new Set;
30790
+ return new Map;
30747
30791
  }
30748
30792
  try {
30749
30793
  return settingKeys(readFileSync11(join11(projectPath, "project.godot"), "utf8"));
30750
30794
  } catch {
30751
- return new Set;
30795
+ return new Map;
30752
30796
  }
30753
30797
  }
30754
30798
  async handleRestartEditor() {
@@ -30780,7 +30824,7 @@ class GodotServer {
30780
30824
  }
30781
30825
  const now = this.godotBridge.getStatus();
30782
30826
  const after = this.settingKeysOf(before.projectPath);
30783
- const dropped = [...settingsBefore].filter((key) => !after.has(key));
30827
+ const dropped = [...settingsBefore].filter(([key]) => !after.has(key));
30784
30828
  return this.jsonTextResponse({
30785
30829
  restarted: true,
30786
30830
  editorPid: now.editorPid,
@@ -30788,8 +30832,8 @@ class GodotServer {
30788
30832
  serverVersion: SERVER_VERSION,
30789
30833
  addonIsStale: now.addonVersion !== SERVER_VERSION,
30790
30834
  staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
30791
- settingsDropped: dropped.length > 0 ? dropped : undefined,
30792
- settingsNote: dropped.length > 0 ? 'The editor saved project.godot on its way out and these keys are no longer in it. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, put them back: nothing else will say they have gone until something depends on one.' : undefined,
30835
+ settingsDropped: dropped.length > 0 ? dropped.map(([setting, was]) => ({ setting, was })) : undefined,
30836
+ settingsNote: dropped.length > 0 ? `The editor saved project.godot on its way out and these keys are no longer in it, with the value each one held. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save; it will be dropped again on the next restart. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, project_settings set puts one back: ${dropped.map(([setting, was]) => `setting "${setting}" value ${JSON.stringify(was)}`).join(", ")}. Nothing else will say they have gone until something depends on one.` : undefined,
30793
30837
  tookMs: Date.now() - began
30794
30838
  });
30795
30839
  }
@@ -30987,6 +31031,14 @@ class GodotServer {
30987
31031
  "GDHARNESS_DAP_PORT points this server at another one"
30988
31032
  ]);
30989
31033
  }
31034
+ const stranger = this.adapterBelongsElsewhere();
31035
+ if (stranger !== null) {
31036
+ return this.createErrorResponse(stranger, [
31037
+ "editor_status names the editor on the bridge and the port it says it serves",
31038
+ "GDHARNESS_DAP_PORT points this server at the right one",
31039
+ "Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
31040
+ ]);
31041
+ }
30990
31042
  const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
30991
31043
  if (answer.isError === true) {
30992
31044
  return answer;
@@ -31406,11 +31458,13 @@ class GodotServer {
31406
31458
  return this.jsonTextResponse({
31407
31459
  stopped: true,
31408
31460
  through: stopped.throughEditor ? "editor" : "gdharness",
31461
+ endedPid: stopped.pid,
31409
31462
  exitedBeforeStop: stopped.exitCode !== null,
31410
31463
  exitCode: stopped.exitCode,
31411
31464
  errors: stopped.log.count("error"),
31412
31465
  warnings: stopped.log.count("warning"),
31413
31466
  clean: stopped.log.count("error") === 0,
31467
+ note: stopped.throughEditor ? "The editor was asked to stop the scene it is playing. Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and is still running." : "The process named under endedPid was ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and is still running.",
31414
31468
  entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
31415
31469
  });
31416
31470
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.9",
3
+ "version": "0.13.11",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",