gdharness 0.6.8 → 0.6.9

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
@@ -15034,6 +15034,13 @@ function readParams(params, key) {
15034
15034
  }
15035
15035
 
15036
15036
  // src/tool-definitions.ts
15037
+ function opTakes(spec, op, name) {
15038
+ const ops = spec.parameters[name]?.ops;
15039
+ return ops === undefined || ops.includes(op);
15040
+ }
15041
+ function argumentsOf(spec, op) {
15042
+ return Object.keys(spec.parameters).filter((name) => opTakes(spec, op, name));
15043
+ }
15037
15044
  function toolSpec(name) {
15038
15045
  return SPECS_BY_NAME[name];
15039
15046
  }
@@ -15059,7 +15066,8 @@ function buildToolDefinitions() {
15059
15066
  };
15060
15067
  }
15061
15068
  for (const [name, schema] of Object.entries(spec.parameters)) {
15062
- properties[name] = schema;
15069
+ const { ops: _ops, ...carried } = schema;
15070
+ properties[name] = carried;
15063
15071
  }
15064
15072
  const required = [...spec.requires];
15065
15073
  if (spec.operations && !spec.defaultOperation) {
@@ -15749,35 +15757,46 @@ var init_tool_definitions = __esm(() => {
15749
15757
  projectPath: RUNNING_PROJECT_PATH,
15750
15758
  nodePath: {
15751
15759
  type: "string",
15760
+ ops: ["tree", "text", "find", "rect", "property"],
15752
15761
  description: "tree, find, text: where to start, default /root. rect: the node to place. property: the node to read."
15753
15762
  },
15754
15763
  property: {
15755
15764
  type: "string",
15765
+ ops: ["property", "find"],
15756
15766
  description: "property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label."
15757
15767
  },
15758
- depth: { type: "number", description: "tree: levels to descend. Default 3." },
15768
+ depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
15759
15769
  includeProperties: {
15760
15770
  type: "boolean",
15771
+ ops: ["tree"],
15761
15772
  description: "tree: include each node's properties. Default false."
15762
15773
  },
15763
15774
  className: {
15764
15775
  type: "string",
15776
+ ops: ["find"],
15765
15777
  description: "find: a native class, matching its subclasses too, or a class_name."
15766
15778
  },
15767
- script: { type: "string", description: "find: the script file the node carries." },
15779
+ script: { type: "string", ops: ["find"], description: "find: the script file the node carries." },
15768
15780
  namePattern: {
15769
15781
  type: "string",
15782
+ ops: ["find"],
15770
15783
  description: 'find: a case-insensitive glob on the node name, such as "Enemy*".'
15771
15784
  },
15772
- group: { type: "string", description: "find: a group the node is in." },
15773
- limit: { type: "number", description: "find: the most nodes to answer with. Default 100." },
15785
+ group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
15786
+ limit: {
15787
+ type: "number",
15788
+ ops: ["find", "text"],
15789
+ description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with truncated saying whether there were more."
15790
+ },
15774
15791
  includeHidden: {
15775
15792
  type: "boolean",
15793
+ ops: ["text"],
15776
15794
  description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
15777
15795
  },
15778
15796
  metrics: {
15779
15797
  type: "array",
15780
15798
  items: { type: "string" },
15799
+ ops: ["metrics"],
15781
15800
  description: "metrics: which to read. Default all."
15782
15801
  }
15783
15802
  },
@@ -15810,10 +15829,14 @@ var init_tool_definitions = __esm(() => {
15810
15829
  parameters: {
15811
15830
  projectPath: RUNNING_PROJECT_PATH,
15812
15831
  nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
15813
- property: { type: "string" },
15814
- value: { description: "set: the value, fitted to the property's type." },
15815
- method: { type: "string" },
15816
- args: { type: "array", description: "call: the arguments, fitted to the method's parameter types." }
15832
+ property: { type: "string", ops: ["set"] },
15833
+ value: { ops: ["set"], description: "set: the value, fitted to the property's type." },
15834
+ method: { type: "string", ops: ["call"] },
15835
+ args: {
15836
+ type: "array",
15837
+ ops: ["call"],
15838
+ description: "call: the arguments, fitted to the method's parameter types."
15839
+ }
15817
15840
  },
15818
15841
  requires: ["nodePath"],
15819
15842
  operations: {
@@ -15828,6 +15851,7 @@ var init_tool_definitions = __esm(() => {
15828
15851
  projectPath: RUNNING_PROJECT_PATH,
15829
15852
  viewportPath: {
15830
15853
  type: "string",
15854
+ ops: ["viewport"],
15831
15855
  description: "viewport: the Viewport node. Default the root viewport."
15832
15856
  },
15833
15857
  width: { type: "number", description: "Scale the image to this width." },
@@ -15847,38 +15871,64 @@ var init_tool_definitions = __esm(() => {
15847
15871
  projectPath: RUNNING_PROJECT_PATH,
15848
15872
  nodePath: {
15849
15873
  type: "string",
15874
+ ops: ["click", "choose"],
15850
15875
  description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
15851
15876
  },
15852
- action: { type: "string", description: "action: the InputMap action name." },
15877
+ action: { type: "string", ops: ["action"], description: "action: the InputMap action name." },
15853
15878
  pressed: {
15854
15879
  type: "boolean",
15855
- description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held."
15880
+ ops: ["action", "key", "mouse_click"],
15881
+ description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
15856
15882
  },
15857
- strength: { type: "number", description: "action: 0 to 1. Default 1." },
15883
+ strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
15858
15884
  keycode: {
15859
15885
  type: ["string", "number"],
15886
+ ops: ["key"],
15860
15887
  description: 'key: the key name, such as "Space" or "A", or its Godot keycode.'
15861
15888
  },
15862
15889
  text: {
15863
15890
  type: "string",
15891
+ ops: ["text", "choose"],
15864
15892
  description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
15865
15893
  },
15866
15894
  index: {
15867
15895
  type: "number",
15896
+ ops: ["choose"],
15868
15897
  description: "choose: the item to take, by where it is in the list, when text will not do."
15869
15898
  },
15870
- shift: { type: "boolean" },
15871
- ctrl: { type: "boolean" },
15872
- alt: { type: "boolean" },
15873
- x: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
15874
- y: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
15899
+ shift: { type: "boolean", ops: ["key"] },
15900
+ ctrl: { type: "boolean", ops: ["key"] },
15901
+ alt: { type: "boolean", ops: ["key"] },
15902
+ x: {
15903
+ type: "number",
15904
+ ops: ["mouse_click", "mouse_motion"],
15905
+ description: "mouse_click, mouse_motion: window pixels."
15906
+ },
15907
+ y: {
15908
+ type: "number",
15909
+ ops: ["mouse_click", "mouse_motion"],
15910
+ description: "mouse_click, mouse_motion: window pixels."
15911
+ },
15875
15912
  button: {
15876
15913
  type: ["string", "number"],
15914
+ ops: ["click", "mouse_click"],
15877
15915
  description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
15878
15916
  },
15879
- doubleClick: { type: "boolean", description: "click, mouse_click: default false." },
15880
- relativeX: { type: "number", description: "mouse_motion: movement since the last event." },
15881
- relativeY: { type: "number", description: "mouse_motion: movement since the last event." }
15917
+ doubleClick: {
15918
+ type: "boolean",
15919
+ ops: ["click", "mouse_click"],
15920
+ description: "click, mouse_click: default false."
15921
+ },
15922
+ relativeX: {
15923
+ type: "number",
15924
+ ops: ["mouse_motion"],
15925
+ description: "mouse_motion: movement since the last event."
15926
+ },
15927
+ relativeY: {
15928
+ type: "number",
15929
+ ops: ["mouse_motion"],
15930
+ description: "mouse_motion: movement since the last event."
15931
+ }
15882
15932
  },
15883
15933
  requires: [],
15884
15934
  operations: {
@@ -15907,14 +15957,16 @@ var init_tool_definitions = __esm(() => {
15907
15957
  projectPath: RUNNING_PROJECT_PATH,
15908
15958
  frames: {
15909
15959
  type: "number",
15960
+ ops: ["frames"],
15910
15961
  description: "frames: how many to let pass, 1 to 600. More than that is refused."
15911
15962
  },
15912
- nodePath: { type: "string", description: "signal, until: the node." },
15913
- signal: { type: "string", description: "signal: the signal name." },
15914
- property: { type: "string", description: "until: the property name." },
15915
- value: { description: "until: the value to wait for, fitted to the property's type." },
15963
+ nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
15964
+ signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
15965
+ property: { type: "string", ops: ["until"], description: "until: the property name." },
15966
+ value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
15916
15967
  timeoutMs: {
15917
15968
  type: "number",
15969
+ ops: ["signal", "until"],
15918
15970
  description: "signal, until: how long to wait before answering anyway, 1 to 120000. Default 5000."
15919
15971
  }
15920
15972
  },
@@ -15962,6 +16014,7 @@ var init_tool_definitions = __esm(() => {
15962
16014
  parameters: {
15963
16015
  frameId: {
15964
16016
  type: "number",
16017
+ ops: ["variables"],
15965
16018
  description: "variables: which frame, from a stack answer. Default the innermost."
15966
16019
  }
15967
16020
  },
@@ -36265,6 +36318,15 @@ class GodotServer {
36265
36318
  };
36266
36319
  }
36267
36320
  }
36321
+ if (op !== null) {
36322
+ const elsewhere = Object.keys(args).filter((key) => key !== "op" && !opTakes(spec, op, key));
36323
+ if (elsewhere.length > 0) {
36324
+ return {
36325
+ ok: false,
36326
+ response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${op} takes: ${argumentsOf(spec, op).join(", ")}.`)
36327
+ };
36328
+ }
36329
+ }
36268
36330
  const required = [...spec.requires, ...op !== null ? spec.operations?.[op]?.requires ?? [] : []];
36269
36331
  const missing = required.filter((field) => {
36270
36332
  const value = Object.hasOwn(args, field) ? args[field] : undefined;
@@ -36389,7 +36451,8 @@ class GodotServer {
36389
36451
  return await this.handleRuntimeCommand("read_text", {
36390
36452
  projectPath: args["projectPath"],
36391
36453
  root: readNonEmptyString(args, "nodePath") ?? "/root",
36392
- include_hidden: readBoolean(args, "includeHidden") ?? false
36454
+ include_hidden: readBoolean(args, "includeHidden") ?? false,
36455
+ limit: readPositiveNumber(args, "limit") ?? 500
36393
36456
  });
36394
36457
  case "rect":
36395
36458
  return await this.handleRuntimeCommand("get_rect", {
@@ -37431,31 +37494,26 @@ class GodotServer {
37431
37494
  });
37432
37495
  }
37433
37496
  async handleRuntimeWait(op, args) {
37497
+ if (op === "frames") {
37498
+ const frames = readPositiveNumber(args, "frames") ?? 1;
37499
+ const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
37500
+ return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
37501
+ }
37434
37502
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
37435
37503
  const nodePath = readNonEmptyString(args, "nodePath") ?? "";
37436
37504
  const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
37437
- switch (op) {
37438
- case "frames": {
37439
- const frames = readPositiveNumber(args, "frames") ?? 1;
37440
- const waited = patienceForFrames(frames, patience);
37441
- return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
37442
- }
37443
- case "signal":
37444
- return await this.handleRuntimeCommand("wait_signal", {
37445
- projectPath: args["projectPath"],
37446
- path: nodePath,
37447
- signal: readString(args, "signal") ?? "",
37448
- timeout_ms: timeoutMs
37449
- }, patience);
37450
- default:
37451
- return await this.handleRuntimeCommand("wait_until", {
37452
- projectPath: args["projectPath"],
37453
- path: nodePath,
37454
- property: readString(args, "property") ?? "",
37455
- value: args["value"],
37456
- timeout_ms: timeoutMs
37457
- }, patience);
37458
- }
37505
+ return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
37506
+ projectPath: args["projectPath"],
37507
+ path: nodePath,
37508
+ signal: readString(args, "signal") ?? "",
37509
+ timeout_ms: timeoutMs
37510
+ }, patience) : await this.handleRuntimeCommand("wait_until", {
37511
+ projectPath: args["projectPath"],
37512
+ path: nodePath,
37513
+ property: readString(args, "property") ?? "",
37514
+ value: args["value"],
37515
+ timeout_ms: timeoutMs
37516
+ }, patience);
37459
37517
  }
37460
37518
  }
37461
37519
  async function runGodotServer() {
@@ -38766,7 +38824,9 @@ function renderToolsMarkdown() {
38766
38824
  const needs = [...tool.requires, ...spec.requires];
38767
38825
  const isDefault = tool.defaultOperation === op ? " (default)" : "";
38768
38826
  const wants = needs.length > 0 ? ` Needs: ${needs.map((name) => `\`${name}\``).join(", ")}.` : "";
38769
- lines.push(`- \`op: ${op}\`${isDefault}: ${spec.summary}.${wants}`);
38827
+ const spare = argumentsOf(tool, op).filter((name) => !needs.includes(name) && name !== "projectPath");
38828
+ const takes = spare.length > 0 ? ` Takes: ${spare.map((name) => `\`${name}\``).join(", ")}.` : "";
38829
+ lines.push(`- \`op: ${op}\`${isDefault}: ${spec.summary}.${wants}${takes}`);
38770
38830
  }
38771
38831
  lines.push("");
38772
38832
  } else if (tool.requires.length > 0) {
@@ -10,8 +10,8 @@ const Values = preload("runtime_values.gd")
10
10
  const FIND_LIMIT: int = 100
11
11
  const FIND_LIMIT_CEILING: int = 1000
12
12
 
13
- ## The most lines one read answers with. A screen is a few dozen; a thousand is a tree somebody
14
- ## pointed this at by mistake.
13
+ ## The most lines one read answers with, unless asked for fewer. A screen is a few dozen; a
14
+ ## thousand is a tree somebody pointed this at by mistake.
15
15
  const READ_LIMIT: int = 500
16
16
 
17
17
  var _host: Node
@@ -153,22 +153,32 @@ func _matches(
153
153
  ## A hidden node is left out and so is everything under it, because what a player reads is what is
154
154
  ## drawn. [param include_hidden] asks for the lot instead, which is what a caller checking that
155
155
  ## something is not showing wants.
156
+ ##
157
+ ## [param limit] is the first few lines rather than all of them, which is how the top of a screen
158
+ ## is read without the hall under it: the bar along the top of a guild is eleven lines and the
159
+ ## panel it sits on is two hundred.
156
160
  func read_text(params: Dictionary) -> Dictionary:
157
161
  var root_path: String = str(params.get("root", "/root"))
158
162
  var include_hidden: bool = bool(params.get("include_hidden", false))
163
+ var limit: int = clampi(int(params.get("limit", READ_LIMIT)), 1, READ_LIMIT)
159
164
 
160
165
  var root: Node = _host.get_tree().root.get_node_or_null(root_path)
161
166
  if root == null:
162
167
  return {"type": "error", "message": "Node not found: " + root_path}
163
168
 
164
- var lines: PackedStringArray = PackedStringArray()
165
- _read_into(root, include_hidden, lines)
169
+ # One line further than asked for, so that whether anything was left behind is read off the
170
+ # walk rather than guessed at from the count: a panel of exactly as many lines as the caller
171
+ # asked for is one they have read all of, and saying otherwise sends them back for nothing.
172
+ var read: PackedStringArray = PackedStringArray()
173
+ _read_into(root, include_hidden, limit + 1, read)
174
+ var more: bool = read.size() > limit
175
+ var lines: PackedStringArray = read.slice(0, limit) if more else read
166
176
  return {
167
177
  "type": "text",
168
178
  "root": root_path,
169
179
  "lines": lines,
170
180
  "count": lines.size(),
171
- "truncated": lines.size() >= READ_LIMIT,
181
+ "truncated": more,
172
182
  }
173
183
 
174
184
 
@@ -180,8 +190,8 @@ func read_text(params: Dictionary) -> Dictionary:
180
190
  ## screen full of forms answered with every label on it and none of the values in it. The hidden
181
191
  ## check covers a [Window] for the same walk: a dropdown's popup is a child that is not drawn
182
192
  ## until it is opened, and reading a closed menu would put every item on the screen.
183
- func _read_into(node: Node, include_hidden: bool, into: PackedStringArray) -> void:
184
- if into.size() >= READ_LIMIT:
193
+ func _read_into(node: Node, include_hidden: bool, most: int, into: PackedStringArray) -> void:
194
+ if into.size() >= most:
185
195
  return
186
196
  if not include_hidden and not _drawn(node):
187
197
  return
@@ -189,7 +199,7 @@ func _read_into(node: Node, include_hidden: bool, into: PackedStringArray) -> vo
189
199
  if not said.is_empty():
190
200
  into.append(said)
191
201
  for child: Node in node.get_children(true):
192
- _read_into(child, include_hidden, into)
202
+ _read_into(child, include_hidden, most, into)
193
203
 
194
204
 
195
205
  ## Whether [param node] is on the screen at all, for the two kinds of thing that can be hidden.
package/build/index.js CHANGED
@@ -27633,6 +27633,13 @@ function installedAddonVersion(projectPath) {
27633
27633
  }
27634
27634
 
27635
27635
  // src/tool-definitions.ts
27636
+ function opTakes(spec, op, name) {
27637
+ const ops = spec.parameters[name]?.ops;
27638
+ return ops === undefined || ops.includes(op);
27639
+ }
27640
+ function argumentsOf(spec, op) {
27641
+ return Object.keys(spec.parameters).filter((name) => opTakes(spec, op, name));
27642
+ }
27636
27643
  var PROJECT_PATH = {
27637
27644
  type: "string",
27638
27645
  description: "Absolute path to the project directory, the one holding project.godot."
@@ -28303,35 +28310,46 @@ var TOOL_SPECS = [
28303
28310
  projectPath: RUNNING_PROJECT_PATH,
28304
28311
  nodePath: {
28305
28312
  type: "string",
28313
+ ops: ["tree", "text", "find", "rect", "property"],
28306
28314
  description: "tree, find, text: where to start, default /root. rect: the node to place. property: the node to read."
28307
28315
  },
28308
28316
  property: {
28309
28317
  type: "string",
28318
+ ops: ["property", "find"],
28310
28319
  description: "property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label."
28311
28320
  },
28312
- depth: { type: "number", description: "tree: levels to descend. Default 3." },
28321
+ depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
28313
28322
  includeProperties: {
28314
28323
  type: "boolean",
28324
+ ops: ["tree"],
28315
28325
  description: "tree: include each node's properties. Default false."
28316
28326
  },
28317
28327
  className: {
28318
28328
  type: "string",
28329
+ ops: ["find"],
28319
28330
  description: "find: a native class, matching its subclasses too, or a class_name."
28320
28331
  },
28321
- script: { type: "string", description: "find: the script file the node carries." },
28332
+ script: { type: "string", ops: ["find"], description: "find: the script file the node carries." },
28322
28333
  namePattern: {
28323
28334
  type: "string",
28335
+ ops: ["find"],
28324
28336
  description: 'find: a case-insensitive glob on the node name, such as "Enemy*".'
28325
28337
  },
28326
- group: { type: "string", description: "find: a group the node is in." },
28327
- limit: { type: "number", description: "find: the most nodes to answer with. Default 100." },
28338
+ group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
28339
+ limit: {
28340
+ type: "number",
28341
+ ops: ["find", "text"],
28342
+ description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with truncated saying whether there were more."
28343
+ },
28328
28344
  includeHidden: {
28329
28345
  type: "boolean",
28346
+ ops: ["text"],
28330
28347
  description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
28331
28348
  },
28332
28349
  metrics: {
28333
28350
  type: "array",
28334
28351
  items: { type: "string" },
28352
+ ops: ["metrics"],
28335
28353
  description: "metrics: which to read. Default all."
28336
28354
  }
28337
28355
  },
@@ -28364,10 +28382,14 @@ var TOOL_SPECS = [
28364
28382
  parameters: {
28365
28383
  projectPath: RUNNING_PROJECT_PATH,
28366
28384
  nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
28367
- property: { type: "string" },
28368
- value: { description: "set: the value, fitted to the property's type." },
28369
- method: { type: "string" },
28370
- args: { type: "array", description: "call: the arguments, fitted to the method's parameter types." }
28385
+ property: { type: "string", ops: ["set"] },
28386
+ value: { ops: ["set"], description: "set: the value, fitted to the property's type." },
28387
+ method: { type: "string", ops: ["call"] },
28388
+ args: {
28389
+ type: "array",
28390
+ ops: ["call"],
28391
+ description: "call: the arguments, fitted to the method's parameter types."
28392
+ }
28371
28393
  },
28372
28394
  requires: ["nodePath"],
28373
28395
  operations: {
@@ -28382,6 +28404,7 @@ var TOOL_SPECS = [
28382
28404
  projectPath: RUNNING_PROJECT_PATH,
28383
28405
  viewportPath: {
28384
28406
  type: "string",
28407
+ ops: ["viewport"],
28385
28408
  description: "viewport: the Viewport node. Default the root viewport."
28386
28409
  },
28387
28410
  width: { type: "number", description: "Scale the image to this width." },
@@ -28401,38 +28424,64 @@ var TOOL_SPECS = [
28401
28424
  projectPath: RUNNING_PROJECT_PATH,
28402
28425
  nodePath: {
28403
28426
  type: "string",
28427
+ ops: ["click", "choose"],
28404
28428
  description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
28405
28429
  },
28406
- action: { type: "string", description: "action: the InputMap action name." },
28430
+ action: { type: "string", ops: ["action"], description: "action: the InputMap action name." },
28407
28431
  pressed: {
28408
28432
  type: "boolean",
28409
- description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held."
28433
+ ops: ["action", "key", "mouse_click"],
28434
+ description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
28410
28435
  },
28411
- strength: { type: "number", description: "action: 0 to 1. Default 1." },
28436
+ strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
28412
28437
  keycode: {
28413
28438
  type: ["string", "number"],
28439
+ ops: ["key"],
28414
28440
  description: 'key: the key name, such as "Space" or "A", or its Godot keycode.'
28415
28441
  },
28416
28442
  text: {
28417
28443
  type: "string",
28444
+ ops: ["text", "choose"],
28418
28445
  description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
28419
28446
  },
28420
28447
  index: {
28421
28448
  type: "number",
28449
+ ops: ["choose"],
28422
28450
  description: "choose: the item to take, by where it is in the list, when text will not do."
28423
28451
  },
28424
- shift: { type: "boolean" },
28425
- ctrl: { type: "boolean" },
28426
- alt: { type: "boolean" },
28427
- x: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
28428
- y: { type: "number", description: "mouse_click, mouse_motion: window pixels." },
28452
+ shift: { type: "boolean", ops: ["key"] },
28453
+ ctrl: { type: "boolean", ops: ["key"] },
28454
+ alt: { type: "boolean", ops: ["key"] },
28455
+ x: {
28456
+ type: "number",
28457
+ ops: ["mouse_click", "mouse_motion"],
28458
+ description: "mouse_click, mouse_motion: window pixels."
28459
+ },
28460
+ y: {
28461
+ type: "number",
28462
+ ops: ["mouse_click", "mouse_motion"],
28463
+ description: "mouse_click, mouse_motion: window pixels."
28464
+ },
28429
28465
  button: {
28430
28466
  type: ["string", "number"],
28467
+ ops: ["click", "mouse_click"],
28431
28468
  description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
28432
28469
  },
28433
- doubleClick: { type: "boolean", description: "click, mouse_click: default false." },
28434
- relativeX: { type: "number", description: "mouse_motion: movement since the last event." },
28435
- relativeY: { type: "number", description: "mouse_motion: movement since the last event." }
28470
+ doubleClick: {
28471
+ type: "boolean",
28472
+ ops: ["click", "mouse_click"],
28473
+ description: "click, mouse_click: default false."
28474
+ },
28475
+ relativeX: {
28476
+ type: "number",
28477
+ ops: ["mouse_motion"],
28478
+ description: "mouse_motion: movement since the last event."
28479
+ },
28480
+ relativeY: {
28481
+ type: "number",
28482
+ ops: ["mouse_motion"],
28483
+ description: "mouse_motion: movement since the last event."
28484
+ }
28436
28485
  },
28437
28486
  requires: [],
28438
28487
  operations: {
@@ -28461,14 +28510,16 @@ var TOOL_SPECS = [
28461
28510
  projectPath: RUNNING_PROJECT_PATH,
28462
28511
  frames: {
28463
28512
  type: "number",
28513
+ ops: ["frames"],
28464
28514
  description: "frames: how many to let pass, 1 to 600. More than that is refused."
28465
28515
  },
28466
- nodePath: { type: "string", description: "signal, until: the node." },
28467
- signal: { type: "string", description: "signal: the signal name." },
28468
- property: { type: "string", description: "until: the property name." },
28469
- value: { description: "until: the value to wait for, fitted to the property's type." },
28516
+ nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
28517
+ signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
28518
+ property: { type: "string", ops: ["until"], description: "until: the property name." },
28519
+ value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
28470
28520
  timeoutMs: {
28471
28521
  type: "number",
28522
+ ops: ["signal", "until"],
28472
28523
  description: "signal, until: how long to wait before answering anyway, 1 to 120000. Default 5000."
28473
28524
  }
28474
28525
  },
@@ -28516,6 +28567,7 @@ var TOOL_SPECS = [
28516
28567
  parameters: {
28517
28568
  frameId: {
28518
28569
  type: "number",
28570
+ ops: ["variables"],
28519
28571
  description: "variables: which frame, from a stack answer. Default the innermost."
28520
28572
  }
28521
28573
  },
@@ -28554,7 +28606,8 @@ function buildToolDefinitions() {
28554
28606
  };
28555
28607
  }
28556
28608
  for (const [name, schema] of Object.entries(spec.parameters)) {
28557
- properties[name] = schema;
28609
+ const { ops: _ops, ...carried } = schema;
28610
+ properties[name] = carried;
28558
28611
  }
28559
28612
  const required = [...spec.requires];
28560
28613
  if (spec.operations && !spec.defaultOperation) {
@@ -29045,6 +29098,15 @@ class GodotServer {
29045
29098
  };
29046
29099
  }
29047
29100
  }
29101
+ if (op !== null) {
29102
+ const elsewhere = Object.keys(args).filter((key) => key !== "op" && !opTakes(spec, op, key));
29103
+ if (elsewhere.length > 0) {
29104
+ return {
29105
+ ok: false,
29106
+ response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${op} takes: ${argumentsOf(spec, op).join(", ")}.`)
29107
+ };
29108
+ }
29109
+ }
29048
29110
  const required = [...spec.requires, ...op !== null ? spec.operations?.[op]?.requires ?? [] : []];
29049
29111
  const missing = required.filter((field) => {
29050
29112
  const value = Object.hasOwn(args, field) ? args[field] : undefined;
@@ -29169,7 +29231,8 @@ class GodotServer {
29169
29231
  return await this.handleRuntimeCommand("read_text", {
29170
29232
  projectPath: args["projectPath"],
29171
29233
  root: readNonEmptyString(args, "nodePath") ?? "/root",
29172
- include_hidden: readBoolean(args, "includeHidden") ?? false
29234
+ include_hidden: readBoolean(args, "includeHidden") ?? false,
29235
+ limit: readPositiveNumber(args, "limit") ?? 500
29173
29236
  });
29174
29237
  case "rect":
29175
29238
  return await this.handleRuntimeCommand("get_rect", {
@@ -30211,31 +30274,26 @@ class GodotServer {
30211
30274
  });
30212
30275
  }
30213
30276
  async handleRuntimeWait(op, args) {
30277
+ if (op === "frames") {
30278
+ const frames = readPositiveNumber(args, "frames") ?? 1;
30279
+ const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
30280
+ return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
30281
+ }
30214
30282
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
30215
30283
  const nodePath = readNonEmptyString(args, "nodePath") ?? "";
30216
30284
  const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
30217
- switch (op) {
30218
- case "frames": {
30219
- const frames = readPositiveNumber(args, "frames") ?? 1;
30220
- const waited = patienceForFrames(frames, patience);
30221
- return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
30222
- }
30223
- case "signal":
30224
- return await this.handleRuntimeCommand("wait_signal", {
30225
- projectPath: args["projectPath"],
30226
- path: nodePath,
30227
- signal: readString(args, "signal") ?? "",
30228
- timeout_ms: timeoutMs
30229
- }, patience);
30230
- default:
30231
- return await this.handleRuntimeCommand("wait_until", {
30232
- projectPath: args["projectPath"],
30233
- path: nodePath,
30234
- property: readString(args, "property") ?? "",
30235
- value: args["value"],
30236
- timeout_ms: timeoutMs
30237
- }, patience);
30238
- }
30285
+ return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
30286
+ projectPath: args["projectPath"],
30287
+ path: nodePath,
30288
+ signal: readString(args, "signal") ?? "",
30289
+ timeout_ms: timeoutMs
30290
+ }, patience) : await this.handleRuntimeCommand("wait_until", {
30291
+ projectPath: args["projectPath"],
30292
+ path: nodePath,
30293
+ property: readString(args, "property") ?? "",
30294
+ value: args["value"],
30295
+ timeout_ms: timeoutMs
30296
+ }, patience);
30239
30297
  }
30240
30298
  }
30241
30299
  async function runGodotServer() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
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",