touchdesigner-mcp-server 1.2.0 → 1.3.1

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.
@@ -28,9 +28,9 @@ export function formatClassList(data, options) {
28
28
  : formatClassListSummary(classes, modules, opts.limit);
29
29
  const ctx = context;
30
30
  return finalizeFormattedText(text, opts, {
31
- template: "classListSummary",
32
31
  context: ctx,
33
32
  structured: ctx,
33
+ template: "classListSummary",
34
34
  });
35
35
  }
36
36
  /**
@@ -49,9 +49,9 @@ export function formatClassDetails(data, options) {
49
49
  : formatClassDetailsSummary(data, opts.limit);
50
50
  const ctx = context;
51
51
  return finalizeFormattedText(text, opts, {
52
- template: "classDetailsSummary",
53
52
  context: ctx,
54
53
  structured: ctx,
54
+ template: "classDetailsSummary",
55
55
  });
56
56
  }
57
57
  function formatClassListMinimal(classes, modules, limit) {
@@ -66,8 +66,8 @@ function formatClassListMinimal(classes, modules, limit) {
66
66
  text += `\nModules (${modules.length}): ${modules.join(", ")}`;
67
67
  }
68
68
  return {
69
- text,
70
69
  context: buildClassListContext(classes, modules),
70
+ text,
71
71
  };
72
72
  }
73
73
  function formatClassListSummary(classes, modules, limit) {
@@ -78,37 +78,37 @@ function formatClassListSummary(classes, modules, limit) {
78
78
  .map((m) => `- ${m}`)
79
79
  .join("\n")}`;
80
80
  return {
81
- text,
82
81
  context: buildClassListContext(classes, modules),
82
+ text,
83
83
  };
84
84
  }
85
85
  function buildClassListContext(classes, modules) {
86
86
  return {
87
87
  classCount: classes.length,
88
- moduleCount: modules.length,
89
88
  classes: classes.map((cls) => ({
90
- name: cls.name,
91
89
  description: cls.description,
90
+ name: cls.name,
92
91
  })),
92
+ moduleCount: modules.length,
93
93
  modules,
94
94
  };
95
95
  }
96
96
  function formatClassDetailsMinimal(data) {
97
97
  const text = `Class: ${data.name}\nType: ${data.type}`;
98
98
  return {
99
- text,
100
99
  context: {
101
- name: data.name,
102
- type: data.type,
103
100
  description: data.description,
101
+ methods: [],
104
102
  methodsShown: 0,
105
103
  methodsTotal: data.methods?.length ?? 0,
106
- methods: [],
104
+ name: data.name,
105
+ properties: [],
107
106
  propertiesShown: 0,
108
107
  propertiesTotal: data.properties?.length ?? 0,
109
- properties: [],
110
108
  truncated: false,
109
+ type: data.type,
111
110
  },
111
+ text,
112
112
  };
113
113
  }
114
114
  function formatClassDetailsSummary(data, limit) {
@@ -148,25 +148,25 @@ function formatClassDetailsSummary(data, limit) {
148
148
  }
149
149
  }
150
150
  return {
151
- text,
152
151
  context: {
153
- name: data.name,
154
- type: data.type,
155
152
  description: data.description,
156
- methodsShown: limitedMethods.length,
157
- methodsTotal: methods.length,
158
153
  methods: limitedMethods.map((method) => ({
159
154
  signature: method.signature || `${method.name}()`,
160
155
  summary: method.description?.split("\n")[0] ?? "",
161
156
  })),
162
- propertiesShown: limitedProps.length,
163
- propertiesTotal: properties.length,
157
+ methodsShown: limitedMethods.length,
158
+ methodsTotal: methods.length,
159
+ name: data.name,
164
160
  properties: limitedProps.map((prop) => ({
165
161
  name: prop.name,
166
162
  type: prop.type,
167
163
  })),
164
+ propertiesShown: limitedProps.length,
165
+ propertiesTotal: properties.length,
168
166
  truncated: methodsTruncated || propsTruncated,
167
+ type: data.type,
169
168
  },
169
+ text,
170
170
  };
171
171
  }
172
172
  function formatDetailed(data, format) {
@@ -175,13 +175,13 @@ function formatDetailed(data, format) {
175
175
  : "TouchDesigner Classes";
176
176
  const payloadFormat = format ?? DEFAULT_PRESENTER_FORMAT;
177
177
  return presentStructuredData({
178
- text: title,
179
- detailLevel: "detailed",
180
- structured: data,
181
178
  context: {
182
- title,
183
179
  payloadFormat,
180
+ title,
184
181
  },
182
+ detailLevel: "detailed",
183
+ structured: data,
185
184
  template: "detailedPayload",
185
+ text: title,
186
186
  }, payloadFormat);
187
187
  }
@@ -4,7 +4,9 @@
4
4
  * Central export point for all response formatters
5
5
  */
6
6
  export { formatClassDetails, formatClassList } from "./classListFormatter.js";
7
+ export { formatModuleHelp } from "./moduleHelpFormatter.js";
7
8
  export { formatNodeDetails } from "./nodeDetailsFormatter.js";
9
+ export { formatNodeErrors } from "./nodeErrorsFormatter.js";
8
10
  export { formatNodeList } from "./nodeListFormatter.js";
9
11
  export { formatCreateNodeResult, formatDeleteNodeResult, formatExecNodeMethodResult, formatTdInfo, formatUpdateNodeResult, } from "./operationFormatter.js";
10
12
  export { formatScriptResult } from "./scriptResultFormatter.js";
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Module Help Formatter
3
+ *
4
+ * Formats TouchDesigner module help information with token optimization.
5
+ * Used by GET_MODULE_HELP tool.
6
+ */
7
+ import { DEFAULT_PRESENTER_FORMAT, presentStructuredData, } from "./presenter.js";
8
+ import { finalizeFormattedText, mergeFormatterOptions, } from "./responseFormatter.js";
9
+ /**
10
+ * Format module help result
11
+ */
12
+ export function formatModuleHelp(data, options) {
13
+ const opts = mergeFormatterOptions(options);
14
+ if (!data?.helpText) {
15
+ return "No help information available.";
16
+ }
17
+ const moduleName = data.moduleName ?? "Unknown";
18
+ const helpText = data.helpText;
19
+ const members = extractModuleMembers(helpText);
20
+ const classInfo = extractClassSummary(helpText);
21
+ if (opts.detailLevel === "detailed") {
22
+ return formatDetailed(moduleName, helpText, opts.responseFormat);
23
+ }
24
+ let formattedText = "";
25
+ let context;
26
+ switch (opts.detailLevel) {
27
+ case "minimal":
28
+ case "summary": {
29
+ const summary = formatSummary(moduleName, helpText, members, classInfo);
30
+ formattedText = summary.text;
31
+ context = summary.context;
32
+ break;
33
+ }
34
+ default:
35
+ formattedText = helpText;
36
+ context = buildHelpContext(moduleName, helpText, members, classInfo);
37
+ }
38
+ const ctx = context;
39
+ return finalizeFormattedText(formattedText, opts, {
40
+ context: ctx,
41
+ structured: ctx,
42
+ template: "moduleHelp",
43
+ });
44
+ }
45
+ /**
46
+ * Summary mode: Module name with key sections
47
+ */
48
+ function formatSummary(moduleName, helpText, members, classInfo) {
49
+ const sections = extractHelpSections(helpText);
50
+ const preview = extractHelpPreview(helpText, 500);
51
+ const memberSummary = formatMemberSummary(members);
52
+ const lines = [`āœ“ Help information for ${moduleName}`];
53
+ if (classInfo?.definition) {
54
+ lines.push(`Class: ${classInfo.definition}`);
55
+ }
56
+ if (classInfo?.description) {
57
+ lines.push(classInfo.description);
58
+ }
59
+ if (classInfo?.methodResolutionOrder?.length) {
60
+ lines.push(`MRO: ${classInfo.methodResolutionOrder.join(" → ")}`);
61
+ }
62
+ lines.push("");
63
+ if (sections.length > 0) {
64
+ lines.push(`Sections: ${sections.join(", ")}`, "");
65
+ }
66
+ lines.push(preview);
67
+ if (memberSummary) {
68
+ lines.push("", memberSummary);
69
+ }
70
+ if (helpText.length > 500) {
71
+ lines.push("", `šŸ’” Use detailLevel='detailed' to see full documentation (${helpText.length} chars total).`);
72
+ }
73
+ return {
74
+ context: {
75
+ classInfo,
76
+ fullLength: helpText.length,
77
+ helpPreview: preview,
78
+ members,
79
+ moduleName,
80
+ sections,
81
+ },
82
+ text: lines.join("\n"),
83
+ };
84
+ }
85
+ /**
86
+ * Detailed mode: Full help text
87
+ */
88
+ function formatDetailed(moduleName, helpText, format) {
89
+ const title = `Help for ${moduleName}`;
90
+ const payloadFormat = format ?? DEFAULT_PRESENTER_FORMAT;
91
+ // For detailed view, return formatted markdown
92
+ let formatted = `# ${title}\n\n`;
93
+ formatted += "```\n";
94
+ formatted += helpText;
95
+ formatted += "\n```";
96
+ return presentStructuredData({
97
+ context: {
98
+ payloadFormat,
99
+ title,
100
+ },
101
+ detailLevel: "detailed",
102
+ structured: {
103
+ helpText,
104
+ length: helpText.length,
105
+ moduleName,
106
+ },
107
+ template: "moduleHelpDetailed",
108
+ text: formatted,
109
+ }, payloadFormat);
110
+ }
111
+ /**
112
+ * Build help context
113
+ */
114
+ function buildHelpContext(moduleName, helpText, members, classInfo) {
115
+ return {
116
+ classInfo,
117
+ fullLength: helpText.length,
118
+ helpPreview: extractHelpPreview(helpText, 200),
119
+ members,
120
+ moduleName,
121
+ sections: extractHelpSections(helpText),
122
+ };
123
+ }
124
+ /**
125
+ * Extract preview from help text
126
+ */
127
+ function extractHelpPreview(helpText, maxChars) {
128
+ const trimmed = helpText.trim();
129
+ if (trimmed.length <= maxChars) {
130
+ return trimmed;
131
+ }
132
+ // Try to cut at a natural break point (newline)
133
+ const firstPart = trimmed.substring(0, maxChars);
134
+ const lastNewline = firstPart.lastIndexOf("\n");
135
+ if (lastNewline > maxChars * 0.7) {
136
+ return `${firstPart.substring(0, lastNewline)}...`;
137
+ }
138
+ return `${firstPart}...`;
139
+ }
140
+ /**
141
+ * Extract section headers from help text
142
+ */
143
+ function extractHelpSections(helpText) {
144
+ const sections = [];
145
+ const lines = helpText.split("\n");
146
+ // Common help section patterns
147
+ const sectionPatterns = [
148
+ /^([A-Z][A-Za-z\s]+):$/,
149
+ /^\s*([A-Z][A-Z\s]+)$/,
150
+ /^-+\s*$/,
151
+ ];
152
+ let lastSection = "";
153
+ for (const line of lines) {
154
+ const trimmed = line.trim();
155
+ // Check for section headers
156
+ for (const pattern of sectionPatterns) {
157
+ const match = trimmed.match(pattern);
158
+ if (match?.[1]) {
159
+ const section = match[1].trim();
160
+ if (section && section !== lastSection && section.length < 50) {
161
+ sections.push(section);
162
+ lastSection = section;
163
+ }
164
+ break;
165
+ }
166
+ }
167
+ // Limit to first 10 sections
168
+ if (sections.length >= 10) {
169
+ break;
170
+ }
171
+ }
172
+ return sections;
173
+ }
174
+ function extractModuleMembers(helpText) {
175
+ const methods = [];
176
+ const properties = [];
177
+ const seenMethods = new Set();
178
+ const seenProperties = new Set();
179
+ const lines = helpText.split("\n");
180
+ let currentCategory;
181
+ for (const line of lines) {
182
+ const trimmed = line.trim();
183
+ if (!trimmed)
184
+ continue;
185
+ const normalized = trimmed.replace(/^\|/, "").trim();
186
+ const headerMatch = normalized.match(/^(.*?):$/);
187
+ if (headerMatch) {
188
+ const newCategory = categorizeSection(headerMatch[1]);
189
+ if (newCategory) {
190
+ currentCategory = newCategory;
191
+ }
192
+ continue;
193
+ }
194
+ if (!currentCategory)
195
+ continue;
196
+ const entryMatch = trimmed.match(/^\|\s{2,4}([A-Za-z_][\w]*)/);
197
+ if (!entryMatch)
198
+ continue;
199
+ const name = entryMatch[1];
200
+ if (!name)
201
+ continue;
202
+ if (currentCategory === "method") {
203
+ if (!seenMethods.has(name)) {
204
+ seenMethods.add(name);
205
+ methods.push(name);
206
+ }
207
+ }
208
+ else if (currentCategory === "property") {
209
+ if (!seenProperties.has(name)) {
210
+ seenProperties.add(name);
211
+ properties.push(name);
212
+ }
213
+ }
214
+ }
215
+ return { methods, properties };
216
+ }
217
+ function extractClassSummary(helpText) {
218
+ const lines = helpText.split("\n");
219
+ let definition;
220
+ const descriptionLines = [];
221
+ const methodResolutionOrder = [];
222
+ let inDescription = false;
223
+ let inMro = false;
224
+ for (let i = 0; i < lines.length; i++) {
225
+ const raw = lines[i];
226
+ const trimmed = raw.trim();
227
+ if (!definition) {
228
+ const defMatch = trimmed.match(/^class\s+(.+)$/);
229
+ if (defMatch) {
230
+ definition = defMatch[1];
231
+ inDescription = true;
232
+ continue;
233
+ }
234
+ }
235
+ if (inDescription) {
236
+ if (!trimmed || trimmed.startsWith("| Methods defined here:")) {
237
+ inDescription = false;
238
+ continue; // Skip further processing of this line
239
+ }
240
+ if (trimmed.startsWith("|")) {
241
+ const desc = trimmed.replace(/^\|\s*/, "");
242
+ if (desc) {
243
+ descriptionLines.push(desc);
244
+ }
245
+ }
246
+ }
247
+ if (trimmed.startsWith("| Method resolution order:")) {
248
+ inMro = true;
249
+ continue;
250
+ }
251
+ if (inMro) {
252
+ if (!trimmed.startsWith("|")) {
253
+ // MRO section ended - fall through to the exit check below
254
+ inMro = false;
255
+ }
256
+ else {
257
+ const entry = trimmed.replace(/^\|\s*/, "");
258
+ if (entry) {
259
+ methodResolutionOrder.push(entry.trim());
260
+ }
261
+ }
262
+ }
263
+ // Exit early once we have collected the MRO and we're done with both sections
264
+ if (methodResolutionOrder.length > 0 && !inMro && !inDescription) {
265
+ break;
266
+ }
267
+ }
268
+ if (!definition &&
269
+ descriptionLines.length === 0 &&
270
+ methodResolutionOrder.length === 0) {
271
+ return undefined;
272
+ }
273
+ return {
274
+ definition,
275
+ description: descriptionLines.slice(0, 3).join(" "),
276
+ methodResolutionOrder: methodResolutionOrder.length
277
+ ? methodResolutionOrder
278
+ : undefined,
279
+ };
280
+ }
281
+ function categorizeSection(sectionName) {
282
+ const normalized = sectionName.toLowerCase();
283
+ if (normalized.includes("method")) {
284
+ return "method";
285
+ }
286
+ if (normalized.includes("descriptor") ||
287
+ normalized.includes("attribute") ||
288
+ normalized.includes("property")) {
289
+ return "property";
290
+ }
291
+ return undefined;
292
+ }
293
+ function formatMemberSummary(members, limitPerGroup) {
294
+ const segments = [];
295
+ const methodSummary = formatMemberGroup("Methods", members.methods, limitPerGroup);
296
+ if (methodSummary) {
297
+ segments.push(methodSummary);
298
+ }
299
+ const propertySummary = formatMemberGroup("Properties", members.properties, limitPerGroup);
300
+ if (propertySummary) {
301
+ segments.push(propertySummary);
302
+ }
303
+ return segments.join("\n\n");
304
+ }
305
+ function formatMemberGroup(label, items, limit) {
306
+ if (items.length === 0) {
307
+ return undefined;
308
+ }
309
+ const effectiveLimit = typeof limit === "number" && Number.isFinite(limit)
310
+ ? Math.max(limit, 0)
311
+ : items.length;
312
+ const displayed = items.slice(0, effectiveLimit);
313
+ const suffix = items.length > effectiveLimit ? ", …" : "";
314
+ return `${label} (${items.length}): ${displayed.join(", ")}${suffix}`;
315
+ }
@@ -32,9 +32,9 @@ export function formatNodeDetails(data, options) {
32
32
  }
33
33
  const context = result.context;
34
34
  return finalizeFormattedText(result.text, opts, {
35
- template: "nodeDetailsSummary",
36
35
  context,
37
36
  structured: context,
37
+ template: "nodeDetailsSummary",
38
38
  });
39
39
  }
40
40
  /**
@@ -47,18 +47,18 @@ function formatMinimal(nodePath, propertyKeys, limit) {
47
47
  text += `\nšŸ’” ${propertyKeys.length - items.length} more properties omitted.`;
48
48
  }
49
49
  return {
50
- text,
51
50
  context: {
52
- nodePath,
53
- type: "",
51
+ displayed: items.length,
54
52
  id: 0,
55
53
  name: "",
56
- total: propertyKeys.length,
57
- displayed: items.length,
54
+ nodePath,
55
+ omittedCount: Math.max(propertyKeys.length - items.length, 0),
58
56
  properties: items.map((name) => ({ name, value: "" })),
57
+ total: propertyKeys.length,
59
58
  truncated,
60
- omittedCount: Math.max(propertyKeys.length - items.length, 0),
59
+ type: "",
61
60
  },
61
+ text,
62
62
  };
63
63
  }
64
64
  /**
@@ -82,18 +82,18 @@ function formatSummary(nodePath, data, limit) {
82
82
  text += `\nšŸ’” ${propertyEntries.length - items.length} more properties omitted.`;
83
83
  }
84
84
  return {
85
- text,
86
85
  context: {
87
- nodePath,
88
- type: data.opType,
86
+ displayed: items.length,
89
87
  id: data.id,
90
88
  name: data.name,
91
- total: propertyEntries.length,
92
- displayed: items.length,
89
+ nodePath,
90
+ omittedCount: Math.max(propertyEntries.length - items.length, 0),
93
91
  properties: propsForContext,
92
+ total: propertyEntries.length,
94
93
  truncated,
95
- omittedCount: Math.max(propertyEntries.length - items.length, 0),
94
+ type: data.opType,
96
95
  },
96
+ text,
97
97
  };
98
98
  }
99
99
  /**
@@ -103,14 +103,14 @@ function formatDetailed(data, format) {
103
103
  const title = `Node ${data.path ?? data.name ?? "details"}`;
104
104
  const payloadFormat = format ?? DEFAULT_PRESENTER_FORMAT;
105
105
  return presentStructuredData({
106
- text: title,
107
- detailLevel: "detailed",
108
- structured: data,
109
106
  context: {
110
- title,
111
107
  payloadFormat,
108
+ title,
112
109
  },
110
+ detailLevel: "detailed",
111
+ structured: data,
113
112
  template: "detailedPayload",
113
+ text: title,
114
114
  }, payloadFormat);
115
115
  }
116
116
  /**
@@ -0,0 +1,68 @@
1
+ import { DEFAULT_PRESENTER_FORMAT, presentStructuredData, } from "./presenter.js";
2
+ import { finalizeFormattedText, limitArray, mergeFormatterOptions, } from "./responseFormatter.js";
3
+ export function formatNodeErrors(data, options) {
4
+ const opts = mergeFormatterOptions(options);
5
+ if (!data) {
6
+ return "No node error information is available.";
7
+ }
8
+ if (opts.detailLevel === "detailed") {
9
+ return formatDetailed(data, opts.responseFormat);
10
+ }
11
+ const errors = data.errors ?? [];
12
+ if (errors.length === 0 || !data.hasErrors || data.errorCount === 0) {
13
+ const noErrorText = `Node ${data.nodePath} has no reported errors.`;
14
+ return finalizeFormattedText(noErrorText, opts, {
15
+ context: {
16
+ errorCount: 0,
17
+ nodeName: data.nodeName,
18
+ nodePath: data.nodePath,
19
+ },
20
+ structured: data,
21
+ template: "nodeErrorSummary",
22
+ });
23
+ }
24
+ const { items, truncated } = limitArray(errors, opts.limit);
25
+ const header = `Node: ${data.nodePath}\nOperator: ${data.opType} (${data.nodeName})\n${data.errorCount} error(s) found\n`;
26
+ const body = opts.detailLevel === "minimal"
27
+ ? formatMinimal(items)
28
+ : formatSummary(items);
29
+ let text = `${header}\n${body}`;
30
+ if (truncated) {
31
+ text += `\nšŸ’” ${data.errorCount - items.length} more errors omitted.`;
32
+ }
33
+ return finalizeFormattedText(text, opts, {
34
+ context: {
35
+ displayed: items.length,
36
+ errorCount: data.errorCount,
37
+ nodeName: data.nodeName,
38
+ nodePath: data.nodePath,
39
+ opType: data.opType,
40
+ },
41
+ structured: data,
42
+ template: "nodeErrorSummary",
43
+ });
44
+ }
45
+ function formatMinimal(errors) {
46
+ return errors
47
+ .map((entry) => `- ${entry.nodePath}: ${entry.message}`)
48
+ .join("\n");
49
+ }
50
+ function formatSummary(errors) {
51
+ return errors
52
+ .map((entry) => `- ${entry.nodePath} (${entry.opType}): ${entry.message}`)
53
+ .join("\n");
54
+ }
55
+ function formatDetailed(data, format) {
56
+ const title = `Node error report for ${data.nodePath}`;
57
+ const payloadFormat = format ?? DEFAULT_PRESENTER_FORMAT;
58
+ return presentStructuredData({
59
+ context: {
60
+ payloadFormat,
61
+ title,
62
+ },
63
+ detailLevel: "detailed",
64
+ structured: data,
65
+ template: "detailedPayload",
66
+ text: title,
67
+ }, payloadFormat);
68
+ }
@@ -40,9 +40,9 @@ export function formatNodeList(data, options) {
40
40
  context.omittedCount = 0;
41
41
  }
42
42
  return finalizeFormattedText(output, opts, {
43
- template: "nodeListSummary",
44
43
  context,
45
44
  structured: context,
45
+ template: "nodeListSummary",
46
46
  });
47
47
  }
48
48
  /**
@@ -53,8 +53,8 @@ function formatMinimal(nodes, parentPath, totalCount, truncated) {
53
53
  const text = `Found ${nodes.length} nodes in ${parentPath}:
54
54
  ${paths.join("\n")}`;
55
55
  return {
56
- text,
57
56
  context: buildNodeListContext(nodes, parentPath, totalCount, truncated),
57
+ text,
58
58
  };
59
59
  }
60
60
  /**
@@ -71,14 +71,14 @@ function formatSummary(nodes, parentPath, totalCount, truncated) {
71
71
  ${nodeLines.join("\n")}`;
72
72
  });
73
73
  return {
74
- text: header + sections.join("\n\n"),
75
74
  context: {
75
+ groups,
76
+ omittedCount: Math.max(totalCount - nodes.length, 0),
76
77
  parentPath,
77
78
  totalCount,
78
- groups,
79
79
  truncated,
80
- omittedCount: Math.max(totalCount - nodes.length, 0),
81
80
  },
81
+ text: header + sections.join("\n\n"),
82
82
  };
83
83
  }
84
84
  /**
@@ -88,23 +88,23 @@ function formatDetailed(nodes, data, format, parentPath) {
88
88
  const title = `Nodes in ${parentPath}`;
89
89
  const payloadFormat = format ?? DEFAULT_PRESENTER_FORMAT;
90
90
  return presentStructuredData({
91
- text: title,
92
- detailLevel: "detailed",
93
- structured: { ...data, nodes },
94
91
  context: {
95
- title,
96
92
  payloadFormat,
93
+ title,
97
94
  },
95
+ detailLevel: "detailed",
96
+ structured: { ...data, nodes },
98
97
  template: "detailedPayload",
98
+ text: title,
99
99
  }, payloadFormat);
100
100
  }
101
101
  function buildNodeListContext(nodes, parentPath, totalCount, truncated) {
102
102
  return {
103
+ groups: buildGroups(nodes),
104
+ omittedCount: Math.max(totalCount - nodes.length, 0),
103
105
  parentPath,
104
106
  totalCount,
105
- groups: buildGroups(nodes),
106
107
  truncated,
107
- omittedCount: Math.max(totalCount - nodes.length, 0),
108
108
  };
109
109
  }
110
110
  function buildGroups(nodes) {
@@ -117,8 +117,8 @@ function buildGroups(nodes) {
117
117
  byType.get(type)?.push(node);
118
118
  }
119
119
  return Array.from(byType.entries()).map(([type, typeNodes]) => ({
120
- type,
121
120
  count: typeNodes.length,
122
121
  nodes: typeNodes.map((n) => ({ name: n.name, path: n.path })),
122
+ type,
123
123
  }));
124
124
  }