gdharness 1.2.30 → 1.2.31

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/index.js CHANGED
@@ -29053,6 +29053,9 @@ class GodotBridge extends EventEmitter {
29053
29053
  const info = this.connectionInfo;
29054
29054
  return this.socket?.readyState === import_websocket.default.OPEN && info !== null && (info.greeted !== true || info.identifying === true);
29055
29055
  }
29056
+ requestsInFlight() {
29057
+ return [...this.pendingRequests.values()].filter((pending) => pending.counted).map((pending) => ({ tool: pending.toolName, startedAt: pending.startedAt })).sort((a, b) => a.startedAt - b.startedAt);
29058
+ }
29056
29059
  getStatus() {
29057
29060
  const identifying = this.connectionInfo?.identifying === true;
29058
29061
  const editor = identifying ? null : this.connectionInfo;
@@ -29074,18 +29077,19 @@ class GodotBridge extends EventEmitter {
29074
29077
  syncsBreakpoints: editor?.syncsBreakpoints,
29075
29078
  turnedAway: this.turnedAway ?? undefined,
29076
29079
  identifying: identifying ? true : undefined,
29077
- pendingRequests: this.pendingRequests.size,
29080
+ pendingRequests: this.requestsInFlight().length,
29078
29081
  queuedResources: this.resourceQueues.size,
29079
29082
  listeningSince: this.listeningSince ?? undefined,
29080
29083
  portWanted: this.wantedPort !== 0 && this.boundPort !== null && this.boundPort !== this.wantedPort ? this.wantedPort : undefined
29081
29084
  };
29082
29085
  }
29083
- invokeTool(toolName, args) {
29086
+ invokeTool(toolName, args, options = {}) {
29087
+ const counted = options.counted ?? true;
29084
29088
  const resourceKey = this.getResourceKey(args);
29085
29089
  if (!resourceKey) {
29086
- return this.invokeToolDirect(toolName, args);
29090
+ return this.invokeToolDirect(toolName, args, counted);
29087
29091
  }
29088
- return this.enqueueResourceRequest(resourceKey, () => this.invokeToolDirect(toolName, args, resourceKey));
29092
+ return this.enqueueResourceRequest(resourceKey, () => this.invokeToolDirect(toolName, args, counted, resourceKey));
29089
29093
  }
29090
29094
  getRequestPathname(url) {
29091
29095
  try {
@@ -29237,7 +29241,7 @@ class GodotBridge extends EventEmitter {
29237
29241
  return;
29238
29242
  }
29239
29243
  }
29240
- invokeToolDirect(toolName, args, resourceKey) {
29244
+ invokeToolDirect(toolName, args, counted, resourceKey) {
29241
29245
  if (!this.isConnected()) {
29242
29246
  return Promise.reject(new Error("Godot is not connected"));
29243
29247
  }
@@ -29259,7 +29263,8 @@ class GodotBridge extends EventEmitter {
29259
29263
  resolve,
29260
29264
  reject,
29261
29265
  startedAt: Date.now(),
29262
- resourceKey
29266
+ resourceKey,
29267
+ counted
29263
29268
  });
29264
29269
  this.emitBridgeEvent("tool_start", {
29265
29270
  tool: toolName,
@@ -31352,11 +31357,26 @@ function shownDiagnostic(entry) {
31352
31357
  ...code === undefined || code === null || code === 0 || code === "" ? {} : { code }
31353
31358
  };
31354
31359
  }
31355
- function shownSymbol(entry) {
31360
+ function linesIn(content) {
31361
+ const lines = content.split(`
31362
+ `);
31363
+ return Math.max(1, content.endsWith(`
31364
+ `) ? lines.length - 1 : lines.length);
31365
+ }
31366
+ function lastLineOf(whole, lastOfDocument) {
31367
+ const end = shownPosition(whole["end"]);
31368
+ if (end === null) {
31369
+ return null;
31370
+ }
31371
+ const start = shownPosition(whole["start"]);
31372
+ const exclusive = end.column === 1 && (start === null || end.line > start.line);
31373
+ return Math.min(exclusive ? end.line - 1 : end.line, lastOfDocument);
31374
+ }
31375
+ function shownSymbol(entry, lastOfDocument) {
31356
31376
  const symbol = asRecord(entry);
31357
31377
  const whole = asRecord(symbol["range"] ?? asRecord(symbol["location"])["range"]);
31358
31378
  const at = shownPosition(asRecord(symbol["selectionRange"] ?? whole)["start"]);
31359
- const last = shownPosition(whole["end"]);
31379
+ const last = lastLineOf(whole, lastOfDocument);
31360
31380
  const children = Array.isArray(symbol["children"]) ? symbol["children"] : [];
31361
31381
  const { documentation, native_class: nativeClass, deprecated, detail } = symbol;
31362
31382
  return {
@@ -31364,11 +31384,11 @@ function shownSymbol(entry) {
31364
31384
  kind: named(symbol["kind"], SYMBOL_KINDS),
31365
31385
  ...typeof detail === "string" && detail !== "" ? { detail } : {},
31366
31386
  ...at === null ? {} : { line: at.line, column: at.column },
31367
- ...last === null ? {} : { lastLine: last.line },
31387
+ ...last === null ? {} : { lastLine: last },
31368
31388
  ...typeof documentation === "string" && documentation !== "" ? { documentation } : {},
31369
31389
  ...typeof nativeClass === "string" && nativeClass !== "" ? { nativeClass } : {},
31370
31390
  ...deprecated === true ? { deprecated } : {},
31371
- ...children.length > 0 ? { children: children.map(shownSymbol) } : {}
31391
+ ...children.length > 0 ? { children: children.map((child) => shownSymbol(child, lastOfDocument)) } : {}
31372
31392
  };
31373
31393
  }
31374
31394
  var MOST_COMPLETIONS = 100;
@@ -31473,7 +31493,7 @@ async function handleLSPTool(client, toolName, args) {
31473
31493
  }
31474
31494
  case "lsp_get_symbols": {
31475
31495
  const symbols = await client.getDocumentSymbols(scriptPath, content);
31476
- return asToolResponse({ symbols: symbols.map(shownSymbol) });
31496
+ return asToolResponse({ symbols: symbols.map((symbol) => shownSymbol(symbol, linesIn(content))) });
31477
31497
  }
31478
31498
  default:
31479
31499
  return asToolResponse({ error: `Unknown LSP tool: ${toolName}` });
@@ -34298,7 +34318,7 @@ var NODE_PATH = {
34298
34318
  };
34299
34319
  var PROPERTIES = {
34300
34320
  type: "object",
34301
- description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds.',
34321
+ description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y"; a component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds.',
34302
34322
  additionalProperties: true
34303
34323
  };
34304
34324
  var XY = {
@@ -34753,7 +34773,11 @@ var TOOL_SPECS = [
34753
34773
  requires: ["projectPath", "scenePath"],
34754
34774
  operations: {
34755
34775
  create: { effect: "destructive", summary: "a new scene with one root node", requires: [] },
34756
- save: { effect: "destructive", summary: "save the scene as it is in the editor", requires: [] },
34776
+ save: {
34777
+ effect: "destructive",
34778
+ summary: "save the scene as it is in the editor, or as the file loads when the editor does not have it open; from says which",
34779
+ requires: []
34780
+ },
34757
34781
  save_as: { effect: "destructive", summary: "save a copy under newPath", requires: ["newPath"] }
34758
34782
  },
34759
34783
  defaultOperation: "create"
@@ -34801,7 +34825,11 @@ var TOOL_SPECS = [
34801
34825
  requires: ["projectPath", "scenePath"],
34802
34826
  operations: {
34803
34827
  add: { effect: "additive", summary: "add a node of any class", requires: ["nodeType", "nodeName"] },
34804
- get: { effect: "read", summary: "read a node's properties", requires: ["nodePath"] },
34828
+ get: {
34829
+ effect: "read",
34830
+ summary: "read a node's properties, and a TileMapLayer's or TileMap's cells as set_tilemap_cells takes them, the first 1000 with cellCount for all",
34831
+ requires: ["nodePath"]
34832
+ },
34805
34833
  set: {
34806
34834
  effect: "destructive",
34807
34835
  summary: "set properties on a node",
@@ -34828,7 +34856,7 @@ var TOOL_SPECS = [
34828
34856
  {
34829
34857
  name: "scene_signal",
34830
34858
  title: "Scene signals",
34831
- description: "Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. Needs the editor connected.",
34859
+ description: "Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. Needs the editor connected.",
34832
34860
  parameters: {
34833
34861
  projectPath: PROJECT_PATH2,
34834
34862
  scenePath: SCENE_PATH,
@@ -38206,7 +38234,7 @@ class GodotServer {
38206
38234
  const { op: _op, ...arguments_ } = args;
38207
38235
  const bridge = async (command, extra = {}) => {
38208
38236
  const contained = this.containProjectFiles({ ...arguments_, ...extra });
38209
- return contained.ok ? await this.handleViaBridge(command, contained.value, tool) : contained.response;
38237
+ return contained.ok ? await this.handleViaBridge(command, contained.value, op === "" ? tool : `${tool} ${op}`) : contained.response;
38210
38238
  };
38211
38239
  switch (tool) {
38212
38240
  case "project_info":
@@ -38936,8 +38964,30 @@ class GodotServer {
38936
38964
  ]
38937
38965
  };
38938
38966
  }
38967
+ const dropped = await this.optionsTheImporterDropped(project.value.path, params, written.payload);
38968
+ if (dropped !== null) {
38969
+ return dropped;
38970
+ }
38939
38971
  return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
38940
38972
  }
38973
+ async optionsTheImporterDropped(projectPath, params, written) {
38974
+ const listed = written["updated_options"];
38975
+ const updated = Array.isArray(listed) ? listed.map(String) : [];
38976
+ const { options: _options, ...where } = params;
38977
+ const reread = await this.operation("get_import_options", where, projectPath);
38978
+ if (!reread.ok || updated.length === 0) {
38979
+ return null;
38980
+ }
38981
+ const held = reread.payload["params"];
38982
+ const kept = held !== null && typeof held === "object" ? Object.keys(held) : [];
38983
+ const gone = updated.filter((name) => !kept.includes(name));
38984
+ if (gone.length === 0) {
38985
+ return null;
38986
+ }
38987
+ const applied = updated.filter((name) => kept.includes(name));
38988
+ const named = gone.join(", ");
38989
+ return this.createErrorResponse(`${named} ${gone.length === 1 ? "is not an option" : "are not options"} of this resource's importer: the reimport left ${gone.length === 1 ? "it" : "them"} out of the import file, so ${gone.length === 1 ? "it changed" : "they changed"} nothing.${applied.length === 0 ? "" : ` ${applied.join(", ")} ${applied.length === 1 ? "was" : "were"} applied.`} Its options are: ${kept.join(", ")}.`, ["project_import options lists every option with the value it holds"]);
38990
+ }
38941
38991
  async reimport(projectPath, resourcePath, force, timeoutMs) {
38942
38992
  const ask = resourcePath === undefined ? { includeUpToDate: force } : { resourcePath };
38943
38993
  const before = await this.operation("get_import_status", ask, projectPath);
@@ -40037,7 +40087,7 @@ class GodotServer {
40037
40087
  }
40038
40088
  try {
40039
40089
  const askedAt = Date.now();
40040
- const asked = this.godotBridge.invokeTool("playing_status", {});
40090
+ const asked = this.godotBridge.invokeTool("playing_status", {}, { counted: false });
40041
40091
  const answer = await Promise.race([asked, delay4(PLAYING_STATUS_MS).then(() => null)]);
40042
40092
  if (answer === null) {
40043
40093
  this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
@@ -40062,7 +40112,7 @@ class GodotServer {
40062
40112
  }
40063
40113
  try {
40064
40114
  const answer = await Promise.race([
40065
- this.godotBridge.invokeTool("scan_status", {}),
40115
+ this.godotBridge.invokeTool("scan_status", {}, { counted: false }),
40066
40116
  delay4(PLAYING_STATUS_MS).then(() => null)
40067
40117
  ]);
40068
40118
  if (answer === null) {
@@ -40130,10 +40180,14 @@ class GodotServer {
40130
40180
  this.getEditorStatusPayload(),
40131
40181
  this.editorServersListening()
40132
40182
  ]);
40183
+ const answeredAt = Date.now();
40184
+ const inFlight = this.godotBridge.requestsInFlight().map((request) => ({ tool: request.tool, forMs: answeredAt - request.startedAt }));
40133
40185
  return this.jsonTextResponse({
40134
40186
  editor: {
40135
40187
  ...editorStatus,
40136
40188
  ...servers,
40189
+ pendingRequests: inFlight.length,
40190
+ pendingRequestTools: inFlight.length === 0 ? undefined : inFlight,
40137
40191
  debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort,
40138
40192
  scan
40139
40193
  },
@@ -42721,14 +42775,16 @@ class GodotServer {
42721
42775
  if (!(answered instanceof BridgeTimeout)) {
42722
42776
  return answered;
42723
42777
  }
42724
- return this.createErrorResponse(`The editor took ${toolName} in and did not answer within ${String(answered.waitedMs / 1000)}s. An editor importing files answers nothing until the import is over, and a hidden one imports on the thread that answers.`, [
42778
+ return this.createErrorResponse(`The editor took ${askedAs ?? toolName} in and did not answer within ${String(answered.waitedMs / 1000)}s. An editor importing files answers nothing until the import is over, and a hidden one imports on the thread that answers.`, [
42725
42779
  "editor_status says whether it is scanning or importing",
42726
42780
  "The editor still carries the request out once it is free, so read what it changed before asking again"
42727
42781
  ]);
42728
42782
  }
42729
42783
  async askViaBridge(toolName, args, askedAs) {
42784
+ const named = askedAs ?? toolName;
42730
42785
  if (!this.godotBridge.isConnected()) {
42731
- const alternative = askedAs !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, askedAs) ? EDITOR_FREE_INSTEAD[askedAs] : undefined;
42786
+ const tool = askedAs?.split(" ")[0];
42787
+ const alternative = tool !== undefined && Object.hasOwn(EDITOR_FREE_INSTEAD, tool) ? EDITOR_FREE_INSTEAD[tool] : undefined;
42732
42788
  const refused = await this.noEditorYet(args, alternative === undefined ? [] : [alternative]);
42733
42789
  if (refused !== null) {
42734
42790
  return refused;
@@ -42752,9 +42808,9 @@ class GodotServer {
42752
42808
  if (message.includes(`Unknown tool: ${toolName}`)) {
42753
42809
  const status = this.godotBridge.getStatus();
42754
42810
  const remedy = addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
42755
- return this.createErrorResponse(`The editor's gdharness addon is older than this ${SERVER_VERSION} server and has no ${toolName}, so it cannot answer this call. ${remedy ?? "Restart it with editor_launch restart to load the addon the project holds."}`, ["editor_status says which addon the editor holds"]);
42811
+ return this.createErrorResponse(`The editor's gdharness addon is older than this ${SERVER_VERSION} server and has no ${named}, so it cannot answer this call. ${remedy ?? "Restart it with editor_launch restart to load the addon the project holds."}`, ["editor_status says which addon the editor holds"]);
42756
42812
  }
42757
- return this.createErrorResponse(`The editor answered ${toolName} with an error: ${message}`);
42813
+ return this.createErrorResponse(`The editor answered ${named} with an error: ${message}`);
42758
42814
  }
42759
42815
  }
42760
42816
  runtimeTimeoutMs() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.30",
3
+ "version": "1.2.31",
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",