gdharness 1.0.6 → 1.0.8

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
@@ -10175,16 +10175,16 @@ import {
10175
10175
  closeSync,
10176
10176
  existsSync as existsSync9,
10177
10177
  fstatSync,
10178
- mkdirSync as mkdirSync8,
10178
+ mkdirSync as mkdirSync9,
10179
10179
  mkdtempSync as mkdtempSync2,
10180
10180
  openSync as openSync2,
10181
10181
  readdirSync as readdirSync6,
10182
- readFileSync as readFileSync14,
10182
+ readFileSync as readFileSync15,
10183
10183
  readSync,
10184
10184
  statSync as statSync5
10185
10185
  } from "node:fs";
10186
10186
  import { tmpdir as tmpdir4 } from "node:os";
10187
- import { basename as basename3, dirname as dirname8, join as join14, normalize as normalize2, relative as relative3 } from "node:path";
10187
+ import { basename as basename3, dirname as dirname9, join as join15, normalize as normalize2, relative as relative3 } from "node:path";
10188
10188
  import { setTimeout as delay3 } from "node:timers/promises";
10189
10189
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10190
10190
  import { promisify as promisify5 } from "node:util";
@@ -25814,18 +25814,49 @@ function unseenByEditor(projectPath, editorHolds) {
25814
25814
  return [...declaredClasses(projectPath)].filter(([name]) => !held.has(name)).map(([className, path]) => ({ className, path })).sort((one, other) => one.className < other.className ? -1 : one.className > other.className ? 1 : 0);
25815
25815
  }
25816
25816
 
25817
+ // src/class-note.ts
25818
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
25819
+ import { dirname as dirname5, join as join7 } from "node:path";
25820
+ function classNotePath(projectPath) {
25821
+ return join7(projectPath, ".godot", "gdharness-classes.json");
25822
+ }
25823
+ function readClassNote(projectPath) {
25824
+ try {
25825
+ const fields = asParams(JSON.parse(readFileSync8(classNotePath(projectPath), "utf8")));
25826
+ const rebuiltWith = readArray(fields, "rebuiltWith");
25827
+ if (rebuiltWith === undefined || !rebuiltWith.every((name) => typeof name === "string")) {
25828
+ return null;
25829
+ }
25830
+ const pid = readNumber(fields, "shortListEditorPid");
25831
+ return {
25832
+ rebuiltWith: rebuiltWith.map(String),
25833
+ ...Object.hasOwn(fields, "shortListEditorPid") ? { shortListEditorPid: pid ?? null } : {}
25834
+ };
25835
+ } catch {
25836
+ return null;
25837
+ }
25838
+ }
25839
+ function writeClassNote(projectPath, note) {
25840
+ try {
25841
+ const path = classNotePath(projectPath);
25842
+ mkdirSync5(dirname5(path), { recursive: true });
25843
+ writeFileSync5(path, `${JSON.stringify(note, null, 2)}
25844
+ `, "utf8");
25845
+ } catch {}
25846
+ }
25847
+
25817
25848
  // src/config-pin.ts
25818
- import { existsSync as existsSync4, readFileSync as readFileSync8 } from "node:fs";
25819
- import { join as join7 } from "node:path";
25849
+ import { existsSync as existsSync4, readFileSync as readFileSync9 } from "node:fs";
25850
+ import { join as join8 } from "node:path";
25820
25851
  var PINNED = /\bgdharness@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/g;
25821
25852
  function versionsNamedInConfig(projectPath) {
25822
- const config = join7(projectPath, ".mcp.json");
25853
+ const config = join8(projectPath, ".mcp.json");
25823
25854
  if (!existsSync4(config)) {
25824
25855
  return [];
25825
25856
  }
25826
25857
  let text;
25827
25858
  try {
25828
- text = readFileSync8(config, "utf8");
25859
+ text = readFileSync9(config, "utf8");
25829
25860
  JSON.parse(text);
25830
25861
  } catch {
25831
25862
  return [];
@@ -27192,7 +27223,7 @@ import { promisify as promisify2 } from "node:util";
27192
27223
  // src/detection.ts
27193
27224
  import { existsSync as existsSync5, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
27194
27225
  import { homedir as homedir2 } from "node:os";
27195
- import { join as join8 } from "node:path";
27226
+ import { join as join9 } from "node:path";
27196
27227
  function resolveHomeDirectory() {
27197
27228
  try {
27198
27229
  return homedir2();
@@ -27216,7 +27247,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
27216
27247
  if (!pattern.test(name)) {
27217
27248
  continue;
27218
27249
  }
27219
- const fullPath = join8(directory, name);
27250
+ const fullPath = join9(directory, name);
27220
27251
  try {
27221
27252
  const stat = statSync3(fullPath);
27222
27253
  if (stat.isFile()) {
@@ -27225,7 +27256,7 @@ function scanDirectoryForGodotBinaries(directory, platform) {
27225
27256
  } catch {}
27226
27257
  }
27227
27258
  matches.sort((a, b) => b.mtime - a.mtime);
27228
- return matches.map((m) => join8(directory, m.name));
27259
+ return matches.map((m) => join9(directory, m.name));
27229
27260
  }
27230
27261
  function conventionalPaths(platform, home) {
27231
27262
  const paths = ["godot"];
@@ -27345,9 +27376,9 @@ class GodotLocator {
27345
27376
 
27346
27377
  // src/headless.ts
27347
27378
  import { execFile as execFile3 } from "node:child_process";
27348
- import { mkdtempSync, writeFileSync as writeFileSync5 } from "node:fs";
27379
+ import { mkdtempSync, writeFileSync as writeFileSync6 } from "node:fs";
27349
27380
  import { tmpdir as tmpdir3 } from "node:os";
27350
- import { join as join9 } from "node:path";
27381
+ import { join as join10 } from "node:path";
27351
27382
  import { promisify as promisify3 } from "node:util";
27352
27383
 
27353
27384
  // src/scratch.ts
@@ -27413,12 +27444,12 @@ function reason(stdout, stderr) {
27413
27444
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
27414
27445
  }
27415
27446
  async function runImport(godotPath, projectPath) {
27416
- const logDir = mkdtempSync(join9(tmpdir3(), "gdharness-import-"));
27447
+ const logDir = mkdtempSync(join10(tmpdir3(), "gdharness-import-"));
27417
27448
  try {
27418
27449
  const { stderr } = await run3(godotPath, [
27419
27450
  "--headless",
27420
27451
  "--log-file",
27421
- join9(logDir, "engine.log"),
27452
+ join10(logDir, "engine.log"),
27422
27453
  "--path",
27423
27454
  projectPath,
27424
27455
  "--import"
@@ -27443,13 +27474,13 @@ async function runImport(godotPath, projectPath) {
27443
27474
  }
27444
27475
  }
27445
27476
  async function runOperation(engine, operation, params, projectPath) {
27446
- const paramsDir = mkdtempSync(join9(tmpdir3(), "gdharness-params-"));
27447
- const paramsFile = join9(paramsDir, `${operation}.json`);
27448
- writeFileSync5(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
27477
+ const paramsDir = mkdtempSync(join10(tmpdir3(), "gdharness-params-"));
27478
+ const paramsFile = join10(paramsDir, `${operation}.json`);
27479
+ writeFileSync6(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
27449
27480
  const args = [
27450
27481
  "--headless",
27451
27482
  "--log-file",
27452
- join9(paramsDir, "engine.log"),
27483
+ join10(paramsDir, "engine.log"),
27453
27484
  "--path",
27454
27485
  projectPath,
27455
27486
  "--script",
@@ -27878,7 +27909,7 @@ function orphansPrinted(printed) {
27878
27909
  import { realpathSync as realpathSync2 } from "node:fs";
27879
27910
  import { readFile, realpath } from "node:fs/promises";
27880
27911
  import { createConnection as createConnection3 } from "node:net";
27881
- import { dirname as dirname5, resolve as resolve3 } from "node:path";
27912
+ import { dirname as dirname6, resolve as resolve3 } from "node:path";
27882
27913
  import { fileURLToPath, pathToFileURL } from "node:url";
27883
27914
  var DEFAULT_LSP_PORT = 6005;
27884
27915
  var DIAGNOSTICS_TIMEOUT_MS = 5000;
@@ -28008,7 +28039,7 @@ class GodotLSPClient {
28008
28039
  if (this.initialized) {
28009
28040
  return;
28010
28041
  }
28011
- const rootPath = this.rootPath ?? dirname5(resolve3(filePath));
28042
+ const rootPath = this.rootPath ?? dirname6(resolve3(filePath));
28012
28043
  await this.initialize(rootPath);
28013
28044
  }
28014
28045
  async sendRequest(method, params) {
@@ -28444,14 +28475,14 @@ function secondsFromClock(printed) {
28444
28475
  }
28445
28476
 
28446
28477
  // src/project-scan.ts
28447
- import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
28448
- import { join as join10 } from "node:path";
28478
+ import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
28479
+ import { join as join11 } from "node:path";
28449
28480
  var SKIPPED = new Set(["node_modules"]);
28450
28481
  function skipped(name) {
28451
28482
  return name.startsWith(".") || SKIPPED.has(name);
28452
28483
  }
28453
28484
  function steppedOver(directory) {
28454
- return existsSync7(join10(directory, ".gdignore"));
28485
+ return existsSync7(join11(directory, ".gdignore"));
28455
28486
  }
28456
28487
  var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
28457
28488
  function projectStructure(projectPath) {
@@ -28465,7 +28496,7 @@ function projectStructure(projectPath) {
28465
28496
  continue;
28466
28497
  }
28467
28498
  if (entry.isDirectory()) {
28468
- visit(join10(directory, entry.name));
28499
+ visit(join11(directory, entry.name));
28469
28500
  } else if (entry.isFile()) {
28470
28501
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
28471
28502
  if (extension === "tscn") {
@@ -28496,14 +28527,14 @@ function scriptsWithoutUid(projectPath) {
28496
28527
  }
28497
28528
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
28498
28529
  if (entry.isDirectory()) {
28499
- visit(join10(directory, entry.name), spelled);
28530
+ visit(join11(directory, entry.name), spelled);
28500
28531
  continue;
28501
28532
  }
28502
28533
  if (!entry.isFile()) {
28503
28534
  continue;
28504
28535
  }
28505
28536
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
28506
- if (CARRIES_A_UID.has(extension) && !existsSync7(join10(directory, `${entry.name}.uid`))) {
28537
+ if (CARRIES_A_UID.has(extension) && !existsSync7(join11(directory, `${entry.name}.uid`))) {
28507
28538
  found.push(spelled);
28508
28539
  }
28509
28540
  }
@@ -28538,7 +28569,7 @@ function searchProject(projectPath, options) {
28538
28569
  if (skipped(entry.name)) {
28539
28570
  continue;
28540
28571
  }
28541
- const entryPath = join10(directory, entry.name);
28572
+ const entryPath = join11(directory, entry.name);
28542
28573
  if (entry.isDirectory()) {
28543
28574
  visit(entryPath);
28544
28575
  continue;
@@ -28549,7 +28580,7 @@ function searchProject(projectPath, options) {
28549
28580
  }
28550
28581
  result.summary.files_searched += 1;
28551
28582
  const matches = [];
28552
- for (const [index, line] of readFileSync9(entryPath, "utf8").split(`
28583
+ for (const [index, line] of readFileSync10(entryPath, "utf8").split(`
28553
28584
  `).entries()) {
28554
28585
  if (full()) {
28555
28586
  break;
@@ -28572,7 +28603,7 @@ function searchProject(projectPath, options) {
28572
28603
  }
28573
28604
 
28574
28605
  // src/resources.ts
28575
- import { readFileSync as readFileSync10 } from "node:fs";
28606
+ import { readFileSync as readFileSync11 } from "node:fs";
28576
28607
  import { extname, resolve as resolve4 } from "node:path";
28577
28608
  var STATIC_RESOURCES = [
28578
28609
  {
@@ -28767,7 +28798,7 @@ function readResourceText(uri, getProjectPath) {
28767
28798
  const parsedUri = parseGodotUri(uri);
28768
28799
  if (parsedUri.kind === "project-info") {
28769
28800
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
28770
- const rawProject = readFileSync10(projectFilePath, "utf-8");
28801
+ const rawProject = readFileSync11(projectFilePath, "utf-8");
28771
28802
  const parsedProject = parseProjectGodot(rawProject);
28772
28803
  return {
28773
28804
  mimeType: "application/json",
@@ -28776,7 +28807,7 @@ function readResourceText(uri, getProjectPath) {
28776
28807
  }
28777
28808
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
28778
28809
  ensureAllowedExtension(parsedUri.kind, filePath);
28779
- const text = readFileSync10(filePath, "utf-8");
28810
+ const text = readFileSync11(filePath, "utf-8");
28780
28811
  return {
28781
28812
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
28782
28813
  text
@@ -28813,16 +28844,16 @@ function setupResourceHandlers(mcp, getProjectPath) {
28813
28844
  }
28814
28845
 
28815
28846
  // src/restart-note.ts
28816
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync11, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
28817
- import { dirname as dirname6, join as join11 } from "node:path";
28847
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "node:fs";
28848
+ import { dirname as dirname7, join as join12 } from "node:path";
28818
28849
  function restartNotePath(projectPath) {
28819
- return join11(projectPath, ".godot", "gdharness-restart.json");
28850
+ return join12(projectPath, ".godot", "gdharness-restart.json");
28820
28851
  }
28821
28852
  function noteRestartBegun(note) {
28822
28853
  const path = restartNotePath(note.projectPath);
28823
28854
  try {
28824
- mkdirSync5(dirname6(path), { recursive: true });
28825
- writeFileSync6(path, `${JSON.stringify(note, null, 2)}
28855
+ mkdirSync6(dirname7(path), { recursive: true });
28856
+ writeFileSync7(path, `${JSON.stringify(note, null, 2)}
28826
28857
  `, "utf8");
28827
28858
  } catch (error) {
28828
28859
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -28830,7 +28861,7 @@ function noteRestartBegun(note) {
28830
28861
  }
28831
28862
  function restartOwed(projectPath) {
28832
28863
  try {
28833
- const parsed = JSON.parse(readFileSync11(restartNotePath(projectPath), "utf8"));
28864
+ const parsed = JSON.parse(readFileSync12(restartNotePath(projectPath), "utf8"));
28834
28865
  if (typeof parsed !== "object" || parsed === null) {
28835
28866
  return null;
28836
28867
  }
@@ -28853,36 +28884,36 @@ function restartSettled(projectPath) {
28853
28884
 
28854
28885
  // src/run-record.ts
28855
28886
  import { execFileSync as execFileSync2 } from "node:child_process";
28856
- import { mkdirSync as mkdirSync6, openSync, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "node:fs";
28857
- import { basename as basename2, join as join12 } from "node:path";
28887
+ import { mkdirSync as mkdirSync7, openSync, readdirSync as readdirSync5, readFileSync as readFileSync13, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync8 } from "node:fs";
28888
+ import { basename as basename2, join as join13 } from "node:path";
28858
28889
  function runsDirectory() {
28859
- return join12(runtimeDirectory(), "runs");
28890
+ return join13(runtimeDirectory(), "runs");
28860
28891
  }
28861
28892
  function recordPath() {
28862
- return join12(runsDirectory(), "run.json");
28893
+ return join13(runsDirectory(), "run.json");
28863
28894
  }
28864
28895
  function recordPaths() {
28865
- return runtimeDirectories().map((directory) => join12(directory, "runs", "run.json"));
28896
+ return runtimeDirectories().map((directory) => join13(directory, "runs", "run.json"));
28866
28897
  }
28867
28898
  function openTranscript(startedAt) {
28868
- mkdirSync6(runsDirectory(), { recursive: true });
28869
- const path = join12(runsDirectory(), `run-${startedAt}.log`);
28899
+ mkdirSync7(runsDirectory(), { recursive: true });
28900
+ const path = join13(runsDirectory(), `run-${startedAt}.log`);
28870
28901
  return { path, fd: openSync(path, "a") };
28871
28902
  }
28872
28903
  function writeRunRecord(record) {
28873
- mkdirSync6(runsDirectory(), { recursive: true });
28874
- writeFileSync7(recordPath(), JSON.stringify(record, null, 2), "utf8");
28904
+ mkdirSync7(runsDirectory(), { recursive: true });
28905
+ writeFileSync8(recordPath(), JSON.stringify(record, null, 2), "utf8");
28875
28906
  }
28876
28907
  function editorNotePath() {
28877
- return join12(runsDirectory(), "editor-run.json");
28908
+ return join13(runsDirectory(), "editor-run.json");
28878
28909
  }
28879
28910
  function writeEditorRunNote(note) {
28880
- mkdirSync6(runsDirectory(), { recursive: true });
28881
- writeFileSync7(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
28911
+ mkdirSync7(runsDirectory(), { recursive: true });
28912
+ writeFileSync8(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
28882
28913
  }
28883
28914
  function readEditorRunNote() {
28884
28915
  for (const directory of runtimeDirectories()) {
28885
- const note = editorNoteAt(join12(directory, "runs", "editor-run.json"));
28916
+ const note = editorNoteAt(join13(directory, "runs", "editor-run.json"));
28886
28917
  if (note !== null) {
28887
28918
  return note;
28888
28919
  }
@@ -28892,7 +28923,7 @@ function readEditorRunNote() {
28892
28923
  function editorNoteAt(path) {
28893
28924
  let parsed;
28894
28925
  try {
28895
- parsed = JSON.parse(readFileSync12(path, "utf8"));
28926
+ parsed = JSON.parse(readFileSync13(path, "utf8"));
28896
28927
  } catch {
28897
28928
  return null;
28898
28929
  }
@@ -28915,7 +28946,7 @@ function recordRunEnded(pid, exitCode) {
28915
28946
  return;
28916
28947
  }
28917
28948
  try {
28918
- writeFileSync7(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
28949
+ writeFileSync8(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
28919
28950
  } catch {}
28920
28951
  }
28921
28952
  function readRunRecord() {
@@ -28930,7 +28961,7 @@ function readRunRecord() {
28930
28961
  function recordAt(path) {
28931
28962
  let parsed;
28932
28963
  try {
28933
- parsed = JSON.parse(readFileSync12(path, "utf8"));
28964
+ parsed = JSON.parse(readFileSync13(path, "utf8"));
28934
28965
  } catch {
28935
28966
  return null;
28936
28967
  }
@@ -28975,7 +29006,7 @@ function runningAs(pid) {
28975
29006
  const began = startedAt(pid, null);
28976
29007
  const when = began === null ? {} : { startedAt: began };
28977
29008
  if (process.platform === "linux") {
28978
- const raw = readFileSync12(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
29009
+ const raw = readFileSync13(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
28979
29010
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
28980
29011
  }
28981
29012
  const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
@@ -29128,7 +29159,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
29128
29159
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
29129
29160
  continue;
29130
29161
  }
29131
- const path = join12(directory, entry);
29162
+ const path = join13(directory, entry);
29132
29163
  if (path === keep || ageMs(path, now) < keepMs) {
29133
29164
  continue;
29134
29165
  }
@@ -29139,8 +29170,8 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
29139
29170
  }
29140
29171
 
29141
29172
  // src/setup.ts
29142
- import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
29143
- import { dirname as dirname7, join as join13 } from "node:path";
29173
+ import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
29174
+ import { dirname as dirname8, join as join14 } from "node:path";
29144
29175
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
29145
29176
  var RUNTIME_AUTOLOAD = {
29146
29177
  name: "GdharnessRuntime",
@@ -29148,11 +29179,11 @@ var RUNTIME_AUTOLOAD = {
29148
29179
  };
29149
29180
  var VERSION_MARKER = ".gdharness-version";
29150
29181
  function installedAddonVersion(projectPath) {
29151
- const marker = join13(projectPath, "addons", ADDONS[0], VERSION_MARKER);
29182
+ const marker = join14(projectPath, "addons", ADDONS[0], VERSION_MARKER);
29152
29183
  if (!existsSync8(marker)) {
29153
29184
  return null;
29154
29185
  }
29155
- const said = readFileSync13(marker, "utf8").trim();
29186
+ const said = readFileSync14(marker, "utf8").trim();
29156
29187
  return said === "" ? null : said;
29157
29188
  }
29158
29189
 
@@ -29468,7 +29499,7 @@ var TOOL_SPECS = [
29468
29499
  requires: []
29469
29500
  },
29470
29501
  refresh_classes: {
29471
- summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
29502
+ summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong. A class the cache held after the last rebuild and had lost when this one began is named under lostSinceLastRebuild: the editor wrote the file without it in between, on a save or a scan, and will again until restarted',
29472
29503
  requires: []
29473
29504
  }
29474
29505
  }
@@ -29493,7 +29524,7 @@ var TOOL_SPECS = [
29493
29524
  },
29494
29525
  {
29495
29526
  name: "project_test",
29496
- description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
29527
+ description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
29497
29528
  parameters: {
29498
29529
  projectPath: PROJECT_PATH,
29499
29530
  path: {
@@ -29710,7 +29741,7 @@ var TOOL_SPECS = [
29710
29741
  },
29711
29742
  {
29712
29743
  name: "script_diagnostics",
29713
- description: "Errors and warnings for a script from the editor's language server, and whether the script 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 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. 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.",
29744
+ description: "Errors and warnings for a script from the editor's language server, and whether the script 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 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. The one exception is named when it applies: 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.",
29714
29745
  parameters: {
29715
29746
  projectPath: PROJECT_PATH,
29716
29747
  scriptPath: SCRIPT_PATH
@@ -30309,7 +30340,7 @@ function buildToolDefinitions() {
30309
30340
  var UPDATE_NOTICE_EVERY = 500;
30310
30341
  var FEEDBACK_NOTICE_EVERY = 250;
30311
30342
  var run5 = promisify5(execFile5);
30312
- var __dirname2 = dirname8(fileURLToPath2(import.meta.url));
30343
+ var __dirname2 = dirname9(fileURLToPath2(import.meta.url));
30313
30344
  var EDITOR_RESTART_TIMEOUT_MS = 90000;
30314
30345
  var HOLD_PING_MS = 2000;
30315
30346
  var SLOWEST_FRAME_RATE = 20;
@@ -30596,7 +30627,7 @@ function aboveTheRunner(entry) {
30596
30627
  }
30597
30628
  function elsewhereIn(projectPath, asked) {
30598
30629
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
30599
- if (wanted === "" || existsSync9(join14(projectPath, wanted))) {
30630
+ if (wanted === "" || existsSync9(join15(projectPath, wanted))) {
30600
30631
  return "";
30601
30632
  }
30602
30633
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -30610,7 +30641,7 @@ function elsewhereIn(projectPath, asked) {
30610
30641
  return near.length === 0 ? "" : ` The project does have ${there}.`;
30611
30642
  }
30612
30643
  function hasMainScene(projectFile) {
30613
- const scene = parseProjectGodot(readFileSync14(projectFile, "utf8"))["application"]?.["run/main_scene"];
30644
+ const scene = parseProjectGodot(readFileSync15(projectFile, "utf8"))["application"]?.["run/main_scene"];
30614
30645
  return typeof scene === "string" && scene !== "";
30615
30646
  }
30616
30647
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -30618,13 +30649,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
30618
30649
  return false;
30619
30650
  }
30620
30651
  try {
30621
- return readFileSync14(join14(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
30652
+ return readFileSync15(join15(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
30622
30653
  } catch {
30623
30654
  return false;
30624
30655
  }
30625
30656
  }
30626
30657
  function editorPlaysHeadless(projectFile) {
30627
- const runArgs = parseProjectGodot(readFileSync14(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
30658
+ const runArgs = parseProjectGodot(readFileSync15(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
30628
30659
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
30629
30660
  }
30630
30661
  function camelCased(params) {
@@ -30640,7 +30671,7 @@ function camelCased(params) {
30640
30671
  class GodotServer {
30641
30672
  mcp;
30642
30673
  locator = new GodotLocator;
30643
- operationsScript = join14(__dirname2, "godot", "operations", "godot_operations.gd");
30674
+ operationsScript = join15(__dirname2, "godot", "operations", "godot_operations.gd");
30644
30675
  godotBridge;
30645
30676
  tools = buildToolDefinitions();
30646
30677
  activeProcess = null;
@@ -31071,8 +31102,15 @@ class GodotServer {
31071
31102
  const contained = this.containProjectFiles(asks);
31072
31103
  return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
31073
31104
  }
31074
- const answered = await this.headless(headless, answerable);
31075
- return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
31105
+ if (headless !== "refresh_class_cache") {
31106
+ return await this.headless(headless, answerable);
31107
+ }
31108
+ const project = this.project(answerable);
31109
+ if (!project.ok) {
31110
+ return project.response;
31111
+ }
31112
+ const answered = this.answer(await this.rebuildClassCache(project.value.path));
31113
+ return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
31076
31114
  }
31077
31115
  const { op: _op, ...arguments_ } = args;
31078
31116
  const bridge = async (command, extra = {}) => {
@@ -31298,7 +31336,7 @@ class GodotServer {
31298
31336
  if (path === undefined) {
31299
31337
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
31300
31338
  }
31301
- const file = join14(path, "project.godot");
31339
+ const file = join15(path, "project.godot");
31302
31340
  if (!existsSync9(file)) {
31303
31341
  return {
31304
31342
  ok: false,
@@ -31522,7 +31560,7 @@ class GodotServer {
31522
31560
  if (!engine.ok) {
31523
31561
  return engine.response;
31524
31562
  }
31525
- const application = parseProjectGodot(readFileSync14(project.value.file, "utf8"))["application"] ?? {};
31563
+ const application = parseProjectGodot(readFileSync15(project.value.file, "utf8"))["application"] ?? {};
31526
31564
  const name = application["config/name"];
31527
31565
  const mainScene = application["run/main_scene"];
31528
31566
  const info = {
@@ -31583,18 +31621,18 @@ class GodotServer {
31583
31621
  if (!engine.ok) {
31584
31622
  return engine.response;
31585
31623
  }
31586
- const folder = dirname8(output.absolutePath);
31624
+ const folder = dirname9(output.absolutePath);
31587
31625
  try {
31588
- mkdirSync8(folder, { recursive: true });
31626
+ mkdirSync9(folder, { recursive: true });
31589
31627
  } catch (error) {
31590
- return this.createErrorResponse(`Cannot create ${dirname8(output.relativePath)} to export into: ${errorMessage(error)}`);
31628
+ return this.createErrorResponse(`Cannot create ${dirname9(output.relativePath)} to export into: ${errorMessage(error)}`);
31591
31629
  }
31592
31630
  const debug = readBoolean(args, "debug") ?? false;
31593
- const exportLogs = mkdtempSync2(join14(tmpdir4(), "gdharness-export-"));
31631
+ const exportLogs = mkdtempSync2(join15(tmpdir4(), "gdharness-export-"));
31594
31632
  const exportArgs = [
31595
31633
  "--headless",
31596
31634
  "--log-file",
31597
- join14(exportLogs, "engine.log"),
31635
+ join15(exportLogs, "engine.log"),
31598
31636
  "--path",
31599
31637
  project.value.path,
31600
31638
  debug ? "--export-debug" : "--export-release",
@@ -31652,7 +31690,7 @@ class GodotServer {
31652
31690
  return contained.response;
31653
31691
  }
31654
31692
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
31655
- if (!existsSync9(join14(project.value.path, runner))) {
31693
+ if (!existsSync9(join15(project.value.path, runner))) {
31656
31694
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
31657
31695
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
31658
31696
  ]);
@@ -31661,7 +31699,7 @@ class GodotServer {
31661
31699
  if (!engine.ok) {
31662
31700
  return engine.response;
31663
31701
  }
31664
- const classes = await this.operation("refresh_class_cache", {}, project.value.path);
31702
+ const classes = await this.rebuildClassCache(project.value.path);
31665
31703
  if (!classes.ok) {
31666
31704
  return this.answer(classes);
31667
31705
  }
@@ -31687,7 +31725,7 @@ class GodotServer {
31687
31725
  ];
31688
31726
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
31689
31727
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
31690
- const userData = mkdtempSync2(join14(tmpdir4(), "gdharness-tests-"));
31728
+ const userData = mkdtempSync2(join15(tmpdir4(), "gdharness-tests-"));
31691
31729
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
31692
31730
  const hung = await new Promise((resolve) => {
31693
31731
  const timer = setTimeout(() => {
@@ -31703,14 +31741,14 @@ class GodotServer {
31703
31741
  resolve(false);
31704
31742
  });
31705
31743
  });
31706
- const reportsDir = join14(project.value.path, ".godot", ...ours.split("/"));
31744
+ const reportsDir = join15(project.value.path, ".godot", ...ours.split("/"));
31707
31745
  let report = null;
31708
31746
  let reportProblem = null;
31709
31747
  try {
31710
31748
  const written = existsSync9(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
31711
31749
  const newest = written.at(-1);
31712
31750
  if (newest !== undefined) {
31713
- report = parseJUnit(readFileSync14(join14(reportsDir, newest, "results.xml"), "utf8"));
31751
+ report = parseJUnit(readFileSync15(join15(reportsDir, newest, "results.xml"), "utf8"));
31714
31752
  }
31715
31753
  } catch (error) {
31716
31754
  reportProblem = errorMessage(error);
@@ -31850,7 +31888,7 @@ class GodotServer {
31850
31888
  return null;
31851
31889
  }
31852
31890
  try {
31853
- return readFileSync14(contained.absolutePath, "utf8");
31891
+ return readFileSync15(contained.absolutePath, "utf8");
31854
31892
  } catch {
31855
31893
  return null;
31856
31894
  }
@@ -31878,7 +31916,8 @@ class GodotServer {
31878
31916
  }
31879
31917
  const stale = unloaded.filter((type) => type.inTheClassCache);
31880
31918
  if (stale.length > 0) {
31881
- notes.push(`${stale.map((type) => type.type).join(", ")} ${stale.length === 1 ? "is" : "are"} in the class cache and still unresolved here, which is the editor's loaded list being behind rather than anything on disk. A game launched now reads the cache and resolves ${stale.length === 1 ? "it" : "them"}, so the run and the diagnostics disagree. editor_rescan clears it, measured against a real editor with waiting the same length ruled out; editor_launch restart also does, and costs more.`);
31919
+ const remedy = this.editorWritesAShortList(projectPath) ? "editor_rescan will not clear it here: this editor wrote the class cache from a list shorter than the file on its last scan and writes the same list on every scan, so editor_launch restart is what loads it." : "editor_rescan clears it, measured against a real editor with waiting the same length ruled out; editor_launch restart also does, and costs more.";
31920
+ notes.push(`${stale.map((type) => type.type).join(", ")} ${stale.length === 1 ? "is" : "are"} in the class cache and still unresolved here, which is the editor's loaded list being behind rather than anything on disk. A game launched now reads the cache and resolves ${stale.length === 1 ? "it" : "them"}, so the run and the diagnostics disagree. ${remedy}`);
31882
31921
  }
31883
31922
  return {
31884
31923
  ...contradicted.length === 0 ? {} : { contradictedByTheFile: contradicted },
@@ -31886,13 +31925,24 @@ class GodotServer {
31886
31925
  ...notes.length === 0 ? {} : { staleAnalysis: notes.join(" ") }
31887
31926
  };
31888
31927
  }
31928
+ editorWritesAShortList(projectPath) {
31929
+ const recorded = readClassNote(projectPath)?.shortListEditorPid;
31930
+ return recorded !== undefined && recorded === (this.godotBridge.getStatus().editorPid ?? null);
31931
+ }
31932
+ noteAShortListEditor(projectPath) {
31933
+ const note = readClassNote(projectPath);
31934
+ writeClassNote(projectPath, {
31935
+ rebuiltWith: note?.rebuiltWith ?? [],
31936
+ shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null
31937
+ });
31938
+ }
31889
31939
  sourceOfClass(projectPath, resourcePath) {
31890
31940
  const contained = resolveWithinProject(projectPath, resourcePath);
31891
31941
  if (!contained.ok || !existsSync9(contained.absolutePath)) {
31892
31942
  return null;
31893
31943
  }
31894
31944
  try {
31895
- return readFileSync14(contained.absolutePath, "utf8");
31945
+ return readFileSync15(contained.absolutePath, "utf8");
31896
31946
  } catch {
31897
31947
  return null;
31898
31948
  }
@@ -32121,7 +32171,7 @@ class GodotServer {
32121
32171
  return null;
32122
32172
  }
32123
32173
  try {
32124
- return settingKeys(readFileSync14(join14(projectPath, "project.godot"), "utf8"));
32174
+ return settingKeys(readFileSync15(join15(projectPath, "project.godot"), "utf8"));
32125
32175
  } catch {
32126
32176
  return null;
32127
32177
  }
@@ -32363,7 +32413,7 @@ class GodotServer {
32363
32413
  }
32364
32414
  async runtimeUp(projectPath, before, budget, withArgs) {
32365
32415
  const budgetMs = budget.ms;
32366
- if (!existsSync9(join14(projectPath, RUNTIME_AUTOLOAD.path))) {
32416
+ if (!existsSync9(join15(projectPath, RUNTIME_AUTOLOAD.path))) {
32367
32417
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
32368
32418
  }
32369
32419
  let asking = null;
@@ -33235,6 +33285,9 @@ class GodotServer {
33235
33285
  }
33236
33286
  const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
33237
33287
  const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
33288
+ if (lost.length > 0) {
33289
+ this.noteAShortListEditor(projectPath);
33290
+ }
33238
33291
  const atMissingPaths = after === null ? [] : cachedAtMissingPaths(after, projectPath);
33239
33292
  const restored = lost.length > 0 || atMissingPaths.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
33240
33293
  const rebuilt = atMissingPaths.length > 0 ? cachedClasses(projectPath) : null;
@@ -33290,7 +33343,7 @@ class GodotServer {
33290
33343
  if (projectPath === "") {
33291
33344
  return [];
33292
33345
  }
33293
- const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
33346
+ const rebuilt = await this.rebuildClassCache(projectPath);
33294
33347
  if (!rebuilt.ok) {
33295
33348
  this.logDebug(`Could not rebuild the class cache after a scan: ${rebuilt.message}`);
33296
33349
  return [];
@@ -33298,6 +33351,36 @@ class GodotServer {
33298
33351
  const now = cachedClasses(projectPath);
33299
33352
  return now === null ? [] : lost.filter((name) => now.has(name));
33300
33353
  }
33354
+ async rebuildClassCache(projectPath) {
33355
+ const before = cachedClasses(projectPath);
33356
+ const noted = readClassNote(projectPath);
33357
+ const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
33358
+ if (!rebuilt.ok) {
33359
+ return rebuilt;
33360
+ }
33361
+ const after = cachedClasses(projectPath);
33362
+ if (after === null) {
33363
+ return rebuilt;
33364
+ }
33365
+ const lost = noted === null || before === null ? [] : noted.rebuiltWith.filter((name) => !before.has(name) && after.has(name)).sort();
33366
+ const seenLosing = lost.length > 0 && this.godotBridge.isConnected();
33367
+ writeClassNote(projectPath, {
33368
+ rebuiltWith: [...after.keys()].sort(),
33369
+ ...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
33370
+ });
33371
+ if (!seenLosing) {
33372
+ return rebuilt;
33373
+ }
33374
+ const them = lost.length === 1 ? "it" : "them";
33375
+ return {
33376
+ ...rebuilt,
33377
+ payload: {
33378
+ ...rebuilt.payload,
33379
+ lostSinceLastRebuild: lost,
33380
+ note: `The cache held ${lost.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`
33381
+ }
33382
+ };
33383
+ }
33301
33384
  async alsoSayWhatTheEditorCannotSee(answered, args) {
33302
33385
  const first = answered.content[0];
33303
33386
  if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
@@ -33308,15 +33391,18 @@ class GodotServer {
33308
33391
  if (checked.unseen.length === 0 && stillHeld.length === 0 && checked.unchecked === undefined) {
33309
33392
  return answered;
33310
33393
  }
33311
- const notes = [];
33394
+ const payload = asParams(JSON.parse(first.text));
33395
+ const said = readString(payload, "note");
33396
+ const notes = said === undefined ? [] : [said];
33312
33397
  if (checked.unseen.length > 0) {
33313
- notes.push("The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan does, on its own and with no change to the declaring script; editor_launch restart also does, and costs more.");
33398
+ const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
33399
+ notes.push(projectPath !== "" && this.editorWritesAShortList(projectPath) ? "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan will not reach it either on this editor, whose last scan wrote the cache from a list shorter than the file and would again; editor_launch restart is what loads them." : "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan does, on its own and with no change to the declaring script; editor_launch restart also does, and costs more.");
33314
33400
  }
33315
33401
  if (stillHeld.length > 0) {
33316
33402
  notes.push(`The editor is still holding ${stillHeld.join(", ")}, which this project no longer declares, and it will write that list back over the cache this call just corrected. The entry returns pointing at a script that is gone, and the next engine to walk get_global_class_list() dies on "File not found". A rescan does not clear this one: it picks up a class that has appeared and does not drop one that has gone. editor_launch restart does.`);
33317
33403
  }
33318
33404
  return this.jsonTextResponse({
33319
- ...asParams(JSON.parse(first.text)),
33405
+ ...payload,
33320
33406
  unseenByEditor: checked.unseen.length > 0 ? checked.unseen : undefined,
33321
33407
  stillHeldByEditor: stillHeld.length > 0 ? stillHeld : undefined,
33322
33408
  classesUnchecked: checked.unchecked,
@@ -33441,8 +33527,8 @@ class GodotServer {
33441
33527
  ]);
33442
33528
  }
33443
33529
  const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
33444
- const screenshotDir = expectsScreenshot ? mkdtempSync2(join14(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
33445
- const screenshotPath = screenshotDir ? join14(screenshotDir, "capture.png") : null;
33530
+ const screenshotDir = expectsScreenshot ? mkdtempSync2(join15(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
33531
+ const screenshotPath = screenshotDir ? join15(screenshotDir, "capture.png") : null;
33446
33532
  try {
33447
33533
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
33448
33534
  if (!reply.ok) {
@@ -33466,7 +33552,7 @@ class GodotServer {
33466
33552
  type: "text",
33467
33553
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
33468
33554
  },
33469
- { type: "image", data: readFileSync14(screenshotPath).toString("base64"), mimeType: "image/png" }
33555
+ { type: "image", data: readFileSync15(screenshotPath).toString("base64"), mimeType: "image/png" }
33470
33556
  ]
33471
33557
  };
33472
33558
  } finally {