gdharness 1.1.48 → 1.1.50
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 +267 -120
- package/build/index.js +182 -24
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -17843,10 +17843,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
17843
17843
|
},
|
|
17844
17844
|
{
|
|
17845
17845
|
name: "script_diagnostics",
|
|
17846
|
-
description: "Errors and warnings for a script from the editor's language server, and whether
|
|
17846
|
+
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
17847
17847
|
parameters: {
|
|
17848
17848
|
projectPath: PROJECT_PATH,
|
|
17849
|
-
scriptPath:
|
|
17849
|
+
scriptPath: {
|
|
17850
|
+
type: ["string", "array"],
|
|
17851
|
+
items: { type: "string" },
|
|
17852
|
+
description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was; several are answered together under scripts, each in that same shape, with clean, errors and warnings for all of them beside it, and a script that could not be read is named there with why while the rest are still asked about. At most 300 scripts a call.'
|
|
17853
|
+
}
|
|
17850
17854
|
},
|
|
17851
17855
|
requires: ["projectPath", "scriptPath"]
|
|
17852
17856
|
},
|
|
@@ -18589,6 +18593,106 @@ var init_headless_operations = __esm(() => {
|
|
|
18589
18593
|
});
|
|
18590
18594
|
});
|
|
18591
18595
|
|
|
18596
|
+
// src/tool-reference.ts
|
|
18597
|
+
function namedType(declared) {
|
|
18598
|
+
if (typeof declared === "string") {
|
|
18599
|
+
return declared;
|
|
18600
|
+
}
|
|
18601
|
+
if (Array.isArray(declared) && declared.every((kind) => typeof kind === "string")) {
|
|
18602
|
+
return declared.join(" or ");
|
|
18603
|
+
}
|
|
18604
|
+
return "any";
|
|
18605
|
+
}
|
|
18606
|
+
function argumentShape(schema) {
|
|
18607
|
+
const named = namedType(schema["type"]);
|
|
18608
|
+
const items = schema["items"];
|
|
18609
|
+
const held = typeof items === "object" && items !== null && !Array.isArray(items) ? namedType(items["type"]) : "any";
|
|
18610
|
+
return named.split(" or ").map((kind) => kind === "array" && held !== "any" ? `array of ${held}s` : kind).join(" or ");
|
|
18611
|
+
}
|
|
18612
|
+
function headlessSection() {
|
|
18613
|
+
const everyOp = new Map;
|
|
18614
|
+
for (const table of [HEADLESS_OPERATIONS, ENGINE_PASSES]) {
|
|
18615
|
+
for (const [tool, ops] of Object.entries(table)) {
|
|
18616
|
+
everyOp.set(tool, [...everyOp.get(tool) ?? [], ...Object.keys(ops)]);
|
|
18617
|
+
}
|
|
18618
|
+
}
|
|
18619
|
+
const byTool = [...everyOp].map(([tool, ops]) => `\`${tool}\` (${ops.join(", ")})`).sort();
|
|
18620
|
+
return [
|
|
18621
|
+
"## What a gate can call",
|
|
18622
|
+
"",
|
|
18623
|
+
"These answer from a short-lived headless engine: no editor window, no running game, and no",
|
|
18624
|
+
"process or port outliving the answer. They are the only calls an automatic check can make at",
|
|
18625
|
+
"all, because every other tool needs an editor somebody has open or a game somebody is running.",
|
|
18626
|
+
"",
|
|
18627
|
+
...byTool.map((line) => `- ${line}`),
|
|
18628
|
+
"",
|
|
18629
|
+
"Needing nothing is not the same as changing nothing, and a gate wants both. Most of these",
|
|
18630
|
+
"write to the project because that is what they are for: `script_edit create` writes a file,",
|
|
18631
|
+
"`project_settings set` rewrites `project.godot`, `project_import reimport` and `refresh_classes`",
|
|
18632
|
+
"write under `.godot`, and `project_import refresh_uids` writes a `.uid` beside each script that",
|
|
18633
|
+
"had none. The reading ops are the ones to gate on, and which is which is in each",
|
|
18634
|
+
"op's own summary below rather than assumed from this list.",
|
|
18635
|
+
"",
|
|
18636
|
+
"Everything else needs the editor (`editor_*`, `debug_*`, `scene_*`, `resource_*`) or a running",
|
|
18637
|
+
"game (`runtime_*`).",
|
|
18638
|
+
"",
|
|
18639
|
+
'One of these can be pointed the other way: `project_settings get` takes `from: "editor"` to ask',
|
|
18640
|
+
"the open editor instead of the file, which is a different reading and costs no engine start. A",
|
|
18641
|
+
"gate leaves it alone, because there is no editor on a build machine and the call is refused",
|
|
18642
|
+
"rather than answered from the file.",
|
|
18643
|
+
""
|
|
18644
|
+
];
|
|
18645
|
+
}
|
|
18646
|
+
function renderToolsMarkdown() {
|
|
18647
|
+
const lines = [
|
|
18648
|
+
"# Tools",
|
|
18649
|
+
"",
|
|
18650
|
+
`${TOOL_SPECS.length} tools, named \`domain_verb\`. A tool that does several related things takes an`,
|
|
18651
|
+
"`op`. An unknown op or argument is refused with the valid set listed. Generated from the server.",
|
|
18652
|
+
"",
|
|
18653
|
+
`${projectPathSentence()}: the \`runtime_*\` and \`debug_*\` ones pick between running games`,
|
|
18654
|
+
"instead, and the others answer about the editor on this server. The pick, when a call names",
|
|
18655
|
+
`neither \`projectPath\` nor \`pid\`: ${theGamePicked()}. An argument a tool does not`,
|
|
18656
|
+
"declare is refused rather than ignored. Answers are read from the engine after the change, not",
|
|
18657
|
+
"echoed from the request. Engine stderr comes back under `engine_messages`.",
|
|
18658
|
+
"",
|
|
18659
|
+
...headlessSection()
|
|
18660
|
+
];
|
|
18661
|
+
for (const tool of TOOL_SPECS) {
|
|
18662
|
+
lines.push(`## ${tool.name}`, "", tool.description, "");
|
|
18663
|
+
const operations = Object.entries(tool.operations ?? {});
|
|
18664
|
+
if (operations.length > 0) {
|
|
18665
|
+
for (const [op, spec] of operations) {
|
|
18666
|
+
const needs = [...tool.requires, ...spec.requires];
|
|
18667
|
+
const isDefault = tool.defaultOperation === op ? " (default)" : "";
|
|
18668
|
+
const wants = needs.length > 0 ? ` Needs: ${needs.map((name) => `\`${name}\``).join(", ")}.` : "";
|
|
18669
|
+
const spare = argumentsOf(tool, op).filter((name) => !needs.includes(name) && name !== "projectPath");
|
|
18670
|
+
const takes = spare.length > 0 ? ` Takes: ${spare.map((name) => `\`${name}\``).join(", ")}.` : "";
|
|
18671
|
+
lines.push(`- \`op: ${op}\`${isDefault}: ${spec.summary}.${wants}${takes}`);
|
|
18672
|
+
}
|
|
18673
|
+
lines.push("");
|
|
18674
|
+
} else if (tool.requires.length > 0) {
|
|
18675
|
+
lines.push(`Needs: ${tool.requires.map((name) => `\`${name}\``).join(", ")}.`, "");
|
|
18676
|
+
}
|
|
18677
|
+
const parameters = Object.entries(tool.parameters).filter(([name]) => name !== "projectPath");
|
|
18678
|
+
if (parameters.length > 0) {
|
|
18679
|
+
lines.push("Arguments:", "");
|
|
18680
|
+
for (const [name, schema] of parameters) {
|
|
18681
|
+
const type = argumentShape(schema);
|
|
18682
|
+
const note = typeof schema["description"] === "string" ? `: ${schema["description"]}` : "";
|
|
18683
|
+
lines.push(`- \`${name}\` (${type})${note}`);
|
|
18684
|
+
}
|
|
18685
|
+
lines.push("");
|
|
18686
|
+
}
|
|
18687
|
+
}
|
|
18688
|
+
return lines.join(`
|
|
18689
|
+
`);
|
|
18690
|
+
}
|
|
18691
|
+
var init_tool_reference = __esm(() => {
|
|
18692
|
+
init_headless_operations();
|
|
18693
|
+
init_tool_definitions();
|
|
18694
|
+
});
|
|
18695
|
+
|
|
18592
18696
|
// node_modules/zod/v3/helpers/util.js
|
|
18593
18697
|
var util, objectUtil, ZodParsedType, getParsedType = (data) => {
|
|
18594
18698
|
const t = typeof data;
|
|
@@ -39388,7 +39492,7 @@ class GodotLSPClient {
|
|
|
39388
39492
|
const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
|
|
39389
39493
|
const timer = setTimeout(() => {
|
|
39390
39494
|
this.diagnosticsWaiters.delete(key);
|
|
39391
|
-
rejectDiagnostics(new
|
|
39495
|
+
rejectDiagnostics(new NoDiagnosticsPublished(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
|
|
39392
39496
|
}, DIAGNOSTICS_TIMEOUT_MS);
|
|
39393
39497
|
this.diagnosticsWaiters.set(key, {
|
|
39394
39498
|
resolve: resolveDiagnostics,
|
|
@@ -39618,11 +39722,12 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
39618
39722
|
return asToolResponse({
|
|
39619
39723
|
error: normalizeLSPError(error, client.port),
|
|
39620
39724
|
...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
|
|
39621
|
-
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
|
|
39725
|
+
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
|
|
39726
|
+
...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {}
|
|
39622
39727
|
});
|
|
39623
39728
|
}
|
|
39624
39729
|
}
|
|
39625
|
-
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, ArgumentRefusal, AnotherProjectsServer;
|
|
39730
|
+
var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, ArgumentRefusal, NoDiagnosticsPublished, AnotherProjectsServer;
|
|
39626
39731
|
var init_lsp_client = __esm(() => {
|
|
39627
39732
|
init_answer_json();
|
|
39628
39733
|
init_errors();
|
|
@@ -39640,6 +39745,8 @@ var init_lsp_client = __esm(() => {
|
|
|
39640
39745
|
};
|
|
39641
39746
|
ArgumentRefusal = class ArgumentRefusal extends Refusal {
|
|
39642
39747
|
};
|
|
39748
|
+
NoDiagnosticsPublished = class NoDiagnosticsPublished extends Error {
|
|
39749
|
+
};
|
|
39643
39750
|
AnotherProjectsServer = class AnotherProjectsServer extends Refusal {
|
|
39644
39751
|
serves;
|
|
39645
39752
|
constructor(port, serves, asked) {
|
|
@@ -42468,6 +42575,82 @@ function testPathsIn(projectPath, given) {
|
|
|
42468
42575
|
value: unique.filter((path) => !unique.some((other) => other !== path && within(path, other))).map((path) => `res://${path}`)
|
|
42469
42576
|
};
|
|
42470
42577
|
}
|
|
42578
|
+
function scriptsToDiagnose(projectPath, given) {
|
|
42579
|
+
const listed = Array.isArray(given);
|
|
42580
|
+
if (!listed) {
|
|
42581
|
+
const one = typeof given === "string" ? resolveWithinProject(projectPath, given) : null;
|
|
42582
|
+
if (one === null || !one.ok || !isDirectory(one.absolutePath)) {
|
|
42583
|
+
return { ok: true, value: null };
|
|
42584
|
+
}
|
|
42585
|
+
}
|
|
42586
|
+
const entries = listed ? given : [given];
|
|
42587
|
+
if (entries.length === 0) {
|
|
42588
|
+
return {
|
|
42589
|
+
ok: false,
|
|
42590
|
+
reason: "scriptPath is an empty list: name a script, a folder, or a list of either."
|
|
42591
|
+
};
|
|
42592
|
+
}
|
|
42593
|
+
const found = [];
|
|
42594
|
+
const empty = [];
|
|
42595
|
+
for (const [index, entry] of entries.entries()) {
|
|
42596
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
42597
|
+
return {
|
|
42598
|
+
ok: false,
|
|
42599
|
+
reason: `scriptPath${listed ? `[${index}]` : ""} is ${typeof entry === "string" ? "empty" : describeValue(entry)}, not a script or a folder in the project.`
|
|
42600
|
+
};
|
|
42601
|
+
}
|
|
42602
|
+
const location = resolveWithinProject(projectPath, entry);
|
|
42603
|
+
if (!location.ok) {
|
|
42604
|
+
return { ok: false, reason: location.reason };
|
|
42605
|
+
}
|
|
42606
|
+
const relative = location.relativePath.replace(/\/+$/, "");
|
|
42607
|
+
if (!isDirectory(location.absolutePath)) {
|
|
42608
|
+
found.push(relative);
|
|
42609
|
+
continue;
|
|
42610
|
+
}
|
|
42611
|
+
const inside = scriptsUnder(location.absolutePath, relative);
|
|
42612
|
+
if (inside.length === 0) {
|
|
42613
|
+
empty.push(entry);
|
|
42614
|
+
}
|
|
42615
|
+
found.push(...inside);
|
|
42616
|
+
}
|
|
42617
|
+
const unique = [...new Set(found)];
|
|
42618
|
+
if (unique.length === 0) {
|
|
42619
|
+
return { ok: false, reason: `No .gd script is under ${empty.join(" or ")}.` };
|
|
42620
|
+
}
|
|
42621
|
+
if (unique.length > MOST_DIAGNOSED) {
|
|
42622
|
+
return {
|
|
42623
|
+
ok: false,
|
|
42624
|
+
reason: `scriptPath names ${unique.length} scripts, and one call asks about at most ${MOST_DIAGNOSED}, each a round trip through the language server: name narrower folders, or call again for the rest.`
|
|
42625
|
+
};
|
|
42626
|
+
}
|
|
42627
|
+
return { ok: true, value: unique };
|
|
42628
|
+
}
|
|
42629
|
+
function isDirectory(path) {
|
|
42630
|
+
try {
|
|
42631
|
+
return statSync9(path).isDirectory();
|
|
42632
|
+
} catch {
|
|
42633
|
+
return false;
|
|
42634
|
+
}
|
|
42635
|
+
}
|
|
42636
|
+
function scriptsUnder(directory, prefix) {
|
|
42637
|
+
if (existsSync20(join26(directory, ".gdignore"))) {
|
|
42638
|
+
return [];
|
|
42639
|
+
}
|
|
42640
|
+
const found = [];
|
|
42641
|
+
for (const entry of readdirSync10(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
42642
|
+
if (entry.name.startsWith(".")) {
|
|
42643
|
+
continue;
|
|
42644
|
+
}
|
|
42645
|
+
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
42646
|
+
if (entry.isDirectory()) {
|
|
42647
|
+
found.push(...scriptsUnder(join26(directory, entry.name), path));
|
|
42648
|
+
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
42649
|
+
found.push(path);
|
|
42650
|
+
}
|
|
42651
|
+
}
|
|
42652
|
+
return found;
|
|
42653
|
+
}
|
|
42471
42654
|
function hasMainScene(projectFile) {
|
|
42472
42655
|
const scene = parseProjectGodot(readFileSync22(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
42473
42656
|
return typeof scene === "string" && scene !== "";
|
|
@@ -42945,7 +43128,14 @@ class GodotServer {
|
|
|
42945
43128
|
const asked = readString(args, "op");
|
|
42946
43129
|
const named = asked !== undefined && spec.operations?.[asked] !== undefined ? asked : null;
|
|
42947
43130
|
const wanted = named === null ? [...known] : [...argumentsOf(spec, named), "op"];
|
|
42948
|
-
const
|
|
43131
|
+
const shaped = wanted.map((name) => {
|
|
43132
|
+
const schema = spec.parameters[name];
|
|
43133
|
+
if (schema !== undefined) {
|
|
43134
|
+
return `${name} (${argumentShape(schema)})`;
|
|
43135
|
+
}
|
|
43136
|
+
return name === "op" && named === null && spec.operations !== undefined ? `op (${Object.keys(spec.operations).join(", ")})` : name;
|
|
43137
|
+
});
|
|
43138
|
+
const takes = wanted.length === 0 ? "It takes no arguments." : `It takes: ${shaped.join(", ")}.`;
|
|
42949
43139
|
const where = named === null ? spec.name : `${spec.name} ${named}`;
|
|
42950
43140
|
return {
|
|
42951
43141
|
ok: false,
|
|
@@ -44289,39 +44479,88 @@ class GodotServer {
|
|
|
44289
44479
|
}
|
|
44290
44480
|
}
|
|
44291
44481
|
async handleScriptDiagnostics(args) {
|
|
44482
|
+
const project = this.project(args);
|
|
44483
|
+
const scripts = project.ok ? scriptsToDiagnose(project.value.path, args["scriptPath"]) : null;
|
|
44484
|
+
if (scripts !== null && !scripts.ok) {
|
|
44485
|
+
return this.createErrorResponse(scripts.reason);
|
|
44486
|
+
}
|
|
44487
|
+
if (scripts?.value == null) {
|
|
44488
|
+
const one = await this.diagnosticsOf(args);
|
|
44489
|
+
return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
|
|
44490
|
+
}
|
|
44491
|
+
const progress = callProgress();
|
|
44492
|
+
const answers = [];
|
|
44493
|
+
let errors = 0;
|
|
44494
|
+
let warnings = 0;
|
|
44495
|
+
let unread = 0;
|
|
44496
|
+
for (const [at, scriptPath] of scripts.value.entries()) {
|
|
44497
|
+
progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
|
|
44498
|
+
const one = await this.diagnosticsOf({ ...args, scriptPath });
|
|
44499
|
+
if ("refusal" in one) {
|
|
44500
|
+
if (!one.perScript) {
|
|
44501
|
+
return one.refusal;
|
|
44502
|
+
}
|
|
44503
|
+
unread += 1;
|
|
44504
|
+
answers.push({ scriptPath, error: one.refusal.content[0]?.text ?? "not read" });
|
|
44505
|
+
continue;
|
|
44506
|
+
}
|
|
44507
|
+
errors += Number(one.answer["errors"]);
|
|
44508
|
+
warnings += Number(one.answer["warnings"]);
|
|
44509
|
+
answers.push(one.answer);
|
|
44510
|
+
}
|
|
44511
|
+
return this.jsonTextResponse(this.markedIfStale({
|
|
44512
|
+
clean: errors === 0 && unread === 0,
|
|
44513
|
+
scriptsChecked: answers.length,
|
|
44514
|
+
errors,
|
|
44515
|
+
warnings,
|
|
44516
|
+
...unread > 0 ? { scriptsNotRead: unread } : {},
|
|
44517
|
+
scripts: answers
|
|
44518
|
+
}));
|
|
44519
|
+
}
|
|
44520
|
+
markedIfStale(answer) {
|
|
44521
|
+
return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
|
|
44522
|
+
}
|
|
44523
|
+
async diagnosticsOf(args) {
|
|
44292
44524
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
44293
44525
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
44294
44526
|
if (payload["error"] !== undefined) {
|
|
44295
44527
|
const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
|
|
44296
44528
|
if (payload["refusedArguments"] === true) {
|
|
44297
|
-
return this.createErrorResponse(reason);
|
|
44529
|
+
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
44298
44530
|
}
|
|
44299
44531
|
if (typeof payload["serves"] === "string") {
|
|
44300
|
-
return
|
|
44301
|
-
|
|
44302
|
-
|
|
44303
|
-
|
|
44304
|
-
|
|
44532
|
+
return {
|
|
44533
|
+
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
44534
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
44535
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
44536
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
44537
|
+
]),
|
|
44538
|
+
perScript: false
|
|
44539
|
+
};
|
|
44305
44540
|
}
|
|
44306
|
-
return
|
|
44307
|
-
|
|
44308
|
-
|
|
44309
|
-
|
|
44541
|
+
return {
|
|
44542
|
+
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
44543
|
+
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
44544
|
+
"GDHARNESS_LSP_PORT points this server at another one"
|
|
44545
|
+
]),
|
|
44546
|
+
perScript: payload["aboutThisFile"] === true
|
|
44547
|
+
};
|
|
44310
44548
|
}
|
|
44311
44549
|
const diagnostics = readArray(payload, "diagnostics") ?? [];
|
|
44312
44550
|
const errors = diagnostics.filter((entry) => {
|
|
44313
44551
|
const severity = asParams(entry)["severity"];
|
|
44314
44552
|
return severity === 1 || severity === "error" || severity === "ERROR";
|
|
44315
44553
|
}).length;
|
|
44316
|
-
|
|
44317
|
-
|
|
44318
|
-
|
|
44319
|
-
|
|
44320
|
-
|
|
44321
|
-
|
|
44322
|
-
|
|
44323
|
-
|
|
44324
|
-
|
|
44554
|
+
return {
|
|
44555
|
+
answer: {
|
|
44556
|
+
scriptPath: readString(args, "scriptPath"),
|
|
44557
|
+
clean: errors === 0,
|
|
44558
|
+
errors,
|
|
44559
|
+
warnings: diagnostics.length - errors,
|
|
44560
|
+
diagnostics,
|
|
44561
|
+
...this.whereTheFileDisagrees(diagnostics, args)
|
|
44562
|
+
}
|
|
44563
|
+
};
|
|
44325
44564
|
}
|
|
44326
44565
|
whereTheFileDisagrees(diagnostics, args) {
|
|
44327
44566
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
@@ -47131,7 +47370,7 @@ async function runGodotServer() {
|
|
|
47131
47370
|
const server = new GodotServer;
|
|
47132
47371
|
await server.run();
|
|
47133
47372
|
}
|
|
47134
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
47373
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, GREETING_WAIT_MS = 1e4, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", MOST_DIAGNOSED = 300;
|
|
47135
47374
|
var init_server3 = __esm(() => {
|
|
47136
47375
|
init_mcp();
|
|
47137
47376
|
init_stdio2();
|
|
@@ -47177,6 +47416,7 @@ var init_server3 = __esm(() => {
|
|
|
47177
47416
|
init_server_version();
|
|
47178
47417
|
init_setup();
|
|
47179
47418
|
init_tool_definitions();
|
|
47419
|
+
init_tool_reference();
|
|
47180
47420
|
init_update_check();
|
|
47181
47421
|
run4 = promisify5(execFile5);
|
|
47182
47422
|
__dirname2 = dirname19(fileURLToPath5(import.meta.url));
|
|
@@ -48170,102 +48410,9 @@ init_setup();
|
|
|
48170
48410
|
// src/skill.ts
|
|
48171
48411
|
init_setup();
|
|
48172
48412
|
init_tool_definitions();
|
|
48413
|
+
init_tool_reference();
|
|
48173
48414
|
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
48174
48415
|
import { basename as basename4, dirname as dirname6, join as join11 } from "node:path";
|
|
48175
|
-
|
|
48176
|
-
// src/tool-reference.ts
|
|
48177
|
-
init_headless_operations();
|
|
48178
|
-
init_tool_definitions();
|
|
48179
|
-
function namedType(declared) {
|
|
48180
|
-
if (typeof declared === "string") {
|
|
48181
|
-
return declared;
|
|
48182
|
-
}
|
|
48183
|
-
if (Array.isArray(declared) && declared.every((kind) => typeof kind === "string")) {
|
|
48184
|
-
return declared.join(" or ");
|
|
48185
|
-
}
|
|
48186
|
-
return "any";
|
|
48187
|
-
}
|
|
48188
|
-
function headlessSection() {
|
|
48189
|
-
const everyOp = new Map;
|
|
48190
|
-
for (const table of [HEADLESS_OPERATIONS, ENGINE_PASSES]) {
|
|
48191
|
-
for (const [tool, ops] of Object.entries(table)) {
|
|
48192
|
-
everyOp.set(tool, [...everyOp.get(tool) ?? [], ...Object.keys(ops)]);
|
|
48193
|
-
}
|
|
48194
|
-
}
|
|
48195
|
-
const byTool = [...everyOp].map(([tool, ops]) => `\`${tool}\` (${ops.join(", ")})`).sort();
|
|
48196
|
-
return [
|
|
48197
|
-
"## What a gate can call",
|
|
48198
|
-
"",
|
|
48199
|
-
"These answer from a short-lived headless engine: no editor window, no running game, and no",
|
|
48200
|
-
"process or port outliving the answer. They are the only calls an automatic check can make at",
|
|
48201
|
-
"all, because every other tool needs an editor somebody has open or a game somebody is running.",
|
|
48202
|
-
"",
|
|
48203
|
-
...byTool.map((line) => `- ${line}`),
|
|
48204
|
-
"",
|
|
48205
|
-
"Needing nothing is not the same as changing nothing, and a gate wants both. Most of these",
|
|
48206
|
-
"write to the project because that is what they are for: `script_edit create` writes a file,",
|
|
48207
|
-
"`project_settings set` rewrites `project.godot`, `project_import reimport` and `refresh_classes`",
|
|
48208
|
-
"write under `.godot`, and `project_import refresh_uids` writes a `.uid` beside each script that",
|
|
48209
|
-
"had none. The reading ops are the ones to gate on, and which is which is in each",
|
|
48210
|
-
"op's own summary below rather than assumed from this list.",
|
|
48211
|
-
"",
|
|
48212
|
-
"Everything else needs the editor (`editor_*`, `debug_*`, `scene_*`, `resource_*`) or a running",
|
|
48213
|
-
"game (`runtime_*`).",
|
|
48214
|
-
"",
|
|
48215
|
-
'One of these can be pointed the other way: `project_settings get` takes `from: "editor"` to ask',
|
|
48216
|
-
"the open editor instead of the file, which is a different reading and costs no engine start. A",
|
|
48217
|
-
"gate leaves it alone, because there is no editor on a build machine and the call is refused",
|
|
48218
|
-
"rather than answered from the file.",
|
|
48219
|
-
""
|
|
48220
|
-
];
|
|
48221
|
-
}
|
|
48222
|
-
function renderToolsMarkdown() {
|
|
48223
|
-
const lines = [
|
|
48224
|
-
"# Tools",
|
|
48225
|
-
"",
|
|
48226
|
-
`${TOOL_SPECS.length} tools, named \`domain_verb\`. A tool that does several related things takes an`,
|
|
48227
|
-
"`op`. An unknown op or argument is refused with the valid set listed. Generated from the server.",
|
|
48228
|
-
"",
|
|
48229
|
-
`${projectPathSentence()}: the \`runtime_*\` and \`debug_*\` ones pick between running games`,
|
|
48230
|
-
"instead, and the others answer about the editor on this server. The pick, when a call names",
|
|
48231
|
-
`neither \`projectPath\` nor \`pid\`: ${theGamePicked()}. An argument a tool does not`,
|
|
48232
|
-
"declare is refused rather than ignored. Answers are read from the engine after the change, not",
|
|
48233
|
-
"echoed from the request. Engine stderr comes back under `engine_messages`.",
|
|
48234
|
-
"",
|
|
48235
|
-
...headlessSection()
|
|
48236
|
-
];
|
|
48237
|
-
for (const tool of TOOL_SPECS) {
|
|
48238
|
-
lines.push(`## ${tool.name}`, "", tool.description, "");
|
|
48239
|
-
const operations = Object.entries(tool.operations ?? {});
|
|
48240
|
-
if (operations.length > 0) {
|
|
48241
|
-
for (const [op, spec] of operations) {
|
|
48242
|
-
const needs = [...tool.requires, ...spec.requires];
|
|
48243
|
-
const isDefault = tool.defaultOperation === op ? " (default)" : "";
|
|
48244
|
-
const wants = needs.length > 0 ? ` Needs: ${needs.map((name) => `\`${name}\``).join(", ")}.` : "";
|
|
48245
|
-
const spare = argumentsOf(tool, op).filter((name) => !needs.includes(name) && name !== "projectPath");
|
|
48246
|
-
const takes = spare.length > 0 ? ` Takes: ${spare.map((name) => `\`${name}\``).join(", ")}.` : "";
|
|
48247
|
-
lines.push(`- \`op: ${op}\`${isDefault}: ${spec.summary}.${wants}${takes}`);
|
|
48248
|
-
}
|
|
48249
|
-
lines.push("");
|
|
48250
|
-
} else if (tool.requires.length > 0) {
|
|
48251
|
-
lines.push(`Needs: ${tool.requires.map((name) => `\`${name}\``).join(", ")}.`, "");
|
|
48252
|
-
}
|
|
48253
|
-
const parameters = Object.entries(tool.parameters).filter(([name]) => name !== "projectPath");
|
|
48254
|
-
if (parameters.length > 0) {
|
|
48255
|
-
lines.push("Arguments:", "");
|
|
48256
|
-
for (const [name, schema] of parameters) {
|
|
48257
|
-
const type = namedType(schema["type"]);
|
|
48258
|
-
const note = typeof schema["description"] === "string" ? `: ${schema["description"]}` : "";
|
|
48259
|
-
lines.push(`- \`${name}\` (${type})${note}`);
|
|
48260
|
-
}
|
|
48261
|
-
lines.push("");
|
|
48262
|
-
}
|
|
48263
|
-
}
|
|
48264
|
-
return lines.join(`
|
|
48265
|
-
`);
|
|
48266
|
-
}
|
|
48267
|
-
|
|
48268
|
-
// src/skill.ts
|
|
48269
48416
|
var SKILL_NAME = "gdharness";
|
|
48270
48417
|
var SHARED_SKILLS = join11(".agents", "skills");
|
|
48271
48418
|
function skillMarkdown(version) {
|
package/build/index.js
CHANGED
|
@@ -29729,6 +29729,9 @@ class LSPTimeout extends Error {
|
|
|
29729
29729
|
class ArgumentRefusal extends Refusal {
|
|
29730
29730
|
}
|
|
29731
29731
|
|
|
29732
|
+
class NoDiagnosticsPublished extends Error {
|
|
29733
|
+
}
|
|
29734
|
+
|
|
29732
29735
|
class AnotherProjectsServer extends Refusal {
|
|
29733
29736
|
serves;
|
|
29734
29737
|
constructor(port, serves, asked) {
|
|
@@ -30116,7 +30119,7 @@ class GodotLSPClient {
|
|
|
30116
30119
|
const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
|
|
30117
30120
|
const timer = setTimeout(() => {
|
|
30118
30121
|
this.diagnosticsWaiters.delete(key);
|
|
30119
|
-
rejectDiagnostics(new
|
|
30122
|
+
rejectDiagnostics(new NoDiagnosticsPublished(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
|
|
30120
30123
|
}, DIAGNOSTICS_TIMEOUT_MS);
|
|
30121
30124
|
this.diagnosticsWaiters.set(key, {
|
|
30122
30125
|
resolve: resolveDiagnostics,
|
|
@@ -30346,7 +30349,8 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
30346
30349
|
return asToolResponse({
|
|
30347
30350
|
error: normalizeLSPError(error, client.port),
|
|
30348
30351
|
...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
|
|
30349
|
-
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
|
|
30352
|
+
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {},
|
|
30353
|
+
...error instanceof NoDiagnosticsPublished ? { aboutThisFile: true } : {}
|
|
30350
30354
|
});
|
|
30351
30355
|
}
|
|
30352
30356
|
}
|
|
@@ -33520,10 +33524,14 @@ var TOOL_SPECS = [
|
|
|
33520
33524
|
},
|
|
33521
33525
|
{
|
|
33522
33526
|
name: "script_diagnostics",
|
|
33523
|
-
description: "Errors and warnings for a script from the editor's language server, and whether
|
|
33527
|
+
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
33524
33528
|
parameters: {
|
|
33525
33529
|
projectPath: PROJECT_PATH,
|
|
33526
|
-
scriptPath:
|
|
33530
|
+
scriptPath: {
|
|
33531
|
+
type: ["string", "array"],
|
|
33532
|
+
items: { type: "string" },
|
|
33533
|
+
description: 'A script inside the project, a folder, or a list of either, such as "scripts/player.gd" or ["scripts/economy", "ui/shop.gd"]. A folder is its .gd files at any depth, skipping what the engine skips. One script is answered as it always was; several are answered together under scripts, each in that same shape, with clean, errors and warnings for all of them beside it, and a script that could not be read is named there with why while the rest are still asked about. At most 300 scripts a call.'
|
|
33534
|
+
}
|
|
33527
33535
|
},
|
|
33528
33536
|
requires: ["projectPath", "scriptPath"]
|
|
33529
33537
|
},
|
|
@@ -34268,6 +34276,23 @@ function buildToolDefinitions() {
|
|
|
34268
34276
|
});
|
|
34269
34277
|
}
|
|
34270
34278
|
|
|
34279
|
+
// src/tool-reference.ts
|
|
34280
|
+
function namedType(declared) {
|
|
34281
|
+
if (typeof declared === "string") {
|
|
34282
|
+
return declared;
|
|
34283
|
+
}
|
|
34284
|
+
if (Array.isArray(declared) && declared.every((kind) => typeof kind === "string")) {
|
|
34285
|
+
return declared.join(" or ");
|
|
34286
|
+
}
|
|
34287
|
+
return "any";
|
|
34288
|
+
}
|
|
34289
|
+
function argumentShape(schema) {
|
|
34290
|
+
const named = namedType(schema["type"]);
|
|
34291
|
+
const items = schema["items"];
|
|
34292
|
+
const held = typeof items === "object" && items !== null && !Array.isArray(items) ? namedType(items["type"]) : "any";
|
|
34293
|
+
return named.split(" or ").map((kind) => kind === "array" && held !== "any" ? `array of ${held}s` : kind).join(" or ");
|
|
34294
|
+
}
|
|
34295
|
+
|
|
34271
34296
|
// src/server.ts
|
|
34272
34297
|
var UPDATE_NOTICE_EVERY = 500;
|
|
34273
34298
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
@@ -35037,6 +35062,83 @@ function testPathsIn(projectPath, given) {
|
|
|
35037
35062
|
value: unique.filter((path) => !unique.some((other) => other !== path && within(path, other))).map((path) => `res://${path}`)
|
|
35038
35063
|
};
|
|
35039
35064
|
}
|
|
35065
|
+
var MOST_DIAGNOSED = 300;
|
|
35066
|
+
function scriptsToDiagnose(projectPath, given) {
|
|
35067
|
+
const listed = Array.isArray(given);
|
|
35068
|
+
if (!listed) {
|
|
35069
|
+
const one = typeof given === "string" ? resolveWithinProject(projectPath, given) : null;
|
|
35070
|
+
if (one === null || !one.ok || !isDirectory(one.absolutePath)) {
|
|
35071
|
+
return { ok: true, value: null };
|
|
35072
|
+
}
|
|
35073
|
+
}
|
|
35074
|
+
const entries = listed ? given : [given];
|
|
35075
|
+
if (entries.length === 0) {
|
|
35076
|
+
return {
|
|
35077
|
+
ok: false,
|
|
35078
|
+
reason: "scriptPath is an empty list: name a script, a folder, or a list of either."
|
|
35079
|
+
};
|
|
35080
|
+
}
|
|
35081
|
+
const found = [];
|
|
35082
|
+
const empty = [];
|
|
35083
|
+
for (const [index, entry] of entries.entries()) {
|
|
35084
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
35085
|
+
return {
|
|
35086
|
+
ok: false,
|
|
35087
|
+
reason: `scriptPath${listed ? `[${index}]` : ""} is ${typeof entry === "string" ? "empty" : describeValue(entry)}, not a script or a folder in the project.`
|
|
35088
|
+
};
|
|
35089
|
+
}
|
|
35090
|
+
const location = resolveWithinProject(projectPath, entry);
|
|
35091
|
+
if (!location.ok) {
|
|
35092
|
+
return { ok: false, reason: location.reason };
|
|
35093
|
+
}
|
|
35094
|
+
const relative = location.relativePath.replace(/\/+$/, "");
|
|
35095
|
+
if (!isDirectory(location.absolutePath)) {
|
|
35096
|
+
found.push(relative);
|
|
35097
|
+
continue;
|
|
35098
|
+
}
|
|
35099
|
+
const inside = scriptsUnder(location.absolutePath, relative);
|
|
35100
|
+
if (inside.length === 0) {
|
|
35101
|
+
empty.push(entry);
|
|
35102
|
+
}
|
|
35103
|
+
found.push(...inside);
|
|
35104
|
+
}
|
|
35105
|
+
const unique = [...new Set(found)];
|
|
35106
|
+
if (unique.length === 0) {
|
|
35107
|
+
return { ok: false, reason: `No .gd script is under ${empty.join(" or ")}.` };
|
|
35108
|
+
}
|
|
35109
|
+
if (unique.length > MOST_DIAGNOSED) {
|
|
35110
|
+
return {
|
|
35111
|
+
ok: false,
|
|
35112
|
+
reason: `scriptPath names ${unique.length} scripts, and one call asks about at most ${MOST_DIAGNOSED}, each a round trip through the language server: name narrower folders, or call again for the rest.`
|
|
35113
|
+
};
|
|
35114
|
+
}
|
|
35115
|
+
return { ok: true, value: unique };
|
|
35116
|
+
}
|
|
35117
|
+
function isDirectory(path) {
|
|
35118
|
+
try {
|
|
35119
|
+
return statSync8(path).isDirectory();
|
|
35120
|
+
} catch {
|
|
35121
|
+
return false;
|
|
35122
|
+
}
|
|
35123
|
+
}
|
|
35124
|
+
function scriptsUnder(directory, prefix) {
|
|
35125
|
+
if (existsSync17(join22(directory, ".gdignore"))) {
|
|
35126
|
+
return [];
|
|
35127
|
+
}
|
|
35128
|
+
const found = [];
|
|
35129
|
+
for (const entry of readdirSync9(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
35130
|
+
if (entry.name.startsWith(".")) {
|
|
35131
|
+
continue;
|
|
35132
|
+
}
|
|
35133
|
+
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
35134
|
+
if (entry.isDirectory()) {
|
|
35135
|
+
found.push(...scriptsUnder(join22(directory, entry.name), path));
|
|
35136
|
+
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
35137
|
+
found.push(path);
|
|
35138
|
+
}
|
|
35139
|
+
}
|
|
35140
|
+
return found;
|
|
35141
|
+
}
|
|
35040
35142
|
function hasMainScene(projectFile) {
|
|
35041
35143
|
const scene = parseProjectGodot(readFileSync20(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
35042
35144
|
return typeof scene === "string" && scene !== "";
|
|
@@ -35514,7 +35616,14 @@ class GodotServer {
|
|
|
35514
35616
|
const asked = readString(args, "op");
|
|
35515
35617
|
const named = asked !== undefined && spec.operations?.[asked] !== undefined ? asked : null;
|
|
35516
35618
|
const wanted = named === null ? [...known] : [...argumentsOf(spec, named), "op"];
|
|
35517
|
-
const
|
|
35619
|
+
const shaped = wanted.map((name) => {
|
|
35620
|
+
const schema = spec.parameters[name];
|
|
35621
|
+
if (schema !== undefined) {
|
|
35622
|
+
return `${name} (${argumentShape(schema)})`;
|
|
35623
|
+
}
|
|
35624
|
+
return name === "op" && named === null && spec.operations !== undefined ? `op (${Object.keys(spec.operations).join(", ")})` : name;
|
|
35625
|
+
});
|
|
35626
|
+
const takes = wanted.length === 0 ? "It takes no arguments." : `It takes: ${shaped.join(", ")}.`;
|
|
35518
35627
|
const where = named === null ? spec.name : `${spec.name} ${named}`;
|
|
35519
35628
|
return {
|
|
35520
35629
|
ok: false,
|
|
@@ -36858,39 +36967,88 @@ class GodotServer {
|
|
|
36858
36967
|
}
|
|
36859
36968
|
}
|
|
36860
36969
|
async handleScriptDiagnostics(args) {
|
|
36970
|
+
const project = this.project(args);
|
|
36971
|
+
const scripts = project.ok ? scriptsToDiagnose(project.value.path, args["scriptPath"]) : null;
|
|
36972
|
+
if (scripts !== null && !scripts.ok) {
|
|
36973
|
+
return this.createErrorResponse(scripts.reason);
|
|
36974
|
+
}
|
|
36975
|
+
if (scripts?.value == null) {
|
|
36976
|
+
const one = await this.diagnosticsOf(args);
|
|
36977
|
+
return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
|
|
36978
|
+
}
|
|
36979
|
+
const progress = callProgress();
|
|
36980
|
+
const answers = [];
|
|
36981
|
+
let errors = 0;
|
|
36982
|
+
let warnings = 0;
|
|
36983
|
+
let unread = 0;
|
|
36984
|
+
for (const [at, scriptPath] of scripts.value.entries()) {
|
|
36985
|
+
progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
|
|
36986
|
+
const one = await this.diagnosticsOf({ ...args, scriptPath });
|
|
36987
|
+
if ("refusal" in one) {
|
|
36988
|
+
if (!one.perScript) {
|
|
36989
|
+
return one.refusal;
|
|
36990
|
+
}
|
|
36991
|
+
unread += 1;
|
|
36992
|
+
answers.push({ scriptPath, error: one.refusal.content[0]?.text ?? "not read" });
|
|
36993
|
+
continue;
|
|
36994
|
+
}
|
|
36995
|
+
errors += Number(one.answer["errors"]);
|
|
36996
|
+
warnings += Number(one.answer["warnings"]);
|
|
36997
|
+
answers.push(one.answer);
|
|
36998
|
+
}
|
|
36999
|
+
return this.jsonTextResponse(this.markedIfStale({
|
|
37000
|
+
clean: errors === 0 && unread === 0,
|
|
37001
|
+
scriptsChecked: answers.length,
|
|
37002
|
+
errors,
|
|
37003
|
+
warnings,
|
|
37004
|
+
...unread > 0 ? { scriptsNotRead: unread } : {},
|
|
37005
|
+
scripts: answers
|
|
37006
|
+
}));
|
|
37007
|
+
}
|
|
37008
|
+
markedIfStale(answer) {
|
|
37009
|
+
return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
|
|
37010
|
+
}
|
|
37011
|
+
async diagnosticsOf(args) {
|
|
36861
37012
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
36862
37013
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
36863
37014
|
if (payload["error"] !== undefined) {
|
|
36864
37015
|
const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
|
|
36865
37016
|
if (payload["refusedArguments"] === true) {
|
|
36866
|
-
return this.createErrorResponse(reason);
|
|
37017
|
+
return { refusal: this.createErrorResponse(reason), perScript: true };
|
|
36867
37018
|
}
|
|
36868
37019
|
if (typeof payload["serves"] === "string") {
|
|
36869
|
-
return
|
|
36870
|
-
|
|
36871
|
-
|
|
36872
|
-
|
|
36873
|
-
|
|
37020
|
+
return {
|
|
37021
|
+
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
37022
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
37023
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
37024
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
37025
|
+
]),
|
|
37026
|
+
perScript: false
|
|
37027
|
+
};
|
|
36874
37028
|
}
|
|
36875
|
-
return
|
|
36876
|
-
|
|
36877
|
-
|
|
36878
|
-
|
|
37029
|
+
return {
|
|
37030
|
+
refusal: this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
37031
|
+
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
37032
|
+
"GDHARNESS_LSP_PORT points this server at another one"
|
|
37033
|
+
]),
|
|
37034
|
+
perScript: payload["aboutThisFile"] === true
|
|
37035
|
+
};
|
|
36879
37036
|
}
|
|
36880
37037
|
const diagnostics = readArray(payload, "diagnostics") ?? [];
|
|
36881
37038
|
const errors = diagnostics.filter((entry) => {
|
|
36882
37039
|
const severity = asParams(entry)["severity"];
|
|
36883
37040
|
return severity === 1 || severity === "error" || severity === "ERROR";
|
|
36884
37041
|
}).length;
|
|
36885
|
-
|
|
36886
|
-
|
|
36887
|
-
|
|
36888
|
-
|
|
36889
|
-
|
|
36890
|
-
|
|
36891
|
-
|
|
36892
|
-
|
|
36893
|
-
|
|
37042
|
+
return {
|
|
37043
|
+
answer: {
|
|
37044
|
+
scriptPath: readString(args, "scriptPath"),
|
|
37045
|
+
clean: errors === 0,
|
|
37046
|
+
errors,
|
|
37047
|
+
warnings: diagnostics.length - errors,
|
|
37048
|
+
diagnostics,
|
|
37049
|
+
...this.whereTheFileDisagrees(diagnostics, args)
|
|
37050
|
+
}
|
|
37051
|
+
};
|
|
36894
37052
|
}
|
|
36895
37053
|
whereTheFileDisagrees(diagnostics, args) {
|
|
36896
37054
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.50",
|
|
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",
|